Files
paperlessmanager/docs/superpowers/plans/2026-11-02-duplikat-neue-version.md
T

24 KiB
Raw Blame History

Duplikat → neue Version – Umsetzungsplan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Ein Beleg, dessen interne Belegnummer in Paperless schon existiert, wird nach einem Vergleichsdialog entweder als neue Version an das vorhandene Dokument gehängt oder verworfen – im E-Mail-Import und im Posteingang.

Architecture: Backend bekommt drei Bausteine im paperless-Modul: Belegnummer-Prüfung gegen Paperless (CF7 + ASN, auf Stammdokument normalisiert), Kennzahlen (Seiten/Wörter) und DokumentVersionService (Upload per update_version + Tag 21 „neue Version hochgeladen“ am Dokument; die Postprocessing-Regeln ignoriert der Nutzer per Regel-Konfiguration). E-Mail-Import und Inbox-Postprocessor nutzen sie; das Frontend bekommt eine gemeinsame Komponente DuplikatVergleichModal.

Tech Stack: NestJS 11, TypeORM/MariaDB, Jest, pdf-lib, pdf-parse (PDFParse), React 19 + Ant Design, Axios.

Spec: docs/superpowers/specs/2026-11-02-duplikat-neue-version-design.md

Global Constraints

  • Sprache: UI-Texte, Kommentare, Commit-Messages auf Deutsch; Bezeichner in bestehender Konvention (gemischt dt./engl. wie im Umfeld).
  • Maßgeblich für „Belegnummer existiert“ ist Paperless: CF7 = interne Belegnummer und ASN = Belegnummer ohne Bindestriche (2026-000123 → 2026000123).
  • Treffer immer auf das Stammdokument normalisieren: root_document ?? id.
  • „Als neue Version hinzufügen“ nur bei genau einem Stammdokument; sonst nur „Verwerfen“.
  • Versions-Upload: POST /api/documents/{id}/update_version/, multipart document + version_label = Korrektur vom TT.MM.JJJJ (max. 64 Zeichen).
  • Nach Versions-Upload bekommt das Stammdokument Tag 21 („neue Version hochgeladen“), vorhandene Tags bleiben. Keine eigene Webhook-Unterdrückung im Code.
  • Checksummen-Prüfungen (checkDuplicate, recheckDuplicates, sperreImporte, Inbox-Schritt 6) bleiben unverändert.
  • Task-Bearbeitung (PaperlessTaskProcessorService) bleibt unverändert; für Versionen wird kein Task angelegt.
  • Produktions-Schema nur per Migration (src/database/migrations/), handgeschrieben wie 1782700000000-CreateWebhookQueue.ts.
  • Backend nach jeder Änderung: npx eslint <Dateien> und npx prettier --check <Dateien> grün für die geänderten Dateien; Frontend: npm run lint und npm run build grün.

Review Focus

  1. Dokument hat Tag 21 schon (zweite Korrektur) – Tag nicht doppelt setzen und bei fehlgeschlagenem Upload nicht entfernen. → Test in Task 4.
  2. Dokument hat schon Versionen: CF7-/ASN-Suche liefert Stamm und Versionen → darf nicht als „mehrdeutig“ gelten. → Test in Task 2.
  3. E-Mail-Import: Benutzer geht nach der Entscheidung zurück und ändert die Belegnummer – die alte Versions-Entscheidung darf nicht am neuen Wert kleben. → Abnahme in Task 7.
  4. Posteingang „Verwerfen“: Aktionen nach PAPERLESS laufen, Aktionen davor nicht doppelt. → Test in Task 5.
  5. PDF ohne Textebene (Scan): Wortzahl „nicht ermittelbar“ statt 0 bzw. falscher Abweichung. → Test in Task 3.

Task 1: Live-Prüfung der Paperless-Versionsfunktion

