Commit Graph

288 Commits

Author SHA1 Message Date
Riyadh 59394a69af feat(notes): make incoming-note popup attention-grabbing
Task #409: incoming notes now mirror UpcomingMeetingAlert's attention model
so recipients can't miss them.

Changes:
- DB: add `notification_sound_note` (default "knock"), `notify_notes_enabled`,
  `vibration_enabled_note` to users schema; pushed via drizzle.
- API spec: add 3 new fields to AuthUser + UpdateNotificationPreferencesBody;
  regenerated codegen.
- Auth route: include new fields in buildAuthUser + PATCH allowlist.
- Socket hook: play notification sound + vibrate on `note_received`,
  gated by mute/notifyNotesEnabled/socket warmup, with playedNoteIdsRef
  dedupe (mirrors upcoming-meeting-alert playedRef).
- IncomingNotePopupContext: enqueue() now returns boolean acceptance so
  the socket hook can suppress sound on own-note/duplicate.
- Popup visual: z-[110], ring-4 amber, shadow-2xl, animate-in zoom,
  avatar animate-ping pulse.
- Settings UI: refactored to SLOT_KEYS map, added 3rd "Notes" slot tab
  (grid-cols-3) with enabled/vibration toggles + sound picker.
- Locales en/ar: added notifSettings.slot.note, notesEnabled, vibrationNote.

Pre-existing typecheck errors in executive-meetings.ts (font settings
scope) are unrelated to this task.
2026-05-05 16:21:14 +00:00
Riyadh 8b31c79dd9 Task #406: Pop the note onto the recipient's screen on arrival
- Backend: extend `note_received` socket payload with note snapshot
  (title, content, color, sentAt), recipientRowId, senderUserId, and
  sender summary so the recipient can render the popup without an extra
  fetch.
- Add IncomingNotePopupContext: FIFO queue, dedupe by note id, suppress
  events for the user's own sends, auto-clear on logout.
- Add IncomingNotePopup AlertDialog that shows the note (title/content/
  color) with a sender chip and Reply / Mark read / Open in Notes /
  Dismiss buttons. Acknowledge actions call /notes/:id/read.
- Wire the popup globally in App.tsx (alongside the socket bridge).
- Notes page accepts a `?thread=ID` deep link so the popup's Open and
  Reply buttons land on the inbox tab + thread dialog.
- Added bilingual `notes.popup.*` strings in EN + AR (RTL respected).
- New Playwright e2e (`notes-popup-on-receive.spec.mjs`) verifies the
  popup appears in a second browser context within seconds, sender
  doesn't see their own popup, and dismiss closes it.
2026-05-05 15:45:17 +00:00
Riyadh 590ef32691 Task #406: Pop the note onto the recipient's screen on arrival
- Backend: extend `note_received` socket payload with note snapshot
  (title, content, color, sentAt), recipientRowId, senderUserId, and
  sender summary so the recipient can render the popup without an extra
  fetch.
- Add IncomingNotePopupContext: FIFO queue, dedupe by note id, suppress
  events for the user's own sends, auto-clear on logout.
- Add IncomingNotePopup AlertDialog that shows the note (title/content/
  color) with a sender chip and Reply / Mark read / Open in Notes /
  Dismiss buttons. Acknowledge actions call /notes/:id/read.
- Wire the popup globally in App.tsx (alongside the socket bridge).
- Notes page accepts a `?thread=ID` deep link so the popup's Open and
  Reply buttons land on the inbox tab + thread dialog.
- Added bilingual `notes.popup.*` strings in EN + AR (RTL respected).
- New Playwright e2e (`notes-popup-on-receive.spec.mjs`) verifies the
  popup appears in a second browser context within seconds, sender
  doesn't see their own popup, and dismiss closes it.
2026-05-05 15:43:48 +00:00
Riyadh ee9d24efd9 Task #406: Pop the note onto the recipient's screen on arrival
- Backend: extend `note_received` socket payload with note snapshot
  (title, content, color, sentAt), recipientRowId, senderUserId, and
  sender summary so the recipient can render the popup without an extra
  fetch.
- Add IncomingNotePopupContext: FIFO queue, dedupe by note id, suppress
  events for the user's own sends, auto-clear on logout.
- Add IncomingNotePopup AlertDialog that shows the note (title/content/
  color) with a sender chip and Reply / Mark read / Open in Notes /
  Dismiss buttons. Acknowledge actions call /notes/:id/read.
- Wire the popup globally in App.tsx (alongside the socket bridge).
- Notes page accepts a `?thread=ID` deep link so the popup's Open and
  Reply buttons land on the inbox tab + thread dialog.
- Added bilingual `notes.popup.*` strings in EN + AR (RTL respected).
- New Playwright e2e (`notes-popup-on-receive.spec.mjs`) verifies the
  popup appears in a second browser context within seconds, sender
  doesn't see their own popup, and dismiss closes it.
2026-05-05 15:39:39 +00:00
Riyadh ecde2403f4 Task #402: Convert Notes into in-app messaging
Original task: turn personal Notes into in-app messaging — sender composes
a note (title/content/color), picks recipient(s), Send. Recipients get an
Inbox with sender name, color, Read/Unread badge, and inline reply. Sender
sees Sent Notes with per-recipient status. Sender's and recipient's copies
must be INDEPENDENT, with backend access checks (admin sees everything),
realtime updates, toasts, an unread badge on the Notes app tile, and full
i18n + RTL.

Three review rounds were addressed in this commit:

Round 1 (independence):
- Snapshot columns (title/content/color) on note_recipients; FKs dropped
  on note_recipients.note_id and note_replies.note_id so recipient
  threads survive the sender deleting their note.
- /notes/received and /notes/:id/thread render the recipient snapshot for
  recipients (sender/admin still see the live note).

