Compare commits

...

5 Commits

Author SHA1 Message Date
mAi
f8af389134 fix(a11y): drop label htmlFor=trigger-event — span isn't labelable
m/paliad#60 (t-paliad-221) — Chrome's Issues tab flagged a label/for
violation on the timeline wizard: <label for="trigger-event"> pointed
at a <span> showing the selected trigger event name. <label for=…>
must target a labelable form control (input/select/textarea/…), never
a span; the browser strips the association and a11y tooling sees a
dangling reference.

Audit found two occurrences — verfahrensablauf.tsx and fristenrechner.tsx
both use the same wizard markup. Switch both captions to plain
<span class="date-label">; the .date-label rule already targets by
class only, so visual styling is unchanged. No other label-for
mismatches surfaced (194 label-fors scanned across frontend/src).
2026-05-20 14:42:49 +02:00
mAi
5843dd38f5 fix(deadlines): event type renders before rule; bundle as Verfahrenshandlung
m/paliad#56 (t-paliad-221) — the deadlines editor read Title → Rule →
Event Type, which inverted the conceptual hierarchy (rule is the
citation under an event type, not its peer). Reorder all three
surfaces so the event-type parent comes first and the rule sits
directly beneath it.

- deadlines-new.tsx: pull the Regel select out of the Due-date row and
  drop it directly under the Typ picker; Due becomes its own row below.
- deadlines-detail.tsx: swap the Typ and Regel <dt>/<dd> rows in the
  detail list.
- approval-edit-modal.ts: remove rule_code from the generic
  DEADLINE_FIELDS list and render it inside a new
  "Verfahrenshandlung (Typ + Regel)" section beneath the event-type
  picker. The shared per-field renderer is extracted so the bundled
  section reuses the same dirty-tracking / pre_image-hint wiring.
- New i18n key approvals.suggest.section.event_type_rule (DE/EN).

Form-level inputs stay independent (some rules attach to multiple
event types and vice versa) — the change is purely about visual
grouping and reading order.
2026-05-20 14:42:38 +02:00
mAi
f16280202a fix(events): drop broken 'From Today' appointment filter; default to today
m/paliad#54 (t-paliad-221) — fix 92780cf added a status=upcoming option
for appointments and made it the default, but DeadlineFilterUpcoming
only narrowed deadlines. The appointment query had no matching case, so
the bucket fell through to the unfiltered path and past events leaked
into "Ab heute" / "From today".

- Drop the 'upcoming' option from STATUS_OPTIONS_APPOINTMENT — confusing
  label that never delivered.
- Default appointments to the 'today' bucket (matches the dashboard
  tile; sane lawyer-relevant view).
- Keep 'Alle (auch vergangene)' as the explicit opt-in at the bottom
  of the list.
- Defensive backend fix: map DeadlineFilterUpcoming to start_at >= today
  in bucketAppointmentWindow so any persisted ?status=upcoming bookmarks
  stop leaking past events.
2026-05-20 14:42:20 +02:00
mAi
01dae43540 fix(approvals): density-picker active state uses brand accent
m/paliad#52 (t-paliad-221) — the Compact/Comfortable segmented control
on /approvals was rendering its active pill in plain --color-surface
(white in light mode, midnight-tinted in dark). Switch to the brand
lime so the segmented controls speak the same primary-action language
as the rest of Paliad.

Introduces three semantic tokens (--color-segment-active-bg / fg /
border) so any future surface that adopts .filter-bar-segment
inherits the same accent treatment without a CSS rewrite. The tokens
resolve to --color-accent / --color-accent-dark in both themes,
keeping the midnight foreground WCAG-AA on lime.
2026-05-20 14:42:10 +02:00
mAi
ebb5ff0caa feat(projects): add 'other' as a real type; drop synthetic Empty filter
m/paliad#51 (t-paliad-221) — the type chip filter on /projects used to
treat unclassified projects as a synthetic "Empty" bucket. Make 'other'
a first-class projects.type value so every row carries a meaningful
label and the filter UI stops needing a NULL/Empty shim.

