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

288 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```bash
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`:
```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:**
```ts
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
```ts
{
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`