Keine Code-Änderung. Ergebnisse als Abschnitt „Ergebnisse Live-Prüfung“ unten in die Spec schreiben. Vor Schritt 2 den Nutzer fragen, welches Testdokument verwendet werden darf (der Upload verändert ein echtes Dokument in Produktion).

  • Step 1: GET /api/documents/?archive_serial_number=<ASN eines Dokuments> und GET /api/documents/<id>/ abrufen; notieren, ob root_document und versions befüllt sind.
  • Step 2: Mit Freigabe des Nutzers eine Test-PDF per curl -F document=@test.pdf -F version_label="Test" .../api/documents/<id>/update_version/ hochladen. Notieren: Antwortformat (Task-UUID?), Ergebnis von GET /api/tasks/?task_id=… nach Abschluss (result_data.document_id?).
  • Step 3: Danach notieren: Liefert die CF7-Suche (custom_field_query=[7,"icontains","…"]) und die ASN-Suche die Version als eigenes Dokument? Welche Werte haben id, root_document, page_count, content am Stamm (neueste Fassung?) und was liefert /download/?
  • Step 4: Im Backend-Log nachsehen, ob und mit welcher Dokument-ID ein Webhook kam (nur zur Info; die Regeln filtern über Tag 21).
  • Step 5: Testversion wieder löschen (DELETE /api/documents/<id>/versions/<version_id>/), Ergebnisse in die Spec schreiben, committen:
git add docs/superpowers/specs/2026-11-02-duplikat-neue-version-design.md
git commit -m "docs: Ergebnisse der Live-Prüfung Paperless-Versionen"

Weichen die Ergebnisse von den Annahmen in Task 2/4 ab (z. B. keine Task-ID, kein root_document), vor Task 2 den Plan anpassen.


Task 2: Belegnummer-Prüfung gegen Paperless

Files:

  • Create: paperless-backend/src/paperless/belegnummer-pruefung.util.ts
  • Create: paperless-backend/src/paperless/belegnummer-pruefung.util.spec.ts
  • Modify: paperless-backend/src/paperless/paperless.service.ts (neben findDocumentIdByCustomField/findDocumentIdByAsn)
  • Modify: paperless-backend/src/paperless/paperless.controller.ts
  • Modify: paperless-frontend/src/api/paperless.ts

Interfaces:

  • Produces:

    • export type BelegnummerStatus = 'frei' | 'eindeutig' | 'mehrdeutig'
    • export interface BelegnummerPruefung { status: BelegnummerStatus; documentId: number | null; kandidaten: number[] }
    • export function belegnummerZuAsn(belegnummer: string): number | null – entfernt -, parseInt; null bei NaN oder leer.
    • export function klassifiziereTreffer(cf7RootIds: number[], asnRootIds: number[]): BelegnummerPruefung – Vereinigung, dedupliziert, aufsteigend sortiert; 0 → frei, 1 → eindeutig (documentId = diese ID), >1 → mehrdeutig (documentId: null).
    • PaperlessService.findRootDocumentIdsByCustomField(fieldId: number, value: string): Promise<number[]> – wie findDocumentIdByCustomField (gleiche case-insensitive Verifikation), aber alle Treffer, je Treffer root_document ?? id.
    • PaperlessService.findRootDocumentIdsByAsn(asn: number): Promise<number[]> – analog zu findDocumentIdByAsn.
    • PaperlessService.pruefeBelegnummer(belegnummer: string): Promise<BelegnummerPruefung>
    • GET /api/paperless/belegnummer-pruefung?belegnummer=2026-000123 → BelegnummerPruefung; @RequirePermissions(Permission.VIEW_MAIL) (wie die E-Mail-Import-Endpunkte).
    • Frontend: paperlessApi.pruefeBelegnummer(belegnummer: string): Promise<BelegnummerPruefung> + exportierte Typen.
  • Step 1: Failing tests in belegnummer-pruefung.util.spec.ts:

