#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:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user