feat(image-import): add resolveImports (pure) and commitImport persisting duties via DataStorage
This commit is contained in:
parent
801757b92c
commit
3b6283ced1
2 changed files with 163 additions and 0 deletions
106
image-import.js
106
image-import.js
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ============================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue