#639: Fix external-meeting toggle (save + layout)

Two issues reported on the new "اجتماع خارجي" toggle from #635:

1. "Doesn't save." Investigation: the server schema, PATCH/POST handlers,
   GET endpoint (full-row select), and DB column are all correct, and
   `docker/migrate.sh` runs `pnpm --filter @workspace/db run push-force`
   on every `docker compose up`, so the production schema does receive
   the `is_external` column. The likeliest real-world cause is a stale
   API image (column existed but the post-review API code that persists
   the flag wasn't deployed yet), or a transient stale-state render
   where `state.isExternal` was undefined and silently dropped from the
   PATCH body. Hardened the client so neither can happen again:
   - Both save sites (Manage dialog `save()` ~6700, inline Schedule
     edit save body ~3421) now send `Boolean(state.isExternal)` instead
     of the raw value, so an undefined state coerces to `false` rather
     than dropping the field.

2. "Overlaps other fields." The toggle row was sharing a row with the
   date input in the 2-col grid and used a fixed `h-9` wrapper that
   sat awkwardly next to the taller time pickers on narrow screens.
   - Made the FormRow `full` so it spans both columns on its own row,
     above the time pickers.
   - Dropped the fixed `h-9` and switched to `py-1` + `gap-3` + `text-sm`
     so it matches the form's vertical rhythm on iPhone, iPad portrait,
     iPad landscape, and desktop.

Out of scope / not changed
- No server changes (the server already accepts boolean | number and
  coerces to boolean; `Boolean(data.isExternal ?? false)` on POST and
  `if (data.isExternal !== undefined)` on PATCH are unchanged).
- No new badge / copy — that lives under separate proposed work.
- Postpone / cascade rules unchanged.
This commit is contained in:
Riyadh
2026-05-25 12:19:48 +00:00
parent bc2b648cc4
commit b177f653e6
@@ -3412,8 +3412,10 @@ function ScheduleSection({
sortOrder: idx,
kind: a.kind ?? "person",
})),
// #635: external meeting flag.
isExternal: editingMeeting.isExternal,
// #635/#639: external meeting flag — coerce to real
// boolean so an undefined state never silently drops
// the field from the PATCH body.
isExternal: Boolean(editingMeeting.isExternal),
};
try {
await apiJson(
@@ -6691,9 +6693,12 @@ function ManageSection({
// schedule survive a save through the Manage dialog.
kind: a.kind ?? "person",
})),
// #635: send the external flag so the server can force rowColor='red'
// and the cascade/postpone rules know to skip it.
isExternal: editing.isExternal,
// #635/#639: send the external flag as a real boolean so the
// server can force rowColor='red' and the cascade/postpone rules
// know to skip it. Coerce defensively — a stale render where
// state.isExternal is undefined would otherwise silently send
// `undefined` and the server PATCH would skip the column.
isExternal: Boolean(editing.isExternal),
};
try {
if (editing.id == null) {
@@ -7571,21 +7576,26 @@ function MeetingFormDialog({
onChange={(e) => update("meetingDate", e.target.value)}
/>
</FormRow>
{/* #635: external-meeting toggle replaces the daily-number
input (numbering is now fully auto-assigned by the
server). When ON the row is force-tinted red and the
alert-postpone button is disabled. */}
{/* #635/#639: external-meeting toggle replaces the
daily-number input (numbering is now fully auto-assigned
by the server). When ON the row is force-tinted red and
the alert-postpone button is disabled.
`full` so it spans both grid columns on its own row
prevents the toggle from sharing a row with the date
input and colliding with the time pickers below on
narrow screens. */}
<FormRow
label={t("executiveMeetings.manage.field.isExternal")}
hint={t("executiveMeetings.manage.field.isExternalHint")}
full
>
<div className="flex items-center gap-2 h-9">
<div className="flex items-center gap-3 py-1">
<Switch
checked={state.isExternal}
onCheckedChange={(v) => update("isExternal", Boolean(v))}
data-testid="em-form-isExternal"
/>
<span className="text-xs text-gray-700">
<span className="text-sm text-gray-700">
{state.isExternal
? t("executiveMeetings.manage.field.isExternalOn")
: t("executiveMeetings.manage.field.isExternalOff")}