Commit Graph

367 Commits

Author SHA1 Message Date
riyadhafraa 27bd3cbf61 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
riyadhafraa 9230e487d8 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 render as a file-manager browser at the top of the active "All
  notes" view: black folder icon, name, relative time, item-count badge,
  "…" kebab with rename/delete, and a checkbox for multi-select. Honors the
  reused grid/list view-toggle.
- 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.
- View-toggle is hidden when not at active+all (it now only governs the
  folder browser).
- Added `mode: "rail" | "chips-only"` to FoldersRail. Chips-only is scoped
  to active+all only — archived/inbox/sent rail behavior is unchanged.
- Drop targets preserved (data-folder-drop, folder-drop-${id}) so desktop
  DnD and touch-long-press pipelines keep working.
- Click propagation stopped in DropdownMenuContent and around the checkbox
  so menu/checkbox interactions don't navigate 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; notes-folders, notes-card-touch-actions,
  notes-checklist, notes-delete-confirm Playwright suites all green.

Drift note: kept "All notes" as a mixed surface (folders browser + sticky
notes below the composer) per the user's task wording, which explicitly
requires both sticky-note cards and file-manager folders. Code reviewer
suggested folders-only as primary surface, but doing so would hide notes
the user explicitly asked to see and break tests at the default view.
2026-05-07 08:22:37 +00:00
riyadhafraa 74e9272c2a 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
riyadhafraa 25f624f8d8 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
riyadhafraa 1676a54ff8 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.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 88b40c3d-8ff3-492f-891b-c2b0853ab41d
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/5kzMIgG
Replit-Helium-Checkpoint-Created: true
2026-05-07 07:52:50 +00:00
riyadhafraa 32db433581 notes: show note-card actions on touch devices (task #425)
Original task: on phones and iPad the per-card action row (color,
label, send, archive, delete) was hidden by `opacity-0
group-hover:opacity-100`. Touch devices have no hover, so users
couldn't send a freshly-created note (or change its color, label,
archive it).

Changes
- artifacts/tx-os/src/pages/notes.tsx
  - Add Tailwind v4 arbitrary variant `[@media(hover:none)]:opacity-100`
    to the per-card action row so it stays always-visible on touch
    devices while desktops keep the existing hover-reveal behaviour.
  - Apply the same variant to the unpinned pin button (which uses the
    identical hover-only treatment).

