Fresh release output after merging dev: apps/api/release (server
bundle, migrations and the database client) and the three front-end
dist folders. All three front-ends type-check clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings the four release/delivery commits (IAM SSO for the admin panel,
both apps and the API; IAM env in docker-compose) onto fix/issues-triage.
Conflicts:
- customer-auth.service.ts imports: both kept.
- both apps' api/client.ts: both kept (AccountingBalance/WageQuote, and
iamAppUrl).
- all build output (admin-panel, user-app, user-app-v2 dist, and the api
release bundle): rebuilt from the merged source instead of picking a
side.
Fixed while verifying:
- The IAM change calls iam.browserBootstrapUrl after every login, but
the identity-service stand-ins in three test files lacked it. 15 tests
failed on release/delivery itself.
- SMS relay: SMS_RELAY_PLATFORM was read but never sent, so the gateway
never got `platform` (documented body: phone, host, text, platform).
relayConfig() now returns it ("default" → null) and the request carries
it. sms.service.spec now passes.
All 249 API tests pass; the three front-ends type-check and build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- PERS-03: «تاریخ تولد» (Jalali DateInput) on the wholesale and retail
forms, as legacy. The server accepts birth_date or birth_day, refuses
impossible or future dates, clears on '', and returns both names.
- SEC-07: setRoles stores only rids in ALL_RIDS or their tree parents.
Anything else is dropped with a warning log, so a tick on a legacy-only
tree leaf never fails the save.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Finished from a parallel session that expired (owner: «بساز»).
- RPT-01: GET admin/report/order-items — legacy report/order_item's
32-column Excel (order items, one row per unit, plus financial
documents), RTL, Jalali dates. The «گزارش تجمیعی» screen gets
«دریافت فایل اکسل». Review fixes:
- weight: computedWeightGrams is already one unit's weight; legacy
divided it by qty again, under-reporting «بدهکار» on multi-unit
lines. Not repeated.
- «درصد»: the final percent (wage − discount), as legacy.
- dates: the end date covers its whole day; a bad date is a 400.
- SMS-01: confirming or rejecting a financial document texts the customer,
as legacy CashController::toggleConfiremCash.
- SMS-02: bulk SMS substitutes %name per recipient and is actually sent
through the configured gateway (D26 relay), no longer only logged.
- Docs: 01-ADMIN-MATRIX and D16-BC-GAP-SUMMARY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cities (owner: as legacy)
- City.parentId (self relation) replaces the flat `province` label; the
migration turns each label into a parent row («استان …») first, so nothing
is lost.
- Admin pickers are legacy's tree (CityTreeSelect): the top level, the
country, is a heading. The city form and grid get «والد».
- Restored legacy _getP: a city without its own percent takes its
ancestor's. Restored the omde-customer form's city → percent autofill.
- Import copies legacy parents; delete refuses a city with children; a
parent loop is refused.
Permissions (owner approved the deletion)
- dashboard / dashboard.omde / dashboard.tak rows become dashboard.view,
then are deleted.
- Import maps them the same way: legacy types 102, 104, 105, 108 and 135
had lost the dashboard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The formula shown as a sentence with coloured terms; each term can be
switched off or given a coefficient; promotion as largest or sum; a
constant, floor, cap and rounding. A sample order line is computed by
the server for the formula in force and the draft side by side, changes
highlighted. Saving needs a change, a confirmed sample and a final
confirm; the approved sample is kept and shown as the formula's
reference, with earlier versions below.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API sends ISO timestamps where legacy sent formatted Jalali strings,
so every grid printed "2026-07-25T12:32:29.000Z". Date columns now render
1405/5/3-16:02. The order-status column is a green/red «فعال/عدم سفارش»
tag again (Customer::$STS), not bare «مجاز».
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Legacy fixed `orders.reagent_id` once, when the order was placed, and its
«میزان فروش» summed `customer_id = X OR reagent_id = X`. The app credited
orders to the customer's CURRENT referrer and left out an expert's own
orders, so 9 of 49 imported experts showed a different figure.
- Order.salesAgentId (+ referrerCustomerId for 17 legacy orders whose
referrer was not an expert); additive migration, recent app orders
backfilled from the customer's referrer
- createOrder records the expert at placement time
- one shared filter (agent-orders.ts) for the experts list, expert
detail, dashboard and the expert app; child orders follow salesAgentId
- customer page gets its «میزان فروش» back
- import carries orders.reagent_id as is; ex-experts kept as admins keep
their profile so their own orders stay linked
- reconcile-sales: 1,557 of 1,558 match legacy's own formula; balances
unchanged
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DEPLOY-01: docker-compose.yml passed only DATABASE_URL and PORT to the api
service, so JWT_SECRET, JWT_CUSTOMER_SECRET, OTP_MASTER_CODE and every SMS_TREZ_*
variable were silently dropped in Docker. A deployed build would have used the
development JWT fallback and would not have had the delivery master code at all,
so the demo login would have failed with no obvious cause. All are now forwarded,
defaulting to empty (which the code treats as unset); verified with
`docker compose config`.
TEST-01: test-sales-expert-roundtrip.cjs had localhost:3077 hardcoded and could
only connect by accident. Now defaults to the repo's API_PORT. 33/33.
DELIVERY.md documents env vars (names only), run/deploy commands, how migrations
run inside the container, the verification results and the known issues.
release/delivery contains both parity branches with no conflicts (fast-forward).
build 5/5, jest 45/45, admin smoke 43/43, apps smoke 43/43, B2B scan 34/34,
B2B writes 25/25, auth-smoke 32/32, typecheck clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
views/order/List.vue handed the timeline activity entry straight to
single/order-sts/edit. That entry is {status, title, proceed, uaj, note} and has
no order id, so UpdateOrderStatusDto rejected every call with 400 — neither
"ثبت وضعیت" nor "حذف وضعیت" worked on the retail order screen. Both handlers now
take the id from tempRow, matching OmdeList.vue and TaminList.vue, which were
already correct.
Found by the new scripts/smoke-admin.cjs (43/43), which walks login, customer
create, sales-expert create/edit, product+variant create/edit and order
list/detail/status, asserting the database changed rather than trusting a 200.
[ref: henza_server/routes/admin.php:59]
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Restores the sales-expert fields that were lost when a sales expert became
an AdminUser instead of a type=1 Customer row.
New SalesExpertProfile holds cityId, gramLevel, phone and nationalCode. A
separate table rather than columns on AdminUser, because these mean
something only for role = SALES_EXPERT. gramLevel is DECIMAL(14,4), matched
to Customer.goldCreditLimitGrams so the same credit limit has identical
precision on both tables. phone and nationalCode copy the reference exactly:
nullable, NOT unique, no format rule — createCustomer validates only mobile
and percent, and both fields are required:false on the reference form.
Inventing a uniqueness constraint would reject data the reference accepts.
reagent_id is deliberately NOT ported (D17). Every use of it in the
reference reads a CUSTOMER's reagent_id, which the target already models as
Customer.referredByAgentId. On a sales expert's own form it was a side
effect of the shared customers form: filling it removed that expert from
the selectable-agent list, because queryReagent filters on
whereNull('reagent_id'). The target's equivalent filters on role and active
only, so no agent can be dropped — strictly better than the reference.
AdminUser gains firstName/lastName with the same additive migration,
backfill and sync trigger as Customer, for all admins rather than only
sales experts. This closes D0 requirement 7, and every remaining
`fname: fullName, lname: ''` was fixed — six further sites across
admin-users, admin-comments, admin-documents, admin-orders and
admin-preorders.
listReagents now returns real city_id, city, gram_level, phone and ncode,
and left-joins so an expert without a profile still lists with empty cells
rather than vanishing. The two grid filters map through the relation. The
service upserts a profile on create and on a role change to SALES_EXPERT,
and the migration backfilled one for every existing expert.
ReagentCustomer.vue gains the four fields plus the city -> commission
autofill, ported verbatim including the reference's falsy test so a city
percent of 0 also falls through to the aux lookup (replicated, not fixed).
Tests: new sales-expert round trip 33/33, covering create with every field,
an edit form with each field populated separately, a byte-identical no-op
save, single-field edits, both grid filters with negative cases, the
left-join case and the referrer dropdown. «محمد علی» + «احمدی» survives
intact. Plus jest 32/32, auth-smoke 53/53, customer trigger 9/9, typecheck
clean on api and admin-panel. Migration dry-run twice in a rolled-back
transaction before applying; drift is zero.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Applies the two approved additive migrations and fixes every consumer.
D0 — Customer.firstName / lastName
The reference has had two columns since 2020
(2020_10_01_120430_customers_table.php:18-19); the port collapsed them, so
the edit form showed the whole name in the "first name" box and saved an
empty last name. Adds both columns, backfills first-token/rest, and keeps
fullName in sync with a customer_sync_full_name trigger. Nothing dropped.
The trigger is SQL, so Prisma cannot see it: writing the parts rebuilds
fullName, writing fullName re-derives the parts, and writing both in one
statement lets THE PARTS WIN. Documented in the migration header, the
Customer model doc comment and DECISIONS.md §D14, because a write can now
store something other than what was passed.
D9 — Customer.makingChargeOffsetPercent
Legacy customers.percent, the per-customer making-charge offset that the
app adds to the variant's making charge (henza_pwa
Mixins/product/index.ts:69-75). toRow() was returning the referring
AGENT's commission rate instead (CUST-02). DB default 0 reproduces
today's behaviour exactly, so no existing customer's price moves; the
reference's application-layer default of 3 for NEW customers
(AdminController.php:338) lives in the create form, and the field is
required on create with the reference's own message (:354).
update() and updateCredit() both accepted percent and silently discarded
it; both now persist it. Sort/filter aliases fname/lname pointed at
fullName and now point at the real columns.
SMS — Trez provider (Sms.php:57-82)
New global SmsModule/SmsService: form-urlencoded POST, 20s timeout, body
trimmed, every message persisted. send() never throws, matching the
reference, which discarded Sms::Send's result so a gateway outage never
aborted the order or approval that triggered it. The OTP text is
byte-identical, including the Android SMS Retriever markers. Credentials
come from env only; production refuses to boot without them rather than
silently locking every customer out. .env.example carries names, no values.
Admin panel: the split(' ') fallback is removed from singleCustomer.vue,
and bulkCustomer.vue defaults percent to 3, requires it, coerces the
Decimal on load so diff-send works, and only re-sends `name` when a name
part changed. ReagentCustomer.vue is untouched — its «درصد کمیسیون» is
AdminUser.wagePercent, a different field.
Tests: 32 jest (incl. 14 new SMS config/template), 9 trigger assertions,
53 auth smoke, and the full D12 round-trip on the customer entity —
«محمد علی» + «احمدی» survives create, edit, a byte-identical no-op save,
and single-field edits. Typecheck clean on api and admin-panel.
Note: migrate dev is interactive-only and could not run here, so the
migrations were applied with migrate deploy after a read-only drift check.
The only drift is the pre-existing _goldbank_migrations table, which
Prisma would DROP; it was NOT dropped (DEAD-07).
[ref: henza_server/app/Models/Sms.php, app/Http/Controllers/Adm/AdminController.php]
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- SingleProduct.vue: rebuild gallery/header/action bar as a self-scrolling
layout with a sticky photo, solid-on-scroll header matching PageBar, and
a full-screen image preview (blurred overlay, centered thumbnail strip)
- Add GallerySlider.vue: pointer-based swipe carousel isolated in an
LTR subtree, fixing RTL/transform sign mismatch that caused slide jumps
- Fix Icon.vue: icon was read into a plain ref once at setup and never
updated on prop change; switch to computed so icon swaps (e.g. cart
qty stepper trash/minus toggle) actually re-render
- Fix quantity stepper DOM order in SingleProduct.vue/CartItem.vue so
plus/minus render correctly under RTL flex
- prisma/assign-downloaded-images.js: seed real product photos (FEATURED/
FEATURED_MOBILE/GALLERY/GALLERY_MOBILE) onto the 5 placeholder products
DashboardView ("میز کار"): Rebuilt from scratch with a new structure, distinct from the old card+grid layout but visually aligned with HomeView (same blurred bar, @ink/@wine palette, staged reveal animation on mount):
Single hero card (ink→wine gradient) showing balance, gold/fiat debt, and identity — no quick-action icons inside it anymore.
Live gold rate folded into a slim inline row at the bottom instead of a titled section.
Iterated shadow/spacing on the hero card per feedback (softer, less clipped).
DialogVant / DialogVantOneButton (shared modal components, used in 7+ call sites: Dashboard, Cart, SingleProduct, Register, App): Replaced Vant's default dialog chrome with the app's own rounded-card language — circular icon badge for the illustration, pill-shaped primary/secondary buttons. No call-site markup changed.
LoginView: Rebuilt from scratch — full-bleed dark hero (brand + step copy) overlapped by a floating white form sheet, animated crossfade transition between the mobile-number and OTP steps, same staged entrance animation as Home/Dashboard. All auth logic (API check, OTP stub, session login) unchanged.
index.html was already in git pointing at hashed bundles, but the assets folder was missing from the image so nginx 404ed every JS/CSS request.
Co-authored-by: Cursor <cursoragent@cursor.com>
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
- Fix silent redirect-to-home for unauthenticated users on protected routes;
now redirects to login with a return path
- Restyle OrderCard, SearchInput, Filters, DropDown, and product card image
sizing/centering to match the app's LESS-token design language
- Fix ant-design Slider handle placement/drag under RTL and seed a valid
default range so handles actually render
- Fix chip-row edge padding and top spacing across Orders/Cart/Search lists
- Add pull-to-refresh to the dashboard and fix its header/padding to match
the fixed PageBar pattern
- Remove the floating WhatsApp button from index.html
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>