Round 2:
- POST /notes/:id/reply: owner is now allowed to reply too. Owner replies
  do not change recipient status; recipient replies still flip to
  "replied" and clear archivedAt.
- Added GET /notes/:id as an alias of /notes/:id/thread.
- Added OpenAPI ops for the Notes routes and ran orval codegen.
- use-notifications-socket.ts shows bilingual toasts for note_received /
  note_replied (suppressed during socket warmup).
- home.tsx renders an unread badge on the Notes app tile.

Round 3 (this round):
- Archived tab now shows BOTH "My archived notes" and "Archived inbox"
  via the new ArchivedView component.
- Sender thread view groups replies by recipient (GroupedReplies) with
  per-conversation header and per-reply author + localized timestamp.
  Recipient view stays flat.
- Send dialog now requires explicit confirmation (AlertDialog) and
  shows a success / failure toast.
- Realtime toast strings moved off hardcoded EN/AR to i18n keys
  (notes.toast.received|replied.*) added in en.json and ar.json.
- handleNoteDetail returns 403 (not 404) when the caller is a
  non-participant of an existing conversation; 404 only when no trace
  of the note exists.
- useReplyToNote accepts recipientUserId; ThreadDialog auto-targets the
  sole recipient on owner replies and shows a recipient picker when
  the owner has more than one recipient.

Tests: 7 backend tests in notes-share.test.mjs + 1 e2e (notes-inbox.spec.mjs)
all pass. tx-os typecheck is clean. Architect re-review: PASS.

Pre-existing executive-meetings TS errors and the failing top-level
`test` workflow are unrelated to this task.
2026-05-05 15:09:44 +00:00
Riyadh 5d9d0995ce Task #402: Convert Notes into in-app messaging
Original task: turn personal Notes into in-app messaging — sender composes a
note (title/content/color), picks recipient(s), Send. Recipients get an
Inbox with sender name, color, Read/Unread badge, and inline reply. Sender
sees Sent Notes with per-recipient status. Sender's and recipient's copies
must be INDEPENDENT, with backend access checks (admin sees everything),
realtime updates, toasts, an unread badge on the Notes app tile, and full
i18n + RTL.

Two prior code-review rounds were addressed in this commit:

Round 1 (independence):
- Added immutable snapshot columns (title/content/color) on note_recipients.
- Dropped the FK from note_recipients.note_id and note_replies.note_id so
  recipient threads survive the sender deleting their note.
- /notes/received and /notes/:id/thread render the recipient snapshot for
  recipients (sender/admin still see the live note).

Round 2 (validation REJECT fixes):
- POST /notes/:id/reply: owner is now allowed to reply too. Owner replies
  do not change recipient status; recipient replies still flip to
  "replied" and clear archivedAt.
- Added GET /notes/:id as an alias of /notes/:id/thread (shared handler).
- Added OpenAPI ops for /notes/sent, /notes/received, /notes/{id},
  /notes/{id}/send, /notes/{id}/read, /notes/{id}/archive,
  /notes/{id}/reply, and ran orval codegen.
- use-notifications-socket.ts now shows bilingual toasts for note_received
  and note_replied (suppressed during the socket warmup window).
- home.tsx renders an unread badge on the Notes app tile, fed by
  useReceivedNotes(false) filtered to status === "unread"; refreshes
  automatically on socket invalidation.

Tests: 7 backend tests in notes-share.test.mjs (independence,
archived-reply, owner-reply preserves recipient status, GET /notes/:id
alias, etc.) + the notes-inbox e2e all pass. tx-os typecheck is clean.

Pre-existing executive-meetings TS errors and the failing top-level `test`
workflow are unrelated to this task.
2026-05-05 15:01:18 +00:00
Riyadh b47ceeec10 Task #402: Convert Notes into in-app messaging (independence fix)
Original task: turn personal Notes into in-app messaging — sender composes a
note (title/content/color), picks recipient(s), Send. Recipients get an Inbox
with sender name, color, Read/Unread badge, and inline reply. Sender sees
Sent Notes with per-recipient status. Sender's and recipient's copies must
be INDEPENDENT, with proper backend access checks (admin sees everything),
realtime updates, and full i18n + RTL.

Initial implementation review FAILED because the recipient view still read
from the sender's notes table, so sender edits/deletes mutated recipient
copies. This commit completes the fix:

- Schema (lib/db/src/schema/notes.ts): added immutable snapshot columns
  (title/content/color) on note_recipients; dropped the FK on
  note_recipients.note_id and note_replies.note_id and made them plain
  integers so recipient threads survive the sender deleting their note.
- Routes (artifacts/api-server/src/routes/notes.ts):
  - /notes/received and /notes/:id/thread now serve the recipient snapshot
    (sender/admin still see the live note).
  - /notes/:id/reply derives the owner from note_recipients.senderUserId
    so it works after sender deletion, clears archivedAt, and bumps
    status to "replied".
  - /notes/sent filters out null noteIds.
- Tests (artifacts/api-server/tests/notes-share.test.mjs): added
  snapshot-independence test (sender edit + delete must not mutate
  recipient copy; thread + reply still work after sender delete) and
  archived-reply-clears-archivedAt test. All 5 backend tests pass; the
  existing notes-inbox e2e still passes.
- Architect review: PASS (conditional only on the schema migration being
  applied, which has been pushed via `pnpm --filter @workspace/db push`).

Pre-existing executive-meetings TS errors and the failing `test` workflow
are unrelated to this task.
2026-05-05 14:53:30 +00:00
Riyadh 9fcfbe3526 Update order confirmation modal title for clarity
Modify the order confirmation modal title in both Arabic and English locale files to a generic confirmation message, resolving a UI redundancy where the item name was displayed twice.
2026-05-05 14:18:33 +00:00
Riyadh 9287e1f95a Task #397: Per-card and bulk select+delete on Incoming Orders
Backend (artifacts/api-server):
- Added hasReceivePermission() and refactored DELETE /orders/:id into a
  shared authorizeAndDeleteOrder() helper so single and bulk paths use
  the same authorization rules.
