From 59cf47b5ed181aa3add270f7aeab184639f71827 Mon Sep 17 00:00:00 2001 From: m Date: Mon, 27 Apr 2026 13:37:56 +0200 Subject: [PATCH] feat(projects): full edit modal + breadcrumb polish + tab toolbar buttons (t-paliad-049) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Edit pencil on /projects/{id} now opens a modal with the same form as /projects/new, pre-filled from the project. Type and parent are intentionally read-only — re-typing/reparenting are structural ops not exposed via PATCH today. - Form body extracted into + shared client/project-form.ts so create and edit share the same fields, visibility logic, parent picker, and payload builder. - Inline title/description edit removed; one edit path is clearer than two. - Breadcrumb rewritten as pill chips with type icons (matching the project tree), chevron separators, hover lime accent, ellipsis truncation, and horizontal-scroll fallback on mobile. - Tab toolbar action buttons standardised — same height, padding, font weight across Verlauf/Team/Untergeordnet/Parteien/Fristen/Termine plus the "Mehr laden" secondary so they no longer drift visually. --- frontend/src/client/i18n.ts | 4 + frontend/src/client/project-form.ts | 225 ++++++++++++++++ frontend/src/client/projects-detail.ts | 240 +++++++++++++----- frontend/src/client/projects-new.ts | 146 ++--------- frontend/src/components/ProjectFormFields.tsx | 169 ++++++++++++ frontend/src/projects-detail.tsx | 26 +- frontend/src/projects-new.tsx | 150 +---------- frontend/src/styles/global.css | 133 ++++++++++ 8 files changed, 756 insertions(+), 337 deletions(-) create mode 100644 frontend/src/client/project-form.ts create mode 100644 frontend/src/components/ProjectFormFields.tsx diff --git a/frontend/src/client/i18n.ts b/frontend/src/client/i18n.ts index b764daf..10e76e8 100644 --- a/frontend/src/client/i18n.ts +++ b/frontend/src/client/i18n.ts @@ -874,6 +874,7 @@ const translations: Record> = { "projekte.field.client_number": "Client-Nr. (7 Ziffern)", "projekte.field.matter_number": "Matter-Nr. (7 Ziffern)", "projekte.field.clientmatter.hint": "HLC-Billing-Nummern. Format CCCCCCC.MMMMMMM. Client-Nr. wird an Unterprojekte vererbt (\u00fcberschreibbar).", + "projekte.field.billing_reference": "Billing-Referenz (optional)", "projekte.field.netdocuments_url": "netDocuments-URL (optional)", "projekte.field.industry": "Branche", "projekte.field.country": "Land (ISO-2)", @@ -892,6 +893,7 @@ const translations: Record> = { "projekte.detail.loading": "L\u00e4dt\u2026", "projekte.detail.notfound": "Projekt nicht gefunden oder keine Berechtigung.", "projekte.detail.edit": "Bearbeiten", + "projekte.detail.edit.modal.title": "Projekt bearbeiten", "projekte.detail.save": "Speichern", "projekte.detail.tab.verlauf": "Verlauf", "projekte.detail.tab.team": "Team", @@ -2047,6 +2049,7 @@ const translations: Record> = { "projekte.field.client_number": "Client no. (7 digits)", "projekte.field.matter_number": "Matter no. (7 digits)", "projekte.field.clientmatter.hint": "HLC billing numbers. Format CCCCCCC.MMMMMMM. Client no. is inherited by sub-projects (overridable).", + "projekte.field.billing_reference": "Billing reference (optional)", "projekte.field.netdocuments_url": "netDocuments URL (optional)", "projekte.field.industry": "Industry", "projekte.field.country": "Country (ISO-2)", @@ -2065,6 +2068,7 @@ const translations: Record> = { "projekte.detail.loading": "Loading\u2026", "projekte.detail.notfound": "Project not found or no access.", "projekte.detail.edit": "Edit", + "projekte.detail.edit.modal.title": "Edit project", "projekte.detail.save": "Save", "projekte.detail.tab.verlauf": "Activity", "projekte.detail.tab.team": "Team", diff --git a/frontend/src/client/project-form.ts b/frontend/src/client/project-form.ts new file mode 100644 index 0000000..2320aff --- /dev/null +++ b/frontend/src/client/project-form.ts @@ -0,0 +1,225 @@ +import { t } from "./i18n"; + +// Shared logic for the Project form rendered by ProjectFormFields.tsx. +// Used by /projects/new and the edit modal on /projects/{id}. + +export interface ProjectMini { + id: string; + title: string; + type: string; + reference?: string | null; +} + +export interface ProjectFormState { + type: string; + parentID: string; + parentTitle: string; + title: string; + reference: string; + description: string; + status: string; + clientNumber: string; + matterNumber: string; + billingReference: string; + netDocumentsURL: string; + industry: string; + country: string; + patentNumber: string; + filingDate: string; + grantDate: string; + court: string; + caseNumber: string; +} + +let parentCandidates: ProjectMini[] = []; + +function $(id: string): HTMLElement { + const el = document.getElementById(id); + if (!el) throw new Error("missing form element: " + id); + return el; +} + +function tryGet(id: string): HTMLElement | null { + return document.getElementById(id); +} + +// showFieldsForType toggles parent-picker + type-specific blocks. +export function showFieldsForType(typeSel: string) { + const parentWrap = tryGet("projekt-parent-wrap") as HTMLDivElement | null; + const clientFields = tryGet("fields-client") as HTMLDivElement | null; + const patentFields = tryGet("fields-patent") as HTMLDivElement | null; + const caseFields = tryGet("fields-case") as HTMLDivElement | null; + if (clientFields) clientFields.style.display = typeSel === "client" ? "block" : "none"; + if (patentFields) patentFields.style.display = typeSel === "patent" ? "block" : "none"; + if (caseFields) caseFields.style.display = typeSel === "case" ? "block" : "none"; + if (parentWrap) parentWrap.style.display = typeSel === "client" ? "none" : "block"; +} + +export async function loadParentCandidates(excludeID?: string) { + try { + const resp = await fetch("/api/projects"); + if (!resp.ok) return; + const all = (await resp.json()) as ProjectMini[]; + parentCandidates = excludeID ? all.filter((p) => p.id !== excludeID) : all; + } catch { + /* network — leave empty */ + } +} + +function esc(s: string): string { + const d = document.createElement("div"); + d.textContent = s; + return d.innerHTML; +} + +export function initParentPicker() { + const input = tryGet("projekt-parent-input") as HTMLInputElement | null; + const hidden = tryGet("projekt-parent-id") as HTMLInputElement | null; + const sugs = tryGet("projekt-parent-suggestions") as HTMLDivElement | null; + if (!input || !hidden || !sugs) return; + + input.addEventListener("input", () => { + const q = input.value.trim().toLowerCase(); + hidden.value = ""; + if (!q) { + sugs.innerHTML = ""; + return; + } + const matches = parentCandidates + .filter((p) => { + const hay = (p.title + " " + (p.reference || "")).toLowerCase(); + return hay.includes(q); + }) + .slice(0, 8); + sugs.innerHTML = matches + .map( + (p) => + `
+ ${esc(p.title)} + ${esc(t("projekte.type." + p.type) || p.type)} +
`, + ) + .join(""); + sugs.querySelectorAll(".akten-collab-suggestion").forEach((el) => { + el.addEventListener("click", () => { + hidden.value = el.dataset.id!; + input.value = el.dataset.title!; + sugs.innerHTML = ""; + }); + }); + }); +} + +// wireTypeChange wires the + + + + + + + + +