Commit Graph

245 Commits

Author SHA1 Message Date
riyadhafraa f53f7307da Task #489: row-wide drag rotates meeting content; time + daily numbers anchored
Backend
- New POST /api/executive-meetings/rotate-content (zod-validated) rotates
  ONLY meeting content through fixed (start_time, end_time, daily_number)
  slots. Same-date enforced; per-meeting expectedUpdatedAt → 409 stale;
  incomplete day (missing visible row) → 400.
- ExecutiveMeetingsRotateContentBody added in lib/api-zod (manual.ts).
- 6 backend tests cover happy path, stale, different_dates, 401, 403,
  incomplete_day. Existing /swap-times tests still pass.

Frontend (artifacts/tx-os)
- Whole <tr> is now the drag handle (the dedicated GripVertical button is
  retired). useSortable is gated on canMutate; safeRowDragListeners
  filters drags whose target is an interactive descendant (button, input,
  edit/time cells, row-actions, bulk-select). useSortable `attributes`
  are spread only when canMutate so view-mode rows stay clickable
  (otherwise aria-disabled blocked the popover trigger).
- onRowDragEnd → rotateContent(fromId, toId): optimistic patch reassigns
  each chronological slot's tuple to the new occupant; rolls back + toast
  on failure.
- Quick-actions popover now contains only Postpone (#486 Move up/down
  buttons removed).

Tests (artifacts/tx-os)
- New tests/executive-meetings-row-drag.spec.mjs: drags Alpha → Charlie
  position by # cell, asserts rotate-content fires and slots stay
  anchored.
- tests/executive-meetings-row-quick-actions.spec.mjs: drops up/down
  cases, keeps Postpone + skip-surfaces + viewer.
- tests/executive-meetings-schedule-features.spec.mjs: two legacy grip
  drag tests rewritten to drag the row body and target /rotate-content
  (the legacy /reorder route + tests are intentionally untouched).

Drift / notes
- Architect flagged a medium-severity hardening note: rotate-content
  FOR UPDATE locks orderedIds but not the day-scope completeness query.
  Out of #489 scope; no follow-up created (proposeFollowUpTasks was
  already consumed on #486).
2026-05-11 12:14:08 +00:00
riyadhafraa 16a818b716 #486: Executive Meetings row click → quick-actions popover
Clicking any meeting row on the Executive Meetings schedule (gated only
on canMutate, not editMode) opens a small popover with Move up / Move
down / Postpone. Move up/down swap only the (startTime, endTime) tuple
between the clicked meeting and its chronological neighbour on the same
date — the Time column stays visually anchored to its row position.

Backend
- POST /executive-meetings/swap-times: transactional swap with FOR
  UPDATE row locking, optimistic-lock conflict shape (stale_meeting +
  conflict payload), date/time-window guards, audit logging, and
  renumberDayByStartTime + day-changed broadcast.
- Zod schema in lib/api-zod/src/manual.ts.

Frontend
- Shared lib/api-json.ts JSON helper.
- ScheduleSection.swapTimes does an optimistic (startTime, endTime)
  swap against the day query cache and rolls back on failure (mirrors
  the existing inline-edit UX).
- MeetingRow uses Popover/PopoverAnchor with skip rules: ARIA roles
  (button/checkbox/switch/combobox/dialog) and em-time-* / em-edit-* /
  em-row-grip / em-row-actions data-testid prefixes do NOT open the
  popover.
- PostponeDialog reused from upcoming-meeting-alert.tsx.

Tests
- Backend swap-times: happy path, stale_meeting (409), different_dates
  (400), no_time_window (400), unauth (401), viewer-no-mutate (403),
  malformed-timestamp (400) — all 7 pass.
- Hardened expectedUpdatedAt zod schema to z.string().datetime() so
  malformed tokens fail at validation with a controlled 400 instead of
  bubbling up as a 500.
- E2E: Move up swap, edge-disable states (solo / first / middle /
  last), Postpone 5-min chip end-to-end, click-exclusion on grip /
  time cell / row-actions — all 4 pass. Each test uses its own future
  date to avoid cross-test pollution.