expect(belegnummerZuAsn('2026-000123')).toBe(2026000123);
expect(belegnummerZuAsn('')).toBeNull();
expect(belegnummerZuAsn('abc')).toBeNull();
expect(klassifiziereTreffer([], [])).toEqual({ status: 'frei', documentId: null, kandidaten: [] });
expect(klassifiziereTreffer([42], [42])).toEqual({ status: 'eindeutig', documentId: 42, kandidaten: [42] });
expect(klassifiziereTreffer([42, 42], [])).toEqual({ status: 'eindeutig', documentId: 42, kandidaten: [42] }); // Review Focus 2: Stamm + Version
expect(klassifiziereTreffer([42], [7])).toEqual({ status: 'mehrdeutig', documentId: null, kandidaten: [7, 42] });
expect(klassifiziereTreffer([], [7])).toEqual({ status: 'eindeutig', documentId: 7, kandidaten: [7] });
  • Step 2: cd paperless-backend && npx jest src/paperless/belegnummer-pruefung.util.spec.ts → FAIL (Modul fehlt).
  • Step 3: Util implementieren; Service-Methoden und Endpoint ergänzen; Frontend-API-Methode ergänzen.
  • Step 4: Test erneut → PASS; ESLint/Prettier für die geänderten Dateien grün.
  • Step 5: Commit feat(paperless): Belegnummer gegen Paperless prüfen (CF7 + ASN)

Task 3: Kennzahlen für den Vergleich (Seiten, Wörter)

Files:

  • Create: paperless-backend/src/paperless/vergleich-kennzahlen.util.ts
  • Create: paperless-backend/src/paperless/vergleich-kennzahlen.util.spec.ts
  • Modify: paperless-backend/src/paperless/paperless.service.ts
  • Modify: paperless-backend/src/inbox/inbox.controller.ts (+ inbox.service.ts, falls Hilfsmethode nötig)
  • Modify: paperless-backend/src/email/email-import.controller.ts, email-import.service.ts
  • Modify: paperless-frontend/src/api/inbox.ts, paperless-frontend/src/api/email-import.ts

Interfaces:

  • Produces:

    • export interface Kennzahlen { seiten: number | null; woerter: number | null }
    • export interface VergleichKennzahlen { vorhanden: Kennzahlen; neu: Kennzahlen }
    • export function zaehleWoerter(text: string | null | undefined): number | null – an /\s+/ trennen, nur Tokens mit mind. einem Buchstaben oder einer Ziffer (/[\p{L}\p{N}]/u) zählen; Seitenmarker -- N of M -- vorher entfernen; ergibt sich 0 → null („nicht ermittelbar“).
    • export async function kennzahlenAusPdf(pdf: Buffer): Promise<Kennzahlen> – Seiten per PDFDocument.load(..., { ignoreEncryption: true }).getPageCount(), Text per PDFParse (Muster aus email/pdf-fingerprint.util.ts, destroy() im finally); Lesefehler → jeweils null.
    • PaperlessService.getDocumentKennzahlen(documentId: number): Promise<Kennzahlen> – page_count und zaehleWoerter(content) aus getDocument (laut Task 1 neueste Fassung).
    • POST /api/inbox/:id/vergleich Body { paperlessDocumentId: number; pages: number[] } → VergleichKennzahlen; neue PDF über inboxService.getSegmentPdfBuffer(id, user, pages).
    • POST /api/email-import/attachments/:attachmentId/vergleich Body { paperlessDocumentId: number; pages?: { start: number; end: number } } → VergleichKennzahlen; @RequirePermissions(Permission.VIEW_MAIL); PDF aus Content.Content1, bei pages nur dieser Bereich (pdf-lib copyPages wie in runImport).
    • Frontend: inboxApi.vergleich(id, paperlessDocumentId, pages), emailImportApi.vergleich(attachmentId, paperlessDocumentId, pages?), emailImportApi.attachmentPdfBlob(attachmentId, pages?) falls es noch keinen PDF-Endpunkt für Anhänge/Bereiche gibt (vorhanden ist printPreview – nur verwenden, wenn ohne Stempel möglich; sonst GET /api/email-import/attachments/:attachmentId/pdf?start=&end= ergänzen).
  • Step 1: Failing tests:

