Compare commits
5 Commits
mai/knuth/
...
mai/euler/
| Author | SHA1 | Date | |
|---|---|---|---|
| f8af389134 | |||
| 5843dd38f5 | |||
| f16280202a | |||
| 01dae43540 | |||
| ebb5ff0caa |
@@ -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>,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">—</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">—</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">—</dd>
|
||||
|
||||
<dt data-i18n="deadlines.detail.source">Quelle</dt>
|
||||
<dd id="deadline-source-display" />
|
||||
|
||||
|
||||
@@ -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ä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älligkeitsdatum</label>
|
||||
<input type="date" id="deadline-due" required />
|
||||
</div>
|
||||
|
||||
<div className="form-field">
|
||||
|
||||
@@ -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ö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ösendes Ereignis:</span>
|
||||
<span id="trigger-event" className="trigger-event-name">—</span>
|
||||
</div>
|
||||
<div className="date-field-row">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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ö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ösendes Ereignis:</span>
|
||||
<span id="trigger-event" className="trigger-event-name">—</span>
|
||||
</div>
|
||||
<div className="date-field-row">
|
||||
|
||||
22
internal/db/migrations/110_project_type_other.down.sql
Normal file
22
internal/db/migrations/110_project_type_other.down.sql
Normal 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'
|
||||
));
|
||||
33
internal/db/migrations/110_project_type_other.up.sql
Normal file
33
internal/db/migrations/110_project_type_other.up.sql
Normal 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'
|
||||
));
|
||||
@@ -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)
|
||||
|
||||
@@ -473,6 +473,8 @@ func humanProjectType(t string) string {
|
||||
return "Verfahren"
|
||||
case services.ProjectTypeProject:
|
||||
return "Projekt"
|
||||
case services.ProjectTypeOther:
|
||||
return "Sonstiges"
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user