The app shows a sales expert their own account through the linked trading
Customer row (/customer/me). That row was seeded from the expert once, at first
sign-in, and never updated again, so a name, mobile, credit-limit or wage-percent
change made in the panel never reached the app. In the reference the agent and
that account were one row, so an edit changed both.
Editing an expert now mirrors exactly the changed fields onto the trading row in
the same transaction: name parts, credit limit (gram_level), wage percent, and the
mobile when no other customer holds it. Existing drift heals on the next save.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- PrismaExceptionFilter returned "Unique constraint failed on: mobile" in
English for a duplicate. It now answers in Persian, naming the field
(mobile, national code, member code, order number), with a safe generic
fallback. Covered by a unit test.
- The admin panel still shipped the old "NZ" favicon. favicon.png/.ico are now
generated from the project logo (the same one the user apps use), in both
public/ and dist/, and the links carry ?v=2 so browsers drop the cached icon.
- Rebuilt apps/api/release/app.js.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The reference (henza_server AdminController::getCustomer) lists type=2 AND
is_archive=0 with no verification or active filter. The develop-branch change
that hid inactive customers with a pending verification step from «عمده»
emptied the list for customers migrated in that state, while their numbers
stayed taken ("already exists"). Restore parity; the sales-expert trading
account exclusion is kept.
Includes the rebuilt release/app.js and a regression test.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
app.js, seed.js, the Prisma client in release/vendor and the new migration
20261026120000_split_admin_table, built with `pnpm build:release` (API only;
the admin panel and user apps are unchanged by this work).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
AdminUser held both panel staff and sales experts, so one mobile could not be
both. Staff now live in "Admin" (panel only); sales experts stay in "AdminUser"
untouched. Uniqueness is per table.
- migration 20261026120000_split_admin_table: additive only. Creates "Admin",
copies non-expert rows with their ids, re-points the "who did this" foreign
keys (ledger, order status, sms, notification, upload, allocation, workshop,
admin session) to "Admin" without moving data. Never writes to "AdminUser".
- login, JWT, SSO, branch/workshop supervisors and the users grid use "Admin";
SALES_EXPERT create/edit still goes through admin/users into "AdminUser".
- an admin cannot be turned into an expert or the reverse (different tables).
- manual/: optional phase-2 script to drop the dormant admin copies from
"AdminUser", and a read-only expert-login diagnostic.
- docs/ADMIN-TABLE-SPLIT.md: what changed, deploy checklist, rollback.
Tested on a copy with 12,000 experts: expert rows byte-identical (md5) before
and after; full migration chain applies and matches schema.prisma.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Keep admin browser bootstrap from falling back to the customer app URL, and exclude activated customers from the pending-verification list so it no longer mirrors the main omde grid.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add paged orders API and infinite scroll, short order numbers, and align the guest invite card with the logged-in hero shell.
Co-authored-by: Cursor <cursoragent@cursor.com>
اپ۲: دکمهی «افزودن به سفارش» در صفحهی جزئیات محصول دیگر از پایین صفحه بیرون نمیافتد و همیشه دیده میشود.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
اپ۲: رنگ دکمهها، نوار نرخ طلا و خطهای جداکنندهی بالای صفحه از سرمهای به قرمز-شرابی برند تغییر کرد.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
اپ۲: رنگ سرمهای از پسزمینهی بالای برنامه و رنگ نوار مرورگر حذف شد؛ همهجا از رنگ صفحهی اصلی استفاده میشود.
پنل: فرم افزودن شخص با هر بار جابهجایی نوع شخص خالی باز میشود و اطلاعات نیمهکارهٔ قبلی دیگر نمایش داده نمیشود.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
پنل: بخشهای «تک فروشی» داشبورد (کارت، نمودار روند و تفکیک عمده/تک) فقط برای مدیر اصلی نمایش داده میشود.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
اپ: لوگوی تازهی برند در صفحهی اصلی، لودر و صفحهی ورود نمایش داده میشود.
پنل: لوگوی تازهی برند در ورود، منو و کامنتها نمایش داده میشود.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
اپ: نام برند در همهی صفحهها و پیامها یکسان شده و از یک تنظیم مرکزی خوانده میشود.
اپ: کارت حسابداری در صفحهی مشتری بازطراحی شد؛ ماندهها و وضعیت لینک واضحتر نمایش داده میشوند.
پنل: کارت حسابداری در پروندهی مشتری بازطراحی شد؛ ماندهها، آخرین ارسال و فرم لینک دستی واضحتر شدهاند.
پنل: ستون «کد» در جدولها بهطور پیشفرض پنهان است و از انتخاب ستونها قابل نمایش است.
پنل: تغییر نمایش ستونها در جدولها بلافاصله اعمال میشود.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Migrations were applied in plain name order, so 0002_admin_schema.sql ran
before 20260910113720_first and failed with relation "City" does not exist.
sortMigrations now always puts the baseline first; existing databases are
unaffected because their applied names are unchanged.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Only the template, styles and decorative icon imports change. The script
logic (login flow, validation, redirects) is identical to dev.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Zarrin v228 was tested on the real install (test account 953): negative
balance = owes, and temporary documents move the balance at once. Update
the admin copy and provider comments to match what was verified.
پنل: متن راهنمای زرین بهروز شد: علامت مانده روی نصب واقعی بررسی شده است، و هشدار مربوط به ثبت سند موقت در «ثبت سفارش» اضافه شد.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Side branches (fix/*, integrate/*, release/*, ui/admin-responsive, migration/legacy-data)
are fully contained in dev and main. dev and main are the only long-lived branches now.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Rebuilt dist/ and api release bundle, with the migration for catalog paging indexes.
Fix Less build error: fade(@token, N%) on theme tokens failed in both user-app
and user-app-v2 CustomerChoiceView; use alpha(), the project's token-aware helper.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Tested 20/20 on the owner's live Zarrin v228 (870 accounts, 656 items):
Reads fixed:
- listKarSakhte: now pages by 100 via pageNum (was returning only first 100 of 656)
- goroohSakhte: HTTP 400 with a valid body is now accepted (was treated as missing)
- parvande «مانده از قبل» row is not a balance; skip it and anchor the running
balance on the live listMandeHesabha balance, walked back through movements
- infogardeshsakhte: hand-printed JSON repaired, timeout raised to 90s
- document delete: now searches all ledger pages, not just the first one
- listhesabha names are «first * last»; the «*» is now stripped
- keep-alive ECONNRESET: a socket hang-up on a reused connection is retried once
on a fresh connection (server closes idle connections)
Writes fixed (test account 953 «تست اپ گلدبانک»):
- temporary flag: set from shomareSanad («موقت»), not noeFactor
- noeFactor «فاکتور موقت» also flags a document left open in Zarrin's own UI
New: test script apps/api/scripts/test-accounting-zarrin.ts (13 read + 7 write checks)
Docs: §13 added to ACCOUNTING-INTEGRATION-PLAN.md; ACC-01 in ISSUES.md updated
New: docs/FRESH-SERVER-DEPLOY-AUDIT.md (fresh-server deploy checklist)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Step 3 of docs/CATALOG-PAGINATION-PLAN.md. Step 2 added the targeted
routes; these are the five callers that were fetching all ~16k products
to find a handful of rows:
product screen whole catalogue, then find(id) -> GET /products/:id
cart re-sync whole catalogue for 2-3 lines -> ?ids=
favourites whole catalogue, filtered here -> ?ids=
home categories categories derived from it -> /products/facets
order row labels joined on variantId -> included with the order
Three of these were not merely slow, they were going to be wrong.
Favourites filtered the loaded catalogue, so once the list pages a
favourite outside the loaded slice silently disappears. Home categories
were derived the same way, so a category whose products all sit further
down would vanish from the home screen. Order rows looked their title up
in the catalogue, which ALREADY failed for a product that had since left
the storefront — exactly the old orders history exists for — and
rendered a blank row.
Order labels went to the server rather than to another id lookup: the
item carries productVariantId, not a product id, so ?ids= could not
answer it. One nested include in getOrders returns the variant, its
product and the product's first image. Deliberately NOT filtered by
storefront visibility, so a de-listed product still shows what was
bought; asserted in the new spec. OrderItemCard is synchronous now — no
fetch, no onMounted, the row renders complete on its first frame.
variantLookup is deleted from catalogStore rather than left behind: it
existed only to label order rows, and keeping it would have kept a
reason to load the full catalogue.
getProductsByIds chunks at 100 because the route caps ids there and
drops the rest. A partial answer reads to callers as "these products are
no longer on the storefront" — the cart would silently drop valid lines.
Favourites already run to 874 rows in the imported data (FAV-01).
A failed cart re-sync now leaves the cart alone: it is the person's own
data in localStorage, and dropping lines because the network blinked is
worse than a slightly stale price. The product screen tells a real 404
from a failed request, so only the former renders the not-found page.
One bug introduced and caught while doing this: moving the home
categories to /facets meant memory.init() no longer loaded the catalogue
as a side effect, and the home rails read catalog.products — the home
screen would have rendered empty rails. HomeView now loads it
explicitly. Found by grepping every caller of configStore.init, which is
also how I know HomeView is the only one.
Still on the unpaged list by design: the home rails and the
search/newest lists. Moving those onto paged reads is step 4, kept
separate because it is the risky part — debounce, loading state, and
telling "no results" from "still loading".
api 334/334 across 30 suites; both apps vue-tsc clean. getOrders has a
spec now (3 cases) because its include is a contract both apps depend on
and the method had no test at all. Nothing click-tested — the cart
re-sync and the favourites screen are the two worth a manual pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The real catalogue reaches ~16k products and GET /catalog/products had no
paging parameters at all: it returned every active product with its
variants, images, category, brand and collections in one response. The
apps' PAGE_SIZE = 20 was only a render window over that full array, so
scrolling made zero requests. The reference paged this server-side; the
port had moved it to the client (STUB-03), so this is a return to
reference behaviour.
Step 1 — indexes, which were not optional. Product carried NO index
whatsoever (not isActive, categoryId, brandId or createdAt) and
ProductVariant had none on productId, so every per-product include
scanned its whole table. Adding take/skip on top of that would have
paged the response without making the query faster, which is the exact
failure this change exists to avoid. Product_isActive_createdAt_idx is
composite so one index serves the filter, the default ordering and the
OFFSET walk.
CREATE INDEX, not CONCURRENTLY: the deploy image's migration runner
wraps each file in a transaction, which CONCURRENTLY cannot join (D15).
Every statement is IF NOT EXISTS because both migration paths run it.
Step 2 — the routes. Paging is opt-in, which is the whole
backward-compatibility story: with no query parameters the route returns
the same plain array from the same cache as before, so the admin panel,
the smoke scripts and installed app builds are untouched. Pass page,
limit or any filter and it becomes { rows, total, page, limit }.
The filters had to move too, not just the paging. The apps filtered
title, category, brand and gram range in the browser over the full
array; paging the response while leaving that in place would have made
search quietly WRONG — searching only the first page and reporting
"محصولی پیدا نشد" for products that exist. So every filter is built into
the Prisma query, and count uses the same where clause, so `total`
describes the set the app is actually scrolling.
Three routes added so the apps can stop fetching everything to find one
thing (wired up in step 3): ?ids=a,b,c for the cart, favourites and
order items; /:id for the product screen, which today downloads the
whole catalogue to find one row; /facets for the filter sheet's options,
which the apps currently derive by scanning the catalogue in the
browser.
Deliberately not cached: CatalogCacheService exists because the unpaged
query is expensive and every screen wanted it. A paged, filtered query
is neither — bounded by limit and index-served — and caching it would
need a key per filter combination, i.e. unbounded memory for cheap
queries plus a second place for stale rows to hide.
D27's two storefront rules (active, has an image) live in one shared
where clause, so the list, its count, the single-product route, by-ids
and the facets can never disagree about what is visible. limit and ids
are both capped at 100; an uncapped limit is just the old "give me
everything" query spelled differently.
17 new cases, 329/329 overall. All 17 passed on the first run, which is
not evidence by itself, so one filter was deliberately broken to confirm
the suite catches it (1 failed/16 passed) and then restored.
Not measured on real data — the database container was not running, so
16k-row behaviour is reasoned from the query plan, not timed. Worth one
EXPLAIN ANALYZE on a full dataset before step 4.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reported: from the profile page, going to login and pressing back landed
on home instead of the profile — with the instruction to audit the whole
structure rather than that one case. Seven defects, four reachable in
ordinary use.
NAVIGATION
PageBar's back arrow pushed a FIXED route name (`backTo`) and ignored
history, so it could not know where the person came from. That was true
of 14 of the 15 PageBar uses in each app — only SearchView used
router.back() — so it was never one screen's bug. Inherited from the
reference, which pushed a fixed name in every component's back arrow;
not replicated (D4).
`backTo` is now a fallback, used only when there is nothing to go back
to (a shared link, a refresh). All 14 declarations are kept untouched,
because that is exactly when they are right.
Answering "is there anywhere to go back to?" needs care: history.length
counts entries from before the app was opened, so a link opened in a
fresh tab would look navigable and back would walk the person out of the
app. navStore records the history `position` of the app's first
navigation and compares the current one against it — direction-proof,
unlike a counter, which only grows and would be wrong after one
back-and-forward. Confirmed vue-router writes `position` on every entry
rather than assuming it.
Three pushes became replaces, each of which left behind a page the
guards immediately bounce, making "back" a no-op or a trip home:
- after sign-in the login screen stayed in history — the worst, since
the same file already honoured ?redirect=, so signing in and
pressing back disagreed about where the person belonged;
- the 401 handler kept the page whose request had just failed;
- complete-profile kept a form that is unreachable once the documents
are in.
CartView's post-order push is left alone: back lands on the emptied
cart, which is a real screen, not a dead end.
READ FRESHNESS
Owner's rule: fetch what a screen needs when it is opened, rather than
caching with a lifetime. Better than the TTL I had proposed — a TTL
guesses how long data stays true, while entering the screen is when it
actually matters.
OrdersView was the worst: it is in the keep-alive list but only loaded
in onMounted, so the second and every later visit made no request at
all, and the first read the cache. Order status changes on the admin
side and nothing here invalidates it, so the list could be stale for a
whole session. Ledger entries had the same shape and a stronger reason —
they are written by the accounting side, which this app can never learn
about. The gold price was forced only by pull-to-refresh on the two
screens that show it most prominently.
The catalogue is deliberately NOT re-read, by the owner's own reasoning
that this must not add server load: it is the entire product list in one
response, it changes when an admin edits it, and pull-to-refresh and
placing an order already invalidate it.
The cache layer itself is sound — concurrent callers share one in-flight
promise, and a response arriving after an invalidate is returned but not
stored, so a write cannot lose a race against a read in flight. Its one
real gap was that nothing ever went stale. The policy is now written at
the top of queryCache.ts so a TTL is not re-introduced.
Both apps vue-tsc clean. Not click-tested in a browser; the back-button
behaviour is worth one manual pass.
اپ: دکمه بازگشت به صفحه قبلی واقعی برمیگردد و اطلاعات هر صفحه در هر بازدید بهروز میشود.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>