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