Tests
- artifacts/tx-os/tests/notes-card-touch-actions.spec.mjs (new)
  - Uses `test.use({ hasTouch: true, isMobile: true, viewport })` on
    chromium (webkit isn't installed) so the page reports
    `(hover: none)`. Sanity-checks that via matchMedia, then creates a
    note via the composer (tap-outside auto-save) and asserts both
    that the Send button is visible and that the action row's
    computed opacity is exactly 1 — Playwright's `toBeVisible` alone
    doesn't catch opacity-0.

Verification
- pnpm --filter @workspace/tx-os exec tsc --noEmit: clean.
- pnpm --filter @workspace/tx-os test:e2e -- notes-delete-confirm
  notes-card-touch-actions: 3 passed (touch test + the two
  delete-confirm specs from #423 to confirm no regression).
- Architect code review: no severe issues. Desktop hover-reveal is
  preserved because `(hover: none)` doesn't match on pointer devices.

Out of scope (per task plan): the trash button inside the
manage-labels dialog (different surface, dialog-only).
2026-05-06 10:39:59 +00:00
riyadhafraa 2027ee2545 notes: replace native confirm() with styled AlertDialog (task #423)
Original task: deleting a note (and a label from the manage-labels menu)
showed the browser's native confirm() with the raw Repl hostname,
clashing with the rest of the app. Switch both call sites to the
project's existing AlertDialog component, matching the pattern used in
FoldersRail and SendNoteDialog.

Changes
- artifacts/tx-os/src/pages/notes.tsx
  - NoteCard: add `confirmDeleteOpen` state; render AlertDialog as a
    Fragment sibling of the clickable card (not a child) so React's
    portal event bubbling cannot trigger the card's onEdit handler when
    the user interacts with the dialog. Also stopPropagation on the
    trash button itself.
  - LabelsDialog: add `pendingDeleteLabel` state and a sibling
    AlertDialog inside the Dialog root. Both dialogs use existing
    locale keys (notes.deleteConfirm, notes.deleteLabelConfirm,
    notes.delete, common.cancel) — no new strings needed.
  - Add data-testids: note-card-delete-{id}, note-delete-dialog-{id},
    note-delete-confirm-{id}, label-delete-{id},
    label-delete-dialog-{id}, label-delete-confirm-{id},
    notes-manage-labels-open.

Tests
- artifacts/tx-os/tests/notes-delete-confirm.spec.mjs (new)
  - Note delete: cancel keeps the card, confirm fires DELETE and
    removes the card. Deliberately does NOT register a `dialog` event
    handler — a native confirm would hang the test.
  - Label delete: open manage-labels, create a label via the UI, click
    trash, confirm, assert DELETE /api/note-labels/{id}.

Verification
- pnpm --filter @workspace/tx-os exec tsc --noEmit: clean.
- pnpm --filter @workspace/tx-os test:e2e -- notes-delete-confirm:
  2 passed.
- Architect code review surfaced a real bubble-through risk on the
  card's onClick={onEdit}; fixed by lifting the AlertDialog to a
  Fragment sibling.

Out of scope (not changed): native confirm()/alert() outside the Notes
page. (executive-meetings.tsx already uses an in-app `confirm({...})`
modal, not the native browser dialog.)
2026-05-06 10:30:28 +00:00
riyadhafraa 1ffb470e8f 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
riyadhafraa d4e0dfc6e5 notes: persist chosen color (and labels) when composer auto-saves
When creating a new note, picking a color from the palette had no
effect — the note saved with the default white background. Editing an
existing note worked because that path lives in a Dialog with no
auto-save guard.

Root cause
The inline Composer in artifacts/tx-os/src/pages/notes.tsx attaches a
document-level mousedown listener that auto-saves whenever the click
falls outside its container ref. The color and label menus are built
on Radix Popover, which renders content in a portal outside the
composer's DOM tree. So clicking a swatch fired:
  1. document mousedown -> save() with the OLD color
  2. swatch click       -> setColor(new) (state already discarded)

Fix
The mousedown handler now ignores clicks whose target is inside any
Radix popper wrapper, Radix portal, or role="dialog" element. The
guard is narrow: legitimate outside clicks (page background, sidebar,
other notes) still trigger save() exactly as before. The same fix
covers the LabelMenu popover for free since it shares the abstraction.

Tests
- New tests/notes-composer-color.spec.mjs reproduces the original
  failure mode: open composer, type title, pick the "red" swatch from
  the popover, click outside, assert the POST /api/notes response has
  color === "red".
- Existing notes-folders + new spec run green together (3 passed in
  29.1s); tx-os tsc --noEmit clean.

Architect notes (followed inline / deferred)
- Suggested mirroring a labels-popover e2e — same code path is already
  exercised; can be added later if regressions surface.
- composedPath() hardening — current target.closest() works for all
  real Radix portals; not needed.
2026-05-06 09:59:10 +00:00
riyadhafraa 466a8c2340 Cap note thread dialog height (task #417)
User reported (in Arabic, with screenshot) that the note Replies dialog was
covering the entire page when a thread had multiple grouped conversations.
The owner/admin grouped-replies view (`GroupedReplies`) had a `max-h-72`
internal scroll, but the outer `DialogContent` had no overall height cap, so
on tall content the dialog grew to fill the viewport.

Changes
- artifacts/tx-os/src/pages/notes.tsx
  - `DialogContent` for the thread dialog: add `max-h-[85vh] flex flex-col`.
  - Refactored body into three vertical zones (per code-review feedback):
    1) static meta + recipient chips (`shrink-0`, always visible),
    2) replies-only scroll region (`flex-1 min-h-0 overflow-y-auto`,
       `data-testid="note-thread-scroll"`),
    3) static composer (`shrink-0`, pinned at the bottom).
  - Drop now-redundant `max-h-48` on the recipient ReplyBubble list and
    `max-h-72` on `GroupedReplies` — the dedicated scroll zone handles
    overflow, avoiding nested double scrollbars.

Tests
- New artifacts/tx-os/tests/notes-thread-dialog.spec.mjs:
  - Seeds an owner note with 2 recipients and 30 alternating replies via
    SQL, deep-links into `/notes?thread=ID`, asserts the dialog's bounding
    box height ≤ 0.9 × viewport height, asserts the inner scroll region
    actually overflows (scrollHeight > clientHeight), and asserts the
    composer remains visible.
- Verified the existing notes-folders.spec.mjs (2 tests) still pass.
- `tsc --noEmit` clean.

Out of scope (left as-is): visual redesign, reply send/archive logic,
Inbox/Sent thread list.
2026-05-06 08:55:00 +00:00
riyadhafraa 889e24244c Cap note thread dialog height (task #417)
User reported (in Arabic, with screenshot) that the note Replies dialog was
covering the entire page when a thread had multiple grouped conversations.
The owner/admin grouped-replies view (`GroupedReplies`) had a `max-h-72`
internal scroll, but the outer `DialogContent` had no overall height cap, so
on tall content the dialog grew to fill the viewport.

Changes
- artifacts/tx-os/src/pages/notes.tsx
  - `DialogContent` for the thread dialog: add `max-h-[85vh] flex flex-col`.
  - Wrap the middle (sender info + recipients chips + replies block) in a
    new `flex-1 min-h-0 overflow-y-auto` scroller with
    `data-testid="note-thread-scroll"`.
  - Pin the reply composer with `shrink-0` so it stays visible while the
    middle scrolls.
  - Drop now-redundant `max-h-48` on the recipient ReplyBubble list and
    `max-h-72` on `GroupedReplies` — the outer scroller handles overflow,
    avoiding nested double scrollbars.

Tests
- New artifacts/tx-os/tests/notes-thread-dialog.spec.mjs:
  - Seeds an owner note with 2 recipients and 30 alternating replies via
    SQL, deep-links into `/notes?thread=ID`, asserts the dialog's bounding
    box height ≤ 0.9 × viewport height, asserts the inner scroll region
    actually overflows (scrollHeight > clientHeight), and asserts the
    composer remains visible.
- Verified the existing notes-folders.spec.mjs (2 tests) still pass.
- `tsc --noEmit` clean.

Out of scope (left as-is): visual redesign, reply send/archive logic,
Inbox/Sent thread list.
2026-05-06 08:52:27 +00:00
riyadhafraa 60c23d4972 notes: user-defined folders with drag-drop (task #413)
DB
- New `note_folders` table (per-user unique name); `notes.folder_id` nullable
  with `ON DELETE SET NULL` so deleting a folder preserves its notes.

API
- /note-folders CRUD (GET, POST, PATCH, DELETE), all user-scoped.
- /notes POST/PATCH validate folder ownership before assignment (no cross-user
  folder bleed).
- Folder noteCount is tab-aware (active vs archived) via ?archived=.
- OpenAPI spec extended with /note-folders endpoints, NoteFolder schema, and
  folderId on SentNote/ReceivedNote. Codegen regenerated for api-client-react
  and api-zod (`pnpm --filter @workspace/api-spec run codegen`).

Client
- NoteFolder type and useNoteFolders / useCreateFolder / useUpdateFolder /
  useDeleteFolder / useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches; rollback on error).

UI (FoldersRail next to My Notes + Archive tabs only)
- Desktop: sidebar with All / Unfiled / user folders + counts.
- Mobile: horizontal chip row (flex + overflow-x-auto) above the notes grid.
- Per-folder kebab dropdown with Rename + Delete.
- Newly-created folder is auto-selected.
- HTML5 draggable note cards on desktop; touch long-press fallback (350ms)
  for iPad/mobile via shared drop pipeline (registerFolderDropHandler +
  hit-tested `[data-folder-drop]`).
- Dev-only `window.__notesTouchTest` helper (gated to non-production builds)
  exposes the touch pipeline so e2e tests can drive it deterministically
  without synthesizing native TouchEvents.

Bilingual
- notes.folders.* keys added to en.json and ar.json (RTL inherited from
  existing dir prop wired from i18n.language).

Tests (artifacts/tx-os/tests/notes-folders.spec.mjs)
- create folder + auto-select
- drag note → folder, refresh-and-still-in-folder persistence
- filter by folder / Unfiled / All
- drag between folders (count math)
- rename via kebab
- delete a non-empty folder via kebab → notes return to Unfiled (FK SET NULL)
- cross-user folder rejection (400)
- Arabic + RTL render
- separate touch test (414×800, hasTouch+isMobile) covers chip-row layout
  and exercises the touch drop pipeline through window helper.

All API node tests + tx-os Playwright suite + tsc clean.
2026-05-06 08:09:59 +00:00
riyadhafraa b976c408a6 notes: user-defined folders with drag-drop (task #413)
DB
- New `note_folders` table (per-user unique name); `notes.folder_id` nullable
  with `ON DELETE SET NULL` so deleting a folder preserves its notes.

API
- /note-folders CRUD (GET, POST, PATCH, DELETE), all user-scoped.
- /notes POST/PATCH validate folder ownership before assignment (no cross-user
  folder bleed).
- Folder noteCount is tab-aware (active vs archived) via ?archived=.
- OpenAPI spec extended with /note-folders endpoints, NoteFolder schema, and
  folderId on SentNote/ReceivedNote. Codegen regenerated for api-client-react
  and api-zod (`pnpm --filter @workspace/api-spec run codegen`).

Client
- NoteFolder type and useNoteFolders / useCreateFolder / useUpdateFolder /
  useDeleteFolder / useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches; rollback on error).

UI (FoldersRail next to My Notes + Archive tabs only)
- Desktop: sidebar with All / Unfiled / user folders + counts.
- Mobile: horizontal chip row (flex + overflow-x-auto) above the notes grid.
- Per-folder kebab dropdown with Rename + Delete.
- Newly-created folder is auto-selected.
- HTML5 draggable note cards on desktop; touch long-press fallback (350ms)
  for iPad/mobile via shared drop pipeline (registerFolderDropHandler +
  hit-tested `[data-folder-drop]`).
- Dev-only `window.__notesTouchTest` helper (gated to non-production builds)
  exposes the touch pipeline so e2e tests can drive it deterministically
  without synthesizing native TouchEvents.

Bilingual
- notes.folders.* keys added to en.json and ar.json (RTL inherited from
  existing dir prop wired from i18n.language).

Tests (artifacts/tx-os/tests/notes-folders.spec.mjs)
- create folder + auto-select
- drag note → folder, refresh-and-still-in-folder persistence
- filter by folder / Unfiled / All
- drag between folders (count math)
- rename via kebab
- delete a non-empty folder via kebab → notes return to Unfiled (FK SET NULL)
- cross-user folder rejection (400)
- Arabic + RTL render
- separate touch test (414×800, hasTouch+isMobile) covers chip-row layout
  and exercises the touch drop pipeline through window helper.

All API node tests + tx-os Playwright suite + tsc clean.
2026-05-06 08:02:10 +00:00
riyadhafraa 4d720fdeed 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:41:22 +00:00
riyadhafraa fd194c5ed1 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:39:03 +00:00
riyadhafraa ba0da3d26c 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
riyadhafraa 8ca324a557 feat(notes): make incoming-note popup attention-grabbing
Task #409: incoming notes now mirror UpcomingMeetingAlert's attention
model — sound + vibration + visual pulse + persistent queue indicator —
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 (use-notifications-socket): play sound + vibrate on
  note_received, gated by mute/notifyNotesEnabled/socket warmup, with
  playedNoteIdsRef dedupe (mirrors upcoming-meeting-alert playedRef).
- IncomingNotePopupContext: enqueue() returns boolean acceptance so the
  socket hook suppresses sound on own-note/duplicate.
- Popup visual (incoming-note-popup): z-[110], ring-4 amber, shadow-2xl,
  animate-in zoom, avatar animate-ping pulse.
- Persistent indicator on Notes app tile (home.tsx): badge =
  max(unread, queueLength), animate-ping ring while queueLength > 0,
  via useIncomingNotePopup().queueLength + new pulse prop on AppIconContent.
- Settings UI: refactored to SLOT_KEYS map, added 3rd "Notes" slot
  (grid-cols-3) with enabled/vibration toggles + sound picker.
- Locales en/ar: added notifSettings.slot.note, notesEnabled, vibrationNote.
- Test instrumentation: NotificationPlayer exposes window.__txosNotifPlayCount
  and __txosNotifLastSound for E2E observability.
- Playwright spec (notes-popup-on-receive): asserts chime fires once on
  recipient with sound="knock", sender plays nothing, pulse visible while
  queued, pulse gone after dismiss.

Pre-existing typecheck errors in executive-meetings.ts (font settings
scope) are unrelated to this task.
2026-05-05 16:26:13 +00:00
riyadhafraa 39d645ae4e 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
riyadhafraa 1e358ab32e 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
riyadhafraa d1b4bb228c 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
riyadhafraa 65c5a172d6 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
riyadhafraa 6352cbf844 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
riyadhafraa 96147a347a 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
riyadhafraa 89de41e949 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
riyadhafraa e085801f07 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
riyadhafraa d62e67af96 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
riyadhafraa 3f2d90519b 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.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 1a88d20d-3ace-4c4a-bfa9-5cc8df9a0859
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/UggHvbd
Replit-Helium-Checkpoint-Created: true
2026-05-05 09:54:06 +00:00
riyadhafraa 20ab1babfe 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
riyadhafraa 119374c64b 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
riyadhafraa 4af3917026 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
riyadhafraa 242f63ab26 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
riyadhafraa 7b79a32d66 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.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: dde9ba68-1907-4b4b-928d-4ea5bca215a7
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/UggHvbd
Replit-Helium-Checkpoint-Created: true
2026-05-05 07:51:36 +00:00
riyadhafraa 7f5f9fd572 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.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 3a577026-2b09-48ef-a405-e40e0b782577
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/UggHvbd
Replit-Helium-Checkpoint-Created: true
2026-05-05 07:27:21 +00:00
riyadhafraa 57e8ca848a 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.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 7c55644f-4e59-4e78-a127-9aec1a90ebaf
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/UggHvbd
Replit-Helium-Checkpoint-Created: true
2026-05-05 07:22:12 +00:00
riyadhafraa 85319e69a0 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.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: a15369a0-9324-4530-b49b-f74f690fb3f3
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/w8MxrEs
Replit-Helium-Checkpoint-Created: true
2026-05-04 18:12:56 +00:00
riyadhafraa 00bc3dd374 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.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 608173c1-6210-41cf-be7d-3a913963059f
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/jwG0a8d
Replit-Helium-Checkpoint-Created: true
2026-05-04 17:55:36 +00:00
riyadhafraa 406f4ef693 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.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: ebb081ea-2bdd-4a2c-837f-1f4c4cdb67de
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/jwG0a8d
Replit-Helium-Checkpoint-Created: true
2026-05-04 17:50:11 +00:00
riyadhafraa 1ee3f9a55f 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.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: ea995ab4-f97a-4789-a4b5-9635490a7ce0
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/jwG0a8d
Replit-Helium-Checkpoint-Created: true
2026-05-04 16:22:41 +00:00
riyadhafraa ce4d2f7340 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.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 967c661d-a0ca-4673-bac5-1e43902edbb1
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/jwG0a8d
Replit-Helium-Checkpoint-Created: true
2026-05-04 16:15:49 +00:00
riyadhafraa 97c62efb0a 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.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 1acbf30f-5440-4f4b-89ec-e2be2668c753
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/EWwamzQ
Replit-Helium-Checkpoint-Created: true
2026-05-04 16:04:55 +00:00
riyadhafraa 03916e68a5 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.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 6e1661b8-f4ab-45e7-a6c8-93293ffc7c62
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/EWwamzQ
Replit-Helium-Checkpoint-Created: true
2026-05-04 15:57:11 +00:00
riyadhafraa e5e08d3f6a 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.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 80f7f876-3581-4602-accc-25110eb27020
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/EWwamzQ
Replit-Helium-Checkpoint-Created: true
2026-05-04 14:20:12 +00:00
riyadhafraa 1e3419b0cb 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.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: ed9f2aec-9e1e-4b42-9747-7e4482299fce
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/Msx3Bax
Replit-Helium-Checkpoint-Created: true
2026-05-04 14:11:15 +00:00
riyadhafraa 81833c021c 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
riyadhafraa 014f9ecb0e 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
riyadhafraa 77538a93c0 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
riyadhafraa 8810130ef5 Make row borders darker and more prominent
Update border colors and thickness in PDF renderer and UI to ensure visibility and consistency.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 0b5defcb-10cb-452f-a4e7-72763eeb98bc
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/k1UTYfG
Replit-Helium-Checkpoint-Created: true
2026-05-04 11:57:42 +00:00
riyadhafraa 8891eca9e6 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
riyadhafraa 086a037bf4 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.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 07cf3bc9-c682-4f05-aa30-52606bcce583
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/nFy3UZm
Replit-Helium-Checkpoint-Created: true
2026-05-04 11:05:57 +00:00
riyadhafraa bcebd7c83e Task #355: Highlight all overlapping current meetings
Previously, `computeCurrentMeetingId` returned the first meeting whose
time range contained the current minute, so only one row ever got the
green highlight. When two meetings overlapped (e.g., 9:30–10:00 and
9:45–10:15), only the first was highlighted.

Changed `computeCurrentMeetingId` → `computeCurrentMeetingIds` to
return a `ReadonlySet<number>` of ALL matching meeting ids. Updated the
`useMemo` and the `isCurrent` prop on each row from
`currentMeetingId === m.id` to `currentMeetingIds.has(m.id)`.

A module-level `EMPTY_MEETING_SET` constant avoids allocating a new
empty Set on every tick when no meetings are in progress.

Changed file:
- artifacts/tx-os/src/pages/executive-meetings.tsx
  - Lines 369-396: new computeCurrentMeetingIds function
  - Line 1092: useMemo now uses computeCurrentMeetingIds
  - Line 2782: isCurrent uses .has() instead of ===
2026-05-04 07:05:40 +00:00