expect(zaehleWoerter('Rechnung Nr. 123 – Betrag 4,50 €')).toBe(5); // "–" und "€" zählen nicht
expect(zaehleWoerter('-- 1 of 2 --')).toBeNull();
expect(zaehleWoerter('')).toBeNull();
expect(zaehleWoerter(null)).toBeNull();
// PDF mit Text (pdf-lib, StandardFonts.Helvetica, 2 Seiten, Text "Hallo Welt" auf Seite 1)
expect(await kennzahlenAusPdf(pdfMitText)).toEqual({ seiten: 2, woerter: 2 });
// PDF ohne Text (1 leere Seite) – Review Focus 5
expect(await kennzahlenAusPdf(leeresPdf)).toEqual({ seiten: 1, woerter: null });
expect(await kennzahlenAusPdf(Buffer.from('kaputt'))).toEqual({ seiten: null, woerter: null });
  • Step 2: npx jest src/paperless/vergleich-kennzahlen.util.spec.ts → FAIL.
  • Step 3: Util, Service-Methode, beide Endpunkte und Frontend-API implementieren.
  • Step 4: Test → PASS; Lint/Prettier grün.
  • Step 5: Commit feat: Seiten- und Wortzahl für den Duplikatvergleich

Task 4: Versions-Upload mit Tag 21

Files:

  • Create: paperless-backend/src/paperless/dokument-version.service.ts
  • Create: paperless-backend/src/paperless/dokument-version.service.spec.ts
  • Modify: paperless-backend/src/paperless/paperless.service.ts, paperless.module.ts

Interfaces:

  • Produces:

    • PaperlessService.uploadDocumentVersion(documentId: number, filePath: string, versionLabel: string): Promise<string> – multipart wie uploadDocument, Timeout 120 s, Rückgabe ohne Anführungszeichen.
    • export const NEUE_VERSION_TAG_ID = 21;
    • export function versionLabel(datum: Date): string → Korrektur vom 02.11.2026.
    • DokumentVersionService.haengeVersionAn(rootDocumentId: number, pdfPath: string): Promise<void> – Reihenfolge:
      1. Dokument lesen; hat es Tag 21 noch nicht, per updateDocument(rootDocumentId, { tags }) bisherige Tags plus 21 setzen.
      2. Danach uploadDocumentVersion(rootDocumentId, pdfPath, versionLabel(new Date())).
      3. Schlägt der Upload fehl und wurde Tag 21 in Schritt 1 neu gesetzt: Dokument erneut lesen, Tag 21 entfernen (updateDocument mit den übrigen Tags), danach den ursprünglichen Fehler weiterwerfen. Schlägt das Entfernen selbst fehl, nur loggen (Tag 21 konnte nach fehlgeschlagenem Versions-Upload nicht entfernt werden) und trotzdem den ursprünglichen Fehler werfen. Exportiert aus PaperlessModule.
  • Step 1: Failing tests (PaperlessService gemockt):

    • versionLabel(new Date(2026, 10, 2)) → 'Korrektur vom 02.11.2026'.
    • Tags [3, 17]: updateDocument(42, { tags: [3, 17, 21] }) wird vor uploadDocumentVersion(42, '/tmp/x.pdf', <Label>) aufgerufen (Reihenfolge über mock.invocationCallOrder prüfen).
    • Tags [21, 5]: kein updateDocument, Upload läuft (Review Focus 1).
    • Upload wirft, Tags vorher [3]: nach dem Setzen [3, 21] wird beim zweiten Lesen [3, 21] geliefert → updateDocument(42, { tags: [3] }); haengeVersionAn wirft den ursprünglichen Fehler.
    • Upload wirft, Tag 21 war schon vorher da: Tag wird nicht entfernt, Fehler wird geworfen.
    • Upload wirft und das Entfernen wirft ebenfalls: der Upload-Fehler wird geworfen.
  • Step 2: npx jest src/paperless/dokument-version.service.spec.ts → FAIL.

  • Step 3: Service und Upload-Methode implementieren.

  • Step 4: Test → PASS; npm run build im Backend grün; Lint/Prettier grün.

  • Step 5: Commit feat(paperless): Belege als neue Version anhängen und mit Tag 21 markieren


Task 5: Posteingang – Entscheidung im Inbox-Postprocessor

