#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:
@@ -231,6 +231,12 @@ const meetingBaseFields = {
|
|||||||
.transform((v) => (v ? 1 : 0))
|
.transform((v) => (v ? 1 : 0))
|
||||||
.optional(),
|
.optional(),
|
||||||
notes: z.string().trim().max(5000).nullable().optional(),
|
notes: z.string().trim().max(5000).nullable().optional(),
|
||||||
|
// #635: external-meeting flag. Coerce truthy/falsy → boolean so older
|
||||||
|
// clients can keep sending 0/1 without surprises.
|
||||||
|
isExternal: z
|
||||||
|
.union([z.boolean(), z.number()])
|
||||||
|
.transform((v) => Boolean(v))
|
||||||
|
.optional(),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const meetingCreateSchema = z
|
const meetingCreateSchema = z
|
||||||
@@ -282,6 +288,7 @@ const meetingPatchSchema = z
|
|||||||
status: meetingBaseFields.status,
|
status: meetingBaseFields.status,
|
||||||
isHighlighted: meetingBaseFields.isHighlighted,
|
isHighlighted: meetingBaseFields.isHighlighted,
|
||||||
notes: meetingBaseFields.notes,
|
notes: meetingBaseFields.notes,
|
||||||
|
isExternal: meetingBaseFields.isExternal,
|
||||||
attendees: z.array(attendeeSchema).optional(),
|
attendees: z.array(attendeeSchema).optional(),
|
||||||
merge: meetingMergeSchema.optional(),
|
merge: meetingMergeSchema.optional(),
|
||||||
// Row-highlight colour (#288). null = clear back to default (no tint).
|
// Row-highlight colour (#288). null = clear back to default (no tint).
|
||||||
@@ -669,6 +676,12 @@ router.post(
|
|||||||
status: data.status ?? "scheduled",
|
status: data.status ?? "scheduled",
|
||||||
isHighlighted: data.isHighlighted ?? 0,
|
isHighlighted: data.isHighlighted ?? 0,
|
||||||
notes: stripTagsToPlainTextOrNull(data.notes),
|
notes: stripTagsToPlainTextOrNull(data.notes),
|
||||||
|
// #635: persist the external-meeting flag; default false.
|
||||||
|
// When true, force rowColor='red' so the schedule always
|
||||||
|
// renders the row in the agreed external-meeting tint even
|
||||||
|
// if the client forgot to pass it.
|
||||||
|
isExternal: Boolean(data.isExternal ?? false),
|
||||||
|
rowColor: data.isExternal ? "red" : (data as { rowColor?: string }).rowColor ?? null,
|
||||||
createdBy: userId,
|
createdBy: userId,
|
||||||
updatedBy: userId,
|
updatedBy: userId,
|
||||||
};
|
};
|
||||||
@@ -843,6 +856,22 @@ router.patch(
|
|||||||
if (data.rowColor !== undefined) {
|
if (data.rowColor !== undefined) {
|
||||||
updateValues.rowColor = data.rowColor;
|
updateValues.rowColor = data.rowColor;
|
||||||
}
|
}
|
||||||
|
// #635: persist isExternal. When switching to external, also
|
||||||
|
// force rowColor='red' (overrides any rowColor in the same
|
||||||
|
// PATCH) so the red tint always tracks the flag.
|
||||||
|
if (data.isExternal !== undefined) {
|
||||||
|
updateValues.isExternal = Boolean(data.isExternal);
|
||||||
|
if (data.isExternal) updateValues.rowColor = "red";
|
||||||
|
}
|
||||||
|
// #635: belt-and-suspenders — if the row is (or is becoming)
|
||||||
|
// external and a non-red rowColor would otherwise be persisted
|
||||||
|
// by this PATCH, force red. Prevents PDF/export paths that read
|
||||||
|
// the raw rowColor from showing a non-red tint for externals.
|
||||||
|
const willBeExternal =
|
||||||
|
data.isExternal !== undefined ? Boolean(data.isExternal) : existing.isExternal;
|
||||||
|
if (willBeExternal && updateValues.rowColor !== undefined && updateValues.rowColor !== "red") {
|
||||||
|
updateValues.rowColor = "red";
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(updateValues).length > 1) {
|
if (Object.keys(updateValues).length > 1) {
|
||||||
await tx
|
await tx
|
||||||
@@ -1126,6 +1155,7 @@ async function computeCascadeShift(
|
|||||||
startTime: executiveMeetingsTable.startTime,
|
startTime: executiveMeetingsTable.startTime,
|
||||||
endTime: executiveMeetingsTable.endTime,
|
endTime: executiveMeetingsTable.endTime,
|
||||||
status: executiveMeetingsTable.status,
|
status: executiveMeetingsTable.status,
|
||||||
|
isExternal: executiveMeetingsTable.isExternal,
|
||||||
})
|
})
|
||||||
.from(executiveMeetingsTable)
|
.from(executiveMeetingsTable)
|
||||||
.where(eq(executiveMeetingsTable.meetingDate, date));
|
.where(eq(executiveMeetingsTable.meetingDate, date));
|
||||||
@@ -1138,6 +1168,10 @@ async function computeCascadeShift(
|
|||||||
const sorted = candidates
|
const sorted = candidates
|
||||||
.filter((c) => c.id !== primaryId)
|
.filter((c) => c.id !== primaryId)
|
||||||
.filter((c) => c.status !== "cancelled" && c.status !== "completed")
|
.filter((c) => c.status !== "cancelled" && c.status !== "completed")
|
||||||
|
// #635: external meetings are excluded from auto-postpone cascades
|
||||||
|
// by design — they're scheduled with the other party and can't be
|
||||||
|
// unilaterally shifted.
|
||||||
|
.filter((c) => !c.isExternal)
|
||||||
.map((c) => ({
|
.map((c) => ({
|
||||||
...c,
|
...c,
|
||||||
_startMin: parseTimeToMinutes(c.startTime),
|
_startMin: parseTimeToMinutes(c.startTime),
|
||||||
@@ -1573,6 +1607,18 @@ router.post(
|
|||||||
if (!locked) {
|
if (!locked) {
|
||||||
return { ok: false, status: 404, error: "Meeting not found", code: "not_found" };
|
return { ok: false, status: 404, error: "Meeting not found", code: "not_found" };
|
||||||
}
|
}
|
||||||
|
// #635: External meetings can never be auto-postponed — they're
|
||||||
|
// scheduled with an outside party and shifting them unilaterally
|
||||||
|
// would put the schedule out of sync. The UI also disables the
|
||||||
|
// Postpone button, so this is a belt-and-suspenders 409.
|
||||||
|
if (locked.isExternal) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
status: 409,
|
||||||
|
error: "External meetings cannot be auto-postponed",
|
||||||
|
code: "external_meeting_no_auto_postpone",
|
||||||
|
};
|
||||||
|
}
|
||||||
// #283: Optimistic-locking guard. The client always sends the
|
// #283: Optimistic-locking guard. The client always sends the
|
||||||
// `updatedAt` it last observed; if the row has been modified
|
// `updatedAt` it last observed; if the row has been modified
|
||||||
// since, refuse to mutate and return the current state + the
|
// since, refuse to mutate and return the current state + the
|
||||||
|
|||||||
@@ -80,6 +80,10 @@ type Meeting = {
|
|||||||
// when another user mutated this meeting after it was loaded. Server
|
// when another user mutated this meeting after it was loaded. Server
|
||||||
// sets it on every update; client echoes it back as expectedUpdatedAt.
|
// sets it on every update; client echoes it back as expectedUpdatedAt.
|
||||||
updatedAt: string;
|
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 DayResponse = { date: string; meetings: Meeting[] };
|
||||||
type AlertState = {
|
type AlertState = {
|
||||||
@@ -698,7 +702,14 @@ export function UpcomingMeetingAlert() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setPostponeOpen(true)}
|
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"
|
data-testid="alert-postpone"
|
||||||
>
|
>
|
||||||
{t("executiveMeetings.alert.postpone")}
|
{t("executiveMeetings.alert.postpone")}
|
||||||
@@ -1145,6 +1156,20 @@ function PostponeDialog({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleErr = (err: unknown) => {
|
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({
|
toast({
|
||||||
title: t("executiveMeetings.alert.saveFailed"),
|
title: t("executiveMeetings.alert.saveFailed"),
|
||||||
description: err instanceof Error ? err.message : String(err),
|
description: err instanceof Error ? err.message : String(err),
|
||||||
|
|||||||
@@ -1246,6 +1246,7 @@
|
|||||||
"atTime": "في {{time}}",
|
"atTime": "في {{time}}",
|
||||||
"done": "تم",
|
"done": "تم",
|
||||||
"postpone": "تأجيل",
|
"postpone": "تأجيل",
|
||||||
|
"externalCannotPostpone": "لا يمكن تأجيل اجتماع خارجي تلقائياً.",
|
||||||
"dismiss": "تجاهل التنبيه",
|
"dismiss": "تجاهل التنبيه",
|
||||||
"dragHandle": "اسحب للتحريك",
|
"dragHandle": "اسحب للتحريك",
|
||||||
"dismissToast": "تم تجاهل التنبيه",
|
"dismissToast": "تم تجاهل التنبيه",
|
||||||
@@ -1474,7 +1475,11 @@
|
|||||||
"platform": "المنصة",
|
"platform": "المنصة",
|
||||||
"status": "الحالة",
|
"status": "الحالة",
|
||||||
"isHighlighted": "تمييز (لون أحمر)",
|
"isHighlighted": "تمييز (لون أحمر)",
|
||||||
"notes": "ملاحظات"
|
"notes": "ملاحظات",
|
||||||
|
"isExternal": "اجتماع خارجي",
|
||||||
|
"isExternalHint": "يصبح صف الاجتماع باللون الأحمر تلقائياً، ولا يدخل في التأجيل التلقائي.",
|
||||||
|
"isExternalOn": "نعم",
|
||||||
|
"isExternalOff": "لا"
|
||||||
},
|
},
|
||||||
"attendees": {
|
"attendees": {
|
||||||
"label": "قائمة الحضور",
|
"label": "قائمة الحضور",
|
||||||
|
|||||||
@@ -1174,6 +1174,7 @@
|
|||||||
"atTime": "at {{time}}",
|
"atTime": "at {{time}}",
|
||||||
"done": "Done",
|
"done": "Done",
|
||||||
"postpone": "Postpone",
|
"postpone": "Postpone",
|
||||||
|
"externalCannotPostpone": "External meetings can't be auto-postponed.",
|
||||||
"dismiss": "Dismiss alert",
|
"dismiss": "Dismiss alert",
|
||||||
"dragHandle": "Drag to move",
|
"dragHandle": "Drag to move",
|
||||||
"dismissToast": "Alert dismissed",
|
"dismissToast": "Alert dismissed",
|
||||||
@@ -1347,7 +1348,11 @@
|
|||||||
"platform": "Platform",
|
"platform": "Platform",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"isHighlighted": "Highlight (red)",
|
"isHighlighted": "Highlight (red)",
|
||||||
"notes": "Notes"
|
"notes": "Notes",
|
||||||
|
"isExternal": "External meeting",
|
||||||
|
"isExternalHint": "Row turns red automatically and is excluded from auto-postpone.",
|
||||||
|
"isExternalOn": "Yes",
|
||||||
|
"isExternalOff": "No"
|
||||||
},
|
},
|
||||||
"attendees": {
|
"attendees": {
|
||||||
"label": "Attendees",
|
"label": "Attendees",
|
||||||
|
|||||||
@@ -194,6 +194,10 @@ type Meeting = {
|
|||||||
// is the same for every viewer. NULL/missing = "default" (no tint).
|
// is the same for every viewer. NULL/missing = "default" (no tint).
|
||||||
// Allowed values come from ROW_COLOR_OPTIONS below.
|
// Allowed values come from ROW_COLOR_OPTIONS below.
|
||||||
rowColor?: string | null;
|
rowColor?: string | null;
|
||||||
|
// #635: External meeting flag. When true, the row is force-tinted red
|
||||||
|
// and the alert-postpone button is disabled (the server also rejects
|
||||||
|
// /postpone-minutes with `external_meeting_no_auto_postpone`).
|
||||||
|
isExternal?: boolean;
|
||||||
// #486: optimistic-lock token used by /executive-meetings/swap-times
|
// #486: optimistic-lock token used by /executive-meetings/swap-times
|
||||||
// (the schedule row quick-actions Move up / Move down popover) and
|
// (the schedule row quick-actions Move up / Move down popover) and
|
||||||
// by the existing PostponeDialog when reused from the schedule.
|
// by the existing PostponeDialog when reused from the schedule.
|
||||||
@@ -1361,7 +1365,12 @@ function ScheduleSection({
|
|||||||
const rowColors = useMemo<Record<number, string>>(() => {
|
const rowColors = useMemo<Record<number, string>>(() => {
|
||||||
const out: Record<number, string> = {};
|
const out: Record<number, string> = {};
|
||||||
for (const m of meetings) {
|
for (const m of meetings) {
|
||||||
if (m.rowColor) out[m.id] = m.rowColor;
|
// #635: external meetings are always tinted red regardless of
|
||||||
|
// (and overriding) any manually-set rowColor, so the visual cue
|
||||||
|
// matches the flag even on legacy rows whose rowColor was set
|
||||||
|
// before isExternal existed.
|
||||||
|
if (m.isExternal) out[m.id] = "red";
|
||||||
|
else if (m.rowColor) out[m.id] = m.rowColor;
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}, [meetings]);
|
}, [meetings]);
|
||||||
@@ -3403,10 +3412,9 @@ function ScheduleSection({
|
|||||||
sortOrder: idx,
|
sortOrder: idx,
|
||||||
kind: a.kind ?? "person",
|
kind: a.kind ?? "person",
|
||||||
})),
|
})),
|
||||||
|
// #635: external meeting flag.
|
||||||
|
isExternal: editingMeeting.isExternal,
|
||||||
};
|
};
|
||||||
if (editingMeeting.dailyNumber.trim()) {
|
|
||||||
body.dailyNumber = Number(editingMeeting.dailyNumber);
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await apiJson(
|
await apiJson(
|
||||||
`/api/executive-meetings/${editingMeeting.id}`,
|
`/api/executive-meetings/${editingMeeting.id}`,
|
||||||
@@ -4984,6 +4992,12 @@ function MeetingRow({
|
|||||||
setQuickOpen(false);
|
setQuickOpen(false);
|
||||||
onQuickPostpone?.();
|
onQuickPostpone?.();
|
||||||
}}
|
}}
|
||||||
|
disabled={meeting.isExternal}
|
||||||
|
title={
|
||||||
|
meeting.isExternal
|
||||||
|
? t("executiveMeetings.alert.externalCannotPostpone")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
className={`flex-1 min-w-0 gap-2 px-3 py-2 h-10 text-sm font-medium shadow-sm ${isRtl ? "flex-row-reverse" : ""}`}
|
className={`flex-1 min-w-0 gap-2 px-3 py-2 h-10 text-sm font-medium shadow-sm ${isRtl ? "flex-row-reverse" : ""}`}
|
||||||
data-testid={`em-row-quick-postpone-${meeting.id}`}
|
data-testid={`em-row-quick-postpone-${meeting.id}`}
|
||||||
>
|
>
|
||||||
@@ -6266,7 +6280,6 @@ type MeetingFormState = {
|
|||||||
titleAr: string;
|
titleAr: string;
|
||||||
titleEn: string;
|
titleEn: string;
|
||||||
meetingDate: string;
|
meetingDate: string;
|
||||||
dailyNumber: string;
|
|
||||||
startTime: string;
|
startTime: string;
|
||||||
endTime: string;
|
endTime: string;
|
||||||
location: string;
|
location: string;
|
||||||
@@ -6276,6 +6289,9 @@ type MeetingFormState = {
|
|||||||
isHighlighted: boolean;
|
isHighlighted: boolean;
|
||||||
notes: string;
|
notes: string;
|
||||||
attendees: Attendee[];
|
attendees: Attendee[];
|
||||||
|
// #635: external meeting flag — when true the row is force-tinted red
|
||||||
|
// and the alert-postpone button is disabled.
|
||||||
|
isExternal: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate a stable client-only attendee id for the manage-dialog DnD.
|
// Generate a stable client-only attendee id for the manage-dialog DnD.
|
||||||
@@ -6294,7 +6310,6 @@ function emptyMeetingForm(date: string): MeetingFormState {
|
|||||||
titleAr: "",
|
titleAr: "",
|
||||||
titleEn: "",
|
titleEn: "",
|
||||||
meetingDate: date,
|
meetingDate: date,
|
||||||
dailyNumber: "",
|
|
||||||
startTime: "",
|
startTime: "",
|
||||||
endTime: "",
|
endTime: "",
|
||||||
location: "",
|
location: "",
|
||||||
@@ -6304,6 +6319,7 @@ function emptyMeetingForm(date: string): MeetingFormState {
|
|||||||
isHighlighted: false,
|
isHighlighted: false,
|
||||||
notes: "",
|
notes: "",
|
||||||
attendees: [],
|
attendees: [],
|
||||||
|
isExternal: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6602,7 +6618,6 @@ function ManageSection({
|
|||||||
titleAr: htmlToPlainText(m.titleAr),
|
titleAr: htmlToPlainText(m.titleAr),
|
||||||
titleEn: htmlToPlainText(m.titleEn ?? ""),
|
titleEn: htmlToPlainText(m.titleEn ?? ""),
|
||||||
meetingDate: m.meetingDate,
|
meetingDate: m.meetingDate,
|
||||||
dailyNumber: String(m.dailyNumber),
|
|
||||||
startTime: m.startTime ? m.startTime.slice(0, 5) : "",
|
startTime: m.startTime ? m.startTime.slice(0, 5) : "",
|
||||||
endTime: m.endTime ? m.endTime.slice(0, 5) : "",
|
endTime: m.endTime ? m.endTime.slice(0, 5) : "",
|
||||||
location: m.location ?? "",
|
location: m.location ?? "",
|
||||||
@@ -6622,6 +6637,7 @@ function ManageSection({
|
|||||||
name: htmlToPlainText(a.name),
|
name: htmlToPlainText(a.name),
|
||||||
_sid: genAttendeeSid(),
|
_sid: genAttendeeSid(),
|
||||||
})),
|
})),
|
||||||
|
isExternal: m.isExternal ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6675,8 +6691,10 @@ function ManageSection({
|
|||||||
// schedule survive a save through the Manage dialog.
|
// schedule survive a save through the Manage dialog.
|
||||||
kind: a.kind ?? "person",
|
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,
|
||||||
};
|
};
|
||||||
if (editing.dailyNumber.trim()) body.dailyNumber = Number(editing.dailyNumber);
|
|
||||||
try {
|
try {
|
||||||
if (editing.id == null) {
|
if (editing.id == null) {
|
||||||
await apiJson(`/api/executive-meetings`, { method: "POST", body });
|
await apiJson(`/api/executive-meetings`, { method: "POST", body });
|
||||||
@@ -7553,16 +7571,26 @@ function MeetingFormDialog({
|
|||||||
onChange={(e) => update("meetingDate", e.target.value)}
|
onChange={(e) => update("meetingDate", e.target.value)}
|
||||||
/>
|
/>
|
||||||
</FormRow>
|
</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. */}
|
||||||
<FormRow
|
<FormRow
|
||||||
label={t("executiveMeetings.manage.field.dailyNumber")}
|
label={t("executiveMeetings.manage.field.isExternal")}
|
||||||
hint={t("executiveMeetings.manage.field.dailyNumberHint")}
|
hint={t("executiveMeetings.manage.field.isExternalHint")}
|
||||||
>
|
>
|
||||||
<Input
|
<div className="flex items-center gap-2 h-9">
|
||||||
type="number"
|
<Switch
|
||||||
min={1}
|
checked={state.isExternal}
|
||||||
value={state.dailyNumber}
|
onCheckedChange={(v) => update("isExternal", Boolean(v))}
|
||||||
onChange={(e) => update("dailyNumber", e.target.value)}
|
data-testid="em-form-isExternal"
|
||||||
/>
|
/>
|
||||||
|
<span className="text-xs text-gray-700">
|
||||||
|
{state.isExternal
|
||||||
|
? t("executiveMeetings.manage.field.isExternalOn")
|
||||||
|
: t("executiveMeetings.manage.field.isExternalOff")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow
|
<FormRow
|
||||||
label={t("executiveMeetings.manage.field.startTime")}
|
label={t("executiveMeetings.manage.field.startTime")}
|
||||||
@@ -7843,6 +7871,7 @@ function AuditDiffSummary({
|
|||||||
"platform",
|
"platform",
|
||||||
"status",
|
"status",
|
||||||
"isHighlighted",
|
"isHighlighted",
|
||||||
|
"isExternal",
|
||||||
"notes",
|
"notes",
|
||||||
"assignedTo",
|
"assignedTo",
|
||||||
"taskType",
|
"taskType",
|
||||||
|
|||||||
@@ -73,6 +73,12 @@ export const executiveMeetingsTable = pgTable(
|
|||||||
// signal about the meeting (urgent / VIP / etc.) rather than a
|
// signal about the meeting (urgent / VIP / etc.) rather than a
|
||||||
// personal viewing preference.
|
// personal viewing preference.
|
||||||
rowColor: varchar("row_color", { length: 16 }),
|
rowColor: varchar("row_color", { length: 16 }),
|
||||||
|
// #635: External meetings (اجتماع خارجي). When true, the row is
|
||||||
|
// tinted red in the schedule grid and is excluded from the
|
||||||
|
// auto-postpone cascade (the alert "Postpone" button is disabled
|
||||||
|
// and `computeCascadeShift` skips it as a follower). Default false
|
||||||
|
// preserves legacy rows as "internal" meetings.
|
||||||
|
isExternal: boolean("is_external").notNull().default(false),
|
||||||
createdBy: integer("created_by").references(() => usersTable.id, {
|
createdBy: integer("created_by").references(() => usersTable.id, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user