- Added POST /orders/bulk-delete returning { deletedIds, failedIds } with
  per-id authorization, dedup, and best-effort partial success.
- Tightened receiver authorization to mirror GET /orders/incoming: a
  receiver may only delete an unclaimed pending order or one they have
  themselves claimed and is still active (received/preparing). Another
  receiver's claimed order, and any terminal order, return 403 even at
  the API layer. Code, comments and OpenAPI description now agree.

API spec (lib/api-spec/openapi.yaml):
- New /orders/bulk-delete operation with BulkDeleteServiceOrdersBody and
  BulkDeleteServiceOrdersResponse schemas. Updated DELETE /orders/{id}
  description. Regenerated react-query hooks via codegen — both
  useDeleteServiceOrder and useBulkDeleteServiceOrders are used by the UI.

UI (artifacts/tx-os/src/pages/orders-incoming.tsx):
- Per-card checkbox visible at all times (RTL-safe leading edge).
- Per-card trash icon for single delete.
- Section-scoped Select-all (مين / unclaimed) with tri-state
  all/some(indeterminate)/none and shadcn Checkbox.
- Sticky bottom bulk action bar shows selected count + Clear selection +
  destructive Delete.
- Single AlertDialog used by both per-card and bulk paths, with
  pluralized confirmation copy. Single delete calls DELETE /orders/:id;
  multi delete calls POST /orders/bulk-delete; partial-failure surfaces
  via toast.

i18n: New incomingOrders keys in ar.json and en.json (select, selectAll,
clearSelection, selectedCount, delete, deleteConfirmTitle/Body, deleted,
deleteFailed, deletePartial) with pluralization in both languages.

Tests:
- artifacts/api-server/tests/service-orders.test.mjs: receivers can
  delete pending/received/preparing; receivers cannot delete terminal
  (completed/cancelled); bulk-delete with mixed authorized / unknown /
  dedup / unauthenticated cases.
- artifacts/tx-os/tests/order-incoming-delete.spec.mjs (new Playwright):
  bulk-delete two unclaimed orders shrinks the list by 2; per-card
  trash on a claimed order deletes one. Both pass.

Pre-existing executive-meetings PDF/font test failures are unrelated.

Follow-ups proposed: #398 (Undo for incoming-order deletes), #399
(safer notification handling in bulk-delete).
2026-05-05 13:46:59 +00:00
Riyadh 7dee0e758d Task #397: Per-card and bulk select+delete on Incoming Orders
Backend (artifacts/api-server):
- Added hasReceivePermission() and refactored DELETE /orders/:id into a
  shared authorizeAndDeleteOrder() helper so single and bulk paths use
  the same authorization rules.
- Added POST /orders/bulk-delete returning { deletedIds, failedIds } with
  per-id authorization, dedup, and best-effort partial success.
- Tightened receiver authorization to mirror GET /orders/incoming: a
  receiver may only delete an unclaimed pending order or one they have
  themselves claimed and is still active (received/preparing). Another
  receiver's claimed order, and any terminal order, return 403 even at
  the API layer. Code, comments and OpenAPI description now agree.

API spec (lib/api-spec/openapi.yaml):
- New /orders/bulk-delete operation with BulkDeleteServiceOrdersBody and
  BulkDeleteServiceOrdersResponse schemas. Updated DELETE /orders/{id}
  description. Regenerated react-query hooks via codegen — both
  useDeleteServiceOrder and useBulkDeleteServiceOrders are used by the UI.

UI (artifacts/tx-os/src/pages/orders-incoming.tsx):
- Per-card checkbox visible at all times (RTL-safe leading edge).
- Per-card trash icon for single delete.
- Section-scoped Select-all (مين / unclaimed) with tri-state
  all/some(indeterminate)/none and shadcn Checkbox.
- Sticky bottom bulk action bar shows selected count + Clear selection +
  destructive Delete.
- Single AlertDialog used by both per-card and bulk paths, with
  pluralized confirmation copy. Single delete calls DELETE /orders/:id;
  multi delete calls POST /orders/bulk-delete; partial-failure surfaces
  via toast.

i18n: New incomingOrders keys in ar.json and en.json (select, selectAll,
clearSelection, selectedCount, delete, deleteConfirmTitle/Body, deleted,
deleteFailed, deletePartial) with pluralization in both languages.

Tests:
- artifacts/api-server/tests/service-orders.test.mjs: receivers can
  delete pending/received/preparing; receivers cannot delete terminal
  (completed/cancelled); bulk-delete with mixed authorized / unknown /
  dedup / unauthenticated cases.
- artifacts/tx-os/tests/order-incoming-delete.spec.mjs (new Playwright):
  bulk-delete two unclaimed orders shrinks the list by 2; per-card
  trash on a claimed order deletes one. Both pass.

Pre-existing executive-meetings PDF/font test failures are unrelated.

Follow-ups proposed: #398 (Undo for incoming-order deletes), #399
(safer notification handling in bulk-delete).
2026-05-05 13:45:04 +00:00
Riyadh e2e4371db6 Task #397: Per-card and bulk select+delete on Incoming Orders
Backend (artifacts/api-server):
- Added hasReceivePermission() and refactored DELETE /orders/:id into a
  shared authorizeAndDeleteOrder() helper so single and bulk paths use
  the same authorization rules.
- Added POST /orders/bulk-delete returning { deletedIds, failedIds } with
  per-id authorization, dedup, and best-effort partial success.
- Tightened receiver authorization: receivers may only delete orders
  currently in the incoming queue (pending/received/preparing). Terminal
  orders remain the owner's cleanup responsibility. Code, comments and
  OpenAPI description now agree.