Files:

  • Create: paperless-backend/src/inbox-postprocessor/paperless-entscheidung.util.ts (+ .spec.ts)
  • Modify: paperless-backend/src/inbox-postprocessor/inbox-postprocessor.service.ts (runForDocument, runPaperless, Interfaces oben)
  • Modify: paperless-backend/src/inbox-postprocessor/inbox-postprocessor.module.ts (falls Provider fehlt)
  • Modify: paperless-backend/src/inbox/inbox.controller.ts (postprocess-Body)
  • Modify: paperless-frontend/src/api/inbox.ts (Typen postprocess)

Interfaces:

  • Consumes: PaperlessService.pruefeBelegnummer (Task 2), DokumentVersionService.haengeVersionAn (Task 4).

  • Produces:

    • export type PaperlessEntscheidung = { aktion: 'version'; documentId: number } | { aktion: 'verwerfen' }
    • export function aktionenAbEntscheidung<T extends { ActionType: string }>(aktionen: T[], entscheidung?: PaperlessEntscheidung): T[] – ohne Entscheidung alle; mit Entscheidung alle ab der ersten PAPERLESS-Aktion (die davor liefen schon); gibt es keine PAPERLESS-Aktion, alle.
    • ActionResult ergänzt um versionMoeglich?: boolean und duplikatKandidaten?: number[]; duplicateOfDocumentId = eindeutige Stamm-ID bzw. erster Kandidat.
    • runForDocument(documentId, preferredUsername, sectionOffset = 0, processOnlyOne = false, entscheidung?: PaperlessEntscheidung) – ersetzt den Parameter replaceDuplicate.
    • Controller-Body: { sectionOffset?: number; processOnlyOne?: boolean; entscheidung?: PaperlessEntscheidung } (replaceDuplicate entfällt).
    • Verhalten der PAPERLESS-Aktion:
      • entscheidung.aktion === 'verwerfen' → Aktion übersprungen, Ergebnis { ok: true, skipped: true, message: 'Verworfen' }, kein Abbruch; folgende Aktionen laufen.
      • entscheidung.aktion === 'version' → Belegnummer/ASN wie heute auflösen, keine Duplikatprüfung, haengeVersionAn(entscheidung.documentId, sectionPdfPath), kein Task; Ergebnis { ok: true, message: 'Als neue Version zu Dokument X hinzugefügt' }; folgende Aktionen laufen.
      • ohne Entscheidung: Duplikatprüfung in dieser Reihenfolge: (1) pruefeBelegnummer(interneBelegnummer) – eindeutig → skipped, versionMoeglich: true; mehrdeutig → skipped, versionMoeglich: false, duplikatKandidaten; (2) lokaler Task (wie heute, versionMoeglich: false); (3) Checksumme (unverändert). Die bisherigen Einzelprüfungen CF7 und ASN (Schritte 4/5) entfallen zugunsten von (1). Bei skipped Abbruch wie heute.
  • Step 1: Failing tests für aktionenAbEntscheidung mit [MAIL, PAPERLESS, EXPORT]:

    • ohne Entscheidung → alle drei;
    • { aktion: 'verwerfen' } → [PAPERLESS, EXPORT] (Review Focus 4: MAIL nicht doppelt, EXPORT läuft);
    • { aktion: 'version', documentId: 1 } → [PAPERLESS, EXPORT];
    • [MAIL, EXPORT] mit Entscheidung → beide.
  • Step 2: npx jest src/inbox-postprocessor/paperless-entscheidung.util.spec.ts → FAIL.

  • Step 3: Util implementieren, runForDocument/runPaperless/Controller/Frontend-Typen umstellen.

  • Step 4: Test → PASS; npm run build und npm test im Backend (bekannte, schon vorher rote Specs pdf-fingerprint.util.spec.ts, attachment-fingerprint.service.spec.ts ausgenommen) grün.

  • Step 5: Commit feat(posteingang): Duplikat als neue Version oder verwerfen


Task 6: E-Mail-Import – Version beim Import hochladen

