Adds the dedicated Submissions/Schriftsätze editor at
/projects/{id}/submissions/{code}/draft (and …/draft/{draft_id}) per
docs/design-submission-page-2026-05-22.md.
Lawyer picks (or creates) a named draft, edits placeholder variables
in a sticky sidebar, sees a read-only HTML preview of the merged
document body, and exports a .docx with project state + lawyer
overrides resolved. Drafts persist in paliad.submission_drafts
keyed on (project_id, submission_code, user_id, name) with RLS via
can_see_project; updates and deletes additionally gated on owner-only
(Q-E4 owner-scoped pick, m-confirmed).
Resurrected from git history per the design's "no rewrite" plan:
SubmissionVarsService ← commit 1765d5e (Slice 2 with patent_number_upc)
SubmissionRenderer ← commit 8ea3509 (in-house merge engine — the
lukasjarosch/go-docx library refuses sibling
placeholders in one run, which patent submissions
use routinely)
ConvertDotmToDocx ← existing format-only convert (kept; reused as
pre-pass so .dotm inputs strip macros before
merge)
New code:
paliad.submission_drafts migration 119 (idempotent — DROP POLICY IF EXISTS
+ CREATE; CREATE OR REPLACE for the shared trigger
function). Applied to live DB.
SubmissionDraftService CRUD + autosave-friendly Update + Export/RenderPreview
entry points
RenderHTML method new on the renderer; walks the same merged
document.xml as Render but emits HTML for the
preview pane (Q-E3 server-side pick)
7 API handlers list / create / get / patch / delete / preview / export
2 page routes /draft and /draft/{draft_id}
submission-draft.tsx stand-alone editor page (header / sidebar /
preview / export button); served via
dist/submission-draft.html
submission-draft.ts client bundle — autosave (500ms debounce),
draft switcher, rename, delete, export with
blob download
Tab integration: existing /projects/{id}/#tab-submissions rows get
[Bearbeiten] alongside the existing [Generieren] one-click format-only
path — additive, no removal.
Slice A template: universal HL Patents Style .dotm (same path
t-paliad-230 uses). resolveSubmissionTemplate carries the
submission_code parameter so Slice B's TemplateRegistry wiring (per-
code .docx fallback chain) is a one-function swap.
Audit trail: paliad.system_audit_log row per export
(event_type='submission.exported') + paliad.project_events row
(event_type='submission_exported', timeline_kind='custom_milestone')
so the export surfaces on the project's Verlauf / SmartTimeline. No
paliad.documents write (Q-E2 inventor pick, head-ratified).
Tests: TestRender_* / TestPlaceholderRegex_* / TestRenderHTML_* +
TestLegalSourcePretty / TestOurSide* / TestPatentNumberUPC — all
green. go build / go vet / go test ./internal/... / bun run build all
clean.
Migration slot taken: 119.
89 lines
4.0 KiB
PL/PgSQL
89 lines
4.0 KiB
PL/PgSQL
-- t-paliad-238: dedicated Submissions/Schriftsätze page.
|
|
--
|
|
-- paliad.submission_drafts holds the lawyer's per-(project, submission_code)
|
|
-- draft state for the new editor at /projects/{id}/submissions/{code}/draft.
|
|
-- Each row is one named draft owned by one user; multiple drafts per
|
|
-- (project, submission_code, user_id) are supported via the `name` field
|
|
-- (auto-generated "Entwurf 1", "Entwurf 2", … and lawyer-renameable).
|
|
--
|
|
-- `variables` carries the lawyer's overrides for the placeholder map
|
|
-- assembled at export time by SubmissionVarsService — empty string forces
|
|
-- the [KEIN WERT: …] marker; absent key falls back to the resolved bag.
|
|
--
|
|
-- `last_exported_at` / `last_exported_sha` record provenance of the most
|
|
-- recent .docx export (template SHA pinned for audit). Audit rows live
|
|
-- in paliad.system_audit_log + paliad.project_events; this table is the
|
|
-- lawyer's working state, not the audit log.
|
|
--
|
|
-- Visibility: per project CLAUDE.md, every project-scoped resource gates
|
|
-- on paliad.can_see_project. UPDATE / DELETE additionally require the
|
|
-- draft's user_id to match auth.uid() so two co-team-members don't stomp
|
|
-- on each other's drafts (head-confirmed Q-E4 owner-scoped pick).
|
|
|
|
CREATE TABLE IF NOT EXISTS paliad.submission_drafts (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
project_id uuid NOT NULL REFERENCES paliad.projects(id) ON DELETE CASCADE,
|
|
submission_code text NOT NULL,
|
|
user_id uuid NOT NULL REFERENCES paliad.users(id) ON DELETE CASCADE,
|
|
name text NOT NULL,
|
|
|
|
variables jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
|
|
last_exported_at timestamptz,
|
|
last_exported_sha text,
|
|
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
|
|
CONSTRAINT submission_drafts_unique_per_user
|
|
UNIQUE (project_id, submission_code, user_id, name)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS submission_drafts_project_user_idx
|
|
ON paliad.submission_drafts (project_id, user_id, submission_code, updated_at DESC);
|
|
|
|
ALTER TABLE paliad.submission_drafts ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS submission_drafts_select ON paliad.submission_drafts;
|
|
CREATE POLICY submission_drafts_select
|
|
ON paliad.submission_drafts FOR SELECT TO authenticated
|
|
USING (paliad.can_see_project(project_id));
|
|
|
|
DROP POLICY IF EXISTS submission_drafts_insert ON paliad.submission_drafts;
|
|
CREATE POLICY submission_drafts_insert
|
|
ON paliad.submission_drafts FOR INSERT TO authenticated
|
|
WITH CHECK (
|
|
user_id = auth.uid()
|
|
AND paliad.can_see_project(project_id)
|
|
);
|
|
|
|
DROP POLICY IF EXISTS submission_drafts_update ON paliad.submission_drafts;
|
|
CREATE POLICY submission_drafts_update
|
|
ON paliad.submission_drafts FOR UPDATE TO authenticated
|
|
USING (user_id = auth.uid() AND paliad.can_see_project(project_id))
|
|
WITH CHECK (user_id = auth.uid() AND paliad.can_see_project(project_id));
|
|
|
|
DROP POLICY IF EXISTS submission_drafts_delete ON paliad.submission_drafts;
|
|
CREATE POLICY submission_drafts_delete
|
|
ON paliad.submission_drafts FOR DELETE TO authenticated
|
|
USING (user_id = auth.uid() AND paliad.can_see_project(project_id));
|
|
|
|
-- updated_at maintenance: trigger function lives once per schema; reuse if
|
|
-- it already exists, otherwise create the standard shape.
|
|
CREATE OR REPLACE FUNCTION paliad.tg_set_updated_at()
|
|
RETURNS trigger
|
|
LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
NEW.updated_at = now();
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
|
|
DROP TRIGGER IF EXISTS submission_drafts_set_updated_at ON paliad.submission_drafts;
|
|
CREATE TRIGGER submission_drafts_set_updated_at
|
|
BEFORE UPDATE ON paliad.submission_drafts
|
|
FOR EACH ROW EXECUTE FUNCTION paliad.tg_set_updated_at();
|
|
|
|
COMMENT ON TABLE paliad.submission_drafts IS
|
|
't-paliad-238: per-(project, submission_code, user) named drafts for the dedicated Submissions/Schriftsätze page. Each row holds the lawyer-edited variable overrides for the .docx export. Audit rows live in paliad.system_audit_log + paliad.project_events.';
|