The "Composer actually works" milestone per the design at
docs/design-submission-generator-v2-2026-05-26.md §12 Slice B. Builds on
Slice A's substrate (submission_bases, submission_sections, base_id on
drafts); no new migrations needed.
Backend additions:
- internal/services/submission_md.go (~240 LoC): Markdown → OOXML
walker. Per the head's Slice B brief, scope is paragraphs +
bold/italic + blank-line spacing. Placeholders pass through
unchanged for the v1 substitution pass. CRLF normalisation; nested
formatting (***bold-italic***); two delimiter forms (* and _);
XML-escaping for &/</>; explicit empty-paragraph emit so blank
lines round-trip. 12 unit tests.
- internal/services/submission_compose.go (~470 LoC): SubmissionComposer
service. Pipeline: ConvertDotmToDocx pre-pass → extract
word/document.xml → render each included section's content_md_<lang>
→ splice via {{#section:KEY}}/{{/section:KEY}} anchor pairs in
the body → strip anchors for excluded sections → append unanchored
sections before <w:sectPr> → repack zip → run v1 placeholder pass.
RE2-friendly anchor scanner walks markers in body-order and matches
open/close pairs with a stack (handles unbalanced anchors
defensively). 6 unit tests covering anchor-mode splice,
append-mode-no-anchors, excluded-section drop, placeholder
resolution, lang column pick, order_index ASC.
- internal/services/submission_section_service.go: SectionPatch +
Update method. Six optional fields (content_md_de/en, included,
label_de/en, order_index). Sentinel ErrSubmissionSectionNotFound on
RLS-filtered miss.
- internal/handlers/submission_sections.go (NEW, ~150 LoC):
PATCH /api/submission-drafts/{draft_id}/sections/{section_id}.
Owner-scoped via SubmissionDraftService.Get; section-belongs-to-draft
cross-check. 404 on both missing-draft and section-belongs-elsewhere
paths.
- internal/handlers/files.go: fetchComposerBaseBytes + composerBaseSlugMap
reuse the existing Gitea proxy cache for base .docx bytes. hlc-letterhead
→ existing firmSkeletonSubmissionSlug, neutral → existing
skeletonSubmissionSlug.
- internal/handlers/submission_drafts.go: exportSubmissionDraft helper
branches on draft.BaseID. When set AND base + bytes + sections all
resolve → Composer pipeline. Else v1 fallback render path stays.
Audit metadata jsonb gains "composer": true + "base_id" flag when
composer was used.
Wiring:
- handlers.Services gains SubmissionComposer.
- dbServices.submissionComposer wired from svc.SubmissionComposer.
- main.go instantiates NewSubmissionComposer with the existing
SubmissionRenderer (so the {{rule.X}} alias contract stays preserved
inside section content).
Frontend additions (~400 LoC):
- client/submission-draft.ts: paintSectionList rewritten to render a
contentEditable per included section with a per-section B/I
toolbar. Per-section autosave debounced 500ms; mousedown handlers on
toolbar buttons preserve editor focus mid-command. domToMarkdown
walks the contentEditable's DOM tree back to Markdown source-of-
truth (b/strong → **…**, i/em → *…*, div/p → paragraph break, br
→ newline). Updated state.view.sections in-place on PATCH success
without re-painting (avoids focus-stealing on every keystroke);
re-paints only on structural changes (included toggle, label edits,
order changes).
- client/submission-draft.ts: onSectionToggleIncluded hides/shows a
section via PATCH. flushSectionAutosave on blur force-flushes
pending edits so leaving an editor doesn't strand unsynced changes.
- styles/global.css: editor surface (contentEditable area with focus
ring + placeholder), toolbar buttons (B/I 1.8rem squares),
per-section "Hide"/"Include" toggle in the head row.
- Updated i18n hint copy: "Inhalt pro Abschnitt — Autosave nach
500ms. Letztes Layout in Word."
Templates regenerated on Gitea:
- _skeleton.docx → composer-mode body (anchors only): blob SHA
ac0cdeaf49f7cd417ec143e2319ffbb02ec65644.
- _firm-skeleton.docx → composer-mode body (anchors only, preserves
sectPr → firm header/footer rIds): blob SHA
f1e9a9fb9a29ca01bf7bee709a45c5dda2a8e317.
- Both uploaded as mAi via --netrc-file ~/.netrc-mai.
- gen-skeleton-submission-template script gains an -anchors flag
(default true) so future regens emit composer-ready bodies. The
_firm-skeleton.docx regen was done via a one-off /tmp helper since
the gen-hl-skeleton-template script requires the proprietary .dotm
source which lives in HL/mWorkRepo; extending that script to accept
an existing .docx as input is a follow-up cleanup.
Build hygiene: go build/vet/test -short ./internal/... ./cmd/... all
clean; bun run build clean (2900 i18n keys, data-i18n scan clean).
NO behavior change for pre-Composer drafts (base_id NULL → v1
fallback render path stays compiled in). NO migrations needed in this
slice — sections were already in the schema from Slice A; only
content_md_de/en UPDATEs happen via the new PATCH endpoint.
Hard rules per Q2/Q10 ratification still honoured:
- No building_block_id lineage (Slice C territory; Q2).
- Caption/letterhead/signature are regular prose sections, seeded from
base spec; lawyer can edit/hide freely (Q10).
- {{rule.X}} aliases preserved (renderer pass unchanged).
NOT in scope per Slice B brief:
- Headings 1–3, lists, blockquote (Slice D's MD walker extension).
- Building blocks library (Slice C).
- Reorder / add-custom-section (Slice F).
- Auto-upgrade of pre-Composer drafts (Slice C — explicitly NOT in
this slice per head's brief msg #2393).
t-paliad-313 Slice B
276 lines
13 KiB
TypeScript
276 lines
13 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";
|
|
|
|
// t-paliad-238 Slice A — dedicated Submissions/Schriftsätze editor page.
|
|
//
|
|
// Lawyer picks (or creates) a draft for one (project, submission_code),
|
|
// edits placeholder variables in a sticky sidebar, sees a read-only
|
|
// HTML preview of the merged document, and exports the result as
|
|
// .docx. Drafts persist server-side per paliad.submission_drafts.
|
|
//
|
|
// Pure shell: client/submission-draft.ts hydrates draft list + variable
|
|
// form + preview pane after page load. The same dist/submission-draft.html
|
|
// serves every (project_id, submission_code, [draft_id]) URL.
|
|
//
|
|
// Design ref: docs/design-submission-page-2026-05-22.md §6.
|
|
|
|
export function renderSubmissionDraft(): 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="submissions.draft.title">Schriftsatz bearbeiten — Paliad</title>
|
|
<link rel="stylesheet" href="/assets/global.css" />
|
|
</head>
|
|
<body className="has-sidebar page-submission-draft">
|
|
<Sidebar currentPath="/projects" />
|
|
<BottomNav currentPath="/projects" />
|
|
|
|
<main>
|
|
<section className="tool-page submission-draft-page">
|
|
<div className="container">
|
|
<a
|
|
id="submission-draft-back-link"
|
|
href="/projects"
|
|
className="back-link"
|
|
data-i18n="submissions.draft.back">
|
|
← Zurück zum Projekt
|
|
</a>
|
|
|
|
<div id="submission-draft-loading" className="entity-loading">
|
|
<p data-i18n="submissions.draft.loading">Lädt…</p>
|
|
</div>
|
|
|
|
<div id="submission-draft-notfound" className="entity-empty" style="display:none">
|
|
<p data-i18n="submissions.draft.notfound">
|
|
Schriftsatz nicht gefunden oder keine Berechtigung.
|
|
</p>
|
|
</div>
|
|
|
|
<div id="submission-draft-error" className="entity-empty" style="display:none" />
|
|
|
|
<div id="submission-draft-body" style="display:none">
|
|
<header className="submission-draft-header">
|
|
<div className="submission-draft-header-text">
|
|
<h1 id="submission-draft-title" />
|
|
<p id="submission-draft-subtitle" className="tool-subtitle" />
|
|
</div>
|
|
<div className="submission-draft-header-actions">
|
|
<button
|
|
id="submission-draft-export-btn"
|
|
type="button"
|
|
className="btn-primary btn-cta-lime"
|
|
data-i18n="submissions.draft.action.export">
|
|
Als .docx exportieren
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="submission-draft-grid">
|
|
{/* Sidebar — draft switcher + variable groups. */}
|
|
<aside className="submission-draft-sidebar" id="submission-draft-sidebar">
|
|
<div className="submission-draft-switcher">
|
|
<label htmlFor="submission-draft-pick" data-i18n="submissions.draft.switcher.label">
|
|
Entwurf
|
|
</label>
|
|
<select id="submission-draft-pick" />
|
|
<button
|
|
type="button"
|
|
id="submission-draft-new-btn"
|
|
className="btn-small btn-secondary"
|
|
data-i18n="submissions.draft.action.new">
|
|
+ Neuer Entwurf
|
|
</button>
|
|
</div>
|
|
|
|
<div className="submission-draft-name-row">
|
|
<input
|
|
type="text"
|
|
id="submission-draft-name"
|
|
className="entity-form-input"
|
|
data-i18n-placeholder="submissions.draft.name.placeholder"
|
|
placeholder="Name dieses Entwurfs"
|
|
/>
|
|
<button
|
|
type="button"
|
|
id="submission-draft-delete-btn"
|
|
className="btn-small btn-link-danger"
|
|
data-i18n="submissions.draft.action.delete">
|
|
Löschen
|
|
</button>
|
|
</div>
|
|
|
|
{/* t-paliad-313 (m/paliad#141) Composer Slice A —
|
|
base picker. Hydrated by client/submission-draft.ts
|
|
once /api/submission-bases returns. Disabled
|
|
for pre-Composer drafts (base_id NULL); switching
|
|
autosaves the draft. */}
|
|
<div
|
|
className="submission-draft-base-row"
|
|
id="submission-draft-base-row"
|
|
style="display:none">
|
|
<label htmlFor="submission-draft-base" data-i18n="submissions.draft.base.label">
|
|
Vorlagenbasis
|
|
</label>
|
|
<select id="submission-draft-base" />
|
|
<p
|
|
className="submission-draft-base-hint"
|
|
id="submission-draft-base-hint"
|
|
data-i18n="submissions.draft.base.hint">
|
|
Steuert Schriftarten, Briefkopf und Abschnitts-Defaults.
|
|
</p>
|
|
</div>
|
|
|
|
{/* t-paliad-276 — output language toggle (DE/EN).
|
|
Hydrated by client/submission-draft.ts; switching
|
|
autosaves the draft and re-renders the preview. */}
|
|
<div
|
|
className="submission-draft-language-row"
|
|
id="submission-draft-language-row"
|
|
role="radiogroup"
|
|
aria-labelledby="submission-draft-language-label">
|
|
<span
|
|
id="submission-draft-language-label"
|
|
className="submission-draft-language-label"
|
|
data-i18n="submissions.draft.language">
|
|
Sprache
|
|
</span>
|
|
<label className="submission-draft-language-option">
|
|
<input
|
|
type="radio"
|
|
name="submission-draft-language"
|
|
value="de"
|
|
id="submission-draft-language-de"
|
|
/>
|
|
<span data-i18n="submissions.draft.language.de">DE</span>
|
|
</label>
|
|
<label className="submission-draft-language-option">
|
|
<input
|
|
type="radio"
|
|
name="submission-draft-language"
|
|
value="en"
|
|
id="submission-draft-language-en"
|
|
/>
|
|
<span data-i18n="submissions.draft.language.en">EN</span>
|
|
</label>
|
|
</div>
|
|
<p
|
|
className="submission-draft-language-fallback"
|
|
id="submission-draft-language-fallback"
|
|
style="display:none"
|
|
data-i18n="submissions.draft.language.fallback_notice">
|
|
Fallback: universelles Skelett (keine sprachspezifische Vorlage).
|
|
</p>
|
|
|
|
<p className="submission-draft-savestatus" id="submission-draft-savestatus" />
|
|
|
|
{/* t-paliad-277: "Aus Projekt importieren" + last-
|
|
imported-at timestamp. Only visible when the
|
|
draft has a project_id attached. */}
|
|
<div
|
|
id="submission-draft-import-row"
|
|
className="submission-draft-import-row"
|
|
style="display:none">
|
|
<button
|
|
type="button"
|
|
id="submission-draft-import-btn"
|
|
className="btn-small btn-secondary"
|
|
data-i18n="submissions.draft.import.button">
|
|
Aus Projekt importieren
|
|
</button>
|
|
<span
|
|
id="submission-draft-import-stamp"
|
|
className="submission-draft-import-stamp"
|
|
/>
|
|
</div>
|
|
|
|
{/* t-paliad-277 / t-paliad-287: multi-select party
|
|
picker plus per-side Add-Party affordance.
|
|
Populated from view.available_parties; checkbox
|
|
per party, grouped by role. Hidden when no
|
|
project is attached; visible even on empty
|
|
rosters so the lawyer can use Add Party to
|
|
populate. */}
|
|
<div
|
|
id="submission-draft-parties"
|
|
className="submission-draft-parties"
|
|
style="display:none">
|
|
<h3
|
|
className="submission-draft-var-group-title"
|
|
data-i18n="submissions.draft.parties.title">
|
|
Parteien
|
|
</h3>
|
|
<p
|
|
className="submission-draft-parties-hint"
|
|
data-i18n="submissions.draft.parties.hint">
|
|
Wählen Sie aus, welche Parteien im Schriftsatz genannt werden sollen.
|
|
</p>
|
|
<div
|
|
id="submission-draft-parties-list"
|
|
className="submission-draft-parties-list"
|
|
/>
|
|
</div>
|
|
|
|
<div className="submission-draft-variables" id="submission-draft-variables" />
|
|
</aside>
|
|
|
|
{/* t-paliad-313 (m/paliad#141) Composer Slice A —
|
|
read-only section list. Painted from
|
|
view.sections. Empty/hidden for pre-Composer
|
|
drafts where no rows have been seeded. Slice B
|
|
turns these into in-place editable prose blocks. */}
|
|
<section
|
|
className="submission-draft-sections-wrap"
|
|
id="submission-draft-sections-wrap"
|
|
style="display:none">
|
|
<header className="submission-draft-sections-header">
|
|
<h2 data-i18n="submissions.draft.sections.title">Abschnitte</h2>
|
|
<span
|
|
className="submission-draft-sections-hint"
|
|
data-i18n="submissions.draft.sections.hint">
|
|
Inhalt pro Abschnitt — Autosave nach 500 ms. Letztes Layout in Word.
|
|
</span>
|
|
</header>
|
|
<ol
|
|
className="submission-draft-sections-list"
|
|
id="submission-draft-sections-list"
|
|
/>
|
|
</section>
|
|
|
|
{/* Preview pane — read-only HTML render of the merged
|
|
document body. Re-renders on autosave round-trip. */}
|
|
<section className="submission-draft-preview-wrap">
|
|
<header className="submission-draft-preview-header">
|
|
<h2 data-i18n="submissions.draft.preview.title">Vorschau</h2>
|
|
<span
|
|
className="submission-draft-preview-hint"
|
|
data-i18n="submissions.draft.preview.hint">
|
|
Read-only Vorschau — finale Bearbeitung in Word.
|
|
</span>
|
|
</header>
|
|
<div className="submission-draft-preview" id="submission-draft-preview" />
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<Footer />
|
|
<PaliadinWidget />
|
|
|
|
<script src="/assets/submission-draft.js"></script>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|