70985d88b0060f2917042ff3cf4166a38057a582
254 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 70985d88b0 |
feat(fristenrechner): Slice S4 — Mode B wizard (m/paliad#146)
Mode B "🧭 Geführt" — the guided 3-5 row wizard defined in
docs/design-fristenrechner-overhaul-2026-05-26.md §3.2. Lands the
user on a single procedural_event (the trigger), then transitions
to the shared §4 result view.
Frontend:
* `fristenrechner-wizard.ts` — row stack with R1..R5:
R1 Was ist passiert? (event_kind, always asked)
R2 Vor welchem Gericht? (jurisdiction, skip if R1 narrows)
R3 In welchem Verfahren? (proceeding_type, auto-skip when
narrowed pool has 1 option)
R4 Welches Schriftstück? (procedural_event, landing)
R5 Welche Seite vertreten Sie? (party, only when follow-ups
differ by primary_party)
Row badges per §11.Q3: R1+R2 = Filter, R3+R4+R5 = Qualifier.
R5 has NO "Beide" option per §11.Q8 — Mode B is the file-mode
where perspective is a qualifier.
* Project prefill — derives R3 + R2 jurisdiction from
project.proceeding_type, R5 from project.our_side. Annotates
pre-filled rows with "aus Akte" tag and implicit rows with
"implizit" tag per §11.Q10 ("erhalten" annotation when a pick is
carried across an upstream change).
* R4-to-result transition — after R4 the wizard fetches /follow-
ups (no dates) to inspect primary_party variance. If both
claimant and defendant rules exist AND R5 isn't already set,
swaps the loading row for the R5 chip picker. Otherwise jumps
straight to mountResultView.
* URL state — `?mode=wizard&kind=…&forum=…&pt=…&r4=…&party=…`
keeps deep-link / back-nav consistent (the launchResult step
sets `event=` so the result view picks up).
* `fristenrechner-result.ts` mountModeShell now dispatches the
"wizard" tab to the wizard module (was a coming-soon
placeholder).
* 18 i18n keys added (DE + EN parity), 145-line CSS block for the
wizard row stack with Filter / Qualifier badge styling and
"aus Akte" annotation chip.
Backend:
* `ProceedingListOptions.EventKind` adds an EXISTS subquery
filter on `paliad.sequencing_rules` ⨯ `paliad.procedural_events`
so Mode B R3 chips only show proceedings whose event roster
contains at least one event of the requested kind (design
§6.3). Endpoint param: `event_kind=` on
/api/tools/proceeding-types.
Test updates:
* `TestListProceedings` switched from SKIP-when-column-missing to
asserting the live filter — mig 153 has landed, `kind` column
is in place. New subtests: kind=proceeding includes
upc.inf.cfi and excludes the phase row upc.cfi.interim;
event_kind=filing narrows to proceedings with filing events.
* `fristenrechner-wizard.test.ts` covers
`followUpsDifferByParty` — the R5 trigger predicate. 7 cases:
asymmetric → true; uniform / both / court / empty → false.
Verified — bun build clean (2971 i18n keys), 256 frontend tests
pass (incl. 7 new), go build + vet clean, live-DB
TestListProceedings passes all 6 subtests against mig 153 data.
|
|||
| 2a2c5b8033 |
feat(fristenrechner): Slice S3 — Mode A direct search (m/paliad#146)
Mode A "⚡ Direkt suchen" — the power-user entry path defined in docs/design-fristenrechner-overhaul-2026-05-26.md §3.1. Renders above the §4 result view; clicking a result row locks the trigger event and transitions to the shared result surface from S2. Frontend: * `fristenrechner-mode-a.ts` — filter strip (Forum / Verfahren / Was passierte / Partei) + free-text search input + result list. Section-split visual hierarchy per m §11.Q3: filter chips in a bordered "Filter (eingrenzen)" strip on top, result list below. Inbox channel chip lives behind an "Erweitert" details summary per §3.3; picking CMS / beA auto-nudges the Forum chip. Party chip retains a "Beide" option (Mode A is filter mode per §11.Q8; Mode B drops it in S4). * `fristenrechner-result.ts` — new `mountModeShell(activeTab)` renders the two mode tabs per §11.Q2 and lazy-imports Mode A. Mode B tab is a placeholder until S4 lands. * `fristenrechner.ts` boot — when `?overhaul=1` is set and `?event` is empty, mountModeShell takes over (default tab = search; `?mode= wizard` opens the wizard tab when S4 ships). With `?event=` the flow still jumps straight to the result view. URL state syncs forum / pt / kind / party / q on every chip click. * 28 i18n keys added (DE + EN parity), 310-line CSS block for the mode tabs + Mode A surface. Backend: * New `ProceedingListOptions { Jurisdiction, Kind }` + service method `ListProceedings(ctx, opts)`. Legacy `ListFristenrechnerTypes` keeps the no-filter signature for existing callers. Handler `/api/tools/proceeding-types` accepts `?jurisdiction=` and `?kind=` query params. * `kind=proceeding` filter targets the taxonomy column landed in mig 153 (parallel branch t-paliad-325, m/paliad#147). Sequenced per the taxonomy doc §7 option (c): mig 153 merges before S3 ships to main, so the filter is never false-positive (no phase / side_action / meta rows leak into the chip strip). Verified — bun build clean (2955 i18n keys, data-i18n attributes clean), 249 frontend tests pass, go build + vet clean. New TestListProceedings — 4 PASS (no-filter, jurisdiction=UPC, jurisdiction=DE, ListFristenrechnerTypes alias) + 1 SKIP for the kind=proceeding case that probes the column and skips when mig 153 hasn't landed yet. S1 + S2 live tests still green. |
|||
| 9ab8dd8e0f |
feat(fristenrechner): Slice S2 — result view under ?overhaul=1 (m/paliad#146)
New `frontend/src/client/fristenrechner-result.ts` module renders the
shared result surface defined in
docs/design-fristenrechner-overhaul-2026-05-26.md §4:
* Sticky trigger card — event icon + name, proceeding/jurisdiction
chips, inline trigger-date input that re-fetches on change.
* Four follow-up groups — Mandatory / Recommended / Optional /
Conditional. SPAWNED rules fold into their priority bucket with
a `⇲ neues Verfahren` badge (§11.Q5). Conditional bucket holds
every rule with sr.condition_expr IS NOT NULL.
* Per-rule rows — title, duration phrase, party chip, legal-source
citation (with youpc.org link when available), pre-checked
checkbox driven by `defaultChecked(r)` (mandatory + recommended
on; conditional + court-set + optional off), inline ✏ Datum
override that re-renders.
* Write-back footer — conditional on `?project=<uuid>` per §11.Q7;
in kontextfrei mode the footer is hidden and an inline nudge
invites the user to pick an Akte. CTA submits to the existing
POST /api/projects/{id}/deadlines/bulk endpoint, stamping each
row with `audit_reason: "Aus Fristenrechner — Trigger: {name}
({date})"` per §11.Q12.
Mount + URL contract — when `?overhaul=1` is set in the URL,
`fristenrechner.ts` hides every legacy panel (`fristen-step1`,
`fristen-step2`, `fristen-pathway-a`, `fristen-pathway-b`,
`fristen-step3a`, the step-1 summary) and shows the overhaul root
instead. With `?overhaul=1&event=<code>&trigger_date=…` the surface
is deep-linkable end-to-end. Without `?event=` the empty-shell
nudge renders — S3+S4 will mount the entry-mode UIs onto this same
root.
Verified — bun build clean, 249 frontend tests pass (incl. 9 new
helper tests for groupFollowUps + defaultChecked), go build + vet
clean, S1 live-DB tests still green.
|
|||
| bd7896ef68 |
feat(submissions): Composer Slice F — section reorder / hide / add custom (m/paliad#141)
The final Composer slice per design doc §12. Lawyer gains full
control over section composition: drag-and-drop reorder, per-section
delete, "+ Add section" picker for custom slugs that don't appear in
the base's default spec. Combined with Slice B's hide toggle, this
closes out the A→F sequence — Composer A→F is complete.
Backend (internal/services/submission_section_service.go, +120 LoC):
- SectionService.Create — adds a new section row to a draft. Validates
section_key + labels + kind (must be prose/requests/evidence).
Auto-assigns next order_index when OrderIndex=0; collisions on
(draft_id, section_key) surface as ErrInvalidInput.
- SectionService.Delete — removes one section by id. Returns
ErrSubmissionSectionNotFound when nothing was deleted.
- SectionService.Reorder — accepts a sequence of section_ids, rewrites
every row's order_index to (1..N)×10 transactionally. Returns the
refreshed list. Sections not present in the sequence are silently
ignored (defensive — partial reorder doesn't lose rows).
Handlers (internal/handlers/submission_sections.go, +180 LoC):
- POST /api/submission-drafts/{draft_id}/sections — owner-scoped via
SubmissionDraftService.Get. 400 on slug collision / invalid kind.
- DELETE /api/submission-drafts/{draft_id}/sections/{section_id} —
owner + section-belongs-to-draft cross-check. 204 on success.
- POST /api/submission-drafts/{draft_id}/sections/reorder — accepts
{"section_order": [uuid, uuid, ...]}; returns refreshed sections list.
Frontend (frontend/src/client/submission-draft.ts, +260 LoC):
- Each section row gains a drag handle (⋮⋮) on the left of the head.
Drag handle is the only draggable element; contentEditable
selections inside the editor body keep working. HTML5 native DnD,
no library.
- Drop-target highlighting via .submission-draft-section--drop-target
(border-top accent). Cleanup on dragend / drop / cancel.
- Per-section "Delete" button next to the existing Hide/Include
toggle. Confirm prompt prevents accidental loss of typed prose.
- "+ Add section" trailing affordance below the section list opens an
inline form (slug + DE label + EN label + kind dropdown). Submit
POSTs to the new endpoint; on success splices the row into
state.view.sections and re-paints.
CSS (frontend/src/styles/global.css, +65 LoC):
- .submission-draft-section-handle (grab cursor + hover background +
active=grabbing).
- .submission-draft-section--dragging / --drop-target visual states.
- .submission-draft-add-section form layout (dashed border + lime
primary submit).
Tests (internal/services/submission_section_slice_f_test.go, NEW,
TEST_DATABASE_URL-gated):
- Create custom section + slug-collision surface as ErrInvalidInput.
- Delete + repeat-delete returns ErrSubmissionSectionNotFound.
- Reorder reverses 10 seeded sections + verifies the resulting
order_index sequence is ascending and matches the input order.
Build hygiene: go build/vet/test -short clean (all packages);
bun run build clean (2906 i18n keys, data-i18n scan clean).
Hard rules honoured:
- NO new migrations (Slice F is pure code on Slice A's schema).
- NO behavior change for pre-Composer drafts (no section rows → no
drag handles to drag).
- {{rule.X}} aliases preserved (custom sections render through the
same composer pipeline as default sections).
- Q2/Q9/Q10 ratifications preserved.
This closes the Composer slice sequence A → F. The full feature set
ratified by m on 2026-05-26 is now in place:
A — base picker + read-only section list (mig 146/147/148)
B — editable prose + anchor-spliced render + MD→OOXML walker
C — building-blocks library + section picker (mig 149)
D — rich prose (headings, lists, blockquote, hyperlinks)
E — specialist bases lg-duesseldorf + upc-formal (mig 150)
F — section reorder / delete / add custom
t-paliad-318 Slice F
|
|||
| ee98db94fa |
feat(submissions): Composer Slice C — building blocks library (m/paliad#141)
Per the design at docs/design-submission-generator-v2-2026-05-26.md §8
and the Q2 / Q9 ratifications:
- Q2 (m, 2026-05-26): building blocks are plain text paste sources.
No building_block_id reference is stored on submission_sections.
- Q9 (m, 2026-05-26): four visibility tiers — private / team / firm
/ global.
Schema (mig 149):
- paliad.submission_building_blocks — library catalog. Columns: slug,
firm (NULL = cross-firm), section_key (binds to one section kind),
proceeding_family (NULL = any), title_de/_en + description_de/_en
+ content_md_de/_en, author_id, visibility (CHECK in 4-tier set),
is_published, created_at, updated_at, deleted_at (soft delete).
RLS: coarse-grained SELECT — every authenticated user sees
non-deleted non-private rows + own private rows. Tier-specific
predicate (private/team/firm/global) applied in Go-layer service so
semantics evolve without RLS migrations. Mutations admin-only (no
RLS write paths).
- paliad.submission_building_block_admin_versions — append-only
history per block, retention=20. Admin-side only; NOT referenced
from submission_sections (per Q2's plain-text-paste model). Exists
so accidental delete / overwrite are recoverable.
Backend:
- internal/services/submission_building_block_service.go (~510 LoC):
BuildingBlockService. ListVisible applies tier predicate at query
time (private = author_id match; firm = firm column NULL OR matches
branding.Name; team = author shares a project_team with caller via
paliad.project_teams self-join; global = open). ListAllForAdmin
drops the predicate. Create + Update + SoftDelete + RestoreVersion
all transactional; appendVersionTx writes one audit row +
GC-deletes anything past the retention=20 horizon in the same tx.
InsertIntoSection (the paste mechanic) clones content_md_<lang>
into the section row with a "\n\n" separator if section already has
content. NO building_block_id stamped per Q2.
- internal/handlers/submission_building_blocks.go (~480 LoC): nine
handlers split between the lawyer-facing picker (list, insert) and
the admin editor (list, get, create, update, delete, list-versions,
restore-version, page). buildingBlockUpdateInput uses presence-
tracking UnmarshalJSON for the four nullable fields (firm,
proceeding_family, description_de/_en) so PATCH can distinguish
"no change" from "set to null".
- Routes registered: lawyer-facing under /api/submission-building-blocks,
admin-gated under /api/admin/submission-building-blocks/* and
/admin/submission-building-blocks (page).
- Wiring: handlers.Services + dbServices + cmd/server/main.go all
gain SubmissionBuildingBlock. NewBuildingBlockService takes the
branding.Name firm hint for the visibility predicate.
Frontend:
- frontend/src/admin-submission-building-blocks.tsx (~85 LoC):
three-pane admin shell (list / editor / version log) registered
in build.ts.
- frontend/src/client/admin-submission-building-blocks.ts (~370
LoC): admin client — list paint, edit form (slug + firm +
section_key + proceeding_family + title/desc/content per lang +
visibility radio + is_published toggle), per-block version log
with restore button. Bilingual labels.
- frontend/src/client/submission-draft.ts: per-section "+ Baustein"
button on the Composer editor toolbar (Slice B substrate gets one
more affordance). openBlockPicker opens a modal filtered to the
section's section_key, 200ms-debounced search by free text against
title/description/content. Click a hit → POST insert-into-section
→ section row's content_md_<lang> gains the block's content
appended at the end (Q2's plain-text paste semantic, no lineage).
- ~240 LoC of CSS: modal overlay + picker rows with tier-colored
visibility chips + admin editor 3-pane grid + form rows + version
list.
- 12 new i18n keys × 2 langs (admin.building_blocks.*).
Tests:
- TestValidVisibility (8 cases including case-sensitivity + empty).
- TestAppendBlockContent (8 cases covering empty-existing / empty-
addition / whitespace-only / trailing newline collapse).
- TestBuildingBlockVisibilityConstants pins the 4 string literals
against drift (RLS predicate + DB CHECK depend on them).
Build hygiene: go build/vet/test -short clean; bun run build clean
(2906 i18n keys, data-i18n scan clean).
Hard rules per ratifications honoured:
- Q2: no building_block_id lineage on sections (paste is plain text).
- Q9: 4 visibility tiers (private/team/firm/global).
- NO behavior change for pre-Composer drafts (the picker just doesn't
show — section list is hidden for base_id NULL drafts).
- {{rule.X}} aliases preserved (block content goes through the same
v1 placeholder pass on export as section prose).
NOT in scope per Slice C brief:
- User-authored private blocks (Slice C ships admin curation only;
any-user create is a follow-up).
- Tier promotion review workflow (admin sets tier directly today).
- Per-section "where is this block used" reverse lookup (no lineage
to query).
- Slice D's rich-prose features (headings, lists, blockquote) still
Slice D's job; this Slice doesn't extend the MD walker.
t-paliad-315 Slice C
|
|||
| f963b0df34 |
feat(submissions): Composer Slice B — editable prose sections + anchor-spliced render (m/paliad#141)
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
|
|||
| e2969fc358 |
feat(submissions): Composer Slice A — base picker + read-only section list (m/paliad#141)
The first slice of the Submission generator v2 ("Composer") per the
design at docs/design-submission-generator-v2-2026-05-26.md §12 Slice A.
Ships the base concept + per-draft section seeding end-to-end with NO
change to the .docx render path — v1 export still works exactly as
today.
Schema (mig 146/147/148):
- paliad.submission_bases — catalog table; one row per template base
(slug, firm, proceeding_family, label_de/en, gitea_path, section_spec
jsonb, is_default_for[]). RLS: wide-open SELECT for authenticated
users, mutations admin-only (handler-enforced, no RLS write paths).
Seeded with 2 rows: hlc-letterhead → _firm-skeleton.docx; neutral →
_skeleton.docx. Each section_spec carries the 10-section default
(letterhead, caption, introduction, requests, facts, legal_argument,
evidence, exhibits, closing, signature) with bilingual labels +
bag-driven seed Markdown for caption/letterhead/signature.
- paliad.submission_drafts gains base_id (FK SET NULL, optional) +
composer_meta jsonb (default '{}'). Purely additive; pre-Composer
drafts keep base_id NULL → v1 fallback render path stays active.
- paliad.submission_sections — per-draft section rows (draft_id,
section_key, order_index, kind ∈ {prose,requests,evidence},
label_de/en, included, content_md_de/en). RLS mirrors
submission_drafts (owner-scoped + can_see_project, four policies).
Backend:
- BaseService (read-only Slice A): List + GetByID + GetBySlug +
GetDefaultForCode (firm/family fallback chain).
- SectionService: ListForDraft + Get + SeedFromSpec (transactional
multi-INSERT).
- SubmissionDraftService.AttachComposer wires both; Create resolves
the firm default base and seeds base_id + section rows in one tx.
Composer wiring is additive — when bases==nil the service stays
v1-shaped.
- Update accepts BaseID **uuid.UUID (set / clear / no-change).
- submissionDraftView gains BaseID, ComposerMeta, Sections fields.
- Routes: GET /api/submission-bases (catalog list). PATCH endpoints
on both project-scoped and global drafts accept "base_id".
Frontend:
- submission-draft.tsx: base picker dropdown above language toggle
(hidden until catalog loads); section-list pane above the preview
(hidden when no rows).
- client/submission-draft.ts: loadBases() parallel-fetches on boot;
paintBasePicker rebuilds <option> list on every paint; onBaseChange
PATCHes base_id and repaints; paintSectionList renders each section
read-only (label + kind chip + excluded badge + Markdown body).
- Per the brief: NO auto-upgrade of existing 11 drafts (that's Slice C).
Pre-Composer drafts get the picker (catalog still loads) but the
section pane stays hidden until they pick a base on a new draft.
Tests:
- TestFamilyOfCode + TestBaseSectionSpec_DecodeShape + _EmptyDecode
(pure unit, no DB).
- TestComposerSeedFlow (live, TEST_DATABASE_URL-gated): asserts mig 146
seeded 10 default sections on both bases; GetDefaultForCode picks
hlc-letterhead for HLC/de.inf.lg.erwidg; new draft via Create seeds
base_id + 10 section rows in tx with ascending order_index and
bilingual labels populated.
NO behavior change to .docx export — the v1 path stays sole render
path this slice. Composer's anchor-based assembly engine + MD→OOXML
walker land in Slice B.
Build hygiene: go build/vet/test -short clean; bun run build clean
(2900 i18n keys, data-i18n scan clean).
t-paliad-313
|
|||
| 446c46e5c5 |
fix(css): repoint 12 var(--color-surface-alt, hex) sites to defined tokens (t-paliad-310, m/paliad#138)
The --color-surface-alt token was never defined in :root or :root[data-theme="dark"], so the var() fallback hex literal always won — leaving 12 surface sites with zero dark-mode treatment. Same pattern as t-paliad-087 / t-paliad-150 / t-paliad-291. Issue #138 surfaced four panels visibly broken in dark mode: 1. submission-draft no-project banner ("Kein Projekt zugeordnet…") — white-on-white 2. submission-draft preview header ("Vorschau / Read-only Vorschau…") — white-on-white 3. smart-timeline rule-chip (e.g. de.null.bpatg.berufung in Vorhersage rows) — grey-on-grey 4. submission-draft addparty manual form (Manuell / Aus DB / Name / …) — white-on-white Eight more latent sites with the same root cause are fixed in the same pass: .submissions-new-chip:hover, .submissions-new-project-item:hover, .submission-draft-import-row, .submission-draft-addparty-search-projref, .collab-invite-hint, .smart-timeline-status-icon, .smart-timeline-kind-chip--projected, .smart-timeline-add-choice:hover. Each site repointed to the semantically correct existing token (--color-surface-2 for #fafafa, --color-surface-muted for #f4f4f4, --color-bg-subtle for #f7f7f0, --color-bg-lime-tint for the lime-tinted collab-invite-hint). All four target tokens are defined in both :root and :root[data-theme="dark"]. No new tokens introduced. Light-mode hex values are functionally identical (#fafafa==#fafafa, #f4f4f4≈#f3f4f6, #f7f7f0≈#f7f3f0). Verified: bun run build clean; Playwright screenshots of the four panels in both light + dark modes show correct rendering. |
|||
| 3097df3918 |
mAi: #133 — Verfahrensablauf duration affordance (hover + toggle)
t-paliad-302 / m/paliad#133. Surface each event card's rule duration ("2 Mo. nach") on /tools/verfahrensablauf — by default as a hover tooltip on the date span, and optionally inline via a new "Dauern anzeigen" header toggle (localStorage key paliad.verfahrensablauf.durations-show). The issue scoped this as pure-frontend on the assumption that the duration fields were already on the /api/tools/fristenrechner payload. They were not: lp.TimelineEntry exposed only the computed dueDate, not the rule's (duration_value, duration_unit, timing) tuple. Added these as three additive optional fields and populated them in both engine emission sites (Calculate + CalculateByTriggerEvent) from the rule row directly. Source values are the base rule fields, not the post-alt-swap arithmetic — the tooltip reads as a property of the rule rather than a recap of which branch fired. Frontend wiring: - formatDurationLabel() in verfahrensablauf-core builds the "<value> <unit> <timing>" string from the existing deadlines.event.unit.<unit>.{one,many} + deadlines.event.timing.* i18n keys, reused from /tools/fristenrechner's event-mode renderer. - deadlineCardHtml attaches the label as title= on the date span (hover, default) and, when CardOpts.showDurations is on, emits an inline <span class="timeline-duration"> in the meta row. - Court-set / zero-duration rules (trigger event, hearings) skip the affordance — durationValue <= 0 short-circuits in formatDurationLabel. - Toggle persisted in localStorage under paliad.verfahrensablauf.durations-show, default off; sits next to the existing "Hinweise anzeigen" toggle. bun run build clean, go test ./pkg/litigationplanner/... and ./internal/... clean, bun test src/client/views clean (89/89). |
|||
| cc13a5b857 |
chore(admin): remove /admin/rules/export page + export-migrations API (t-paliad-297)
Workflow shifted to hand-written numbered migrations; the audit-row SQL
export tool no longer has any consumers. Pure deletion — /admin/rules
and /admin/rules/{id}/edit stay; only the export-to-SQL flow goes.
Deleted:
- frontend/src/admin-rules-export.tsx
- frontend/src/client/admin-rules-export.ts
Removed:
- routes GET /admin/rules/export and GET /admin/api/rules/export-migrations
- handleAdminExportRuleMigrations + handleAdminRulesExportPage
- RuleEditorService.ExportMigrationsSince + ExportResult + sqlEscape helper
- build.ts entries (import, client bundle, dist HTML write)
- Sidebar "Regel-Migrations" nav item + "Migrations exportieren" button on /admin/rules
- all admin.rules.export.* + nav.admin.rules_export + admin.rules.list.export i18n keys (DE+EN)
- .admin-rules-export-* CSS rules (dead after page deletion)
Doc references in design-fristen-phase2-2026-05-15.md and
design-paliad-data-export-2026-05-19.md updated to mark the endpoint as
removed (acceptance #2 requires grep to return zero hits).
|
|||
| 7ca6b2d643 |
feat(verfahrensablauf): event-card overhaul — iconified state + caret-popover unhide (t-paliad-293)
m/paliad#125 — concern A (horizontal scroll) and concern B (compact event-card UX). Concern A: the inline "Wieder einblenden" chip from t-paliad-290 pushed hidden cards past their column width on 375/414/768, causing horizontal page scroll. Fix: drop the chip entirely; surface the un-hide as a prominent "Wieder einblenden" entry inside the caret popover (matches the m's "actions live in the caret menu" framing). The card title row now also wraps + shrinks (flex-wrap + min-width:0 + overflow-wrap) so no inline child can ever blow the row width. Concern B (the bigger UX): cards now speak m's "cut the tree of possibilities" vocabulary via iconified state markers in the title row: - Optional event → ⊙ (timeline-state-icon--optional) - Hidden by user → 👁⃠ (timeline-state-icon--hidden) - Conditional anchor → already covered by the "abhängig von <parent>" chip on the date column (t-paliad-289); no duplicate marker. - CCR-included / appellant picks → already on the per-card chip. The legacy `.optional-badge` text chip and `.event-card-choices-unhide` inline chip are gone — both replaced by the icon language + popover entry. Renderer wires the unhide path with two contracts: - data-is-hidden="1" on the caret button when isHidden=true, so the popover knows to render the prominent unhide block on top. - Defensive fallback: if a rule's choices_offered was edited away after the user had already saved skip=true (so isHidden=true but choicesOffered is empty), the renderer synthesizes {skip:[true, false]} so the popover still has an un-hide path. CSS: - .timeline-item min-height 4rem → 2.75rem (less vertical air). - .timeline-content padding-bottom 1rem → 0.6rem (tighter gutter). - .timeline-item-header gains flex-wrap + min-width:0. - .timeline-name gains min-width:0 + overflow-wrap:anywhere (long German compounds wrap mid-word instead of overflowing). - New: .timeline-state-icon[--optional|--hidden] icon-style markers. - New: .event-card-choices-unhide-btn — prominent full-width lime pill inside the popover, midnight-text in both themes (matches the active-option pin from m/paliad#123). i18n: - state.optional.tooltip — "Optionales Ereignis" / "Optional event" - state.hidden.tooltip — "Ausgeblendet — über Optionen-Menü wieder einblenden" / "Hidden — restore via the options menu" - choices.unhide.chip kept (now used as the popover button label). Tests: 27 → 29 tests in verfahrensablauf-core.test.ts. Old isHidden inline-chip cases replaced by state-icon + caret-data-is-hidden contract cases. Added defensive-fallback case for the synthesized skip offer. Added regression guard that the legacy .event-card-choices-unhide class is no longer emitted. Added optional-priority → ⊙ icon contract pair. Hard rules respected: - Title + date + Rule citation unchanged (m likes these). - Click-to-edit on date span (.frist-date-edit) untouched. - Conditional rendering (t-paliad-289 chip + dotted border) untouched. - Per-card actions (skip, appellant pick, include-CCR, unhide) all reachable via the caret popover. go build ./... && go test ./internal/... && cd frontend && bun run build && bun test — all green (181 tests). |
|||
| 293e612582 |
feat(projection): IsConditional for uncertain-anchor rules (t-paliad-289)
Rules anchored on uncertain triggers (R.109 backward-anchor without oral-hearing date; R.118(4) without validity decision; R.262(2) without recorded Vertraulichkeitsantrag) previously rendered concrete dates fabricated off the trigger date. Add IsConditional projection flag so the SmartTimeline + Verfahrensablauf surfaces "abhängig von <parent>" instead of a misleading date. Backend (fristenrechner.go): - Add IsConditional + ParentRuleCode/Name/NameEN to UIDeadline. - Pre-pass populates courtSet from rule.is_court_set=true BEFORE the main loop, so order-of-evaluation in sequence_order no longer matters for the parent-court-set check. Fixes R.109(1) "Antrag auf Simultanübersetzung" (sequence_order=45 < Mündliche Verhandlung's sequence_order=50): the timing='before' backward arithmetic was computing 1 month before the trigger date because the court-set parent hadn't been classified yet. - Set IsConditional=true on every IsCourtSetIndirect branch (catches R.109 backward + R.118(4) cons_orders chain off the decision). - Set IsConditional=true for priority='optional' + primary_party='both' rules whose data-model parent is the trigger anchor (covers R.262(2) confidentiality_response: the data anchors on SoC, but the real trigger is the opposing party's confidentiality motion which may never happen). Suppressed by IsOverridden so user anchors win. Backend (projection_service.go): - Add IsConditional to TimelineEvent + propagate from UIDeadline. - New Status="conditional" for projected rows; clears Date, populates DependsOnRuleCode/Name from UIDeadline.ParentRule* so the row carries the "abhängig von <parent>" payload even when the parent has no computed date for annotateDependsOn to discover. Frontend (verfahrensablauf-core.ts + CSS + i18n): - CalculatedDeadline gains isConditional + parentRule* fields. - deadlineCardHtml renders "abhängig von <parent>" chip with click-to-edit affordance in place of the date column when isConditional=true. IsConditional wins over IsCourtSet for the date column (they overlap; "abhängig von <parent>" names the specific blocker). - .timeline-item--conditional / .fr-col-item--conditional CSS: dotted border + faded text so the conditional state reads at glance. - Replaced escHtml's DOM-backed implementation with a pure-JS regex escape so the module is testable in bun test without jsdom (the old form forced fixtures to leave several fields empty just to avoid the DOM dependency). Tests: - TestApplyLookaheadCap_ConditionalRowsPassThrough: pure-function lock that conditional rows pass through applyLookaheadCap untouched (don't count against ProjectedTotal/Shown, don't get capped). - TestUIDeadline_IsConditional_UncertainAnchors (TEST_DATABASE_URL): asserts R.109(1)/(4), R.118(4) chain, and R.262(2) all render IsConditional=true with empty DueDate + populated ParentRule*; SoD stays non-conditional; override on the oral hearing flips R.109(1) back to concrete date. - 4 new bun tests for the conditional rendering branches in deadlineCardHtml. UX path verified by tests + manual review of the live rule corpus: opening a UPC inf project without oral-hearing date now surfaces R.109(1) + R.109(4) as conditional; recording the Vertraulichkeitsantrag (anchoring R.262(2) via the existing "Datum setzen" flow) flips it back to a concrete date. go build / go test / bun test / bun run build all clean. |
|||
| 9d3325bd88 | Merge: t-paliad-291 — dark-mode lime-chip contrast fix across 6 selectors (m/paliad#123) | |||
| 18d2e743ba |
fix(styles): dark-mode contrast on lime-active chips (t-paliad-291)
Six surfaces paired a lime background with var(--color-text), which
flips to cream in dark mode and collapses contrast on the high-luminance
brand lime. Switch them to var(--color-accent-dark) — the design token
already defined to stay midnight in both themes as the WCAG-AA fg on
lime.
Affected:
- .event-card-choices-option--active (Berufung durch … popover —
m's primary report on m/paliad#123)
- .fristen-row.is-active .fristen-row-num
- .form-hint-badge
- .paliadin-widget-send-btn
- .smart-timeline-anchor-submit
- .admin-rules-chip.active
Lime hue and non-active states untouched.
Refs: m/paliad#123
|
|||
| 07d2eb472c | Merge: t-paliad-287 — submission form revision (Frist drop + grouped sections + Add Party + DB picker) (m/paliad#119) | |||
| 7cdccd55ae |
feat(submission-draft): grouped sections + per-side Add Party with DB picker (t-paliad-287)
Restructures the submission-draft sidebar per m's m/paliad#119 review. Three changes on the variable form (Part B): - VARIABLE_GROUPS collapses into four lawyer-facing sections: Mandant & Verfahren (firm.* + project.* + procedural_event.*), Parteien (manual {{parties.<role>.*}} overrides), Frist (the now-internal deadline.* block, COLLAPSED by default since the skeletons no longer render it), Sonstiges (today.* / user.* trim). - Group sections are click-to-collapse via a sticky state map; the Frist + Parteien-override sections open closed so the visible form stays tight on first load. - The legacy {{rule.*}} aliases drop off the sidebar — still resolved by SubmissionVarsService for old templates, no longer surfaced as override rows (they cluttered the form and the canonical procedural_event.* names cover the same ground). Multi-party + Add Party (Part C): - The party picker now renders all three role buckets (claimants / defendants / others) even when empty, so the lawyer can populate via Add Party. The block is hidden only when no project is attached. - Each side gets a "+ Partei hinzufügen (Klägerseite / Beklagtenseite / Weitere Parteien)" button that opens an inline panel with two tabs: - Manual entry — name, role (pre-filled from side), representative. Submits to POST /api/projects/{id}/parties, creating a real paliad.parties row that immediately surfaces in available_parties. - Aus DB übernehmen — debounced (200ms) search against the new GET /api/parties/search endpoint. Returns hits across every visible project with project_title + reference for context. Already-on-this-project rows are filtered out client-side. Picking a hit clones name/role/representative into a fresh row on the current project — the simplest semantics that survives the paliad.parties.project_id NOT NULL contract while honouring m's "no manual re-typing" requirement. - Newly-added parties land in selected_parties immediately so the new party is rendered in the next preview round-trip without an extra click. Implicit-"all" default is preserved (empty selected_parties still means "every party on the project, including this new one"). - Search-result repaints reach only into the <ul>, not the whole picker — keeps focus + selection on the search input across keystrokes. CSS: - Collapsible-section caret rotation, busy/disabled form states, tab highlights, DB-picker result rows with project chip + hover, all inherit the existing lime-tint accent so the new affordances look native to the editor. TSX: - Comment update on the parties block; no structural change. The bilingual hint copy in i18n.ts now nudges towards Add Party. |
|||
| c3eaa9b1d4 | Merge: t-paliad-290 — show-hidden toggle + un-hide chip on Verfahrensablauf (m/paliad#122) | |||
| 80883eaac5 |
feat(verfahrensablauf): re-surface hidden optional events — show-hidden toggle + un-hide chip (t-paliad-290)
m/paliad#122. atlas's #96 Slice A added per-card 'Überspringen' but no un-skip path — hidden cards just disappeared from the timeline. This adds the missing return path: - CalcOptions.IncludeHidden (default false) tells the calculator to re-surface skipRules entries as faded rows instead of dropping them. When true, the rule renders with UIDeadline.IsHidden=true and the descendant-suppression cascade is bypassed so children compute their dates off the un-suppressed parent. - UIResponse.HiddenCount always reflects the projection's hide count (gate-passed rules whose submission_code is in skipRules) so the "Ausgeblendete (N)" badge stays accurate regardless of toggle state. - /tools/verfahrensablauf gets a "Ausgeblendete anzeigen" checkbox next to the perspective + appellant selectors. URL-driven (?show_hidden=1) so the state is shareable and survives reload. The row hides itself on projections with zero hidden cards. - Hidden cards render via .timeline-item--hidden / .fr-col-item--hidden (opacity 0.55 + dotted border, mirroring the existing --skipped fade) and carry an inline "Wieder einblenden" chip. Clicking the chip removes the skip choice via the page's existing attachEventCardChoices remove callback (URL state + recalc included) and runs through a new delegated handler in event-card-choices.ts. - 3 new i18n keys (DE+EN): choices.show_hidden.label, choices.show_hidden.count, choices.unhide.chip. The skip-choice storage shape (paliad.project_event_choices, atlas's table) is unchanged — un-hide is just a delete of the skip row. Tests: 3 new bun-test cases pin the chip contract (emits on isHidden= true with submission_code, suppressed otherwise); go test ./internal/... + bun run build clean. |
|||
| 0e1f62e375 |
feat(verfahrensablauf): replace 'Beide' chip with 'Nicht festgelegt' (t-paliad-288)
The Verfahrensablauf side selector offered Klägerseite / Beklagtenseite / Beide. 'Beide' is legally impossible (no party is on both sides) — the state being modelled is "perspective not yet picked", not "both sides". Rename the chip to 'Nicht festgelegt' (DE) / 'Undefined' (EN) without changing the underlying state value or projection behaviour. - frontend/src/verfahrensablauf.tsx: chip label flips to deadlines.side.undefined; add inline hint chip "Wählen Sie eine Seite, um die Spalten zu fokussieren." next to the radio cluster, shown only while no side is picked. - frontend/src/client/verfahrensablauf.ts: sideLabelI18n() returns the new key for null; syncSideHintVisibility() toggles hint display from initPerspectiveControls, the side-radio change handler, and showSideRadioCluster (chip→radio override path). - frontend/src/client/i18n.ts: rename deadlines.side.both → deadlines.side.undefined (DE: Nicht festgelegt, EN: Undefined); add deadlines.side.hint in both languages. - frontend/src/i18n-keys.ts: rename in the union, keep alphabetical order. - frontend/src/styles/global.css: .side-radio-cluster becomes inline-flex so the hint sits next to the toggle; .side-hint styled muted+italic. URL backward-compat: ?side=both is already silently treated as null by readSideFromURL (only accepts claimant|defendant) — same column behaviour as before, no migration needed. projects.field.our_side.both is a different concept (a project being a multi-party participant) and stays untouched. Tests: 17/17 in verfahrensablauf-core.test.ts still pass; the "default (no opts) mirrors 'both' rules into ours AND opponent" case already covers the unchanged null-side projection. Go build + tests clean. Frontend build clean (i18n scan: 2901 keys, data-i18n attributes clean). m/paliad#120 |
|||
| e4c694e01c |
mAi: #108 - t-paliad-276 submission generator language selector (DE/EN)
Per-draft `language` column drives the .docx output language for the
submission generator. The lawyer picks DE or EN on the draft editor's
sidebar; the generator selects the language-matched template variant
(falling back through {code}.{lang} → {code} → _skeleton.{lang} →
_skeleton → letterhead) and resolves language-aware variables
({{procedural_event.name}} → name_de vs name_en).
Schema (mig 130 — bumped from 129 to deconflict with atlas's #96):
- paliad.submission_drafts.language text NOT NULL DEFAULT 'de'
CHECK IN ('de','en'). Existing rows inherit 'de' via the default,
preserving every legacy draft's behaviour byte-for-byte.
Backend (Go):
- SubmissionVarsContext.Lang overrides the user's UI lang. Build()
uses it when set; falls back to user.Lang otherwise — Slice 1's
format-only /generate path keeps working unchanged.
- SubmissionDraftService.BuildRenderBag now threads draft.Language
through. Create/EnsureLatest seed from the UI lang (DE default).
- DraftPatch.Language landed; Update validates and rejects values
outside {de,en}. Project-scoped + global PATCH endpoints both
surface the field.
- resolveSubmissionTemplate(ctx, code, lang) replaces the lang-less
predecessor. Returns the matched tier (per_code_lang / per_code /
skeleton_lang / skeleton / letterhead) so the editor knows whether
to surface the "Fallback: universelles Skelett" notice.
- fileRegistry registers the EN skeleton sibling (`_skeleton.en.docx`)
alongside the DE one; per-code EN variants land in a parallel
submissionTemplateENRegistry (empty for now — EN templates land per
HLC authoring). 404s from Gitea fall through silently.
- /api/projects/{id}/submissions/{code}/generate accepts
`?language=de|en` query override (one-shot path, no draft row to
pull the column from); defaults to the user's UI lang.
Frontend (TS/JSX):
- DE/EN radio above the variables list in the draft editor sidebar.
Switching the radio PATCHes `language` and the server returns the
freshly-resolved bag + preview HTML so the lawyer sees EN values
immediately.
- Fallback notice ("Fallback: universelles Skelett (keine
sprachspezifische Vorlage)") shows when the resolved tier doesn't
match the requested language.
- 4 new i18n keys (DE + EN) + CSS for the toggle.
Tests:
- normalizeDraftLanguage covers DE/EN/case/whitespace/unknown.
- addRuleVars language-pick test pins procedural_event.name and the
rule.name alias to the language-matched value.
- languageFallback truth table covers all 10 (lang × tier) combos.
Build hygiene: go build/vet/test clean; bun run build clean.
|
|||
| c6267e4e6d | Merge: t-paliad-277 — submission party selector + import-from-project (mig 131) (m/paliad#109) | |||
| 8e696487e0 |
Merge: t-paliad-279 — Verfahrensablauf form reorder, party-after-proceeding-type (m/paliad#111)
# Conflicts: # frontend/src/client/verfahrensablauf.ts |
|||
| 4fc3005db8 |
mAi: #109 - t-paliad-277 submission generator party selector + import-from-project
Multi-select party picker on the dedicated submission draft editor —
lawyer picks which of the project's parties to mention in this
specific submission. Adds the t-paliad-277 variable-bag multi-party
shape ({{parties.claimants}}, {{parties.claimant.0.name}}) while
keeping the legacy flat aliases ({{parties.claimant.name}}) for every
existing .docx template authored before the rename.
Surfaces an explicit "Aus Projekt importieren" button + last-imported
timestamp at the top of the variable sidebar so the lawyer can re-pull
project-derived variables (project.*, parties.*, deadline.*,
procedural_event.*, rule.*) when the project data drifts away from the
saved draft overrides. firm.*, today.*, user.* overrides survive the
import — those values aren't sourced from the project record.
Schema: mig 131 adds two columns to paliad.submission_drafts:
- selected_parties uuid[] DEFAULT '{}'::uuid[]
Empty = include every party (legacy default).
Non-empty = restrict to the subset, grouped by role at substitution.
- last_imported_at timestamptz NULL
Bumped each "Aus Projekt importieren" click; surfaced in UI.
Backend:
- SubmissionVarsContext gains SelectedParties — filterPartiesBySelection
restricts the resolved bag before role bucketing.
- addPartyVars emits THREE coexisting forms per role: comma-joined
(parties.claimants), indexed (parties.claimant.0.name), and flat
legacy (parties.claimant.name → first selected claimant). Flat
aliases are kept forever per the issue's backward-compat contract.
- SubmissionDraftService.ImportFromProject strips overrides for
project-derived prefixes and bumps last_imported_at; rejects
project-less drafts (nothing to import from).
- New endpoint POST /api/submission-drafts/{id}/import-from-project.
- DraftPatch + PATCH handlers accept selected_parties.
- submissionDraftView now ships available_parties so the editor can
render the picker without an extra round-trip.
Frontend:
- submission-draft.tsx: new import-row + parties block in the sidebar.
- client/submission-draft.ts: paintImportRow / paintPartyPicker /
onPartySelectionChange / onImportFromProject; group parties by
role bucket (claimant / defendant / other) with DE+EN role-string
matching to mirror the backend bucketing.
- 3 new i18n keys (DE+EN): import.button, parties.title, parties.hint.
- CSS for the picker + import row in global.css.
Tests: 6 new unit tests in submission_vars_parties_test.go covering
the multi-party bag emission, German role-string bucketing, flat-alias
first-of-role resolution, empty-selection-means-all default, non-empty
restriction, and the isProjectDerivedKey policy that powers the
import path.
Build hygiene: go build/vet clean; go test -short ./internal/... pass;
bun run build clean (2876 i18n keys, scan clean).
|
|||
| a6d0acbcb4 |
mAi: #111 - t-paliad-279 — Verfahrensablauf form reorder + project auto-fill chip
Reorder Verfahrensablauf 'Browse a proceeding' so the user-input flow matches the importance hierarchy: proceeding-type → side → appellant → date / court / flags. Side was previously below the date input; it is the most-defining input after proceeding-type, so it belongs above. - frontend/src/verfahrensablauf.tsx: move .verfahrensablauf-perspective block above .date-input-group inside step-2. Wrap the side radio cluster in #side-radio-cluster and add a sibling #side-chip (hidden by default) that the client swaps in when a project pre-fills the side. Add a 1px divider between perspective and date-input groups. Update step-2 heading from "Ausgangsdatum eingeben" → "Perspektive und Datum" to honestly describe both controls now under the heading. - frontend/src/client/verfahrensablauf.ts: read ?project=<id> on init, fetch /api/projects/<id>, map our_side onto the side axis (mirrors fristenrechner.ts ourSideToPerspective: claimant/applicant/appellant → claimant, defendant/respondent → defendant, else null) and render the side row as a read-only chip + "Andere Seite wählen" override link. The chip respects ?side= as an explicit user pick — URL wins over project auto-fill, same precedence as fristenrechner. Override swaps back to the radio cluster and drops ?project= from the URL. Side-chip label is language-aware via onLangChange. - frontend/src/styles/global.css: .verfahrensablauf-step2-divider (1px hr between perspective and date blocks); .side-chip / -tag / -value / -override styles mirror .proceeding-summary's chip look so the two read as the same visual family. - frontend/src/client/i18n.ts + i18n-keys.ts: 3 new keys (deadlines.step2.perspective, deadlines.side.from_project, deadlines.side.override) in DE + EN. URL state stays backward-compatible: ?side= and ?appellant= survive the reorder unchanged. Adding ?project= opts in to auto-fill; without it the page behaves identically to before. No backend / projection logic change. |
|||
| 96eab90044 | Merge: t-paliad-280 — search input icon-text padding fix (m/paliad#112) | |||
| 5348cb548f |
mAi: #112 - fix Fristenrechner Akte-picker icon overlap
The Akte-picker (Step 1) wraps its magnifying-glass icon + input in a flexbox row (`.fristen-step1-search-row`) with `gap: 0.5rem`, expecting the icon to participate in the flex layout. But the shared `.fristen-search-icon` rule (used by the B2 search input) sets `position: absolute; left: 0.875rem;` — and the step1-scoped override only tweaked color + flex-shrink without resetting `position`. Result: the icon was absolutely-positioned out of the flex flow and overlapped the input text (since `.fristen-akte-search` has no padding-left). Resetting `position: static` for the step1 context lets flexbox + gap handle the spacing naturally — same pattern as `.fristen-row-search-panel-input-wrap`, which already works. Audited other search inputs with leading magnifying-glass icons: - `.glossar-search` (Glossary, Courts, Links, Team, AdminTeam, AdminEventTypes) — wrap `.glossar-search-wrap` is `position: relative`, input has `padding: 0.65rem 4.5rem 0.65rem 2.5rem`. Fine. - `.projects-search-input` (/projects index) — wrap is `position: relative`, input has `padding: 0.5rem 0.75rem 0.5rem 2.4rem`. Fine. - `.fristen-search-input` (Fristenrechner B2) — wrap `.fristen-search-row` is `position: relative`, input has `padding: 0.75rem 2.5rem 0.75rem 2.6rem`. Fine. - `.fristen-row-search-panel-input` (Fristenrechner row-search panel) — pure flex layout with `gap`, icon non-positioned. Fine. - `.sidebar-search-input` (global sidebar search) — pure flex layout. Fine. - Other search inputs (`event-search-input`, `event-type-search`, `submissions-new-search`, submissions index) have no leading icon. N/A. |
|||
| b1340e2be4 | Merge: t-paliad-278 — date-range picker 3-column layout Past/NOW/Future (m/paliad#110) | |||
| 1292aa575d | Merge: t-paliad-265 — per-event-card choices Slice A+B (popover + CCR + projection engine, mig 129) (m/paliad#96) | |||
| 87c200a47e |
feat(t-paliad-265): caret + popover + chip on Verfahrensablauf cards
m/paliad#96 — frontend wiring of the per-event-card choice flow on both consumer surfaces. Shared rendering core (verfahrensablauf-core.ts): - CalculatedDeadline gains choicesOffered + appellantContext (mirror the new server fields). - deadlineCardHtml emits a ▾ caret next to the date when a rule carries a non-empty choicesOffered, plus an inert chip span next to the title that the popover module rehydrates after every render. - bucketDeadlinesIntoColumns prefers appellantContext over the page-level appellant for "both" rows when the per-card context is set to claimant or defendant. "both" / "none" / "" all fall back to the existing collapse logic. New test cases cover all three paths. - CalcParams + calculateDeadlines pass projectId / perCardChoices through to the backend. New module (client/views/event-card-choices.ts): - attachEventCardChoices wires a delegated click handler on the result container; the caret opens a body-anchored popover with one block per choice-kind the rule offers (appellant: 4 radio-style buttons; include_ccr + skip: 2-way toggle). - Active picks render as small chips on the card title; reseedChips() repaints them after every renderResults() innerHTML rewrite. - Skipped rows fade to 55% opacity via the timeline-item--skipped class. Page wiring: - /tools/verfahrensablauf (unbound): commits mutate an in-memory list + the ?event_choices= URL param, then schedule a recalc. Shareable via link, no persistence — same idiom as ?side= / ?appellant=. - /tools/fristenrechner (project-bound): commits POST/DELETE to /api/projects/{id}/event-choices. The next calculate() call sends projectId so the server folds the persisted choices in. i18n: 17 new keys under choices.* (DE primary + EN secondary). Caret title, appellant/include_ccr/skip block titles + value labels, chip labels, reset action, commit error toast. CSS: caret, popover, options, chip parts, skipped-row fade. Tests: 3 new bucketer cases covering AppellantContext propagation (157 frontend tests pass). |
|||
| 4f910e31ea |
mAi: #110 - t-paliad-278 — 3-column date-range picker (Past/NOW/Future, closeness-to-NOW sort)
Restructures atlas's #79 horizontal row into 3 vertical columns: Past (left), NOW (middle), Future (right). Each column sorts by closeness to NOW (closest at top, farthest at bottom) — the picker now reads as a spatial map of time around the current moment instead of a flat horizontal fan. Layout Vergangenheit ⌖ Zukunft Letzte 7 Tage Heute Nächste 7 Tage Letzte 30 Tage Alles Nächste 30 Tage Letzte 90 Tage Nächste 90 Tage Ganze Vergangenheit Ganze Zukunft Changes - date-range-picker.ts — renderPanel builds .date-range-grid with three vertical .date-range-col children. Past column iterates PAST_HORIZONS reversed (past_1d → past_all top-to-bottom). NOW column hosts next_1d ("Heute") + any ("Alles") plus a ⌖ glyph header. Future column iterates NEXT_HORIZONS minus next_1d (which moved to NOW). Legacy "all" horizon still lights up the Alles chip for saved-Custom-View back-compat. - global.css — replace .date-range-row/.date-range-fan/.date-range- center{,-btn,-glyph,-label} with .date-range-grid + .date-range-col + .date-range-col-heading. Chips stretch to 100% column width for a clean vertical stack. Panel widened from 32rem to 34rem so "Ganze Vergangenheit" never wraps. Mobile (max-width 540px) collapses the grid to a single column, preserving in-column sort. - i18n.ts — next_1d label fixed from "Morgen"/"Tomorrow" to "Heute"/ "Today". next_1d's bounds are [today, tomorrow) = single-day today, so the prior label was semantically wrong; renaming aligns the label with the bounds and matches m's "Heute" spec for the NOW column. - axes.ts — DEFAULT_TIME_PRESETS updated to match m's spec (4 past + Heute + Alles + 4 future + custom). projects-detail.ts continues to override via timePresets for its past-only Verlauf surface. 12 horizon values in the union remain unchanged — PAST_HORIZONS / NEXT_HORIZONS registries and parseURL still accept past_1d / past_14d / next_14d for back-compat with saved URLs; the default picker UI just no longer surfaces chips for them. Surfaces that want the finer granularity can opt back in via timePresets. Verification - bun test src/client/date-range-picker-pure.test.ts — 38 pass - bun run build — i18n + branding + bundle clean - go build ./... — clean - go test ./internal/... — pass |
|||
| d4df81e374 |
mAi: #106 - t-paliad-274 — bidirectional draft editor link + click-field-highlights
Extension of #92 (m/paliad/issues/106). Two related polish fixes for the submission draft editor's preview ↔ sidebar wiring. Concern A — link persists after fill (regression coverage + UX visibility) Audited the Go renderer: substituteInTextNodes / substituteAcrossRuns already pass both filled and missing values through htmlPreviewWrapper, so the <span class="draft-var" data-var="…"> wrapping is present for every substituted placeholder regardless of source (resolved bag, lawyer override, missing marker). What looked broken to m was a visibility problem: the always-on rgba(198, 244, 28, 0.12) tint is imperceptible against the serif preview prose, so a filled value reads as plain text and the user concludes "the link is gone". Added TestRenderHTML_WrapsOverriddenValueSameAsResolved that pins the invariant explicitly — an override (project.case_number = "UPC_CFI_ 42/2026") and a resolved value (firm.name = "HLC") both end up in matching draft-var spans. Locks future refactors out of dropping the wrap on either path. CSS rewrite per m's "prose stays clean when not interacting" guidance (issue body): drop the always-on background; on hover of a --has-input span, layer a dotted-underline + brighter lime tint so the click affordance reveals itself. Missing markers carry their own [KEIN WERT: …] / [NO VALUE: …] gap-text and don't need extra visual. Concern B — sidebar-field-focus → preview-occurrence highlight (new) Reverse direction of the click-to-jump from #92. focusin on any .submission-draft-var-input applies .draft-var--active to every matching span in the preview; focusout (or focus shift via Tab) clears them. Sticky-while-focused, not a one-shot flash — the lawyer can scan "where does this variable land in my prose?" while the field stays focused. New CSS class .draft-var--active uses a brighter lime + box-shadow ring so all occurrences pop at once. Handlers are wired in paintVariables and re-applied at the end of both paintVariables AND paintPreview because: - paintVariables runs after autosave and re-creates inputs via innerHTML, so the focusin listener attached to the old input is gone; restoreVarFocus puts focus back programmatically without firing focusin again. We re-apply explicitly to bridge. - paintPreview blows away the preview HTML on every autosave, so any prior --active class is gone too. Re-apply based on the currently-focused sidebar input. Files internal/services/submission_merge_test.go — new regression test frontend/src/client/submission-draft.ts — focus handlers + re-apply frontend/src/styles/global.css — draft-var rewrite, --active Hard rules - .docx export path unchanged (Render passes nil wrap, covered by existing TestRender_DocxOutputUnchangedByPreviewWrap). - Both directions survive autosave-driven preview re-renders (see paintPreview re-apply + paintVariables re-apply). - go build ./... && go test ./internal/... && bun run build all clean. |
|||
| f0c343c638 | Merge: t-paliad-267 — Auto-rule resolved name on its own row in deadline form (m/paliad#98) | |||
| aa2f4aacc6 |
mAi: #98 - move Auto-rule resolved name to its own row
The Auto-mode resolved rule name was rendered as an inline-flex pill that sat visually crammed next to the [Eigene Regel eingeben] toggle. Promote .rule-mode-auto to a full-width block-level flex row (width: 100%, margin-top: 0.35rem) so it sits cleanly on its own line beneath the toggle, and render the rule label via the canonical formatRuleLabelHTML helper so the citation gets the muted-secondary styling from rule-label.ts. Applies to both /deadlines/new and /deadlines/:id edit form. Custom mode (free-text input) is unaffected — the input already filled the column. Refs: m/paliad#98 (t-paliad-267), addendum to t-paliad-258 / m/paliad#89. |
|||
| 013facb9db |
mAi: #100 - paliadin trigger: lift above bottom-nav at <=767px (t-paliad-269)
The Paliadin floating-button trigger was overlapping the PWA bottom-nav on mobile because its lift rule was scoped to @media (max-width: 640px) while .bottom-nav itself appears at @media (max-width: 767px). Phones in landscape and small tablets between those breakpoints saw the desktop bottom: 20px and got covered by the navbar. Two changes: - Widen the trigger lift breakpoint to 767px (matches .bottom-nav). - Replace hardcoded 72px with calc(var(--bottom-nav-height) + 16px + env(safe-area-inset-bottom, 0px)) so the math tracks the navbar height variable already used elsewhere (e.g. dashboard-save-toast). The drawer's full-screen rule (.paliadin-widget-drawer width: 100vw) stays at <=640px — only the trigger lift moves. Desktop layout (bottom: 20px) unchanged; widget open/close animation unchanged. |
|||
| 31d78526cf |
feat(date-range-picker): t-paliad-248 — symmetric picker + filter-bar wiring
Slice A complete. Builds on the additive backend constants (commit
|
|||
| 7e66da8def |
mAi: #92 - t-paliad-261 — submission-draft autosave focus + click-variable-in-preview jump
Two related editor polish fixes.
(A) Autosave-refresh focus preservation
paintVariables() replaces every input via innerHTML, blowing away
the focused-input reference and dropping the cursor mid-edit. Fix:
capture the active variable input's data-var key + selectionStart/
End/Direction before the repaint, restore on the new element after
(by data-var lookup + setSelectionRange). Cursor stays put across
autosave, rename, and reset cycles. Works for <input> and
<textarea> via the shared selectionRange contract.
(B) Click variable in preview → jump to sidebar input
Go renderer wraps every substituted placeholder value in the HTML
preview with <span class="draft-var" data-var="key">…</span>.
Implemented via a valueWrapperFn plumbed through
substituteInDocumentXML → substituteInTextNodes /
substituteAcrossRuns → replacePlaceholders. RenderHTML passes
htmlPreviewWrapper which marks values with three PUA sentinels
(U+E100/U+E101/U+E102) that emitTextWithDraftVars converts to the
span pair inside docXMLToHTML. Missing-marker text is wrapped too
so a clicked [KEIN WERT: foo] jumps to the empty field.
Render() (.docx export) passes nil for wrap → output is byte-
identical to pre-261. New test
TestRender_DocxOutputUnchangedByPreviewWrap asserts the .docx never
carries draft-var/data-var markup or PUA sentinels.
Client wireDraftVars() adds .draft-var--has-input only to spans
whose key resolves to a sidebar input — derived variables (e.g.
today.iso) stay non-clickable. Click handler:
scrollIntoView(smooth, center) → focus + select after 50ms →
1.2s lime flash on the row.
Keyboard accessible (Enter / Space) with role=button + aria-label.
CSS adds a subtle lime tint to every .draft-var so the user sees
what was substituted; --has-input layers cursor: pointer + brighter
hover background. Flash animation respects prefers-reduced-motion
via a steps(1, end) fallback.
Tests: TestRenderHTML_ExtractsParagraphsAndFormatting updated to
assert the new span wrap. New tests for missing-marker wrap +
.docx-path-untouched. Go + frontend builds clean.
|
|||
| ef21e43375 | Merge: t-paliad-260 — submission-draft mobile layout (m/paliad#91) | |||
| 4cb99fb627 |
mAi: #91 - t-paliad-260 — submission-draft mobile layout: drop sticky on sidebar at ≤900px
Approach A: stack vertically. At single-column widths the variable
editor was sticky + max-height: calc(100vh - 2rem), so it stayed
pinned at the top of the viewport while the user scrolled down to
read the preview, visually overlaying the preview pane.
Add a media-query override that switches the sidebar to position:
static, max-height: none, overflow-y: visible at the same ≤900px
breakpoint where the grid already collapses to one column. The
sidebar now reflows above the preview, takes its natural height,
and scrolls away as the user moves down — no overlay, no
horizontal scroll. Desktop (≥901px) layout unchanged: sidebar
keeps its sticky behavior side-by-side with the preview.
Verified at 375 / 414 / 768 / 1280 px in Playwright on the
populated editor body — same renderer serves both URL shapes
(/submissions/draft/{id} and
/projects/{id}/submissions/{code}/draft/{id}).
|
|||
| 452ccdf127 | Merge: t-paliad-258 — Deadline form Auto/Custom rule field + canonical rule-label display (m/paliad#89) | |||
| 045accc6d9 |
mAi: #89 - deadline rule field binary Auto/Custom + canonical rule-label display
t-paliad-258. m's verdict on t-paliad-251's rule UI: "too many options"
(4 'Oral hearings' across courts, etc.). Replace the full deadline_rules
catalog dropdown + sort selector with a binary model and unify the rule
display contract across every surface that prints a rule label.
Binary Rule field on the deadline form
- Auto (default): rule_id is derived from the chosen Type. The resolved
rule renders read-only as 'Auto | <Name · Citation>' next to the
field. No catalog picker, no sort options.
- Custom: free-text input. Stored as deadlines.custom_rule_text (new
nullable column, migration 122). Mutually exclusive with rule_id at
the persistence boundary.
- Toggle link flips between modes. Re-toggling to Auto re-resolves from
the current Type — no stale state.
Schema + service (additive)
- migration 122 adds paliad.deadlines.custom_rule_text (nullable).
Existing rows: empty custom_rule_text + non-null rule_id = Auto-
equivalent. Both NULL = "keine Regel" (consistent with today).
- models.Deadline.CustomRuleText + service SELECTs include the column.
- CreateDeadlineInput accepts custom_rule_text; the service drops it
when rule_id is set (catalog wins; simple invariant at the boundary).
- UpdateDeadlineInput grows a {RuleSet, RuleID, CustomRuleText} triple.
RuleSet=true is the discriminator so absent fields don't overwrite
the row (PATCH semantics). RuleID and CustomRuleText are mutually
exclusive in one request; service rejects "both set".
- EventListItem (the /api/events union) carries CustomRuleText so list
surfaces can render it.
Frontend: deadlines-new
- Drop the rule <select>, the by_proceeding/by_court/alpha sort
dropdown, the override-warning slot, and the collapsed-by-Regel Typ
view. Strip the (Rule→Type) auto-fill machinery — direction is now
one-way (Type → Auto-resolved Rule).
- Keep Type→Rule resolution: resolveAutoRuleForType picks the canonical
rule by project's proceeding, then jurisdiction match, then first
candidate. Same logic, just re-aimed at the read-only display.
- Standardtitel preserves the chain (event type → Auto rule label →
Custom text → proceeding → fallback) so the recipe still produces a
sensible title even when Custom is used.
Frontend: deadlines-detail
- Read-only display: catalog rule → Name · Citation, else
custom_rule_text + Custom badge, else legacy rule_code, else "—".
- Edit mode: mirror the create form with the Auto/Custom toggle.
enterEdit initialises the mode from the persisted deadline; Save
PATCHes with rule_set:true + the chosen rule pointer.
Rule-label addendum (m's 14:31 follow-up)
- Canonical contract everywhere: Name primary, Citation muted secondary
("Notice of Appeal · UPC.RoP.220.1"). Custom rules render the text
with a "Custom" pill.
- New frontend/src/client/rule-label.ts exports formatRuleLabel /
formatRuleLabelHTML / formatCustomRuleLabelHTML — one helper per
shape (plain text vs muted-citation HTML).
- Wired into: deadlines-new Auto display, deadlines-detail read +
Standardtitel, events.ts ruleDisplay (REGEL column on /events),
projects-detail.ts Fristen table, views/shape-list.ts generic
rule column.
- Verfahrensablauf (views/verfahrensablauf-core.ts) already renders
name + citation chip separately and matches the canonical pattern;
no change needed. Schriftsätze table is column-shaped (name + code
in distinct columns) and out of scope per the addendum.
CSS
- New .rule-mode-auto / .rule-mode-custom / .rule-label-* family.
- Drop the dead .rule-sort-select rule and the .event-type-collapsed*
family (retired with the catalog dropdown).
i18n
- DE+EN. Remove 10 stale keys (rule.none, autofill, autofill_inline,
mismatch, override, override_warn, sort.*). Add 6 (auto_no_match,
auto_pick_type, custom_badge, custom_placeholder,
mode.toggle_to_auto, mode.toggle_to_custom).
Build hygiene
- go build + go test ./internal/... clean.
- frontend bun build clean (2803 keys, scan clean).
Out of scope (per issue)
- Promoting Custom entries back to the catalog ("save as new rule").
- Filtering/searching custom_rule_text in deadline lists.
- Touching the event-type browse modal (Part 1 of #82 — that stays).
Files
- internal/db/migrations/122_deadlines_custom_rule_text.{up,down}.sql
- internal/models/models.go
- internal/services/deadline_service.go (Create+Update+SELECT)
- internal/services/event_service.go (union projection)
- frontend/src/client/rule-label.ts (new helper)
- frontend/src/client/deadlines-new.ts (rewrite)
- frontend/src/client/deadlines-detail.ts (Auto/Custom editor + display)
- frontend/src/client/events.ts (REGEL column)
- frontend/src/client/projects-detail.ts (Fristen table cell)
- frontend/src/client/views/shape-list.ts (generic rule column)
- frontend/src/client/i18n.ts + i18n-keys.ts (DE+EN delta)
- frontend/src/deadlines-new.tsx (strip dropdown+sort, add toggle)
- frontend/src/deadlines-detail.tsx (Auto/Custom edit slots)
- frontend/src/styles/global.css (rule-mode + rule-label families)
|
|||
| 538c2d2da9 | Merge: t-paliad-257 — Verfahrensablauf user-perspective column axis (Unsere Seite / Gericht / Gegnerseite) (m/paliad#88) | |||
| a9a9adbd2a |
mAi: #88 - Verfahrensablauf: column axis reframed to user-perspective
Replaces the misleading Proaktiv/Reaktiv column pair with a static
"Unsere Seite" / "Gericht" / "Gegnerseite" axis ("WE always on the
left", per m's t-paliad-257 ask). The side toggle now drives row
PLACEMENT into the ours/opponent buckets — the column labels stay
truthful regardless of which physical party occupies them.
Old framing lied half the time: Klägerseite is sometimes proactive
(filing the claim) and sometimes reactive (responding to a CCR),
so "Proaktiv (Klägerseite)" was wrong whenever the user's perspective
flipped. New axis is purely positional with semantic labels.
Changes:
- frontend/src/client/views/verfahrensablauf-core.ts:
• ColumnsRow fields proactive/reactive → ours/opponent.
• renderColumnsBody picks static "Unsere Seite" / "Gegnerseite"
labels — no more variant-by-side label keys.
• bucketDeadlinesIntoColumns routes the user's party into `ours`
when opts.side ∈ {"defendant"}; default (null) keeps the legacy
"we are claimant" fallback so claimant-on-left layout survives.
- verfahrensablauf-core.test.ts: rewritten expectations on the new
ours/opponent fields. Added two new tests pinning the WE-on-left
semantics and the side+appellant interaction (side=defendant +
appellant=claimant → "both" collapses into opponent).
- fristenrechner.ts: wires currentPerspective into renderColumnsBody
as `side` so the columns honour the chip-strip perspective.
Without this, a defendant-perspective user would see claimant
filings under the "Unsere Seite" header — the old code didn't
need the wire-up because the labels weren't perspective-aware.
- i18n.ts: replaces deadlines.col.proactive(.defendant) +
deadlines.col.reactive(.claimant) with deadlines.col.ours +
deadlines.col.opponent ("Unsere Seite"/"Client Side",
"Gegnerseite"/"Opponent Side"). Court key unchanged.
- i18n-keys.ts: regenerated key union.
- global.css: .fr-col-proactive/.fr-col-reactive renamed to
.fr-col-ours/.fr-col-opponent.
Out of scope (kept intact):
- Side and appellant URL-state plumbing.
- Appellant selector for Appeal-type proceedings (separate axis).
- Project-default side-from-our_side wiring — /tools/verfahrensablauf
has no project context, and /tools/fristenrechner already does this
via applyOurSidePredefine().
Build: bun run build clean (2794 keys), go build ./... clean.
Tests: 112 frontend tests pass (was 110, +2 new); all Go tests
cached green.
|
|||
| f24a90b722 | Merge: t-paliad-252 — Approval withdraw warning modal + edit-instead path (m/paliad#83) | |||
| 72b64140e9 |
mAi: #83 - approval withdraw warning modal + edit-instead path
t-paliad-252. Replace the silent confirm()-then-DELETE with a three-path
warning modal: Cancel / Edit event (primary) / Withdraw and delete
(destructive). The edit-instead path lets the requester revise the
in-flight entity without withdrawing the approval request.
Backend — new service method + endpoint
- ApprovalService.EditPendingEntity(requestID, callerID, fields):
- validates caller == requested_by AND status = pending
- reuses the existing wider counter-allowlist (buildCounterSetClauses
from SuggestChanges) — every editable field on the entity, not just
the date triggers
- applies the field updates to the entity row via applyEntityUpdate
(including the event_type_ids junction rewrite for deadlines)
- merges new fields into approval_requests.payload (jsonb) so the
approver inbox sees what was revised
- emits a distinct *_approval_edited_by_requester project_event so the
Verlauf surfaces the revision separately from the original *_requested
row and any decision row
- request stays pending; entity.approval_status stays pending
- POST /api/approval-requests/{id}/edit-entity
- Body: {"fields": {<entity-shape>}}
- Errors reuse the existing mapApprovalError mapping:
400 suggestion_requires_change, 403 not_authorized,
404, 409 request_not_pending
- Distinguishing audit event types per the spec:
- destructive Withdraw path: existing <entity>_approval_revoked
(no behaviour change — for CREATE deletes the entity, for UPDATE /
COMPLETE reverts to pre_image, for DELETE cancels the delete request)
- edit-instead path: new <entity>_approval_edited_by_requester
Frontend — shared withdraw warning modal
- frontend/src/client/components/withdraw-warning-modal.ts
- Built on the unified openModal() primitive (t-paliad-217 Slice A)
- Primary CTA "Termin bearbeiten" highlights the non-destructive path
- Secondary defaults to "Abbrechen" (handled by openModal)
- Destructive button "Endgültig zurückziehen und löschen" lives inside
the body (red, separated by a dashed border) so the safe path stays
visually primary in the footer
- Copy adapts per lifecycle:
CREATE → "Wenn Sie zurückziehen, wird die Frist/der Termin gelöscht."
UPDATE → "Ihre vorgeschlagenen Änderungen werden verworfen."
DELETE → "Der Eintrag bleibt bestehen."
Frontend — wiring on both detail pages
- deadlines-detail.ts + appointments-detail.ts:
- Replace confirm() in withdraw flow with openWithdrawWarningModal()
- Edit path: set module-level pendingEditMode = true + enter edit mode
(override existing pending-state freeze on appointments; expose
enterEdit() via late-bound pendingEnterEdit on deadlines)
- Save handler in pendingEditMode routes to /edit-entity instead of
PATCH /api/<entity>/{id} (which still 409s on pending state)
- Destructive Withdraw path: existing /revoke endpoint unchanged
- For CREATE-lifecycle revokes the entity is gone — bounce to the
/events list instead of trying to re-fetch (was reload() before)
i18n: +14 keys DE+EN under approvals.withdraw.* (modal title, primary,
destructive, cancel, lead.create.{deadline,appointment}, lead.update,
lead.delete, sub.create, sub.update, sub.delete)
CSS: .withdraw-warning-body + .withdraw-warning-{intro,sub,
destructive-row,destructive-btn} — lime-tint sibling palette consistent
with the existing form-hint pattern; destructive button uses .btn-danger.
Build hygiene:
- go build + go vet + go test ./internal/... clean
- frontend bun run build clean (2807 keys, +14 new, scan clean)
Files of note:
- internal/services/approval_service.go (EditPendingEntity + sortedKeys
helper; maps.Copy for the payload merge)
- internal/handlers/approvals.go (handleEditPendingEntity)
- internal/handlers/handlers.go (route registration)
- frontend/src/client/components/withdraw-warning-modal.ts (new shared
component)
- frontend/src/client/deadlines-detail.ts (initWithdraw rewrite + Save
pending-edit branch)
- frontend/src/client/appointments-detail.ts (withdrawAppointmentRequest
rewrite + Save pending-edit branch + form-freeze respects
pendingEditMode)
Out of scope (intentionally):
- Reopening already-deleted approval requests (the destructive path
stays final).
- Approval-request analytics / metrics.
- Notifying the original approval-requester via channel.
|
|||
| 50cd80a4a6 | Merge: t-paliad-255 — kill /events horizontal scroll on mobile (m/paliad#86) | |||
| 716f6d7ece |
fix(events): t-paliad-255 — kill /events horizontal scroll on mobile
A native <select> sizes itself to the widest <option> text. With long project titles in the matters filter, the select grew wider than the viewport and the /events page scrolled horizontally on mobile. The existing 480px media query forced .entity-select to width:100% on phones, but the 481-1000px range (tablet portrait + landscape phones) had no constraint at all and inherited the intrinsic select width. Fix: cap .filter-group and .entity-select at max-width:100% with min-width:0 so the cell can shrink to fit its flex container at every viewport. Desktop layout is preserved — normal-length options still sit in one row across the page; only pathological content (a single title wider than the row) wraps onto its own line. Approach: A — let the trigger respect its container at every width. Verified: zero horizontal scroll at 320 / 375 / 414 / 768 px with a realistic 130-character project title injected into the matters selector. Desktop (1280px) keeps all four filter-groups in one row. |
|||
| 1bf62c78e3 | Merge: t-paliad-251 — Deadline form overhaul (m/paliad#82) | |||
| 8caaf6a631 |
mAi: #82 - deadline form overhaul: type-modal filter chips, type→rule autofill, Auto mode, Standardtitel
t-paliad-251. Four bundled concerns from m's 2026-05-25 reports, one
worker, one branch.
Part 1 — Event-type browse modal (search + filters)
- Modal already had a search input; added court-type filter chips
(UPC / EPA / DPMA / DE / Allgemein) under the search.
- Chips render only the jurisdictions actually present in the data;
any future flavour lands at the end of the row.
- Active chip uses the lime-tint chip palette already established by
the .event-type-collapsed* family (t-paliad-165).
- Search input keeps autofocus; chip + search filters intersect.
Part 2 — Type → Rule auto-fill + sort options
- Inverted the existing rule.concept_default_event_type_id mapping
client-side: given a chosen event_type X, candidate rules are
those with concept_default_event_type_id === X.
- Resolution picks (1) exact match on the project's
proceeding_type_id, (2) jurisdiction match on the rule's
proceeding (EPA→EPO canonicalised), (3) first candidate.
- Sort dropdown next to the Rule label: by proceeding sequence,
by court (jurisdiction grouping with optgroup), alphabetical.
Defaults to "by court"; localStorage-persisted per browser.
- All sorts are client-side over the existing /api/deadline-rules
payload — no new endpoint.
Part 3 — Auto rule mode + clearer override warning
- Auto badge (.form-hint--auto, lime-tint pill + " — <rule name>")
surfaces whenever the Rule was derived from the chosen Type.
Disappears the moment the user manually picks a different rule.
- Override warning names BOTH sides + the actually-applied rule:
"Typ ergibt Regel: X. Gewählte Regel: Y. Es wird Y angewendet."
- Symmetric `lastAutoFilledRuleID` sticky-replace flag mirrors the
existing `lastAutoFilledEventTypeID` (t-paliad-165) so the auto-
fill only replaces its own previous suggestion, never a manual
pick.
- Collapsed Typ view (t-paliad-165) is suppressed when the rule was
auto-derived from the type — the "vorgegeben durch Regel" copy
reads backwards in that case; show picker + Auto badge instead.
Part 4 — Standardtitel button (create + edit)
- Button rendered next to the Title field on both /deadlines/new
and /deadlines/{id} (edit mode only).
- Recipe (recipe-docs-here-so-future-templates-can-mirror-it):
head =
1. event_type label (if exactly one Typ chip is set)
2. rule code+name (when a Rule is set — "RoP.023 — Klageerwiderung")
3. proceeding type name from project (create form only)
4. fallback: t("deadlines.field.title.default_fallback")
suffix = " — <project.reference>" when ref is set and not
already in head.
Examples:
Klageerwiderung — C-UPC-0042 (type known)
RoP.023 — Klageerwiderung — REF (rule known, no type)
UPC — Verletzungsverfahren — REF (only proceeding type)
Neue Frist — REF (fallback)
- Click REPLACES current title; no destructive confirmation
because the user invoked it explicitly. Focus moves into the
title input afterwards so the user can fine-tune.
Build hygiene:
- go build + go vet + go test ./internal/... clean.
- frontend/build.ts clean (2786 keys, +10 new DE+EN, scan clean).
- All changes client-side / CSS / i18n + 2 small TSX edits; no
schema, no service, no migration.
Files touched:
- frontend/src/client/event-types.ts (browse-modal chips)
- frontend/src/client/deadlines-new.ts (rewrite — Type→Rule, sort,
Auto badge, override warn, Standardtitel)
- frontend/src/client/deadlines-detail.ts (edit-mode Standardtitel
+ show/hide on enter/exit edit)
- frontend/src/deadlines-new.tsx (label-row + sort dropdown + Auto
badge slot + override-warn slot + Standardtitel button)
- frontend/src/deadlines-detail.tsx (Standardtitel button)
- frontend/src/styles/global.css (.event-type-browse-chip*,
.form-hint--auto, .form-hint-badge, .form-field-label-row,
.btn-link-action, .rule-sort-select)
- frontend/src/client/i18n.ts (+10 keys DE+EN)
|
|||
| 02255c4234 |
mAi: #81 - verfahrensablauf side+appellant selectors + UPC Appeal trigger label
Concerns A + B + C from m/paliad#81:
A. Browse-a-proceeding (/tools/verfahrensablauf) gains a side selector
(Kläger/Beklagter/Beide) and an appellant selector. The side selector
swaps which column labels which user-side; the appellant selector
collapses party='both' rules into the appellant's column (no mirror)
so role-swap proceedings (Appeal, etc.) stop showing every row
twice in the timeline. Both selectors are URL-driven (?side= +
?appellant=) and re-render without a backend round-trip.
The appellant row hides itself for proceedings without an appellant
axis (first-instance Inf/Rev/Opp) via a small allowlist.
B. UPC Appeal trigger-event caption now reads "Anfechtbare Entscheidung"
/ "Appealable Decision" instead of falling back to the proceeding
name ("Berufungsverfahren" / "Appeal"). Implemented as an optional
trigger_event_label_{de,en} column on paliad.proceeding_types (mig
121); the frontend prefers it over the proceedingName fallback that
fires when no rule has IsRootEvent=true. No new deadline rules, no
slug changes (hard rule from the issue).
C. Parameter contract for the column projection is unified in
bucketDeadlinesIntoColumns(deadlines, {side, appellant}) — a pure
helper extracted from renderColumnsBody so the routing behaviour
stays unit-testable without a DOM. Tests cover the default mirror,
appellant-collapse for both sides, side-swap of column ownership,
the combined case, and row alignment by dueDate.
Verification
- go build ./... clean
- go test ./... all green
- bun run build (frontend) clean
- bun test (frontend/src) 110/110 pass (12 new + 98 prior)
- Migration 121 applied to paliad schema; UPC Appeal proceeding now
carries the curated trigger label pair.
Out of scope (filed for follow-up): per-rule role tagging so
respondent-side filings (Response to Appeal, Cross-Appeal) land in
the respondent's column when an appellant is selected. The current
issue scope (one-row-per-deadline collapse) is delivered; the
realistic-per-row routing needs a deadline_rules schema bump that
the hard rules of #81 excluded.
|
|||
| 1714b788d2 |
feat(projects-detail): t-paliad-245 — demote Daten Export into Verwaltung tab
m/paliad#76. The export button no longer pokes out of the tabs nav with a non-tab styling — instead it lives inside a new "Verwaltung" tab (last in the project tab list) as a normal section with heading, description, and a plain btn-secondary trigger. Same gate as before (canExportProject). Archive co-locates in the same tab as a pointer to the Edit-modal danger zone: click "Bearbeiten öffnen" → modal opens scrolled to the archive button. Single source of truth for the destructive action stays in the modal; the Verwaltung pointer just gives it discoverability. If neither sub-section is visible to the caller (no export entitlement, not global_admin), the Verwaltung tab hides itself — an empty tab is worse UX than no tab. |