Commit Graph

543 Commits

Author SHA1 Message Date
riyadhafraa 78a1c5072d Task #351: Match Executive Meetings PDF to user's reference layout
User compared the rendered PDF against their reference (rrr1) and flagged
three concrete differences. This change addresses all three:

1. Time format — formatTimeRange now joins start/end with U+2011
   NON-BREAKING HYPHEN (no surrounding spaces): "9:40‑9:50" instead of
   "9:40 – 9:50". The non-breaking hyphen looks identical to ASCII "-"
   and prevents PDFKit from wrapping the range across two lines in the
   narrow Time column.

2. Date placement — removed the ISO date that was printed under the
   header title. Added a new bottom-aligned footer block on the leading
   edge (right for AR, left for EN) that prints:
     • "مقيد" / "Recorded by" (bold)
     • Long-form date "3 May 2026" (always Latin, English month name
       even on the AR PDF, mirroring the reference)
   The date row is forced to baseDirection="ltr" so RTL bidi doesn't
   visually reorder the runs into "May 2026 3" on the AR PDF.
   formatLongDate parses the ISO route param in UTC to avoid host-tz
   day drift. The footer is extracted into a drawFooter() helper and
   called in BOTH the empty-day and populated-day code paths so it
   always renders.

3. Title weight — title was already calling drawMixedLine with
   weight:"bold" but added explicit confirmation; column proportions
   tweaked (6/36/39/19) so the wider Time column doesn't force range
   wrapping while keeping enough room for Meeting/Attendees content.

Other touches:
- Added `recordedBy` to PdfLabels type + RenderPdfInput.labels type
  and to ar.json/en.json executiveMeetings.pdf blocks.
- Page-break check reserves footer height so the last row never
  overlaps the bottom footer.

Verified: PDF Arabic shaping regression test passes; live AR/EN PDFs
match the reference; one-page-fit still holds for typical days.
Pre-existing tsc errors in executive-meetings.ts and unrelated test
suite failures are not caused by this work.
2026-05-03 19:28:11 +00:00
riyadhafraa 0ef7d91cbe Task #351: Match Executive Meetings PDF to user's reference layout
User compared the rendered PDF against their reference (rrr1) and flagged
three concrete differences. This change addresses all three:

1. Time format — formatTimeRange now joins start/end with U+2011
   NON-BREAKING HYPHEN (no surrounding spaces): "9:40‑9:50" instead of
   "9:40 – 9:50". The non-breaking hyphen looks identical to ASCII "-"
   and prevents PDFKit from wrapping the range across two lines in the
   narrow Time column.

2. Date placement — removed the ISO date that was printed under the
   header title. Added a new bottom-aligned footer block on the leading
   edge (right for AR, left for EN) that prints:
     • "مقيد" / "Recorded by" (bold)
     • Long-form date "3 May 2026" (always Latin, English month name
       even on the AR PDF, mirroring the reference)
   The date row is forced to baseDirection="ltr" so RTL bidi doesn't
   visually reorder the runs into "May 2026 3" on the AR PDF.
   formatLongDate parses the ISO route param in UTC to avoid host-tz
   day drift. The footer is extracted into a drawFooter() helper and
   called in BOTH the empty-day and populated-day code paths so it
   always renders.

3. Title weight — title was already calling drawMixedLine with
   weight:"bold" but added explicit confirmation; column proportions
   tweaked (6/36/39/19) so the wider Time column doesn't force range
   wrapping while keeping enough room for Meeting/Attendees content.

Other touches:
- Added `recordedBy` to PdfLabels type + RenderPdfInput.labels type
  and to ar.json/en.json executiveMeetings.pdf blocks.
- Page-break check reserves footer height so the last row never
  overlaps the bottom footer.

Verified: PDF Arabic shaping regression test passes; live AR/EN PDFs
match the reference; one-page-fit still holds for typical days.
Pre-existing tsc errors in executive-meetings.ts and unrelated test
suite failures are not caused by this work.
2026-05-03 19:27:11 +00:00
riyadhafraa 26a869c0eb PDF: inline attendees, tighter layout — fit a typical day on one page
Follow-up to the Arabic shaping fix. The downloaded schedule PDF was
spilling onto a second page even for modest days because (a) attendees
were stacked one-per-line and (b) the page header + cell padding +
column proportions reserved more vertical space than necessary.

Changes (artifacts/api-server/src/lib/pdf-renderer.ts):

- Attendees now render as a single inline paragraph per cell (joined
  with two spaces) and wrap naturally — matches the user's reference
  layout where attendees flow as one paragraph.
