Phase 2, completing the app-wide rollout of the home/product-detail design
language (LESS colour tokens, BEM class names, hairline-bordered cards,
SectionHead, dashed empty states) onto the account side: dashboard, orders,
transactions, profile, login and register.
Reuse over re-implementation: the dashboard's live rate block is now
components/home/RateStrip.vue rather than a second hand-built strip, and
every screen uses components/ui/PageBar.vue for its chrome.
OrderCard and OrderItemCard are restyled to match. OrderItemCard drops its
always-empty note row: our OrderItem schema has no note field (legacy's
per-item note was a real column there; ours is not), so it could only ever
print a dash.
OrdersView swaps ant Tabs for the same scrolling chip rail the rest of the
redesign uses, and its full-screen Modal with hand-written CSS overrides for
a bottom Drawer, matching how every other detail surface opens.
Deleted AppHeader.vue, orphaned once every screen moved to PageBar. Its
shutter animation was driven by transitionFlag, which nothing reads any
more — the router afterEach comment claiming otherwise is corrected rather
than left asserting something untrue, and the now-inert half is flagged in
place. ScrollFetchSearch loses its isFav prop: it gated two empty-state
branches that rendered the identical image, and its only caller
(FavoritesView) now renders its own.
Behaviour is preserved throughout. Orders still reads GET
/customers/:id/orders and filters client-side; transactions still reads
GET /customers/:id/ledger-entries with legacy's CREDIT/DEBIT colour meaning
intact; profile stays read-only over real fields; login keeps the real
customer lookup plus the documented OTP stub; register stays stubbed with
copy that does not claim an account was created.
Verified in a real browser against the running API and native database:
dashboard 4 tiles with a real balance, orders 1 real order card, profile 4
real spec rows, transactions the correct empty state for a customer with no
ledger rows, and login/register confirmed with the session cleared (the
guard bounces a signed-in user away from /login, which is why they needed a
separate pass). No console errors, no horizontal overflow. vue-tsc and vite
build both clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Applies the home/product-detail design language (LESS colour tokens, BEM
class names, hairline-bordered cards, SectionHead, dashed empty states) to
the screens a shopper actually walks through: search, collection, newest,
cart and favorites.
Two shared pieces are extracted so the language stays consistent rather than
re-typed per screen:
- components/ui/PageBar.vue — the sticky translucent bar with blur and a
hairline, lifted from the home screen's own header so every other screen
gets identical chrome instead of the old dark AppHeader.
- components/product/ProductCard.vue — the catalogue workhorse card. It is
deliberately a sibling of home/ShowcaseCard (which keeps its editorial "wide"
variant and badge) and additionally surfaces stock, which the
catalogue needs and the home screen does not.
Deleted as orphaned once nothing imported them: ProductList,
ProductVerticalFull, ProductVertical, ProductHorizontal, CollectionCard —
all Tailwind utility soup carried over from the legacy PWA.
Behaviour is preserved throughout; only chrome and cards change. The cart
still posts to the real POST /orders with wagePercent from each item's
pinned variant, still gates on a real session, and still keeps the note
drawer and success step. Search keeps its client-side filtering, the Filters
sheet, category chips and collection tiles. No rial price is printed
anywhere, for the same reason as the rest of the redesign: a finished
piece's price is the live gold rate x weight plus wage, computed at order
time and never stored on the product.
FIXED a regression this redesign introduced before it shipped: dropping
ScrollFetchSearch from the search grid meant all 205 catalogue products
mounted at once (205 cards, 205 images). Both search and newest now render
a 20-item window that grows as the user reaches the end.
Verified in a real browser against the running API and native database, not
just a green build: home 13 cards, search 20 -> 40 -> 60 -> 80 on scroll,
newest 20 -> 40 -> 60, cart empty state, favorites 3 cards behind a seeded
session. All images loaded (0 broken), no horizontal overflow, no console
errors beyond pre-existing vue3-lazyload noise that predates this work.
vue-tsc and vite build both clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Design language introduced here and reused across both screens:
- LESS colour tokens (@ink/@inkSoft/@inkMuted/@inkFaint, @wine/@wineSoft,
@canvas/@canvasSoft, @hairline/@hairlineSoft) added to
themeConfig/colors.json and injected globally via vite's modifyVars, so
scoped <style lang="less"> blocks resolve them with no import
- BEM-ish scoped class names, hairline-bordered cards at 14-18px radii,
ink surfaces with a wine accent, dashed-border empty states
- components/home/: SectionHead, ShowcaseCard, ShowcaseSlider, CategoryRail,
RateStrip, AccountStrip
Data honesty is deliberate throughout: no rial price is printed for a piece,
because a finished item's price is the live gold rate x weight plus wage,
computed at order time and never stored on the product. Only genuinely
present fields are shown (weight, making-charge percent, stock, category,
brand). Categories and the hero are derived from the real catalogue, so an
empty catalogue renders an empty state rather than placeholder tiles.
Also fixes a real bug in the product detail view: it ran, at module scope,
history.pushState + window.onpopstate = () => history.go(1). Because the
drawer mounts its slot eagerly, that fired for every product card on the
home screen and permanently hijacked the back button for the session.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The folder-discovery fix in the previous commit surfaced a second problem.
Databases migrated BEFORE that fix recorded the baseline under the name of
the hand-added flat duplicate that used to sit beside it
(prisma/migrations/0001_init.sql). Once the runner started discovering the
real folder (20260910113720_first), it saw a name it had never recorded and
tried to apply the baseline again — against a database that already had it:
Raw query failed. Code: 42710.
ERROR: type "AdminRole" already exists
The two files are identical SQL (verified byte-for-byte after normalising
line endings; they differ only by one trailing blank line), so the recorded
legacy name genuinely means that migration is applied. A LEGACY_NAME_ALIASES
map now treats '0001_init.sql' as an already-applied marker for
'20260910113720_first', so upgrading an existing database is a no-op instead
of a crash.
Verified against the local dev database this actually broke on
(postgresql://localhost:5432/gold_banking, which had recorded only the old
flat name): 'npm run dev' in apps/api now logs 'No pending migrations' and
'Nest application successfully started' instead of dying. That database's
tracking table was also reconciled to record both real migration names, and
it was confirmed to already contain both migrations' schema (AdminRole,
City.percent, user_type, CustomerKind all present) — nothing was re-run
against it. The dockerised database is unaffected: it already records the
real names, so the alias is inert there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three real bugs found and fixed while wiring up product images end-to-end:
1. src/prisma/migrate.ts only discovered flat prisma/migrations/<name>.sql
files, never Prisma's own folder-per-migration layout
(<timestamp>_<name>/migration.sql). Every migration created the normal
way (via 'prisma migrate dev') was silently skipped; only a hand-added
flat duplicate (prisma/migrations/0001_init.sql, now removed) ever ran.
This meant schema.prisma had drifted well ahead of what any real deploy
database actually had (missing columns on City, Customer, Branch,
Product, several new tables). Fixed to discover both layouts; the
drift is closed with a new 20260914165255_sync_schema_drift migration.
2. Neither nginx.conf (production) nor vite.config.ts (dev) proxied
/uploads/ to the API — only /api/. UploadedFile/ProductImage URLs
(/uploads/<file>, served by main.ts's useStaticAssets OUTSIDE the /api
prefix) 404'd through both. Both now proxy /uploads/ the same way /api/
already was.
3. docker-compose.yml's api service had no volume for storage/uploads, so
every uploaded file — and by extension every ProductImage row pointing
at one — was lost on the next container recreate/redeploy. Added a
named volume.
Also: main.ts's static file serving now sets Content-Type: image/avif
explicitly (serve-static's bundled mime table predates .avif and served it
as application/octet-stream; browsers rendered it anyway by sniffing, but
a stricter client would not have).
Product photos: copied 15 real jewelry photos into the uploads volume,
registered them as UploadedFile rows, and assigned each of the 5 existing
Product rows a randomly picked one (duplicates allowed, per request),
replacing their /img/no-items.png placeholder ProductImage row. Verified
end-to-end in a real browser against the real containerized API+DB: logged
in, opened the admin product list, confirmed all 5 rows render their photo
(naturalWidth/Height populated, network 200s, no console errors).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each now has an add button at the top of its own list page, so the nav
entry duplicated it:
- 'افزودن دسته بندی' (/category/add)
- 'وارد کردن سند مالی مشتری' (/add/findoc)
- 'تعریف تخفیف تک' (/major/promotion/tak/add)
- 'تعریف تخفیف عمده' (/major/promotion/add)
- 'افزودن فاکتور' (/invoice/add)
- 'افزودن شهر' (/city/create)
Only adminMenu is touched; all six routes still exist and still work as a
direct-link fallback. Parent submenus keep their remaining children
('تخفیف اتوماتیک' and 'لیست تخفیفات ' both still sit under 'تخفیفات', for
instance).
Verified in a real browser: menu items 26 -> 20, none of the six labels
render, all five parent submenus (دسته بندی ها، اسناد مالی، تخفیفات، فاکتور
انتقال، شهر ها) still present with their list children, icons unchanged at
16, no console errors. Committed dist/ artifact rebuilt to match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each of these now has an add button at the top of its own list page, so the
nav entry duplicated it:
- 'اضافه کردن شخص' (/customer/add)
- 'افزودن شعب' (/branch/add)
- 'افزودن کارگاه' (/workshop/add)
- 'افزودن مدیر' (/user/create)
Only the adminMenu tree is touched; branchMenu keeps its own /customer/add
entry, since the branch build has no converted add-modal. All four routes
still exist and still work as a direct-link fallback, and the parent
submenus keep their remaining children (including 'دسترسی ها' under مدیران).
Verified in a real browser: menu items 30 -> 26, none of the four labels
render, all four parent submenus still present with their list children,
icons unchanged at 16, no console errors. Committed dist/ artifact rebuilt
to match (force-added, as it is gitignored but deliberately tracked for
Dokploy).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dist/ is gitignored by apps/admin-panel/.gitignore but is deliberately
committed for Dokploy copy-only deploys (see root .gitignore's note), so it
is force-added. The previous artifact was a pre-port prototype build; the
previous commit removed its two stale assets without being able to add the
new ones, leaving index.html pointing at files absent from git. This restores
a self-consistent artifact built from current source.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Dockerfile does `COPY dist /usr/share/nginx/html` and nginx serves that
folder's root, but vite's outDir was 'dist/export' (carried over from legacy's
vue.config.js). So the deployed root kept serving whatever index.html had been
committed at dist/ — a stale pre-port prototype build (lang="en", title
"Gold Bank — Admin Panel", no ant-design-vue at all, committed 2026-09-10 in
cea3b80). That artifact, not the current source, renders as an unstyled stack
of bullets and bare tables.
outDir is now 'dist' (and 'branchDist'), matching what the image serves and
matching apps/user-app's convention.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The ADV4 upgrade dropped the old `ant-design-vue/dist/antd.less` import but
never added its replacement. ADV4 injects each COMPONENT's styles at runtime
via CSS-in-JS, but NOT the global baseline — that ships separately as
dist/reset.css and must be imported explicitly. Without it the page lost
box-sizing, the body margin reset and the browser default list/heading
resets, so the layout rendered unstyled.
Verified in a real headless browser this time, not just a green build:
reset.css now resolves through LESS and its rules are present in the built
CSS, the CSS-in-JS engine injects component styles (14 <style> tags on the
login page), and buttons/inputs/cards compute real ant metrics with dir=rtl.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bulkCustomer.vue (reached today only via /customer/add's AddCustomer.vue tab
wrapper, and via /bulkCustomer/edit/:id from 5 different list variants) gets
open/id modal support; both routes stay as a fallback.
OmdeCustomerList.vue is shared by 5 list pages (OmdeList, BankerList,
ShopkeeperList, OmdeNonVerifiedList, OmdeArchivedList). Per user decision, the
add button and modal-based edit are wired only on OmdeList.vue (the one this
form conceptually belongs to) via a new showAddButton prop, default false —
the other four keep navigating to the edit route unchanged.
singleCustomer.vue and ReagentCustomer.vue were also planned as add-form
candidates but have no create entry point anywhere in the app (edit-only,
reachable solely via /singleCustomer/edit/:id and /reagent/edit/:id) — left
untouched, nothing to convert. offersIgnore-add-edit.vue is a standalone
settings page with no list counterpart, not a create form — left as a page.
Verified: pnpm build passes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PromotionForm.vue (shared by the omde and tak variants) gains open/id modal
support; both thin wrappers (promotion_add_edit.vue, promotion_tak_add_edit.vue)
pass the props/emits through unchanged. promotion_list.vue gets two
ListPageHeader buttons (offers.create / offers.create.tak, matching the two
routes' rids) and picks which variant's modal to open based on the edited
row's type_customer, exactly as the old toEdit() picked which route to
navigate to.
Verified: pnpm build passes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same open/id pattern, kept at width=xl since it embeds its own product-picker
XTable plus a running cart table. Also deduped ListInvoice's confirmDelete
onto the shared useDeleteHandler composable (missed in the earlier delete-
handler pass since it wasn't byte-identical to the other 6 — it used
message.success + a different fallback string, both preserved).
Verified: pnpm build passes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same pattern as branch/workshop/city: open/id props switch UserCreate.vue and
apps/category/Add.vue into BaseFormModal mode; the /user/create,
/user/edit/:id and /category/add/:id? routes are unchanged as a fallback.
ListUser.vue's pre-existing add button (previously a router.push, added
outside the original port and left there per its own comment) now opens the
modal via ListPageHeader instead.
Verified: pnpm build passes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both share FacilityList.vue, which now accepts an addRid/addLabel pair and a
#form slot: when addRid is supplied, edit opens the slot's modal instead of
navigating, and a ListPageHeader add button appears above the list. Branch
and workshop each plug their own AddBranches/AddWorkshop instance into that
slot, keyed by editingId so switching rows remounts the form (fresh
onMounted load) rather than needing new watch-based reactivity.
The /branch/add, /branch/edit/:id, /workshop/add, /workshop/edit/:id routes
are unchanged and still work as a route-based fallback.
Verified: pnpm build passes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Establishes the pattern the remaining 15 add/edit forms will follow:
- The form component accepts open?/id? props. When open is defined it's
used as-is; asModal is fixed at mount (not reactive) since the parent
decides once whether this instance is a modal or a route page.
- As a modal it renders through BaseFormModal (header=title, footer=
buttons, body=form) and emits success + update:open instead of
navigating; as a route page (the old /city/create still exists and still
works) it falls back to the original <a-card> + button layout unchanged.
- The list page gets a ListPageHeader add button above the list, gated on
the exact rid the route used to gate on (cities.create), opening the
modal instead of navigating.
Verified: pnpm build passes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- VerticalNavMenu.vue now renders the icon: values that navMenuItems.ts
already carried on 33 nodes but were never displayed, via the shared
resolveIcon() map. Sub-menus and leaf items get a #icon slot each; ADV4's
Menu shows icons only (labels hidden) once :inline-collapsed is true, which
is now wired to the collapsed prop.
- Fixed openKeys: it previously started at [] and was only ever updated by
user clicks, so a direct link or page refresh landed with the active
item's parent submenu collapsed. It's now derived from the current route
on load and on every navigation.
- Sidebar collapse state moves from a local ref in Main.vue to
stores/layout.ts's existing (previously unused) sidebarCollapsed, persisted
to localStorage so it survives a reload.
Verified: pnpm build passes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New src/components/common/:
- BaseFormModal.vue — the standard shell every converted add/edit modal will
use: header=title, footer=buttons, body=form. Replaces the ad-hoc
60%/70%/80% <a-modal> widths used across the codebase with 4 named sizes.
- ListPageHeader.vue — the add button that goes above every list page,
gated on the same rid the create route used (modelled on the one place
this already existed, ListUser.vue's add-user button).
New src/components/icons.ts: the legacy-Feather-name -> ant-icon map,
extracted out of TooltipBtn.vue so the sidebar can resolve the same names
through the same map instead of a second copy.
New src/composables/useDeleteHandler.ts: the delete-then-refetch-then-toast
handler that was copy-pasted verbatim in 6 list views (city/cityList.vue,
user/ListUser.vue, apps/category/List.vue, finical/list.vue,
apps/product/ProductList.vue, manage/Const.vue). Preserves each site's exact
success-message wording and message.success/info choice, and ProductList's
irregular POST-based delete endpoint.
Verified: pnpm build passes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Approved departure from the port's byte-faithful rule (docs/ADMIN-PORT-PLAN.md
bug rows 4-6), fixed and logged rather than silently changed:
- farsi.less: bold/700 @font-face blocks now load iran-yekan-700.woff2
instead of the 400 file, so bold text actually renders bold. No source
file exists for weight 300, so that face is dropped rather than faked.
- format("woff") corrected to format("woff2") on all faces.
- Removed 'font-weight: inherit' from the body,* reset, which was flattening
every heading's weight.
Also deduped 24 view files that each redeclared .grid-2/.grid-3/.grid-4 in
a scoped <style> block with drifted minmax values, though utilities.less
already defines all three globally. Removed the scoped copies; kept
PromotionForm's genuinely different 0.5rem gap as a small .grid-4--tight
modifier instead of a full redeclaration.
Verified: pnpm build passes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Bump ant-design-vue 3.2.20 -> 4.2.6, @ant-design/icons-vue 6.1.0 -> 7.0.1.
- ADV4 removed LESS theming (modifyVars) for CSS-in-JS design tokens: drop
the vite.config.ts modifyVars block and the antd.less import; move the
three tokens ant actually consumed (primary/link/border-radius) to
App.vue's <a-config-provider :theme>.
- Delete src/assets/styles/ant.less (426 lines of hand-written RTL flips):
ADV4 renders RTL natively via CSS logical properties given direction="rtl",
which App.vue already sets. Preserve only the small residue ADV4 doesn't
cover natively (upload filename LTR, select dropdown z-index, tabs nav
direction) in a new rtl-overrides.css.
- Clean up the stale --primary/--accent token comment in utilities.less now
that ant.less is gone.
- Verified: pnpm build (vue-tsc -b && vite build) passes with zero errors;
the codebase already used v-model:open everywhere (no ADV3 visible prop
usage to migrate).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>