Commit Graph

177 Commits

Author SHA1 Message Date
Riyadh bd62b79d0c Collapsible booking cards in Protocol → Bookings list (Task #676)
Original task: make the protocol booking-requests LIST cards collapsed by
default, showing a compact summary and expanding full details only when opened;
add numbering, show the booking reference, and highlight today's bookings green.

Changes (artifacts/tx-os/src/pages/protocol.tsx):
- renderBookingCard(b, index) now renders a collapsible card. Collapsed shows
  only: sequence number (1-based over the current/filtered list), title,
  room · date-time range, booking reference, status/public pills, and a chevron.
- Summary header is a real <button type="button"> with onClick toggle and
  aria-expanded for keyboard accessibility.
- Expanded reveals the previously-shown detail fields (requester, department,
  meeting type, entity, phone, attendee count, purpose, key attendees) and the
  action buttons (approve/reject/cancel/edit/delete) — no functionality lost.
- Bookings starting today (sameYmd(startsAt, now)) get a green card
  (emerald border/bg/ring) plus an "اليوم"/"Today" badge.
- Added expandedBookings Set state + toggleBookingExpanded near booking state.
- List map updated to pass 1-based index.

i18n: added protocol.bookings.today ("اليوم" / "Today") to ar.json and en.json.

Scope: list view only; calendar view, public request form, and other tabs
unchanged. Client-only "today" derivation, no backend changes.

Verification: tsc --noEmit passes; architect code review PASSED (no critical
issues). Screenshot skipped — protocol page requires auth.
2026-07-07 14:08:51 +00:00
Riyadh 3ff1338c7e Task #670: revamp public room-booking form (protocol)
Public booking form now captures department, meeting type (internal/
external), meeting purpose, and a repeatable "key attendees" table
(name + position + side). External meetings require the entity name
(reuses requesterOrg). "عدد الحضور" relabeled to "العدد المتوقع للحضور".
Added a static red disclaimer note about possible cancellation.

Each booking now gets an auto-generated searchable reference
(RB-{year}-{pad4}) shown on the success screen; staff can search the
admin bookings list by reference OR phone.

Changes:
- lib/db schema: department, meetingType, purpose, keyAttendees (jsonb),
  bookingReference (unique) + requesterPhone index; new enums/types.
  Applied via drizzle push; backfilled existing 4 rows.
- api-server: extended public booking schema + refine (org required when
  external), makeBookingReference, public/internal POST generate+return
  bookingReference.
- tx-os public form rewritten; admin card shows new fields + client-side
  search with empty state.
- ar/en i18n keys under protocol.bookings.

Notes:
- Pre-existing push.ts typecheck errors are unrelated to this task.
- Architect review: PASS, no blocking issues.
2026-07-07 12:26:19 +00:00
Riyadh 97f0b9217d Task #668: Rename Protocol side-menu items (bilingual AR/EN)
Relabeled three Protocol (العلاقات العامة والمراسم) menu items to match real usage:
- photos: "تصوير" -> "الصور" / "Photos"
- gifts (item catalog where أصناف are added): "الإهداءات" -> "المستودع" / "Warehouse"
- issues (giving gifts out): "طلبات الصرف" -> "الإهداءات" / "Dedications"

Swapped menu icons to match new meanings: gifts->Warehouse, issues->Gift,
photos->ImageIcon. Removed now-unused imports (PackageCheck, Camera).

Grouped المستودع + الإهداءات under one expandable submenu (parent label
"الهدايا"/"Gifts", new i18n key protocol.tabs.giftsGroup, HandHeart icon,
ChevronDown toggle). Group auto-expands when a child tab is active; state
giftsGroupOpen. RTL-aware (ms-auto chevron, md:ps-6 child indent).

Aligned in-screen wording with new names in both locales:
dashboard.pendingIssues, issues.new, issues.empty reworded from
issuance/صرف to dedication/إهداء.

Deviation: submenu grouping and parent-group naming ("الهدايا") were an
interpretation of the user's "sub-menu" request (user_query was unavailable
during planning). Kept scope tight: remaining secondary "issuance/صرف"
strings in reports/actions/dashboard stats (e.g. giftsIssuedThisMonth,
actions.issued, reports.byGift) were left unchanged and proposed as a
follow-up.

Unchanged (deliberately): internal tab keys (gifts/issues/photos) and URL
slugs (gifts, gift-issues, photos) so deep links keep working. No feature,
API, or data changes.

Verified: pnpm --filter @workspace/tx-os run typecheck passes; architect
code review passed after issues icon corrected to Gift.
2026-07-07 11:58:20 +00:00
Riyadh a8f4693fce Task #666: Today & Book buttons in room-booking calendar view
Added two quick actions inside the Protocol bookings "Calendar" view's day
panel header (artifacts/tx-os/src/pages/protocol.tsx):
- "Today" (اليوم): outline button that resets selectedDay to new Date(), so
  the month picker, day time-grid, and red now-line all snap to today.
- "Book" (حجز): primary button gated by the same c?.canRequest capability as
  the existing toolbar new-booking button. Opens the new-booking dialog
  pre-filled with the currently selected day.

Implementation:
- Extended bookingDialog state with an optional `day?: Date`.
- BookingDialog now accepts an `initialDay?: Date` prop; for new (non-edit)
  bookings it pre-fills startsAt=09:00 and endsAt=10:00 on that day via a
  local dayAt(hour) helper (reuses toLocalInput). Edit flow unchanged.
- Added i18n keys protocol.bookings.today / protocol.bookings.book to both
  ar.json and en.json. Buttons use logical spacing (me-1) so they mirror in
  Arabic RTL.

No backend/API changes. List/Calendar toggle unaffected. tx-os typecheck
passes; HMR clean, no console errors. Screenshot tool unavailable (no
artifact registered in this repo), verified via typecheck + logs.
2026-07-07 10:34:16 +00:00
Riyadh 83f07c7a22 Task #662: Google-Calendar-style room booking calendar view
Replaced the Protocol bookings "Calendar" view (previously an agenda list
grouped by day) with a two-panel Google-Calendar layout in
artifacts/tx-os/src/pages/protocol.tsx:
- LEFT: a mini month day-picker (reuses components/ui/calendar.tsx,
  react-day-picker single mode) driving a selectedDay state. Days that
  have bookings get a small sky dot via a `booked` modifier.
- RIGHT: a single-day vertical time-grid (BookingDayGrid) over working
  hours 07:00–22:00 (56px/hour). Bookings render as absolutely-positioned
  blocks: top = start offset, height = duration (min 22px), colored by
  status (BOOKING_BLOCK_STYLE) with an inline-start accent border and a
  title/room/time label.
- Overlapping bookings are laid out side-by-side via interval-partition
  column assignment (layoutDayBookings), chained overlaps share a column
  count.
- A live RED current-time line + red dot (useNow, 60s) shows only when the
  selected day is today and within grid hours.

Details:
- View-only (no drag/create); single-day grid only. No backend changes.
- RTL/i18n: logical props (insetInlineStart, border-s) so it mirrors in
  Arabic; added protocol.bookings.emptyDay to ar.json + en.json.
- Blocks with no visible intersection with the grid range are skipped
  (fixes a phantom-block bug for bookings fully before 7am / after 10pm,
  found in code review).