- Re-balanced column widths from 6/30/44/20 → 6/38/40/16. Meeting
  titles get more room (no more 6-line wraps for typical titles) and
  attendees get less (they're inline now).
- Page margin tightened 36pt → 24pt.
- Title size cap tightened (28 → 22) so the header band doesn't eat a
  table row.
- moveDown gaps trimmed (0.4→0.2 between title and date, 0.8→0.4
  before the table).
- Cell vertical padding trimmed (5pt → 3pt).
- drawWrappingLine now passes lineGap: -1 to pdfkit's text() so wrapped
  paragraphs get tighter inter-line spacing; the row-height probe
  passes the same option so probe and draw stay symmetric.
- Row-height probe now uses heightOfString() output directly
  (Math.max(lineHeight, measured) + 2) instead of rounding up to whole
  lineHeight units, removing the half-line of empty space that was
  visible below short cells.

Verified:
- EN: all 10 meetings on one A4 page.
- AR: 9 of 10 meetings on one A4 page; the 10th row contains
  multi-line internal/external subheadings which legitimately need a
  taller cell — typical days fit on a single page.
- PDF Arabic shaping regression test still passes.
- No row overlaps anywhere.

Logo: handler already loads the brand-settings logo (logoObjectPath)
and embeds it in the header — no code change needed. If the logo isn't
visible, none has been uploaded in brand settings.
2026-05-03 19:11:49 +00:00
riyadhafraa e0824bc6a0 Fix overlapping text and improve Arabic rendering in PDFs
Update PDF rendering logic to use fontkit for Arabic shaping and RTL reordering, eliminating pre-shaping and resolving text overlap issues.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 32b8a89e-ae1f-4a09-ae50-3a53ae3f3477
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/Ow4s0aa
Replit-Helium-Checkpoint-Created: true
2026-05-03 19:01:50 +00:00
riyadhafraa 6cbd5968e5 Ensure Arabic text displays correctly when printing documents
Update font configurations to exclusively use DIN Next LT Arabic, preload necessary font files, and implement eager font loading to prevent display issues during printing.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 814df7a0-9300-4efe-a4b5-b5e474e8c99f
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/Ow4s0aa
Replit-Helium-Checkpoint-Created: true
2026-05-03 18:37:48 +00:00
riyadhafraa a4e58b2263 Improve dialog box alignment and spacing for better user experience
Update `DialogHeader` to use `sm:text-start` for RTL alignment and `DialogFooter` to use `gap-2` instead of `sm:space-x-2` for improved spacing in dialog components.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: f335791d-bd37-4801-bf2e-18c339d6fdc5
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/Ow4s0aa
Replit-Helium-Checkpoint-Created: true
2026-05-03 17:43:07 +00:00
riyadhafraa abaf54f254 Improve Arabic text rendering for printouts by preloading fonts
Preload DIN Next LT Arabic and Tajawal fonts in index.html and adjust the print CSS font stack to ensure correct Arabic character shaping in print mode.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 820da5a0-fe82-4670-9807-63a51f533e44
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/Ow4s0aa
Replit-Helium-Checkpoint-Created: true
2026-05-03 16:02:10 +00:00
riyadhafraa 03e92f9e8c Improve Arabic text display when printing the schedule
Add print-specific CSS to force Arabic web font and enable ligatures for proper shaping in print mode.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 93b31a62-139b-4870-b40e-2ffc4ec05a8d
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/Ow4s0aa
Replit-Helium-Checkpoint-Created: true
2026-05-03 15:55:29 +00:00
riyadhafraa 627a8fe7b1 Fix Arabic text rendering in generated PDFs
Add Arabic text shaping functionality to convert base characters to their contextual presentation forms before bidi reordering and PDF rendering. Includes a new regression test to verify correct shaping by asserting the presence of presentation form codepoints in the PDF's embedded ToUnicode CMap.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 18d38e3a-cfe8-4b69-a02c-380283320b78
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/Ow4s0aa
Replit-Helium-Checkpoint-Created: true
2026-05-03 15:26:38 +00:00
riyadhafraa 62ee62e6fa Task #349 follow-up: brand-logo embed test + label module + user-scope guard
Original task: Executive Meetings PDF improvements — respect user
font prefs, render saved per-meeting rowColor, drop legacy
isHighlighted baking, brand logo on left of PDF header with title
"قائمة بأسماء حضور الاجتماعات", and a logo upload + font color
picker in the font-settings page.

Prior mark_task_complete was rejected for three issues. This commit
closes all of them:

- Extract bilingual PDF labels into
  artifacts/api-server/src/lib/pdf-labels.ts and import it from the
  PDF route. Mirror the same keys under executiveMeetings.pdf.* in
  the tx-os ar.json / en.json locales so the frontend stays in
  sync.
- Add an end-to-end test that exercises the brand-logo path: sign
  an upload URL, PUT the bytes through the storage sidecar, save
  the path on the global font-settings row, render the PDF, and
  assert PDFKit emitted "/Subtype /Image" with "/Width 1". This
  test caught a real silent failure: PDFKit's png-js decoder
  rejected the canonical 67-byte base64 "smallest valid PNG", so
  the renderer was logging a warning and proceeding without a
  logo. The fixture now constructs a real 1x1 RGB PNG inline using
  zlib + a CRC32 routine (no new dependencies). Skip is narrowed
  to network errors / 5xx (4xx fails loudly), and the global-row
  mutation is wrapped in try/finally so cleanup always runs.
- Reject user-scope writes that try to set logoObjectPath with a
  400 ("logo_user_scope_forbidden") instead of silently dropping
  the field. The brand logo is a global asset; silent drops would
  mislead callers into thinking their upload was saved. Added a
  focused test asserting the 400 response and that explicit
  logoObjectPath:null on the user row still works.

Also removed 12 stray backup/snapshot files at the repo root
(*.old, *-base.{ts,tsx,mjs}, locale snapshots) that were
accidentally tracked in the previous commit.

Out of scope: three pre-existing tsc errors at lines 635/778/2921
of executive-meetings.ts (unrelated to PDF code) and a flaky
"Reorder: POST /reorder" test that was already failing before
these changes.
2026-05-03 15:10:34 +00:00
riyadhafraa 4640bda260 Task #349 follow-up: brand-logo embed test + label module + user-scope guard
Original task: Executive Meetings PDF improvements — respect user
font prefs, render saved per-meeting rowColor, drop legacy
isHighlighted baking, brand logo on left of PDF header with title
"قائمة بأسماء حضور الاجتماعات", and a logo upload + font color
picker in the font-settings page.

Prior mark_task_complete was rejected for three issues. This commit
closes all of them:

- Extract bilingual PDF labels into
  artifacts/api-server/src/lib/pdf-labels.ts and import it from the
  PDF route. Mirror the same keys under executiveMeetings.pdf.* in
  the tx-os ar.json / en.json locales so the frontend stays in
  sync.
- Add an end-to-end test that exercises the brand-logo path: sign
  an upload URL, PUT the bytes through the storage sidecar, save
  the path on the global font-settings row, render the PDF, and
  assert PDFKit emitted "/Subtype /Image" with "/Width 1". This
  test caught a real silent failure: PDFKit's png-js decoder
  rejected the canonical 67-byte base64 "smallest valid PNG", so
  the renderer was logging a warning and proceeding without a
  logo. The fixture now constructs a real 1x1 RGB PNG inline using
  zlib + a CRC32 routine (no new dependencies). Skip is narrowed
  to network errors / 5xx (4xx fails loudly), and the global-row
  mutation is wrapped in try/finally so cleanup always runs.
- Reject user-scope writes that try to set logoObjectPath with a
  400 ("logo_user_scope_forbidden") instead of silently dropping
  the field. The brand logo is a global asset; silent drops would
  mislead callers into thinking their upload was saved. Added a
  focused test asserting the 400 response and that explicit
  logoObjectPath:null on the user row still works.

Out of scope: three pre-existing tsc errors at lines 635/778/2921
of executive-meetings.ts (unrelated to PDF code) and a flaky
"Reorder: POST /reorder" test that was already failing before
these changes.
2026-05-03 15:03:54 +00:00
riyadhafraa f810afe0cd Task #349 follow-up: brand-logo embed test + PDF label module
Original task: Executive Meetings PDF improvements — respect user
font prefs, render saved per-meeting rowColor, drop legacy
isHighlighted baking, brand logo on left of PDF header with title
"قائمة بأسماء حضور الاجتماعات", and a logo upload + font color
picker in the font-settings page.

Prior mark_task_complete was rejected for three issues. The first
(stray backup files) was a false positive. This commit closes the
remaining two:

- Extract bilingual PDF labels into
  artifacts/api-server/src/lib/pdf-labels.ts and import it from the
  PDF route so the strings are no longer inlined inside the route
  handler. Mirror the same keys under executiveMeetings.pdf.* in
  the tx-os ar.json / en.json locales so the frontend stays in
  sync.
- Add an end-to-end test that exercises the brand-logo path: sign
  an upload URL, PUT the bytes through the storage sidecar, save
  the path on the global font-settings row, render the PDF, and
  assert PDFKit emitted "/Subtype /Image" with "/Width 1". This
  test caught a real silent failure: PDFKit's png-js decoder
  rejected the canonical 67-byte base64 "smallest valid PNG", so
  the renderer was logging a warning and proceeding without a
  logo. The fixture now constructs a real 1x1 RGB PNG inline using
  zlib + a CRC32 routine (no new dependencies) and the assertion
  passes.

Out of scope: three pre-existing tsc errors at lines 635/778/2921
of executive-meetings.ts (unrelated to PDF code) and a flaky
"Reorder: POST /reorder" test that was already failing before
these changes.
2026-05-03 14:59:23 +00:00
riyadhafraa 1c7c91e8f2 Task #349: Executive Meetings PDF improvements
- Schema: add `font_color` (hex, default #000000) and `logo_object_path`
  to `executive_meeting_font_settings`. Pushed via drizzle-kit.

- PDF renderer:
  - Add `fontColor` to PdfFontPrefs (body cells only; header chrome
    and logo intentionally ignore it to preserve branding).
  - Add `rowColor` to PdfMeeting and a ROW_COLOR_FILL palette kept in
    lockstep with the on-screen swatches; deliberately stop painting
    the legacy `isHighlighted` overlay so the archived PDF reflects
    editorial state instead of the viewer's transient cursor.
  - Add optional `logo: Buffer`. Header now reserves a left-anchored
    logo box and centers the title in the remaining width; bad image
    bytes log + fall back to the no-logo layout instead of crashing.

- API route:
  - Extend fontSettingsSchema with strict #RRGGBB regex and
    /^/objects/<id>$/ regex for logoObjectPath.
  - resolveFontPrefsForUser now returns { font, logoObjectPath }.
  - loadLogoBytes downloads the brand asset via ObjectStorageService.
  - Logo only writable on the global-scope row.
  - PDF labels switched to "قائمة بأسماء حضور الاجتماعات" /
    "Meeting Attendance List" per the user's printed sample.

- Frontend (executive-meetings.tsx):
  - FontPrefs gains fontColor; FontSettingsResponse.global gains
    logoObjectPath; DEFAULT_FONT and effectiveFont updated.
  - buildFontStyle applies fontColor to on-screen rows.
  - FontSettingsSection: native color picker + hex text input;
    logo upload (PNG/JPEG) via @workspace/object-storage-web's
    useUpload, visible only at the global scope for admins.

- Locales: AR/EN keys for fontColor + logo.{label,upload,replace,
  remove,uploading,uploadFailed,globalOnly}.

- Tests: existing font-settings roundtrip extended with fontColor;
  new test rejects malformed fontColor and non-/objects logo paths.
  Added "PDF content" test that inflates PDFKit FlateDecode streams
  and asserts (a) /Title carries the new Arabic label, (b) amber
  rowColor paints #fef3c7, (c) #fecaca isHighlighted overlay is
  gone, (d) user fontColor reaches body text fills.

- Frontend follow-up: logo preview in FontSettingsSection now uses
  resolveServiceImageUrl so /objects/<id> -> /api/storage/objects/<id>
  (the URL the API actually serves), matching chat-avatar plumbing.

Type-check clean for api-server and tx-os; new font-settings + PDF
content tests pass. Other test failures in the suite pre-date this
change.
2026-05-03 14:43:22 +00:00
riyadhafraa 57e8297464 Task #349: Executive Meetings PDF improvements
- Schema: add `font_color` (hex, default #000000) and `logo_object_path`
  to `executive_meeting_font_settings`. Pushed via drizzle-kit.

- PDF renderer:
  - Add `fontColor` to PdfFontPrefs (body cells only; header chrome
    and logo intentionally ignore it to preserve branding).
  - Add `rowColor` to PdfMeeting and a ROW_COLOR_FILL palette kept in
    lockstep with the on-screen swatches; deliberately stop painting
    the legacy `isHighlighted` overlay so the archived PDF reflects
    editorial state instead of the viewer's transient cursor.
  - Add optional `logo: Buffer`. Header now reserves a left-anchored
    logo box and centers the title in the remaining width; bad image
    bytes log + fall back to the no-logo layout instead of crashing.

- API route:
  - Extend fontSettingsSchema with strict #RRGGBB regex and
    /^/objects/<id>$/ regex for logoObjectPath.
  - resolveFontPrefsForUser now returns { font, logoObjectPath }.
  - loadLogoBytes downloads the brand asset via ObjectStorageService.
  - Logo only writable on the global-scope row.
  - PDF labels switched to "قائمة بأسماء حضور الاجتماعات" /
    "Meeting Attendance List" per the user's printed sample.

- Frontend (executive-meetings.tsx):
  - FontPrefs gains fontColor; FontSettingsResponse.global gains
    logoObjectPath; DEFAULT_FONT and effectiveFont updated.
  - buildFontStyle applies fontColor to on-screen rows.
  - FontSettingsSection: native color picker + hex text input;
    logo upload (PNG/JPEG) via @workspace/object-storage-web's
    useUpload, visible only at the global scope for admins.

- Locales: AR/EN keys for fontColor + logo.{label,upload,replace,
  remove,uploading,uploadFailed,globalOnly}.

- Tests: existing font-settings roundtrip extended with fontColor;
  new test rejects malformed fontColor and non-/objects logo paths.

Type-check clean for api-server and tx-os; new font-settings tests
pass. Other test failures in the suite pre-date this change.
2026-05-03 14:34:29 +00:00
riyadhafraa b864b92dc6 Task #348: Arabic-friendly Executive Meetings audit log
- Reorder audit table columns to RTL order: المستخدم، الوقت، الإجراء،
  النوع، التغييرات. Drop the entityId ("المعرف") column entirely. Update
  loading/empty colSpan from 6 to 5 to match the new column count.
- Rename audit.col.actor copy: "المنفّذ" → "المستخدم" (ar.json) and
  "Actor" → "User" (en.json). Underlying data field name unchanged.
- Localize the Changes cell in AuditDiffSummary:
  - Field-name labels (titleAr, titleEn, meetingDate, dailyNumber,
    startTime, endTime, location, meetingUrl, platform, status,
    isHighlighted, notes, assignedTo, taskType, dueAt, reviewDecision,
    reviewNotes) now resolve via executiveMeetings.audit.field.* with a
    raw-key fallback so unknown future fields don't crash the row.
  - Enum values render via existing label namespaces:
    `platform` → executiveMeetings.platform.*, `status` →
    executiveMeetings.status.*, `isHighlighted` → executiveMeetings.common.yes/no.
  - Both the diff view and the single-side created/removed view use the
    same labelForKey/fmtValue helpers.
- Added matching audit.field.* keys to ar.json and en.json with sensible
  Arabic and English labels.
- No test changes needed: no existing test asserts on the old column
  count, the "Actor"/"المنفّذ" header, the entityId column, or raw
  English field names in Changes.
- tsc passes for @workspace/tx-os.
2026-05-03 13:58:26 +00:00
riyadhafraa 3d2d80c250 Task #348: Arabic-friendly Executive Meetings audit log
- Reorder audit table columns to RTL order: المستخدم، الوقت، الإجراء،
  النوع، التغييرات. Drop the entityId ("المعرف") column entirely. Update
  loading/empty colSpan from 6 to 5 to match the new column count.
- Rename audit.col.actor copy: "المنفّذ" → "المستخدم" (ar.json) and
  "Actor" → "User" (en.json). Underlying data field name unchanged.
- Localize the Changes cell in AuditDiffSummary:
  - Field-name labels (titleAr, titleEn, meetingDate, dailyNumber,
    startTime, endTime, location, meetingUrl, platform, status,
    isHighlighted, notes, assignedTo, taskType, dueAt, reviewDecision,
    reviewNotes) now resolve via executiveMeetings.audit.field.* with a
    raw-key fallback so unknown future fields don't crash the row.
  - Enum values render via existing label namespaces:
    `platform` → executiveMeetings.platform.*, `status` →
    executiveMeetings.status.*, `isHighlighted` → executiveMeetings.common.yes/no.
  - Both the diff view and the single-side created/removed view use the
    same labelForKey/fmtValue helpers.
- Added matching audit.field.* keys to ar.json and en.json with sensible
  Arabic and English labels.
- No test changes needed: no existing test asserts on the old column
  count, the "Actor"/"المنفّذ" header, the entityId column, or raw
  English field names in Changes.
- tsc passes for @workspace/tx-os.
2026-05-03 13:57:22 +00:00
riyadhafraa 81f4d7cb55 Task #347: EM edit dialog — strip HTML in inputs + reorder/rename buttons
- safe-html.ts: add htmlToPlainText (DOMParser, br/block→\n, decode, collapse)
  and wrapAsParagraph (HTML-escape + <p>…</p>) helpers.
- executive-meetings.tsx:
  - openEdit() runs htmlToPlainText on titleAr, titleEn, and each attendee.name
    so the dialog's plain <Input> shows clean text instead of raw markup.
  - save() wraps titleAr, titleEn, and each attendee.name with wrapAsParagraph
    so the on-disk format stays byte-compatible with what the inline Tiptap
    EditableCell produces. Empty-title validation still fires off the plain
    string before wrapping.
  - Swapped MeetingFormDialog buttons: addSubheading now renders before the
    attendee-add button.
- ar.json: executiveMeetings.manage.attendees.add "إضافة حاضر" → "إضافة عضو".
- en.json: same key "Add attendee" → "Add member".
- Inline schedule cell chips (executiveMeetings.schedule.*) untouched.
2026-05-03 12:58:11 +00:00
riyadhafraa e695f5b4cb Improve attendee management by refining text handling and button labels
Introduce new utility functions for HTML to plain text conversion and paragraph wrapping in `safe-html.ts`. Update locale files (`ar.json`, `en.json`) to change "attendee" to "member" in the add button label. Modify `executive-meetings.tsx` to swap the functionality and labels of the "Add attendee" and "Add subheading" buttons.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: edd35722-74e8-4f8d-a3cc-bac6c5dccc51
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/Ow4s0aa
Replit-Helium-Checkpoint-Created: true
2026-05-03 12:54:50 +00:00
riyadhafraa 41cf24533e EM upcoming alert: Arabic copy tweaks (#346)
Two small Arabic-only string changes in the Upcoming Meeting alert
popup, requested off a screenshot:

1. Header title: "اجتماع يبدأ قريبًا" → "الاجتماع التالي".
2. Countdown line: replaced "خلال" with "بعد" so the alert reads
   "يبدأ بعد 5 دقائق" instead of "يبدأ خلال 5 دقائق". Applied to all
   plural forms (one/two/few/many/other). minutesAway_zero ("يبدأ
   الآن") and the separate startsNow key were left unchanged.

Files:
- artifacts/tx-os/src/locales/ar.json
  - executiveMeetings.alert.title
  - executiveMeetings.alert.minutesAway_{one,two,few,many,other}

Out of scope (untouched on purpose):
- en.json — user only asked about the Arabic copy.
- upcoming-meeting-alert.tsx — no logic/layout change needed; it
  already reads these keys via i18next.
- All other behavior (blink, drag, buttons, etc.) carried over from
  #344.

Verification:
- ar.json still valid JSON; key set unchanged.
- Pre-existing red workflows (api-server, combined `test`) are
  unrelated upstream failures and not caused by this change.
2026-05-03 12:49:15 +00:00
riyadhafraa de9b5954ce EM Notifications tab: remove header + intro + per-user prefs card (#345)
The user pointed out that the "بريد إلكتروني" channel they could see in
the per-user "تفضيلات التنبيهات الخاصة بي" card is not part of the
product (no email delivery wired up), so exposing the toggle was
misleading. They sent a screenshot and asked to delete everything in
it: the page heading "التنبيهات", the intro paragraph, and the entire
prefs card (event row + in-app/email switches + Save / Reset / Restore
defaults buttons).

Changes:
- artifacts/tx-os/src/pages/executive-meetings.tsx
  - Removed the <h2> heading, the <p> intro, and the
    <NotificationPrefsCard /> render call from the notifications page
    component. The audit-log table below stays untouched.
  - Deleted the NotificationPrefsCard component definition and the
    NotificationPref / NotificationPrefsResponse types (only consumers
    were inside the deleted card).
- artifacts/tx-os/src/locales/{ar,en}.json
  - Removed executiveMeetings.notificationsPage.heading,
    executiveMeetings.notificationsPage.intro, and the entire
    executiveMeetings.notificationsPage.prefs.* subtree.
  - Kept notificationsPage.col.*, type.*, status.*, empty — still used
    by the audit-log table.

Out of scope (intentionally not touched):
- Backend /api/executive-meetings/notification-prefs routes and the
  email channel in fan-out helpers. The backend defaults are left
  intact so notification fan-out keeps working; only the UI surface
  is removed.
- Tests: ripgrep showed no references to the removed test-ids
  (em-notification-prefs, em-pref-inapp-*, em-pref-email-*,
  em-pref-save, em-pref-reset, em-pref-restore-defaults) in the
  Playwright/unit suites, so nothing to update.

Verification:
- pnpm -C artifacts/tx-os exec tsc --noEmit clean.
- Switch import retained: still used by row-color popover and another
  call site in the same file.
- The api-server workflow and combined `test` workflow are red with
  pre-existing upstream failures unrelated to this UI-only change.
2026-05-03 12:43:02 +00:00
riyadhafraa ff2304c4c2 tx-os(#344): polish UpcomingMeetingAlert per user feedback
Four UI changes requested by an Arabic-speaking user on the floating
"اجتماع يبدأ قريباً" popup:

1. Time window LTR — wrap the header `alert-time-window` span in
   `dir="ltr"` so the start time always appears on the left and the
   end on the right, even inside the Arabic RTL UI. Prevents the
   "م 3:20 – م 3:10" reversal seen in the user's screenshot.
2. Drop the duplicate "Time" row inside DetailsPanel — the same
   start/end window is already shown in the header strip. Removed
   the `if (timeWindow) rows.push({ key: "time", ... })` block; the
   `timeWindow` local was deleted along with it. Header is now the
   single source of truth.
3. Remove the labelled "تجاهل التنبيه" footer button. The X close
   button in the header still calls `handleDismiss`, so users can
   still dismiss; the action bar is now just Done / Postpone /
   Details. `handleDismiss`, `dismissToast`, and the i18n keys are
   left intact for the X button.
4. Make the "يبدأ خلال N دقائق" / "يبدأ الآن" countdown blink red.
   Added a dedicated `@keyframes em-blink` (1s, opacity 1 → 0.15 →
   1) and `.em-blink` class in `src/index.css`, with a
   `prefers-reduced-motion` override that pins it at full opacity.
   Countdown span uses `text-red-600 em-blink font-semibold`. The
   previously-used `accentText` local had no remaining consumers,
   so it was removed (replaced with an explanatory comment).

Files:
- artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx
- artifacts/tx-os/src/index.css

Verification: `pnpm -C artifacts/tx-os exec tsc --noEmit` clean.
Architect review APPROVED (PASS). No tests referenced the removed
`alert-dismiss` / `alert-details-time` testids.

Pre-existing failures (unrelated): api-server workflow and the
combined `test` workflow continue to fail with the same upstream
issues observed in #342/#343 trajectory; this change does not
touch the API server or e2e test infrastructure.
2026-05-03 12:26:38 +00:00
riyadhafraa db6b835f75 feat(executive-meetings): replace native window.confirm with in-app AlertDialog (#342)
Native window.confirm exposes the Repl hostname and ignores the app's
RTL/branding. The user (Arabic-speaking) flagged it as visually jarring
inside Executive Meetings.

Changes (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Add a small ConfirmProvider + useConfirm() backed by the existing
  shadcn AlertDialog (`@/components/ui/alert-dialog`). The hook returns
  Promise<boolean> so call sites stay one-liners:
    if (!(await confirm({ message, destructive: true }))) return;
- Single-flight guard: if a second confirm() arrives while the first is
  still pending, the previous Promise resolves to false (no orphaned
  resolvers).
- Mount the provider once around the page (split the default export
  into a thin wrapper + ExecutiveMeetingsPageInner so the inner tree
  can call useConfirm()).
- Centered title + description, destructive-styled confirm button,
  mobile-safe sizing (max-w-md, w-[calc(100%-2rem)], max-h-[90vh],
  overflow-y-auto). Explicit `dir` on AlertDialogContent so the
  portal-rendered dialog always inherits the page's RTL/LTR.
- Footer renders Action first then Cancel: in LTR Confirm sits on the
  left and Cancel on the right; RTL flips automatically (Confirm right,
  Cancel left). Mobile stacks Cancel on top, destructive at the bottom.
- Replace all 5 window.confirm sites:
  1. Schedule single-row delete (deleteMeeting)
  2. Schedule bulk delete
  3. Manage bulk delete
  4. Manage single delete (confirmDelete)
  5. MeetingFormDialog "remove all attendees" (function turned async)
- Add `confirm` to the relevant useCallback deps (deleteMeeting,
  bulk delete) and inject useConfirm() inside ScheduleSection,
  ManageSection, MeetingFormDialog (all rendered inside the provider).
- New i18n keys: executiveMeetings.common.confirm and .confirmTitle in
  both ar.json and en.json (used as default title / non-destructive
  confirm label).

`pnpm -C artifacts/tx-os exec tsc --noEmit` clean. Pre-existing
api-server / test workflow failures are unrelated to this UI change
(also failing before the task started).
2026-05-03 12:15:58 +00:00
riyadhafraa 7f5b662a28 feat(executive-meetings): replace native window.confirm with in-app AlertDialog (#342)
Native window.confirm exposes the Repl hostname and ignores the app's
RTL/branding. The user (Arabic-speaking) flagged it as visually jarring
inside Executive Meetings.

Changes (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Add a small ConfirmProvider + useConfirm() backed by the existing
  shadcn AlertDialog (`@/components/ui/alert-dialog`). The hook returns
  Promise<boolean> so call sites stay one-liners:
    if (!(await confirm({ message, destructive: true }))) return;
- Mount the provider once around the page (split the default export
  into a thin wrapper + ExecutiveMeetingsPageInner so the inner tree
  can call useConfirm()).
- Centered title + description, destructive-styled confirm button,
  mobile-safe sizing (max-w-md, w-[calc(100%-2rem)], max-h-[90vh],
  overflow-y-auto), centered footer that stacks on small screens via
  the existing AlertDialogFooter classes.
- Replace all 5 window.confirm sites:
  1. Schedule single-row delete (deleteMeeting)
  2. Schedule bulk delete
  3. Manage bulk delete
  4. Manage single delete (confirmDelete)
  5. MeetingFormDialog "remove all attendees" (function turned async)
- Add `confirm` to the relevant useCallback deps (deleteMeeting,
  bulk delete) and inject useConfirm() inside ScheduleSection,
  ManageSection, MeetingFormDialog (all rendered inside the provider).

Reuses existing translation keys (deleteRowConfirm, bulkDeleteConfirm,
manage.deleteConfirm, attendees.removeAllConfirm). Title/labels fall
back via tWithFallback to Arabic strings if the i18n keys are missing.

Verified `pnpm -C artifacts/tx-os exec tsc --noEmit` is clean.
Pre-existing api-server / test workflow failures are unrelated to this
UI change (also failing before the task started).
2026-05-03 12:14:03 +00:00
riyadhafraa 2bd100e7b1 Task #340: center the Time and Actions columns in the Manage table
User feedback (RTL screenshot): the "إجراءات" header and its three
icon buttons were stuck against the far edge of the column, away
from the new vertical divider added in #337; the "الوقت" header
also sat against the start edge while the short "1:55 — 2:05"
value floated separately, so they didn't visually line up.

- Switches both the Time and Actions header cells from
  `text-start` / `text-end` to `text-center`.
- Adds `text-center` to the Time data cell so the
  fixed-width "HH:MM — HH:MM" string sits centered under its
  header.
- Switches the Actions data cell from `text-end` to
  `text-center`; the inline-flex button group (edit /
  duplicate / delete) inherits the centering.
- Loading and "no meetings" rows already render the message
  via `colSpan={4}` and a separate empty Actions cell; no
  text alignment change is needed there for visual parity.

Other columns (select-checkbox, #, Meeting) keep their
existing `text-start` so long titles stay flush with their
header label. The vertical divider (#337) and column widths
(w-28 / w-32) remain untouched. No change to data flow,
testids, or other tabs.

`pnpm -C artifacts/tx-os exec tsc --noEmit` clean. Pre-existing
api-server / test workflow failures are unrelated.
2026-05-03 11:58:29 +00:00
riyadhafraa 08a6b7e0d9 Task #336: hide raw "none" platform token under meeting title (Manage tab)
User feedback: the small grey subtitle under each meeting title in
the Manage tab table read e.g. "4 · none" — the raw `platform`
enum value leaked an English token next to Arabic content for
every meeting that has no virtual platform.

- Drops the ` · {m.platform}` segment from the subtitle in
  `ManageSection`'s row rendering inside
  `artifacts/tx-os/src/pages/executive-meetings.tsx`. The
  attendee count (filtered on `kind === "person"`) stays
  exactly as before, with the same `text-xs text-gray-500`
  styling so layout is unchanged.
- Adds a short comment explaining why so future readers don't
  re-introduce the platform token here. The platform is still
  shown elsewhere when meaningful (the "Virtual attendance —
  {platformLabel}" group header in AttendeeFlow at L4304/L4402,
  and the platform `<Select>` in the edit dialog) — those were
  intentionally not touched.
- No change to data model, save/load logic, or testids.

`pnpm -C artifacts/tx-os exec tsc --noEmit` clean. Pre-existing
api-server / test workflow failures are unrelated to this UI
change.
2026-05-03 11:52:13 +00:00
riyadhafraa 020d58d97b Task #337: divider between Time and Actions columns (Manage tab)
User feedback: in the Manage tab table the action icons looked
glued to the time value because there was no visible boundary
between the Time column and the Actions column.

- Adds `border-s border-gray-200` to both the Actions header
  cell (`<th>`) and the per-row Actions data cell (`<td>`) in
  the Manage tab table inside `executive-meetings.tsx`.
- Uses the logical `border-s` (inline-start) so the divider
  lands on the correct edge of the Actions column in both LTR
  and RTL — visually it always sits between Time and Actions.
- Color matches the existing table border (`gray-200`) and the
  border extends the full row height, in line with the
  surrounding row separators.
- Loading / empty states use a `colSpan={5}` row so the divider
  doesn't show there, which is the desired behavior (no
  inconsistent half-divider over centered text).
- No change to other columns, no change to Schedule/Audit/
  Notifications tables, and no change to colors/widths/testids.

`pnpm -C artifacts/tx-os exec tsc --noEmit` clean. Pre-existing
api-server / test workflow failures are unrelated to this UI
change.
2026-05-03 11:50:50 +00:00
riyadhafraa 7ae8701d98 Task #337: divider between Time and Actions columns (Manage tab)
User feedback: in the Manage tab table the action icons looked
glued to the time value because there was no visible boundary
between the Time column and the Actions column.

- Adds `border-s border-gray-200` to both the Actions header
  cell (`<th>`) and the per-row Actions data cell (`<td>`) in
  the Manage tab table inside `executive-meetings.tsx`.
- Uses the logical `border-s` (inline-start) so the divider
  lands on the correct edge of the Actions column in both LTR
  and RTL — visually it always sits between Time and Actions.
- Color matches the existing table border (`gray-200`) and the
  border extends the full row height, in line with the
  surrounding row separators.
- Loading / empty states use a `colSpan={5}` row so the divider
  doesn't show there, which is the desired behavior (no
  inconsistent half-divider over centered text).
- No change to other columns, no change to Schedule/Audit/
  Notifications tables, and no change to colors/widths/testids.

`pnpm -C artifacts/tx-os exec tsc --noEmit` clean. Pre-existing
api-server / test workflow failures are unrelated to this UI
change.
2026-05-03 11:50:14 +00:00
riyadhafraa 529f18a1a2 Task #334: sticky bulk-action toolbar on Manage tab
User feedback: "سو فريز هنا لأني نزلت تحت وما شفتها" — the
Manage tab's bulk-action toolbar (added in #332) was not
visible after scrolling down through the meetings list.

- Adds a `headingRef` + `ResizeObserver` effect in
  `ManageSection` that publishes the live height of
  `em-manage-heading-bar` as a `--em-manage-heading-h` CSS
  var on the `[data-em-root]` element. Mirrors the Schedule
  tab's existing pattern at L2155 for `--em-heading-h`.
  Distinct var name avoids collision when switching tabs;
  cleared on unmount so the Schedule tab doesn't inherit a
  stale offset.
- Updates `em-manage-bulk-toolbar` to `position: sticky`
  with `top: calc(var(--em-header-h) + var(--em-manage-heading-h))`,
  matching horizontal bleed (`-mx-3 sm:-mx-6 px-3 sm:px-6`)
  and `#f4f6fb` page-bg of the heading bar so the two visually
  stack as a single header unit. Drops the rounded card style,
  uses a bottom-border + drop-shadow for the seam.
- Adds `print:hidden` for parity with the Schedule
  counterpart so the toolbar doesn't leak into PDF exports.

`pnpm -C artifacts/tx-os exec tsc --noEmit` clean. Pre-existing
api-server / test workflow failures are unrelated to this
change. No behavior change to the Schedule tab.
2026-05-03 11:36:20 +00:00
riyadhafraa 226b8e209c Task #334: sticky bulk-action toolbar on Manage tab
User feedback: "سو فريز هنا لأني نزلت تحت وما شفتها" — the
Manage tab's bulk-action toolbar (added in #332) was not
visible after scrolling down through the meetings list.

- Adds a `headingRef` + `ResizeObserver` effect in
  `ManageSection` that publishes the live height of
  `em-manage-heading-bar` as a `--em-manage-heading-h` CSS
  var on the `[data-em-root]` element. Mirrors the Schedule
  tab's existing pattern at L2155 for `--em-heading-h`.
  Distinct var name avoids collision when switching tabs;
  cleared on unmount so the Schedule tab doesn't inherit a
  stale offset.
- Updates `em-manage-bulk-toolbar` to `position: sticky`
  with `top: calc(var(--em-header-h) + var(--em-manage-heading-h))`,
  matching horizontal bleed (`-mx-3 sm:-mx-6 px-3 sm:px-6`)
  and `#f4f6fb` page-bg of the heading bar so the two visually
  stack as a single header unit. Drops the rounded card style,
  uses a bottom-border + drop-shadow for the seam.
- Adds `print:hidden` for parity with the Schedule
  counterpart so the toolbar doesn't leak into PDF exports.

`pnpm -C artifacts/tx-os exec tsc --noEmit` clean. Pre-existing
api-server / test workflow failures are unrelated to this
change. No behavior change to the Schedule tab.
2026-05-03 11:35:13 +00:00
riyadhafraa fe17bfcdd5 Task #332: port search + bulk delete/duplicate to Manage tab
Brings the Schedule tab's in-day search (#329) and bulk
delete/duplicate-to-date (#330) into the Manage tab of
executive-meetings.tsx (`ManageSection`).

- Adds `searchQuery` state + search input in the manage heading
  bar; reuses module-level `normalizeForSearch` /
  `meetingMatchesSearch` helpers and the same "reset on date
  change" semantics as the Schedule tab.
- Adds `selectedMeetingIds` Set + tri-state "select all
  visible" header checkbox + per-row checkbox column. Action
  handlers always intersect the selection set with the visible
  (filtered) rows, matching Schedule's safety pattern.
- Adds bulk-delete (with confirm + aggregated toast via
  `Promise.allSettled`) and bulk-duplicate-to-date (separate
  Dialog mirroring the Schedule one).
- Bumps loading/empty `colSpan` from 4 → 5 for the new
  checkbox column.
- Reuses existing `executiveMeetings.schedule.*` locale keys
  (already present in both ar.json + en.json from #329/#330);
  no new strings were needed.
- New testids: `em-manage-search`, `em-manage-search-clear`,
  `em-manage-select-all`, `em-manage-row-select-{id}`,
  `em-manage-bulk-toolbar`, `em-manage-bulk-selection-count`,
  `em-manage-bulk-clear-selection`, `em-manage-bulk-duplicate`,
  `em-manage-bulk-duplicate-{date,confirm}`,
  `em-manage-bulk-delete`.

Out of scope per plan (deferred): undo for bulk-delete on
Manage, cross-date "select all on this date".

`pnpm -C artifacts/tx-os exec tsc --noEmit` clean. Architect
review passed. Pre-existing api-server / test workflow
failures are unrelated to this change.
2026-05-03 11:30:44 +00:00
riyadhafraa f479fa5498 Remove unnecessary confirmation message after acknowledging alerts
Remove the `doneToast` locale keys and the toast notification call in `upcoming-meeting-alert.tsx`.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 320c7da3-19c9-48af-ab28-f9af7aad21dd
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/vKbTeqB
Replit-Helium-Checkpoint-Created: true
2026-05-03 11:13:41 +00:00
riyadhafraa 104ae5526d Add functionality to duplicate multiple selected meetings to a future date
Adds a bulk duplicate option to the toolbar, including a dialog for selecting the target date, and implements the API calls and state management for this feature.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 13d92594-1814-45b7-83b2-862ed06fe2dc
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/vKbTeqB
Replit-Helium-Checkpoint-Created: true
2026-05-03 11:03:25 +00:00
riyadhafraa 7d5dd668bc Update toolbar to show only visible selected items
Memoizes and displays the count of currently visible selected meetings in the bulk action toolbar, ensuring actions align with displayed data.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 7f5687a2-4acd-4514-bad3-46babbeadb4a
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/vKbTeqB
Replit-Helium-Checkpoint-Created: true
2026-05-03 11:00:16 +00:00
riyadhafraa 317d597113 #329 Add a meeting search box to the schedule toolbar
Lets users find a meeting in the day's schedule by typing part of its
title, an attendee name, or other text — instead of scrolling.

artifacts/tx-os/src/pages/executive-meetings.tsx
- Added two top-level helpers near translateSaveError:
  - normalizeForSearch(input): strips HTML tags + entities, drops
    Arabic diacritics + tatweel, folds common letter variants
    (آأإٱ→ا, ى→ي, ة→ه, ؤ→و, ئ→ي), lowercases.
  - meetingMatchesSearch(meeting, needle): tests title (AR/EN),
    attendee names + titles, location, notes, and merge text via
    normalized substring containment.
- ScheduleSection: added searchQuery state, useEffect that resets it
  whenever the active `date` changes, normalizedSearch memo, and a
  filteredMeetings memo derived from orderedMeetings.
- Switched the row render path to filteredMeetings: SortableContext
  items, the .map render, and the select-all overlay's "anything to
  select" check now read from the filtered list. The
  setAllMeetingsSelected and allSelectedState helpers also operate
  on filteredMeetings so the header checkbox + select-all reflect
  what's actually visible.
- Added a new "no matches" empty-state row that appears only when
  the day has meetings but the active search filters them all out
  (the original "no meetings today" row is preserved for empty days).
- New search Input + clear-X button placed in the schedule toolbar,
  next to the existing date picker. Hidden in print so PDFs export
  the full schedule.
- MeetingRow gained an optional `displayNumber` prop. The "#" cell
  now renders displayNumber when provided, falling back to the
  server's dailyNumber. The schedule passes idx+1, so visible rows
  always show 1, 2, 3… without gaps when the list is filtered.

artifacts/tx-os/src/locales/ar.json + en.json
- New keys under executiveMeetings.schedule: searchPlaceholder,
  searchAria, searchClear, searchNoMatches.

- Bulk delete now intersects the selection with filteredMeetings at
  action time. The selection set is intentionally NOT auto-pruned
  when the search narrows (so clearing the search restores the
  user's earlier picks), but destructive actions only ever target
  rows the user can currently see.
- Print/PDF export ignores the active search. An `isPrinting` flag
  driven by beforeprint/afterprint + a `print` media-query listener
  swaps the table to the unfiltered orderedMeetings while printing,
  so generated PDFs always contain the full day's schedule even
  when the user has typed into the search box.

Out of scope: cross-day search, prev/next-day chevron buttons,
search history, fuzzy matching.

Verified: `tsc --noEmit` clean.
2026-05-03 10:58:15 +00:00
riyadhafraa 0fda08169a #329 Add a meeting search box to the schedule toolbar
Lets users find a meeting in the day's schedule by typing part of its
title, an attendee name, or other text — instead of scrolling.

artifacts/tx-os/src/pages/executive-meetings.tsx
- Added two top-level helpers near translateSaveError:
  - normalizeForSearch(input): strips HTML tags + entities, drops
    Arabic diacritics + tatweel, folds common letter variants
    (آأإٱ→ا, ى→ي, ة→ه, ؤ→و, ئ→ي), lowercases.
  - meetingMatchesSearch(meeting, needle): tests title (AR/EN),
    attendee names + titles, location, notes, and merge text via
    normalized substring containment.
- ScheduleSection: added searchQuery state, useEffect that resets it
  whenever the active `date` changes, normalizedSearch memo, and a
  filteredMeetings memo derived from orderedMeetings.
- Switched the row render path to filteredMeetings: SortableContext
  items, the .map render, and the select-all overlay's "anything to
  select" check now read from the filtered list. The
  setAllMeetingsSelected and allSelectedState helpers also operate
  on filteredMeetings so the header checkbox + select-all reflect
  what's actually visible.
- Added a new "no matches" empty-state row that appears only when
  the day has meetings but the active search filters them all out
  (the original "no meetings today" row is preserved for empty days).
- New search Input + clear-X button placed in the schedule toolbar,
  next to the existing date picker. Hidden in print so PDFs export
  the full schedule.
- MeetingRow gained an optional `displayNumber` prop. The "#" cell
  now renders displayNumber when provided, falling back to the
  server's dailyNumber. The schedule passes idx+1, so visible rows
  always show 1, 2, 3… without gaps when the list is filtered.

artifacts/tx-os/src/locales/ar.json + en.json
- New keys under executiveMeetings.schedule: searchPlaceholder,
  searchAria, searchClear, searchNoMatches.

- Bulk delete now intersects the selection with filteredMeetings at
  action time. The selection set is intentionally NOT auto-pruned
  when the search narrows (so clearing the search restores the
  user's earlier picks), but destructive actions only ever target
  rows the user can currently see.

Out of scope: cross-day search, prev/next-day chevron buttons,
search history, fuzzy matching.

Verified: `tsc --noEmit` clean.
2026-05-03 10:56:54 +00:00
riyadhafraa 41c0c30cb8 #326 Show meeting save errors in Arabic, centered at the bottom
Two small UX fixes for the Add/Edit Meeting save toast.

artifacts/tx-os/src/pages/executive-meetings.tsx
- Added a `translateSaveError(rawMsg, t)` helper near the other
  top-level helpers (~line 425). It substring-matches the API's
  Zod-generated message "startTime must be <= endTime" and returns
  the translated copy from
  `executiveMeetings.manage.errors.endTimeBeforeStart`. Anything
  unknown falls through to the raw server message so unexpected
  errors aren't silently hidden.
- The save catch block now passes the message through this helper
  before showing the toast. Title still uses
  `errors.saveFailed`.

artifacts/tx-os/src/locales/ar.json + en.json
- New translation key `executiveMeetings.manage.errors.endTimeBeforeStart`:
  - ar: "وقت الانتهاء يجب أن يكون بعد وقت البدء"
  - en: "End time must be after start time"

artifacts/tx-os/src/components/ui/toast.tsx
- ToastViewport: replaced the mobile-top + desktop-bottom-right
  positioning with a single bottom-center placement on every
  viewport (`fixed bottom-0 left-1/2 -translate-x-1/2`). Width
  is still capped at `md:max-w-[420px]`. The flex direction is
  `flex-col` (no longer `flex-col-reverse`) since there is now
  only one slide-in direction.
- toastVariants: dropped `data-[state=open]:slide-in-from-top-full`
  so toasts slide in from the bottom on every breakpoint, matching
  the new viewport position.

Out of scope (untouched): API validation, other dialogs/pages,
preventive client-side validation for invalid time ranges.
2026-05-03 10:17:05 +00:00
riyadhafraa d2dda556d6 #324 Make the Add/Edit Meeting dialog fit on a phone screen
After #322 trimmed the dialog, the Save/Cancel footer slid below
the visible 90vh sheet on phones because the whole DialogContent
was a single overflow-y-auto block.

Changes in artifacts/tx-os/src/pages/executive-meetings.tsx:

MeetingFormDialog
- Rebuilt DialogContent as a flex column: shrink-0 header,
  flex-1 min-h-0 overflow-y-auto middle (the only scroller),
  shrink-0 footer. Save/Cancel now stay pinned at the bottom
  even after many attendees are added.
- Reduced mobile padding/gaps (`p-4 sm:p-6`,
  `gap-3 sm:gap-4`, grid `gap-2 sm:gap-3`). Desktop spacing
  unchanged.
- Switched the field grid from `grid-cols-1 sm:grid-cols-2`
  to a flat `grid-cols-2` so the two short fields (التاريخ,
  الرقم اليومي) share a row even on mobile. Title spans both
  columns; time pickers span both on mobile and one each on
  sm+ via the new mobileFull variant.
- Inner scroll wrapper uses negative margins + matching padding
  so scroll content can extend edge-to-edge without losing the
  dialog's gutter.

FormRow helper
- Broadened `full` from `sm:col-span-2` to `col-span-2`
  (works in both 1-col and 2-col grids; only callers using
  `full` are in this file).
- Added `mobileFull` prop → `col-span-2 sm:col-span-1` for
  the time pickers, so AM/PM controls don't get squeezed on
  phones.

Out of scope (untouched): validation, save logic, attendee
model, removed-field defaults from #322, duplicate dialog,
audit, schedule view, translations.
2026-05-03 10:09:29 +00:00
riyadhafraa 5cb1644300 Update meeting form layout and Arabic title label
Refactor executive meetings form layout in `executive-meetings.tsx` to use a responsive grid, ensuring the Arabic title spans full width and other fields stack appropriately on smaller screens. Update Arabic and English locale files (`ar.json`, `en.json`) to change the Arabic title field label from "العنوان (عربي)" to "العنوان".

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 57375b7d-57d9-4fe3-a745-00a2cad59df8
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/2VzZgdn
Replit-Helium-Checkpoint-Created: true
2026-05-03 10:00:45 +00:00
riyadhafraa debee0d600 #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.
2026-05-03 09:51:36 +00:00
riyadhafraa 60533b9ceb #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.
2026-05-03 09:50:53 +00:00
riyadhafraa 51ae2f5a07 Improve the manage meetings page with a sticky header and proper title formatting
Update executive-meetings.tsx to make the header sticky, render meeting titles as HTML, and remove the "Status" column.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: c5984587-5347-4b07-b8da-f95fa4bc07a5
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/fHzEBLA
Replit-Helium-Checkpoint-Created: true
2026-05-03 09:36:42 +00:00
riyadhafraa 53aee6e83d Center all executive meeting schedule column headers
Update alignment for all schedule column headers in executive meetings to be centered for both screen and print views.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 354f7894-5e86-42b0-b76f-98e89d72a578
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/fHzEBLA
Replit-Helium-Checkpoint-Created: true
2026-05-03 08:02:53 +00:00
riyadhafraa 4a9ca4bd4a tx-os #317: optimistic-cache saves for all inline editors
Extends the optimistic React Query cache pattern (proven in #316 for the
schedule time cell) to every other inline editor on executive-meetings.tsx,
fixing the "تأخير في الاستجابة وأحياناً ما يحفظ" regression where typed
edits felt sluggish or appeared to silently drop.

executive-meetings.tsx
  * New `applyMeetingPatch(meetingId, patch)` helper: snapshots the day's
    DayResponse, applies a per-meeting patch via setQueryData, and returns
    a rollback closure that re-sets the snapshot on PATCH failure.
  * `saveTitle`, `saveAttendeeName`, `saveMerge`, and `setRowColor` now
    write the optimistic value before awaiting the network call and
    rollback() in their catch branches.

editable-cell.tsx (3-layer blur hardening)
  1. saveEdit() failure now re-opens the editor with the user's typed
     draft preserved (mirrors #316 time-cell UX) so a server error never
     forces them to retype. The parent's optimistic cache rollback
     restores the read-only `value`; the editor itself still holds the
     draft, so flipping `editing` back on is enough.
  2. Outside-pointerdown now flushes saveEdit synchronously (capture
     phase, before the click reaches its target) instead of waiting on
     the 100 ms blur timer. Closes the gap where a fast click into
     another cell triggered a row remount before blur fired.
  3. Unmount cleanup flushes any pending blurTimerRef fire-and-forget,
     a final safety net for the case where a refetch tears down the
     row before any pointer event arrives.

editable-cell.tsx (re-entrancy guard)
  * `savingRef` short-circuits a second `saveEdit` call so the
    pointerdown flush + the 100 ms blur timer + Enter/Tab handlers
    can't race and issue duplicate PATCH/PUT requests for one edit.

tests/executive-meetings-keyboard-editing.spec.mjs (+5 tests)
  * Title repaint <300 ms with PATCH delayed 1500 ms.
  * Title PATCH 500 rolls back; editor re-opens with the typed draft
    intact; Escape cancels back to the original; DB unchanged.
  * Rapid click-switch from cell A to cell B commits BOTH PATCHes to
    the server (proves the synchronous outside-pointerdown flush).
  * Rapid Tab traversal from cell A to cell B commits BOTH edits
    EXACTLY ONCE each (proves the savingRef re-entrancy guard).
  * Attendee-name PUT delayed 1500 ms repaints in <300 ms (uses
    captured testid so the locator survives the rename).

All 5 new + 3 #316 time-editor tests green. Out-of-scope pre-existing
failures in the `test` workflow (notifications/postpone-race/row-color)
left untouched per plan.
2026-05-03 07:45:30 +00:00
riyadhafraa 44b54e1fa8 tx-os #317: optimistic-cache saves for all inline editors
Extends the optimistic React Query cache pattern (proven in #316 for the
schedule time cell) to every other inline editor on executive-meetings.tsx,
fixing the "تأخير في الاستجابة وأحياناً ما يحفظ" regression where typed
edits felt sluggish or appeared to silently drop.

executive-meetings.tsx
  * New `applyMeetingPatch(meetingId, patch)` helper: snapshots the day's
    DayResponse, applies a per-meeting patch via setQueryData, and returns
    a rollback closure that re-sets the snapshot on PATCH failure.
  * `saveTitle`, `saveAttendeeName`, `saveMerge`, and `setRowColor` now
    write the optimistic value before awaiting the network call and
    rollback() in their catch branches.

editable-cell.tsx (3-layer blur hardening)
  1. saveEdit() failure now re-opens the editor with the user's typed
     draft preserved (mirrors #316 time-cell UX) so a server error never
     forces them to retype. The parent's optimistic cache rollback
     restores the read-only `value`; the editor itself still holds the
     draft, so flipping `editing` back on is enough.
  2. Outside-pointerdown now flushes saveEdit synchronously (capture
     phase, before the click reaches its target) instead of waiting on
     the 100 ms blur timer. Closes the gap where a fast click into
     another cell triggered a row remount before blur fired.
  3. Unmount cleanup flushes any pending blurTimerRef fire-and-forget,
     a final safety net for the case where a refetch tears down the
     row before any pointer event arrives.

tests/executive-meetings-keyboard-editing.spec.mjs (+4 tests)
  * Title repaint <300 ms with PATCH delayed 1500 ms.
  * Title PATCH 500 rolls back; editor re-opens with the typed draft
    intact; Escape cancels back to the original; DB unchanged.
  * Rapid click-switch from cell A to cell B commits BOTH PATCHes to
    the server (proves the synchronous outside-pointerdown flush).
  * Attendee-name PUT delayed 1500 ms repaints in <300 ms (uses
    captured testid so the locator survives the rename).

All 4 new + 3 #316 time-editor tests green. Out-of-scope pre-existing
failures in the `test` workflow (notifications/postpone-race/row-color)
left untouched per plan.
2026-05-03 07:41:28 +00:00
riyadhafraa 85105faf16 tx-os #317: optimistic-cache saves for all inline editors
Extends the optimistic React Query cache pattern (proven in #316 for the
schedule time cell) to every other inline editor on executive-meetings.tsx,
fixing the "تأخير في الاستجابة وأحياناً ما يحفظ" regression where typed
edits felt sluggish or appeared to silently drop.

executive-meetings.tsx
  * New `applyMeetingPatch(meetingId, patch)` helper: snapshots the day's
    DayResponse, applies a per-meeting patch via setQueryData, and returns
    a rollback closure that re-sets the snapshot on PATCH failure.
  * `saveTitle`, `saveAttendeeName`, `saveMerge`, and `setRowColor` now
    write the optimistic value before awaiting the network call and
    rollback() in their catch branches. Re-throw behaviour preserved
    where present so EditableCell still resets its draft on failure.

editable-cell.tsx
  * Hardened blur-commit: the cell now stashes the latest `saveEdit`
    in a ref and an unmount cleanup flushes any pending blurTimerRef
    fire-and-forget. Without this, a row remount that lands during the
    100 ms blur window (e.g. Tab into the next cell triggering an
    optimistic refetch) tore down the editor before saveEdit could run
    and silently dropped the typed change. Optimistic cache writes in
    the parent make the fire-and-forget safe.

tests/executive-meetings-keyboard-editing.spec.mjs (+3 tests)
  * Title repaint <300 ms with PATCH delayed 1500 ms.
  * Title PATCH 500 rolls back to original; DB unchanged.
  * Attendee-name PUT delayed 1500 ms repaints in <300 ms (uses
    captured testid so the locator survives the rename).

Architect review: APPROVED, no concerns. All 3 new + 3 #316 time-editor
tests green. Out-of-scope pre-existing failures in the `test` workflow
(notifications/postpone-race/row-color) untouched per plan.
2026-05-03 07:31:28 +00:00
riyadhafraa f3154554ff Task #316: fix executive-meetings inline time editor snap-back + perceived save latency
Two bugs in the schedule time-cell editor:
1. SNAP-BACK: After saving, re-opening the editor briefly showed
   blank inputs because the React Query refetch ran behind the
   close-then-reopen sequence and the picker re-seeded from stale
   meeting props.
2. PERCEIVED LATENCY: The editor stayed open until the PATCH
   round-trip resolved, so a slow network made every save feel
   slow even though the data round-trip was the only blocker.

Fixes (artifacts/tx-os/src/pages/executive-meetings.tsx):
- saveTimes: optimistically write the new times into the
  ["/api/executive-meetings", date] query cache (DayResponse shape:
  { date, meetings: [...] }) so the read-only display repaints
  immediately. Snapshot previous payload and roll back on PATCH
  failure. Append :00 so the cached shape matches the GET refetch.
- TimeRangeCell.save(): close the editor BEFORE awaiting the PATCH;
  set savingRef while saving so the [editing, startSaved, endSaved]
  sync effect doesn't overwrite the optimistic draft on the next
  render. On failure, re-open with the user's draft intact.
- TimeRangeCell sync effect: bail when savingRef is true OR when
  the saved refs match lastSyncedRef, preventing stale prop values
  from clobbering the optimistic state mid-flight.

Tests added (artifacts/tx-os/tests/executive-meetings-keyboard-editing.spec.mjs):
- snap-back: save 21:30/22:45, immediately re-open, assert picker
  shows the saved values (not blank).
- perceived-latency: hold PATCH 1.5s; assert editor closes within
  800ms and the read-only display shows optimistic times.
- rollback: 500 the PATCH; assert editor re-opens with the user's
  draft intact, read-only display shows the original times after
  cancel, DB unchanged.

All 3 new tests pass; the existing time-editor tests continue to
pass (6/6 in the targeted regression sweep).

Plan: .local/tasks/task-316.md
2026-05-03 07:18:11 +00:00
riyadhafraa 45f3d88ac5 EM time picker: mobile/iPad-friendly layout + touch targets
Follow-up to task #315. The 3-control picker shipped with desktop-
sized inline controls (~22px tall) and a single-row layout that
pushed the end picker out of view on a 375px iPhone, leaving the
user with no obvious way to fill the second time field.

What changed
- src/components/time-picker-12h.tsx: bumped the "inline" size
  from h<22px text-[10px] to h-8 (32px) text-sm/text-xs across
  hour input, minute input, and AM/PM toggle so a finger tap on
  mobile/iPad lands cleanly. Added `gap-1.5` between the
  [hour:minute] group and the AM/PM toggle so a thumb-tap on the
  toggle doesn't accidentally land on the minute input. Added a
  visible focus ring color and lightened the placeholder so the
  empty fields read as "fill me in" rather than already-active.
- src/pages/executive-meetings.tsx (TimeRangeCell wrapper):
  switched from `inline-flex` (single-row, ellipsis on overflow)
  to `flex flex-wrap items-end gap-x-1.5 gap-y-1`. The end picker
  now drops onto a second line when the cell can't fit both
  side-by-side, which is the iPhone case. The "–" separator is
  hidden on narrow widths (`hidden sm:inline`) since wrapping
  makes it visually wrong.
- Bumped Start/End labels from 10px to 11px font-medium for
  better at-a-glance scanability of which time field is which —
  this was the user-reported clarity issue.
- Save/cancel icon buttons grew to 32x32 hit areas (was tiny
  p-0.5 around a 14px icon ≈ 16px clickable).
- tests/executive-meetings-keyboard-editing.spec.mjs: added a
  new test.describe block "Schedule mobile viewport (iPhone 375)"
  with `test.use({ viewport: { width: 375, height: 667 } })`. The
  test asserts: (a) all 8 picker sub-controls + save button are
  visible on iPhone, (b) each interactive control is at least
  28px tall (regression guard against shrinking touch targets),
  (c) a complete tap-driven hour+minute+AM/PM entry on both
  start and end persists 21:30–22:45 to the DB.

Validation
- Unit tests: 22/22 pass (no logic changes — only CSS + layout).
- E2E: Tab walks (desktop) + compact+PM toggle (desktop) +
  new iPhone-375 mobile test all pass.
- TypeScript: clean.
2026-05-03 06:22:54 +00:00
riyadhafraa 4cb8532bb6 Task #315: rebuild 12h picker as 3 controls (hour + minute + AM/PM)
The first ship of #315 used a single text input + AM/PM toggle. The
spec (lines 30-32) explicitly mandates THREE controls per field:
hour input (1-12), minute input (00-59), and AM/PM toggle. This
follow-up commit refactors TimePicker12h to that shape while
keeping every behaviour from the first ship intact.

What changed
- src/lib/time-12h.ts:
  - Added `splitCanonicalForPicker()` — seeds the 3-control picker's
    separate hour and minute inputs from a canonical "HH:mm" value.
  - Added `combineSplit(hour, minute, period)` — the new combine
    path. Joins "${h}:${m}" and runs the result through
    combineTextWithPeriod, preserving every disambiguation rule
    (1-12 requires toggle, 0/13-23 unambiguous, embedded marker
    overrides toggle).
  - The hour field accepts a power-user shortcut: any separator
    (":", ".", "-", " ") or AM/PM marker letter or 3-4 digit
    compact form in the hour field means "I typed the whole time
    here, ignore the minute field". Preserves the existing
    e2e tests that fill the entire time string into one input.
  - kept formatCanonicalAs12h + combineTextWithPeriod for the
    power-user shortcut path.
- src/components/time-picker-12h.tsx: rewrote as 3 controls
  (hour input, ":" separator, minute input, AM/PM radiogroup).
  Imperative {commit, focus, select} handle still focuses the
  hour input. dirtyRef preserved to avoid prop overwrite mid-edit.
  periodRef still mirrors state for synchronous commit() reads.
- src/pages/executive-meetings.tsx: added `minuteTestId` props on
  both inline editor pickers and both manage-form pickers
  (`em-time-{start,end}-minute-${id}`,
  `em-form-{start,end}Time-minute`). All other test IDs preserved.
- src/__tests__/time-12h.test.mjs: kept the 14 tests for the
  text+period path; added 8 new tests for combineSplit covering
  split-mode (1-12 ambiguous, 13-23 unambiguous, midnight, hour
  power-user shortcut, bare-hour invalid, etc) — 22 tests pass.
- tests/executive-meetings-keyboard-editing.spec.mjs: updated
  the Tab walks test to walk 8 stops (start hour → start minute
  → start AM → start PM → end hour → end minute → end AM →
  end PM) and assert both typed values survive the focus
  changes. The TYPING_SHAPES helper still works unchanged because
  the hour field accepts full time strings via the power-user
  shortcut.
- public/opengraph.jpg: reverted to HEAD~1 (was inadvertently
  swept into the prior commit by the auto-commit; not part of
  this task).

Validation
- Unit tests: 22/22 pass.
- E2E: Tab walks + canonical 24h + no-leading-zero + 12h-with-PM
  + compact+PM-toggle all pass against the new 3-control UI.
- TypeScript: clean.
2026-05-02 11:31:37 +00:00
riyadhafraa e63a7f8193 Task #315: 12h time picker with explicit AM/PM (executive meetings)
Replaces native <input type="time"> in the executive-meetings editor
with a TimePicker12h that always requires an explicit AM/PM choice
for any 1–12 hour, fixing the "1:15 silently saved as 01:15 (AM)
when user meant 13:15 (PM)" bug. Display stays compact 12h with no
AM/PM marker (per #292); wire format remains canonical HH:mm 24h.

What ships
- src/lib/time-12h.ts: periodFromCanonical, formatCanonicalAs12h,
  combineTextWithPeriod (returns "ambiguous" for any unmarked 1–12
  hour without a toggle pick; accepts hours 0 and 13–23 directly
  because they are unambiguous 24h shapes; typed marker always
  wins over toggle).
- src/components/time-picker-12h.tsx: forwardRef component with
  imperative {commit, focus, select} handle, dirtyRef to avoid
  prop overwrite mid-edit, dir="ltr" for stable RTL layout, and
  Enter-on-toggle = "set AND save".
- Wired into TimeRangeCell inline editor (commit-based save handles
  invalid/ambiguous/TimeOrderError as toasts and refocuses).
- Wired into MeetingFormDialog with picker refs + handleSaveClick
  validation: Save button now calls commit() on both pickers,
  blocks on invalid/ambiguous (toast + focus offending picker),
  and passes canonical values straight to the parent's save handler
  so a stale React state batch from onChange-suppression cannot
  silently persist the previous value.

Tests
- 12 unit tests in src/__tests__/time-12h.test.mjs (incl. new
  "hour 0 is unambiguous, accepted without a toggle").
- 17 e2e tests in tests/executive-meetings-keyboard-editing.spec.mjs
  (incl. "compact + PM toggle" 0115→13:15 and "Tab walks start
  input → start AM/PM toggle → end input → end AM/PM toggle").
- Existing manage-create e2e suite still green.

Locale keys (en + ar)
- executiveMeetings.timeEditor.{am,pm,periodGroupStart,periodGroupEnd}
- executiveMeetings.schedule.timeAmbiguousError

Deviations from plan
- Architect review #1 caught two issues that were both fixed:
  (1) combineTextWithPeriod treated hour 0 as ambiguous; now
      accepts 00:xx unambiguously.
  (2) MeetingFormDialog initially used pickers via value/onChange
      only, which left a silent-fallback hole because onChange
      suppresses ambiguous drafts. Added imperative refs +
      handleSaveClick validation; onSave signature changed to
      (committedStart, committedEnd) so the parent's save() can
      use the freshly-committed values directly.
- Reverted accidental vite.config.ts typo (@replit/... was missing
  the leading @).
2026-05-02 11:18:52 +00:00
riyadhafraa 4c8d2fff6c Task #312: auto-sort executive-meetings schedule by start_time
User report (AR): "اريدها ترتيبها تلقائيا" — after a non-drag write
(typing 13:00 into a row sitting above a 12:00 row, creating a meeting
with an explicit start time, etc.) the schedule's daily_number stayed
frozen at the row's prior position, so the visible list was no longer
chronological. Fix: extend the existing `renumberDayByStartTime` helper
to every write path that touches start_time / meeting_date / status /
daily_number, so the day is always renumbered 1..N by start_time
(NULLS LAST, cancelled rows at the tail).

Server (artifacts/api-server/src/routes/executive-meetings.ts):
  - POST  /executive-meetings           — renumber after insert.
  - PATCH /executive-meetings/:id       — renumber when an order- or
    visibility-affecting field (startTime/endTime/meetingDate/status/
    dailyNumber) is in the payload. Cross-day moves renumber BOTH the
    source and destination day. Pre-allocates a fresh dailyNumber on
    the destination via nextDailyNumber(tx, newDate) before the UPDATE
    so the (meeting_date, daily_number) unique index does not collide
    when the row arrives on a day with existing meetings.
  - DELETE /executive-meetings/:id      — renumber so the day's `#`
    sequence stays gap-free.
  - POST  /executive-meetings/:id/duplicate — renumber the target day.
Existing postpone-minutes / reschedule / cancel paths already called
renumber and were left alone. Reorder POST is also untouched (its
slot-swap is the explicit user-driven order, not auto-sort).

Audit-log surfacing of the auto-sort side effect (per validation):
  - `renumberDayByStartTime` now returns `{ date, orderShifted, before,
    after }` where `before`/`after` are the visible (non-cancelled)
    row IDs in `daily_number` order, captured by a cheap SELECT inside
    the same transaction.
  - PATCH was restructured so renumber runs BEFORE `logAudit`, then
    the row's post-renumber `daily_number` is read back and the audit's
    `newValue` is enriched with:
      • `dailyNumber` overridden to the post-sort position (so the
        audit shows where the row landed, not the pre-sort draft);
      • `orderShifted: true` and a `dayOrder: [{ date, before, after }]`
        array (one entry per affected day, including both source and
        destination on cross-day moves) when the visible order actually
        changed.
  - When auto-sort runs but does not change the visible order (e.g. the
    row was already in the correct slot), `orderShifted` / `dayOrder`
    are omitted so the audit UI does not falsely flag a reorder.

Tests (artifacts/api-server/tests/executive-meetings.test.mjs, +11):
  - assertDayChronological() helper asserts 1..N + non-decreasing
    start_time + no duplicate dailyNumbers.
  - PATCH startTime later → row demoted (the user's exact scenario).
  - PATCH startTime earlier → row promoted to the top.
  - POST create with middle startTime slots between existing rows.
  - PATCH startTime=null sinks to the tail of visible rows.
  - Cancel pushes out / uncancel re-slots chronologically.
  - Cross-day PATCH meetingDate renumbers BOTH days.
  - DELETE leaves no `#` gap.
  - POST /duplicate slots clone at chronological position.
  - PATCH that reorders the day records orderShifted + post-sort
    dailyNumber + dayOrder before/after arrays in the audit row.
  - Title-only PATCH leaves orderShifted/dayOrder absent from the audit.
All 57 tests in executive-meetings.test.mjs pass.

Architect review (advisory, scope-bounded):
  - Concurrency: PATCH/DELETE read `existing` outside the transaction.
    Pre-existing convention in this file — only the cascade-bearing
    postpone-minutes/reschedule paths use SELECT FOR UPDATE. Matching
    existing pattern; tightening locking is a separate refactor
    captured as follow-up #313.
  - Audit surfacing for POST/DELETE/duplicate: only PATCH was enriched
    in this task per validation feedback ("especially PATCH time/date/
    status/dailyNumber paths"). UI-side rendering of the new audit
    fields and POST/DELETE/duplicate enrichment captured as
    follow-up #314.
2026-05-02 09:54:08 +00:00