Files
paliad/frontend/src/client/projects-flat.ts
m a5f7b5009b feat(t-paliad-149) PR1 step 2/3: frontend rewrite — chips + pin star + last-view restore
frontend/src/projects.tsx — strip the legacy 3-select toolbar; replace with
search input + view-mode segment-control (Tree | Liste) + chip filter row
(Alle / Nur meine / Angepinnt / Status / Typ / Mit aktiven Fristen). Tree
container is the default visible mount; flat-table hidden until view mode
toggles.

frontend/src/client/projects.ts — orchestrator. Owns chip + search + view-
mode state. Last-viewed restore from sessionStorage (Q1 lock-in), URL params
override on load, syncURL on every state change. Debounced search (250ms).
Multi-select panels via <details> for status/type. Delegates rendering to
project-tree.ts (tree mode) or projects-flat.ts (flat mode).

frontend/src/client/projects-flat.ts (NEW) — extracted table render from the
old projects.ts so the orchestrator can mount/unmount cleanly.

frontend/src/client/project-tree.ts — extends ProjectTreeNode shape with
pinned, inherited_visibility, match_kind, *_subtree fields. Renders pin
star button (always-visible per design §4.6 — touch-friendly), greyed-
ancestor opacity for InheritedVisibility=true, lime backdrop on
match_kind=self. Pin click does optimistic toggle + POST/DELETE
/api/projects/{id}/pin then invalidates the tree cache.

frontend/src/styles/global.css — toolbar + chips + pin star + greyed-
ancestor + match highlighting. ~200 LoC appended.

frontend/src/client/i18n.ts — 29 new keys DE+EN under projects.toolbar.*,
projects.chip.*, projects.tree.deadlines.*, projects.tree.pin/unpin,
projects.search.match.*, projects.empty.filtered.action.

internal/services/pin_service_test.go (NEW) — live-DB tests for PinService
(pin/unpin/idempotent/owner-scope/visibility-gate) + 2 BuildTreeWithOptions
cases (PinnedSet surfaces, ScopeMine greys ancestors). Skips without
TEST_DATABASE_URL; pure-Go path runs clean.

Frontend bun build clean. go build / vet / test (short) clean.
2026-05-07 22:29:39 +02:00

82 lines
2.8 KiB
TypeScript

import { tDyn, getLang } from "./i18n";
// Flat-list (table) rendering for /projects.
// Extracted from the pre-t-paliad-149 client/projects.ts so the orchestrator
// can mount/unmount table view alongside the tree view without code duplication.
export interface ProjectFlatRow {
id: string;
type: string;
parent_id?: string | null;
path: string;
title: string;
reference?: string | null;
status: string;
client_number?: string | null;
matter_number?: string | null;
updated_at: string;
}
interface RenderOpts {
rows: ProjectFlatRow[];
}
// renderFlatList writes the table rows + wires row-click navigation.
// Caller is responsible for showing/hiding the wrapping table element.
export function renderFlatList(opts: RenderOpts) {
const tbody = document.getElementById("projects-body")!;
tbody.innerHTML = opts.rows
.map((p) => {
const typeLabel = tDyn(`projects.type.${p.type}`) || p.type;
const statusLabel = tDyn(`projects.filter.status.${p.status}`) || p.status;
const clientMatter =
p.client_number && p.matter_number
? `${p.client_number}.${p.matter_number}`
: p.client_number || p.matter_number || "";
const refCell = p.reference ? esc(p.reference) : "&mdash;";
const clientMatterCell = clientMatter ? esc(clientMatter) : "&mdash;";
return `<tr class="entity-row" data-id="${esc(p.id)}">
<td class="entity-col-title">${esc(p.title)}</td>
<td><span class="entity-type-chip entity-type-${esc(p.type)}">${esc(typeLabel)}</span></td>
<td class="entity-col-ref">${refCell}</td>
<td class="entity-col-ref">${clientMatterCell}</td>
<td class="entity-col-status"><span class="entity-status-chip entity-status-${esc(p.status)}">${esc(statusLabel)}</span></td>
<td class="entity-col-updated">${fmtDate(p.updated_at)}</td>
</tr>`;
})
.join("");
// F-23: when every visible row shares the same status, hide the column to
// cut redundant noise. The toggle re-runs on every filter change, so the
// column comes back as soon as the rows mix again.
const statusUnique = new Set(opts.rows.map((p) => p.status)).size;
const table = document.getElementById("entity-table");
table?.classList.toggle("entity-table--hide-status", statusUnique <= 1);
tbody.querySelectorAll<HTMLTableRowElement>(".entity-row").forEach((row) => {
row.addEventListener("click", () => {
const id = row.dataset.id!;
window.location.href = `/projects/${id}`;
});
});
}
function fmtDate(iso: string): string {
try {
const d = new Date(iso);
return d.toLocaleDateString(getLang() === "de" ? "de-DE" : "en-GB", {
year: "numeric",
month: "2-digit",
day: "2-digit",
});
} catch {
return iso;
}
}
function esc(s: string): string {
const d = document.createElement("div");
d.textContent = s;
return d.innerHTML;
}