- Removed the now-unused groupBookingsByDay helper.
- List toggle preserved. tx-os typecheck passes.
2026-07-07 10:27:15 +00:00
Riyadh a36adb5e9e Add download for protocol visit photos (Task #660)
Users could upload photos into Protocol "تصوير" albums but had no way to
download them — clicking a photo only opened it inline. This adds per-photo
downloads and a whole-album ZIP download.

Backend:
- objectStorage.ts: new exported extensionForContentType() mapping content
  types to file extensions (stored paths are extension-less UUIDs).
- storage.ts: opt-in ?download=1 mode on GET /storage/objects/*path sets
  Content-Disposition: attachment (with optional ?name base + derived ext);
  default inline behavior and per-entity authz unchanged.
- protocol.ts: new GET /protocol/photo-albums/:id/download streams all album
  photos as a ZIP (guarded by requireProtocolAccess), named from the album;
  unreadable objects are skipped rather than failing the archive. Empty album
  returns 404.
- Added archiver@8 dependency (+@types/archiver dev).

Frontend (protocol.tsx):
- Per-photo download button (attachment, name = album-index) in album detail.
- "تحميل الكل" / "Download all" ZIP button in album header (hidden when empty).
- Added photoDownloadSrc/albumDownloadSrc helpers, Download icon.
- AR/EN locale strings protocol.photos.download / downloadAll.

Notes / deviations:
- archiver v8 is ESM-only with no callable default export; used
  `new ZipArchive(...)` instead of `archiver("zip")`. Recorded in memory.
- Could not run authenticated e2e (seed admin password is a secret). Verified:
  api-server typecheck + build pass, tx-os typecheck passes, server healthy,
  new routes wired (401 auth-required not 404). Pre-existing typecheck errors
  in executive-meetings.ts / push.ts are unrelated and untouched.
2026-07-07 09:34:06 +00:00
Riyadh 224e69f111 Add functionality to manage visit photos and albums for protocols
Introduce new database tables for photo albums and photos, add API endpoints for CRUD operations on albums and photos, and implement frontend components for managing photo albums.
2026-07-07 09:14:07 +00:00
Riyadh 1d7ae96b9c Update tab name for dedications in protocol app
Rename "Gifts & Shields" to "Dedications" in English and "الهدايا والدروع" to "الإهداءات" in Arabic within the protocol app's locale files.
2026-07-07 08:06:53 +00:00
Riyadh e0d85ba12c feat(protocol): shareable public booking link bar in Room Bookings
Task #655: staff need a one-click way to copy and share the public
(no-login) room-booking link with guests.

- Add a link bar at the top of the "حجوزات القاعات" (Room Bookings)
  section showing the absolute public URL, with a "نسخ الرابط" copy
  button that gives toast + a temporary "تم النسخ" confirmation state.
- URL is built from window.location.origin + BASE_URL (trailing slash
  stripped) + "/protocol/request", so it works on root and subpath
  deploys and when pasted into a browser/WhatsApp.
- Clipboard uses navigator.clipboard with a textarea+execCommand
  fallback; fallback now checks execCommand's return and reports failure
  (destructive toast) instead of falsely showing success. Textarea is
  removed in a finally block. Copied-state timer stored in a ref and
  cleared on repeat clicks to avoid a reset race.
- URL text forced dir="ltr" inside the RTL layout; new i18n keys
  (shareLinkTitle, copyLink, linkCopied, linkCopyFailed) added under
  protocol.bookings in both ar.json and en.json.

Verified: tsc typecheck passes; /protocol serves 200; module transforms
cleanly via Vite; architect review issues (fallback false-success,
timer race) fixed.
2026-07-06 12:20:17 +00:00
Riyadh 74327027c3 Add attendee count field to booking requests and display it
Updates API schemas and database to include attendee count for bookings, modifies the public request form to accept this new optional field, and displays the attendee count on the booking details.
2026-07-06 11:33:58 +00:00
Riyadh 50030ea29f Add public interface for submitting room booking requests
Introduces a new public-facing API endpoint and UI for submitting room booking requests without authentication, including rate limiting and input validation.
2026-07-06 11:18:14 +00:00
Riyadh 2f84bd2c6f Task #649: complete Public Relations & Protocol module (fix 8 review gaps)
Addresses the 8 spec gaps that blocked prior completion, all additive and
scoped to the protocol_* module / /protocol routes. executive-meetings untouched.

Backend (api-server/routes/protocol.ts, lib/db schema, seed):
- External meetings create as "pending" (status dropped from create schema);
  added approve + reject endpoints with state-conditional WHERE status='pending'
  and 409 on contention; patch status enum restricted to completed/cancelled.
- Added approvedBy/approvedAt/rejectionReason columns + status index; default
  status now "pending".
- Booking reject and gift-issue reject made state-conditional (409 on 0 rows).
- Dashboard: roomsAvailableNow (NOT EXISTS), giftsIssuedThisMonth/Qty;
  upcomingExternal filtered to future pending|approved.
- Reports: added externalMeetings summary counts by status.
- Seed: room names corrected (AR + EN).

Frontend (tx-os/pages/protocol.tsx, App.tsx, locales):
- URL-synced tabs under /protocol/:tab (slug<->tab maps).
- Dedicated Rooms tab (moved out of Gifts); RoomDialog isActive toggle.
- External approve/reject buttons + rejectionReason display; ExternalDialog
  create omits status, edit limited to completed/cancelled.
- Dashboard cards clickable to navigate; reports external section.
- Bookings list/calendar (grouped-by-day) view toggle.
- New i18n keys in ar.json + en.json.

Verification: tx-os + protocol.ts typecheck clean; drizzle push + re-seed done;
both workflows restarted; /api/protocol/me returns 401 unauth; architect
re-review approved. Pre-existing typecheck errors in executive-meetings.ts and
push.ts are unrelated and were not touched.

Note: had to rebuild lib/db declarations (tsc --build --force) so api-server
project references picked up the new schema columns.
2026-07-06 09:46:12 +00:00
Riyadh 874d49c439 Add Public Relations & Protocol module (Task #649)
New, fully separate app module "العلاقات العامة والمراسم / Public
Relations and Protocol". Does not touch executive-meetings; all new
tables are prefixed protocol_ and are additive-only, routes under
/protocol.

Includes:
- 6 protocol_ tables (rooms, room bookings, external meetings, gifts,
  gift issues, audit log) in lib/db.
- API routes + role-scoped middleware (protocol_super_admin, pr_manager,
  officer, requester, viewer) with requireProtocolAccess gating.
- Room-booking conflict prevention (overlap rule newStart<existingEnd
  AND newEnd>existingStart vs pending/approved) with AR error message.
- Gifts/shields (دروع) registry + issuance with stock tracking.
- Bilingual AR/EN frontend page (tabs + dialogs) at /protocol, app tile,
  i18n keys, and seed data (roles, permission, default rooms).

Concurrency/security hardening from code review:
- All role guards now validate users.isActive (inactive-user bypass fix).
- Per-room SELECT ... FOR UPDATE lock serializes booking check-then-write.
- Approve paths re-read inside the transaction and use state-conditional
  updates (WHERE ... AND status='pending' RETURNING).
- Gift issuance claims the issue conditionally, then does an atomic
  conditional stock decrement; failure rolls back the claim.

Verified: drizzle push (6 tables), seed, frontend + api-server typecheck
(only pre-existing errors in untouched files), unauth gating returns 401,
conflict predicate confirmed. Architect review PASS.
2026-07-06 09:25:40 +00:00
Riyadh 7bfb4afd1a #635: External meetings + auto-numbering
User asked for three things in the executive-meetings module:
1. Remove the "daily number" input from the add/edit dialog — numbering
   is auto-assigned by the server already, so the field was just noise.
2. Add an "اجتماع خارجي" (external meeting) toggle. When ON, the row
   is force-tinted red in the schedule grid.
3. External meetings must be excluded from the auto-postpone flow:
   the alert "Postpone" button is disabled, and the cascade shift
   skips them as followers.

Changes
- lib/db/src/schema/executive-meetings.ts
  + `is_external boolean not null default false` column. Pushed via
    drizzle-kit (no destructive migration; defaults backfill).
- artifacts/api-server/src/routes/executive-meetings.ts
  + `isExternal` added to base zod fields, POST insert, PATCH update.
  + POST/PATCH force `rowColor='red'` when `isExternal=true`.
  + POST /:id/postpone-minutes rejects external meetings with
    HTTP 409 + code `external_meeting_no_auto_postpone`.
  + `computeCascadeShift` filters out external followers (preview +
    writer stay in lockstep, as the comment promises).
- artifacts/tx-os/src/pages/executive-meetings.tsx
  + Meeting type + MeetingFormState gain `isExternal`; dropped the
    `dailyNumber` field from the form state entirely.
  + `emptyMeetingForm` / `openEdit` updated; ManageSection.save() and
    the inline ScheduleSection save body now send `isExternal` and
    never send `dailyNumber`.
  + MeetingFormDialog: removed the daily-number FormRow and added a
    Switch-based external-meeting FormRow with localized hint.
  + `rowColors` memo coerces to "red" whenever `m.isExternal`.
  + Audit-diff "interesting" list now includes `isExternal`.
- artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx
  + Meeting type gains optional `isExternal`.
  + "Postpone" button disabled + tooltip when meeting is external.
- locales: added `field.isExternal`, `field.isExternalHint`,
  `field.isExternalOn/Off`, and `alert.externalCannotPostpone` in
  both ar.json and en.json. Existing dailyNumber keys kept (still
  referenced by the manage-table column header / cell display).

Post-review hardening (from architect pass)
- PATCH now coerces rowColor → 'red' for any PATCH that touches a row
  that is (or becomes) external. Prevents PDF/export paths that read
  raw rowColor from showing a non-red tint for externals.
- Schedule row quick-actions popover: the "Postpone" button is now
  disabled with a localized tooltip when the meeting is external,
  matching the upcoming-meeting alert behavior.
- PostponeDialog.handleErr maps the 409 code
  `external_meeting_no_auto_postpone` to the localized
  `alert.externalCannotPostpone` toast so users see a real reason
  instead of the raw English error string.

Notes
- The `dailyNumber` column stays in DB (heavily used for slot
  persistence and the unique index); only the form input was removed.
  Server's `nextDailyNumber()` already auto-assigns when the field is
  absent on POST, and PATCH leaves it untouched when absent.
- Two pre-existing TS errors remain in api-server (`isHighlighted`
  type-inference quirk on `z.union(...).transform(...)` and a
  font_settings comparison) — not in scope for this task.
- Tests not added; verification by typecheck on changed surfaces and
  workflow-restart smoke. No e2e harness was wired.
2026-05-25 11:59:54 +00:00
Riyadh b69a2d1728 Drag-to-reorder service cards (iOS jiggle mode)
Long-press a service tile (~500 ms) → grid enters edit mode: every
card jiggles, a floating "تم / Done" pill appears, and cards become
draggable via @dnd-kit. Drop on another tile to reorder. Tap Done or
outside the grid to persist; the new order is shared globally via
servicesTable.sortOrder.

Backend
- POST /api/services/reorder (admin-gated, requireAdmin)
- Validates full-set match (no missing/dup/unknown IDs)
- Transactional sortOrder rewrite — no partial writes
- Route registered before /services/:id to avoid path-param collision
- OpenAPI op + ReorderServicesBody Zod schema → regenerated client

Frontend
- artifacts/tx-os/src/pages/services.tsx rewritten
- PointerSensor (distance 6) + TouchSensor (delay 150 ms) — long-press
  and tap-to-open stay distinct
- touch-none only applied while in edit mode so normal scrolling is
  unaffected outside it
- exitEditMode awaits mutation + invalidate before flipping editMode,
  preventing snap-back from stale react-query cache
- exitingRef guard against duplicate exit calls (Done button +
  outside-tap handler firing for the same gesture)
- Non-admins can enter edit mode (haptic affordance) but local reorder
  is silently reverted on exit
- i18n: services.editMode.{done,hint,saveFailed} added to ar.json/en.json

Pre-existing unrelated typecheck errors in push.ts and
executive-meeting-font-settings.ts are not touched by this change.
2026-05-20 11:51:28 +00:00
Riyadh bb501b8eb3 Improve audit log filtering with a searchable user list and better date formatting
Introduces a combobox for filtering audit logs by actor, replaces multiple date formatting calls with a dedicated helper function, and updates locale files for new filter UI elements.
2026-05-19 12:11:54 +00:00
Riyadh 295f6cd53a Remove Notifications tab from Executive Meetings (Task #616)
The Notifications tab inside Executive Meetings only ever rendered a
log of `meeting_created` fan-out rows (the only event type currently
wired), was filtered to the selected schedule date, and overlapped
with the Audit log. For real users it was almost always empty and
just took space in the tab strip — see attached iPhone screenshot in
the conversation. User asked explicitly to delete it.

Changes:

1. `artifacts/tx-os/src/pages/executive-meetings.tsx`
   - Removed the `{ key: "notifications", icon: Bell }` entry from
     the `SECTIONS` array so the tab no longer appears in the strip.
   - Removed the `case "notifications": return me.canRead;` branch
     from `isSectionVisible`.
   - Removed the `{section === "notifications" && …}` render branch
     and the `<NotificationsSection …/>` JSX.
   - Deleted the `NotificationsSection` component entirely (~90 lines)
     and the unused `NotificationRow` type.
   - Left the `Bell` import in place — still used by the bell button
     elsewhere in the page (line ~8580).

2. `artifacts/tx-os/src/locales/{ar,en}.json`
   - Removed the entire `executiveMeetings.notificationsPage` block
     (headers, status labels, type labels, empty state). All keys
     were verified to only be consumed by the just-deleted component.
   - Left `executiveMeetings.notifications.*` alone — those are the
     per-user preferences UI strings and are unrelated.

3. Backend left untouched on purpose:
   - `GET /api/executive-meetings/notifications` still exists.
   - `recordExecutiveMeetingNotifications` still runs on meeting
     create and still writes to `executive_meeting_notifications`.
   - The bell icon, push, and notification preferences UI are
     unaffected because they read `notificationsTable`, not the
     executive-meeting-specific table.

The orphaned `GET /executive-meetings/notifications` route now has
no frontend caller. I'm leaving it in this commit (it's harmless,
still permission-gated) and proposing a follow-up to remove it
cleanly instead of expanding scope here.

Code review: not run yet — will run after committing per the
standard flow.
2026-05-19 12:02:41 +00:00
Riyadh eff3dbf96d Task #613: replace per-app hide toggles with single Dock visibility switch
The per-app hidden feature shipped in #609 was rejected — user wanted one
toggle that hides/shows the entire bottom AppDock bar, leaving Home apps
untouched.

Removed:
- lib/db/src/schema/user-hidden-apps.ts (and index.ts export); table
  dropped via `pnpm --filter @workspace/db run push`
- GET /me/apps and PUT /me/apps/:appId/hidden routes
- getHiddenAppIdsForUser + getVisibleNonHiddenAppsForUser helpers
- MyApp + UpdateMyAppHiddenBody OpenAPI schemas
- MyAppsBody settings UI + related i18n keys
- Regenerated api-client-react + api-zod from the trimmed spec
- Reverted GET /apps back to getVisibleAppsForUser

Added:
- artifacts/tx-os/src/hooks/use-dock-visible.ts — localStorage-backed
  preference with a custom window event for in-tab sync and the native
  `storage` event for cross-tab sync. Default = true.
- DockBody in settings-panel: single ToggleRow under a new "App dock"
  section ("settingsPanel.section.dock" / "settingsPanel.dock.show")
- AppDock now returns null when the preference is off and clears
  `--app-dock-height` so page padding doesn't stay reserved.

Code review: PASS (architect). No remaining references to the removed
infra. Typecheck shows only pre-existing errors in executive-meetings.ts
and push.ts unrelated to this change.

Follow-up: publish to Gitea + redeploy on Mac (proposed as a follow-up
task).
2026-05-19 11:35:13 +00:00
Riyadh 876b8b1545 Add ability to show and hide applications in settings
Update settings panel and locales to include functionality for hiding and showing applications, adding new aria-labels for improved accessibility.
2026-05-19 11:08:06 +00:00
Riyadh d7d8ab3a2a Task #609: Per-user app enable/disable in Settings
- New `user_hidden_apps` table (userId+appId composite PK, cascade) in
  lib/db/src/schema/user-hidden-apps.ts; registered in schema index;
  pushed to dev DB via drizzle-kit.
- Backend (artifacts/api-server/src/routes/apps.ts):
  - GET /me/apps — returns every globally-active app visible to the
    user with a `hidden` flag.
  - PUT /me/apps/:appId/hidden — toggles a row in user_hidden_apps,
    gated by getVisibleAppsForUser + isActive so a user can't toggle
    apps they can't reach.
  - GET /apps (Home/Dock) now uses getVisibleNonHiddenAppsForUser so
    hidden apps disappear immediately.
- lib/appsVisibility.ts: added getHiddenAppIdsForUser and
  getVisibleNonHiddenAppsForUser helpers.
- OpenAPI: added /me/apps + /me/apps/{appId}/hidden, MyApp and
  UpdateMyAppHiddenBody schemas; regenerated api-zod + api-client-react.
- Frontend (settings-panel.tsx): added "My apps" accordion section as
  first GroupItem with MyAppsBody — Switch per app, optimistic update,
  invalidates getListMyAppsQueryKey + getListAppsQueryKey so Home/Dock
  refresh without reload.
- Translations: added settingsPanel.section.myApps + settingsPanel.myApps
  in ar.json + en.json.
- Code review fix: /me/apps and PUT gating filter isActive even for
  admins, so inactive apps don't appear in the Settings list.
- Proposed follow-up #611 (Playwright test that hidden apps disappear
  from Home and Dock).
2026-05-19 11:06:21 +00:00
Riyadh 97a669ba13 Improve how user roles are managed and displayed on the admin page
Introduce separation of direct and inherited roles in user profiles and API responses. Modify the admin UI to disable the "order receiver" toggle when a role is inherited, providing a clearer user experience. Update API endpoints and schemas to reflect these changes, alongside locale updates for translated strings.
2026-05-19 06:41:10 +00:00
Riyadh c9b0afeb55 Task #601: iOS PWA push notifications enablement
PWA + Web Push infra was already fully built (SW, VAPID auto-gen,
subscribe/unsubscribe API, sendPushToUser called from orders/meetings/
notes/replies, iOS-PWA detection hook). The user wasn't getting
lock-screen alerts because iPhone/iPad weren't installed as a PWA —
iOS only delivers Web Push from a Home Screen icon, not a Safari tab.

This commit polishes the iOS install path so the gap is obvious:

1. push-enable-prompt.tsx — Added a dedicated iOS install-steps card
   variant that renders on iPhone/iPad Safari when running outside
   standalone mode. Shows Share → Add to Home Screen → open from icon.
   Independent 14-day dismiss memory from the regular Enable card.

2. notification-settings.tsx — PushToggleRow now detects iOS-non-
   standalone and shows the unsupported_ios_safari hint inline with a
   disabled toggle, instead of teasing an Enable affordance that always
   fails.

3. use-push-subscription.ts — Exported isIosSafariNonStandalone() so
   both surfaces share the detection logic (was private).

4. lib/push.ts (server) — Added info logs in loadVapid() for both the
   env-key and disk-key paths, removed an accidental duplicate disk-read
   block introduced during editing. Now the production redeploy check
   is a one-liner: grep for "VAPID keys loaded" in the api logs.

5. ar.json / en.json — Added notifSettings.push.iosInstall.{title,desc,
   step1,step2,step3} bilingual strings for the new card.

No DB migrations. No deps changed. tx-os tsc passes; api-server tsc
errors are pre-existing (routes/push.ts handler type, font_settings)
and not touched by this change.

Smoke test on real iPhone/iPad still owed (out-of-band — task step #4
is a manual verification on the user's devices after redeploy).
2026-05-18 13:13:12 +00:00
Riyadh 65b4890eca Update quick note button label to be shorter
Update the label for the quick note button in the executive meetings header from "Take a note..." to "Note" in English and "اكتب ملاحظة…" to "ملاحظة" in Arabic.
2026-05-18 13:01:59 +00:00
Riyadh c20b59e656 Task #590: Daily meetings — clock, inline notes, fullscreen header
Three small UX fixes on /executive-meetings (schedule tab):

1) HeaderClock: dropped the digital "HH:MM:SS م" string next to the
   weekday — the analog dial already conveys the live time, so it was
   redundant. Bumped AnalogClock size from 36 to 44 so the dial reads
   cleanly without the digital crutch. Weekday + date stay (calendar
   info, not clock info). Removed the now-unused `useState`/`useEffect`
   ticker inside HeaderClock — AnalogClock has its own.

2) "اكتب ملاحظة" yellow side tab no longer navigates away to /notes.
   Added a transient `quickNoteOpen` local state in
   ExecutiveMeetingsPageInner. The tab's onClick now toggles it on; a
   new `InlineQuickNotePanel` is rendered above <ScheduleSection> with
   an auto-focused textarea, Cancel/Send buttons, Cmd/Ctrl+Enter to
   submit, Esc to close. Submit POSTs to /api/notes with the existing
   contract ({ content, color: "yellow" }) and closes on success. The
   side tab hides while the panel is open so they don't double up.
   State auto-resets when the user leaves the schedule tab.

3) Fullscreen exit pill overlap: the floating "الخروج من ملء الشاشة"
   pill (fixed top-3 end-3) was covering the schedule table's "الوقت"
   column header. Added `pt-12 sm:pt-14` to the fullscreen branch of
   <main> so the table starts below the pill on iPad landscape
   (1024-wide). Non-fullscreen layout unchanged.

i18n: added executiveMeetings.quickNote.{placeholder,send} in EN+AR;
existing executiveMeetings.quickNote.label reused for the panel
header. All keys also have inline fallbacks so missing translations
don't break.

Drive-by: fixed a TypeScript narrowing error in admin.tsx left over
from the prior task's review-comment cleanup (data.startedAt is
already typed as string, so the typeof check produced `never` in the
else branch — replaced with a direct call).

Verified: pnpm --filter @workspace/tx-os exec tsc --noEmit passes
clean, Vite HMR updated cleanly in dev.
2026-05-18 11:04:27 +00:00
Riyadh 608e15cf91 Task #589: Live build stamp + server start time in System Updates panel
Problem: After `git pull && docker compose up -d --build api` on the
Mac, the admin → System Updates panel kept showing the same version
(0.1.0-dev) and the same build (20260517.8a5bd5db) because both come
from version.json, a hand-edited file that nothing rewrites on every
build. No visible signal that a new image was actually running.

Two independent signals were added — neither needs editing version.json
by hand:

1) Auto-stamp version.json at Docker build time
   - docker/api-server.Dockerfile: new ARGs GIT_SHA and BUILD_DATE.
     A `node -e` step rewrites version.json's `build` field to
     `${YYYYMMDDTHHmm}.${sha}` (e.g. 20260518T1042.71da8e01). When
     the args are missing/"unknown", version.json is left untouched
     so plain `docker build` and Replit `pnpm dev` still work.
   - docker-compose.yml: api.build.args wires through GIT_SHA and
     BUILD_DATE with `${GIT_SHA:-unknown}` fallbacks.
   - scripts/redeploy.sh: exports GIT_SHA (git rev-parse --short HEAD)
     and BUILD_DATE (date -u +%Y-%m-%dT%H:%M:%SZ) before `docker
     compose build`, so the helper script just works.

2) startedAt timestamp from the API
   - artifacts/api-server/src/routes/system.ts: SERVER_STARTED_AT
     captured at module load (frozen for the process lifetime) and
     added to every branch of GET /system/version (not-configured,
     error, invalid-version, up-to-date, update-available, catch).
   - lib/api-spec/openapi.yaml: added `startedAt: date-time` to
     SystemVersionResponse (required). Regenerated api-client-react
     and api-zod via `pnpm --filter @workspace/api-spec run codegen`.

3) Admin UI surfacing both signals
   - artifacts/tx-os/src/pages/admin.tsx: new formatBuildStamp parses
     `YYYYMMDD[THHmm].sha` and renders a localized date next to the
     raw token. A new "Server started" line below the build number
     formats the boot timestamp via the existing formatChecked helper.
   - artifacts/tx-os/src/locales/{en,ar}.json: added
     admin.systemUpdates.serverStartedAt ("Server started" /
     "بدأ تشغيل الخادم").

4) Docs
   - replit.md: documented how to verify a real redeploy via the
     System Updates panel and the manual one-liner for `docker
     compose build` without the helper script.

Verified: codegen succeeds, API workflow restarted clean, Vite served
fine. Pre-existing typecheck errors in push.ts and font-settings are
unrelated and untouched.

Mac next steps:
  cd ~/Downloads/TX && git pull && ./scripts/redeploy.sh
Then open admin → System Updates: build line should show a new
`YYYYMMDDTHHmm.sha` stamp and "Server started" should reflect the
new boot time.
2026-05-18 10:39:17 +00:00
Riyadh 2dbb88308f feat(meetings): center clock, fullscreen toggle, quick-note tab (#583)
- Replace HeaderClock with richer analog+weekday+digital block, always
  centered in the header (both president and normal views). Drop the
  PresidentClockBar render from the toolbar; the dead helper is left
  in place but unused.
- Lift fullscreen state to ExecutiveMeetingsPageInner with
  sessionStorage persistence, Esc handler, auto-exit when the user
  leaves the schedule tab. Hide the page header when active and show a
  floating "Exit fullscreen" pill. New Maximize/Minimize toggle in the
  schedule toolbar portal.
- Add MeetingsQuickNoteTab: a fixed amber vertical pill pinned to the
  left edge of the schedule view that navigates to /notes?new=1.
- notes.tsx: detect ?new=1, switch to the Active tab, bump a numeric
  openTrigger that the top Composer reacts to by opening + focusing
  the textarea, then scrub the query param so navigation doesn't
  re-trigger.
- i18n: add executiveMeetings.fullscreen.{enter,exit} and
  executiveMeetings.quickNote.label in ar.json and en.json.

tsc clean. Push to Gitea still blocked by Tailscale; user to retry.
2026-05-18 09:32:55 +00:00
Riyadh b6e26f47f3 feat(meetings): center clock, fullscreen toggle, quick-note tab (#583)
- Replace HeaderClock with richer analog+weekday+digital block, always
  centered in the header (both president and normal views). Drop the
  PresidentClockBar render from the toolbar; the dead helper is left
  in place but unused.
- Lift fullscreen state to ExecutiveMeetingsPageInner with
  sessionStorage persistence, Esc handler, auto-exit when the user
  leaves the schedule tab. Hide the page header when active and show a
  floating "Exit fullscreen" pill. New Maximize/Minimize toggle in the
  schedule toolbar portal.
- Add MeetingsQuickNoteTab: a fixed amber vertical pill pinned to the
  left edge of the schedule view that navigates to /notes?new=1.
- notes.tsx: detect ?new=1, switch to the Active tab, bump a numeric
  openTrigger that the top Composer reacts to by opening + focusing
  the textarea, then scrub the query param so navigation doesn't
  re-trigger.
- i18n: add executiveMeetings.fullscreen.{enter,exit} and
  executiveMeetings.quickNote.label in ar.json and en.json.

tsc clean. Push to Gitea still blocked by Tailscale; user to retry.
2026-05-18 09:26:12 +00:00
Riyadh 08af3fca72 notes: file-manager layout, folders section header, kill grid gap (#578)
Cleans up the Notes page after a user creates their first folder:

1. FoldersRail: in `rail` mode (every view except "All notes"), insert a
   small "My folders" / "مجلداتي" section header above the user's folder
   list so it no longer visually nests under the dark "Unfiled" pill.
   New i18n key `notes.folders.myFolders` in en.json and ar.json.

2. notes.tsx filteredNotes: when `folderSelection.kind === "all"`, hide
   notes whose `folderId !== null`. Combined with the existing
   FoldersBrowser cards this gives a file-manager feel — notes inside a
   folder are reachable by opening the folder card and no longer appear
   in the flat list, so users stop thinking move = copy. Counts on the
   "All notes" rail entry stay as the total (totalCount prop unchanged).

3. notes.tsx grid: replaced the two separate grid cells for
   composer/content with a single wrapper that uses `display: contents`
   on mobile (children flow into the outer grid honoring order-1/2/3)
   and `flex flex-col gap-4` on md+ (composer and content stack in the
   same cell, no empty grid-row gap when the FoldersRail is taller than
   the composer). Removed unused conditional row-start/row-span on the
   inner content div.

Out of scope per plan: dnd-kit behavior, shared-with-me logic, composer
or folder card redesign.

Validation: `pnpm --filter @workspace/tx-os exec tsc --noEmit` clean.
Vite HMR successful after fix; no babel/JSX errors.
2026-05-18 08:00:28 +00:00
Riyadh e44c9a8976 Improve handling of notification permission prompts
Update notification permission logic to differentiate between denied and dismissed states, and adjust locale messages accordingly.
2026-05-17 17:21:18 +00:00
Riyadh bf5bfa29c4 #576: diagnose push enable + open full edit page from quick actions
Three connected fixes on the Executive Meetings schedule.

1) Push enable diagnostics (use-push-subscription.ts):
   - `enable()` now returns { ok, reason } instead of a bare boolean.
   - Reasons: unsupported, unsupported_ios_safari, permission_denied,
     vapid_unavailable, subscribe_failed, server_rejected.
   - Detects iPhone/iPad Safari not running as a PWA (checks
     navigator.standalone + display-mode media query, with iPadOS
     MacIntel + maxTouchPoints fallback) and short-circuits with
     unsupported_ios_safari so the user gets a concrete
     "open from Home Screen first" message instead of a silent fail.
   - Wrapped VAPID fetch and pushManager.subscribe in their own
     try/catches so each failure mode maps to its own reason.

2) Specific failure toasts (push-enable-prompt.tsx,
   notification-settings.tsx):
   - Both call sites now look up
     `notifSettings.push.errors.<reason>` and fall back to the
     legacy generic copy if the key is missing.
   - Added enableSuccess toast on the toggle path too — previously
     only the prompt-card path showed feedback on success.
   - New AR + EN strings under notifSettings.push.errors.*.

3) Quick-actions row sheet (executive-meetings.tsx):
   - Edit + Postpone buttons now sit SIDE BY SIDE on every viewport
     (flex + flex-1 + min-w-0 instead of flex-wrap + min-w-[11rem]),
     same height, equal width — fixes the stacked / over-sized Edit
     button shown in the user's iPhone screenshot.
   - "Edit" no longer opens the small Meeting edit Dialog. It now
     flips the whole schedule into edit mode (same as the toolbar
     pencil) and paints a transient accent ring on the originating
     row (~1.6s) + smooth-scrolls it into view. Matches image 3 in
     #576 where the user expects to see the full editable list.
   - Added `highlightEdit` prop on MeetingRow that composes an
     extra inset+outer ring on top of the existing quickOpen /
     selection / current / press shadows, plus a soft background
     tint, and clears automatically when ScheduleSection clears
     highlightEditRowId.

Out of scope (left for follow-up tasks): server-side push delivery
(payloads/sounds), the edit-mode UI itself.
2026-05-17 17:19:42 +00:00
Riyadh 8a5bd5db51 Add system updates page to admin section
Add a new API endpoint and UI section for checking system updates, including internationalized locale strings and necessary dependency updates.
2026-05-17 14:59:36 +00:00
Riyadh e79a8dee41 #570: stronger selected-row highlight + Edit button in Quick Actions
Task: on iPad the bulk-selected row was nearly invisible (only a 2px
inset navy ring), and the row-tap Quick Actions dialog had only a
Postpone button — user asked for a real visible selection fill and
an Edit (pencil) action wired to the existing edit-meeting flow.

Changes (artifacts/tx-os/src/pages/executive-meetings.tsx):
- MeetingRow: add `selectionBg = rgba(11,30,63,0.18)` painted on the
  row's `<tr>` backgroundColor in addition to the existing
  `selectionShadow` 2px navy ring. Composition order in trStyle is
  `quickOpenBg ?? selectionBg ?? baseBg` so an open quick-actions
  surface still wins (transient action state), user-picked row
  colours still show when nothing is selected, and the navy tint
  reads clearly on iPad against the table grid.
- MeetingRow: new optional `onQuickEdit?: () => void` prop. Quick
  Actions Dialog gains a pencil-icon "Edit" button before the
  existing Postpone button, wrapped in `flex flex-wrap gap-2` so the
  two buttons sit side-by-side on iPad/desktop and stack on narrow
  viewports. Button only renders when a handler is wired
  (defensive — `quickActionsCanMutate` already gates the surface).
- ScheduleSection: host a local `editingMeeting` + `editingSaving`
  state plus a `MeetingFormDialog` mount, with a save handler whose
  body mirrors ManageSection's `save()` exactly (same PATCH payload
  projection, same `wrapAsParagraph` / attendee shaping, same
  toast/error translation). Edit button on a row opens this in-place
  dialog instead of switching the user to the Manage tab.
- Locale: add `executiveMeetings.quickActions.edit` = "تعديل" / "Edit".

Notes:
- No new icon import needed — `Pencil` was already in the lucide
  import group.
- Selection ring (`#0B1E3F`) and tint use the same navy so they
  read as one cohesive selected state, not competing layers.
- Pre-existing `use-push-subscription.ts` TS noise unchanged.
2026-05-17 14:40:34 +00:00
Riyadh 46414ec4bd #568 silent delete for finished orders
User requested that deleting a finished order from My Orders happen
silently — one click on the trash icon, no confirmation dialog, no
success toast. Just click and the card disappears.

Changes:
- Removed the delete-confirm AlertDialog from OrderCard along with
  its `confirmDeleteOpen` state. The trash icon now calls
  `handleDelete` directly.
- Removed `toast({ title: t("myOrders.deleted") })` from
  `scheduleDelete`. The 7-second soft-delete grace window stays in
  place (pendingDeleteIds filter + setTimeout) so the API DELETE
  still fires after the window. The `deleteFailed` error toast is
  preserved so silent API failures don't lose orders.
- Dropped the now-unused locale keys from ar.json and en.json:
  `deleteConfirmTitle`, `deleteConfirmBody`, `deleteConfirm`,
  `deleted`.
- Removed the unused `AlertDialogDescription` import (the remaining
  cancel-confirm dialog uses only the title).

Cancel-order flow remains untouched (still shows confirm dialog and
undo toast as in #566).
2026-05-17 14:13:42 +00:00
Riyadh 54dc04f12d #566 clean up My Orders page (3 removals)
Per user request on the طلباتي page:

1. Removed the "Clear finished orders" bulk button entirely. The button
   was showing as a raw i18n key (myOrder c...) for the user and they
   asked for it gone. Dropped the related AlertDialog, confirmClearOpen
   state, handleClearFinished handler, and the locale keys
   clearFinished_*, clearConfirmTitle/Body_*, clearConfirm,
   clearedCount_*, and clearedPartial in both ar.json and en.json.

2. Removed the "تراجع / Undo" ToastAction shown after deleting an
   order. The toast now displays only the "Order deleted" title; the
   7-second timer + pendingDeleteIds soft-delete window is preserved
   so the actual DELETE still fires after the grace period. The undo
   closure was deleted since nothing references it anymore. Single-id
   call site stays untouched (scheduleDelete still receives [id]).

3. Removed the "ستتاح لك ثوانٍ قليلة للتراجع" description from the
   cancel-order confirmation dialog. The cancelConfirmBody key was
   dropped from both locales. The AlertDialog now shows only the
   title + the two action buttons.

Out of scope (kept as-is):
- The undo ToastAction after CANCEL (user only asked about delete).
- Incoming-orders page.

Notes:
- ToastAction import is still used by the cancel-undo path.
- Trash2 import still used by the per-card delete button.
- Pre-existing TS error in use-push-subscription.ts is unrelated.
2026-05-17 14:07:23 +00:00
Riyadh 55d6e65456 fix(executive-meetings): quick-actions UX + reschedule/cancel polish (Task #563)
User-reported polish on the executive-meetings quick-actions flow on
iPad/mobile. Six fixes, all client-side:

1. Quick-actions surface now opens from ANY click on a meeting row
   (was: only the attendees cell). Root cause: handleRowClick guard
   skipped any `em-edit-*`, `em-merge-edit-*`, and `em-time-*`
   descendants. Those affordances only do anything in edit mode, but
   handleRowClick already early-returns when edit mode is on, so the
   testid guard was always over-aggressive. Dropped those guards plus
   the `[role='button']` rule from the interactive selector, kept the
   real `button`/`input`/`textarea`/`a`/`menuitem`/`checkbox`/etc.
   guards and the `em-row-actions-*` / `em-row-select-*` testid
   guards (those affordances are visible outside edit mode and must
   not trigger the surface).
   File: artifacts/tx-os/src/pages/executive-meetings.tsx (~L4350).

2. Originating row is now unmistakable on iPad: quickOpenShadow is
   `inset 0 0 0 4px highlight, 0 0 0 2px highlight` (was a single
   3px inset) and the row background tints to
   `rgba(highlight, 0.08)` while quick-actions is open.
   File: artifacts/tx-os/src/pages/executive-meetings.tsx
   (~L4287, L4344).

3. Postpone dialog header was overlapping the Radix close X in RTL.
   Added `pr-8` on the DialogHeader (same pattern already used for
   the row-quick mini-dialog).
   File: artifacts/tx-os/src/components/executive-meetings/
   upcoming-meeting-alert.tsx (~L1447).

4. Reschedule tab: date / start / end inputs collided on iPad
   portrait. Grid is now `grid-cols-1 md:grid-cols-3` (single column
   through iPad portrait, three across on landscape/desktop), with
   `min-w-0` on each cell and `w-full` on every Input so Safari's
   native date/time pickers stop overflowing.
   Same file (~L1761).

5. Cancel tab X overlap is resolved by the same DialogHeader fix in
   step 3 (one Dialog wraps all three tabs).

6. Removed the `cancelHint` paragraph + the `cancelHint` translation
   keys in ar.json (L1252) and en.json (L1164) — fully unused now.

NOT pushed to Gitea (Tailscale to desktop-11cj93j still flaky).
Pending pushes: #560 (VAPID), #561 (CEO roles), #563 (this).
2026-05-17 10:18:13 +00:00
Riyadh c540d0ab86 feat(push): add Web Push (VAPID) for lock-screen notifications on iPad PWA
Task #554. Adds a full Web Push stack on top of the existing Socket.IO
notification fan-out so the iPad PWA (and any installed browser) receives
system notifications when Tx OS is closed or backgrounded.

Backend
- New `push_subscriptions` table (userId + unique endpoint + p256dh/auth
  keys + ua + timestamps), exported from `@workspace/db`.
- `artifacts/api-server/src/lib/push.ts`:
  - VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT
    (Docker volume) with /tmp fallback for Replit dev, else ephemeral.
  - `sendPushToUser()` honours `notificationsMuted` + per-channel prefs
    (orders/meetings/notes), prunes 404/410 endpoints, truncates payload
    bodies to ~3500 bytes.
  - **De-dup gate:** skips push when the user has any active Socket.IO
    connection (uses `io.in(\`user:\${uid}\`).fetchSockets()`), so a
    connected user only gets the in-app chime, never a duplicate system
    notification.
  - `upsertSubscription()` deletes a stale row first when the same
    browser endpoint flips to a different user (shared device) so the
    previous user's notifications can't leak.
- Three new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`,
  `POST /api/push/unsubscribe` (auth-gated, Zod-validated).
- Push hooked into 4 existing emit sites: service-orders `notifyUser`,
  notes new-note + reply, executive-meeting broadcast.

Frontend
- `artifacts/tx-os/public/sw.js`: push + notificationclick only (no
  asset caching). All URLs (icon, badge, navigation target) resolved
  against `self.registration.scope`, so the SW works at root or under
  a subpath without code changes.
- SW registration in `main.tsx` scoped to `BASE_URL`.
- `use-push-subscription` hook (enable/disable/refresh + status).
- New `PushEnablePrompt` card mounted in `App` — appears on first
  launch when supported + permission still "default", one-tap enable,
  dismiss persists for 14 days. Silent on unsupported devices.
- `PushToggleRow` added inside Notification Settings.
- ar/en strings: `notifSettings.push.*` and `common.later`.

Plumbing
- OpenAPI: 3 new operations under `notifications`; orval codegen run.
- `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY /
  VAPID_SUBJECT through to the api service.
- `pnpm --filter @workspace/db run push` applied the schema.
- web-push + @types/web-push installed in api-server.

Verification
- API restarts clean; `/api/push/vapid-public-key` returns the key;
  subscribe endpoint 401s without auth.
- New `artifacts/api-server/tests/push.test.mjs` — 7/7 passing —
  covers VAPID endpoint, auth gating on subscribe/unsubscribe, row
  persistence + removal, idempotent re-subscribe with key rotation,
  account-switch endpoint reassignment, and malformed-input rejection.
- Two architect rounds: first flagged race + payload size (fixed),
  second flagged dedup gate + first-launch UX + SW base-path + tests
  (all fixed in this commit).
2026-05-16 15:29:28 +00:00
Riyadh cff2f454da feat(push): add Web Push (VAPID) for lock-screen notifications on iPad PWA
Task #554. Adds a full Web Push stack on top of the existing Socket.IO
notification fan-out so the iPad PWA (and any installed browser) receives
system notifications when Tx OS is closed or backgrounded.

Backend
- New `push_subscriptions` table (userId + unique endpoint + p256dh/auth
  keys + ua + timestamps), exported from `@workspace/db`.
- `artifacts/api-server/src/lib/push.ts`:
  - VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT (Docker
    volume) with /tmp fallback for Replit dev, else ephemeral in-memory.
  - `sendPushToUser()` honours `notificationsMuted` + per-channel prefs
    (orders/meetings/notes), prunes 404/410 endpoints, truncates payload
    bodies to ~3500 bytes so over-sized notes don't kill delivery.
  - `upsertSubscription()` deletes a stale row first when the same
    browser endpoint flips to a different user (account switch on shared
    device) so the previous user's notifications can't leak.
- Three new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`,
  `POST /api/push/unsubscribe` (auth-gated, Zod-validated).
- Push hooked into the 4 existing emit sites: service-orders `notifyUser`,
  notes new-note + reply, executive-meeting broadcast.

Frontend
- `artifacts/tx-os/public/sw.js`: push + notificationclick handlers only
  (no asset caching). Focuses an existing tab or opens a new one at the
  payload URL.
- SW registration in `main.tsx` scoped to `BASE_URL`.
- `use-push-subscription` hook (enable/disable/refresh + status:
  unsupported / denied / default / subscribed).
- New `PushToggleRow` in `NotificationSettingsContent` with ar/en strings
  (notifSettings.push.*). iPad copy explains that the user must add to
  Home Screen first for iOS to allow Web Push.

Plumbing
- OpenAPI: 3 new operations under `notifications` tag; orval codegen run.
- `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY /
  VAPID_SUBJECT through to the api service.
- `pnpm --filter @workspace/db run push` applied the schema.
- web-push + @types/web-push installed in api-server.

Verification
- API restarts clean; `/api/push/vapid-public-key` returns the generated
  key; subscribe endpoint 401s without auth as expected.
- Architect review surfaced two HIGH issues (account-switch leak,
  payload size); both fixed before completion.
2026-05-16 15:26:18 +00:00
Riyadh f0d77c625c Task #527: Unify topbar prefs into per-user Settings panel
- New `settings-panel.tsx` consolidates four duplicate topbar buttons
  (clock-style picker, quick-mute, notification-settings popover,
  language-toggle globe) behind a single gear icon that opens a
  right-side Sheet (full-width on mobile, capped on desktop).
- Panel groups: Language (ar/en), Notifications (per-slot
  enabled/sound/vibration), Sound (master mute), Clock (visibility,
  12/24h, style).
- Reuses existing hooks (useUpdateLanguage, useUpdate{Clock*},
  useUpdatePrefs, useHome/TopbarClockVisibility) so all persistence
  paths (DB columns + localStorage) stay identical — no migration.
- home.tsx topbar now renders only inbox link, single notification
  bell (kept as nav link), Settings gear, and logout.
- New i18n keys under `settingsPanel.*` in ar.json + en.json.
- Old ClockStylePicker / NotificationSettingsPicker / QuickMuteButton
  exports remain in their original files (unused) — kept to minimize
  diff scope; can be removed in a follow-up.
2026-05-13 14:46:50 +00:00
Riyadh f0b74ad7f0 Task #517: App image upload, external links, built-in route lock
Admin Add/Edit App now supports:
- Custom image upload (or fall back to Lucide icon) via the existing
  ServiceImageUploader; rendered on the home launcher when set.
- Open mode picker: internal (default), external_tab (window.open),
  external_iframe (renders inside /embedded/:id). External URL input
  shown conditionally and required by the form when an external mode
  is chosen.
- Route field is locked (readOnly + lock hint) when editing a built-in
  app, since those slugs are hardcoded in the SPA router.

Backend:
- apps schema gains image_url, external_url, open_mode (default
  'internal'); drizzle-kit push applied.
- New lib/db/src/built-in-apps.ts exports BUILTIN_APP_SLUGS +
  isBuiltinAppSlug, re-exported from lib/db.
- PATCH /apps/:id rejects route changes whose previous slug is
  built-in with 400 + code='builtin_route_locked'. Same-route no-op
  is allowed; non-route updates on built-ins still work.

Other:
- New SPA route /embedded/:id and embedded-app page (iframe host with
  back + open-in-new-tab + error/not-embeddable states).
- OpenAPI App / CreateAppBody / UpdateAppBody extended; codegen ran.
- en/ar locales: admin.appImage, appExternalUrl, appOpenMode.*,
  builtinPathLocked, embeddedFrame.*.
- New tests in apps-builtin-route-lock.test.mjs (4/4 pass) covering
  reject built-in route change, allow non-route built-in updates,
  allow non-builtin route changes, allow built-in same-route no-op.

Notes / drift:
- BUILTIN_APP_SLUGS is duplicated inline in admin.tsx
  (BUILTIN_APP_SLUGS_FE) because the browser bundle cannot import
  @workspace/db (pulls pg). Comment points at the canonical source;
  drift risk filed as a follow-up.
- Pre-existing failures unrelated to this task: 3 tests in
  executive-meetings-* and tsc errors in
  api-server/src/routes/executive-meetings.ts. Out of scope.
2026-05-12 12:23:38 +00:00
Riyadh ef53bc8649 Task #515: Reply to a note from the popup without leaving the page
The floating "new note" popup's Reply button used to navigate to
/notes?thread=X&reply=1, yanking the user out of whatever page they
were on (Tasks, Calendar, etc.). Now Reply opens an inline composer
inside the popup card itself.

Changes (artifacts/tx-os/src/components/notes/incoming-note-popup.tsx):
- New composerOpen / replyText / justSent state, reset whenever the
  popup head rotates so a half-typed reply never leaks across payloads.
- handleReply now opens the inline composer and focuses the textarea
  (rAF) instead of routing. Acknowledge (mark-as-read) is deferred to
  successful send so Cancel keeps the note unread.
- handleSubmitReply uses the existing useReplyToNote hook. Resolves
  recipientUserId from replyPayload.replier.id for the reply variant
  (owner replying back), undefined for the note variant (recipient
  replying — server infers the original sender).
- On success: brief on-card "Reply sent" confirmation (aria-live),
  then auto-dismiss after 700ms. On failure: destructive toast with
  the textarea preserved.
- Composer textarea: Cmd/Ctrl+Enter to send, plain Enter for newline,
  Esc to cancel (stops propagation so the global Esc listener doesn't
  dismiss the whole popup with an unsent reply). dir="auto" so the
  user's reply renders RTL/LTR per its own characters.
- pointerdown/click stopPropagation on the composer wrapper so typing
  doesn't accidentally drag the card.

Locale keys added to ar.json + en.json under notes.popup:
inlineReplyPlaceholder, sendReply, cancelReply, replySending,
replySent, replyFailed.

Pre-existing failing tests (groups-crud, executive-meetings-
notifications, executive-meetings-postpone-race) are unrelated to
this change.
2026-05-12 11:57:08 +00:00
Riyadh 8775e6a0b0 Task #512: Per-recipient Delete in Notes Inbox
Adds a recipient-scoped delete that's distinct from Archive: a recipient
can remove a note from their own inbox without touching the underlying
note or other recipients' rows.

Backend (artifacts/api-server/src/routes/notes.ts):
- DELETE /notes/received/:id — recipient-only; 404 if no row.
- POST /notes/received/bulk-delete — body {ids:number[]}, max 500,
  single SQL DELETE, returns {ok, notFound} for partial-success UI.
- Both registered before DELETE /notes/:id so Express matches /received
  first.

Frontend (artifacts/tx-os):
- New hooks useDeleteReceivedNote / useBulkDeleteReceivedNotes in
  src/lib/notes-api.ts; both invalidate notes + folders queries.
- Inbox bulk bar gets a Delete button (rose) next to Archive plus a
  confirm AlertDialog with all/none/partial toasts (src/pages/notes.tsx).
- ThreadDialog gets a per-row Delete next to Archive plus a single
  confirm dialog that closes the thread on success.
- AR + EN locale strings added for all new copy.

Tests:
- artifacts/api-server/tests/notes-inbox-delete.test.mjs — recipient
  delete, non-recipient/sender 404, bulk mix of valid+missing ids, and
  DB-level checks that the note + other recipients survive.
- artifacts/tx-os/tests/notes-inbox-bulk-delete.spec.mjs — Playwright
  flow seeds three received notes, bulk-deletes two from the inbox,
  then deletes the third via ThreadDialog per-row.
2026-05-12 11:14:33 +00:00
Riyadh 1e5a22d2f9 Task #511: Fully remove Chat feature
Destructive removal per user confirmation ("حذف نهائي ما يرجع").

Removed:
- API: routes/conversations.ts, schema/conversations.ts, all chat
  socket handlers in src/index.ts, /admin/users/:id/dependents/
  conversations+messages endpoints, conversation/message dependency
  counts in users/stats routes.
- Web: pages/chat.tsx, /chat route, dock chat filter, MessageSquare
  icon and messages StatCard on home, all chat-related UI in
  notifications + admin (dependency badges, delete-dialog rows,
  UserDependentConversations/Messages sections, count map keys).
- Locales: nav.chat, home.stats.messages, full chat.* block,
  admin.deleteUser conv/msgCount, admin.users.counts.conv/msg,
  admin.audit.unit.conversation_*/message_*, admin.dependents.user*.
- OpenAPI spec: tags, all /conversations/* paths, conv/msg dependent
  paths, related schemas (ConversationWithDetails, MessageWithSender,
  UserDependentConversation/MessageItem+Page, etc.), UserProfile and
  UserDeletionConflict conv/msg fields, HomeStats.unreadMessages.
  Regenerated client via orval.
- Database: dropped message_reads, messages,
  conversation_participants, conversations (CASCADE); deleted
  notifications with related_type='conversation' or type='chat';
  deleted apps row with slug='chat'; ran drizzle push-force.
- Seed: removed chat:access permission + user-role assignment +
  seeded chat app entry from scripts/src/seed.ts.
- Tests: deleted conversations-leave.test.mjs; cleaned chat refs from
  list-dependency-counts, delete-force-warnings, audit-log-coverage,
  and admin-inline-dependency-counts (e2e) — replaced chat dependents
  with note dependents where needed for force-delete coverage.

Notes preserved: notes.tsx noConversationsYet/conversationWith refer
to NOTE THREADS (not chat) and were intentionally NOT touched.

executive-meetings.ts not modified per replit.md restriction.
Pre-existing flaky test failures in executive-meetings/group/etc
suites remain unrelated to this task.
2026-05-12 10:51:31 +00:00
Riyadh a51b49a427 Add ability to archive multiple incoming notes at once
Introduces a bulk archive feature for the received notes tab, including API integration, UI selection state, confirmation dialogs, and internationalization support.
2026-05-12 10:25:41 +00:00
Riyadh 285a3d585f Task #508: Remove unused السعر (Price) field from admin Services UI
User requested removal of the price field from the admin Services edit
form ("بالنسبه الى الاسعار أحذفها لايوجد اسعار") because there are no
prices in the product.

Changes — frontend only, all in tx-os:
- artifacts/tx-os/src/pages/admin.tsx
  - Drop `price: string` from ServiceForm type.
  - Drop `price: "0.00"` from emptyServiceForm.
  - Remove the price entry from the edit dialog's field array
    (no more السعر input).
  - Remove the `<div>{service.price}</div>` from the services list.
  - Remove the `price` line from the edit-load mapper.
- artifacts/tx-os/src/locales/ar.json + en.json
  - Drop the now-unused `servicePrice` key.

Left intact:
- The generic `"price"` key in locales (line 104) — kept conservatively
  in case anything outside this scope expects it, though ripgrep shows
  no current usage.
- The `services.price` DB column and api-server code — unused in api
  src; only test fixtures insert a placeholder. Removing would require
  a migration and break tests for zero benefit.
- The user-facing services page — never displayed price.

Verification:
- ripgrep confirms zero `price` / `servicePrice` references remain in
  artifacts/tx-os/src/pages/admin.tsx and the locales (other than the
  generic shared key).
- Vite HMR applied cleanly with no TS errors.
- Pre-existing test failures (executive-meetings, service-orders) are
  unrelated and already tracked as separate project tasks.

Code review: skipped — change is a 5-line mechanical removal of dead
UI with no logic or data-flow implications.

Follow-ups: none proposed — this is a self-contained, complete fix.
2026-05-12 09:16:52 +00:00
Riyadh 26393ec155 Task #497: Allow row-drag rotation without time windows
Lifted the per-row missing-time drag block (originally #492). Meetings
without a (startTime, endTime) tuple are a normal state and now rotate
freely alongside timed rows.

Server (artifacts/api-server/src/routes/executive-meetings.ts):
- Dropped the `no_time_window` early-return guard in
  /api/executive-meetings/rotate-content.
- Sort + slot construction now tolerate null start times (NULLS LAST,
  tie-break by id) so a null tuple rotates as a real slot.
- All other safeguards (cancelled_in_rotate, optimistic lock,
  renumberDayByStartTime, audit logging) remain intact.

Client (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Removed missingTimeCount / dayRotatable memos, the dayRotatable
  prop wiring, dragBlockedByMissingTime + effectiveDragEnabled
  composition, the row's data-drag-blocked / native title /
  cursor-not-allowed-opacity-80 / aria-disabled overrides, the entire
  DragBlockedTooltipButton component + its render site, and the
  no_time_window errorToast branch in rotateContent. Pruned now-dead
  Tooltip + AlertTriangle imports.

i18n: removed the `executiveMeetings.rotate.needsTimeWindow` block
(tooltip + errorToast) from en.json and ar.json.

Tests: deleted `executive-meetings-rotate-needs-time-window.spec.mjs`
and added `executive-meetings-rotate-allows-missing-times.spec.mjs`,
which inserts 3 meetings (one with null times), verifies no
drag-blocked warning button / aria-disabled, drags the top row to the
bottom slot, asserts the rotate-content POST succeeds, and confirms
exactly one row still has a null tuple after rotation.

Verified: row-drag, row-quick-actions, and the new spec all pass
(8/8). Architect code review PASSED.
2026-05-11 15:18:06 +00:00
Riyadh 47949b2f52 Task #496: hide row Quick Actions in edit mode + drop missing-time banner
- ScheduleSection: gate `quickActionsCanMutate` on `canMutate && !editMode`
  so clicking a row in edit mode no longer opens the Postpone dialog.
  In view mode the quick-actions surface still works as before.
- MeetingRow: add a useEffect that force-closes local `quickOpen` when
  capability flips off (edit-mode toggle while dialog is open) and gate
  `quickOpenShadow` + `data-quick-open` on capability so no stale frame
  paints. Caught by code review.
- Remove the amber `em-rotate-blocked-hint` missing-time banner per
  product feedback. Kept `missingTimeCount` / `dayRotatable` so row
  drag still aborts client-side and the per-row tooltip + native
  title still explain why.
- Drop now-unused i18n keys `executiveMeetings.rotate.needsTimeWindow.hint*`
  from en.json and ar.json (zero/one/two/few/many/other variants).
  `tooltip` + `errorToast` strings stay (consumed by tooltip button
  + rotate-content catch handler).
- Update executive-meetings-rotate-needs-time-window.spec.mjs to drop
  the banner visibility assertion and the post-unblock `toHaveCount(0)`
  re-check. Tooltip / aria-disabled / no-rotate-POST / successful-
  rotate-after-unblock checks remain.

Verified: rotate-needs-time-window + row-quick-actions specs (7 tests)
all pass with --workers=1.
2026-05-11 14:57:13 +00:00
Riyadh 77493df365 Block EM row-drag when any meeting on the day lacks a time window (#492)
The /api/executive-meetings/rotate-content endpoint hard-rejects with
`code: "no_time_window"` whenever any visible non-cancelled meeting on
the date is missing (start_time, end_time). Before this change, dragging
a row on such a day produced an opaque English error toast ("Every
meeting must have a scheduled time window to rotate") that confused AR
users and didn't tell them what to fix.

Fix:
- Compute `missingTimeCount` / `dayRotatable` from `orderedMeetings`
  in executive-meetings.tsx and thread `dayRotatable` into MeetingRow.
- MeetingRow now gates useSortable + safeRowDragListeners on
  `effectiveDragEnabled = dragEnabled && dayRotatable`. When drag is
  blocked specifically by missing time, the <tr> shows
  `cursor-not-allowed`, dimmed opacity, `aria-disabled="true"`,
  `data-drag-blocked="no_time_window"`, and a bilingual `title`
  tooltip. The aria-disabled is spread AFTER dnd-kit's attributes so
  it wins the prop merge.
- Inline amber `em-rotate-blocked-hint` banner above the bulk toolbar
  surfaces the missing-time count so mutators see why drag is paused.
- rotateContent's catch handler now detects ApiError code
  "no_time_window" (local apiJson now attaches .code/.status to the
  thrown Error) and shows the bilingual
  `executiveMeetings.rotate.needsTimeWindow.errorToast` instead of
  the raw server message.
- Locale keys added under `executiveMeetings.rotate.needsTimeWindow`
  in en.json + ar.json (tooltip, hint_one/_other [+ AR plurals],
  errorToast).

Tests:
- New tests/executive-meetings-rotate-needs-time-window.spec.mjs
  inserts one untimed + two timed meetings and asserts the hint
  banner + every row's aria-disabled + data-drag-blocked, then
  attempts a drag and asserts NO rotate-content POST is sent and
  DB state is unchanged. After UPDATE-ing the missing time, asserts
  the hint and aria-disabled lift.
- Existing executive-meetings-row-drag and touch-reorder specs still
  pass — drag works exactly as before on fully-timed days.

Files: artifacts/tx-os/src/pages/executive-meetings.tsx,
       artifacts/tx-os/src/locales/{en,ar}.json,
       artifacts/tx-os/tests/executive-meetings-rotate-needs-time-window.spec.mjs
Server route untouched.
2026-05-11 14:21:02 +00:00
Riyadh 06a60a3eff #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
Riyadh 4dcad6f69a #481 Polish cascade-affected meetings table in postpone/reschedule prompt
- Drop the leading "#" index column; dailyNumber already serves as a
  stable per-row identifier.
- Replace the hard-coded amber background/border with the user's chosen
  alert accent via hexToRgba(accent, 0.12 / 0.45). Threaded `accent`
  through PostponeDialog → CascadePromptBlock so children don't read
  prefs directly. Both the loading and main prompt blocks track the
  accent; the rose blocked-by-midnight variant is intentionally kept.
- Render times as localized 12-hour with ص/م (AR) or AM/PM (EN) by
  reusing the shared formatTime helper instead of slicing "HH:mm".
- Drop now-unused i18n key cascadeColIndex from ar.json + en.json.
- Update the e2e test to match the 3-column schema (meeting#, title,
  times); kept the dailyNumber assertion in the first cell.

tsc clean. Cascade specs pass (Postpone by 10, Reschedule cascade,
Cascade prompt UI: Shift/Keep, no-followers fallthrough).
2026-05-10 16:07:20 +00:00
Riyadh eb50ea80cb Update translations for cascade meeting list notes
Add and update localization strings for the cascade meeting list, including a note about `cascadeListItem` being intentionally kept for backward compatibility.
2026-05-10 15:41:04 +00:00