Commit Graph

177 Commits

Author SHA1 Message Date
Riyadh 43a590277b #480 Show cascade-affected meetings as a numbered table
Replace the simple <ul> in the cascade-prompt panel inside the upcoming-
meeting alert with a 4-column table: row index, the meeting's
dailyNumber, title (truncate + title=), and "from → to". The new
`meetingNumbersById` map is built once in UpcomingMeetingAlert from
the already-loaded dayData and threaded through PostponeDialog into
both CascadePromptBlock call sites (postpone-minutes and reschedule).
Existing `cascade-followers-list` and `cascade-follower-<id>` testids
are preserved on the table/rows so prior selectors keep matching.

Adds 4 new i18n keys (cascadeColIndex/MeetingNumber/Title/Times) in
ar.json and en.json. The legacy cascadeListItem key is left in place
since it isn't worth a separate cleanup.

Extends the existing "Cascade prompt UI: Shift sends cascadeFollowing"
e2e test to assert the first row's index cell renders "1" and the
meeting-number cell renders the seeded follower's dailyNumber.
insertImminentMeeting now also returns dailyNumber so the assertion
can read it. tsc clean; targeted cascade specs pass (3/3).
2026-05-10 15:40:17 +00:00
Riyadh a192734259 notes: bulk-select + bulk-delete on Sent tab (task #472)
- Add `useBulkDeleteSentNotes` hook that fans out parallel DELETE
  /notes/:id calls via Promise.allSettled and returns {ok, failed}
  so the UI can render a precise toast on partial failure. Caches
  invalidated once via `invalidateNotesAndFolders`.
- NotesPage: page-level `sentSelectionMode` + `selectedSentIds`
  Set, auto-reset when navigating away from the Sent tab.
- Header gets a Select / Cancel toggle, gated on
  `view === "sent" && filteredSent.length > 0`.
- New sticky bulk-action bar above the sent list with count,
  select-all/clear toggle, and Delete button.
- SentList accepts optional selectionMode/selectedIds/onToggle
  props; cards swap onClick (open → toggle) and show a Checkbox
  overlay + rose ring when checked.
- AlertDialog confirms before deleting; toast reports all-success,
  partial, or all-failed using pluralized i18n keys.
- New i18n keys under `notes.bulk*` in ar.json + en.json.

No server changes (existing DELETE /notes/:id is reused).

Out of scope (per task spec): undo, bulk-archive, bulk endpoint,
multi-select on Active/Received/Archived tabs.
2026-05-10 14:32:02 +00:00
Riyadh ab1d1e207e Task #461: Fix notification sounds on iPad (route via media channel)
Problem: User reported zero sound on iPad — even the "Test sound"
button in notification settings produced silence. YouTube and other
videos worked fine on the same iPad.

Root cause: The app's notification player used the Web Audio API
(AudioContext + BufferSource). On iOS Safari (iPad/iPhone), Web Audio
plays through the **ringer channel**, which is silenced by:
  - The hardware silent switch (older iPads),
  - Control Center's mute icon,
  - Or simply having ringer volume at 0 (independent of media volume).
HTMLAudioElement (the API behind <video> and YouTube) plays through
the **media channel**, which is what users actually have turned up.

Fix in artifacts/tx-os/src/lib/notification-sounds.ts:
- Added an HTMLAudioElement playback engine alongside the existing
  Web Audio engine. Engine selection is one-time per session via the
  existing `detectIos()` helper (handles iPadOS-as-Mac masquerade).
- iOS path: lazy `new Audio(url)` per sound id with preload="auto",
  primed in `unlock()` by play()-then-pause() at volume 0 inside the
  user gesture so each element is permanently allowed to play later
  out-of-gesture (iOS requirement). Real `play()` resets currentTime
  and plays at full volume; rejection of the play promise dispatches
  the existing autoplay-blocked hint.
- Non-iOS path: unchanged Web Audio behavior preserved verbatim.
- All public API preserved: `unlock()`, `play()`, `testPlay()`,
  `isUnlocked()`, `AUTOPLAY_BLOCKED_EVENT`. No changes needed to
  use-audio-unlock, use-autoplay-hint, use-notifications-socket, or
  notification-settings.
- Throttling (3s burst-coalesce), bypass for testPlay, vibration,
  test-only observability globals, and isIos hint flag all preserved.
- `__txosNotifCtxState` test hook now reports "running"/"suspended"
  on iOS based on `unlocked` flag (no AudioContext exists there).

Locale updates (ar.json + en.json):
- `notifSettings.autoplayHintIos` rewritten: now tells users to raise
  the **media** volume (same one that controls YouTube) instead of
  the old "turn off Silent Mute" advice — matches the new playback
  channel.

Verification: tsc --noEmit clean. Pre-existing `test` workflow failure
on unrelated executive-meetings.ts type errors is out of scope.
Out of scope per task: no PWA, no push notifications, no settings UI
redesign, no new sound files. Manual iPad QA needed before merging.
2026-05-10 11:40:07 +00:00
Riyadh e179addcd4 notes(#454): per-recipient view/edit folder sharing
- Schema: noteFolderShares.permission ('view'|'edit', default 'view').
- Server: resolveFolderEditAccess + emitFolderChanged helpers.
  - POST/PATCH/DELETE/checklist now allow folder editors.
  - Editors stamp notes with folder owner's userId; labels validated
    against owner; PATCH editors blocked from unfile (folderId=null)
    and cross-folder moves to non-matching owners.
  - emitFolderChanged fires for owner AND editor mutations across
    create / patch (old + new folder) / delete / checklist toggle.
  - PUT /shares accepts both legacy recipientUserIds and new
    recipients:[{userId,permission}]; diffs add/update/remove/perm-flip.
  - GET /shares, /shared-with-me, /shared-notes return permission /
    myPermission / readOnly.
- Client:
  - notes-api types: FolderSharePermission, myPermission on
    SharedFolder/SharedFolderView, permission on FolderShareRecipient,
    readOnly:boolean on SharedFolderNote.
  - useUpdateFolderShares takes recipients[].
  - useCreate/Update/DeleteNote also invalidate ['note-folders'] so
    actor's shared-folder rail badge stays fresh.
  - FolderShareDialog: per-row checkbox + segmented View/Edit toggle.
  - SharedFolderView: editor mode mounts Composer + NoteCard with
    full edit/delete/archive affordances; Send hidden in editor
    context (Composer + NoteCard hideSend prop) since /send is
    owner-only.
  - folders-rail: per-folder permission badge.
  - socket: note-folder-shared also invalidates ['notes'] +
    ['note-folders'] for fanout to owner / other editors.
  - Locales: shareDescriptionPerm, permissionView/Edit, canEdit
    (en + ar).
- Pre-existing executive-meetings.ts type errors are out of scope.
2026-05-10 10:16:05 +00:00
Riyadh 1ff59263a9 Notes: live folder-sharing (read-only) — Task #445
Owner can share a whole folder with other users; recipients see it
under "Shared with me" in the folders rail and view notes read-only
(cannot edit, add, move, or delete).

- DB: new noteFolderSharesTable (folderId/recipientUserId, cascade,
  unique idx). Pushed via drizzle-kit.
- API (artifacts/api-server/src/routes/notes.ts):
  - GET/PATCH/POST /note-folders return sharedWithCount.
  - GET /note-folders/shared-with-me, GET /note-folders/:id/shares,
    PUT /note-folders/:id/shares (idempotent diff), DELETE
    /note-folders/:id/shares/:userId, GET /note-folders/:id/shared-notes
    (verifies recipient via noteFolderSharesTable, stamps readOnly).
  - Emits note-folder-shared / note-folder-unshared on share changes.
  - Existing note write paths remain owner-scoped → recipients can't
    mutate owner notes.
- Frontend types/hooks (notes-api.ts): SharedFolder, FolderShareRecipient,
  SharedFolderNote/View; useFolderShares, useUpdateFolderShares,
  useSharedWithMeFolders, useSharedFolderNotes.
- FoldersRail: Share menu item, sharedWithCount badge, "Shared with me"
  section, "shared-folder" selection kind, onShareFolder prop.
- notes.tsx: SharedFolderView (no Composer, read-only cards using
  ChecklistView), FolderShareDialog (seeds existing recipients,
  idempotent PUT on save), revocation fallback effect, dialog mount.
- use-notifications-socket.ts: subscribe to note-folder-shared /
  -unshared → invalidate shared-with-me + open shared-folder notes for
  live rail/view refresh.
- i18n: AR/EN keys for share/sharedBy/sharedByName/sharedWithCount/
  sharedWithMe/readOnly/shareRevoked/sharedEmpty/shareSaved/shareFailed.

Pre-existing failing `test` workflow (executive-meetings type errors)
is unrelated and out of scope.
2026-05-09 11:07:30 +00:00
Riyadh 28b8e9e4fa Task #441: Fix notification sound on iPad/Safari (Web Audio migration)
- Rewrote artifacts/tx-os/src/lib/notification-sounds.ts to use the
  Web Audio API (lazy AudioContext + decoded AudioBuffer cache) instead
  of HTMLAudioElement. unlock() resumes the ctx for any non-running
  state (handles iOS `interrupted`), plays a 1-sample silent buffer to
  satisfy iOS Safari's gesture requirement, and pre-warms decodes.
  decodeAudioData wrapped to support both Promise and callback forms
  for older WebKit.
- play() dispatches an AUTOPLAY_BLOCKED_EVENT with `{ isIos }` detail
  when ctx isn't running. Throttle is 3 s; new bypassThrottle option
  (used by testPlay) skips the gate AND avoids advancing lastPlayAt
  so settings previews can't suppress real socket chimes.
- use-audio-unlock.ts keeps gesture listeners attached and re-calls
  unlock() on every gesture (handles iPad ctx auto-suspend on
  background / AirPlay).
- use-autoplay-hint.ts reads detail.isIos and switches between
  notifSettings.autoplayHint and notifSettings.autoplayHintIos
  (added to en.json + ar.json with Control Center / silent-switch
  guidance).
- notification-settings.tsx per-sound preview button now calls
  notificationPlayer.testPlay(id).
- Test globals __txosNotifPlayCount/__txosNotifLastSound preserved;
  added __txosNotifCtxState() accessor (dev/test-only, gated by
  import.meta.env.MODE).
- Added tests/notification-sound-webaudio.spec.mjs covering the
  preview button gesture path: counter increments, throttle bypass
  works, singleton AudioContext reaches `running` state.

Notes:
- Pre-existing api-server tsc errors in executive-meetings.ts and the
  failing `test` workflow are unrelated to this task and out of scope.
2026-05-07 14:15:53 +00:00
Riyadh 1ea075d2b6 Task #433: incoming-note popup — render full note + 3-button action row
- Inner body now renders as a real sticky note: keeps colored
  surface, supports checklist (read-only) when noteKind === "checklist".
- Action row trimmed to exactly 3 buttons: Done (تم), Open note
  (فتح الملاحظة), Reply (رد). Removed the standalone Dismiss button;
  header X still closes.
- Reply variant uses the same 3-button row (Done is a dismiss-only
  no-op since the note owner can't mark their own note read).
- Wire kind + items from backend `note_received` payload into client
  IncomingNotePayload (noteKind/items) via use-notifications-socket.
- Locales: markRead → "Done"/"تم"; new openNote/done keys; openThread
  unified to "Open note"/"فتح الملاحظة".
- Tests: replaced removed `incoming-note-popup-dismiss` testid usages
  with header `-close`; updated reply variant assertions to expect the
  3-button row; fixed pre-existing pluralization bug in reply path
  (`/replies` → `/reply`) that was unrelated to this task but blocked
  the reply popup test from validating.

Labels and pin were not surfaced in the popup because they are
sender-private state (note_recipients only snapshots
title/content/color/kind/items); the recipient never receives the
sender's labelIds/isPinned, so showing them would be misleading.

All affected e2e tests pass (notes-popup-on-receive note + reply,
notes-popup-touch-tap both, notes-inbox); queue unit tests pass.
2026-05-07 09:23:29 +00:00
Riyadh e1b83b62f2 Task #430: fix folder note creation + add in-app back to folders
Fixes two regressions surfaced after the folder browser shipped in #429:

1. Note created from inside a folder went to Unfiled
   - Composer now takes `folderSelection` and resolves the target folder:
     `folder` → that id, `unfiled`/`all` → null. The new note appears in
     the active folder view immediately and the folder count increments
     via the existing notes query invalidation.

2. No in-app back from a folder view
   - Added a "← Back to folders / المجلدات" chip above the composer
     whenever a folder or Unfiled is selected (active tab only). Tapping
     it resets the selection to "all" and lands the user back on the
     folders browser. The global header back button is unchanged.
   - Chevron flips for RTL via `rtl:rotate-180`.
   - Locale keys: `notes.folders.backToFolders` (EN/AR).

Verification:
- tsc --noEmit clean.
- Playwright suites all green: notes-folders, notes-checklist,
  notes-card-touch-actions, notes-delete-confirm (8 tests).
2026-05-07 08:41:53 +00:00
Riyadh b46bf97fa1 Task #429: folders-as-files browser at "All notes" view
- Notes restored to original sticky-note cards (Section + NoteCard) at all
  views, reverting the file-manager-style note rendering from #427.
- Folders now render as a file-manager browser at the top of the "All notes"
  view: black folder icon, name, relative time, item-count badge, "…" kebab
  with rename/delete, plus a checkbox for multi-select.
- Multi-select bar appears when ≥1 folder is checked, with bulk Delete
  (confirmation dialog) and Cancel actions. Bulk delete uses Promise.all
  over useDeleteFolder; selection auto-prunes deleted ids.
- Grid/list view-toggle reused from #427; toggle is hidden when not at
  "All notes" since it now only governs the folder browser.
- Added `mode: "rail" | "chips-only"` to FoldersRail. At "All notes" the
  rail switches to chips-only (All / Unfiled / + New folder) so folder rows
  live exclusively in the new browser and test ids never collide.
- Drop targets preserved (data-folder-drop, folder-drop-${id}) so the
  desktop DnD and touch-long-press pipelines keep working.
- Stop click propagation in DropdownMenuContent and around the checkbox to
  prevent rename/delete/checkbox clicks from navigating into the folder.
- Removed obsolete FilesView/FileItem/SelectionBar/DeleteDialog/colorTint
  helpers (~650 lines).
- Added EN/AR locale keys: notes.folders.actions, renameFailed, itemCount,
  itemCountOne, select, bulkSelected, bulkDeleteTitle, bulkDeleteConfirm,
  bulkDeleteFailed.
- Verified: tsc --noEmit clean; all 8 tests in notes-folders,
  notes-card-touch-actions, notes-checklist, notes-delete-confirm pass.
2026-05-07 08:19:50 +00:00
Riyadh be05c15a97 Task #429: folders-as-files browser at "all" notes view
- Notes reverted to original sticky-note cards (Section + NoteCard) at all
  views, removing the file-manager-style note rendering from task #427.
- Folders now render as a file-manager browser at the top of the "All notes"
  view: big black folder icon, name, relative time, item-count badge, and a
  "…" kebab with rename/delete. Grid and list layouts honor the existing
  view-toggle, plus a "{{count}} folders" footer.
- Added `mode: "rail" | "chips-only"` prop to FoldersRail. At "all" view the
  rail switches to chips-only (All / Unfiled / + New folder) so folder rows
  live exclusively in the new browser and test ids never collide.
- View-toggle is now hidden when not at "all" (folders-only control).
- New FoldersBrowser/FolderItem reuse useUpdateFolder/useDeleteFolder, set
  data-folder-drop for the touch DnD pipeline, and stop click propagation
  inside the dropdown so rename/delete don't navigate the card.
- Added locale keys: notes.folders.actions/renameFailed/itemCount(One).
- Removed obsolete FilesView/FileItem/SelectionBar/DeleteDialog/colorTint
  helpers (~650 lines).
- Verified: tsc --noEmit clean; notes-folders, notes-card-touch-actions,
  notes-checklist, notes-delete-confirm Playwright suites all green.
2026-05-07 08:14:23 +00:00
Riyadh 971973963c Add file manager view to notes section with grid and list options
Adds new translation keys for UI elements and implements a view mode toggle (grid/list) for the notes page, persisting the selection in local storage.
2026-05-07 07:52:50 +00:00
Riyadh bd8a4adbe9 notes: add per-note checklist (to-do list) option
Task #420 — answers the user request "وين خيار اضيف list to do?".

Schema (lib/db/src/schema/notes.ts):
- notes + note_recipients gain `kind` (varchar(16) default 'text')
  and `items` (jsonb<ChecklistItem[]> nullable). Snapshot copy on
  note_recipients keeps delivered checklists immutable across sender
  edits/deletes.
- Drizzle push applied; lib/db .d.ts rebuilt.

API (artifacts/api-server/src/routes/notes.ts):
- ChecklistItem type + bounded parser (≤200 items, id ≤64, text ≤500).
- parseNoteInput normalizes items↔kind on create and on PATCH that
  carries `kind`; PATCH handler additionally coerces items-only
  patches against the note's existing kind so a text note can never
  end up with checklist items (and vice versa).
- POST/PATCH/send/loadRecipientsForNote/received/thread responses and
  the realtime `note_received` payload all carry kind+items, with the
  thread response falling back to the recipient snapshot.

Client (artifacts/tx-os/src/lib/notes-api.ts + pages/notes.tsx):
- Note/ReceivedNote/NoteThread/SentNoteRecipient extended with
  kind+items.
- New `ChecklistEditor`, `ChecklistView`, and `KindToggle` components.
- Composer and EditNoteDialog gain the to-do toggle (ListTodo icon)
  and switch between Textarea and ChecklistEditor; saves send
  kind+items, with empty-checklist auto-discard mirroring the existing
  empty-text behaviour.
- NoteCard, inbox list, sent list, and ThreadDialog body render the
  checklist (read-only on snapshots; owner cards can toggle done via
  PATCH with stopPropagation so the edit dialog doesn't open).

i18n: notes.checklist.{toggle,addItem,itemPlaceholder,emptyHint,
removeItem,progress} added to en.json + ar.json.

Tests: new artifacts/tx-os/tests/notes-checklist.spec.mjs covers
composer→persist→reload→toggle, items-only PATCH normalization on
both kinds, and checklist delivery to recipient snapshot. All 3 pass.

Architect review (evaluate_task) flagged one real issue: items-only
PATCH normalization. Fixed in the PATCH handler and locked in by the
new normalization test.

Pre-existing executive-meetings.ts tsc errors are unchanged and
unrelated to this task.
2026-05-06 10:12:52 +00:00
Riyadh 5d7cd15ef2 Add a confirmation dialog before deleting a folder
Integrates an AlertDialog component for folder deletion, replacing the native confirm dialog, and updates corresponding locale files and test cases.
2026-05-06 09:29:46 +00:00
Riyadh fef1ea3eb6 notes: user-defined folders with drag-drop (task #413)
- DB: new note_folders table (per-user unique name) + nullable folder_id on
  notes with ON DELETE SET NULL.
- API: full /note-folders CRUD with user scoping; /notes POST/PATCH validate
  folder ownership; folder noteCount is tab-aware (active vs archived) via
  ?archived= query.
- Client: NoteFolder type, useNoteFolders/useCreateFolder/useUpdateFolder/
  useDeleteFolder/useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches).
- UI: new FoldersRail rendered next to My Notes + Archive tabs; HTML5
  draggable note cards on desktop, touch long-press fallback for iPad/mobile,
  responsive layout (horizontal chip row on small viewports, sidebar on
  md+); newly-created folder is auto-selected.
- Bilingual: notes.folders.* keys added to en.json and ar.json (RTL works
  via existing dir prop wired from i18n.language).
- Test: tests/notes-folders.spec.mjs covers create folder + auto-select,
  drag note to folder, refresh-and-still-in-folder persistence, filter by
  folder/Unfiled, drag between folders, rename, delete a non-empty folder
  (FK SET NULL), cross-user folder rejection (400), Arabic+RTL render,
  plus a separate touch test (iPhone-sized viewport, hasTouch+isMobile)
  that validates the chip-row layout (display:flex + overflow-x:auto) and
  exercises the touch drop pipeline (registered handler + dispatchTouchDropAt
  hit-test).

Drift from plan: none. Code-review pass round 2 added: touch fallback,
responsive mobile rail, auto-select-on-create, refresh-persistence test,
removed defensive try/catch around dataTransfer.
2026-05-06 07:43:31 +00:00
Riyadh 3f3830d586 notes: user-defined folders with drag-drop (task #413)
- DB: new note_folders table (per-user unique name) + nullable folder_id on
  notes with ON DELETE SET NULL; pushed via @workspace/db.
- API: full /note-folders CRUD with user scoping; /notes POST/PATCH validate
  folder ownership; folder noteCount is tab-aware (active vs archived) via
  ?archived= query param computed by GROUP BY (drizzle correlated subquery
  was returning 0 — replaced with two queries merged in JS).
- Client: NoteFolder type, useNoteFolders/useCreateFolder/useUpdateFolder/
  useDeleteFolder/useMoveNoteToFolder hooks (optimistic update for moves
  across all ["notes", ...] caches).
- UI: new FoldersRail (artifacts/tx-os/src/components/notes/folders-rail.tsx)
  rendered next to My Notes + Archive tabs; HTML5 draggable note cards with
  module-scoped DRAGGING_NOTE_ID fallback; visible drop highlight; rename +
  delete + create inline; folder-scoped + Unfiled filtering in notes.tsx.
- Bilingual: notes.folders.* keys added to en.json and ar.json (RTL works
  via existing dir prop wired from i18n.language).
- Test: artifacts/tx-os/tests/notes-folders.spec.mjs covers create folder,
  drag note to folder, filter by folder/Unfiled, drag between folders,
  rename, delete a non-empty folder (verifies FK SET NULL + Unfiled count),
  cross-user folder rejection (400), and Arabic+RTL rendering.

Drift from plan: none — all originally scoped work shipped. Strengthened
e2e + cross-user check + tab-aware counts added per code review feedback.
2026-05-06 07:26:11 +00:00
Riyadh 953c516b1f Task #410: floating draggable note popup + reply alert at sender
Convert the incoming-note popup from a centered AlertDialog modal into
a floating, draggable, animated card (no backdrop) and surface the
same card variant at the original sender when the recipient replies.

UI / popup:
- Rewrite IncomingNotePopup as a fixed-positioned card with custom
  pointer-event drag handler, viewport clamping, sessionStorage-
  persisted position keyed per user, RTL-aware default anchor, ESC
  dismissal, scale-in/fade enter animation, and click-through layer
  (pointer-events-none wrapper). Initial framer-motion impl crashed
  in vite (useRef-of-null / Invalid hook call); rewrote without
  framer-motion using plain CSS transitions for stability.
- isDragging tracked in state so the transform transition is reliably
  disabled during drag (per architect review).

Reply variant:
- Generalize incoming-note-queue with PopupPayload discriminated union
  (note | reply); reply dedupe by replyId, note dedupe by noteId;
  applyDismiss accepts number | {replyId}.
- Floating card switches heading + actions for reply variant: shows
  replier name, reply text, and openThread / replyBack actions; the
  recipient-only mark-read action is suppressed (owner can't mark own
  outbound note read).

Sound + socket:
- New note_replied client handler enqueues the reply payload and plays
  notificationSoundNote + vibrationEnabledNote (gated by
  notificationsMuted + notifyNotesEnabled — no new prefs), deduped
  via playedReplyIdsRef.
- Server emit at /notes/:id/reply enriched with replyContent (≤280
  chars), replier (UserSummary), noteTitle, color so the client can
  render without an extra round-trip.

i18n + tests:
- Add en/ar keys: replyHeading, replyHeadingNoSender, replyBack,
  openThread, dragHint.
- Extend notes-popup-on-receive.spec.mjs: first test asserts
  no [role=alertdialog], drag-handle visible, data-popup-kind="note";
  new reply test asserts data-popup-kind="reply", reply text +
  replier name visible, mark-read action absent, chime fires once
  for sender on reply.

Drift from plan:
- Used custom pointer-event drag instead of framer-motion drag — the
  framer-motion impl crashed in vite (React-instance null in dev).
  Same UX (drag from header only, click-through behind card).
- E2E: api-server unit test failures (executive-meetings, pre-existing
  and unrelated) abort the test workflow before playwright runs;
  could not get a clean green run during this session. Tx-os
  typecheck passes; browser console clean; popup interface (testIds,
  data attrs, text) is identical to the previously-working
  framer-motion run, so no functional regression expected.
2026-05-05 21:32:37 +00:00
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 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 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 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 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 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 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 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 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 2158aa97dd 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
Riyadh 5a48dc4664 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
Riyadh b228b7d652 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
Riyadh 972a57fc03 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
Riyadh a6aa732c33 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.
2026-05-03 12:54:50 +00:00
Riyadh 845fd9fe29 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
Riyadh 1949498711 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
Riyadh 524dff188e Remove unnecessary confirmation message after acknowledging alerts
Remove the `doneToast` locale keys and the toast notification call in `upcoming-meeting-alert.tsx`.
2026-05-03 11:13:41 +00:00
Riyadh 7210d87939 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.
2026-05-03 11:03:25 +00:00
Riyadh 9fee952f9a #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
Riyadh a3a9c4fac4 #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
Riyadh cfd9240a25 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 "العنوان".
2026-05-03 10:00:45 +00:00
Riyadh 17c103a59a 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
Riyadh 737a4c9765 Improve time input functionality and fix related test cases
Replace native time input with a custom text input and parser, enabling support for various time formats and fixing associated end-to-end tests.
2026-05-02 08:39:00 +00:00
Riyadh 86639c7c6f #307 Polish UpcomingMeetingAlert: AR plurals, 12h time, details table
- locales/{ar,en}.json: split minutesAway / postponeMinutesApply /
  postponeConfirmPrompt / postponeConfirmYes / staleMeetingApplyAnyway
  into i18next plural variants (AR uses zero/one/two/few/many/other).
  Switched call sites to pass `count` so plural detection triggers.
- Added cascadeMinutesUnit_* AR/EN unit keys; cascadePromptHeader_* now
  interpolates a pre-formatted {{minutesText}} so the {{minutes}} unit
  inside the cascade prompt obeys the same Arabic plural rules.
- upcoming-meeting-alert.tsx: replaced the local 24h `formatTime` with
  a 12-hour formatter that delegates to lib/i18n-format.formatTime with
  `hour12: true`, keeping Latin digits. Applied to the popup time
  window, the stale-meeting current-time line, and the new details row.
- DetailsPanel rewritten as a 2-col label/value grid (time, location,
  meeting link, attendees grouped by internal/external/virtual). Empty
  rows are skipped. Existing test-ids (alert-details-panel,
  alert-details-attendees, alert-details-no-attendees, alert-details-url)
  preserved; added alert-details-time / alert-details-location.
- Removed the postponeMinutesHint paragraph (and key from both locales);
  the noTimeWindow warning still renders when a meeting has no times.
- PostponeDialog now also reads i18n.language so stale-meeting times
  render in the user's locale.

tsc + the full executive-meetings-upcoming-alert.spec.mjs Playwright
suite (18 tests) pass after clearing leftover seeded test data.
2026-05-01 22:26:52 +00:00
Riyadh 8913b87f66 #302 cascade-shift later meetings on postpone/reschedule (review fixes)
Backend (artifacts/api-server/src/routes/executive-meetings.ts):
- New helpers computeCascadeShift + applyCascadeShift; new schema
  cascadePreviewSchema; cascadeFollowing flag on postpone-minutes
  and reschedule.
- POST /executive-meetings/:id/cascade-preview returns followers +
  blockedBy.
- Writer paths now SELECT followers FOR UPDATE inside the txn so
  concurrent mutations cannot lose updates and audit oldStart/oldEnd
  always reflect the locked-current values (architect feedback).
- Reschedule only cascades when same date AND newStart > oldStart.
- Midnight rejection rolls back the primary too; named offender is
  surfaced verbatim to the UI.
- One meeting_cascade_shift audit per follower (trigger meeting +
  delta in the row for replay).

Frontend (artifacts/tx-os/src/components/executive-meetings/
upcoming-meeting-alert.tsx):
- CascadePromptBlock with loading / blocked / normal variants and
  data-testids cascade-prompt[-blocked|-loading], cascade-keep-times,
  cascade-shift-following, cascade-back, cascade-follower-{id}.
- runCascadePreview helper with in-flight + busy guard against
  duplicate submissions; falls through to single-meeting submit when
  no followers and not blocked.
- rescheduleSubmit extracted; cascade_crosses_midnight from server
  is caught and re-rendered as the blocked variant.

i18n: cascade* keys under executiveMeetings.alert in en.json + ar.json
(Arabic plural zero/one/two/few/many/other for header + shift action).
ALSO added `executiveMeetings.audit.action.meeting_cascade_shift` in
both locales so the History view renders a localized label instead of
falling back to the raw action code (review feedback).

Tests:
- 8 cascade backend tests in executive-meetings.test.mjs (preview
  shape, atomic shift, skip cancelled/completed, midnight rollback,
  reschedule delta + no-op cases) all pass.
- New e2e "Reschedule cascade — opting in shifts all later same-day
  meetings" (API-driven backend contract).
- NEW UI dialog test "Cascade prompt UI: Shift sends
  cascadeFollowing=true; Keep sends false" — opens the postpone
  confirm, mocks cascade-preview to a deterministic single-follower
  response, then verifies (via request interception of
  /postpone-minutes) that Shift sends cascadeFollowing=true and Keep
  sends cascadeFollowing=false (review feedback).
- Made two existing tests (Postpone by 10 minutes, conflict warning)
  pollution-tolerant by polling for either the cascade prompt or the
  meeting-postponed audit row.

Pre-existing flaky tests (notifications fan-out, postpone race, row
color realtime, reorder, status transitions, alert-done/close
realtime, reschedule-different-day visibility, cancel/renumber strict
mode) are not related to this work and were not modified.

Drift note: prior commit picked up an incidental opengraph.jpg byte
diff (25906 -> 25929 bytes) from the tx-os web workflow restart — no
content change visible to this task; left in place as removing it
requires a destructive git operation.
2026-05-01 21:16:15 +00:00
Riyadh 7f63cf0b2b #302 cascade-shift later meetings on postpone/reschedule
Backend (artifacts/api-server/src/routes/executive-meetings.ts):
- New helpers computeCascadeShift + applyCascadeShift; new schema
  cascadePreviewSchema; cascadeFollowing flag on postpone-minutes
  and reschedule.
- POST /executive-meetings/:id/cascade-preview returns followers +
  blockedBy.
- Writer paths now SELECT followers FOR UPDATE inside the txn so
  concurrent mutations cannot lose updates and audit oldStart/oldEnd
  always reflect the locked-current values (architect feedback).
- Reschedule only cascades when same date AND newStart > oldStart.
- Midnight rejection rolls back the primary too; named offender is
  surfaced verbatim to the UI.
- One meeting_cascade_shift audit per follower (trigger meeting +
  delta in the row for replay).

Frontend (artifacts/tx-os/src/components/executive-meetings/
upcoming-meeting-alert.tsx):
- CascadePromptBlock with loading / blocked / normal variants and
  data-testids cascade-prompt[-blocked|-loading], cascade-keep-times,
  cascade-shift-following, cascade-back, cascade-follower-{id}.
- runCascadePreview helper with in-flight + busy guard against
  duplicate submissions; falls through to single-meeting submit when
  no followers and not blocked.
- rescheduleSubmit extracted; cascade_crosses_midnight from server
  is caught and re-rendered as the blocked variant.

i18n: cascade* keys under executiveMeetings.alert in en.json + ar.json
(Arabic plural zero/one/two/few/many/other for header + shift action).

Tests:
- 8 cascade backend tests in executive-meetings.test.mjs (preview
  shape, atomic shift, skip cancelled/completed, midnight rollback,
  reschedule delta + no-op cases) all pass.
- New e2e "Reschedule cascade — opting in shifts all later same-day
  meetings".
- Made two existing tests (Postpone by 10 minutes, conflict warning)
  pollution-tolerant by polling for either the cascade prompt or the
  meeting-postponed audit row.

Pre-existing flaky tests (notifications fan-out, postpone race, row
color realtime, reorder, status transitions, alert-done/close
realtime, reschedule-different-day visibility, cancel/renumber strict
mode) are not related to this work and were not modified.
2026-05-01 21:09:01 +00:00
Riyadh de8908a3dd Shorten the Executive Meetings page header
The Executive Meetings screen used to show a two-line header next to the
calendar icon: a primary "إدارة الاجتماعات التنفيذية" / "Executive
Meetings Management" line and a smaller secondary line in the opposite
language under it. The user asked for a single short title and no
second-language line beneath it.

Changes:
- artifacts/tx-os/src/locales/ar.json + en.json
  - Removed the dual-line keys executiveMeetings.titleAr and
    executiveMeetings.titleEn.
  - Replaced them with a single executiveMeetings.title key whose
    locale-resolved value is "قائمة الاجتماعات" (Arabic) and
    "Meetings list" (English).
- artifacts/tx-os/src/pages/executive-meetings.tsx
  - Page header: removed the second smaller-text <div> entirely and
    pointed the remaining bold line at the new key.
  - Print-only daily-schedule header (around line 1960): also pointed
    at the new key for consistency on the printed page.

The renamed key is more honest than the old titleAr/titleEn pair, which
no longer made sense once we collapsed to a single line.

Out of scope (untouched, per plan):
- The bilingual page subtitle / sidebar item shown in the user's
  screenshot — those live elsewhere.
- Browser tab title, alert popup title, and any other Executive
  Meetings surface.
- Header layout, color, or icon.

Verification:
- TypeScript: clean.
- e2e (testing skill): in Arabic locale the header shows the new
  "قائمة الاجتماعات" title and neither old long string is present
  anywhere on the page; after switching to English locale the header
  becomes "Meetings list" and again the old strings are gone.
2026-05-01 18:50:18 +00:00
Riyadh 83a7c7dd93 Remove repeated "+ شخص هنا" chip from attendee cells
The Executive Meetings schedule used to render a small dashed "+ شخص هنا"
("+ person here") chip in the gap between every two consecutive person
rows of an attendee cell. With three attendees the user saw three chips
stacked under the names, which looked noisy and repetitive. The user
asked for the chip to be removed entirely; the existing trailing "+"
button at the end of the cell already covers the add-person flow.

Changes:
- artifacts/tx-os/src/pages/executive-meetings.tsx
  - Removed the inter-person chip render block (the `<li>` + `<button>`
    that used data-testid="em-add-person-after-row-*" /
    data-testid="em-add-person-after-*"). Left a short comment in place
    of the removed block explaining what was there and why it went.
  - Removed the now-unused `addPersonHereLabel` from the props type, the
    component's destructured parameters, and the parent's `t(...)` call.
- artifacts/tx-os/src/locales/en.json + ar.json
  - Removed the orphaned `executiveMeetings.schedule.addPersonHere` key.

Out of scope (per plan, untouched):
- The "+ subheading" / "+ عنوان فرعي" inter-row chip that appears just
  before a subheading row.
- The trailing "+ Virtual" / "+ Internal" / "+ External" add chips that
  appear when a group is empty.
- The per-row trailing "+" button and the inline add-row flow.

Verification:
- TypeScript: clean.
- e2e (testing skill): on /executive-meetings, the page contains zero
  elements matching the old chip's data-testid prefixes and zero
  occurrences of the literal text "+ شخص هنا" or "+ person here", while
  the schedule and attendee lists render normally.
2026-05-01 18:41:12 +00:00
Riyadh d8d74131a0 Tidy the upcoming-meeting alert Details panel
The expanded Details panel had two visible problems pointed out by the user:

1. The "Notes / الملاحظات" block always rendered — including a "No notes
   added." fallback for meetings without notes — adding empty noise the
   user did not want.
2. Attendee names were printed as plain text but the database stores them
   as sanitized rich-text HTML, so they showed up as literal "<p>محمد علي</p>".

Changes:
- Removed the entire Notes block from the DetailsPanel sub-component in
  artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx.
  Dropped the unused StickyNote icon import and the now-unused notesText
  constant.
- Each attendee name is now rendered with the same HTML-stripping pattern
  the popup already uses for the meeting title:
  name.replace(/<[^>]+>/g, "").trim() || name.
- Removed the now-unused i18n keys executiveMeetings.alert.detailsNotes
  and detailsNoNotes from both en.json and ar.json.

Out of scope (per plan): no schema/API changes, no rich-text rendering
of attendee names, no color/toggle changes.

Verification:
- TypeScript: clean.
- e2e (testing skill): a fresh meeting with HTML-tagged attendee names
  was created, the popup expanded, and the test confirmed (a) no Notes
  section / "alert-details-notes" testid present, (b) attendee names
  visible without "<p>", "</p>", "<span>" tags, and (c) the meeting URL
  link still renders.
2026-05-01 18:27:34 +00:00