feat(image-import): add resolveImports (pure) and commitImport persisting duties via DataStorage

This commit is contained in:
Kenearos 2026-05-12 18:23:27 +02:00
parent 801757b92c
commit 3b6283ced1
2 changed files with 163 additions and 0 deletions

View file

@ -714,6 +714,112 @@ class ImageImporter {
this.showStage(1);
}
/**
* Pure: turn session state into a commit plan.
* @param {object} session
* @returns {{ newEmployees: string[], commits: Array<{employeeName:string,year:number,month:number,date:Date,dateStr:string,share:number}>, skippedOutsideMonth: number }}
*/
resolveImports(session) {
const choiceByCandidate = new Map();
for (const u of session.unknowns) {
choiceByCandidate.set(u.candidate, u.choice);
}
const newEmployees = new Set();
const commits = [];
let skippedOutsideMonth = 0;
for (const e of session.entries) {
let resolved;
if (session.resolvedNames.has(e.name)) {
resolved = session.resolvedNames.get(e.name);
} else {
const choice = choiceByCandidate.get(e.name) || 'new';
if (choice === 'ignore') continue;
if (choice === 'new') {
resolved = e.name;
newEmployees.add(e.name);
} else if (choice.startsWith('assign:')) {
resolved = choice.slice('assign:'.length);
} else {
resolved = e.name;
}
}
const y = e.date.getFullYear();
const m = e.date.getMonth() + 1;
if (y !== session.targetYear || m !== session.targetMonth) {
skippedOutsideMonth++;
continue;
}
commits.push({
employeeName: resolved,
year: session.targetYear,
month: session.targetMonth,
date: e.date,
dateStr: e.dateStr,
share: e.share
});
}
return { newEmployees: Array.from(newEmployees), commits, skippedOutsideMonth };
}
/**
* Stage 3 to Stage 4. Resolve plan, persist via DataStorage, refresh UI.
*/
async commitImport() {
if (!this.session) return;
const plan = this.resolveImports(this.session);
for (const name of plan.newEmployees) {
this.storage.addEmployee(name);
}
let okCount = 0;
let errCount = 0;
const affectedEmployees = new Set();
for (const c of plan.commits) {
try {
this.storage.addDuty(c.employeeName, c.year, c.month, c.date, c.share);
affectedEmployees.add(c.employeeName);
okCount++;
} catch (e) {
console.error('commitImport: addDuty failed', e);
errCount++;
break; // per spec 13.4
}
}
if (this.app) {
if (plan.newEmployees.length > 0) {
this.app.loadEmployeeSelects();
this.app.loadEmployeeList();
}
this.app.loadDutiesForSelectedEmployee();
if (errCount > 0) {
this.app.showToast(`Speicherfehler - Import unvollstaendig (${okCount} von ${plan.commits.length} erfolgreich)`, 'error');
} else {
const msg = `${okCount} Dienste fuer ${affectedEmployees.size} Mitarbeiter importiert`;
this.app.showToast(msg, 'success');
if (plan.skippedOutsideMonth > 0) {
setTimeout(() => {
this.app.showToast(`${plan.skippedOutsideMonth} Eintraege ausserhalb des gewaehlten Monats uebersprungen`, 'info');
}, 1600);
}
}
}
const doneSummary = document.getElementById('image-import-done-summary');
if (doneSummary) {
doneSummary.textContent = `${okCount} Dienste fuer ${affectedEmployees.size} Mitarbeiter importiert.`;
}
this.showStage(4);
setTimeout(() => this.close(), 1500);
}
/**
* Validate file (type + size), set into session, render thumbnail, enable Erkennen.
* @param {File} file

View file

@ -848,6 +848,63 @@ runner.test('Classify: Werktag = weekday', (t) => {
t.assertEqual(importer.classify(mon), 'weekday', 'Werktag');
});
// ============================================================================
// ImageImporter Tests - resolveImports (pure) (Feature A)
// ============================================================================
runner.test('Resolve: gemischte unknowns (new + assign + ignore)', (t) => {
const importer = new ImageImporter(null);
const session = {
entries: [
{ name: 'Max Mustermann', date: new Date('2025-11-22T12:00:00'), dateStr: '2025-11-22', share: 1.0 },
{ name: 'Max Mustermannn', date: new Date('2025-11-23T12:00:00'), dateStr: '2025-11-23', share: 0.5 },
{ name: 'Egon Olsen', date: new Date('2025-11-28T12:00:00'), dateStr: '2025-11-28', share: 1.0 },
{ name: 'Hugo Ignored', date: new Date('2025-11-29T12:00:00'), dateStr: '2025-11-29', share: 1.0 }
],
unknowns: [
{ candidate: 'Max Mustermannn', suggested: 'Max Mustermann', choice: 'assign:Max Mustermann' },
{ candidate: 'Egon Olsen', suggested: null, choice: 'new' },
{ candidate: 'Hugo Ignored', suggested: null, choice: 'ignore' }
],
resolvedNames: new Map([['Max Mustermann', 'Max Mustermann']]),
targetYear: 2025,
targetMonth: 11
};
const plan = importer.resolveImports(session);
t.assertEqual(plan.newEmployees.length, 1, '1 neuer MA');
t.assertEqual(plan.newEmployees[0], 'Egon Olsen', 'Egon ist neu');
t.assertEqual(plan.commits.length, 3, '3 Commits (Hugo ignoriert)');
t.assertEqual(plan.skippedOutsideMonth, 0, 'Keine ausserhalb Monat');
const max22 = plan.commits.find(c => c.employeeName === 'Max Mustermann' && c.dateStr === '2025-11-22');
t.assertTrue(!!max22, 'Max am 22.11 vorhanden');
t.assertEqual(max22.share, 1.0, 'Share 1.0');
const maxFromFuzzy = plan.commits.find(c => c.employeeName === 'Max Mustermann' && c.dateStr === '2025-11-23');
t.assertTrue(!!maxFromFuzzy, 'Fuzzy-Match wurde aufgeloest');
t.assertEqual(maxFromFuzzy.share, 0.5, 'Share 0.5');
const egon = plan.commits.find(c => c.employeeName === 'Egon Olsen');
t.assertTrue(!!egon, 'Egon committed');
});
runner.test('Resolve: ausserhalb Monat wird uebersprungen', (t) => {
const importer = new ImageImporter(null);
const session = {
entries: [
{ name: 'A', date: new Date('2025-11-22T12:00:00'), dateStr: '2025-11-22', share: 1.0 },
{ name: 'A', date: new Date('2025-12-01T12:00:00'), dateStr: '2025-12-01', share: 1.0 }
],
unknowns: [{ candidate: 'A', suggested: null, choice: 'new' }],
resolvedNames: new Map(),
targetYear: 2025,
targetMonth: 11
};
const plan = importer.resolveImports(session);
t.assertEqual(plan.commits.length, 1, 'Nur November-Eintrag bleibt');
t.assertEqual(plan.skippedOutsideMonth, 1, '1 uebersprungen');
});
// ============================================================================
// Display Functions
// ============================================================================