Task #138: Show a clear message when meeting end time is before start time

Original ask
- In the executive-meetings schedule, when an admin edits a meeting's
  inline time and types end < start, the change was silently discarded.
- Surface a localized toast for both the client-side guard and the
  server's 400 validation, and keep the cell in edit mode with the
  user's draft preserved so they can fix the typo without retyping.

What changed
- artifacts/tx-os/src/pages/executive-meetings.tsx
  - TimeRangeCell.save now defers setEditing(false) until after the
    server PATCH succeeds. On a thrown TimeOrderError it stays in edit
    mode (preserving the typed draft) and refocuses the start input.
    On any other failure it rolls back to the saved values as before.
  - The parent saveTimes callback inspects the API error message; when
    it matches the server's "startTime must be <= endTime" validation,
    it shows the same localized timeOrderError toast (instead of the
    raw English error string) and rethrows a tagged Error with
    name "TimeOrderError" so TimeRangeCell can react.
- artifacts/tx-os/src/locales/en.json + ar.json
  - Updated the existing executiveMeetings.schedule.timeOrderError
    keys to the exact wording from the task spec:
      EN: "End time is before start time"
      AR: "وقت النهاية قبل وقت البداية"

Notes / non-changes
- The client-side guard at the top of TimeRangeCell.save already
  emitted the toast and returned without exiting edit mode, so it only
  needed the new copy. The change to defer setEditing(false) avoids a
  race where the !editing useEffect would wipe the draft to startSaved
  during the network round-trip on the server-side error path.
- No new i18n keys were added; existing keys were repurposed with the
  spec's exact wording.

Verification
- pnpm tsc on tx-os: no new TypeScript errors in executive-meetings.tsx
  (pre-existing errors in admin.tsx / use-notifications-socket.ts are
  unrelated to this task).
- e2e Playwright test: created a meeting at 10:00–11:00, opened the
  inline editor, set 09:00 / 08:00 and clicked save → destructive
  toast "وقت النهاية قبل وقت البداية" appeared, inputs stayed visible
  with the typed values intact. Corrected end to 10:00 and saved →
  cell exited edit mode showing 09:00 – 10:00 and the API persisted
  the new times.

Replit-Task-Id: f701faa8-02a9-4389-b130-92e522744128
This commit is contained in:
riyadhafraa
2026-04-30 08:35:24 +00:00
parent 0fe28fe16d
commit b1f2bab1cd
4 changed files with 28 additions and 6 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

+1 -1
View File
@@ -865,7 +865,7 @@
"newMeetingDefaultEn": "New meeting",
"timeStart": "وقت البداية",
"timeEnd": "وقت النهاية",
"timeOrderError": "وقت النهاية يجب أن يكون بعد وقت البداية",
"timeOrderError": "وقت النهاية قبل وقت البداية",
"addAttendee": "أضف حاضر",
"addVirtualAttendee": "+ حاضر عبر الاتصال المرئي",
"addInternalAttendee": "+ حاضر داخلي",
+1 -1
View File
@@ -818,7 +818,7 @@
"newMeetingDefaultEn": "New meeting",
"timeStart": "Start time",
"timeEnd": "End time",
"timeOrderError": "End time must be after start time",
"timeOrderError": "End time is before start time",
"addAttendee": "Add attendee",
"addVirtualAttendee": "+ Virtual",
"addInternalAttendee": "+ Internal",
@@ -1060,6 +1060,20 @@ function ScheduleSection({
refreshDay();
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
// The server enforces start <= end and returns a 400 like
// "endTime: startTime must be <= endTime". Surface the same
// friendly localized message the client-side guard uses, and
// throw a tagged error so TimeRangeCell can re-open edit mode
// with the user's draft values intact instead of resetting.
if (msg.includes("startTime must be <= endTime")) {
toast({
title: t("executiveMeetings.schedule.timeOrderError"),
variant: "destructive",
});
const e = new Error("time-order");
e.name = "TimeOrderError";
throw e;
}
toast({
title: t("common.error"),
description: msg,
@@ -2887,15 +2901,23 @@ function TimeRangeCell({
startInputRef.current?.focus();
return;
}
setEditing(false);
try {
await onSaveTimes(newStart, newEnd);
} catch {
setEditing(false);
} catch (err) {
// The parent `saveTimes` handler is responsible for surfacing the
// error toast; we only need to roll back the local draft state so
// the cell shows the previously-saved values.
// error toast. For the server-side time-order validation error we
// stay in edit mode with the user's draft values intact so they
// can fix the typo without re-entering both times, then refocus
// the start input. For any other failure we drop the draft and
// return to the previously-saved values.
if (err instanceof Error && err.name === "TimeOrderError") {
startInputRef.current?.focus();
return;
}
setStart(startSaved);
setEnd(endSaved);
setEditing(false);
}
}, [start, end, startSaved, endSaved, onSaveTimes]);