Files
paliad/frontend/src/checklists-detail.tsx
mAi a93277a072 feat(checklists): t-paliad-225 Slice B frontend — share modal + admin promote/demote on detail page
m/paliad#61 Slice B frontend pass.

Detail page (/checklists/{slug}) gains:
- Provenance line ("Erstellt von <author>") for authored templates,
  populated from the catalog response's owner_display_name.
- Owner action buttons: Bearbeiten (links to
  /checklists/templates/{slug}/edit per the Slice A hotfix), Teilen,
  Löschen. Reveal driven by /api/me email match against the catalog
  response's owner_email.
- global_admin action buttons: "Als Firmen-Vorlage hinterlegen"
  (promote) when visibility != 'global'; "Aus Katalog entfernen"
  (demote) when visibility == 'global'. Reveal driven by /api/me
  global_role.

Share modal:
- Single modal with a kind-picker (Kollege / Office / Dezernat /
  Projekt) and a matching select per kind — sections toggle on the
  active kind.
- Recipient pickers populated from /api/users, /api/partner-units,
  /api/projects (loaded in parallel on open). Office options use the
  canonical 8-key set from internal/offices.
- Existing grants surface in a list under the form with per-row
  Entfernen buttons; Revoke confirms before DELETE.
- Errors surface inline (recipient-required, generic share failure).

i18n: 32 new keys per language (DE+EN) under checklisten.share.*
and checklisten.detail.promote/demote/delete.*. Total 2653 keys.

Build hygiene: go build/vet/test ./internal/... + ./cmd/server/ all
green; bun run build clean.
2026-05-20 15:38:43 +02:00

237 lines
14 KiB
TypeScript

