459999a9e2236c7a70312e79d48171e66828107a
177 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
43a590277b |
#480 Show cascade-affected meetings as a numbered table
Replace the simple <ul> in the cascade-prompt panel inside the upcoming- meeting alert with a 4-column table: row index, the meeting's dailyNumber, title (truncate + title=), and "from → to". The new `meetingNumbersById` map is built once in UpcomingMeetingAlert from the already-loaded dayData and threaded through PostponeDialog into both CascadePromptBlock call sites (postpone-minutes and reschedule). Existing `cascade-followers-list` and `cascade-follower-<id>` testids are preserved on the table/rows so prior selectors keep matching. Adds 4 new i18n keys (cascadeColIndex/MeetingNumber/Title/Times) in ar.json and en.json. The legacy cascadeListItem key is left in place since it isn't worth a separate cleanup. Extends the existing "Cascade prompt UI: Shift sends cascadeFollowing" e2e test to assert the first row's index cell renders "1" and the meeting-number cell renders the seeded follower's dailyNumber. insertImminentMeeting now also returns dailyNumber so the assertion can read it. tsc clean; targeted cascade specs pass (3/3). |
||
|
|
a192734259 |
notes: bulk-select + bulk-delete on Sent tab (task #472)
- Add `useBulkDeleteSentNotes` hook that fans out parallel DELETE
/notes/:id calls via Promise.allSettled and returns {ok, failed}
so the UI can render a precise toast on partial failure. Caches
invalidated once via `invalidateNotesAndFolders`.
- NotesPage: page-level `sentSelectionMode` + `selectedSentIds`
Set, auto-reset when navigating away from the Sent tab.
- Header gets a Select / Cancel toggle, gated on
`view === "sent" && filteredSent.length > 0`.
- New sticky bulk-action bar above the sent list with count,
select-all/clear toggle, and Delete button.
- SentList accepts optional selectionMode/selectedIds/onToggle
props; cards swap onClick (open → toggle) and show a Checkbox
overlay + rose ring when checked.
- AlertDialog confirms before deleting; toast reports all-success,
partial, or all-failed using pluralized i18n keys.
- New i18n keys under `notes.bulk*` in ar.json + en.json.
No server changes (existing DELETE /notes/:id is reused).
Out of scope (per task spec): undo, bulk-archive, bulk endpoint,
multi-select on Active/Received/Archived tabs.
|
||
|
|
ab1d1e207e |
Task #461: Fix notification sounds on iPad (route via media channel)
Problem: User reported zero sound on iPad — even the "Test sound" button in notification settings produced silence. YouTube and other videos worked fine on the same iPad. Root cause: The app's notification player used the Web Audio API (AudioContext + BufferSource). On iOS Safari (iPad/iPhone), Web Audio plays through the **ringer channel**, which is silenced by: - The hardware silent switch (older iPads), - Control Center's mute icon, - Or simply having ringer volume at 0 (independent of media volume). HTMLAudioElement (the API behind <video> and YouTube) plays through the **media channel**, which is what users actually have turned up. Fix in artifacts/tx-os/src/lib/notification-sounds.ts: - Added an HTMLAudioElement playback engine alongside the existing Web Audio engine. Engine selection is one-time per session via the existing `detectIos()` helper (handles iPadOS-as-Mac masquerade). - iOS path: lazy `new Audio(url)` per sound id with preload="auto", primed in `unlock()` by play()-then-pause() at volume 0 inside the user gesture so each element is permanently allowed to play later out-of-gesture (iOS requirement). Real `play()` resets currentTime and plays at full volume; rejection of the play promise dispatches the existing autoplay-blocked hint. - Non-iOS path: unchanged Web Audio behavior preserved verbatim. - All public API preserved: `unlock()`, `play()`, `testPlay()`, `isUnlocked()`, `AUTOPLAY_BLOCKED_EVENT`. No changes needed to use-audio-unlock, use-autoplay-hint, use-notifications-socket, or notification-settings. - Throttling (3s burst-coalesce), bypass for testPlay, vibration, test-only observability globals, and isIos hint flag all preserved. - `__txosNotifCtxState` test hook now reports "running"/"suspended" on iOS based on `unlocked` flag (no AudioContext exists there). Locale updates (ar.json + en.json): - `notifSettings.autoplayHintIos` rewritten: now tells users to raise the **media** volume (same one that controls YouTube) instead of the old "turn off Silent Mute" advice — matches the new playback channel. Verification: tsc --noEmit clean. Pre-existing `test` workflow failure on unrelated executive-meetings.ts type errors is out of scope. Out of scope per task: no PWA, no push notifications, no settings UI redesign, no new sound files. Manual iPad QA needed before merging. |
||
|
|
e179addcd4 |
notes(#454): per-recipient view/edit folder sharing
- Schema: noteFolderShares.permission ('view'|'edit', default 'view').
- Server: resolveFolderEditAccess + emitFolderChanged helpers.
- POST/PATCH/DELETE/checklist now allow folder editors.
- Editors stamp notes with folder owner's userId; labels validated
against owner; PATCH editors blocked from unfile (folderId=null)
and cross-folder moves to non-matching owners.
- emitFolderChanged fires for owner AND editor mutations across
create / patch (old + new folder) / delete / checklist toggle.
- PUT /shares accepts both legacy recipientUserIds and new
recipients:[{userId,permission}]; diffs add/update/remove/perm-flip.
- GET /shares, /shared-with-me, /shared-notes return permission /
myPermission / readOnly.
- Client:
- notes-api types: FolderSharePermission, myPermission on
SharedFolder/SharedFolderView, permission on FolderShareRecipient,
readOnly:boolean on SharedFolderNote.
- useUpdateFolderShares takes recipients[].
- useCreate/Update/DeleteNote also invalidate ['note-folders'] so
actor's shared-folder rail badge stays fresh.
- FolderShareDialog: per-row checkbox + segmented View/Edit toggle.
- SharedFolderView: editor mode mounts Composer + NoteCard with
full edit/delete/archive affordances; Send hidden in editor
context (Composer + NoteCard hideSend prop) since /send is
owner-only.
- folders-rail: per-folder permission badge.
- socket: note-folder-shared also invalidates ['notes'] +
['note-folders'] for fanout to owner / other editors.
- Locales: shareDescriptionPerm, permissionView/Edit, canEdit
(en + ar).
- Pre-existing executive-meetings.ts type errors are out of scope.
|
||
|
|
1ff59263a9 |
Notes: live folder-sharing (read-only) — Task #445
Owner can share a whole folder with other users; recipients see it
under "Shared with me" in the folders rail and view notes read-only
(cannot edit, add, move, or delete).
- DB: new noteFolderSharesTable (folderId/recipientUserId, cascade,
unique idx). Pushed via drizzle-kit.
- API (artifacts/api-server/src/routes/notes.ts):
- GET/PATCH/POST /note-folders return sharedWithCount.
- GET /note-folders/shared-with-me, GET /note-folders/:id/shares,
PUT /note-folders/:id/shares (idempotent diff), DELETE
/note-folders/:id/shares/:userId, GET /note-folders/:id/shared-notes
(verifies recipient via noteFolderSharesTable, stamps readOnly).
- Emits note-folder-shared / note-folder-unshared on share changes.
- Existing note write paths remain owner-scoped → recipients can't
mutate owner notes.
- Frontend types/hooks (notes-api.ts): SharedFolder, FolderShareRecipient,
SharedFolderNote/View; useFolderShares, useUpdateFolderShares,
useSharedWithMeFolders, useSharedFolderNotes.
- FoldersRail: Share menu item, sharedWithCount badge, "Shared with me"
section, "shared-folder" selection kind, onShareFolder prop.
- notes.tsx: SharedFolderView (no Composer, read-only cards using
ChecklistView), FolderShareDialog (seeds existing recipients,
idempotent PUT on save), revocation fallback effect, dialog mount.
- use-notifications-socket.ts: subscribe to note-folder-shared /
-unshared → invalidate shared-with-me + open shared-folder notes for
live rail/view refresh.
- i18n: AR/EN keys for share/sharedBy/sharedByName/sharedWithCount/
sharedWithMe/readOnly/shareRevoked/sharedEmpty/shareSaved/shareFailed.
Pre-existing failing `test` workflow (executive-meetings type errors)
is unrelated and out of scope.
|
||
|
|
28b8e9e4fa |
Task #441: Fix notification sound on iPad/Safari (Web Audio migration)
- Rewrote artifacts/tx-os/src/lib/notification-sounds.ts to use the
Web Audio API (lazy AudioContext + decoded AudioBuffer cache) instead
of HTMLAudioElement. unlock() resumes the ctx for any non-running
state (handles iOS `interrupted`), plays a 1-sample silent buffer to
satisfy iOS Safari's gesture requirement, and pre-warms decodes.
decodeAudioData wrapped to support both Promise and callback forms
for older WebKit.
- play() dispatches an AUTOPLAY_BLOCKED_EVENT with `{ isIos }` detail
when ctx isn't running. Throttle is 3 s; new bypassThrottle option
(used by testPlay) skips the gate AND avoids advancing lastPlayAt
so settings previews can't suppress real socket chimes.
- use-audio-unlock.ts keeps gesture listeners attached and re-calls
unlock() on every gesture (handles iPad ctx auto-suspend on
background / AirPlay).
- use-autoplay-hint.ts reads detail.isIos and switches between
notifSettings.autoplayHint and notifSettings.autoplayHintIos
(added to en.json + ar.json with Control Center / silent-switch
guidance).
- notification-settings.tsx per-sound preview button now calls
notificationPlayer.testPlay(id).
- Test globals __txosNotifPlayCount/__txosNotifLastSound preserved;
added __txosNotifCtxState() accessor (dev/test-only, gated by
import.meta.env.MODE).
- Added tests/notification-sound-webaudio.spec.mjs covering the
preview button gesture path: counter increments, throttle bypass
works, singleton AudioContext reaches `running` state.
Notes:
- Pre-existing api-server tsc errors in executive-meetings.ts and the
failing `test` workflow are unrelated to this task and out of scope.
|
||
|
|
1ea075d2b6 |
Task #433: incoming-note popup — render full note + 3-button action row
- Inner body now renders as a real sticky note: keeps colored surface, supports checklist (read-only) when noteKind === "checklist". - Action row trimmed to exactly 3 buttons: Done (تم), Open note (فتح الملاحظة), Reply (رد). Removed the standalone Dismiss button; header X still closes. - Reply variant uses the same 3-button row (Done is a dismiss-only no-op since the note owner can't mark their own note read). - Wire kind + items from backend `note_received` payload into client IncomingNotePayload (noteKind/items) via use-notifications-socket. - Locales: markRead → "Done"/"تم"; new openNote/done keys; openThread unified to "Open note"/"فتح الملاحظة". - Tests: replaced removed `incoming-note-popup-dismiss` testid usages with header `-close`; updated reply variant assertions to expect the 3-button row; fixed pre-existing pluralization bug in reply path (`/replies` → `/reply`) that was unrelated to this task but blocked the reply popup test from validating. Labels and pin were not surfaced in the popup because they are sender-private state (note_recipients only snapshots title/content/color/kind/items); the recipient never receives the sender's labelIds/isPinned, so showing them would be misleading. All affected e2e tests pass (notes-popup-on-receive note + reply, notes-popup-touch-tap both, notes-inbox); queue unit tests pass. |
||
|
|
e1b83b62f2 |
Task #430: fix folder note creation + add in-app back to folders
Fixes two regressions surfaced after the folder browser shipped in #429: 1. Note created from inside a folder went to Unfiled - Composer now takes `folderSelection` and resolves the target folder: `folder` → that id, `unfiled`/`all` → null. The new note appears in the active folder view immediately and the folder count increments via the existing notes query invalidation. 2. No in-app back from a folder view - Added a "← Back to folders / المجلدات" chip above the composer whenever a folder or Unfiled is selected (active tab only). Tapping it resets the selection to "all" and lands the user back on the folders browser. The global header back button is unchanged. - Chevron flips for RTL via `rtl:rotate-180`. - Locale keys: `notes.folders.backToFolders` (EN/AR). Verification: - tsc --noEmit clean. - Playwright suites all green: notes-folders, notes-checklist, notes-card-touch-actions, notes-delete-confirm (8 tests). |
||
|
|
b46bf97fa1 |
Task #429: folders-as-files browser at "All notes" view
- Notes restored to original sticky-note cards (Section + NoteCard) at all views, reverting the file-manager-style note rendering from #427. - Folders now render as a file-manager browser at the top of the "All notes" view: black folder icon, name, relative time, item-count badge, "…" kebab with rename/delete, plus a checkbox for multi-select. - Multi-select bar appears when ≥1 folder is checked, with bulk Delete (confirmation dialog) and Cancel actions. Bulk delete uses Promise.all over useDeleteFolder; selection auto-prunes deleted ids. - Grid/list view-toggle reused from #427; toggle is hidden when not at "All notes" since it now only governs the folder browser. - Added `mode: "rail" | "chips-only"` to FoldersRail. At "All notes" the rail switches to chips-only (All / Unfiled / + New folder) so folder rows live exclusively in the new browser and test ids never collide. - Drop targets preserved (data-folder-drop, folder-drop-${id}) so the desktop DnD and touch-long-press pipelines keep working. - Stop click propagation in DropdownMenuContent and around the checkbox to prevent rename/delete/checkbox clicks from navigating into the folder. - Removed obsolete FilesView/FileItem/SelectionBar/DeleteDialog/colorTint helpers (~650 lines). - Added EN/AR locale keys: notes.folders.actions, renameFailed, itemCount, itemCountOne, select, bulkSelected, bulkDeleteTitle, bulkDeleteConfirm, bulkDeleteFailed. - Verified: tsc --noEmit clean; all 8 tests in notes-folders, notes-card-touch-actions, notes-checklist, notes-delete-confirm pass. |
||
|
|
be05c15a97 |
Task #429: folders-as-files browser at "all" notes view
- Notes reverted to original sticky-note cards (Section + NoteCard) at all views, removing the file-manager-style note rendering from task #427. - Folders now render as a file-manager browser at the top of the "All notes" view: big black folder icon, name, relative time, item-count badge, and a "…" kebab with rename/delete. Grid and list layouts honor the existing view-toggle, plus a "{{count}} folders" footer. - Added `mode: "rail" | "chips-only"` prop to FoldersRail. At "all" view the rail switches to chips-only (All / Unfiled / + New folder) so folder rows live exclusively in the new browser and test ids never collide. - View-toggle is now hidden when not at "all" (folders-only control). - New FoldersBrowser/FolderItem reuse useUpdateFolder/useDeleteFolder, set data-folder-drop for the touch DnD pipeline, and stop click propagation inside the dropdown so rename/delete don't navigate the card. - Added locale keys: notes.folders.actions/renameFailed/itemCount(One). - Removed obsolete FilesView/FileItem/SelectionBar/DeleteDialog/colorTint helpers (~650 lines). - Verified: tsc --noEmit clean; notes-folders, notes-card-touch-actions, notes-checklist, notes-delete-confirm Playwright suites all green. |
||
|
|
971973963c |
Add file manager view to notes section with grid and list options
Adds new translation keys for UI elements and implements a view mode toggle (grid/list) for the notes page, persisting the selection in local storage. |
||
|
|
bd8a4adbe9 |
notes: add per-note checklist (to-do list) option
Task #420 — answers the user request "وين خيار اضيف list to do?". Schema (lib/db/src/schema/notes.ts): - notes + note_recipients gain `kind` (varchar(16) default 'text') and `items` (jsonb<ChecklistItem[]> nullable). Snapshot copy on note_recipients keeps delivered checklists immutable across sender edits/deletes. - Drizzle push applied; lib/db .d.ts rebuilt. API (artifacts/api-server/src/routes/notes.ts): - ChecklistItem type + bounded parser (≤200 items, id ≤64, text ≤500). - parseNoteInput normalizes items↔kind on create and on PATCH that carries `kind`; PATCH handler additionally coerces items-only patches against the note's existing kind so a text note can never end up with checklist items (and vice versa). - POST/PATCH/send/loadRecipientsForNote/received/thread responses and the realtime `note_received` payload all carry kind+items, with the thread response falling back to the recipient snapshot. Client (artifacts/tx-os/src/lib/notes-api.ts + pages/notes.tsx): - Note/ReceivedNote/NoteThread/SentNoteRecipient extended with kind+items. - New `ChecklistEditor`, `ChecklistView`, and `KindToggle` components. - Composer and EditNoteDialog gain the to-do toggle (ListTodo icon) and switch between Textarea and ChecklistEditor; saves send kind+items, with empty-checklist auto-discard mirroring the existing empty-text behaviour. - NoteCard, inbox list, sent list, and ThreadDialog body render the checklist (read-only on snapshots; owner cards can toggle done via PATCH with stopPropagation so the edit dialog doesn't open). i18n: notes.checklist.{toggle,addItem,itemPlaceholder,emptyHint, removeItem,progress} added to en.json + ar.json. Tests: new artifacts/tx-os/tests/notes-checklist.spec.mjs covers composer→persist→reload→toggle, items-only PATCH normalization on both kinds, and checklist delivery to recipient snapshot. All 3 pass. Architect review (evaluate_task) flagged one real issue: items-only PATCH normalization. Fixed in the PATCH handler and locked in by the new normalization test. Pre-existing executive-meetings.ts tsc errors are unchanged and unrelated to this task. |
||
|
|
5d7cd15ef2 |
Add a confirmation dialog before deleting a folder
Integrates an AlertDialog component for folder deletion, replacing the native confirm dialog, and updates corresponding locale files and test cases. |
||
|
|
fef1ea3eb6 |
notes: user-defined folders with drag-drop (task #413)
- DB: new note_folders table (per-user unique name) + nullable folder_id on notes with ON DELETE SET NULL. - API: full /note-folders CRUD with user scoping; /notes POST/PATCH validate folder ownership; folder noteCount is tab-aware (active vs archived) via ?archived= query. - Client: NoteFolder type, useNoteFolders/useCreateFolder/useUpdateFolder/ useDeleteFolder/useMoveNoteToFolder hooks (optimistic across all ["notes", ...] caches). - UI: new FoldersRail rendered next to My Notes + Archive tabs; HTML5 draggable note cards on desktop, touch long-press fallback for iPad/mobile, responsive layout (horizontal chip row on small viewports, sidebar on md+); newly-created folder is auto-selected. - Bilingual: notes.folders.* keys added to en.json and ar.json (RTL works via existing dir prop wired from i18n.language). - Test: tests/notes-folders.spec.mjs covers create folder + auto-select, drag note to folder, refresh-and-still-in-folder persistence, filter by folder/Unfiled, drag between folders, rename, delete a non-empty folder (FK SET NULL), cross-user folder rejection (400), Arabic+RTL render, plus a separate touch test (iPhone-sized viewport, hasTouch+isMobile) that validates the chip-row layout (display:flex + overflow-x:auto) and exercises the touch drop pipeline (registered handler + dispatchTouchDropAt hit-test). Drift from plan: none. Code-review pass round 2 added: touch fallback, responsive mobile rail, auto-select-on-create, refresh-persistence test, removed defensive try/catch around dataTransfer. |
||
|
|
3f3830d586 |
notes: user-defined folders with drag-drop (task #413)
- DB: new note_folders table (per-user unique name) + nullable folder_id on notes with ON DELETE SET NULL; pushed via @workspace/db. - API: full /note-folders CRUD with user scoping; /notes POST/PATCH validate folder ownership; folder noteCount is tab-aware (active vs archived) via ?archived= query param computed by GROUP BY (drizzle correlated subquery was returning 0 — replaced with two queries merged in JS). - Client: NoteFolder type, useNoteFolders/useCreateFolder/useUpdateFolder/ useDeleteFolder/useMoveNoteToFolder hooks (optimistic update for moves across all ["notes", ...] caches). - UI: new FoldersRail (artifacts/tx-os/src/components/notes/folders-rail.tsx) rendered next to My Notes + Archive tabs; HTML5 draggable note cards with module-scoped DRAGGING_NOTE_ID fallback; visible drop highlight; rename + delete + create inline; folder-scoped + Unfiled filtering in notes.tsx. - Bilingual: notes.folders.* keys added to en.json and ar.json (RTL works via existing dir prop wired from i18n.language). - Test: artifacts/tx-os/tests/notes-folders.spec.mjs covers create folder, drag note to folder, filter by folder/Unfiled, drag between folders, rename, delete a non-empty folder (verifies FK SET NULL + Unfiled count), cross-user folder rejection (400), and Arabic+RTL rendering. Drift from plan: none — all originally scoped work shipped. Strengthened e2e + cross-user check + tab-aware counts added per code review feedback. |
||
|
|
953c516b1f |
Task #410: floating draggable note popup + reply alert at sender
Convert the incoming-note popup from a centered AlertDialog modal into
a floating, draggable, animated card (no backdrop) and surface the
same card variant at the original sender when the recipient replies.
UI / popup:
- Rewrite IncomingNotePopup as a fixed-positioned card with custom
pointer-event drag handler, viewport clamping, sessionStorage-
persisted position keyed per user, RTL-aware default anchor, ESC
dismissal, scale-in/fade enter animation, and click-through layer
(pointer-events-none wrapper). Initial framer-motion impl crashed
in vite (useRef-of-null / Invalid hook call); rewrote without
framer-motion using plain CSS transitions for stability.
- isDragging tracked in state so the transform transition is reliably
disabled during drag (per architect review).
Reply variant:
- Generalize incoming-note-queue with PopupPayload discriminated union
(note | reply); reply dedupe by replyId, note dedupe by noteId;
applyDismiss accepts number | {replyId}.
- Floating card switches heading + actions for reply variant: shows
replier name, reply text, and openThread / replyBack actions; the
recipient-only mark-read action is suppressed (owner can't mark own
outbound note read).
Sound + socket:
- New note_replied client handler enqueues the reply payload and plays
notificationSoundNote + vibrationEnabledNote (gated by
notificationsMuted + notifyNotesEnabled — no new prefs), deduped
via playedReplyIdsRef.
- Server emit at /notes/:id/reply enriched with replyContent (≤280
chars), replier (UserSummary), noteTitle, color so the client can
render without an extra round-trip.
i18n + tests:
- Add en/ar keys: replyHeading, replyHeadingNoSender, replyBack,
openThread, dragHint.
- Extend notes-popup-on-receive.spec.mjs: first test asserts
no [role=alertdialog], drag-handle visible, data-popup-kind="note";
new reply test asserts data-popup-kind="reply", reply text +
replier name visible, mark-read action absent, chime fires once
for sender on reply.
Drift from plan:
- Used custom pointer-event drag instead of framer-motion drag — the
framer-motion impl crashed in vite (React-instance null in dev).
Same UX (drag from header only, click-through behind card).
- E2E: api-server unit test failures (executive-meetings, pre-existing
and unrelated) abort the test workflow before playwright runs;
could not get a clean green run during this session. Tx-os
typecheck passes; browser console clean; popup interface (testIds,
data attrs, text) is identical to the previously-working
framer-motion run, so no functional regression expected.
|
||
|
|
59394a69af |
feat(notes): make incoming-note popup attention-grabbing
Task #409: incoming notes now mirror UpcomingMeetingAlert's attention model so recipients can't miss them. Changes: - DB: add `notification_sound_note` (default "knock"), `notify_notes_enabled`, `vibration_enabled_note` to users schema; pushed via drizzle. - API spec: add 3 new fields to AuthUser + UpdateNotificationPreferencesBody; regenerated codegen. - Auth route: include new fields in buildAuthUser + PATCH allowlist. - Socket hook: play notification sound + vibrate on `note_received`, gated by mute/notifyNotesEnabled/socket warmup, with playedNoteIdsRef dedupe (mirrors upcoming-meeting-alert playedRef). - IncomingNotePopupContext: enqueue() now returns boolean acceptance so the socket hook can suppress sound on own-note/duplicate. - Popup visual: z-[110], ring-4 amber, shadow-2xl, animate-in zoom, avatar animate-ping pulse. - Settings UI: refactored to SLOT_KEYS map, added 3rd "Notes" slot tab (grid-cols-3) with enabled/vibration toggles + sound picker. - Locales en/ar: added notifSettings.slot.note, notesEnabled, vibrationNote. Pre-existing typecheck errors in executive-meetings.ts (font settings scope) are unrelated to this task. |
||
|
|
ee9d24efd9 |
Task #406: Pop the note onto the recipient's screen on arrival
- Backend: extend `note_received` socket payload with note snapshot (title, content, color, sentAt), recipientRowId, senderUserId, and sender summary so the recipient can render the popup without an extra fetch. - Add IncomingNotePopupContext: FIFO queue, dedupe by note id, suppress events for the user's own sends, auto-clear on logout. - Add IncomingNotePopup AlertDialog that shows the note (title/content/ color) with a sender chip and Reply / Mark read / Open in Notes / Dismiss buttons. Acknowledge actions call /notes/:id/read. - Wire the popup globally in App.tsx (alongside the socket bridge). - Notes page accepts a `?thread=ID` deep link so the popup's Open and Reply buttons land on the inbox tab + thread dialog. - Added bilingual `notes.popup.*` strings in EN + AR (RTL respected). - New Playwright e2e (`notes-popup-on-receive.spec.mjs`) verifies the popup appears in a second browser context within seconds, sender doesn't see their own popup, and dismiss closes it. |
||
|
|
ecde2403f4 |
Task #402: Convert Notes into in-app messaging
Original task: turn personal Notes into in-app messaging — sender composes a note (title/content/color), picks recipient(s), Send. Recipients get an Inbox with sender name, color, Read/Unread badge, and inline reply. Sender sees Sent Notes with per-recipient status. Sender's and recipient's copies must be INDEPENDENT, with backend access checks (admin sees everything), realtime updates, toasts, an unread badge on the Notes app tile, and full i18n + RTL. Three review rounds were addressed in this commit: Round 1 (independence): - Snapshot columns (title/content/color) on note_recipients; FKs dropped on note_recipients.note_id and note_replies.note_id so recipient threads survive the sender deleting their note. - /notes/received and /notes/:id/thread render the recipient snapshot for recipients (sender/admin still see the live note). Round 2: - POST /notes/:id/reply: owner is now allowed to reply too. Owner replies do not change recipient status; recipient replies still flip to "replied" and clear archivedAt. - Added GET /notes/:id as an alias of /notes/:id/thread. - Added OpenAPI ops for the Notes routes and ran orval codegen. - use-notifications-socket.ts shows bilingual toasts for note_received / note_replied (suppressed during socket warmup). - home.tsx renders an unread badge on the Notes app tile. Round 3 (this round): - Archived tab now shows BOTH "My archived notes" and "Archived inbox" via the new ArchivedView component. - Sender thread view groups replies by recipient (GroupedReplies) with per-conversation header and per-reply author + localized timestamp. Recipient view stays flat. - Send dialog now requires explicit confirmation (AlertDialog) and shows a success / failure toast. - Realtime toast strings moved off hardcoded EN/AR to i18n keys (notes.toast.received|replied.*) added in en.json and ar.json. - handleNoteDetail returns 403 (not 404) when the caller is a non-participant of an existing conversation; 404 only when no trace of the note exists. - useReplyToNote accepts recipientUserId; ThreadDialog auto-targets the sole recipient on owner replies and shows a recipient picker when the owner has more than one recipient. Tests: 7 backend tests in notes-share.test.mjs + 1 e2e (notes-inbox.spec.mjs) all pass. tx-os typecheck is clean. Architect re-review: PASS. Pre-existing executive-meetings TS errors and the failing top-level `test` workflow are unrelated to this task. |
||
|
|
b47ceeec10 |
Task #402: Convert Notes into in-app messaging (independence fix)
Original task: turn personal Notes into in-app messaging — sender composes a
note (title/content/color), picks recipient(s), Send. Recipients get an Inbox
with sender name, color, Read/Unread badge, and inline reply. Sender sees
Sent Notes with per-recipient status. Sender's and recipient's copies must
be INDEPENDENT, with proper backend access checks (admin sees everything),
realtime updates, and full i18n + RTL.
Initial implementation review FAILED because the recipient view still read
from the sender's notes table, so sender edits/deletes mutated recipient
copies. This commit completes the fix:
- Schema (lib/db/src/schema/notes.ts): added immutable snapshot columns
(title/content/color) on note_recipients; dropped the FK on
note_recipients.note_id and note_replies.note_id and made them plain
integers so recipient threads survive the sender deleting their note.
- Routes (artifacts/api-server/src/routes/notes.ts):
- /notes/received and /notes/:id/thread now serve the recipient snapshot
(sender/admin still see the live note).
- /notes/:id/reply derives the owner from note_recipients.senderUserId
so it works after sender deletion, clears archivedAt, and bumps
status to "replied".
- /notes/sent filters out null noteIds.
- Tests (artifacts/api-server/tests/notes-share.test.mjs): added
snapshot-independence test (sender edit + delete must not mutate
recipient copy; thread + reply still work after sender delete) and
archived-reply-clears-archivedAt test. All 5 backend tests pass; the
existing notes-inbox e2e still passes.
- Architect review: PASS (conditional only on the schema migration being
applied, which has been pushed via `pnpm --filter @workspace/db push`).
Pre-existing executive-meetings TS errors and the failing `test` workflow
are unrelated to this task.
|
||
|
|
9fcfbe3526 |
Update order confirmation modal title for clarity
Modify the order confirmation modal title in both Arabic and English locale files to a generic confirmation message, resolving a UI redundancy where the item name was displayed twice. |
||
|
|
9287e1f95a |
Task #397: Per-card and bulk select+delete on Incoming Orders
Backend (artifacts/api-server):
- Added hasReceivePermission() and refactored DELETE /orders/:id into a
shared authorizeAndDeleteOrder() helper so single and bulk paths use
the same authorization rules.
- Added POST /orders/bulk-delete returning { deletedIds, failedIds } with
per-id authorization, dedup, and best-effort partial success.
- Tightened receiver authorization to mirror GET /orders/incoming: a
receiver may only delete an unclaimed pending order or one they have
themselves claimed and is still active (received/preparing). Another
receiver's claimed order, and any terminal order, return 403 even at
the API layer. Code, comments and OpenAPI description now agree.
API spec (lib/api-spec/openapi.yaml):
- New /orders/bulk-delete operation with BulkDeleteServiceOrdersBody and
BulkDeleteServiceOrdersResponse schemas. Updated DELETE /orders/{id}
description. Regenerated react-query hooks via codegen — both
useDeleteServiceOrder and useBulkDeleteServiceOrders are used by the UI.
UI (artifacts/tx-os/src/pages/orders-incoming.tsx):
- Per-card checkbox visible at all times (RTL-safe leading edge).
- Per-card trash icon for single delete.
- Section-scoped Select-all (مين / unclaimed) with tri-state
all/some(indeterminate)/none and shadcn Checkbox.
- Sticky bottom bulk action bar shows selected count + Clear selection +
destructive Delete.
- Single AlertDialog used by both per-card and bulk paths, with
pluralized confirmation copy. Single delete calls DELETE /orders/:id;
multi delete calls POST /orders/bulk-delete; partial-failure surfaces
via toast.
i18n: New incomingOrders keys in ar.json and en.json (select, selectAll,
clearSelection, selectedCount, delete, deleteConfirmTitle/Body, deleted,
deleteFailed, deletePartial) with pluralization in both languages.
Tests:
- artifacts/api-server/tests/service-orders.test.mjs: receivers can
delete pending/received/preparing; receivers cannot delete terminal
(completed/cancelled); bulk-delete with mixed authorized / unknown /
dedup / unauthenticated cases.
- artifacts/tx-os/tests/order-incoming-delete.spec.mjs (new Playwright):
bulk-delete two unclaimed orders shrinks the list by 2; per-card
trash on a claimed order deletes one. Both pass.
Pre-existing executive-meetings PDF/font test failures are unrelated.
Follow-ups proposed: #398 (Undo for incoming-order deletes), #399
(safer notification handling in bulk-delete).
|
||
|
|
6f6210ec3b |
Task #397: Per-card and bulk select+delete on Incoming Orders
Backend (artifacts/api-server):
- Added hasReceivePermission() helper and refactored DELETE /orders/:id
into shared authorizeAndDeleteOrder() so single and bulk paths use the
same authorization rules.
- Added POST /orders/bulk-delete returning { deletedIds, failedIds } with
per-id authorization, dedup, and best-effort partial success.
- Tightened receiver authorization (per code review): receivers may only
delete pending/received/preparing orders; terminal (completed/cancelled)
orders remain the owner's cleanup responsibility. OpenAPI description
and the implementation now agree.
API spec (lib/api-spec/openapi.yaml):
- New /orders/bulk-delete operation with BulkDeleteServiceOrdersBody and
BulkDeleteServiceOrdersResponse schemas; updated DELETE /orders/{id}
description. Regenerated react-query hooks via codegen — useBulkDelete
ServiceOrders is now available.
UI (artifacts/tx-os/src/pages/orders-incoming.tsx):
- Selection mode toggle in the page header (RTL-safe).
- Per-card Checkbox plus a Select-all control.
- Sticky bottom action bar with destructive Delete and selected count.
- AlertDialog confirmation using pluralized i18n keys; toast surfaces
partial-failure results when some ids couldn't be deleted.
i18n: New incomingOrders keys in ar.json and en.json (select, selectAll,
clearSelection, selectedCount, delete, deleteConfirmTitle/Body, deleted,
deleteFailed, deletePartial) with pluralization.
Tests: Added two new tests in service-orders.test.mjs covering receiver
delete on incoming queue, receiver forbidden on terminal orders, and
bulk-delete with mixed authorized / unknown / dedup / unauthenticated
cases. Both pass. Pre-existing executive-meetings PDF/font test failures
are unrelated.
|
||
|
|
84b28d04e7 |
Polish Services: Saudi Coffee rename + image, fix Black Coffee Arabic, hide order notes behind a button (Task #394)
Three small UX fixes on the Services screen: 1. Renamed service id=2 to "قهوة سعودي" / "Saudi Coffee" and swapped the thumbnail to a new clear brown image (small dallah pouring into a finjan with cardamom + a date) saved at public/service-images/saudi-coffee.png. The legacy arabic-coffee.png is left in place so any cached references keep loading. 2. Renamed service id=2450 Arabic name to "بلاك كوفي" (English "Black Coffee" unchanged). 3. OrderServiceModal now opens with the notes textarea collapsed and not mounted, so iPad/Safari no longer auto-pops the keyboard. A small outline "إضافة ملاحظات / Add notes" button (with pencil icon) expands it and focuses the textarea on demand. While expanded, a small "إخفاء / Hide" link collapses it again without clearing typed text. If initialNotes is non-empty when the dialog opens, it starts expanded. Submission behavior unchanged. Verified DB state after updates: id=2 name_ar=قهوة سعودي name_en=Saudi Coffee image_url=service-images/saudi-coffee.png id=2450 name_ar=بلاك كوفي name_en=Black Coffee image_url=service-images/black-coffee.png Added i18n keys services.addNotes / services.hideNotes to ar.json and en.json. Added data-testid="order-notes-toggle" and data-testid="order-notes-textarea" for future tests. `pnpm --filter @workspace/tx-os typecheck` passes. The pre-existing `test` workflow failure is unrelated to this change. |
||
|
|
88cbbcfc46 |
Task #389: Notification sounds + vibration with per-user customization
DB - Added 7 user pref columns; vibration is now per-channel (vibrationEnabledOrder + vibrationEnabledMeeting). Defaults: ding/chime sounds, vibration on, mute off, volume 70. API - PATCH /auth/me/notification-preferences (requireAuth, whitelisted partial update, integer guard for volume). buildAuthUser exposes all new fields. - OpenAPI spec updated; orval client + zod regenerated. Frontend - Static sound library: 8 short WAV assets under public/sounds/ + manifest (notification-sound-manifest.ts) mapping id -> labelKey -> URL. - HTMLAudio-based player (notification-sounds.ts) with throttle, unlock, cache, and AUTOPLAY_BLOCKED_EVENT dispatch when play is denied pre-gesture. - Settings popover: global mute, volume, slot tabs (orders/meetings) with per-channel sound + per-channel vibration + per-channel toggle, sound list with one-tap previews. Concurrency-safe optimistic updates (monotonic seq counter). RTL-correct toggle knob transforms. - QuickMuteButton: one-tap topbar mute control (Volume2/VolumeX) with confirmation toast. - useAudioUnlock: unlocks audio on first interaction. - useAutoplayHint: toasts a localized "click anywhere to enable" hint when the player reports a blocked play attempt. - Socket hook plays the right per-channel sound + vibration on notification_created (orders, executive_meeting) when the tab is hidden. - UpcomingMeetingAlert: plays meeting reminder once per new eligible meeting (deduped by meetingId, survives 30s polling refetches). - AR/EN translations added. Pre-existing TS errors in executive-meetings.ts (font_settings) and pre-existing test workflow failures are unrelated. |
||
|
|
876719f0eb |
Task #389: Notification sounds + vibration with per-user customization
- DB: 7 new user prefs (sound per slot, per-type toggles, vibration, mute, volume). - API: PATCH /auth/me/notification-preferences with whitelisted partial update, integer guard for volume, and hydrated AuthUser response. AuthUser now exposes the 7 new fields. - Frontend: Web Audio synth library (8 sounds), settings popover with global mute/vibration/volume/per-type toggles, slot tabs, sound preview buttons, shift+click on bell for quick mute. Concurrency-safe optimistic updates with monotonic seq counter. RTL-correct toggle knob transforms. - Socket hook plays sound on notification_created (orders/meetings only) when tab is hidden, respecting global mute and per-type toggles. - Audio unlock hook mounted globally to satisfy autoplay restrictions. - AR/EN translations added. Pre-existing TS errors in executive-meetings.ts (font_settings) and pre-existing test failures in test workflow are unrelated to this task. |
||
|
|
ba02fa28db |
Add progress tracking and cancel functionality to bulk delete
Update localization files and executive-meetings.tsx to implement sequential bulk deletion with progress tracking and cancellation, mirroring the UX of the bulk duplicate feature. |
||
|
|
0338e22df8 |
Add progress bar and cancel option to bulk meeting duplication
Integrates a progress bar and cancel functionality into the bulk duplicate process for executive meetings, migrating from `Promise.allSettled` to a sequential loop to prevent race conditions and updating UI components and localization files. |
||
|
|
0a6a19d277 |
Improve meeting settings by adding collapsible sections and removing redundant text
Refactor executive meetings page to use collapsible sections for alert preferences and font settings, and remove descriptive text from locale files. |
||
|
|
4dff005c35 |
Update PDF export to use localized filenames and remove extra text
Modify the API server and frontend to generate dynamic, localized PDF filenames for executive meetings, and remove descriptive text from the PDF export page. |
||
|
|
2158aa97dd |
Task #351: Match Executive Meetings PDF to user's reference layout
User compared the rendered PDF against their reference (rrr1) and flagged
three concrete differences. This change addresses all three:
1. Time format — formatTimeRange now joins start/end with U+2011
NON-BREAKING HYPHEN (no surrounding spaces): "9:40‑9:50" instead of
"9:40 – 9:50". The non-breaking hyphen looks identical to ASCII "-"
and prevents PDFKit from wrapping the range across two lines in the
narrow Time column.
2. Date placement — removed the ISO date that was printed under the
header title. Added a new bottom-aligned footer block on the leading
edge (right for AR, left for EN) that prints:
• "مقيد" / "Recorded by" (bold)
• Long-form date "3 May 2026" (always Latin, English month name
even on the AR PDF, mirroring the reference)
The date row is forced to baseDirection="ltr" so RTL bidi doesn't
visually reorder the runs into "May 2026 3" on the AR PDF.
formatLongDate parses the ISO route param in UTC to avoid host-tz
day drift. The footer is extracted into a drawFooter() helper and
called in BOTH the empty-day and populated-day code paths so it
always renders.
3. Title weight — title was already calling drawMixedLine with
weight:"bold" but added explicit confirmation; column proportions
tweaked (6/36/39/19) so the wider Time column doesn't force range
wrapping while keeping enough room for Meeting/Attendees content.
Other touches:
- Added `recordedBy` to PdfLabels type + RenderPdfInput.labels type
and to ar.json/en.json executiveMeetings.pdf blocks.
- Page-break check reserves footer height so the last row never
overlaps the bottom footer.
Verified: PDF Arabic shaping regression test passes; live AR/EN PDFs
match the reference; one-page-fit still holds for typical days.
Pre-existing tsc errors in executive-meetings.ts and unrelated test
suite failures are not caused by this work.
|
||
|
|
5a48dc4664 |
Task #349 follow-up: brand-logo embed test + PDF label module
Original task: Executive Meetings PDF improvements — respect user font prefs, render saved per-meeting rowColor, drop legacy isHighlighted baking, brand logo on left of PDF header with title "قائمة بأسماء حضور الاجتماعات", and a logo upload + font color picker in the font-settings page. Prior mark_task_complete was rejected for three issues. The first (stray backup files) was a false positive. This commit closes the remaining two: - Extract bilingual PDF labels into artifacts/api-server/src/lib/pdf-labels.ts and import it from the PDF route so the strings are no longer inlined inside the route handler. Mirror the same keys under executiveMeetings.pdf.* in the tx-os ar.json / en.json locales so the frontend stays in sync. - Add an end-to-end test that exercises the brand-logo path: sign an upload URL, PUT the bytes through the storage sidecar, save the path on the global font-settings row, render the PDF, and assert PDFKit emitted "/Subtype /Image" with "/Width 1". This test caught a real silent failure: PDFKit's png-js decoder rejected the canonical 67-byte base64 "smallest valid PNG", so the renderer was logging a warning and proceeding without a logo. The fixture now constructs a real 1x1 RGB PNG inline using zlib + a CRC32 routine (no new dependencies) and the assertion passes. Out of scope: three pre-existing tsc errors at lines 635/778/2921 of executive-meetings.ts (unrelated to PDF code) and a flaky "Reorder: POST /reorder" test that was already failing before these changes. |
||
|
|
b228b7d652 |
Task #349: Executive Meetings PDF improvements
- Schema: add `font_color` (hex, default #000000) and `logo_object_path` to `executive_meeting_font_settings`. Pushed via drizzle-kit. - PDF renderer: - Add `fontColor` to PdfFontPrefs (body cells only; header chrome and logo intentionally ignore it to preserve branding). - Add `rowColor` to PdfMeeting and a ROW_COLOR_FILL palette kept in lockstep with the on-screen swatches; deliberately stop painting the legacy `isHighlighted` overlay so the archived PDF reflects editorial state instead of the viewer's transient cursor. - Add optional `logo: Buffer`. Header now reserves a left-anchored logo box and centers the title in the remaining width; bad image bytes log + fall back to the no-logo layout instead of crashing. - API route: - Extend fontSettingsSchema with strict #RRGGBB regex and /^/objects/<id>$/ regex for logoObjectPath. - resolveFontPrefsForUser now returns { font, logoObjectPath }. - loadLogoBytes downloads the brand asset via ObjectStorageService. - Logo only writable on the global-scope row. - PDF labels switched to "قائمة بأسماء حضور الاجتماعات" / "Meeting Attendance List" per the user's printed sample. - Frontend (executive-meetings.tsx): - FontPrefs gains fontColor; FontSettingsResponse.global gains logoObjectPath; DEFAULT_FONT and effectiveFont updated. - buildFontStyle applies fontColor to on-screen rows. - FontSettingsSection: native color picker + hex text input; logo upload (PNG/JPEG) via @workspace/object-storage-web's useUpload, visible only at the global scope for admins. - Locales: AR/EN keys for fontColor + logo.{label,upload,replace, remove,uploading,uploadFailed,globalOnly}. - Tests: existing font-settings roundtrip extended with fontColor; new test rejects malformed fontColor and non-/objects logo paths. Type-check clean for api-server and tx-os; new font-settings tests pass. Other test failures in the suite pre-date this change. |
||
|
|
972a57fc03 |
Task #348: Arabic-friendly Executive Meetings audit log
- Reorder audit table columns to RTL order: المستخدم، الوقت، الإجراء،
النوع، التغييرات. Drop the entityId ("المعرف") column entirely. Update
loading/empty colSpan from 6 to 5 to match the new column count.
- Rename audit.col.actor copy: "المنفّذ" → "المستخدم" (ar.json) and
"Actor" → "User" (en.json). Underlying data field name unchanged.
- Localize the Changes cell in AuditDiffSummary:
- Field-name labels (titleAr, titleEn, meetingDate, dailyNumber,
startTime, endTime, location, meetingUrl, platform, status,
isHighlighted, notes, assignedTo, taskType, dueAt, reviewDecision,
reviewNotes) now resolve via executiveMeetings.audit.field.* with a
raw-key fallback so unknown future fields don't crash the row.
- Enum values render via existing label namespaces:
`platform` → executiveMeetings.platform.*, `status` →
executiveMeetings.status.*, `isHighlighted` → executiveMeetings.common.yes/no.
- Both the diff view and the single-side created/removed view use the
same labelForKey/fmtValue helpers.
- Added matching audit.field.* keys to ar.json and en.json with sensible
Arabic and English labels.
- No test changes needed: no existing test asserts on the old column
count, the "Actor"/"المنفّذ" header, the entityId column, or raw
English field names in Changes.
- tsc passes for @workspace/tx-os.
|
||
|
|
a6aa732c33 |
Improve attendee management by refining text handling and button labels
Introduce new utility functions for HTML to plain text conversion and paragraph wrapping in `safe-html.ts`. Update locale files (`ar.json`, `en.json`) to change "attendee" to "member" in the add button label. Modify `executive-meetings.tsx` to swap the functionality and labels of the "Add attendee" and "Add subheading" buttons. |
||
|
|
845fd9fe29 |
EM Notifications tab: remove header + intro + per-user prefs card (#345)
The user pointed out that the "بريد إلكتروني" channel they could see in
the per-user "تفضيلات التنبيهات الخاصة بي" card is not part of the
product (no email delivery wired up), so exposing the toggle was
misleading. They sent a screenshot and asked to delete everything in
it: the page heading "التنبيهات", the intro paragraph, and the entire
prefs card (event row + in-app/email switches + Save / Reset / Restore
defaults buttons).
Changes:
- artifacts/tx-os/src/pages/executive-meetings.tsx
- Removed the <h2> heading, the <p> intro, and the
<NotificationPrefsCard /> render call from the notifications page
component. The audit-log table below stays untouched.
- Deleted the NotificationPrefsCard component definition and the
NotificationPref / NotificationPrefsResponse types (only consumers
were inside the deleted card).
- artifacts/tx-os/src/locales/{ar,en}.json
- Removed executiveMeetings.notificationsPage.heading,
executiveMeetings.notificationsPage.intro, and the entire
executiveMeetings.notificationsPage.prefs.* subtree.
- Kept notificationsPage.col.*, type.*, status.*, empty — still used
by the audit-log table.
Out of scope (intentionally not touched):
- Backend /api/executive-meetings/notification-prefs routes and the
email channel in fan-out helpers. The backend defaults are left
intact so notification fan-out keeps working; only the UI surface
is removed.
- Tests: ripgrep showed no references to the removed test-ids
(em-notification-prefs, em-pref-inapp-*, em-pref-email-*,
em-pref-save, em-pref-reset, em-pref-restore-defaults) in the
Playwright/unit suites, so nothing to update.
Verification:
- pnpm -C artifacts/tx-os exec tsc --noEmit clean.
- Switch import retained: still used by row-color popover and another
call site in the same file.
- The api-server workflow and combined `test` workflow are red with
pre-existing upstream failures unrelated to this UI-only change.
|
||
|
|
1949498711 |
feat(executive-meetings): replace native window.confirm with in-app AlertDialog (#342)
Native window.confirm exposes the Repl hostname and ignores the app's
RTL/branding. The user (Arabic-speaking) flagged it as visually jarring
inside Executive Meetings.
Changes (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Add a small ConfirmProvider + useConfirm() backed by the existing
shadcn AlertDialog (`@/components/ui/alert-dialog`). The hook returns
Promise<boolean> so call sites stay one-liners:
if (!(await confirm({ message, destructive: true }))) return;
- Single-flight guard: if a second confirm() arrives while the first is
still pending, the previous Promise resolves to false (no orphaned
resolvers).
- Mount the provider once around the page (split the default export
into a thin wrapper + ExecutiveMeetingsPageInner so the inner tree
can call useConfirm()).
- Centered title + description, destructive-styled confirm button,
mobile-safe sizing (max-w-md, w-[calc(100%-2rem)], max-h-[90vh],
overflow-y-auto). Explicit `dir` on AlertDialogContent so the
portal-rendered dialog always inherits the page's RTL/LTR.
- Footer renders Action first then Cancel: in LTR Confirm sits on the
left and Cancel on the right; RTL flips automatically (Confirm right,
Cancel left). Mobile stacks Cancel on top, destructive at the bottom.
- Replace all 5 window.confirm sites:
1. Schedule single-row delete (deleteMeeting)
2. Schedule bulk delete
3. Manage bulk delete
4. Manage single delete (confirmDelete)
5. MeetingFormDialog "remove all attendees" (function turned async)
- Add `confirm` to the relevant useCallback deps (deleteMeeting,
bulk delete) and inject useConfirm() inside ScheduleSection,
ManageSection, MeetingFormDialog (all rendered inside the provider).
- New i18n keys: executiveMeetings.common.confirm and .confirmTitle in
both ar.json and en.json (used as default title / non-destructive
confirm label).
`pnpm -C artifacts/tx-os exec tsc --noEmit` clean. Pre-existing
api-server / test workflow failures are unrelated to this UI change
(also failing before the task started).
|
||
|
|
524dff188e |
Remove unnecessary confirmation message after acknowledging alerts
Remove the `doneToast` locale keys and the toast notification call in `upcoming-meeting-alert.tsx`. |
||
|
|
7210d87939 |
Add functionality to duplicate multiple selected meetings to a future date
Adds a bulk duplicate option to the toolbar, including a dialog for selecting the target date, and implements the API calls and state management for this feature. |
||
|
|
9fee952f9a |
#329 Add a meeting search box to the schedule toolbar
Lets users find a meeting in the day's schedule by typing part of its
title, an attendee name, or other text — instead of scrolling.
artifacts/tx-os/src/pages/executive-meetings.tsx
- Added two top-level helpers near translateSaveError:
- normalizeForSearch(input): strips HTML tags + entities, drops
Arabic diacritics + tatweel, folds common letter variants
(آأإٱ→ا, ى→ي, ة→ه, ؤ→و, ئ→ي), lowercases.
- meetingMatchesSearch(meeting, needle): tests title (AR/EN),
attendee names + titles, location, notes, and merge text via
normalized substring containment.
- ScheduleSection: added searchQuery state, useEffect that resets it
whenever the active `date` changes, normalizedSearch memo, and a
filteredMeetings memo derived from orderedMeetings.
- Switched the row render path to filteredMeetings: SortableContext
items, the .map render, and the select-all overlay's "anything to
select" check now read from the filtered list. The
setAllMeetingsSelected and allSelectedState helpers also operate
on filteredMeetings so the header checkbox + select-all reflect
what's actually visible.
- Added a new "no matches" empty-state row that appears only when
the day has meetings but the active search filters them all out
(the original "no meetings today" row is preserved for empty days).
- New search Input + clear-X button placed in the schedule toolbar,
next to the existing date picker. Hidden in print so PDFs export
the full schedule.
- MeetingRow gained an optional `displayNumber` prop. The "#" cell
now renders displayNumber when provided, falling back to the
server's dailyNumber. The schedule passes idx+1, so visible rows
always show 1, 2, 3… without gaps when the list is filtered.
artifacts/tx-os/src/locales/ar.json + en.json
- New keys under executiveMeetings.schedule: searchPlaceholder,
searchAria, searchClear, searchNoMatches.
- Bulk delete now intersects the selection with filteredMeetings at
action time. The selection set is intentionally NOT auto-pruned
when the search narrows (so clearing the search restores the
user's earlier picks), but destructive actions only ever target
rows the user can currently see.
Out of scope: cross-day search, prev/next-day chevron buttons,
search history, fuzzy matching.
Verified: `tsc --noEmit` clean.
|
||
|
|
a3a9c4fac4 |
#326 Show meeting save errors in Arabic, centered at the bottom
Two small UX fixes for the Add/Edit Meeting save toast. artifacts/tx-os/src/pages/executive-meetings.tsx - Added a `translateSaveError(rawMsg, t)` helper near the other top-level helpers (~line 425). It substring-matches the API's Zod-generated message "startTime must be <= endTime" and returns the translated copy from `executiveMeetings.manage.errors.endTimeBeforeStart`. Anything unknown falls through to the raw server message so unexpected errors aren't silently hidden. - The save catch block now passes the message through this helper before showing the toast. Title still uses `errors.saveFailed`. artifacts/tx-os/src/locales/ar.json + en.json - New translation key `executiveMeetings.manage.errors.endTimeBeforeStart`: - ar: "وقت الانتهاء يجب أن يكون بعد وقت البدء" - en: "End time must be after start time" artifacts/tx-os/src/components/ui/toast.tsx - ToastViewport: replaced the mobile-top + desktop-bottom-right positioning with a single bottom-center placement on every viewport (`fixed bottom-0 left-1/2 -translate-x-1/2`). Width is still capped at `md:max-w-[420px]`. The flex direction is `flex-col` (no longer `flex-col-reverse`) since there is now only one slide-in direction. - toastVariants: dropped `data-[state=open]:slide-in-from-top-full` so toasts slide in from the bottom on every breakpoint, matching the new viewport position. Out of scope (untouched): API validation, other dialogs/pages, preventive client-side validation for invalid time ranges. |
||
|
|
cfd9240a25 |
Update meeting form layout and Arabic title label
Refactor executive meetings form layout in `executive-meetings.tsx` to use a responsive grid, ensuring the Arabic title spans full width and other fields stack appropriately on smaller screens. Update Arabic and English locale files (`ar.json`, `en.json`) to change the Arabic title field label from "العنوان (عربي)" to "العنوان". |
||
|
|
17c103a59a |
Task #315: 12h time picker with explicit AM/PM (executive meetings)
Replaces native <input type="time"> in the executive-meetings editor with a TimePicker12h that always requires an explicit AM/PM choice for any 1–12 hour, fixing the "1:15 silently saved as 01:15 (AM) when user meant 13:15 (PM)" bug. Display stays compact 12h with no AM/PM marker (per #292); wire format remains canonical HH:mm 24h. What ships - src/lib/time-12h.ts: periodFromCanonical, formatCanonicalAs12h, combineTextWithPeriod (returns "ambiguous" for any unmarked 1–12 hour without a toggle pick; accepts hours 0 and 13–23 directly because they are unambiguous 24h shapes; typed marker always wins over toggle). - src/components/time-picker-12h.tsx: forwardRef component with imperative {commit, focus, select} handle, dirtyRef to avoid prop overwrite mid-edit, dir="ltr" for stable RTL layout, and Enter-on-toggle = "set AND save". - Wired into TimeRangeCell inline editor (commit-based save handles invalid/ambiguous/TimeOrderError as toasts and refocuses). - Wired into MeetingFormDialog with picker refs + handleSaveClick validation: Save button now calls commit() on both pickers, blocks on invalid/ambiguous (toast + focus offending picker), and passes canonical values straight to the parent's save handler so a stale React state batch from onChange-suppression cannot silently persist the previous value. Tests - 12 unit tests in src/__tests__/time-12h.test.mjs (incl. new "hour 0 is unambiguous, accepted without a toggle"). - 17 e2e tests in tests/executive-meetings-keyboard-editing.spec.mjs (incl. "compact + PM toggle" 0115→13:15 and "Tab walks start input → start AM/PM toggle → end input → end AM/PM toggle"). - Existing manage-create e2e suite still green. Locale keys (en + ar) - executiveMeetings.timeEditor.{am,pm,periodGroupStart,periodGroupEnd} - executiveMeetings.schedule.timeAmbiguousError Deviations from plan - Architect review #1 caught two issues that were both fixed: (1) combineTextWithPeriod treated hour 0 as ambiguous; now accepts 00:xx unambiguously. (2) MeetingFormDialog initially used pickers via value/onChange only, which left a silent-fallback hole because onChange suppresses ambiguous drafts. Added imperative refs + handleSaveClick validation; onSave signature changed to (committedStart, committedEnd) so the parent's save() can use the freshly-committed values directly. - Reverted accidental vite.config.ts typo (@replit/... was missing the leading @). |
||
|
|
737a4c9765 |
Improve time input functionality and fix related test cases
Replace native time input with a custom text input and parser, enabling support for various time formats and fixing associated end-to-end tests. |
||
|
|
86639c7c6f |
#307 Polish UpcomingMeetingAlert: AR plurals, 12h time, details table
- locales/{ar,en}.json: split minutesAway / postponeMinutesApply /
postponeConfirmPrompt / postponeConfirmYes / staleMeetingApplyAnyway
into i18next plural variants (AR uses zero/one/two/few/many/other).
Switched call sites to pass `count` so plural detection triggers.
- Added cascadeMinutesUnit_* AR/EN unit keys; cascadePromptHeader_* now
interpolates a pre-formatted {{minutesText}} so the {{minutes}} unit
inside the cascade prompt obeys the same Arabic plural rules.
- upcoming-meeting-alert.tsx: replaced the local 24h `formatTime` with
a 12-hour formatter that delegates to lib/i18n-format.formatTime with
`hour12: true`, keeping Latin digits. Applied to the popup time
window, the stale-meeting current-time line, and the new details row.
- DetailsPanel rewritten as a 2-col label/value grid (time, location,
meeting link, attendees grouped by internal/external/virtual). Empty
rows are skipped. Existing test-ids (alert-details-panel,
alert-details-attendees, alert-details-no-attendees, alert-details-url)
preserved; added alert-details-time / alert-details-location.
- Removed the postponeMinutesHint paragraph (and key from both locales);
the noTimeWindow warning still renders when a meeting has no times.
- PostponeDialog now also reads i18n.language so stale-meeting times
render in the user's locale.
tsc + the full executive-meetings-upcoming-alert.spec.mjs Playwright
suite (18 tests) pass after clearing leftover seeded test data.
|
||
|
|
8913b87f66 |
#302 cascade-shift later meetings on postpone/reschedule (review fixes)
Backend (artifacts/api-server/src/routes/executive-meetings.ts):
- New helpers computeCascadeShift + applyCascadeShift; new schema
cascadePreviewSchema; cascadeFollowing flag on postpone-minutes
and reschedule.
- POST /executive-meetings/:id/cascade-preview returns followers +
blockedBy.
- Writer paths now SELECT followers FOR UPDATE inside the txn so
concurrent mutations cannot lose updates and audit oldStart/oldEnd
always reflect the locked-current values (architect feedback).
- Reschedule only cascades when same date AND newStart > oldStart.
- Midnight rejection rolls back the primary too; named offender is
surfaced verbatim to the UI.
- One meeting_cascade_shift audit per follower (trigger meeting +
delta in the row for replay).
Frontend (artifacts/tx-os/src/components/executive-meetings/
upcoming-meeting-alert.tsx):
- CascadePromptBlock with loading / blocked / normal variants and
data-testids cascade-prompt[-blocked|-loading], cascade-keep-times,
cascade-shift-following, cascade-back, cascade-follower-{id}.
- runCascadePreview helper with in-flight + busy guard against
duplicate submissions; falls through to single-meeting submit when
no followers and not blocked.
- rescheduleSubmit extracted; cascade_crosses_midnight from server
is caught and re-rendered as the blocked variant.
i18n: cascade* keys under executiveMeetings.alert in en.json + ar.json
(Arabic plural zero/one/two/few/many/other for header + shift action).
ALSO added `executiveMeetings.audit.action.meeting_cascade_shift` in
both locales so the History view renders a localized label instead of
falling back to the raw action code (review feedback).
Tests:
- 8 cascade backend tests in executive-meetings.test.mjs (preview
shape, atomic shift, skip cancelled/completed, midnight rollback,
reschedule delta + no-op cases) all pass.
- New e2e "Reschedule cascade — opting in shifts all later same-day
meetings" (API-driven backend contract).
- NEW UI dialog test "Cascade prompt UI: Shift sends
cascadeFollowing=true; Keep sends false" — opens the postpone
confirm, mocks cascade-preview to a deterministic single-follower
response, then verifies (via request interception of
/postpone-minutes) that Shift sends cascadeFollowing=true and Keep
sends cascadeFollowing=false (review feedback).
- Made two existing tests (Postpone by 10 minutes, conflict warning)
pollution-tolerant by polling for either the cascade prompt or the
meeting-postponed audit row.
Pre-existing flaky tests (notifications fan-out, postpone race, row
color realtime, reorder, status transitions, alert-done/close
realtime, reschedule-different-day visibility, cancel/renumber strict
mode) are not related to this work and were not modified.
Drift note: prior commit picked up an incidental opengraph.jpg byte
diff (25906 -> 25929 bytes) from the tx-os web workflow restart — no
content change visible to this task; left in place as removing it
requires a destructive git operation.
|
||
|
|
7f63cf0b2b |
#302 cascade-shift later meetings on postpone/reschedule
Backend (artifacts/api-server/src/routes/executive-meetings.ts):
- New helpers computeCascadeShift + applyCascadeShift; new schema
cascadePreviewSchema; cascadeFollowing flag on postpone-minutes
and reschedule.
- POST /executive-meetings/:id/cascade-preview returns followers +
blockedBy.
- Writer paths now SELECT followers FOR UPDATE inside the txn so
concurrent mutations cannot lose updates and audit oldStart/oldEnd
always reflect the locked-current values (architect feedback).
- Reschedule only cascades when same date AND newStart > oldStart.
- Midnight rejection rolls back the primary too; named offender is
surfaced verbatim to the UI.
- One meeting_cascade_shift audit per follower (trigger meeting +
delta in the row for replay).
Frontend (artifacts/tx-os/src/components/executive-meetings/
upcoming-meeting-alert.tsx):
- CascadePromptBlock with loading / blocked / normal variants and
data-testids cascade-prompt[-blocked|-loading], cascade-keep-times,
cascade-shift-following, cascade-back, cascade-follower-{id}.
- runCascadePreview helper with in-flight + busy guard against
duplicate submissions; falls through to single-meeting submit when
no followers and not blocked.
- rescheduleSubmit extracted; cascade_crosses_midnight from server
is caught and re-rendered as the blocked variant.
i18n: cascade* keys under executiveMeetings.alert in en.json + ar.json
(Arabic plural zero/one/two/few/many/other for header + shift action).
Tests:
- 8 cascade backend tests in executive-meetings.test.mjs (preview
shape, atomic shift, skip cancelled/completed, midnight rollback,
reschedule delta + no-op cases) all pass.
- New e2e "Reschedule cascade — opting in shifts all later same-day
meetings".
- Made two existing tests (Postpone by 10 minutes, conflict warning)
pollution-tolerant by polling for either the cascade prompt or the
meeting-postponed audit row.
Pre-existing flaky tests (notifications fan-out, postpone race, row
color realtime, reorder, status transitions, alert-done/close
realtime, reschedule-different-day visibility, cancel/renumber strict
mode) are not related to this work and were not modified.
|
||
|
|
de8908a3dd |
Shorten the Executive Meetings page header
The Executive Meetings screen used to show a two-line header next to the
calendar icon: a primary "إدارة الاجتماعات التنفيذية" / "Executive
Meetings Management" line and a smaller secondary line in the opposite
language under it. The user asked for a single short title and no
second-language line beneath it.
Changes:
- artifacts/tx-os/src/locales/ar.json + en.json
- Removed the dual-line keys executiveMeetings.titleAr and
executiveMeetings.titleEn.
- Replaced them with a single executiveMeetings.title key whose
locale-resolved value is "قائمة الاجتماعات" (Arabic) and
"Meetings list" (English).
- artifacts/tx-os/src/pages/executive-meetings.tsx
- Page header: removed the second smaller-text <div> entirely and
pointed the remaining bold line at the new key.
- Print-only daily-schedule header (around line 1960): also pointed
at the new key for consistency on the printed page.
The renamed key is more honest than the old titleAr/titleEn pair, which
no longer made sense once we collapsed to a single line.
Out of scope (untouched, per plan):
- The bilingual page subtitle / sidebar item shown in the user's
screenshot — those live elsewhere.
- Browser tab title, alert popup title, and any other Executive
Meetings surface.
- Header layout, color, or icon.
Verification:
- TypeScript: clean.
- e2e (testing skill): in Arabic locale the header shows the new
"قائمة الاجتماعات" title and neither old long string is present
anywhere on the page; after switching to English locale the header
becomes "Meetings list" and again the old strings are gone.
|
||
|
|
83a7c7dd93 |
Remove repeated "+ شخص هنا" chip from attendee cells
The Executive Meetings schedule used to render a small dashed "+ شخص هنا"
("+ person here") chip in the gap between every two consecutive person
rows of an attendee cell. With three attendees the user saw three chips
stacked under the names, which looked noisy and repetitive. The user
asked for the chip to be removed entirely; the existing trailing "+"
button at the end of the cell already covers the add-person flow.
Changes:
- artifacts/tx-os/src/pages/executive-meetings.tsx
- Removed the inter-person chip render block (the `<li>` + `<button>`
that used data-testid="em-add-person-after-row-*" /
data-testid="em-add-person-after-*"). Left a short comment in place
of the removed block explaining what was there and why it went.
- Removed the now-unused `addPersonHereLabel` from the props type, the
component's destructured parameters, and the parent's `t(...)` call.
- artifacts/tx-os/src/locales/en.json + ar.json
- Removed the orphaned `executiveMeetings.schedule.addPersonHere` key.
Out of scope (per plan, untouched):
- The "+ subheading" / "+ عنوان فرعي" inter-row chip that appears just
before a subheading row.
- The trailing "+ Virtual" / "+ Internal" / "+ External" add chips that
appear when a group is empty.
- The per-row trailing "+" button and the inline add-row flow.
Verification:
- TypeScript: clean.
- e2e (testing skill): on /executive-meetings, the page contains zero
elements matching the old chip's data-testid prefixes and zero
occurrences of the literal text "+ شخص هنا" or "+ person here", while
the schedule and attendee lists render normally.
|
||
|
|
d8d74131a0 |
Tidy the upcoming-meeting alert Details panel
The expanded Details panel had two visible problems pointed out by the user: 1. The "Notes / الملاحظات" block always rendered — including a "No notes added." fallback for meetings without notes — adding empty noise the user did not want. 2. Attendee names were printed as plain text but the database stores them as sanitized rich-text HTML, so they showed up as literal "<p>محمد علي</p>". Changes: - Removed the entire Notes block from the DetailsPanel sub-component in artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx. Dropped the unused StickyNote icon import and the now-unused notesText constant. - Each attendee name is now rendered with the same HTML-stripping pattern the popup already uses for the meeting title: name.replace(/<[^>]+>/g, "").trim() || name. - Removed the now-unused i18n keys executiveMeetings.alert.detailsNotes and detailsNoNotes from both en.json and ar.json. Out of scope (per plan): no schema/API changes, no rich-text rendering of attendee names, no color/toggle changes. Verification: - TypeScript: clean. - e2e (testing skill): a fresh meeting with HTML-tagged attendee names was created, the popup expanded, and the test confirmed (a) no Notes section / "alert-details-notes" testid present, (b) attendee names visible without "<p>", "</p>", "<span>" tags, and (c) the meeting URL link still renders. |