#322 Simplify Add/Edit Meeting dialog and attendee rows

Trim the إضافة اجتماع / تعديل اجتماع dialog and attendee rows in
artifacts/tx-os/src/pages/executive-meetings.tsx down to the fields
the user actually fills in.

MeetingFormDialog
- Removed FormRows: titleEn, location, meetingUrl, platform, status,
  isHighlighted, notes.
- Kept titleAr, meetingDate, dailyNumber, startTime, endTime, and
  the attendees section.
- Save validation now only requires titleAr.
- titleEn is always mirrored from titleAr on save. The English input
  is gone, so there is no way to keep an independent English title;
  always syncing avoids leaving a stale server-side titleEn after the
  Arabic title is edited. The API still receives a non-empty titleEn
  so POST validation (min length 1) is satisfied.
- MeetingFormState, emptyMeetingForm, and openEdit are unchanged so
  hidden fields (location, notes, platform, status, isHighlighted,
  meetingUrl) round-trip through the save body and existing data is
  preserved on edit.

SortableAttendeeRow
- Removed the parenthesized title Input and the
  internal/virtual/external Select.
- Dropped the now-unused onChangeTitle / onChangeAttendanceType
  props from the component signature and the call site.
- Existing attendees keep their stored title and attendanceType;
  new attendees added via addAttendee continue to default to
  title=null and attendanceType="internal" so the schedule's
  grouping keeps working.

Cleanup
- Removed the now-unused Textarea import (only the notes field
  used it). Switch is still imported because it is used elsewhere
  in the file.

Code review found two issues that were fixed before completion:
- Mirroring made deterministic (always sync) instead of
  blank-only fallback.
- Orphaned Textarea import removed.