API spec (lib/api-spec/openapi.yaml):
- New /orders/bulk-delete operation with BulkDeleteServiceOrdersBody and
  BulkDeleteServiceOrdersResponse schemas. Updated DELETE /orders/{id}
  description. Regenerated react-query hooks via codegen — both
  useDeleteServiceOrder and useBulkDeleteServiceOrders are used by the UI.

UI (artifacts/tx-os/src/pages/orders-incoming.tsx):
- Per-card checkbox visible at all times (RTL-safe leading edge).
- Per-card trash icon for single delete.
- Section-scoped Select-all (مين / unclaimed) with tri-state
  all/some(indeterminate)/none and shadcn Checkbox.
- Sticky bottom bulk action bar shows selected count + Clear selection +
  destructive Delete.
- Single AlertDialog used by both per-card and bulk paths, with
  pluralized confirmation copy. Single delete calls DELETE /orders/:id;
  multi delete calls POST /orders/bulk-delete; partial-failure surfaces
  via toast.

i18n: New incomingOrders keys in ar.json and en.json (select, selectAll,
clearSelection, selectedCount, delete, deleteConfirmTitle/Body, deleted,
deleteFailed, deletePartial) with pluralization in both languages.

Tests:
- artifacts/api-server/tests/service-orders.test.mjs: receivers can
  delete pending/received/preparing; receivers cannot delete terminal
  (completed/cancelled); bulk-delete with mixed authorized / unknown /
  dedup / unauthenticated cases.
- artifacts/tx-os/tests/order-incoming-delete.spec.mjs (new Playwright):
  bulk-delete two unclaimed orders shrinks the list by 2; per-card
  trash on a claimed order deletes one. Both pass.

Pre-existing executive-meetings PDF/font test failures are unrelated.

Follow-ups proposed: #398 (Undo for incoming-order deletes), #399
(safer notification handling in bulk-delete).
2026-05-05 13:40:18 +00:00
Riyadh 6f6210ec3b Task #397: Per-card and bulk select+delete on Incoming Orders
Backend (artifacts/api-server):
- Added hasReceivePermission() helper and refactored DELETE /orders/:id
  into shared authorizeAndDeleteOrder() so single and bulk paths use the
  same authorization rules.
- Added POST /orders/bulk-delete returning { deletedIds, failedIds } with
  per-id authorization, dedup, and best-effort partial success.
- Tightened receiver authorization (per code review): receivers may only
  delete pending/received/preparing orders; terminal (completed/cancelled)
  orders remain the owner's cleanup responsibility. OpenAPI description
  and the implementation now agree.

API spec (lib/api-spec/openapi.yaml):
- New /orders/bulk-delete operation with BulkDeleteServiceOrdersBody and
  BulkDeleteServiceOrdersResponse schemas; updated DELETE /orders/{id}
  description. Regenerated react-query hooks via codegen — useBulkDelete
  ServiceOrders is now available.

UI (artifacts/tx-os/src/pages/orders-incoming.tsx):
- Selection mode toggle in the page header (RTL-safe).
- Per-card Checkbox plus a Select-all control.
- Sticky bottom action bar with destructive Delete and selected count.
- AlertDialog confirmation using pluralized i18n keys; toast surfaces
  partial-failure results when some ids couldn't be deleted.

i18n: New incomingOrders keys in ar.json and en.json (select, selectAll,
clearSelection, selectedCount, delete, deleteConfirmTitle/Body, deleted,
deleteFailed, deletePartial) with pluralization.