import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { PaliadinWidget } from "./components/PaliadinWidget";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
import { PWAHead } from "./components/PWAHead";
// Template detail page. Shows template metadata + list of existing
// instances + CTA to create a new instance. Clicking an instance takes
// the user to /checklisten/instances/{id} where the interactive
// checkboxes live.
export function renderChecklistsDetail(): 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="#BFF355" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<PWAHead />
<title data-i18n="checklisten.title">Checkliste &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/checklists" />
<BottomNav currentPath="/checklists" />
<main>
<section className="tool-page">
<div className="container">
<a href="/checklists" className="checklist-back">
<span className="checklist-back-arrow">&larr;</span>
<span data-i18n="checklisten.back">Zur&uuml;ck zur &Uuml;bersicht</span>
</a>
<div className="tool-header checklist-detail-header">
<div className="checklist-detail-head-row">
<div>
<h1 id="checklist-title">&nbsp;</h1>
<p className="tool-subtitle" id="checklist-subtitle">&nbsp;</p>
{/* Provenance line — visible only for authored
templates; populated by the client from the
catalog response's owner_display_name. */}
<p className="checklist-provenance" id="checklist-provenance" style="display:none" />
<dl className="checklist-meta" id="checklist-meta" />
</div>
<div className="checklist-actions">
<button type="button" id="btn-new-instance" className="btn-primary btn-cta-lime" data-i18n="checklisten.newInstance">
Neue Instanz
</button>
{/* Owner controls (Slice B) — toggled on by the
client once /api/checklists/{slug} returns
origin='authored' AND owner_email matches the
logged-in user. Kept hidden by default so
guests / non-owners never see them. */}
<a id="btn-edit-template" className="btn-cta-lime btn-outline" style="display:none" data-i18n="checklisten.detail.edit">Bearbeiten</a>
<button type="button" id="btn-share-template" className="btn-cta-lime btn-outline" style="display:none" data-i18n="checklisten.detail.share">Teilen</button>
<button type="button" id="btn-delete-template" className="btn-cta-lime btn-outline" style="display:none" data-i18n="checklisten.mine.delete">L&ouml;schen</button>
{/* global_admin controls — revealed by the client
when /api/me reports global_role='global_admin'. */}
<button type="button" id="btn-promote-template" className="btn-cta-lime btn-outline" style="display:none" data-i18n="checklisten.detail.promote">Als Firmen-Vorlage hinterlegen</button>
<button type="button" id="btn-demote-template" className="btn-cta-lime btn-outline" style="display:none" data-i18n="checklisten.detail.demote">Aus Katalog entfernen</button>
<button type="button" id="btn-feedback" className="btn-cta-lime btn-outline">
<span data-i18n="checklisten.feedback.btn">Feedback</span>
</button>
</div>
</div>
</div>
<section className="checklist-instances-section">
<h2 data-i18n="checklisten.instances.heading">Instanzen</h2>
<p className="tool-subtitle" data-i18n="checklisten.instances.sub">
Jede Instanz hat ihren eigenen Fortschritt und kann optional an eine Akte geh&auml;ngt werden.
</p>
<div id="instances-loading" className="entity-loading">
<p data-i18n="checklisten.instances.loading">L&auml;dt&hellip;</p>
</div>
<div id="instances-empty" className="entity-events-empty" style="display:none">
<p data-i18n="checklisten.instances.empty">
Noch keine Instanzen. Klicken Sie auf &bdquo;Neue Instanz&ldquo;, um zu beginnen.
</p>
</div>
<div className="entity-table-wrap" id="instances-tablewrap" style="display:none">
<table className="entity-table">
<thead>
<tr>
<th data-i18n="checklisten.instances.col.name">Name</th>
<th data-i18n="checklisten.instances.col.progress">Fortschritt</th>
<th data-i18n="checklisten.instances.col.akte">Akte</th>
<th data-i18n="checklisten.instances.col.created">Angelegt</th>
<th />
</tr>
</thead>
<tbody id="instances-body" />
</table>
</div>
</section>
<div className="checklist-print-footer">
<p className="checklist-disclaimer" data-i18n="checklisten.disclaimer">
Hinweis: Diese Checklisten dienen als Ged&auml;chtnisst&uuml;tze und ersetzen keine Pr&uuml;fung im Einzelfall. Ma&szlig;geblich sind die jeweils geltenden Verfahrensregeln.
</p>
</div>
</div>
</section>
</main>
{/* Neue Instanz modal */}
<div className="modal-overlay" id="new-instance-modal" style="display:none">
<div className="modal-card">
<div className="modal-header">
<h2 data-i18n="checklisten.newInstance.title">Neue Checklisten-Instanz</h2>
<button className="modal-close" id="new-instance-close" type="button">&times;</button>
</div>
<form id="new-instance-form" autocomplete="off">
<div className="form-field">
<label htmlFor="new-instance-name" data-i18n="checklisten.newInstance.name">Name</label>
<input type="text" id="new-instance-name" required maxLength={200} />
<p className="form-hint" data-i18n="checklisten.newInstance.name.hint">z.B. &bdquo;M&uuml;ller v. Schmidt &mdash; SoC&ldquo;.</p>
</div>
<div className="form-field">
<label htmlFor="new-instance-project" data-i18n="checklisten.newInstance.akte">Akte (optional)</label>
<select id="new-instance-project">
<option value="" data-i18n="checklisten.newInstance.akte.none">&mdash; keine Akte &mdash;</option>
</select>
<p className="form-hint" data-i18n="checklisten.newInstance.akte.hint">Wenn verkn&uuml;pft, sehen B&uuml;rokollegen die Instanz.</p>
</div>
<div className="form-actions">
<button type="button" className="btn-cancel" id="new-instance-cancel" data-i18n="checklisten.newInstance.cancel">Abbrechen</button>
<button type="submit" className="btn-primary btn-cta-lime" data-i18n="checklisten.newInstance.submit">Anlegen</button>
</div>
<p className="form-msg" id="new-instance-msg" />
</form>
</div>
</div>
{/* Share modal (Slice B) — owner-only, hidden until btn-share-template
opens it. Four recipient kinds in a single modal: pick the kind,
then the matching entity (user / office / partner_unit / project). */}
<div className="modal-overlay" id="share-modal" style="display:none">
<div className="modal-card modal-card-wide">
<div className="modal-header">
<h2 data-i18n="checklisten.share.title">Vorlage teilen</h2>
<button className="modal-close" id="share-close" type="button">&times;</button>
</div>
<div className="form-field">
<label data-i18n="checklisten.share.kind">Empf&auml;ngertyp</label>
<div className="filter-pills" id="share-kind-pills">
<button type="button" className="filter-pill active" data-kind="user" data-i18n="checklisten.share.kind.user">Kollege</button>
<button type="button" className="filter-pill" data-kind="office" data-i18n="checklisten.share.kind.office">Office</button>
<button type="button" className="filter-pill" data-kind="partner_unit" data-i18n="checklisten.share.kind.partner_unit">Dezernat</button>
<button type="button" className="filter-pill" data-kind="project" data-i18n="checklisten.share.kind.project">Projekt</button>
</div>
</div>
<div className="form-field share-kind-section" data-kind="user">
<label htmlFor="share-user" data-i18n="checklisten.share.kind.user">Kollege</label>
<select id="share-user">
<option value="" data-i18n="checklisten.share.pick">&mdash; ausw&auml;hlen &mdash;</option>
</select>
</div>
<div className="form-field share-kind-section" data-kind="office" style="display:none">
<label htmlFor="share-office" data-i18n="checklisten.share.kind.office">Office</label>
<select id="share-office">
<option value="" data-i18n="checklisten.share.pick">&mdash; ausw&auml;hlen &mdash;</option>
</select>
</div>
<div className="form-field share-kind-section" data-kind="partner_unit" style="display:none">
<label htmlFor="share-partner-unit" data-i18n="checklisten.share.kind.partner_unit">Dezernat</label>
<select id="share-partner-unit">
<option value="" data-i18n="checklisten.share.pick">&mdash; ausw&auml;hlen &mdash;</option>
</select>
</div>
<div className="form-field share-kind-section" data-kind="project" style="display:none">
<label htmlFor="share-project" data-i18n="checklisten.share.kind.project">Projekt</label>
<select id="share-project">
<option value="" data-i18n="checklisten.share.pick">&mdash; ausw&auml;hlen &mdash;</option>
</select>
</div>
<div className="form-actions">
<button type="button" className="btn-cancel" id="share-cancel" data-i18n="checklisten.share.cancel">Abbrechen</button>
<button type="button" className="btn-primary btn-cta-lime" id="share-submit" data-i18n="checklisten.share.submit">Freigeben</button>
</div>
<p className="form-msg" id="share-msg" />
{/* Existing grants — populated on open from
/api/checklists/templates/{slug}/shares. */}
<h3 className="share-grants-heading" data-i18n="checklisten.share.grants.heading">Bestehende Freigaben</h3>
<ul className="share-grants-list" id="share-grants-list">
<li className="entity-events-empty" id="share-grants-empty" data-i18n="checklisten.share.grants.empty">Keine Freigaben.</li>
</ul>
</div>
</div>
{/* Feedback modal */}
<div className="modal-overlay" id="feedback-modal" style="display:none">
<div className="modal-card">
<div className="modal-header">
<h2 data-i18n="checklisten.feedback.title">Feedback zur Checkliste</h2>
<button className="modal-close" id="modal-close" type="button">&times;</button>
</div>
<form id="feedback-form">
<div className="form-field">
<label htmlFor="feedback-type" data-i18n="checklisten.feedback.type">Art</label>
<select id="feedback-type" required>
<option value="error" data-i18n="checklisten.feedback.error">Fehler gefunden</option>
<option value="missing" data-i18n="checklisten.feedback.missing">Fehlender Punkt</option>
<option value="suggestion" data-i18n="checklisten.feedback.suggestion">Verbesserungsvorschlag</option>
<option value="other" data-i18n="checklisten.feedback.other">Sonstiges</option>
</select>
</div>
<div className="form-field">
<label htmlFor="feedback-message" data-i18n="checklisten.feedback.message">Nachricht</label>
<textarea id="feedback-message" rows={4} required />
</div>
<div className="form-actions">
<button type="button" className="btn-cancel" id="modal-cancel" data-i18n="checklisten.feedback.cancel">Abbrechen</button>
<button type="submit" className="btn-submit" data-i18n="checklisten.feedback.submit">Absenden</button>
</div>
<p className="form-msg" id="feedback-msg" />
</form>
</div>
</div>
<Footer />
<PaliadinWidget />
<script src="/assets/checklists-detail.js"></script>
</body>
</html>
);
}