Platform admins can generate a permanent recovery key per café (admin
panel → Cafés). The café Owner uses it to sign in when OTP access is lost;
once authenticated, all server-side data syncs as normal (data is per-café
on the server, the device only caches it).
Backend:
- Cafe.RecoveryKeyHash (SHA-256, unique index) + RecoveryKeyCreatedAt; migration
- RecoveryKeyGenerator util: MZ-XXXXX-XXXXX-XXXXX-XXXXX, ~190-bit entropy,
stored as SHA-256 (API-token pattern — raw key shown once, never retrievable)
- Admin: POST/DELETE /api/admin/cafes/{id}/recovery-key (key returned once);
café list now reports HasRecoveryKey + RecoveryKeyCreatedAt
- Login: POST /api/auth/login-key → exact-hash lookup → resolves café Owner →
issues normal JWT; rate-limited (auth-otp), suspended/no-owner guarded, logged
Admin UI: per-café generate / regenerate / revoke with one-time reveal + copy.
Dashboard login: discreet "ورود با کلید بازیابی" link → key field. fa/en/ar.
86 tests pass; all tsc clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1. The /q/{code} guest menu returned HTTP 500 on every load. Root cause:
menu-item-model-viewer.tsx did a top-level `import "@google/model-viewer"`,
a browser-only lib that touches `self` at module evaluation. Next pulled
it into the server module graph (page → qr-guest-menu → qr-menu-3d-sheet →
model-viewer) and SSR crashed with "self is not defined". Now the library
is imported lazily inside useEffect (client-only); a poster placeholder
shows until the custom element registers. Verified /q/* now returns 200.
2. Removed the "discover" (browse other cafés) item from the café owner
sidebar — café discovery belongs in Koja, not the owner panel. The owner
still manages their OWN Koja listing from Settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The platform no longer sells SMS. Each café saves its OWN Kavenegar API
key + sender line (new Cafes columns + migration) and campaigns are sent
and billed through that account.
Backend:
- GET/PUT /sms/settings (Manager/Owner; key echoed masked, verified
against the provider before saving)
- campaign + balance use the café's credentials; SMS_NOT_CONFIGURED
error when missing; plan-tier SMS gating removed everywhere
(PlanLimitChecker, SmsMarketingService, billing status)
- platform Kavenegar config stays ONLY for login OTPs (env/DB)
- design-time DbContext factory so `dotnet ef migrations add` works
without booting the host
Dashboard:
- SMS screen: provider-settings card, not-configured callout, campaign
form disabled until configured; quota bar removed (usage stays as info)
- subscription screen + plan comparison no longer show SMS limits
Admin panel:
- Kavenegar/SMS section removed from integrations (request field now
optional; stored OTP config untouched)
- SMS limit field removed from the plan editor
- nav label "درگاه و پیامک" → "درگاه پرداخت و AI"
fa/en/ar translations. 86 tests pass; all tsc clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The merchant plan page hard-coded 4 tiers, prices and a feature matrix
that drifted from the admin-editable platform catalog (Starter tier
missing, stale prices/features). PlanComparison and CheckoutScreen now
consume /platform/plans + new /platform/features-catalog:
- columns = active plans by SortOrder (incl. Starter), names from
DisplayNameFa/En, prices from MonthlyPriceToman
- limit rows from PlanLimitsData (int.MaxValue → "نامحدود")
- feature rows from the feature catalog, ticked via FeatureKeys
- checkout validates the ?plan= param against isBillableOnline and
prices from the catalog — no more client-side price constants
fa/en/ar limit-row labels added.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Local tsconfig.json has uncommitted target changes, so `[...voidIds]`
passed locally but failed CI's tsc (TS2802, target < es2015).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Backend:
- POST /orders/{id}/payments/corrections (Manager/Owner): void wrong
payments (marked Refunded, never deleted) and/or record replacements
atomically; mandatory reason; requires an open register shift; full
before/after written to the immutable audit trail.
- GET /orders/closed?date= — closed orders of one Iran-calendar day,
paged, the browsing surface for corrections.
- CalculateExpectedCash now subtracts cash refunds so corrections keep
the drawer expectation honest.
Dashboard (reports screen now has three tabs):
- عملکرد و سود: existing KPIs/charts + new day-by-day breakdown table
(orders, revenue, expenses, net profit per Jalali day).
- اصلاح سند: closed-orders browser with payment chips + correction
dialog (void checkboxes, replacement rows, live balance, reason).
- گزارش عملیات: filterable audit-log viewer (category, Jalali range,
branch) with expandable structured details.
fa/en/ar translations included. 86 backend tests pass; dashboard tsc clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full Persian calendar:
- New JalaliDateField — Shamsi popover picker (Saturday-first weeks,
Persian digits, امروز shortcut); wire format stays ISO Gregorian
YYYY-MM-DD. Falls back to the native input for the en locale.
- Replaces all 5 native type="date" inputs (Gregorian-only pickers):
reservations, expenses from/to, reports from/to.
- Reservations list date now renders Jalali instead of the raw ISO
string; branches purge timestamp now formats with fa-IR.
Responsive shell (mobile + tablet):
- New MobileNav: hamburger in the topbar (< md) opening an RTL-aware
slide-over drawer with all nav destinations, permission-filtered,
Escape/backdrop close and body scroll lock.
- Desktop sidebar hidden below md; header center cluster (clock/plan)
hidden below md; language switcher hidden below sm.
- Main content padding scales p-3 → p-4 → p-6.
- Verified at 375px and 768px: no horizontal overflow, drawer and
Jalali picker fully functional.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The sidebar had 22 items in 5 accordion groups, all defaulting closed:
first visit showed five vague headers and zero destinations, there was
no Dashboard/Home link at all, and rare pages (taxes, subscription) had
equal weight with POS. Restructured around usage frequency:
- Flat primary (always visible, no header): Dashboard, POS, Tables,
Kitchen, Queue, Reservations, Menu, Reports
- Two collapsible groups: Customers & marketing (crm, coupons, sms,
reviews, discover) and Café management (inventory, expenses, shifts,
taxes, hr, branches)
- Footer utility icons: settings, subscription, support
- Removed "notifications" from the nav (duplicate of the topbar bell)
Other fixes folded in:
- Deleted [locale]/page.tsx which redirected "/" to /pos — it made the
POS exit button a no-op loop and left OverviewScreen unreachable.
"/" now renders the overview home; login still lands on /pos.
- Branch gating moved from group-level to an item whitelist
(BRANCH_ALLOWED_NAV_KEYS) — also closes the hole where branch
accounts could deep-link to /reports etc. past the RouteGuard.
- RouteGuard now checks footer items too (subscription stays gated).
- revalidate=300 on the locale layout: Next emitted s-maxage=31536000
and the WCDN edge kept serving year-old HTML shells after deploys.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
POS runs in the (fullscreen) layout which strips the sidebar.
Adds a Home → داشبورد button at the top-left of the table board so
users can navigate back to the dashboard without being stuck.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Race fix: orderBranchId now returns `undefined` (not null) while the /branches
query is in flight. usePos2Menu treats undefined as "not yet determined" and
skips the fetch, preventing getBranchMenu(cafeId, null) → empty array.
Once branchesFetched=true, orderBranchId resolves to the correct branchId
(or null for café-wide fallback).
Layout: desktop order screen now shows a left vertical category sidebar
(116 px, md+) instead of horizontal chips, giving the classic POS sidebar
feel. Horizontal chips kept for mobile (<md). Menu grid columns adjusted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The menu/tables are branch-scoped. v2 used the raw stored branchId, which is
null or stale for users who never opened the classic POS (it has no branch
picker), so getBranchMenu returned an empty menu. Now v2 fetches /branches,
auto-selects the first valid branch (self-healing the stored id), and loads the
branch menu + tables + order submission against that resolved branch — matching
the classic POS exactly. Also adds a visible "menu failed to load / retry"
state instead of a silent empty grid.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The (fullscreen) layout redirected to /login whenever user.accessToken was
falsy — but on a page refresh that fires before Zustand finishes rehydrating
the persisted auth from localStorage, so an authenticated user was bounced to
login on every refresh. Gate the redirect on _hasHydrated (and show a loader
while rehydrating), matching RouteGuard. Tokens themselves are already long
(30d access / 365d refresh), so sessions now survive refreshes as expected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Card is now the pre-selected payment method (and split rows default to Card),
matching Iran's card-dominant payments. Card already sits first in the selector.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reorder the payment method tabs to کارت / نقدی / تقسیم (Card first, most common
in Iran) while keeping Cash as the pre-selected default method.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the four POS v2 roadmap items:
1. Real split payments — split tab records N separate payment rows (equal split,
last row takes the remainder), each row toggles Cash/Card; posts payments[].
2. Card-terminal push — confirmPay sums Card amounts and calls requestPosPayment
(POS device) before recording; surfaces POS_DEVICE_* errors.
3. Customer + coupons + loyalty — reuses PosCustomerPicker (attach/search/create)
and validates coupons via /coupons/validate (discount in totals). Pay sheet
offers loyalty redemption (1 point = 100 toman) when a customer is attached.
4. Promote to default — /pos now renders POS v2 (full-screen, café-themed); the
classic terminal moves to /pos-classic with its sidebar+topbar chrome. The
"نسخه کلاسیک" link points there.
Order submission already carried customerId/guestName/guestPhone/couponId via the
shared cart store, so customer + coupon flow straight through send + pay.
tsc --noEmit clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
POS v2 is now a real, working point of sale at /[locale]/pos2 (was a static
mock). It reuses the existing data layer so it shares the React Query cache and
offline pipeline with the classic POS:
- Table board ← fetchCafeTableBoard (Free/Busy/Reserved/Cleaning, live totals,
guest-QR badge); polls every 15s. Open a free table to start an order; open a
busy table to hydrate its existing order (GET order → cart hydrateFromOrder).
- Order screen ← real branch/café menu + categories, bound to useCartStore
(add/qty/remove). Send via submitOrderToApi (online + offline outbox) then
re-hydrate; "ارسال (n)" shows the pending (unsynced) line count.
- Pay sheet ← POST /orders/{id}/payments. Cash (numpad + change), Card, and a
Split helper (records the full amount; split is cashier guidance for now).
- Online/offline badge, loading/empty states, toasts, busy overlay, and a
"نسخه کلاسیک" link back to /pos.
The static design mock stays at /[locale]/pos2-preview (dev-only, 404 in prod).
tsc --noEmit clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Responsive RTL big-touch reimagining of the POS order screen for judging the
redesign on real devices before decomposing the 1568-line pos-screen.tsx + wiring
real logic. Self-contained: mock menu + local cart, no API/store/SignalR.
- 3 zones: category chips + item grid · order ticket · sticky action bar.
- lg+ side-panel ticket; smaller screens get a "view order" bar + slide-over
(covers landscape tablet, portrait tablet, phone).
- Big-touch (56px primary / 44px qty), brand green #0F6E56, Toman totals.
Send/Pay are mock toasts. tsc clean.
Guest orders from the QR/digital menu already notified via SignalR, but only
screens that were open (KDS/POS/tables) reacted — and silently (a data refresh,
no alert). So staff on any other screen never knew a menu order arrived.
- Add a global useOrderAlerts() mounted in the dashboard shell: connects to
/hubs/kds, joins the café group, and on a new GUEST order plays a chime + shows
a toast (localized fa/en/ar) + nudges order/KDS/POS lists to refresh — on every
screen.
- Filter to guest QR-menu orders only (not staff POS orders): LiveOrderDto now
carries Source, set in MapLiveOrder (+ the delivery/snappfood mappers).
86 API tests pass; dashboard tsc + build clean.
Guest QR menu shows a "ساختهشده با میزی" watermark under the menu unless the café's
plan has the `watermark_removed` feature (Starter+).
- PublicMenuDto gains ShowWatermark; PublicService computes it from
IsFeatureEnabledForCafeAsync("watermark_removed") for both slug and branch menus.
- Guest menu renders the watermark footer when showWatermark.
- NoOpPlatformCatalogService test double (all features on) for the PublicService
ctor; QrMenuTests updated.
86 tests pass; dashboard tsc clean.
Previously the only Employee records were the Owner (created at café signup) and
one Manager per branch — there was no way to add a waiter/cashier/chef. Adds it.
Backend:
- POST /api/cafes/{cafeId}/employees (HrController). Owner/Manager only; creating a
Manager requires Owner; Owner cannot be created here. Validates name/phone/role,
enforces one-employee-per-phone, validates branch belongs to the café, and can
optionally set username/password login in the same step (same hashing + uniqueness
as the credentials endpoint). Returns EmployeeSummaryDto.
Dashboard:
- New "Team" tab on the HR screen (now the default): employee roster (name, role,
phone, base salary) + an "Add employee" button (owner/manager) opening an inline
form — name, phone, role, optional branch, optional base salary, optional login.
- Role labels + all form strings in fa/en/ar.
86 API tests pass; dashboard tsc + build clean.
Builds on the outbox engine to take the whole dashboard offline in one place
instead of wiring 114 mutation sites individually.
Frontend (single chokepoint = the API client):
- offline-write: any write auto-queues to the outbox on offline/network failure
and returns an optimistic value; the online path is unchanged apart from an
Idempotency-Key header (so even online retries de-dup). entityType is derived
from the URL; POSTs get a remappable local id.
- client.doWrite unifies POST/PUT/PATCH/DELETE through this path. WriteOptions
gains `offline: "queue" | "reject" | "manual"`.
- Guardrails: auth / billing / payments / SMS / exports are online-only and throw
OFFLINE_UNAVAILABLE offline rather than queueing (no queued double-charges or
surprise SMS blasts). use-api-error resolves the friendly localized message
(fa/en/ar).
- submit-order opts out ("manual") to keep its richer local-Order mock; shared
helpers de-duplicated into offline-write.
- Request persistent storage on mount so unsynced writes survive eviction.
Backend:
- IdempotencyCleanupJob: daily purge of idempotency records older than 7 days
(the table now gets a row per keyed write). Registered in Hangfire. No migration.
86 API tests pass; dashboard tsc + build clean.
Completes offline Phase 1 (frontend). Generalises the POS-orders-only queue into
a reusable write engine and fixes the two correctness bugs in the old path.
- offline-db: generic `outbox` store (DB v3, order_queue/kv preserved) with
enqueue/list/update/remove + a persisted client→server id map.
- outbox.ts: drains in causal order — remaps local_* ids to server ids (blocking
an op until its creator syncs), sends each op with its idempotency key, and
classifies failures (offline → stop; 5xx / in-progress → retry; 4xx → poison
after 5 attempts). remap/blocked logic validated against representative cases.
- client: apiPost/Put/Patch/Delete take an optional idempotencyKey →
`Idempotency-Key` header; ApiClientError now carries HTTP status.
- submit-order: generates ONE idempotency key per submit, used for both the
online attempt and the queued replay → server de-dups (no more double-create);
offline create carries createsClientId so a later add-items remaps onto the
real order instead of spawning a second order.
- use-offline-sync: drains the outbox, one-time migrates legacy order_queue
items, invalidates queries after a successful sync.
tsc + production build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First slice of offline-first (Phase 1). Makes every dashboard area *viewable*
offline with last-synced data, instead of empty lists on an offline reload
(previously only next-pwa's 10-min API cache survived).
- offline-db: add a generic `kv` IndexedDB store (DB v2, preserves order_queue)
with kvGet/kvSet/kvDelete; all degrade silently on quota/unavailable.
- query-persister: debounced snapshot of the React Query cache via
dehydrate/hydrate (no new dependency). Restore is guarded by a schema buster,
24h max-age, and a café scope so one tenant never hydrates another's data.
- providers: gcTime 24h so hydrated data isn't GC'd; restore on mount + persist
on cache changes, re-scoped when the signed-in café changes.
No write-path changes; the existing POS order queue is untouched. Next in
Phase 1: generalize that queue into an idempotent outbox with client→server
ID remapping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Before, buying a plan immediately switched the tier and stacked the duration.
Now a purchase made while the café still has paid coverage is QUEUED to start
when the current coverage ends, and the owner can cancel a queued one.
Model:
- SubscriptionPayment gains EffectiveFrom/EffectiveTo; status gains Scheduled
(paid, queued) and Cancelled. EF migration AddSubscriptionScheduling (nullable).
BillingService:
- On payment completion, compute coverage end (latest of active expiry + furthest
queued period). If it is in the future → Scheduled (queued, café tier/expiry
untouched); else activate immediately as before. Periods chain correctly.
- GetStatusAsync lazily promotes any due queued period to active, and returns the
queue (QueuedPlans).
- CancelQueuedAsync cancels a Scheduled period (owner-only) and re-packs the queue
so later periods slide earlier. Active prepaid plan is never cut short; no
automatic refund (manual, per product decision).
- Confirmation SMS distinguishes "activated until X" vs "queued, starts X".
API: BillingStatusDto.QueuedPlans + DELETE /api/billing/queued/{paymentId}.
Dashboard:
- Subscription screen shows a "Queued subscriptions" card (tier, window, cancel
with confirm).
- Checkout shows "you already have an active subscription — this will start on
{date}" when the café is still covered.
- i18n fa/en/ar.
81 API tests pass; dashboard typechecks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Delete (every manageable entity that only had "add" now has delete):
- Ingredients (warehouse): new DELETE /inventory/ingredients/{id} (soft-delete via
the global DeletedAt filter — no FK trouble with recipes/movements) + NoOp stub +
trash button in the materials cards.
- Reservations: new DELETE /reservations/{id} (soft-delete) + per-card delete button.
- Coupons & Customers: backend DELETE already existed; wired delete buttons in the UI.
- Shared ConfirmDialog component used by all delete flows (RTL-aware).
- Audit result: tables/branches/taxes/kitchen-stations/expenses/menu/terminals already
had delete; HR has no "add" so no delete needed; shifts intentionally excluded
(financial open/close records, not add-style entities).
Koja visibility:
- New Cafe.ShowOnKoja flag, default TRUE (DB default true so existing cafés stay
listed). Discover query now filters IsVerified && !Deleted && ShowOnKoja.
- public-profile GET/PUT expose showOnKoja; dashboard public-profile panel has an
on-by-default toggle that persists immediately. Platform IsVerified gate unchanged.
- EF migration AddCafeShowOnKoja (defaultValue: true).
Also: added the missing errors.generic i18n key (fa/en/ar) so useApiError's fallback
resolves instead of rendering the literal "errors.generic". 81 API tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The branch-menu-overrides availability switch (dashboard) and the BlogToggle
(admin website editor) still moved their knob with translate-x while inheriting
RTL, so the knob escaped the track on the right. Pin both to dir="ltr" like the
other switches. All four role="switch" toggles in the codebase now share the fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add DELETE /api/cafes/{cafeId}/menu/items/{id} (DeleteItemAsync soft-delete,
mirroring the existing category delete) — item delete had no backend route.
- Dashboard menu admin: destructive "delete" action in the item and category
edit modals, behind a shared confirm dialog (AlertDialog). Deleting the
selected category falls back to "all items".
- Fix the availability ToggleSwitch in RTL: force dir="ltr" so the knob's
translate-x stays inside the track instead of escaping on the right
(same fix as the admin-panel toggles).
- i18n: deleteItem/deleteCategory confirm + success strings (fa/en/ar).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lets the POS agent and the QR/app customer attach a free-text note to each
order line (e.g. "no tomato", "extra hot") that reaches the kitchen/bar.
- Backend already supported it (OrderItem.Notes persists; CreateOrderItemRequest
and OrderItemDto carry Notes; LiveOrderDto items include it) — this wires the UI.
- cart.store: add setNotes(menuItemId, notes); notes already travel in
getPendingLines and round-trip via hydrateFromOrder.
- POS pos-screen: a note input under each cart line.
- QR guest menu: a note input under each cart line (QrCartLine.note).
- KDS: render the note prominently under each item so kitchen/bar sees it.
- i18n: pos.itemNotePlaceholder + qrMenu.itemNote (fa/ar/en).
Note: notes are captured on items being added; editing a note on an
already-submitted line is out of scope (no pending delta to re-send).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Location card gets a 'موقعیت فعلی من' button that fills lat/lng from the browser's geolocation. Support ticket list now shows the resolved (localized) error instead of a generic message, so a failure is diagnosable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Error toasts surfaced the raw English backend message. Added an errors namespace (fa/ar/en) keyed by error code + a useApiError() resolver that maps ApiClientError.code to the localized message (fallback to a localized generic). Wired into menu, tables, demo banner, and subscription checkout; hardened getErrorMessage so it never returns the raw backend message.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The dashboard demo-data banner is shown to Owner and Manager, but the /demo/seed endpoint required strictly Owner, so a Manager clicking it got a silent 403 (the banner had no error handler) — appearing as 'nothing happens, no tables or items'. The endpoint now allows Owner or Manager, and the banner shows the error on failure.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Marketing-site login/register/dashboard links were locale-less (app.meezi.ir/login), so the dashboard auto-detected locale from the browser Accept-Language (en-US) and redirected Persian users to /en. Links now carry the current locale, and the dashboard sets localeDetection:false so any locale-less entry defaults to fa (Iran-first) instead of guessing from the browser.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dashboard & API bug fixes for owner-reported breakage:
- MenuController validators (PosValidators): NameEn was required but the
dashboard sends null when blank, so every manual menu-item create failed
and category create failed 100% (the form never sends nameEn). Now optional.
- DemoDataBanner: only showed when a cafe was exactly empty, so
showcase-seeded cafes (2-3 cats / 3-5 items) could never trigger the
one-click seed. Widened gate to sparse menus (<5 cats && <10 items) and
added a clear "nothing to add" message when already populated.
- client.ts: added one-time JWT refresh-and-retry on 401 (shared in-flight
promise) before bouncing to /login. Expired access tokens silently broke
ticket list, add-table, and other reads.
- Surface API errors as toasts on menu + table mutations (were swallowed
silently, so failures looked like "nothing happens").
- Admin blog editor: saving an edit dropped IsPublished (defaulted false,
silently unpublishing the post on every save); now persisted with a
toggle. Also hoisted the inner Field component to module scope - it was
remounting every input on each keystroke and dropping focus.
- Admin integrations: replaced raw radio gateway selector with a styled
RadioDot matching the iOS toggles.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds POST /api/cafes/{cafeId}/demo/seed (owner-only) that seeds:
- 9% default VAT tax
- 7 menu categories + 59+ items via DemoMenuSeeder
- 15 inventory ingredients (coffee shop staples)
- 10 tables across 3 floors on the first active branch
Frontend DemoDataBanner appears on menu, tables, and inventory
pages when the café is completely empty, so owners can populate
demo data in one click instead of entering everything manually.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- dashboard layout: wait for Zustand _hasHydrated before redirecting to /login
(was redirecting on first render before localStorage was read)
- admin shell: same fix using new _hasHydrated on admin auth store
- admin-auth.store: add _hasHydrated + onRehydrateStorage to mirror merchant store
- AdminPlansScreen: replace direct cache mutation with per-plan PlanCard component
that owns its own useState — fixes other plans disappearing after save
- AdminSettingsScreen: detect boolean values and render iOS-style Toggle switches
- AdminIntegrationsScreen: replace all <input type=checkbox> with Toggle switches;
replace OpenAI model text input with <select> dropdown (gpt-4o-mini/4o/4-turbo/4/3.5)
- blog editor: fix form never syncing existing post data into state (editing was broken);
all fields now use local form state, save uses form directly
- blog links: fix broken relative hrefs (website/blog/new → /admin/website/blog/new)
and back button using proper Link components
- ci-cd: remove image prune step entirely — never removes containers or images
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the subscribe mutation had no onError handler, so any
payment initiation failure (wrong merchant ID, ZarinPal API error,
disabled payment method) would silently re-enable the button with
no user feedback. Now errors are shown below the Pay button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sidebar:
- All groups start collapsed on first load (v4 storage key resets old state)
- Opening one group closes all others (accordion)
- Navigating to a section opens only that section's group
Koja slug:
- SlugHelper: Persian->Latin transliteration, slug validation
- Registration accepts optional custom slug; auto-derives from cafe name
- Slug can be updated from dashboard Settings -> Profile
- Settings PATCH validates uniqueness (SLUG_TAKEN) and format (INVALID_SLUG)
- koja.meezi.ir/{slug} now redirects to /fa/cafe/{slug} (short URL support)
Bug fix:
- SupportTicketService: cafeId/status filters applied before Select() projection
to fix EF "could not be translated" crash on the support tickets page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
npm ci failed in Docker because package-lock.json was stale (missing three
and the workbox/PWA deps) and @google/model-viewer@4.2.0 requires three@^0.182.0
while package.json pinned ^0.163.0. Bumped three and regenerated the lockfile.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Plan comparison and website pricing advertised branch counts that did not
match PlanLimitsData.ForTier: Pro now shows 3 (was 1) and Business shows
unlimited (was 5), matching what the backend actually enforces.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Insert a factor/invoice page between plan selection and payment showing
billing-period choice, line items, and totals before redirecting to the
gateway, moving payment-method selection to where the charge happens.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wrap the POS terminal in the sidebar + topbar layout via a nested
fullscreen layout, and make the sidebar collapse to an icon-only rail
with a persisted toggle so operators keep navigation on the POS screen.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Next 16 defaults `next build` to Turbopack, which requires native SWC
bindings unavailable for Alpine musl from our npm mirror (only the WASM
fallback loads). Pass --webpack so the build uses the WASM SWC fallback
and succeeds inside the Docker images.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduce an OTP input box on login/register, surface user roles and a
cafe chooser, add a dashboard switch button in the POS screen, and
register OTP validators explicitly to survive Docker layer caching.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Embed Vazirmatn web font in printed bills, add branded header with logo
and tagline, and wait for fonts to load before printing for clean output.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Problem: window.print() on the main page used A4 height (blank paper
after receipt), no RTL direction, and Tailwind styles leaked into print.
Solution — iframe isolation:
- lib/thermal-print.ts: builds a self-contained HTML document
(@page { size: 80mm auto; margin: 0 }, html { direction: rtl })
and fires it through a hidden off-screen <iframe>. The iframe
document contains only the receipt so height == content height.
- pos-slip-modal.tsx: Print button calls printThermal(buildThermalDocument())
instead of window.print(). Preview panel is unchanged (screen only).
- pos-receipt-print.css: updated @page + direction as fallback for any
remaining window.print() callers.
Works with USB driver (Atom A300) as default printer — OS print spooler
receives the job exactly as if it were any other document.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>