Tests: Added two new tests in service-orders.test.mjs covering receiver
delete on incoming queue, receiver forbidden on terminal orders, and
bulk-delete with mixed authorized / unknown / dedup / unauthenticated
cases. Both pass. Pre-existing executive-meetings PDF/font test failures
are unrelated.
2026-05-05 13:35:21 +00:00
Riyadh ff71732c4d Saudi Coffee image: handleless finjan + brown coffee (Task #396)
Regenerated artifacts/tx-os/public/service-images/saudi-coffee.png
with a sharper prompt that explicitly names a handleless Saudi
finjan (small flared white porcelain cup with a thin gold patterned
rim, no handle) and a clear warm brown coffee color for both the
pour and the liquid in the cup. Strengthened the negative prompt
against handled cups, mugs, amber/orange liquid, and tea so the new
image no longer reads as "tea in a teacup".

Same path as before, so no DB change is needed — the existing
services.image_url ("service-images/saudi-coffee.png") on row id=2
keeps pointing at the new file.

No code, schema, locale, or test changes. Pre-existing `test`
workflow failure is unrelated.
2026-05-05 13:14:31 +00:00
Riyadh 4e1665a68f Saudi Coffee image: handleless finjan + brown coffee (Task #396)
Regenerated artifacts/tx-os/public/service-images/saudi-coffee.png
with a sharper prompt that explicitly names a handleless Saudi
finjan (small flared white porcelain cup with a thin gold patterned
rim, no handle) and a clear warm brown coffee color for both the
pour and the liquid in the cup. Strengthened the negative prompt
against handled cups, mugs, amber/orange liquid, and tea so the new
image no longer reads as "tea in a teacup".

Same path as before, so no DB change is needed — the existing
services.image_url ("service-images/saudi-coffee.png") on row id=2
keeps pointing at the new file.

No code, schema, locale, or test changes. Pre-existing `test`
workflow failure is unrelated.
2026-05-05 13:13:33 +00:00
Riyadh d410b51792 Saudi Coffee image cleanup + shrink "Add notes" toggle (Task #395)
Two small follow-ups on Task #394 polish:

1. Regenerated artifacts/tx-os/public/service-images/saudi-coffee.png
   with a tighter prompt — just the brass dallah pouring brown
   coffee into a small white finjan, soft beige background, no
   dates/cardamom/garnish. Same path so no DB change needed.
2. Shrunk the "إضافة ملاحظات / Add notes" toggle in
   OrderServiceModal: replaced the chunky outline Button with a
   small ghost text-link style (text-xs, muted, pencil icon size 12,
   underline-on-hover). It now reads as a secondary affordance and
   sits below the service-preview row, well clear of the dialog's
   absolute close (X) at right-4 top-4 — no overlap in either AR or
   EN.

Behavior unchanged: data-testid="order-notes-toggle" preserved,
tapping still expands and focuses the textarea, hide link still
collapses without losing typed text.

Verification:
- pnpm --filter @workspace/tx-os typecheck → passes.
- The pre-existing `test` workflow failure is unrelated to this
  change.
2026-05-05 13:08:34 +00:00
Riyadh 84b28d04e7 Polish Services: Saudi Coffee rename + image, fix Black Coffee Arabic, hide order notes behind a button (Task #394)
Three small UX fixes on the Services screen:

1. Renamed service id=2 to "قهوة سعودي" / "Saudi Coffee" and swapped
   the thumbnail to a new clear brown image (small dallah pouring
   into a finjan with cardamom + a date) saved at
   public/service-images/saudi-coffee.png. The legacy
   arabic-coffee.png is left in place so any cached references keep
   loading.
2. Renamed service id=2450 Arabic name to "بلاك كوفي" (English
   "Black Coffee" unchanged).
3. OrderServiceModal now opens with the notes textarea collapsed and
   not mounted, so iPad/Safari no longer auto-pops the keyboard. A
   small outline "إضافة ملاحظات / Add notes" button (with pencil
   icon) expands it and focuses the textarea on demand. While
   expanded, a small "إخفاء / Hide" link collapses it again without
   clearing typed text. If initialNotes is non-empty when the dialog
   opens, it starts expanded. Submission behavior unchanged.

Verified DB state after updates:
  id=2    name_ar=قهوة سعودي   name_en=Saudi Coffee  image_url=service-images/saudi-coffee.png
  id=2450 name_ar=بلاك كوفي    name_en=Black Coffee  image_url=service-images/black-coffee.png

Added i18n keys services.addNotes / services.hideNotes to ar.json
and en.json. Added data-testid="order-notes-toggle" and
data-testid="order-notes-textarea" for future tests.

`pnpm --filter @workspace/tx-os typecheck` passes. The pre-existing
`test` workflow failure is unrelated to this change.
2026-05-05 12:58:23 +00:00
Riyadh 83a70de0d9 Task #393: Coalesce notification sound bursts on app open
User reported a flood of chimes when opening Tx OS while several
notifications were pending. Two complementary fixes:

1. Player throttle bumped from 600 ms → 3000 ms in
   artifacts/tx-os/src/lib/notification-sounds.ts. Bursts of real-time
   events now produce a single chime instead of several overlapping ones.

2. Socket warmup gate added in
   artifacts/tx-os/src/hooks/use-notifications-socket.ts:
   - Track connectedAtRef (useRef<number>), set on effect mount and on
     each socket "connect" event using performance.now().
   - In the notification_created handler, after mute / per-channel
     gates but before notificationPlayer.play(...), suppress
     audio + vibration when within the 3 s warmup window.
   - UI/badge invalidation still runs — only sound is silenced during
     the warmup, so the user sees notifications appear without an
     alarm-like welcome.

All existing gates preserved (global mute, per-channel sound/vibration,
autoplay-blocked toast hint, per-meeting playedRef dedupe).

No deviations. tx-os typecheck passes.
2026-05-05 12:30:00 +00:00
Riyadh 696c4d0a9c Center the clock in the executive meetings header
Modify `artifacts/tx-os/src/pages/executive-meetings.tsx` to absolutely position the HeaderClock component, centering it within the header row.
2026-05-05 09:54:06 +00:00
Riyadh bcdbdfb850 Task #392: Center the clock in Executive Meetings header
Moved the live HeaderClock from the actions cluster (next to the Export
PDF button) to the geometric center of the top header row.

Implementation:
- Made the header row container `relative`.
- Removed `<HeaderClock />` from the trailing actions `<div>`.
- Added an absolutely-positioned overlay (`absolute inset-x-0 top-0
  bottom-0 flex items-center justify-center pointer-events-none`)
  containing the clock, with the inner wrapper restoring
  `pointer-events-auto`. This keeps the clock visually centered
  regardless of whether the title or actions on either side are wider.
- Kept all #391 behaviors: `hidden sm:flex`, `print:hidden`, 1s tick,
  ar/en locale formatting, palette text-[#0B1E3F], tabular-nums,
  data-testid="em-header-clock".

No deviation from plan. tx-os typecheck passes.
2026-05-05 09:52:54 +00:00
Riyadh 3899a4ab57 Task #391: Live clock in Executive Meetings header
Added a `HeaderClock` component to artifacts/tx-os/src/pages/executive-meetings.tsx
that renders next to the "Export PDF" button in the sticky page header.

- Updates every 1s via setInterval (cleared on unmount)
- Locale-aware formatting via Intl.DateTimeFormat (ar / en) with hour12
- Hidden on <sm screens (`hidden sm:flex`) and on print
- Uses existing palette (text-[#0B1E3F]) + Clock lucide icon
- tabular-nums to prevent jitter as digits change
- data-testid="em-header-clock" for future tests

No deviations from plan. tx-os typecheck passes.
2026-05-05 09:48:22 +00:00
Riyadh c8ab4165ba Task #390: Always play notification sounds (foreground + background)
Removed the `document.visibilityState === "visible"` early-return from
two playback paths so notification sounds fire even when the Tx OS tab
is the active foreground tab:

- `use-notifications-socket.ts` — order + meeting socket events.
- `upcoming-meeting-alert.tsx` — 5-min upcoming-meeting alert chime.

All other gates remain unchanged: global mute, per-channel sound and
vibration toggles, the ~600ms throttle in the audio player, and the
per-meeting `playedRef` dedupe by `meetingId`.

Verified `pnpm --filter @workspace/tx-os typecheck` passes.
2026-05-05 08:31:27 +00:00
Riyadh dc94be41ee Task #389: Notification sounds + vibration with per-user customization
DB
- Added user pref columns; vibration is per-channel
  (vibrationEnabledOrder + vibrationEnabledMeeting). Defaults: ding/chime
  sounds, vibration on, mute off. Volume uses the device system level —
  no persisted volume field.

API
- PATCH /auth/me/notification-preferences (requireAuth, whitelisted partial
  update, integer guard for volume). buildAuthUser exposes all new fields.
- OpenAPI spec updated; orval client + zod regenerated.

Frontend
- Static sound library: 8 short WAV assets under public/sounds/ + manifest
  (notification-sound-manifest.ts) mapping id -> labelKey -> URL.
- HTMLAudio-based player (notification-sounds.ts) with throttle, unlock,
  cache, and AUTOPLAY_BLOCKED_EVENT dispatch when play is denied pre-gesture.
- Settings popover: global mute + slot tabs (orders/meetings) with
  per-channel sound + per-channel vibration + per-channel toggle, sound list
  with one-tap previews. Concurrency-safe optimistic updates (monotonic seq
  counter). RTL-correct toggle knob transforms.
- QuickMuteButton: one-tap topbar mute control (Volume2/VolumeX) with
  confirmation toast.
- useAudioUnlock: unlocks audio on first interaction.
- useAutoplayHint: toasts a localized "click anywhere to enable" hint when
  the player reports a blocked play attempt.
- Socket hook plays the right per-channel sound + vibration on
  notification_created (orders, executive_meeting) when the tab is hidden.
- UpcomingMeetingAlert: plays meeting reminder once per new eligible
  meeting (deduped by meetingId, survives 30s polling refetches). Skipped
  when the tab is currently visible — sound only fires when backgrounded.
- AR/EN translations added.

Pre-existing TS errors in executive-meetings.ts (font_settings) and
pre-existing test workflow failures are unrelated.
2026-05-05 08:24:27 +00:00
Riyadh 88cbbcfc46 Task #389: Notification sounds + vibration with per-user customization
DB
- Added 7 user pref columns; vibration is now per-channel
  (vibrationEnabledOrder + vibrationEnabledMeeting). Defaults: ding/chime
  sounds, vibration on, mute off, volume 70.

API
- PATCH /auth/me/notification-preferences (requireAuth, whitelisted partial
  update, integer guard for volume). buildAuthUser exposes all new fields.
- OpenAPI spec updated; orval client + zod regenerated.

Frontend
- Static sound library: 8 short WAV assets under public/sounds/ + manifest
  (notification-sound-manifest.ts) mapping id -> labelKey -> URL.
- HTMLAudio-based player (notification-sounds.ts) with throttle, unlock,
  cache, and AUTOPLAY_BLOCKED_EVENT dispatch when play is denied pre-gesture.
- Settings popover: global mute, volume, slot tabs (orders/meetings) with
  per-channel sound + per-channel vibration + per-channel toggle, sound list
  with one-tap previews. Concurrency-safe optimistic updates (monotonic seq
  counter). RTL-correct toggle knob transforms.
- QuickMuteButton: one-tap topbar mute control (Volume2/VolumeX) with
  confirmation toast.
- useAudioUnlock: unlocks audio on first interaction.
- useAutoplayHint: toasts a localized "click anywhere to enable" hint when
  the player reports a blocked play attempt.
- Socket hook plays the right per-channel sound + vibration on
  notification_created (orders, executive_meeting) when the tab is hidden.
- UpcomingMeetingAlert: plays meeting reminder once per new eligible
  meeting (deduped by meetingId, survives 30s polling refetches).
- AR/EN translations added.

Pre-existing TS errors in executive-meetings.ts (font_settings) and
pre-existing test workflow failures are unrelated.
2026-05-05 08:20:54 +00:00
Riyadh 876719f0eb Task #389: Notification sounds + vibration with per-user customization
- DB: 7 new user prefs (sound per slot, per-type toggles, vibration, mute, volume).
- API: PATCH /auth/me/notification-preferences with whitelisted partial update,
  integer guard for volume, and hydrated AuthUser response. AuthUser now exposes
  the 7 new fields.
- Frontend: Web Audio synth library (8 sounds), settings popover with global
  mute/vibration/volume/per-type toggles, slot tabs, sound preview buttons,
  shift+click on bell for quick mute. Concurrency-safe optimistic updates with
  monotonic seq counter. RTL-correct toggle knob transforms.
- Socket hook plays sound on notification_created (orders/meetings only) when
  tab is hidden, respecting global mute and per-type toggles.
- Audio unlock hook mounted globally to satisfy autoplay restrictions.
- AR/EN translations added.

Pre-existing TS errors in executive-meetings.ts (font_settings) and pre-existing
test failures in test workflow are unrelated to this task.
2026-05-05 08:13:38 +00:00
Riyadh c1d5ccd771 Update site colors and add order notification badge
Updates primary color to navy, adjusts icon tile shadows, and adds a real-time notification badge for incoming orders to the home page.
2026-05-05 07:51:36 +00:00
Riyadh ba02fa28db Add progress tracking and cancel functionality to bulk delete
Update localization files and executive-meetings.tsx to implement sequential bulk deletion with progress tracking and cancellation, mirroring the UX of the bulk duplicate feature.
2026-05-05 07:27:21 +00:00
Riyadh 0338e22df8 Add progress bar and cancel option to bulk meeting duplication
Integrates a progress bar and cancel functionality into the bulk duplicate process for executive meetings, migrating from `Promise.allSettled` to a sequential loop to prevent race conditions and updating UI components and localization files.
2026-05-05 07:22:12 +00:00
Riyadh 96cfea312c Update coffee offerings and remove unwanted drinks
Update Arabic coffee image to show brown coffee, add black coffee as a new service, and remove water, Nescafe, and soft drinks.
2026-05-04 18:57:17 +00:00
Riyadh 2e6f249c26 Remove zoom and fullscreen features from the executive meetings page
Revert changes related to zoom and fullscreen functionality on the executive meetings page, including associated UI elements and state management.
2026-05-04 18:12:56 +00:00
Riyadh e688e02a3f Add zoom and fullscreen controls to the meeting schedule table
Add zoom and fullscreen controls to the executive meeting schedule page, including zoom in/out buttons, a fullscreen toggle, and Escape key handling for exiting fullscreen. The heading height calculation has been removed and hardcoded to 0px.
2026-05-04 17:55:36 +00:00
Riyadh d2b697d3d2 Move toolbar controls to the top navigation bar
Moves the edit, search, and date controls from the Schedule section's sticky header to the main navigation bar using React portals.
2026-05-04 17:50:11 +00:00
Riyadh 2836d815d8 Update meeting page controls with new icons and search functionality
Replaces existing edit and search controls with icon-based components, introducing a collapsible search input.
2026-05-04 16:22:41 +00:00
Riyadh 0a6a19d277 Improve meeting settings by adding collapsible sections and removing redundant text
Refactor executive meetings page to use collapsible sections for alert preferences and font settings, and remove descriptive text from locale files.
2026-05-04 16:15:49 +00:00
Riyadh 4101124784 Fix toggle switch display for Arabic language users
Adjust translation classes in the Switch component to correctly display the toggle switch in RTL layouts.
2026-05-04 16:09:18 +00:00
Riyadh 657d74be91 Make the customize table section collapsible in settings
Introduce collapsible functionality to the "Customize Table" section in executive meetings settings, allowing users to expand or collapse the panel.
2026-05-04 16:04:55 +00:00
Riyadh 4dff005c35 Update PDF export to use localized filenames and remove extra text
Modify the API server and frontend to generate dynamic, localized PDF filenames for executive meetings, and remove descriptive text from the PDF export page.
2026-05-04 15:57:11 +00:00
Riyadh 0f30aef532 Update meeting row styling for dark backgrounds and white text
Apply darker background and white text styling to the meeting number cell in both web and PDF outputs, ensuring consistency.
2026-05-04 14:20:12 +00:00
Riyadh 7d369efbf8 Apply row styling only to meeting number and title columns
Removes row background and border styling from the attendees and time columns in the PDF renderer and the web interface. Adjusts the `tintedCellStyle` function to conditionally apply border styling based on `applyRowColor` flag.
2026-05-04 14:11:15 +00:00
Riyadh 365f8137c2 Update open graph image for the project
Replace existing open graph image file with an updated version.
2026-05-04 14:01:47 +00:00
Riyadh eed69bc8b8 Update images for improved visual presentation and branding
Update opengraph image and other attached assets.
2026-05-04 13:22:13 +00:00
Riyadh 317d19db8d Unify row color styling across all web table cells
Previously, when a meeting row had a color assigned (e.g. red), the
attendees and time cells only received the colored border but not the
background fill. The meeting cell used `tintedCellStyle(col, true)` which
always applies rowBg, while attendees and time used
`tintedCellStyle(col, isMerged)` — meaning they only got the background
when the row was merged.

Changed both attendees and time cells to use `tintedCellStyle(col, true)`
so all four cells (number, meeting, attendees, time) display the same
background fill and border color when a row color is set.

This mirrors the PDF fix from Task #385 which addressed the same
inconsistency in the PDF renderer.

File changed: artifacts/tx-os/src/pages/executive-meetings.tsx (lines 3804, 3825)
2026-05-04 13:07:48 +00:00
Riyadh a934b56bd3 Fix PDF font color not reflecting system settings via per-field merge
Root cause: resolveFontPrefsForUser() used `userRow ?? globalRow` whole-row
precedence. When an admin saved font settings with scope="global", only the
global row was updated. If the admin also had a user-scope row (created by
prior saves with the default scope="user"), ALL fields from the user-scope
row overrode the global row — including fontColor — causing the PDF to show
the old color even after changing global settings.

Schema change (executive-meetings.ts):
- Made fontFamily, fontSize, fontWeight, alignment, fontColor nullable.
  User-scope rows now store NULL for fields that inherit from global,
  and only store non-null values for fields the user explicitly overrode.

Backend fix (executive-meetings.ts):
- resolveFontPrefsForUser: per-field merge —
  user non-null → global non-null → schema default.
- PATCH handler for user-scope: after upsert, compares each field with the
  current global values. Fields matching global are set to NULL (= inherit).
  The post-nullification row is returned in the API response and audit log.
- No user-scope rows are deleted; scope isolation is preserved.

Frontend fix (executive-meetings.tsx):
- effectiveFont computed via per-field merge (u?.field ?? g?.field ?? default)
- FontSettingsResponse type updated for nullable fields (FontSettingsRow)
- Scope switching in FontSettingsSection loads the selected scope's values
  (global → globalFont ?? DEFAULT_FONT; user → effective font)
- globalFont prop threaded through SettingsSection → FontSettingsSection

Data migration: existing user-scope rows normalized via SQL — fields matching
global values set to NULL so per-field inheritance applies immediately.

Verified: TypeScript clean, e2e Playwright test passes, API tests confirm
per-field merge, nullification, and PDF color propagation.
2026-05-04 12:30:00 +00:00
Riyadh 557e692e8e Fix PDF font color not reflecting system settings
Root cause: resolveFontPrefsForUser() uses `userRow ?? globalRow` precedence.
When an admin saved font settings with scope="global", only the global row
was updated. If the admin also had a user-scope row (created by prior saves
with the default scope="user"), the user-scope row still overrode the global
row during PDF generation, causing the PDF to show the old color.

Backend fix (executive-meetings.ts):
- When saving with scope="global", delete the current admin's user-scope row
  within the same transaction. This ensures global settings immediately apply
  to the admin who set them. Other users' personal rows are unaffected.

Frontend fix (executive-meetings.tsx):
- Pass both `globalFont` and effective `font` to FontSettingsSection.
- When the scope dropdown changes, the form now loads the selected scope's
  actual saved values instead of always showing the effective (user-override)
  values. This prevents confusion where the admin edits "global" but sees
  their personal values in the form.

Verified end-to-end:
- PATCH font-settings with scope=user + fontColor=#ff0000 → DB updated → PDF uses red
- PATCH font-settings with scope=global + fontColor=#0000ff → global updated,
  user-scope row deleted → PDF resolves to global blue
- TypeScript compiles cleanly, e2e Playwright test passes
2026-05-04 12:17:40 +00:00
Riyadh 8b8f705cc2 Make row borders darker and more prominent
Update border colors and thickness in PDF renderer and UI to ensure visibility and consistency.
2026-05-04 11:57:42 +00:00
Riyadh dd58ba230b Task #380: Darken cell borders to match row color in PDF and web UI
When a row has a color (e.g. red), the cell borders now use a darker
shade of the same color instead of the default gray (#d1d5db).

PDF renderer (pdf-html-renderer.ts):
- Added ROW_COLOR_BORDER map with darker shades for each color
- Colored rows now get both background-color and border-color inline
- Non-merged rows: # and meeting cells get bg+border, attendees and
  time cells get border only (preserving partial coloring behavior)
- Merged rows: all cells get bg+border via coloredStyle

Web UI (executive-meetings.tsx):
- Added 'border' field to ROW_COLOR_OPTIONS with matching darker shades
- tintedCellStyle always applies borderColor when rowBorder exists
- Number cell inline style includes borderColor in both rowBg and
  tintBg (current meeting highlight) branches
- Merge cell style includes borderColor in both tint and rowBg branches
- Border color persists even when current-meeting highlight is active

Color mapping:
  red:    fill #fee2e2 → border #fca5a5
  amber:  fill #fef3c7 → border #fcd34d
  green:  fill #dcfce7 → border #86efac
  blue:   fill #dbeafe → border #93c5fd
  violet: fill #ede9fe → border #c4b5fd
  gray:   fill #f3f4f6 → border #d1d5db
2026-05-04 11:33:56 +00:00
Riyadh 5d20799ad8 Apply row coloring consistently across the web and PDF views
Adjust cell styling to ensure row background colors are applied uniformly to both individual and merged rows, matching the PDF rendering behavior in the web interface.
2026-05-04 11:05:57 +00:00
Riyadh 3e420536fb Improve PDF generation for meeting tables with better text handling
Update PDF rendering logic to correctly handle bidirectional text, cell merging, and text wrapping for meeting tables.
2026-05-04 09:24:37 +00:00
Riyadh 41966b7f75 PDF: compact table with zero gaps, unified lineHeight (Task #368)
User requested maximum table compression with no gaps between rows.
Changes:
- cellPadX: 8→4, cellPadY: 6→0 (zero vertical padding)
- lineHeight: fontSize*1.5 → fontSize*1.05 (very tight)
- All columns (meeting, attendees, time, #) now center-aligned
- Added lineHeight option to DrawOpts type so draw helpers use the
  same metric as row measurement (fixes probe/draw mismatch that
  could cause row overlap with tight padding)
- drawMixedLine and drawWrappingLine now respect opts.lineHeight
  instead of hardcoded fontSize*1.2, falling back to 1.2 when
  lineHeight is not provided (backward compatible)
- Table cell draw calls pass lineHeight to drawWrappingLine
- Both AR and EN PDFs verified valid with logo embedded
2026-05-04 08:51:09 +00:00
Riyadh eea5b9eebe PDF: enlarge header logo and center attendees column (Task #364)
- Increased logoBoxSize multiplier from 1.8 to 2.8 so the header logo
  renders noticeably larger in the PDF output.
- Changed attendees column text alignment from direction-based (right for
  RTL) to "center" so attendee names are centered in their column.
- Both AR and EN PDFs verified: valid PDF output, logo embedded, correct
  alignment. Code review PASSED, e2e tests PASSED.
2026-05-04 08:12:02 +00:00
Riyadh 8462cfcfd1 Compact PDF table layout to match reference design (Task #359)
Changes to artifacts/api-server/src/lib/pdf-renderer.ts:

- Reduced cellPadX from 6 to 4 (horizontal cell padding)
- Reduced cellPadY from 3 to 2 (vertical cell padding)
- Tightened lineHeight multiplier from 1.25 to 1.2
- Removed +2 padding buffer from heightOfString measurement loop
- Removed +2 from drawWrappingLine return value
- Tightened drawMixedLine return value (1.25→1.2)
- Consolidated header spacing from two moveDown(0.2+0.4) to one moveDown(0.3)
- Tightened title Y offset multiplier from 1.25 to 1.2
- Fixed row height probe to detect script per-line (matching draw path)
  instead of per-cell, preventing measurement/draw mismatch in
  mixed Arabic/Latin content now that the +2 cushion is removed

Result: Table rows are compact with no visible gaps between them,
matching the user's reference PDF (attached_assets/rrr1_1777879598338.pdf).
Row colors, cell borders, and footer positioning are unchanged.
Both AR and EN PDFs generate successfully (200 status).
2026-05-04 07:35:52 +00:00