- mig 110: extend projects.type CHECK to include 'other'; backfill any
  NULL rows defensively (production query confirmed zero, but the
  NOT NULL constraint isn't load-bearing once the IN-list changes).
- Go: add ProjectTypeOther constant; isValidProjectType + humanProjectType
  recognise it; handler doc lists 'other' in the ?type whitelist.
- Frontend: new chip in the projects.tsx type filter, new option in the
  Create-Project form, DE "Sonstiges" / EN "Other" labels for the
  projects.type and projects.chip.type i18n families.

Also drops a stray data-i18n-text attribute on the existing 'project'
chip checkbox (it had no consumer in i18n.ts and the surrounding markup
was nesting a <span> inside an <input>).
2026-05-20 14:42:02 +02:00
17 changed files with 210 additions and 69 deletions

View File

@@ -76,12 +76,15 @@ interface FieldSpec {
required?: boolean;
}
// Deadline-only fields rendered in the editable section. `rule_code` and
// `event_type_ids` are intentionally NOT here — they're bundled into the
// dedicated "Verfahrenshandlung" section below the base fields so the
// event-type (parent concept) reads before the rule (m/paliad#56).
const DEADLINE_FIELDS: ReadonlyArray<FieldSpec> = [
{ key: "title", labelKey: "deadlines.field.title", inputType: "text", required: true },
{ key: "due_date", labelKey: "deadlines.field.due", inputType: "date" },
{ key: "original_due_date", labelKey: "approvals.suggest.field.original_due_date", inputType: "date" },
{ key: "warning_date", labelKey: "approvals.suggest.field.warning_date", inputType: "date" },
{ key: "rule_code", labelKey: "approvals.suggest.field.rule_code", inputType: "text" },
{ key: "description", labelKey: "approvals.suggest.field.description", inputType: "textarea" },
{ key: "notes", labelKey: "deadlines.field.notes", inputType: "textarea" },
];
@@ -121,7 +124,7 @@ export async function openApprovalEditModal(
let eventTypePicker: PickerHandle | null = null;
let eventTypePickerLoaded = false;
if (args.entityType === "deadline") {
const pickerSection = renderEventTypePickerSection();
const pickerSection = renderEventTypePickerSection(original, preImage);
body.appendChild(pickerSection.section);
void (async () => {
try {
@@ -191,67 +194,94 @@ function renderFieldsSection(
section.appendChild(h);
for (const f of fields) {
const wrap = document.createElement("div");
wrap.className = "form-field approval-suggest-field";
const label = document.createElement("label");
label.textContent = t(f.labelKey as never);
wrap.appendChild(label);
const value = formatFieldForInput(original[f.key], f.inputType);
let input: HTMLInputElement | HTMLTextAreaElement;
if (f.inputType === "textarea") {
input = document.createElement("textarea");
input.rows = 3;
(input as HTMLTextAreaElement).value = value;
} else {
input = document.createElement("input");
(input as HTMLInputElement).type = f.inputType;
(input as HTMLInputElement).value = value;
}
input.dataset.suggestField = f.key;
input.dataset.suggestOriginal = value;
input.dataset.suggestInputType = f.inputType;
if (f.required) input.required = true;
// Wire the <label> to focus the <input> on click.
const inputID = `suggest-field-${f.key}`;
input.id = inputID;
label.setAttribute("for", inputID);
wrap.appendChild(input);
// "Vorher" hint when pre_image carries a distinct value for this field.
const preVal = formatFieldForInput(preImage[f.key], f.inputType);
if (preVal && preVal !== value) {
const hint = document.createElement("span");
hint.className = "approval-suggest-prehint";
hint.textContent = `${t("approvals.diff.before")}: ${preVal}`;
wrap.appendChild(hint);
}
section.appendChild(wrap);
section.appendChild(renderSingleField(f, original, preImage));
}
return section;
}
function renderEventTypePickerSection(): { section: HTMLElement; host: HTMLElement } {
// Verfahrenshandlung section — bundles the event-type picker and the
// rule_code input so the editor reads "what procedural step? which rule
// cites it?" instead of two disconnected fields with rule above type
// (m/paliad#56). The hint underneath spells out the parent/child
// relationship so first-time editors don't read them as peers.
function renderEventTypePickerSection(
original: Record<string, unknown>,
preImage: Record<string, unknown>,
): { section: HTMLElement; host: HTMLElement } {
const section = document.createElement("section");
section.className = "approval-suggest-section approval-suggest-section--editable";
const h = document.createElement("h3");
h.className = "approval-suggest-section-title";
h.textContent = t("deadlines.field.event_type");
h.textContent = t("approvals.suggest.section.event_type_rule");
section.appendChild(h);
const host = document.createElement("div");
host.className = "approval-suggest-event-type-picker";
section.appendChild(host);
// Rule citation — rendered as a sub-field directly beneath the picker so
// the visual hierarchy matches the conceptual one (rule is meta on the
// event type, not a peer).
const ruleField: FieldSpec = {
key: "rule_code",
labelKey: "approvals.suggest.field.rule_code",
inputType: "text",
};
section.appendChild(renderSingleField(ruleField, original, preImage));
return { section, host };
}
// renderSingleField builds one labelled input in the same shape as the
// fields-section loop. Extracted so the Verfahrenshandlung section can
// host the rule_code input next to the picker without duplicating the
// wiring (dirty-tracking, pre_image hint, label/for binding).
function renderSingleField(
f: FieldSpec,
original: Record<string, unknown>,
preImage: Record<string, unknown>,
): HTMLElement {
const wrap = document.createElement("div");
wrap.className = "form-field approval-suggest-field";
const label = document.createElement("label");
label.textContent = t(f.labelKey as never);
wrap.appendChild(label);
const value = formatFieldForInput(original[f.key], f.inputType);
let input: HTMLInputElement | HTMLTextAreaElement;
if (f.inputType === "textarea") {
input = document.createElement("textarea");
input.rows = 3;
(input as HTMLTextAreaElement).value = value;
} else {
input = document.createElement("input");
(input as HTMLInputElement).type = f.inputType;
(input as HTMLInputElement).value = value;
}
input.dataset.suggestField = f.key;
input.dataset.suggestOriginal = value;
input.dataset.suggestInputType = f.inputType;
if (f.required) input.required = true;
const inputID = `suggest-field-${f.key}`;
input.id = inputID;
label.setAttribute("for", inputID);
wrap.appendChild(input);
const preVal = formatFieldForInput(preImage[f.key], f.inputType);
if (preVal && preVal !== value) {
const hint = document.createElement("span");
hint.className = "approval-suggest-prehint";
hint.textContent = `${t("approvals.diff.before")}: ${preVal}`;
wrap.appendChild(hint);
}
return wrap;
}
function renderContextSection(
args: ApprovalEditModalArgs,
original: Record<string, unknown>,

View File

@@ -125,8 +125,11 @@ const STATUS_OPTIONS_DEADLINE: StatusOption[] = [
{ value: "completed", key: "deadlines.filter.completed" },
];
// Appointment status options — m/paliad#54: the legacy 'upcoming' /
// "Ab heute" option was a UI lie (backend never narrowed past events for
// appointments) and is removed. 'today' is the sane default — matches the
// dashboard tile. 'all' stays as the explicit opt-in for past events.
const STATUS_OPTIONS_APPOINTMENT: StatusOption[] = [
{ value: "upcoming", key: "events.filter.status.upcoming" },
{ value: "today", key: "deadlines.filter.today" },
{ value: "this_week", key: "deadlines.filter.thisweek" },
{ value: "next_week", key: "deadlines.filter.nextweek" },
@@ -140,7 +143,7 @@ function statusOptionsFor(type: EventTypeChoice): StatusOption[] {
}
function defaultStatusFor(type: EventTypeChoice): string {
return type === "appointment" ? "upcoming" : "pending";
return type === "appointment" ? "today" : "pending";
}
let currentType: EventTypeChoice = "deadline";

View File

@@ -1405,6 +1405,7 @@ const translations: Record<Lang, Record<string, string>> = {
"projects.type.patent": "Patent",
"projects.type.case": "Verfahren",
"projects.type.project": "Projekt",
"projects.type.other": "Sonstiges",
"projects.team.role.lead": "Leitung",
"projects.team.role.associate": "Associate",
"projects.team.role.pa": "PA",
@@ -1465,6 +1466,7 @@ const translations: Record<Lang, Record<string, string>> = {
"projects.chip.type.patent": "Patent",
"projects.chip.type.case": "Verfahren",
"projects.chip.type.project": "Projekt",
"projects.chip.type.other": "Sonstiges",
"projects.chip.multi.none": "Keine Auswahl",
"projects.chip.multi.count": "{n} ausgew\u00e4hlt",
"projects.empty.filtered.action": "Filter zur\u00fccksetzen",
@@ -2293,6 +2295,7 @@ const translations: Record<Lang, Record<string, string>> = {
"approvals.suggest.next_request_link": "→ Neuer Vorschlag von {name}",
"approvals.suggest.unsupported_lifecycle": "Änderungen vorschlagen ist nur für Update-Anfragen möglich.",
"approvals.suggest.section.editable": "Felder",
"approvals.suggest.section.event_type_rule": "Verfahrenshandlung (Typ + Regel)",
"approvals.suggest.section.context": "Kontext",
"approvals.suggest.context.project": "Projekt",
"approvals.suggest.context.requester": "Eingereicht von",
@@ -4079,6 +4082,7 @@ const translations: Record<Lang, Record<string, string>> = {
"projects.type.patent": "Patent",
"projects.type.case": "Case",
"projects.type.project": "Project",
"projects.type.other": "Other",
"projects.team.role.lead": "Lead",
"projects.team.role.associate": "Associate",
"projects.team.role.pa": "PA",
@@ -4139,6 +4143,7 @@ const translations: Record<Lang, Record<string, string>> = {
"projects.chip.type.patent": "Patent",
"projects.chip.type.case": "Case",
"projects.chip.type.project": "Project",
"projects.chip.type.other": "Other",
"projects.chip.multi.none": "Nothing selected",
"projects.chip.multi.count": "{n} selected",
"projects.empty.filtered.action": "Reset filters",
@@ -4964,6 +4969,7 @@ const translations: Record<Lang, Record<string, string>> = {
"approvals.suggest.next_request_link": "→ New suggestion by {name}",
"approvals.suggest.unsupported_lifecycle": "Suggest changes is only available for update requests.",
"approvals.suggest.section.editable": "Fields",
"approvals.suggest.section.event_type_rule": "Event type + rule",
"approvals.suggest.section.context": "Context",
"approvals.suggest.context.project": "Project",
"approvals.suggest.context.requester": "Submitted by",

View File

@@ -22,6 +22,7 @@ export function ProjectFormFields(): string {
<option value="patent" data-i18n="projects.type.patent">Patent</option>
<option value="case" data-i18n="projects.type.case">Verfahren</option>
<option value="project" data-i18n="projects.type.project">Projekt (generisch)</option>
<option value="other" data-i18n="projects.type.other">Sonstiges</option>
</select>
</div>

View File

@@ -82,15 +82,21 @@ export function renderDeadlinesDetail(): string {
<input type="date" id="deadline-due-edit" style="display:none" />
</dd>
<dt data-i18n="deadlines.detail.rule">Regel</dt>
<dd id="deadline-rule-display">&mdash;</dd>
{/* m/paliad#56 — Verfahrenshandlung block.
Event type (parent concept) renders first; rule
sits beneath as the citation under that event
type. Editor splits them back into separate
pickers but the read-only stack reads as one
compound "Typ — Regel" surface. */}
<dt data-i18n="deadlines.field.event_type">Typ (optional)</dt>
<dd>
<span id="deadline-event-types-display">&mdash;</span>
<div id="deadline-event-types-edit" className="event-type-picker-host" style="display:none" />
</dd>
<dt data-i18n="deadlines.detail.rule">Regel</dt>
<dd id="deadline-rule-display">&mdash;</dd>
<dt data-i18n="deadlines.detail.source">Quelle</dt>
<dd id="deadline-source-display" />

View File

@@ -101,18 +101,19 @@ export function renderDeadlinesNew(): string {
</p>
</div>
<div className="form-field-row">
<div className="form-field">
<label htmlFor="deadline-due" data-i18n="deadlines.field.due">F&auml;lligkeitsdatum</label>
<input type="date" id="deadline-due" required />
</div>
{/* m/paliad#56 — Regel sits directly beneath the Typ
picker so the parent/child relationship reads at a
glance. Due date is its own row below. */}
<div className="form-field">
<label htmlFor="deadline-rule" data-i18n="deadlines.field.rule">Regel (optional)</label>
<select id="deadline-rule">
<option value="" data-i18n="deadlines.field.rule.none">Keine Regel</option>
</select>
</div>
<div className="form-field">
<label htmlFor="deadline-rule" data-i18n="deadlines.field.rule">Regel (optional)</label>
<select id="deadline-rule">
<option value="" data-i18n="deadlines.field.rule.none">Keine Regel</option>
</select>
</div>
<div className="form-field">
<label htmlFor="deadline-due" data-i18n="deadlines.field.due">F&auml;lligkeitsdatum</label>
<input type="date" id="deadline-due" required />
</div>
<div className="form-field">

View File

@@ -485,7 +485,10 @@ export function renderFristenrechner(): string {
<div className="date-input-group">
<div className="date-field-row">
<label htmlFor="trigger-event" className="date-label" data-i18n="deadlines.trigger.event">Ausl&ouml;sendes Ereignis:</label>
{/* Read-only caption labelling the value <span>. Not a
<label htmlFor> — m/paliad#60: <label for=…> must
point at a labelable form control, never a span. */}
<span className="date-label" data-i18n="deadlines.trigger.event">Ausl&ouml;sendes Ereignis:</span>
<span id="trigger-event" className="trigger-event-name">&mdash;</span>
</div>
<div className="date-field-row">

View File

@@ -658,6 +658,7 @@ export type I18nKey =
| "approvals.suggest.note_placeholder"
| "approvals.suggest.section.context"
| "approvals.suggest.section.editable"
| "approvals.suggest.section.event_type_rule"
| "approvals.suggest.submit"
| "approvals.suggest.submit_disabled_hint"
| "approvals.suggest.unsupported_lifecycle"
@@ -1972,6 +1973,7 @@ export type I18nKey =
| "projects.chip.type.case"
| "projects.chip.type.client"
| "projects.chip.type.litigation"
| "projects.chip.type.other"
| "projects.chip.type.patent"
| "projects.chip.type.project"
| "projects.col.clientmatter"
@@ -2287,6 +2289,7 @@ export type I18nKey =
| "projects.type.case"
| "projects.type.client"
| "projects.type.litigation"
| "projects.type.other"
| "projects.type.patent"
| "projects.type.project"
| "projects.unavailable"

View File

@@ -127,7 +127,8 @@ export function renderProjects(): string {
<label><input type="checkbox" value="litigation" /><span data-i18n="projects.chip.type.litigation">Streitsache</span></label>
<label><input type="checkbox" value="patent" /><span data-i18n="projects.chip.type.patent">Patent</span></label>
<label><input type="checkbox" value="case" /><span data-i18n="projects.chip.type.case">Verfahren</span></label>
<label><input type="checkbox" value="project" data-i18n-text="projects.chip.type.project"><span data-i18n="projects.chip.type.project">Projekt</span></input></label>
<label><input type="checkbox" value="project" /><span data-i18n="projects.chip.type.project">Projekt</span></label>
<label><input type="checkbox" value="other" /><span data-i18n="projects.chip.type.other">Sonstiges</span></label>
</div>
</details>
<button type="button" className="projects-chip" data-chip="has_open_deadlines" data-i18n="projects.chip.has_open_deadlines">Mit aktiven Fristen</button>

View File

@@ -59,6 +59,14 @@
--color-overlay-strong: rgba(0, 0, 0, 0.10);
--color-overlay-modal: rgba(0, 0, 0, 0.4); /* modal/drawer scrim */
/* Segmented-control active pill — brand-lime accent so every density /
view-mode toggle reads as the same primary action (m/paliad#52).
Surfaces consuming these tokens: .filter-bar-segment (FilterBar
density + future view-mode segments). Override on dark mode below. */
--color-segment-active-bg: var(--color-accent);
--color-segment-active-fg: var(--color-accent-dark);
--color-segment-active-border: var(--color-accent);
/* Status palette — five buckets (red/amber/green/blue/neutral) shared
across dashboard cards, frist-due-chips, agenda urgency, termin
badges, login forms. Light values match the existing pastel-on-dark
@@ -173,6 +181,13 @@
--color-overlay-strong: rgba(255, 255, 255, 0.12);
--color-overlay-modal: rgba(0, 0, 0, 0.65);
/* Segmented active pill — lime stays the brand on dark mode too; the
--color-accent-dark token already resolves to midnight in both
themes, keeping the foreground WCAG-AA on lime. */
--color-segment-active-bg: var(--color-accent);
--color-segment-active-fg: var(--color-accent-dark);
--color-segment-active-border: var(--color-accent);
--shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.45);
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.55);
@@ -14103,8 +14118,9 @@ dialog.quick-add-sheet::backdrop {
border: 1px solid transparent;
}
.filter-bar-segment .filter-bar-chip.agenda-chip-active {
background: var(--color-surface, #ffffff);
border-color: var(--color-border, #e5e7eb);
background: var(--color-segment-active-bg);
color: var(--color-segment-active-fg);
border-color: var(--color-segment-active-border);
}
.filter-bar-chip-pending {

View File

@@ -163,7 +163,10 @@ export function renderVerfahrensablauf(): string {
<div className="date-input-group">
<div className="date-field-row">
<label htmlFor="trigger-event" className="date-label" data-i18n="deadlines.trigger.event">Ausl&ouml;sendes Ereignis:</label>
{/* Read-only caption labelling the value <span>. Not a
<label htmlFor> — m/paliad#60: <label for=…> must
point at a labelable form control, never a span. */}
<span className="date-label" data-i18n="deadlines.trigger.event">Ausl&ouml;sendes Ereignis:</span>
<span id="trigger-event" className="trigger-event-name">&mdash;</span>
</div>
<div className="date-field-row">

View File

@@ -0,0 +1,22 @@
-- mig 110 (down) — revert 'other' addition to paliad.projects.type
--
-- Coerces any 'other' rows back to 'project' (the historical catch-all)
-- so the narrower CHECK constraint can re-attach. This is a lossy
-- rollback: rows that were genuinely 'other' lose that distinction.
SELECT set_config(
'paliad.audit_reason',
'mig 110 (down): revert ''other'' from projects.type CHECK; coerce rows to ''project''',
true);
UPDATE paliad.projects
SET type = 'project'
WHERE type = 'other';
ALTER TABLE paliad.projects
DROP CONSTRAINT IF EXISTS projects_type_check;
ALTER TABLE paliad.projects
ADD CONSTRAINT projects_type_check
CHECK (type IN (
'client', 'litigation', 'patent', 'case', 'project'
));

View File

@@ -0,0 +1,33 @@
-- mig 110 — add 'other' as a sixth paliad.projects.type value
--
-- m/paliad#51 (t-paliad-221): the type chip filter on /projects used to
-- treat unclassified projects as a synthetic "Empty" bucket. We replace
-- that with a real 'other' type so every row carries a meaningful label
-- and the filter UI stops needing a NULL/Empty shim.
--
-- Defensive backfill: NOT NULL + the original IN-list CHECK already
-- forbid NULL rows, but we coerce any stray rows just in case a future
-- migration ever relaxed the constraint. As of 2026-05-20 production
-- carries zero rows that would change here (live query confirmed).
--
-- The Go-side source of truth lives in
-- internal/services/project_service.go (ProjectType constants +
-- isValidProjectType); this migration keeps the DB in sync.
SELECT set_config(
'paliad.audit_reason',
'mig 110: add ''other'' to projects.type CHECK + backfill NULLs (m/paliad#51)',
true);
-- Backfill first so the new CHECK never rejects a pre-existing row.
UPDATE paliad.projects
SET type = 'other'
WHERE type IS NULL;
ALTER TABLE paliad.projects
DROP CONSTRAINT IF EXISTS projects_type_check;
ALTER TABLE paliad.projects
ADD CONSTRAINT projects_type_check
CHECK (type IN (
'client', 'litigation', 'patent', 'case', 'project', 'other'
));

View File

@@ -351,7 +351,7 @@ func handleListProjectChildren(w http.ResponseWriter, r *http.Request) {
// Query parameters (all optional, additive):
// ?scope=all|mine|pinned — chip-driven scope (default "all")
// ?status=active,archived,closed — status whitelist (CSV; default = no narrowing)
// ?type=client,litigation,patent,case,project — type whitelist
// ?type=client,litigation,patent,case,project,other — type whitelist
// ?has_open_deadlines=true|false — narrow by deadline activity
// ?q=<term> — search title / reference / clientmatter
// ?subtree_counts=true|false — populate *_subtree fields (default true)

View File

@@ -473,6 +473,8 @@ func humanProjectType(t string) string {
return "Verfahren"
case services.ProjectTypeProject:
return "Projekt"
case services.ProjectTypeOther:
return "Sonstiges"
}
return t
}

View File

@@ -279,7 +279,12 @@ func shouldExcludeAppointmentsForStatus(status DeadlineStatusFilter) bool {
// matches a bucket-style deadline status — used to filter the
// appointment side when the user clicks a card on the unified events
// page. Returns (nil, nil) for non-bucket statuses (pending / all /
// upcoming / "" / overdue / completed — those are handled separately).
// "" / overdue / completed — those are handled separately).
//
// DeadlineFilterUpcoming maps to "start_at >= today" so legacy
// `?status=upcoming` URLs hide past appointments instead of falling
// through to the unfiltered query (m/paliad#54 — the UI option that
// surfaced this status has been removed, but bookmarks may persist).
func bucketAppointmentWindow(status DeadlineStatusFilter, b deadlineBucketBounds) (*time.Time, *time.Time) {
switch status {
case DeadlineFilterToday:
@@ -293,6 +298,8 @@ func bucketAppointmentWindow(status DeadlineStatusFilter, b deadlineBucketBounds
return &b.nextMonday, &t
case DeadlineFilterLater:
return &b.weekAfter, nil
case DeadlineFilterUpcoming:
return &b.today, nil
}
return nil, nil
}

View File

@@ -54,12 +54,16 @@ var (
)
// ProjectType values enumerated on the projects.type CHECK constraint.
// 'other' (mig 110, m/paliad#51) is the explicit "unclassified" bucket —
// previously this appeared as a synthetic "Empty" option in the type
// filter; the chip now offers it as a real selectable type.
const (
ProjectTypeClient = "client"
ProjectTypeLitigation = "litigation"
ProjectTypePatent = "patent"
ProjectTypeCase = "case"
ProjectTypeProject = "project"
ProjectTypeOther = "other"
)
// Legacy ProjectRole values that used to live on paliad.project_teams.role.
@@ -1890,7 +1894,7 @@ func typeSpecificColumns(t string) []string {
func isValidProjectType(t string) bool {
switch t {
case ProjectTypeClient, ProjectTypeLitigation, ProjectTypePatent,
ProjectTypeCase, ProjectTypeProject:
ProjectTypeCase, ProjectTypeProject, ProjectTypeOther:
return true
}
return false