diff --git a/docs/craftvia/ARCHITEKTUR.md b/docs/craftvia/ARCHITEKTUR.md index 6ef129d..af7f270 100644 --- a/docs/craftvia/ARCHITEKTUR.md +++ b/docs/craftvia/ARCHITEKTUR.md @@ -26,6 +26,8 @@ Siehe `src/server/rbac.ts` (vom Fundament angelegt). Rollen: `tenant-admin`, `ba - Kunden/Objekte für Monteure: nur lesbar, wenn über einen sichtbaren Auftrag erreichbar (`customerScope`, `siteScope` in derselben Datei). Jede Query auf WorkOrder und abhängige Entitäten (Photos, Reports, Documents …) für Nicht-Backoffice-Rollen MUSS diesen Scope verwenden. +**Objekt-Historie für Feldrollen (Entscheidung, US-005/US-011):** Monteure/Teamleiter sehen an einem Objekt, das sie über einen eigenen sichtbaren Auftrag erreichen (`siteScope`), die FREIGEGEBENEN Einsätze aller Teams (Aufträge mit freigegebenem Bericht; `getSiteHistory` in `services/sites/history.ts`). Interne Hinweise sind nie Teil der Historie. + **Dokument-Sichtbarkeit:** `documentVisibilityFilter(ctx)`: `backoffice_only` nur mit `document:read_internal`; `team_lead` nur Teamleiter/Backoffice; `team`/`customer_report` für berechtigte Auftragsbeteiligte. ## 3. Statusmodell Auftrag diff --git a/scripts/test-berichte-flow.ts b/scripts/test-berichte-flow.ts index 082d2ef..07aed1e 100644 --- a/scripts/test-berichte-flow.ts +++ b/scripts/test-berichte-flow.ts @@ -22,7 +22,7 @@ import { rejectReport } from "../src/server/services/reports/reject"; import { createNewVersion } from "../src/server/services/reports/new-version"; import { requireVisibleReport } from "../src/server/services/reports/common"; import { listReports } from "../src/server/services/reports/queries"; -import { storeFile } from "../src/server/services/reports/_stubs/documents"; +import { storeFile } from "../src/server/services/documents/store"; let failures = 0; const ok = (cond: boolean, msg: string) => { diff --git a/scripts/test-berichte-pdf.ts b/scripts/test-berichte-pdf.ts index 4dfc166..b36db0a 100644 --- a/scripts/test-berichte-pdf.ts +++ b/scripts/test-berichte-pdf.ts @@ -15,7 +15,8 @@ import { submitReport } from "../src/server/services/reports/submit"; import { approveReport } from "../src/server/services/reports/approve"; import { generateReportPdf } from "../src/server/services/reports/pdf"; import { openReportFile } from "../src/server/services/reports/files"; -import { readFileBytes, sha256Hex, storeFile } from "../src/server/services/reports/_stubs/documents"; +import { readStoredBytes } from "../src/server/services/documents/read"; +import { sha256Hex, storeFile } from "../src/server/services/documents/store"; let failures = 0; const ok = (cond: boolean, msg: string) => { @@ -101,7 +102,7 @@ async function main() { ok(doc.fileSize > 0 && doc.mimeType === "application/pdf", `Dokument gespeichert (${doc.fileSize} Bytes)`); ok(doc.category === "daily_report" && doc.visibility === "customer_report" && doc.workOrderId === wo.id, "Kategorie daily_report, Sichtbarkeit customer_report, am Auftrag"); ok(stored.pdfChecksum === doc.checksum && /^[0-9a-f]{64}$/.test(stored.pdfChecksum ?? ""), "SHA-256-Prüfsumme am Bericht gespeichert"); - const bytes = await readFileBytes(doc.storageKey); + const bytes = await readStoredBytes(doc.storageKey); if (bytes) { ok(Buffer.from(bytes).subarray(0, 4).toString() === "%PDF", "Datei beginnt mit %PDF"); ok(sha256Hex(bytes) === stored.pdfChecksum, "Prüfsumme entspricht den gespeicherten Bytes"); diff --git a/scripts/test-einsatz-field.ts b/scripts/test-einsatz-field.ts index da11770..593a38f 100644 --- a/scripts/test-einsatz-field.ts +++ b/scripts/test-einsatz-field.ts @@ -13,7 +13,7 @@ import { upsertMaterialUsage } from "../src/server/services/field/materials"; import { toggleChecklistItem } from "../src/server/services/field/checklist"; import { createNote } from "../src/server/services/field/notes"; import { getFieldOrderDetail, listFieldOrders, listTodayOrders, getFieldBundle } from "../src/server/services/field/queries"; -import { transitionWorkOrder } from "../src/server/services/field/stubs/work-order-transition"; +import { transitionWorkOrder } from "../src/server/services/work-orders/transition"; import { createFixture, expectCode, failures, ok } from "./lib/einsatz-fixture"; const min = (n: number) => n * 60 * 1000; diff --git a/scripts/test-import-flow.ts b/scripts/test-import-flow.ts index 2ab4b2b..78b2e7d 100644 --- a/scripts/test-import-flow.ts +++ b/scripts/test-import-flow.ts @@ -180,7 +180,8 @@ async function main() { const wo1 = await prisma.workOrder.findUnique({ where: { id: res1.workOrderId }, include: { statusHistory: { orderBy: { createdAt: "asc" } }, materialPlans: true } }); ok(wo1?.status === "planned" && wo1.sourceImportId === job1.id && wo1.customerId === existing.id && wo1.siteId === existingSite.id, "(K3) work order planned, linked to import, customer and site"); ok(wo1?.number.startsWith("A-") === true && wo1.externalOrderNumber === "AB-2026-0815" && wo1.plannedStart?.toISOString().startsWith("2026-10-12") === true, "(K4) number allocated, document order number and dates taken over"); - ok(wo1?.statusHistory.map((s) => s.toStatus).join(">") === "review_required>planned", "(K5) status history review_required → planned"); + // The review happens on the ImportJob ("Entwurf – Prüfung erforderlich"); the confirmed order starts planned. + ok(wo1?.statusHistory.map((s) => s.toStatus).join(">") === "planned", "(K5) confirmed order starts with status history → planned"); ok(wo1?.materialPlans.length === 1 && wo1.materialPlans[0].articleNumber === "WP-AT-12", "(K6) only positions marked as material become material plan"); ok((await prisma.customer.count({ where: { tenantId: tA.id } })) === customersBefore, "(K7) no new customer when existing one is used"); const docAfter = await prisma.document.findUnique({ where: { id: job1.documentId } }); diff --git a/scripts/test-import-rules.ts b/scripts/test-import-rules.ts index e345307..a8c0f5f 100644 --- a/scripts/test-import-rules.ts +++ b/scripts/test-import-rules.ts @@ -9,8 +9,9 @@ import type { WorkOrderExtraction } from "../src/server/ai/providers"; import { emptyExtraction, extractionJsonSchema, parseExtraction, EXTRACTION_FIELDS } from "../src/lib/imports/extraction"; import { checkPlausibility, parseDate, toIsoDate, VIOLATION_CONFIDENCE } from "../src/lib/imports/plausibility"; import { computeCorrections, extractionToForm, formFieldMeta, reviewFormSchema } from "../src/lib/imports/review"; -import { normalizeCompany, normalizeStreet } from "../src/server/services/imports/duplicates-stub"; -import { normalizeFileName, sniffMime } from "../src/server/services/imports/document-store-stub"; +import { normalizeCompanyName as normalizeCompany, normalizeStreet } from "../src/lib/customers/duplicates"; +import { detectMime as sniffMime } from "../src/server/services/documents/scanner"; +import { normalizeFileName } from "../src/server/services/documents/store"; import { buildPdf } from "./make-sample-pdfs"; let failures = 0; @@ -191,7 +192,7 @@ function sample(): WorkOrderExtraction { ok(sniffMime(pdf) === "application/pdf", "(F1) generated sample is a PDF by magic bytes"); ok(sniffMime(Buffer.from([0xff, 0xd8, 0xff, 0xe0])) === "image/jpeg", "(F2) JPEG magic bytes"); ok(sniffMime(Buffer.from("MZ\x90\x00 fake exe", "latin1")) === null, "(F3) executable rejected"); - ok(normalizeFileName("../../etc/pass.pdf") === "pa_ss_wd_.pdf", "(F4) file name without path and control characters"); + ok(normalizeFileName("../../etc/pass.pdf") === "pass_wd_.pdf", "(F4) file name without path, control characters removed, reserved characters replaced"); const raw = pdf.toString("latin1"); ok(raw.includes("(Auftragsbest\\344tigung Gr\\366\\337e 5 \\200)") && raw.trimEnd().endsWith("%%EOF"), "(F5) PDF writer encodes umlauts/€ as WinAnsi octal escapes and closes the file"); } diff --git a/src/app/(field)/m/(core)/orders/[id]/page.tsx b/src/app/(field)/m/(core)/orders/[id]/page.tsx index f8d60a5..2e54d39 100644 --- a/src/app/(field)/m/(core)/orders/[id]/page.tsx +++ b/src/app/(field)/m/(core)/orders/[id]/page.tsx @@ -271,8 +271,8 @@ export default async function OrderDetailPage({ params }: { params: Promise<{ id {h.workOrderNumber} · {h.workOrderTitle} - {h.pdfDocumentId && ( - + {h.pdfHref && ( + {t("detail.openDocument")} )} diff --git a/src/components/work-orders/detail-tabs.tsx b/src/components/work-orders/detail-tabs.tsx index 38276a2..86de23b 100644 --- a/src/components/work-orders/detail-tabs.tsx +++ b/src/components/work-orders/detail-tabs.tsx @@ -339,9 +339,9 @@ export async function PhotosTab({ ctx, wo, locale, tz }: TabProps) {