fix(image-import): Zielmonat aus Live-Dropdown, Titel-Namen matchen, Importer-Instanziierung nach Sync-Boot

- Zielmonat/-jahr aus month-select/year-select statt veraltetem app.currentMonth (stand fix auf heute)
- normalizeName entfernt Anreden/Titel (Herr/Frau/Dr.) -> matcht titellose Mitarbeiter, kein Doppelanlegen
- Regression aus Sync-Boot behoben: app.js erzeugt imageImporter nach window.app (DOMContentLoaded-Listener lief zu frueh)
- totes app.currentMonth/currentYear entfernt; SW-Cache v5->v6
This commit is contained in:
Kenearos 2026-07-07 19:23:45 +02:00
parent 7d50e5d938
commit 0001735207
4 changed files with 77 additions and 11 deletions

10
app.js
View file

@ -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);
}
});

View file

@ -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\` (112) 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);
}
});

44
image-import.test.js Normal file
View file

@ -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');
});

2
sw.js
View file

@ -1,4 +1,4 @@
const CACHE_NAME = 'dienstplan-pro-v5';
const CACHE_NAME = 'dienstplan-pro-v6';
const ASSETS = [
'./',
'./index.html',