Code review approved on second pass. Pre-existing failures in other
suites (executive-meetings reorder, font-settings, notes-share,
service-orders) are unrelated to this task and predate it.

Follow-ups proposed: #487 (keyboard a11y on the popover), #488 (edit-
mode test gaps).
2026-05-11 11:06:13 +00:00
riyadhafraa 02b3b374b0 #486: Executive Meetings row click → quick-actions popover
Clicking any meeting row on the Executive Meetings schedule (gated only
on canMutate, not editMode) opens a small popover with Move up / Move
down / Postpone. Move up/down swap only the (startTime, endTime) tuple
between the clicked meeting and its chronological neighbour on the same
date — the Time column stays visually anchored to its row position.

Backend
- POST /executive-meetings/swap-times: transactional swap with FOR
  UPDATE row locking, optimistic-lock conflict shape (stale_meeting +
  conflict payload), date/time-window guards, audit logging, and
  renumberDayByStartTime + day-changed broadcast.
- Zod schema in lib/api-zod/src/manual.ts.

Frontend
- Shared lib/api-json.ts JSON helper.
- ScheduleSection.swapTimes does an optimistic (startTime, endTime)
  swap against the day query cache and rolls back on failure (mirrors
  the existing inline-edit UX).
- MeetingRow uses Popover/PopoverAnchor with skip rules: ARIA roles
  (button/checkbox/switch/combobox/dialog) and em-time-* / em-edit-* /
  em-row-grip / em-row-actions data-testid prefixes do NOT open the
  popover.
- PostponeDialog reused from upcoming-meeting-alert.tsx.

Tests
- Backend swap-times: happy path, stale_meeting (409), different_dates
  (400), no_time_window (400), unauth (401), viewer-no-mutate (403) —
  all 6 pass.
- E2E: Move up swap, edge-disable states (solo / first / middle /
  last), Postpone 5-min chip end-to-end, click-exclusion on grip /
  time cell / row-actions — all 4 pass. Each test uses its own future
  date to avoid cross-test pollution.

Code review approved on second pass. Pre-existing failures in other
suites (executive-meetings reorder, font-settings, notes-share,
service-orders) are unrelated to this task and predate it.

Follow-ups proposed: #487 (keyboard a11y on the popover), #488 (edit-
mode test gaps).
2026-05-11 11:03:24 +00:00
riyadhafraa b1b77395d0 #486 Executive Meetings: row-click quick actions popover (Move up / Move down / Postpone)
Clicking any meeting row on the schedule now opens a small popover with three
quick actions: Move up, Move down, and Postpone. Gated only on canMutate (NOT
editMode) per spec.

Backend
- New POST /executive-meetings/swap-times endpoint (artifacts/api-server/src/
  routes/executive-meetings.ts). FOR UPDATE locks both rows by ascending id to
  avoid deadlocks, optimistic-lock check via expectedUpdatedAt{A,B} (returns
  409 stale_meeting + conflict.lastActor — same shape PostponeDialog
  understands), guards different_dates and no_time_window, swaps only
  (startTime, endTime), audits each row as `meeting_swap_times`, calls
  renumberDayByStartTime so the # column matches the new chronological order,
  and broadcasts emitExecutiveMeetingsDayChanged.
- New zod body schema ExecutiveMeetingsSwapTimesBody in lib/api-zod/src/manual.ts.

Frontend
- New shared lib/api-json.ts (ApiError + apiJson) extracted from upcoming-
  meeting-alert.tsx so the page can reuse the same fetch/error contract.
- upcoming-meeting-alert.tsx exports PostponeDialog so the page can mount it
  for quick-action postpone without duplicating UI.
- artifacts/tx-os/src/pages/executive-meetings.tsx: Schedule wires
  swapTimes/quickMoveUp/quickMoveDown/postpone state, computes per-day
  neighbours via meetingNumbersById, and mounts a single page-level
  PostponeDialog. MeetingRow wraps `<tr>` in a Popover/PopoverAnchor; row
  onClick opens the popover with skip rules for buttons/inputs/contenteditable
  and ARIA roles (button/checkbox/switch/combobox/dialog) plus testid
  prefixes (em-row-grip/-actions/-select, em-edit-*, em-merge-edit-*, em-time-*)
  so the time cell's inline editor and other affordances don't collide.