Files:

  • Modify: paperless-backend/src/email/email-import.service.ts (Typen von executeImport/runImport, Upload-Schleife)
  • Modify: paperless-backend/src/email/email-import.service.spec.ts
  • Modify: paperless-backend/src/email/email.module.ts (falls Provider fehlt)
  • Modify: paperless-frontend/src/api/email-import.ts (AttachmentImportData.versionZuDokumentId?: number | null)

Interfaces:

  • Consumes: DokumentVersionService.haengeVersionAn (Task 4).

  • Produces: Anhang-Eintrag in executeImport akzeptiert versionZuDokumentId?: number | null. Ist er gesetzt:

    • kein validateAsnNotExists, kein uploadDocument, kein Task, kein Task-Polling, kein setBelegnummer;
    • je Upload-Datei haengeVersionAn(versionZuDokumentId, uploadItem.path); paperlessIds[rangeKey] = versionZuDokumentId;
    • ImportStatus = 1, InterneBelegnummer wie bisher am Attachment gesetzt;
    • Ergebnis enthält { attachmentId, paperlessIds, alsVersion: true }.
  • Step 1: Failing test in email-import.service.spec.ts, neuer describe('Import als Version') mit dem bestehenden Testmodul (+ Provider DokumentVersionService gemockt), Anhang mit versionZuDokumentId: 42, belegnummer: '2026-000123', barcode.nummer: '000123':

    • dokumentVersionService.haengeVersionAn einmal mit 42 und einem .pdf-Pfad aufgerufen;
    • paperlessService.uploadDocument, paperlessService.validateAsnNotExists und taskRepo.save nicht aufgerufen;
    • gespeichertes Attachment hat PaperlessDocumentIds { full: 42 } und ImportStatus: 1.
  • Step 2: npx jest src/email/email-import.service.spec.ts → neuer Test FAIL.

  • Step 3: Zweig in runImport implementieren (eigene private Methode, damit die Upload-Schleife lesbar bleibt).

  • Step 4: Test → PASS, übrige Tests der Datei weiter grün; Lint/Prettier grün.

  • Step 5: Commit feat(mail): Beleg beim Import als neue Version anhängen


Task 7: Frontend – gemeinsamer Vergleichsdialog, E-Mail-Import und Posteingang

Files:

  • Create: paperless-frontend/src/components/DuplikatVergleichModal.tsx
  • Modify: paperless-frontend/src/pages/InboxDetailPage.tsx (CompareModal entfernen, Wizard umstellen)
  • Modify: paperless-frontend/src/components/MailImportWizard.tsx

Interfaces:

  • Consumes: Frontend-APIs aus Task 2, 3, 5, 6.
  • Produces: DuplikatVergleichModal mit Props
    {
      open: boolean;
      belegnummer: string;
      pruefung: BelegnummerPruefung | null;      // eindeutig → beide Knöpfe; mehrdeutig → nur Verwerfen + Hinweis
      paperlessDocumentId: number | null;        // linke Seite
      ladeNeuesPdf: () => Promise<Blob>;         // rechte Seite
      ladeKennzahlen: () => Promise<VergleichKennzahlen>;
      onVersion: () => void;
      onVerwerfen: () => void;
      onClose: () => void;
    }
    
    Titel Belegnummer ${belegnummer} existiert bereits. Auffälligkeiten-Leiste über den PDFs:
    • je Kennzahl eine Zeile Seiten: 2 (vorhanden) → 3 (neu) bzw. Wörter: 412 → 398; Abweichung als orange Tag, null als „nicht ermittelbar“ (grau, keine Abweichung);
    • keine Abweichung und nichts null → Alert type="success" „Seiten- und Wortzahl gleich“;
    • mehrdeutig → Alert type="warning" „Mehrere Dokumente in Paperless mit dieser Belegnummer (IDs: …) – eine neue Version ist nicht möglich.“
    • Knöpfe: Verwerfen und primär Als neue Version hinzufügen (nur bei eindeutig). Layout der PDFs wie bisher (flex, mobil untereinander, 75vh), Object-URLs beim Schließen freigeben.

