diff --git a/app.js b/app.js index 5770bac..04e1b3d 100644 --- a/app.js +++ b/app.js @@ -13,9 +13,6 @@ class DienstplanApp { this.holidayProvider = new HolidayProvider(); this.calculator = new BonusCalculator(this.holidayProvider); - this.currentMonth = new Date().getMonth() + 1; - this.currentYear = new Date().getFullYear(); - this.init(); } @@ -1237,4 +1234,11 @@ document.addEventListener('DOMContentLoaded', async () => { } app = new DienstplanApp(); window.app = app; + + // Bild-Import hier erzeugen (nicht in einem eigenen DOMContentLoaded-Listener): + // window.app wird erst nach dem await oben gesetzt, ein paralleler Listener + // liefe da noch ohne app und wuerde den Importer nie anlegen. + if (window.ImageImporter && !window.imageImporter) { + window.imageImporter = new window.ImageImporter(app); + } }); diff --git a/image-import.js b/image-import.js index a76025b..96f3b92 100644 --- a/image-import.js +++ b/image-import.js @@ -211,7 +211,14 @@ class ImageImporter { * @returns {string} */ normalizeName(name) { - return String(name).toLowerCase().trim().replace(/\s+/g, ' '); + return String(name) + .toLowerCase() + .trim() + .replace(/\s+/g, ' ') + // Anreden/Titel am Anfang entfernen (auch gestapelt: "Herr Dr."), + // damit "Herr Alsholi" auf einen ohne Anrede gespeicherten "Alsholi" matcht. + .replace(/^((herr|frau|hr|fr|dr|prof)\.?\s+)+/i, '') + .trim(); } /** @@ -288,6 +295,13 @@ class ImageImporter { key = input.trim(); } + // Zielmonat/-jahr aus dem Live-Dropdown lesen (einzige Quelle der Wahrheit). + // Ein gecachtes Feld driftet, sobald das Dropdown ohne Sync geaendert wird. + const monthEl = document.getElementById('month-select'); + const yearEl = document.getElementById('year-select'); + const selMonth = monthEl ? parseInt(monthEl.value, 10) : NaN; + const selYear = yearEl ? parseInt(yearEl.value, 10) : NaN; + this.session = { file: null, thumbnailUrl: null, @@ -296,8 +310,9 @@ class ImageImporter { entries: [], unknowns: [], resolvedNames: new Map(), - targetYear: this.app ? this.app.currentYear : new Date().getFullYear(), - targetMonth: this.app ? this.app.currentMonth : (new Date().getMonth() + 1), + targetYear: Number.isInteger(selYear) ? selYear : new Date().getFullYear(), + targetMonth: (Number.isInteger(selMonth) && selMonth >= 1 && selMonth <= 12) + ? selMonth : (new Date().getMonth() + 1), detectedMonth: null, detectedYear: null, notes: [] @@ -853,6 +868,7 @@ Regeln: - Datum stets im ISO-Format YYYY-MM-DD. - Wenn das Bild einen Monatstitel zeigt (z.B. „November 2025"), gib \`month\` (1–12) und \`year\` (vierstellig) in der Antwort an. Sonst null. - Wenn ein Name unklar zu lesen ist, übernimm deinen besten Ratevorschlag und vermerke es in \`notes\`. +- Gib Namen OHNE Anrede/Titel zurück (kein „Herr", „Frau", „Hr.", „Fr.", „Dr.", „Prof.") — nur den Nachnamen wie in der Tabelle. Antworte STRIKT in diesem JSON-Schema und sonst nichts: { @@ -864,13 +880,15 @@ Antworte STRIKT in diesem JSON-Schema und sonst nichts: "notes": ["string", ...] }`; -// Make available globally -window.ImageImporter = ImageImporter; +// Make available globally (Browser) + require-bar (Node-Tests) +if (typeof window !== 'undefined') window.ImageImporter = ImageImporter; +if (typeof module !== 'undefined' && module.exports) module.exports = ImageImporter; -// Auto-instantiate when DOM + app are ready +// Fallback-Instantiierung: app.js erzeugt den Importer regulaer nach dem +// Sync-Boot. Dieser Listener greift nur, falls app schon frueher bereitsteht. if (typeof document !== 'undefined') { document.addEventListener('DOMContentLoaded', () => { - if (window.app) { + if (window.app && !window.imageImporter) { window.imageImporter = new ImageImporter(window.app); } }); diff --git a/image-import.test.js b/image-import.test.js new file mode 100644 index 0000000..0448f43 --- /dev/null +++ b/image-import.test.js @@ -0,0 +1,44 @@ +const { test } = require('node:test'); +const assert = require('node:assert'); +const ImageImporter = require('./image-import.js'); + +const imp = new ImageImporter(null); + +test('normalizeName entfernt Anrede/Titel (auch gestapelt)', () => { + assert.strictEqual(imp.normalizeName('Herr Alsholi'), 'alsholi'); + assert.strictEqual(imp.normalizeName('Frau Cabrera'), 'cabrera'); + assert.strictEqual(imp.normalizeName('Herr Dr. Azizi'), 'azizi'); + assert.strictEqual(imp.normalizeName('Hr. Schierholz'), 'schierholz'); + assert.strictEqual(imp.normalizeName('Prof. Müller'), 'müller'); +}); + +test('normalizeName laesst titellose Namen unveraendert', () => { + assert.strictEqual(imp.normalizeName('Alsholi'), 'alsholi'); + assert.strictEqual(imp.normalizeName(' Elsharawy '), 'elsharawy'); + // "Frei" darf nicht als Titel "fr" fehlinterpretiert werden (kein Space danach) + assert.strictEqual(imp.normalizeName('Frei'), 'frei'); +}); + +test('matchNames matcht Titel-Namen auf titellose Mitarbeiter (kein Doppelanlegen)', () => { + const entries = [ + { name: 'Herr Alsholi', date: '2026-06-10', share: 1.0 }, + { name: 'Frau Cabrera', date: '2026-06-11', share: 1.0 }, + { name: 'Herr Dr. Azizi', date: '2026-06-02', share: 1.0 }, + ]; + const existing = ['Alsholi', 'Cabrera', 'Azizi']; + const { matched, unknowns } = imp.matchNames(entries, existing); + assert.strictEqual(unknowns.length, 0, 'keine Unbekannten mehr'); + assert.strictEqual(matched.length, 3); + assert.strictEqual(matched[0].resolvedName, 'Alsholi'); + assert.strictEqual(matched[1].resolvedName, 'Cabrera'); + assert.strictEqual(matched[2].resolvedName, 'Azizi'); +}); + +test('matchNames meldet echten Unbekannten weiterhin als unknown', () => { + const entries = [{ name: 'Herr Weiszflog', date: '2026-06-10', share: 1.0 }]; + const existing = ['Alsholi', 'Cabrera']; + const { matched, unknowns } = imp.matchNames(entries, existing); + assert.strictEqual(matched.length, 0); + assert.strictEqual(unknowns.length, 1); + assert.strictEqual(unknowns[0].candidate, 'Herr Weiszflog'); +}); diff --git a/sw.js b/sw.js index ac3420d..933d659 100644 --- a/sw.js +++ b/sw.js @@ -1,4 +1,4 @@ -const CACHE_NAME = 'dienstplan-pro-v5'; +const CACHE_NAME = 'dienstplan-pro-v6'; const ASSETS = [ './', './index.html',