Ship the installability bits that t-paliad-041 deferred so iOS / Android users can add Paliad to their home screen. What landed: - frontend/public/manifest.json — name=Paliad, theme_color #65a30d (lime), display=standalone, scope=/, start_url=/dashboard, four icon entries (192/512 × any/maskable). Served from /manifest.json with the spec-mandated application/manifest+json content type (servePWAManifest in internal/handlers/pwa.go). - frontend/public/icons/ — lime "p" logo rendered to 192/512 PNGs in both "any" and maskable variants (maskable variant has extra safe-zone padding), 180×180 apple-touch-icon, 32×32 favicon. SVG sources kept under frontend/icons-src/ for regeneration via rsvg-convert. - frontend/public/sw.js — minimal cache-first for /assets/* and /icons/*, network-first for /api/*, network passthrough for everything else. CACHE_VERSION + activate-clean lets us bump and purge cleanly. Served from /sw.js so its scope can claim /; Service-Worker-Allowed: / header set, no-cache on the SW file itself so updates take effect on next load. - frontend/src/components/PWAHead.tsx — head fragment (manifest link, apple-touch-icon, favicon, app-name metas, <script src="/assets/app.js" defer>). Added to all 30 page TSX files via mechanical insertion. - frontend/src/client/app.ts — universal client bundle loaded on every page. Three jobs: register the service worker, init the BottomNav (icarus flagged that bottom-nav.ts was written but never wired into the build — m reproduced the broken [+] Anlegen and Menü buttons in prod), and surface the install banner. - frontend/src/client/pwa-install.ts — install banner UI. Two flows: beforeinstallprompt for Chromium/Android (deferred → CTA → prompt), one-time iOS Safari hint pointing at the share sheet. Both dismissals persist in localStorage (paliad-install-dismissed / -ios-shown). - frontend/src/styles/global.css — banner styles, sits above BottomNav on mobile and pinned bottom-right on desktop, lime-on-white card with the brand "p" mark. - frontend/build.ts — copies frontend/public → dist verbatim so the manifest, icons, and SW land at the application root. Verification before merge: - bun run build clean, go build/vet/test clean. - Local server smoke: curl -sI confirmed manifest.json (200, application/manifest+json), all icon files (200, image/png), sw.js (200, Service-Worker-Allowed: /), app.js (200, text/javascript). - Playwright at 390×844: Chrome fired beforeinstallprompt, the banner rendered with "Paliad installieren" + "Installieren" CTA in German, dismiss persisted across reload via localStorage. Manifest validated in-browser (name/short_name/start_url/display/scope all correct, all four icon URLs returned 200). - The InvalidStateError on serviceWorker.register() seen in the MCP Playwright profile is a known headless flag; SW registration works in real Chrome / Safari on localhost and HTTPS production. Out of scope: push notifications, runtime offline mode (SW intentionally stays minimal — cache shell + assets, network passthrough for everything else).
368 lines
22 KiB
TypeScript
368 lines
22 KiB
TypeScript
import { h } from "./jsx";
|
|
import { Sidebar } from "./components/Sidebar";
|
|
import { BottomNav } from "./components/BottomNav";
|
|
import { Footer } from "./components/Footer";
|
|
import { PWAHead } from "./components/PWAHead";
|
|
|
|
// Project detail shell (v2). DOM IDs use the English `project-*` /
|
|
// `parties-*` / `deadlines-*` / `appointments-*` / `notes-*` / `checklists-*`
|
|
// naming. The client TS in client/projects-detail.ts queries these IDs in
|
|
// lockstep — keep names in sync.
|
|
export function renderProjectsDetail(): string {
|
|
return "<!DOCTYPE html>" + (
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
<meta name="theme-color" content="#65a30d" />
|
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
|
<PWAHead />
|
|
<title data-i18n="projekte.detail.title">Projekt — Paliad</title>
|
|
<link rel="stylesheet" href="/assets/global.css" />
|
|
</head>
|
|
<body className="has-sidebar">
|
|
<Sidebar currentPath="/projects" />
|
|
<BottomNav currentPath="/projects" />
|
|
|
|
<main>
|
|
<section className="tool-page">
|
|
<div className="container">
|
|
<a href="/projects" className="akten-back-link" data-i18n="projekte.detail.back">← Zurück zur Übersicht</a>
|
|
|
|
<nav className="projekt-breadcrumb" id="project-breadcrumb" aria-label="Breadcrumb" />
|
|
|
|
<div id="project-detail-loading" className="akten-loading">
|
|
<p data-i18n="projekte.detail.loading">Lädt…</p>
|
|
</div>
|
|
|
|
<div id="project-detail-notfound" className="akten-empty" style="display:none">
|
|
<p data-i18n="projekte.detail.notfound">Projekt nicht gefunden oder keine Berechtigung.</p>
|
|
</div>
|
|
|
|
<div id="project-detail-body" style="display:none">
|
|
<header className="akten-detail-header">
|
|
<div className="akten-detail-title-row">
|
|
<div className="akten-detail-title-col">
|
|
<h1 id="project-title-display" />
|
|
<input type="text" id="project-title-edit" className="akten-title-input" style="display:none" />
|
|
<div className="akten-detail-meta">
|
|
<span id="project-type-chip" className="akten-type-chip" />
|
|
<span className="akten-ref" id="project-ref-display" />
|
|
<span id="project-clientmatter" className="akten-ref" />
|
|
<span id="project-status-chip" className="akten-status-chip" />
|
|
<a id="project-netdocs" className="akten-netdocs-link" target="_blank" rel="noopener" style="display:none">netDocuments ↗</a>
|
|
</div>
|
|
</div>
|
|
<div className="akten-detail-actions">
|
|
<button id="project-edit-btn" className="btn-icon" type="button" aria-label="Bearbeiten" data-i18n-title="projekte.detail.edit" title="Bearbeiten">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
|
</svg>
|
|
</button>
|
|
<button id="project-save-btn" className="btn-primary btn-cta-lime" type="button" style="display:none" data-i18n="projekte.detail.save">Speichern</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="akten-detail-description">
|
|
<h3 data-i18n="projekte.detail.description.heading">Notizen</h3>
|
|
<p id="project-description-display" className="akten-detail-description-text" />
|
|
<textarea id="project-description-edit" className="akten-detail-description-input" rows={4} style="display:none" />
|
|
</div>
|
|
|
|
<nav className="akten-tabs" id="project-tabs">
|
|
<a className="akten-tab" data-tab="history" href="#" data-i18n="projekte.detail.tab.verlauf">Verlauf</a>
|
|
<a className="akten-tab" data-tab="team" href="#" data-i18n="projekte.detail.tab.team">Team</a>
|
|
<a className="akten-tab" data-tab="children" href="#" data-i18n="projekte.detail.tab.kinder">Untergeordnet</a>
|
|
<a className="akten-tab" data-tab="parties" href="#" data-i18n="projekte.detail.tab.parteien">Parteien</a>
|
|
<a className="akten-tab" data-tab="deadlines" href="#" data-i18n="projekte.detail.tab.fristen">Fristen</a>
|
|
<a className="akten-tab" data-tab="appointments" href="#" data-i18n="projekte.detail.tab.termine">Termine</a>
|
|
<a className="akten-tab" data-tab="notes" href="#" data-i18n="projekte.detail.tab.notizen">Notizen</a>
|
|
<a className="akten-tab" data-tab="checklists" href="#" data-i18n="projekte.detail.tab.checklisten">Checklisten</a>
|
|
</nav>
|
|
|
|
{/* History (Verlauf) */}
|
|
<section className="akten-tab-panel" id="tab-history">
|
|
<ul className="akten-events" id="project-events-list" />
|
|
<p className="akten-events-empty" id="project-events-empty" style="display:none" data-i18n="projekte.detail.verlauf.empty">
|
|
Noch keine Ereignisse aufgezeichnet.
|
|
</p>
|
|
<div className="akten-events-loadmore" id="project-events-loadmore-wrap" style="display:none">
|
|
<button type="button" className="btn-secondary" id="project-events-loadmore" data-i18n="projekte.detail.verlauf.loadMore">
|
|
Mehr laden
|
|
</button>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Team */}
|
|
<section className="akten-tab-panel" id="tab-team" style="display:none">
|
|
<div className="akten-parteien-controls">
|
|
<button id="team-add-btn" className="btn-primary btn-cta-lime btn-small" type="button" data-i18n="projekte.detail.team.add">
|
|
Mitglied hinzufügen
|
|
</button>
|
|
</div>
|
|
|
|
<form id="team-form" className="akten-form akten-partei-form" style="display:none" autocomplete="off">
|
|
<div className="form-field-row">
|
|
<div className="form-field">
|
|
<label htmlFor="team-user-input" data-i18n="projekte.detail.team.form.user">Benutzer</label>
|
|
<input type="text" id="team-user-input" placeholder="Name oder E-Mail..." autocomplete="off" />
|
|
<input type="hidden" id="team-user-id" />
|
|
<div id="team-user-suggestions" className="akten-collab-suggestions" />
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="team-role" data-i18n="projekte.detail.team.form.role">Rolle</label>
|
|
<select id="team-role">
|
|
<option value="lead" data-i18n="projekte.team.role.lead">Lead</option>
|
|
<option value="associate" selected data-i18n="projekte.team.role.associate">Associate</option>
|
|
<option value="pa" data-i18n="projekte.team.role.pa">PA</option>
|
|
<option value="of_counsel" data-i18n="projekte.team.role.of_counsel">Of Counsel</option>
|
|
<option value="local_counsel" data-i18n="projekte.team.role.local_counsel">Local Counsel</option>
|
|
<option value="expert" data-i18n="projekte.team.role.expert">Experte</option>
|
|
<option value="observer" data-i18n="projekte.team.role.observer">Beobachter</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div className="form-actions">
|
|
<button type="button" className="btn-cancel" id="team-cancel" data-i18n="projekte.detail.team.form.cancel">Abbrechen</button>
|
|
<button type="submit" className="btn-primary btn-cta-lime" data-i18n="projekte.detail.team.form.submit">Hinzufügen</button>
|
|
</div>
|
|
<p className="form-msg" id="team-msg" />
|
|
</form>
|
|
|
|
<table className="akten-parteien-table">
|
|
<thead>
|
|
<tr>
|
|
<th data-i18n="projekte.detail.team.col.name">Name</th>
|
|
<th data-i18n="projekte.detail.team.col.role">Rolle</th>
|
|
<th data-i18n="projekte.detail.team.col.source">Herkunft</th>
|
|
<th />
|
|
</tr>
|
|
</thead>
|
|
<tbody id="team-body" />
|
|
</table>
|
|
|
|
<p className="akten-events-empty" id="team-empty" style="display:none" data-i18n="projekte.detail.team.empty">
|
|
Noch keine Teammitglieder.
|
|
</p>
|
|
</section>
|
|
|
|
{/* Children (Untergeordnet) */}
|
|
<section className="akten-tab-panel" id="tab-children" style="display:none">
|
|
<div className="akten-parteien-controls">
|
|
<a id="child-add-link" className="btn-primary btn-cta-lime btn-small" href="/projects/new" data-i18n="projekte.detail.kinder.add">
|
|
Untervorhaben anlegen
|
|
</a>
|
|
</div>
|
|
<ul id="children-list" className="projekt-children-list" />
|
|
<p className="akten-events-empty" id="children-empty" style="display:none" data-i18n="projekte.detail.kinder.empty">
|
|
Keine untergeordneten Projekte.
|
|
</p>
|
|
</section>
|
|
|
|
{/* Parties (Parteien) */}
|
|
<section className="akten-tab-panel" id="tab-parties" style="display:none">
|
|
<div className="akten-parteien-controls">
|
|
<button id="party-add-btn" className="btn-primary btn-cta-lime btn-small" type="button" data-i18n="projekte.detail.parteien.add">
|
|
Partei hinzufügen
|
|
</button>
|
|
</div>
|
|
|
|
<form id="party-form" className="akten-form akten-partei-form" style="display:none" autocomplete="off">
|
|
<div className="form-field-row">
|
|
<div className="form-field">
|
|
<label htmlFor="party-name" data-i18n="projekte.detail.parteien.form.name">Name</label>
|
|
<input type="text" id="party-name" required />
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="party-role" data-i18n="projekte.detail.parteien.form.role">Rolle</label>
|
|
<select id="party-role">
|
|
<option value="claimant" data-i18n="projekte.detail.parteien.role.claimant">Kläger</option>
|
|
<option value="defendant" data-i18n="projekte.detail.parteien.role.defendant">Beklagter</option>
|
|
<option value="thirdparty" data-i18n="projekte.detail.parteien.role.thirdparty">Streitverkündeter / Drittpartei</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="party-rep" data-i18n="projekte.detail.parteien.form.rep">Vertreter (optional)</label>
|
|
<input type="text" id="party-rep" />
|
|
</div>
|
|
<div className="form-actions">
|
|
<button type="button" className="btn-cancel" id="party-cancel" data-i18n="projekte.detail.parteien.form.cancel">Abbrechen</button>
|
|
<button type="submit" className="btn-primary btn-cta-lime" data-i18n="projekte.detail.parteien.form.submit">Hinzufügen</button>
|
|
</div>
|
|
<p className="form-msg" id="party-msg" />
|
|
</form>
|
|
|
|
<table className="akten-parteien-table">
|
|
<thead>
|
|
<tr>
|
|
<th data-i18n="projekte.detail.parteien.col.name">Name</th>
|
|
<th data-i18n="projekte.detail.parteien.col.role">Rolle</th>
|
|
<th data-i18n="projekte.detail.parteien.col.rep">Vertreter</th>
|
|
<th />
|
|
</tr>
|
|
</thead>
|
|
<tbody id="parties-body" />
|
|
</table>
|
|
|
|
<p className="akten-events-empty" id="parties-empty" style="display:none" data-i18n="projekte.detail.parteien.empty">
|
|
Noch keine Parteien eingetragen.
|
|
</p>
|
|
</section>
|
|
|
|
{/* Deadlines (Fristen) */}
|
|
<section className="akten-tab-panel" id="tab-deadlines" style="display:none">
|
|
<div className="akten-parteien-controls">
|
|
<a id="deadline-add-link" className="btn-primary btn-cta-lime btn-small" data-i18n="projekte.detail.fristen.add" href="#">
|
|
Frist hinzufügen
|
|
</a>
|
|
</div>
|
|
<div className="akten-table-wrap" id="project-deadlines-tablewrap">
|
|
<table className="akten-table fristen-table">
|
|
<thead>
|
|
<tr>
|
|
<th />
|
|
<th data-i18n="fristen.col.due">Fällig</th>
|
|
<th data-i18n="fristen.col.title">Titel</th>
|
|
<th data-i18n="fristen.col.rule">Regel</th>
|
|
<th data-i18n="fristen.col.status">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="project-deadlines-body" />
|
|
</table>
|
|
</div>
|
|
<p className="akten-events-empty" id="project-deadlines-empty" style="display:none" data-i18n="projekte.detail.fristen.empty">
|
|
Für dieses Projekt sind noch keine Fristen erfasst.
|
|
</p>
|
|
</section>
|
|
|
|
{/* Appointments (Termine) */}
|
|
<section className="akten-tab-panel" id="tab-appointments" style="display:none">
|
|
<div className="akten-parteien-controls">
|
|
<button type="button" id="appointment-add-btn" className="btn-primary btn-cta-lime btn-small" data-i18n="projekte.detail.termine.add">
|
|
Termin hinzufügen
|
|
</button>
|
|
</div>
|
|
|
|
<form id="project-appointment-form" className="akten-partei-form" style="display:none">
|
|
<div className="form-field-row">
|
|
<div className="form-field">
|
|
<label htmlFor="project-appointment-title" data-i18n="termine.field.title">Titel</label>
|
|
<input type="text" id="project-appointment-title" required />
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="project-appointment-type" data-i18n="termine.field.type">Typ</label>
|
|
<select id="project-appointment-type">
|
|
<option value="" data-i18n="termine.field.type.none">Kein Typ</option>
|
|
<option value="hearing" data-i18n="termine.type.hearing">Verhandlung</option>
|
|
<option value="meeting" data-i18n="termine.type.meeting">Besprechung</option>
|
|
<option value="consultation" data-i18n="termine.type.consultation">Beratung</option>
|
|
<option value="deadline_hearing" data-i18n="termine.type.deadline_hearing">Fristverhandlung</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div className="form-field-row">
|
|
<div className="form-field">
|
|
<label htmlFor="project-appointment-start" data-i18n="termine.field.start">Beginn</label>
|
|
<input type="datetime-local" id="project-appointment-start" required />
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="project-appointment-end" data-i18n="termine.field.end">Ende (optional)</label>
|
|
<input type="datetime-local" id="project-appointment-end" />
|
|
</div>
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="project-appointment-location" data-i18n="termine.field.location">Ort</label>
|
|
<input type="text" id="project-appointment-location" />
|
|
</div>
|
|
<div className="form-actions">
|
|
<button type="button" className="btn-cancel" id="project-appointment-cancel" data-i18n="projekte.detail.termine.form.cancel">Abbrechen</button>
|
|
<button type="submit" className="btn-primary btn-cta-lime" data-i18n="projekte.detail.termine.form.submit">Hinzufügen</button>
|
|
</div>
|
|
<p className="form-msg" id="project-appointment-msg" />
|
|
</form>
|
|
|
|
<div className="akten-table-wrap" id="project-appointments-tablewrap">
|
|
<table className="akten-table fristen-table">
|
|
<thead>
|
|
<tr>
|
|
<th />
|
|
<th data-i18n="termine.col.start">Beginn</th>
|
|
<th data-i18n="termine.col.title">Titel</th>
|
|
<th data-i18n="termine.col.location">Ort</th>
|
|
<th data-i18n="termine.col.type">Typ</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="project-appointments-body" />
|
|
</table>
|
|
</div>
|
|
<p className="akten-events-empty" id="project-appointments-empty" style="display:none" data-i18n="projekte.detail.termine.empty">
|
|
Für dieses Projekt sind noch keine Termine erfasst.
|
|
</p>
|
|
</section>
|
|
|
|
{/* Notes (Notizen) */}
|
|
<section className="akten-tab-panel" id="tab-notes" style="display:none">
|
|
<div id="notes-container" className="notiz-container" data-parent-type="project" />
|
|
</section>
|
|
|
|
{/* Checklists (Checklisten) */}
|
|
<section className="akten-tab-panel" id="tab-checklists" style="display:none">
|
|
<p id="project-checklists-empty" className="akten-events-empty" style="display:none" data-i18n="projekte.detail.checklisten.empty">
|
|
Für dieses Projekt sind noch keine Checklisten-Instanzen erfasst.
|
|
</p>
|
|
<div className="akten-table-wrap" id="project-checklists-tablewrap" style="display:none">
|
|
<table className="akten-table">
|
|
<thead>
|
|
<tr>
|
|
<th data-i18n="projekte.detail.checklisten.col.template">Vorlage</th>
|
|
<th data-i18n="projekte.detail.checklisten.col.name">Name</th>
|
|
<th data-i18n="projekte.detail.checklisten.col.progress">Fortschritt</th>
|
|
<th data-i18n="projekte.detail.checklisten.col.created">Angelegt</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="project-checklists-body" />
|
|
</table>
|
|
</div>
|
|
<p className="tool-subtitle akten-checklisten-hint" data-i18n="projekte.detail.checklisten.hint">
|
|
Instanzen werden auf der Vorlagen-Seite unter <a href="/checklists">Checklisten</a> angelegt.
|
|
</p>
|
|
</section>
|
|
|
|
<div className="akten-detail-footer" id="project-delete-wrap" style="display:none">
|
|
<button id="project-delete-btn" className="btn-danger" type="button" data-i18n="projekte.detail.delete">
|
|
Projekt archivieren
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Delete confirmation modal */}
|
|
<div className="modal-overlay" id="delete-modal" style="display:none">
|
|
<div className="modal-card">
|
|
<div className="modal-header">
|
|
<h2 data-i18n="projekte.detail.delete.confirm.title">Projekt wirklich archivieren?</h2>
|
|
<button className="modal-close" id="delete-modal-close" type="button">×</button>
|
|
</div>
|
|
<p data-i18n="projekte.detail.delete.confirm.body">
|
|
Das Projekt wird archiviert. Es kann nicht direkt wiederhergestellt werden.
|
|
</p>
|
|
<div className="form-actions">
|
|
<button type="button" className="btn-cancel" id="delete-modal-cancel" data-i18n="projekte.detail.delete.confirm.cancel">Abbrechen</button>
|
|
<button type="button" className="btn-danger" id="delete-modal-confirm" data-i18n="projekte.detail.delete.confirm.ok">Archivieren</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<Footer />
|
|
<script src="/assets/projects-detail.js"></script>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|