- en/ar locales gain executiveMeetings.quickActions.{label,moveUp,moveDown,postpone}.

Tests
- artifacts/api-server/tests/executive-meetings-swap-times.test.mjs (new):
  happy path, 409 stale_meeting (with conflict actor), 400 different_dates,
  400 no_time_window. Each scenario uses a distinct far-future date to avoid
  daily_number races with seeded data.
- artifacts/tx-os/tests/executive-meetings-row-quick-actions.spec.mjs (new):
  drives the date input, verifies row click → popover, Move up swap reflected
  in DB, and Postpone item opens the dialog.

Code review (architect) flagged an edit-mode conflict where the time cell
(role=button div) would also bubble to the row handler; fixed by adding ARIA
role and em-time-* skip rules. Architect also flagged keyboard-trigger gap
and edit-mode test gaps — proposed as follow-ups.

Other test failures in the repo (executive-meetings reorder, font-settings,
notes-share, service-orders) pre-date this change and are unrelated.
2026-05-11 10:55:34 +00:00
riyadhafraa f937362868 Task #463: @dnd-kit notes drag + reorder
- Replace HTML5+touch drag with @dnd-kit; MouseSensor (desktop, 8px)
  + TouchSensor (iPad, 200ms long-press) so input sources never overlap.
- Add sort_order column; ORDER BY asc(sortOrder), updatedAt desc.
- PATCH /notes/reorder: strict isPinned boolean check, bucket+permission
  scoped, all writes wrapped in db.transaction for atomicity.
- PATCH /notes/:id stamps sort_order = min-1 on folder/pin bucket change.
- Client useReorderNotes (PATCH) with optimistic cache update.
- handleDragEnd builds reorder payload from FULL bucket (owner notes
  or shared-folder bucket via ref), not the filtered subset, so hidden
  siblings under search/label filters keep their order.
- Drag guarded for view-only contexts: source must be owned OR in
  editable shared bucket; folder-drop additionally requires isOwn.
- SharedFolderView publishes its data.notes via bucketRef when viewer
  has edit permission, enabling correct reorder in shared folders.
- Layout fix at narrow viewport: rail stacks above notes
  (flex-col md:flex-row) so iPad portrait drag has proper bbox.
- Playwright tests: notes-folders.spec.mjs both desktop pointer drag
  and touch long-press drag pass (33s).
- OpenAPI codegen skipped: notes-api.ts is hand-written.
- Out of scope (pre-existing failures): executive-meetings reorder/font,
  notes-share PATCH 403/404, groups-crud rollback.
2026-05-10 13:17:00 +00:00
riyadhafraa daa4f6c038 Task #463: @dnd-kit notes drag + reorder
- Replace HTML5+touch drag with @dnd-kit (PointerSensor distance:8,
  TouchSensor delay:200/tol:8) matching home.tsx pattern.
- Add sort_order column to notes; ORDER BY asc(sortOrder), updatedAt desc.
- New PATCH /notes/reorder endpoint: strict isPinned boolean validation,
  bucket+permission scoped, all writes in db.transaction for atomicity.
- PATCH /notes/:id stamps sort_order = min-1 on folder/pin bucket change.
- Client useReorderNotes hook with optimistic cache update.
- handleDragEnd builds reorder payload from FULL bucket (owner notes or
  shared-folder bucket via ref), not the filtered/search subset, so
  hidden siblings retain stable order.
- SharedFolderView publishes its data.notes via bucketRef when viewer
  has edit permission, enabling correct reorder in shared folders.
- Layout fix at narrow viewport: rail stacks above notes
  (flex-col md:flex-row) so iPad portrait drag has proper bbox.
- Playwright tests: notes-folders.spec.mjs both desktop pointer drag
  and touch long-press drag pass (32s).