No DB schema, API, schedule view, Manage list, audit, notifications,
or translation changes.
This commit is contained in:
riyadhafraa
2026-05-03 09:50:53 +00:00
parent fd7c3c6498
commit 60533b9ceb
@@ -47,7 +47,6 @@ import {
type TimePicker12hHandle,
} from "@/components/time-picker-12h";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Dialog,
DialogContent,
@@ -4821,13 +4820,20 @@ function ManageSection({
async function save(timeOverrides?: { startTime: string; endTime: string }) {
if (!editing) return;
if (!editing.titleAr.trim() || !editing.titleEn.trim()) {
if (!editing.titleAr.trim()) {
toast({
title: t("executiveMeetings.manage.errors.titleRequired"),
variant: "destructive",
});
return;
}
// #322: the English-title input was removed from the dialog, so
// there is no way for the user to set a separate English value
// anymore. Always mirror the Arabic title — otherwise editing the
// Arabic title on an existing meeting would leave a stale English
// copy on the server. The API requires `titleEn` (min length 1)
// on POST, and PATCH will update it on every save.
const titleEn = editing.titleAr.trim();
// Prefer the dialog-supplied canonical values from the time
// picker's commit() result over the controlled `editing.*`
// state — the picker's onChange suppresses ambiguous/invalid
@@ -4843,7 +4849,7 @@ function ManageSection({
setSaving(true);
const body: Record<string, unknown> = {
titleAr: editing.titleAr.trim(),
titleEn: editing.titleEn.trim(),
titleEn,
meetingDate: editing.meetingDate,
startTime,
endTime,
@@ -5106,8 +5112,6 @@ function SortableAttendeeRow({
onMoveUp,
onMoveDown,
onChangeName,
onChangeTitle,
onChangeAttendanceType,
onRemove,
}: {
attendee: Attendee;
@@ -5117,8 +5121,6 @@ function SortableAttendeeRow({
onMoveUp: () => void;
onMoveDown: () => void;
onChangeName: (v: string) => void;
onChangeTitle: (v: string) => void;
onChangeAttendanceType: (v: Attendee["attendanceType"]) => void;
onRemove: () => void;
}) {
const sid = attendee._sid ?? `__missing-sid-${index}`;
@@ -5195,8 +5197,13 @@ function SortableAttendeeRow({
{t("executiveMeetings.manage.attendees.subheadingBadge")}
</span>
)}
{/* #322: name is the only attendee field exposed in the dialog.
The parenthesized title and the internal/virtual/external
select were removed; existing rows keep their stored values
and new rows default to title=null + attendanceType="internal"
(see addAttendee). */}
<Input
className={isSub ? "flex-[2]" : "flex-1"}
className="flex-1"
placeholder={
isSub
? t("executiveMeetings.manage.attendees.subheadingName")
@@ -5205,32 +5212,6 @@ function SortableAttendeeRow({
value={attendee.name}
onChange={(e) => onChangeName(e.target.value)}
/>
{!isSub && (
<Input
className="flex-1"
placeholder={t("executiveMeetings.manage.attendees.title")}
value={attendee.title ?? ""}
onChange={(e) => onChangeTitle(e.target.value)}
data-testid={`em-attendee-title-${index}`}
/>
)}
<Select
value={attendee.attendanceType}
onValueChange={(v) =>
onChangeAttendanceType(v as Attendee["attendanceType"])
}
>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
{(["internal", "virtual", "external"] as const).map((tp) => (
<SelectItem key={tp} value={tp}>
{t(`executiveMeetings.manage.attendanceType.${tp}`)}
</SelectItem>
))}
</SelectContent>
</Select>
<Button size="icon" variant="ghost" onClick={onRemove}>
<Trash2 className="w-4 h-4 text-red-600" />
</Button>
@@ -5434,9 +5415,6 @@ function MeetingFormDialog({
data-testid="em-form-titleAr"
/>
</FormRow>
<FormRow label={t("executiveMeetings.manage.field.titleEn")}>
<Input value={state.titleEn} onChange={(e) => update("titleEn", e.target.value)} />
</FormRow>
<FormRow label={t("executiveMeetings.manage.field.meetingDate")}>
<Input
type="date"
@@ -5487,59 +5465,11 @@ function MeetingFormDialog({
pmTestId="em-form-endTime-pm"
/>
</FormRow>
<FormRow label={t("executiveMeetings.manage.field.location")}>
<Input value={state.location} onChange={(e) => update("location", e.target.value)} />
</FormRow>
<FormRow label={t("executiveMeetings.manage.field.meetingUrl")}>
<Input
value={state.meetingUrl}
onChange={(e) => update("meetingUrl", e.target.value)}
/>
</FormRow>
<FormRow label={t("executiveMeetings.manage.field.platform")}>
<Select
value={state.platform}
onValueChange={(v) => update("platform", v as Meeting["platform"])}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{(["none", "webex", "teams", "zoom", "other"] as const).map((p) => (
<SelectItem key={p} value={p}>
{t(`executiveMeetings.manage.platform.${p}`)}
</SelectItem>
))}
</SelectContent>
</Select>
</FormRow>
<FormRow label={t("executiveMeetings.manage.field.status")}>
<Select value={state.status} onValueChange={(v) => update("status", v)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{(["scheduled", "cancelled", "completed", "postponed"] as const).map((s) => (
<SelectItem key={s} value={s}>
{t(`executiveMeetings.manage.status.${s}`)}
</SelectItem>
))}
</SelectContent>
</Select>
</FormRow>
<FormRow label={t("executiveMeetings.manage.field.isHighlighted")}>
<Switch
checked={state.isHighlighted}
onCheckedChange={(v) => update("isHighlighted", v)}
/>
</FormRow>
<FormRow label={t("executiveMeetings.manage.field.notes")} full>
<Textarea
value={state.notes}
onChange={(e) => update("notes", e.target.value)}
rows={3}
/>
</FormRow>
{/* #322: location, meetingUrl, platform, status,
isHighlighted, and notes inputs were removed from the
dialog. Their values still round-trip through
MeetingFormState and the save body so existing data is
preserved for meetings being edited. */}
</div>
<div className="border-t border-gray-200 pt-3 mt-1">
@@ -5621,12 +5551,6 @@ function MeetingFormDialog({
onMoveUp={() => moveAttendee(i, -1)}
onMoveDown={() => moveAttendee(i, 1)}
onChangeName={(v) => setAttendee(i, { name: v })}
onChangeTitle={(v) =>
setAttendee(i, { title: v || null })
}
onChangeAttendanceType={(v) =>
setAttendee(i, { attendanceType: v })
}
onRemove={() => removeAttendee(i)}
/>
))}