Add a menu toggle button to show/hide the sidebar. Update locale files for `menuToggle` key. Refactor `protocol.tsx` to manage menu state, adjust layout for responsiveness, and apply square corners to the menu.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 470babcf-8feb-4bdb-8f18-66e74df799e6
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/ZgwFc55
Replit-Helium-Checkpoint-Created: true
- New shared component artifacts/tx-os/src/components/attendee-table-editor.tsx:
AttendeeTableEditor (# | name | position | side select | remove), plus
syncAttendeeRows/emptyAttendeeRow helpers and MAX_ATTENDEE_ROWS=50 cap.
- Internal booking dialog (protocol.tsx): replaced the two side-by-side
BookingAttendeeColumn lists with the single numbered table; each row now
carries its own side. Edit mode prefills existing keyAttendees then pads
to attendeeCount. useEffect syncs row count with the attendeeCount field:
growing adds empty rows, shrinking removes only empty trailing rows (never
filled ones), min 1 row.
- Public form (protocol-request.tsx): same conversion (AttendeeColumn removed,
Arabic labels passed directly since the page is forced-RTL Arabic).
- Submission payload unchanged: only rows with non-empty name sent, same
keyAttendees {name, position, side} shape — no API/schema changes.
- i18n: added protocol.bookings.form.side ("الجهة"/"Side") in ar.json/en.json.
- Removed dead code (BookingAttendeeColumn, AttendeeColumn, AttendeeRow type,
unused Plus/X imports in protocol-request).
- Verified: tsc --noEmit clean; page serves 200; architect review PASS.
- Follow-up proposed: #719 UI test for count sync/prefill/submit.
- SlotsAdminSection (artifacts/tx-os/src/pages/protocol.tsx): replaced the
2-column card grid of booking slots with a compact shadcn Table sorted by
startTime: # | time range | duration | days | status | actions.
Rows are dense (py-1.5, small text, h-7 icon buttons), RTL/LTR safe.
- New edit flow: pencil button opens a Dialog to change start time, duration
(5–480, step 5) and days-of-week (same day-chip UI as the generator).
Saves via existing PATCH /api/protocol/booking-slots/:id (no backend
changes needed — route already accepted startTime/durationMinutes/daysOfWeek).
409 duplicate_slot shows a dedicated destructive toast; other errors fall
back to the shared onError. Invalidates slots-admin + parent slots queries.
- i18n: new keys under protocol.slots in ar.json/en.json (colTime, colStatus,
colActions, edit, editTitle, editSaved, duplicateSlot, durationValue with
full Arabic plural categories).
- tsc --noEmit passes for tx-os. Architect review: Pass; added the suggested
<=480 client-side duration guard.
- Follow-up #717 proposed (UI test for edit + 409 toast).
- DB: protocol_booking_slots.days_of_week integer[] NOT NULL default {0..6} (0=Sun..6=Sat, Riyadh); pushed via drizzle-kit, legacy rows keep all-days behaviour.
- API (routes/protocol.ts): slot create/patch accept daysOfWeek (validated, deduped, sorted); new POST /protocol/booking-slots/generate (requireManageRooms) generates stepped slots in [from,to); duplicate rule is day-scoped: same start time on a different day merges the days into the existing slot (existing duration kept), fully covered times skipped; returns {created,merged,skipped,slots}; manual POST create also merges disjoint days instead of 409; matchesActiveSlot and public availability filter by Riyadh weekday.
- UI (protocol.tsx): interval-generation form (from/to/duration + day toggle pills + workdays/all-days presets), toast shows created/merged/skipped, slot list shows day names, booking dialog filters slots by chosen date weekday with stale-selection reset and empty-day hint. i18n keys added (ar/en).
- Tests: 13/13 pass incl. generation, day-scoped merge (same time different day is NOT a duplicate), duplicate skipping, validation, weekday filtering in availability + booking rejection, viewer 403 on generate.
- شريط "رابط الحجز العام (للمشاركة مع الضيوف)" في صفحة المراسم:
- أُزيل عرض نص الرابط الكامل (بقي العنوان والأيقونة فقط).
- أُضيف زر "فتح" (ExternalLink) بجانب زر "نسخ الرابط" يفتح
publicBookingUrl في تبويب جديد عبر window.open مع noopener,noreferrer.
- زر النسخ يعمل كما هو دون تغيير.
- مفاتيح ترجمة جديدة: protocol.bookings.openLink (ar: "فتح", en: "Open").
- لا تغييرات في الخادم. tsc نظيف. مراجعة architect: Pass.
- DB: protocol_booking_slots (وقت "HH:mm" بتوقيت الرياض، مدة، تفعيل، ترتيب)
+ protocol_settings (booking.minLeadMinutes، الافتراضي 60 دقيقة).
- API: CRUD /protocol/booking-slots (الكتابة تتطلب protocol.rooms.manage،
تكرار الوقت → 409 duplicate_slot)، GET/PATCH /protocol/settings (upsert + سجل
تدقيق)، ونقطة عامة /protocol/public/availability (محدّد معدل خاص، تعيد
booked/tooSoon/available لكل وقت).
- الفرض في الخادم: الطلب العام يجب أن يطابق وقتاً مفعّلاً + يحترم المهلة
(400 invalid_slot / lead_time برسائل عربية)؛ الموظفون ملزمون بالأوقات لكن
معفيون من المهلة. بدون أوقات مفعّلة يعمل الوضع الحر القديم.
- الواجهة: نموذج الطلب العام يعرض شبكة أوقات حسب التوفر (مع fallback قديم)؛
تبويب القاعات يعرض قسم إدارة الأوقات + المهلة لمن يملك canManageRooms؛
حوار الحجز الداخلي يتحول لتاريخ + قائمة أوقات عند وجود أوقات مفعّلة.
- i18n: مفاتيح protocol.slots.* بالعربية والإنجليزية.
- اختبارات: tests/protocol-booking-slots.test.mjs — 10 اختبارات ناجحة
(CRUD/تكرار، الإعدادات، التوفر، invalid_slot، lead_time، تعارض، وقت معطّل،
إعفاء الموظفين، صلاحيات القراءة/الكتابة، الحذف).
- مراجعة المعماري: PASS؛ عولجت الملاحظات (توثيق قرار الصلاحيات + اختبار
regression له، ومنع الإرسال في الواجهة قبل اختيار التاريخ والوقت).
- تعديل بعد الرفض الأول: قراءة قائمة الأوقات الكاملة والإعدادات تتطلب
protocol.rooms.manage (حسب المواصفة)؛ أُضيفت نقطة قراءة مصغّرة
/protocol/booking-slots/active (protocol.access) لحوار الحجز فقط.
- تعديل بعد الرفض الثاني: مهلة الحجز المسبقة (minLeadMinutes) تُطبَّق الآن
أيضًا على الحجوزات الداخلية (إنشاء وتعديل) — لا استثناء للموظفين.
- DB: protocol_booking_slots (وقت "HH:mm" بتوقيت الرياض، مدة، تفعيل، ترتيب)
+ protocol_settings (booking.minLeadMinutes، الافتراضي 60 دقيقة).
- API: CRUD /protocol/booking-slots (الكتابة تتطلب protocol.rooms.manage،
تكرار الوقت → 409 duplicate_slot)، GET/PATCH /protocol/settings (upsert + سجل
تدقيق)، ونقطة عامة /protocol/public/availability (محدّد معدل خاص، تعيد
booked/tooSoon/available لكل وقت).
- الفرض في الخادم: الطلب العام يجب أن يطابق وقتاً مفعّلاً + يحترم المهلة
(400 invalid_slot / lead_time برسائل عربية)؛ الموظفون ملزمون بالأوقات لكن
معفيون من المهلة. بدون أوقات مفعّلة يعمل الوضع الحر القديم.
- الواجهة: نموذج الطلب العام يعرض شبكة أوقات حسب التوفر (مع fallback قديم)؛
تبويب القاعات يعرض قسم إدارة الأوقات + المهلة لمن يملك canManageRooms؛
حوار الحجز الداخلي يتحول لتاريخ + قائمة أوقات عند وجود أوقات مفعّلة.
- i18n: مفاتيح protocol.slots.* بالعربية والإنجليزية.
- اختبارات: tests/protocol-booking-slots.test.mjs — 10 اختبارات ناجحة
(CRUD/تكرار، الإعدادات، التوفر، invalid_slot، lead_time، تعارض، وقت معطّل،
إعفاء الموظفين، صلاحيات القراءة/الكتابة، الحذف).
- مراجعة المعماري: PASS؛ عولجت الملاحظات (توثيق قرار الصلاحيات + اختبار
regression له، ومنع الإرسال في الواجهة قبل اختيار التاريخ والوقت).
- Extended internal BookingDialog (artifacts/tx-os/src/pages/protocol.tsx) to match
the public booking form: requester name, phone, room, department, meeting title,
meeting type toggle (داخلي / مع جهة خارجية), conditional entity name (external only),
expected attendee count, purpose, from/to, key attendees in two columns
(من الهيئة / خارج الهيئة) with add/remove name+position rows, and notes.
- Edit mode prefills all new fields from the booking; click-to-book initialStart
behavior unchanged.
- api-server (artifacts/api-server/src/routes/protocol.ts): bookingCreateSchema and
bookingPatchSchema now accept requesterPhone, requesterOrg, department, meetingType,
purpose, keyAttendees; POST/PATCH /protocol/bookings persist them. requesterOrg is
nulled whenever effective meetingType is internal (create and patch).
- Added i18n keys protocol.bookings.form.{name,position,addRow,removeRow} (ar/en);
all other labels reuse existing protocol.bookings keys.
- Verified: tsc clean for changed files (push.ts / executive-meetings.ts errors are
pre-existing and unrelated); API workflow restarted and serving; architect review PASS.
- Note: entity name (اسم الجهة) is not enforced as required for external meetings on
the internal endpoint (unlike the strict public endpoint) — internal staff keep
flexibility; can be tightened later if requested.
- artifacts/tx-os/src/pages/protocol.tsx: replaced the dash-separated
<ul> list of keyAttendees in the booking details dialog with a shadcn
Table (already imported) with 3 columns: Name, Position/Title, Side.
Headers use text-start for correct RTL alignment; empty name/position
cells render an em dash; side rendered as the existing pill using
protocol.bookings.attendeeSides.*; table wrapped in rounded border +
overflow-x-auto so it stays responsive inside the dialog.
- i18n: added protocol.bookings.details.attendeeCols.{name,position,side}
to ar.json (الاسم/الصفة/الجهة) and en.json (Name/Title/Side).
- Scope limited to the details dialog; the expanded-row attendee display
elsewhere is unchanged (per task out-of-scope).
- Verified: both locale JSONs valid, tsc --noEmit clean, architect review
PASS.
- No follow-ups proposed: pending Task #695 already covers e2e testing of
the details dialog contents.
Replaces the card-based list view with a table component in `protocol.tsx`. Adds new translation keys for table headers and meeting types in `ar.json` and `en.json`. Formats booking references to display only numeric characters and dashes.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: e1793d98-fbd5-4c99-99a0-e01d493482cc
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/ychh3tH
Replit-Helium-Checkpoint-Created: true
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.
Renamed two Arabic sidebar tab labels in the protocol (room-booking)
module in artifacts/tx-os/src/locales/ar.json:
- protocol.tabs.bookings: "حجوزات القاعات" → "حجز القاعات"
- protocol.tabs.external: "اللقاءات الخارجية" → "اجتماعات خارجية"
English translations left unchanged. The chart label
"اللقاءات الخارجية حسب الحالة" was left as-is per the task's out-of-scope
note. Text-only change, no logic touched.
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.
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.
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.
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.
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.
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.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: ad3facca-9626-4e19-95c0-125e6d3bc7e2
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/1OXOhLE
Replit-Helium-Checkpoint-Created: true
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.
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.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 9c655ed7-866c-454d-b646-e458df854c65
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/OsTuUKE
Replit-Helium-Checkpoint-Created: true
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.
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.
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.
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.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 50672f01-2472-4869-a234-ecc811a41e0b
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/eDmI6vt
Replit-Helium-Checkpoint-Created: true
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.
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).
- 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).
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.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 33aee19f-8f0f-4399-98e0-39fe09a87e1b
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/m92e9kU
Replit-Helium-Checkpoint-Created: true
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).
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.
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.b5efd9eb) 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.a34eb5f5). 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.
- 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.
- 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.
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.
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.
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.
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).
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.