- OpenAPI codegen skipped: notes-api.ts is hand-written.
- Out of scope (pre-existing failures): executive-meetings reorder/font,
  notes-share PATCH 403/404, groups-crud rollback.
2026-05-10 13:10:01 +00:00
riyadhafraa 4e00a20bda notes(#454): per-recipient view/edit folder sharing
- Schema: noteFolderShares.permission ('view'|'edit', default 'view').
  Project uses drizzle-kit push (no migration files); existing rows
  pick up the default automatically on next push.
- 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.
  - Distinct socket events: note-folder-shared (added),
    note-folder-share-updated (permission flipped),
    note-folder-unshared (removed). Permission events carry the new
    permission so clients can react.
- 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: handlers for shared / share-updated / unshared all
    invalidate the right query keys for live UI flips.
  - Locales: shareDescriptionPerm, permissionView/Edit, canEdit
    (en + ar).
- Tests: new permission roundtrip test in notes-share.test.mjs covering
  view-rejects-write, edit-can-write, editor-cannot-unfile,
  editor-create-stamped-to-owner, downgrade/upgrade flips.
- Pre-existing executive-meetings.ts type errors are out of scope.
2026-05-10 11:11:45 +00:00
riyadhafraa 1aad708cb7 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
riyadhafraa 7384711705 notes: address review feedback for folder sharing (Task #445)
Re-applied review fixes on top of the live folder-sharing implementation:

- PATCH/DELETE /notes/:id now do an id-only lookup and return an explicit
  403 when the caller is not the owner (previously returned an ambiguous
  404). This makes the read-only contract for shared-folder recipients
  precise instead of silently masquerading as "not found".
- PUT /note-folders/:id/shares rejects self-share with 400
  "Cannot share folder with yourself" instead of silently filtering the
  caller's id out of the recipient set.
- GET /note-folders/shared-with-me now includes a `noteCount` (live count
  of the owner's non-archived notes in the folder) computed via SQL
  subquery; SharedFolder TS type updated; folders rail renders the count
  next to each shared folder.
- SharedFolderView accepts the page's `search` value and filters the
  read-only list client-side over title + content + checklist item text,
  matching the owner's search experience. Empty state distinguishes
  "no notes" from "no matches".
- Added GET /notes?sharedFolderId=:id as a spec-aligned alias that
  returns the owner's live notes inside a folder shared with the caller
  (gated by an active row in note_folder_shares).
- Updated the stale comment block above the share routes that claimed
  recipient writes "just 404"; documents the new 403 contract.

Deviations / out of scope:
- Project uses `drizzle-kit push` (no migrations directory); no SQL
  migration file was added.
- Pre-existing TS errors in artifacts/api-server/src/routes/executive-meetings.ts
  remain; unrelated to Task #445.
- "Strict reject for nonexistent recipient ids in share PUT" left as
  optional follow-up (currently silently dropped per existing behavior).
2026-05-09 11:13:56 +00:00
riyadhafraa a4949983f3 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
riyadhafraa a430812998 Task #438: collaborative checklists + meeting alert animation parity
Backend
- New POST /notes/:id/checklist/:itemId/toggle endpoint. Owner or any
  active (non-archived) recipient can flip an item's done flag.
- Wrapped in a DB transaction with SELECT ... FOR UPDATE on the live
  note row so concurrent toggles from multiple collaborators can't
  lose each other's updates. Live notes.items + every
  note_recipients.items snapshot are mirrored atomically in the same
  tx.
- Emits note_checklist_changed to the audience minus the actor with
  the full updated items array so receivers can patch state without
  an extra fetch.

Frontend
- useToggleChecklistItem mutation hook with optimistic updates across
  notes/sent/received/thread caches and rollback on error.
- Incoming-note popup checklist is now interactive with a local
  override for instant visual feedback (popup renders from socket
  payload, not from the query cache).
- Thread checklist toggles are gated by effective permission (owner,
  admin, or non-archived recipient) to mirror server auth.
- Socket handler for note_checklist_changed invalidates relevant
  query keys AND patches the open popup payload directly via a new
  IncomingNotePopupContext.updateChecklistItems action so
  collaborators' open popups stay in sync.

Meeting alert animation parity (upcoming-meeting-alert.tsx)
- Added scale-95 -> scale-100 + fade entrance with the same spring
  cubic-bezier the note popup uses, retriggered per eligible meeting.
- Replaced the 1px border with a ring-4 colored frame driven by
  alertPrefs.accent (via boxShadow so the color is dynamic).
- Added an animate-ping accent halo around the drag-handle icon to
  match the popup's avatar pulse.

Tests
- artifacts/api-server/tests/notes-checklist-toggle.test.mjs (7 tests:
  owner/recipient toggle + mirroring, 403 non-recipient, 403
  archived, 400 non-checklist, 404 unknown item, 400 invalid body).
- artifacts/tx-os/tests/notes-popup-checklist-collab.spec.mjs (e2e:
  recipient ticks an item from the popup, server + sender snapshot
  both reflect the change).

Code review (architect) findings addressed:
- (critical) lost-update race -> tx with row lock.
- (critical) non-atomic snapshot mirror -> same tx.
- (critical) open popup didn't re-render on socket fanout ->
  updateChecklistItems context action.
- UI permission parity for archived recipients -> thread gates
  onToggle.
2026-05-07 11:24:57 +00:00
riyadhafraa dcaebb38f5 notes: re-send fires popup + notification again
Previously POST /api/notes/:id/send used onConflictDoNothing on
(noteId, recipientUserId) and only iterated `inserted` rows for
side-effects, so a re-send to the same recipient was silent — no
note_received socket event, no notification — even though the API
returned 200. Reported by the user: "I sent again to rh, why
didn't it reach them?"

Fix: after the idempotent insert, load the existing recipient
rows for any recipients that were skipped, and run the
notification + socket emit loop over BOTH inserted and existing
rows. The DB row is still not duplicated (snapshot semantics
preserved), but the recipient now sees the popup and gets a fresh
notification on every re-send.

Verified:
- @workspace/api-server tests/notes-share.test.mjs — 12/12 pass
  (including "re-sending to the same recipient does not duplicate
  the recipient row").
- tx-os e2e notes-popup-on-receive — 3/3 pass.
2026-05-07 10:32:25 +00:00
riyadhafraa 9937cb1b08 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 17d62187-995b-45bd-98b9-6f10e3ed6096
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/XE1wdRt
Replit-Helium-Checkpoint-Created: true
2026-05-06 11:25:48 +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 f562749214 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 07:57:57 +00:00
riyadhafraa 9fd4671327 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 07:52:54 +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 4258aa092c 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
riyadhafraa aaadd9b520 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
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 5b13fe16f5 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.

Four 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.

Round 2:
- POST /notes/:id/reply: owner is now allowed to reply too.
- Added GET /notes/:id as alias of /notes/:id/thread.
- Added OpenAPI ops for the Notes routes; 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:
- Archived tab now shows BOTH "My archived notes" and "Archived inbox".
- Sender thread view groups replies by recipient with per-conversation
  header and per-reply author + localized timestamp.
- Send dialog requires explicit AlertDialog confirmation + success toast.
- Realtime toast strings moved off hardcoded EN/AR to i18n keys.
- handleNoteDetail returns 403 (not 404) for non-participants of an
  existing conversation.
- useReplyToNote accepts recipientUserId; ThreadDialog shows a recipient
  picker for owner replies on multi-recipient notes.

Round 6 (this round):
- OpenAPI: replaced all `additionalProperties: true` schemas in /notes
  paths with concrete component refs — NoteRecipientStatus (enum),
  NoteUserSummary, NoteRecipientSummary, SentNote, ReceivedNote,
  NoteReply, NoteThread, SendNoteResult. Re-ran orval; api-zod and
  api-client-react regenerated cleanly.
- Trimmed narrative comments in artifacts/api-server/src/routes/notes.ts
  (handleNoteDetail, /sent, /received, /send, /read, /reply).
- Schema FK + enum policy: kept noteId as plain integer (no FK) on
  note_recipients/note_replies — this is required so recipient
  snapshots survive sender deletion. Status remains a varchar but is
  constrained at the API boundary by the new NoteRecipientStatus enum
  schema and TS string-literal union on both server and client.
- Migrations: this monorepo uses `drizzle-kit push` exclusively
  (lib/db has no migrations directory; drizzle.config.ts is push-only;
  package.json defines only push/push-force scripts). No migration
  files committed by design.

Round 5: admin authorization fix
- handleNoteDetail: admin bypass is now evaluated BEFORE the
  "non-participant 403" branch. When the sender has deleted the live
  note row but recipient snapshots remain, an admin GET /notes/:id now
  returns 200 with thread payload assembled from a fallback recipient
  snapshot (instead of 403).
- New regression test: "admin can read a note whose sender deleted
  their copy (recipient snapshot remains)".

Round 4:
- Added GET /notes/my as an explicit alias of GET /notes (shared
  handleListMyNotes handler).
- Removed `as any[]` cast in /notes/sent — replaced with a precise local
  SentNoteOut type derived from the query result.
- Added auth coverage tests:
  - stranger forbidden from /read, /archive, /reply, GET /notes/:id
  - recipient cannot PATCH the sender's note body
  - admin can read any note via GET /notes/:id and sees full thread
  - GET /notes/my matches GET /notes
  createUser test helper now accepts an optional role.

Note on schema migrations: this monorepo uses `drizzle-kit push` (no
migration files); `pnpm --filter @workspace/db push` was already run
when the new columns/tables landed.

Tests: 11 backend tests in notes-share.test.mjs + 1 e2e (notes-inbox)
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:26:23 +00:00
riyadhafraa 73c9953675 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.

Four 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.

Round 2:
- POST /notes/:id/reply: owner is now allowed to reply too.
- Added GET /notes/:id as alias of /notes/:id/thread.
- Added OpenAPI ops for the Notes routes; 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:
- Archived tab now shows BOTH "My archived notes" and "Archived inbox".
- Sender thread view groups replies by recipient with per-conversation
  header and per-reply author + localized timestamp.
- Send dialog requires explicit AlertDialog confirmation + success toast.
- Realtime toast strings moved off hardcoded EN/AR to i18n keys.
- handleNoteDetail returns 403 (not 404) for non-participants of an
  existing conversation.
- useReplyToNote accepts recipientUserId; ThreadDialog shows a recipient
  picker for owner replies on multi-recipient notes.

Round 5 (this round): admin authorization fix
- handleNoteDetail: admin bypass is now evaluated BEFORE the
  "non-participant 403" branch. When the sender has deleted the live
  note row but recipient snapshots remain, an admin GET /notes/:id now
  returns 200 with thread payload assembled from a fallback recipient
  snapshot (instead of 403).
- New regression test: "admin can read a note whose sender deleted
  their copy (recipient snapshot remains)".

Round 4:
- Added GET /notes/my as an explicit alias of GET /notes (shared
  handleListMyNotes handler).
- Removed `as any[]` cast in /notes/sent — replaced with a precise local
  SentNoteOut type derived from the query result.
- Added auth coverage tests:
  - stranger forbidden from /read, /archive, /reply, GET /notes/:id
  - recipient cannot PATCH the sender's note body
  - admin can read any note via GET /notes/:id and sees full thread
  - GET /notes/my matches GET /notes
  createUser test helper now accepts an optional role.

Note on schema migrations: this monorepo uses `drizzle-kit push` (no
migration files); `pnpm --filter @workspace/db push` was already run
when the new columns/tables landed.

Tests: 11 backend tests in notes-share.test.mjs + 1 e2e (notes-inbox)
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:18:45 +00:00
riyadhafraa 672cb6280c 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.

Four 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.

Round 2:
- POST /notes/:id/reply: owner is now allowed to reply too.
- Added GET /notes/:id as alias of /notes/:id/thread.
- Added OpenAPI ops for the Notes routes; 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:
- Archived tab now shows BOTH "My archived notes" and "Archived inbox".
- Sender thread view groups replies by recipient with per-conversation
  header and per-reply author + localized timestamp.
- Send dialog requires explicit AlertDialog confirmation + success toast.
- Realtime toast strings moved off hardcoded EN/AR to i18n keys.
- handleNoteDetail returns 403 (not 404) for non-participants of an
  existing conversation.
- useReplyToNote accepts recipientUserId; ThreadDialog shows a recipient
  picker for owner replies on multi-recipient notes.

Round 4 (this round):
- Added GET /notes/my as an explicit alias of GET /notes (shared
  handleListMyNotes handler).
- Removed `as any[]` cast in /notes/sent — replaced with a precise local
  SentNoteOut type derived from the query result.
- Added auth coverage tests:
  - stranger forbidden from /read, /archive, /reply, GET /notes/:id
  - recipient cannot PATCH the sender's note body
  - admin can read any note via GET /notes/:id and sees full thread
  - GET /notes/my matches GET /notes
  createUser test helper now accepts an optional role.

Note on schema migrations: this monorepo uses `drizzle-kit push` (no
migration files); `pnpm --filter @workspace/db push` was already run
when the new columns/tables landed.

Tests: 11 backend tests in notes-share.test.mjs + 1 e2e (notes-inbox)
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:14:25 +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 4e657a2d4d 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:42:43 +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 f8947c0bc7 Task #389: Notification sounds + vibration with per-user customization
DB
- Added user pref columns; vibration is per-channel
  (vibrationEnabledOrder + vibrationEnabledMeeting). Defaults: ding/chime
  sounds, vibration on, mute off. Volume uses the device system level —
  no persisted volume field.

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

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

Pre-existing TS errors in executive-meetings.ts (font_settings) and
pre-existing test workflow failures are unrelated.
2026-05-05 08:24:27 +00:00
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 14b4401a55 Thicken table borders to improve PDF visibility
Adjust border thickness in `pdf-html-renderer.ts` and `pdf-renderer.ts` from 1px to 1.5px to ensure navy borders are clearly rendered in PDF outputs.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: a36bb9eb-e52a-45dc-96a0-05e8a101630b
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:41:57 +00:00
riyadhafraa 47605e33a1 Update PDF table borders to dark navy color
Modify `pdf-html-renderer.ts` and `pdf-renderer.ts` to change table border colors from light gray to dark navy.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 5058c11b-d2a5-4f94-8797-c6595e0470d5
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:35:42 +00:00
riyadhafraa 6a65ece874 Update PDF filename generation to use Arabic day names
Corrected a variable name typo from `langRaw` to `lang` in the server-side PDF filename generation logic for `executive-meetings.ts`, ensuring consistent use of Arabic day names in the output filename.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: a6cafe17-82fd-43c4-b54f-d2d57f431b99
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:01:59 +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 116d2edd17 Adjust PDF footer layout to position elements correctly
Update CSS in pdf-html-renderer.ts to ensure 'recorded by' text is centered and the date is aligned to the left within the PDF footer.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 75779042-7169-4d9e-9699-fdf091cbe17b
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:12:17 +00:00
riyadhafraa d3410c06ca Center the title and move the recorded by text to the footer
Modify the header to center the title and relocate the "recorded by" text to the footer with a specific color.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: aee6d2cd-fb54-4d40-8202-0e09e4f3058c
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:04:40 +00:00
riyadhafraa 81acd695ad Improve PDF attendee and header formatting
Modify `buildAttendeesHtml` to use `htmlToSafeHtml` for preserving attendee name formatting, adjust PDF header layout for centered title and logo, and relocate "recorded by" text.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 4dd3128f-949f-491c-9f30-425e81521e82
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:01:25 +00:00
riyadhafraa da07d9665a Improve PDF generation by preserving text formatting and updating header layout
Introduce `htmlToSafeHtml` function to retain text formatting (color, bold) in PDFs and restructure the header layout, moving the logo to the right and title to the left.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 0e884234-17af-436a-a1a7-65cdf2c77dfe
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:51:45 +00:00
riyadhafraa 74d6fb0969 Make number cells in PDFs consistently bold
Add `font-weight: bold` to the `.num-cell` CSS class in the PDF renderer to match the web interface's styling.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 99c66f52-cb52-4783-8607-366e0e0777c7
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:34:49 +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 3d8b1a9036 Unify row color styling across all PDF table cells
Previously, when a meeting row had a color assigned (e.g. red), only the
number and meeting title cells received both the background fill and colored
border, while the attendees and time cells received only the colored border
(no background fill). This made the row styling inconsistent.

Changed attendees-cell and time-cell to use `coloredStyle` (background +
border) instead of `borderStyle` (border only), matching the number and
meeting cells. All four cells in a colored row now display uniformly.

Rows without a color assignment are unaffected (default gray border, no fill).

File changed: artifacts/api-server/src/lib/pdf-html-renderer.ts (lines 222-223)
2026-05-04 13:03:42 +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 02e1856f3d Task #377: Improve PDF cell spacing, line-height, and use system fontColor
Changes in artifacts/api-server/src/lib/pdf-html-renderer.ts:

1. Increased cell padding from 2px 4px to 10px 12px for both header
   and body cells for better readability
2. Increased line-height from 1.05 to 1.8 (body cells) and 1.2 to 1.6
   (header cells and body baseline)
3. Page title (h1) now uses system fontColor instead of hardcoded #000
4. Footer text now uses system fontColor for consistency
5. Empty-state text uses system fontColor with opacity instead of #666
6. Attendee items get direction-aware margin (12px) for better spacing
7. Logo header uses conditional min-height (55px when logo present,
   auto when absent) to prevent overlap and empty space
8. Footer date-line margin increased from 2px to 4px

Header background (#0B1E3F), header text (white), and border color
(#d1d5db) remain as branded defaults — no DB columns exist for these
yet (follow-up task #378 proposed for that).

No changes to web UI, schema, or legacy pdf-renderer.ts.
Tested: AR and EN PDFs generate successfully with correct sizes.
E2E test passed: login, PDF download (both languages), and UI verified.
2026-05-04 11:17:12 +00:00
riyadhafraa ea9217d483 Task #377: Improve PDF cell spacing, line-height, and use system fontColor
Changes in artifacts/api-server/src/lib/pdf-html-renderer.ts:

1. Increased cell padding from 2px 4px to 10px 12px (body cells) and
   3px 4px to 8px 12px (header cells) for better readability
2. Increased line-height from 1.05 to 1.8 (body cells) and 1.2 to 1.6
   (header cells and body baseline)
3. Page title (h1) now uses system fontColor instead of hardcoded #000
4. Footer text now uses system fontColor for consistency
5. Attendee items get direction-aware margin (12px) for better spacing
6. Logo header uses conditional min-height (55px when logo present,
   auto when absent) to prevent overlap and empty space
7. Footer date-line margin increased from 2px to 4px

Header background (#0B1E3F), header text (white), and border color
(#d1d5db) remain as branded defaults — no DB columns exist for these
yet (follow-up task #378 proposed for that).

No changes to web UI, schema, or legacy pdf-renderer.ts.
Tested: AR and EN PDFs generate successfully with correct sizes.
E2E test passed: login, PDF download (both languages), and UI verified.
2026-05-04 11:15:55 +00:00
riyadhafraa 446ed292b5 PDF row color: apply only to # and meeting columns, full row on merge
Task #375: Changed row coloring behavior in the HTML-table PDF renderer
so that normal rows only color the # (number) and الاجتماع (meeting)
cells, leaving الحضور (attendees) and الوقت (time) cells white.

When a row has merged columns (colspan), the entire row is colored
as before.

Implementation: `buildMeetingRow` now assigns `bgStyle` directly to
the first two cells' `style` field. `renderCells` takes an `allColored`
flag — true for merged rows (applies bgStyle to all cells), false for
normal rows (uses each cell's own style). The `<tr>` element no longer
carries the background style.

File changed: artifacts/api-server/src/lib/pdf-html-renderer.ts
2026-05-04 10:46:10 +00:00