#635: External meetings + auto-numbering

User asked for three things in the executive-meetings module:
1. Remove the "daily number" input from the add/edit dialog — numbering
   is auto-assigned by the server already, so the field was just noise.
2. Add an "اجتماع خارجي" (external meeting) toggle. When ON, the row
   is force-tinted red in the schedule grid.
3. External meetings must be excluded from the auto-postpone flow:
   the alert "Postpone" button is disabled, and the cascade shift
   skips them as followers.

Changes
- lib/db/src/schema/executive-meetings.ts
  + `is_external boolean not null default false` column. Pushed via
    drizzle-kit (no destructive migration; defaults backfill).
- artifacts/api-server/src/routes/executive-meetings.ts
  + `isExternal` added to base zod fields, POST insert, PATCH update.
  + POST/PATCH force `rowColor='red'` when `isExternal=true`.
  + POST /:id/postpone-minutes rejects external meetings with
    HTTP 409 + code `external_meeting_no_auto_postpone`.
  + `computeCascadeShift` filters out external followers (preview +
    writer stay in lockstep, as the comment promises).
- artifacts/tx-os/src/pages/executive-meetings.tsx
  + Meeting type + MeetingFormState gain `isExternal`; dropped the
    `dailyNumber` field from the form state entirely.
  + `emptyMeetingForm` / `openEdit` updated; ManageSection.save() and
    the inline ScheduleSection save body now send `isExternal` and
    never send `dailyNumber`.
  + MeetingFormDialog: removed the daily-number FormRow and added a
    Switch-based external-meeting FormRow with localized hint.
  + `rowColors` memo coerces to "red" whenever `m.isExternal`.
  + Audit-diff "interesting" list now includes `isExternal`.
- artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx
  + Meeting type gains optional `isExternal`.
  + "Postpone" button disabled + tooltip when meeting is external.
- locales: added `field.isExternal`, `field.isExternalHint`,
  `field.isExternalOn/Off`, and `alert.externalCannotPostpone` in
  both ar.json and en.json. Existing dailyNumber keys kept (still
  referenced by the manage-table column header / cell display).

Post-review hardening (from architect pass)
- PATCH now coerces rowColor → 'red' for any PATCH that touches a row
  that is (or becomes) external. Prevents PDF/export paths that read
  raw rowColor from showing a non-red tint for externals.
- Schedule row quick-actions popover: the "Postpone" button is now
  disabled with a localized tooltip when the meeting is external,
  matching the upcoming-meeting alert behavior.
- PostponeDialog.handleErr maps the 409 code
  `external_meeting_no_auto_postpone` to the localized
  `alert.externalCannotPostpone` toast so users see a real reason
  instead of the raw English error string.

Notes
- The `dailyNumber` column stays in DB (heavily used for slot
  persistence and the unique index); only the form input was removed.
  Server's `nextDailyNumber()` already auto-assigns when the field is
  absent on POST, and PATCH leaves it untouched when absent.
- Two pre-existing TS errors remain in api-server (`isHighlighted`
  type-inference quirk on `z.union(...).transform(...)` and a
  font_settings comparison) — not in scope for this task.
- Tests not added; verification by typecheck on changed surfaces and
  workflow-restart smoke. No e2e harness was wired.
This commit is contained in:
riyadhafraa
2026-05-25 11:59:54 +00:00
parent 5dc42fdb81
commit 736785e3d3
6 changed files with 135 additions and 19 deletions
@@ -80,6 +80,10 @@ type Meeting = {
// when another user mutated this meeting after it was loaded. Server
// sets it on every update; client echoes it back as expectedUpdatedAt.
updatedAt: string;
// #635: external meeting flag — the alert "Postpone" button is
// disabled when true (the server also rejects with
// `external_meeting_no_auto_postpone`).
isExternal?: boolean;
};
type DayResponse = { date: string; meetings: Meeting[] };
type AlertState = {
@@ -698,7 +702,14 @@ export function UpcomingMeetingAlert() {
size="sm"
variant="outline"
onClick={() => setPostponeOpen(true)}
disabled={savingAction !== null}
// #635: external meetings can't be auto-postponed (the
// server also rejects with external_meeting_no_auto_postpone).
disabled={savingAction !== null || meeting.isExternal === true}
title={
meeting.isExternal
? t("executiveMeetings.alert.externalCannotPostpone")
: undefined
}
data-testid="alert-postpone"
>
{t("executiveMeetings.alert.postpone")}
@@ -1145,6 +1156,20 @@ function PostponeDialog({
}
};
const handleErr = (err: unknown) => {
// #635: map the external-meeting 409 to the localized reason so
// schedule quick-action callers don't show the raw English error
// from the server.
const code =
err && typeof err === "object" && "code" in err
? (err as { code?: unknown }).code
: undefined;
if (code === "external_meeting_no_auto_postpone") {
toast({
title: t("executiveMeetings.alert.externalCannotPostpone"),
variant: "destructive",
});
return;
}
toast({
title: t("executiveMeetings.alert.saveFailed"),
description: err instanceof Error ? err.message : String(err),