Posteingang (PostprocessWizardModal):

  • processSection(idx, entscheidung?: PaperlessEntscheidung); Ergebnis mit versionMoeglich oder duplikatKandidaten öffnet den Dialog automatisch (setCompareForSection(idx)).
  • Zeilen-Knöpfe bei Duplikat: Vergleichen (öffnet Dialog, nur wenn eine Dokument-ID vorliegt) und Verwerfen; „Neue Version erstellen“ (alter replaceDuplicate-Weg) entfällt.
  • onVersion → processSection(idx, { aktion: 'version', documentId }); onVerwerfen und Zeilen-„Verwerfen“ → processSection(idx, { aktion: 'verwerfen' }); danach Abschnitt-Status skipped mit Meldung „Verworfen“ bzw. success.
  • Rechte Seite: inboxApi.downloadSegmentBlob(documentId, documents[idx].pages); Kennzahlen: inboxApi.vergleich(documentId, paperlessDocumentId, documents[idx].pages).

E-Mail-Import (MailImportWizard):

  • State versionEntscheidung: Record<virtualId, { belegnummer: string; documentId: number }>.

  • In nextStep bei currentStep === 1, nach recheckDuplicates und vor preFetchBelegnummern: für jedes nicht ignorierte Item im Modus manuell die Belegnummer ${jahr}-${nummer.padStart(6,'0')} bilden und paperlessApi.pruefeBelegnummer aufrufen; eine vorhandene Entscheidung gilt nur, wenn ihre belegnummer gleich ist (sonst verwerfen – Review Focus 3).

  • Treffer (eindeutig/mehrdeutig) ohne gültige Entscheidung nacheinander im Dialog zeigen; Schrittwechsel erst, wenn alle entschieden sind. Schließen ohne Entscheidung → Warteschlange abbrechen, in Schritt 2 bleiben.

  • onVersion → Entscheidung speichern; onVerwerfen → Item auf type: 'IGNORE'; sind danach alle Items IGNORE → ignoreEmail() statt Schrittwechsel.

  • Rechte Seite: PDF des Anhangs bzw. Bereichs (Task 3), Kennzahlen: emailImportApi.vergleich(...).

  • executeImport: versionZuDokumentId aus gültiger Entscheidung in finalData übernehmen.

  • In Schritt 3 („Abschluss“) bei Version-Items einen Hinweis „wird als neue Version zu Dokument X hinzugefügt“ anzeigen.

  • Step 1: Komponente bauen, CompareModal in InboxDetailPage.tsx ersetzen und Posteingang umstellen.

  • Step 2: MailImportWizard umstellen.

  • Step 3: cd paperless-frontend && npm run lint && npm run build → beide ohne Fehler.

  • Step 4: Abnahme im Browser (Dev-Server, Backend gegen Paperless), nur mit Freigabe des Nutzers für echte Uploads:

    • Mail mit manueller, vorhandener Belegnummer → Dialog erscheint vor Schritt 3, Auffälligkeiten stimmen; „Verwerfen“ stellt auf Ignorieren; „Als neue Version“ → Import hängt Version an (in Paperless sichtbar, kein neues Dokument, Tag 21 gesetzt).
    • Zurück, Belegnummer ändern, Weiter → alte Entscheidung greift nicht (Review Focus 3).
    • Posteingang-Abschnitt mit vorhandener Belegnummer → Dialog öffnet automatisch; beide Entscheidungen; Aktionen nach PAPERLESS laufen.
  • Step 5: Commit feat(frontend): Vergleichsdialog für Duplikate mit neuer Version


Task 8: Todos und Abschluss

Files:

  • Modify: docs/todos.md

  • Step 1: Die drei Einträge aus dem Spec-Abschnitt „Todos-Einträge“ wörtlich als - [ ]-Punkte an docs/todos.md anhängen.

  • Step 2: Backend npm run build, npm test (bekannte rote Specs ausgenommen), Frontend npm run build – alles grün.

  • Step 3: Commit docs: offene Punkte zu Versionen und Task-Bearbeitung