Compare commits

74 Commits
Author SHA1 Message Date
s.esmaeiliandClaude Opus 5.5 2bbd9408dd build: rebuild the server bundle, admin panel and both apps from main
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>
2026-10-10 10:24:53 +03:30
s.esmaeiliandClaude Opus 5.5 8ff805a11c Merge dev into main
Brings main up to date with dev: the issues triage (favourites, ledger
debt, making-charge formula and «اجرت» screen, order ban, cities tree,
permissions, birth date, unused-code removal, production safety checks)
and release/delivery's IAM single sign-on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 10:20:19 +03:30
s.esmaeiliandClaude Opus 5.5 1bc3739e78 Merge release/delivery (IAM single sign-on) into the triage work
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>
2026-10-10 09:03:53 +03:30
s.esmaeiliandClaude Opus 5.5 9d3b5388ba fix(security): a production server refuses to start until its keys, allowed origins and test login are right (REL-01, REL-03, SEC-04)
With NODE_ENV=production (always, in the Docker image) the API lists
every problem at once and does not start while any of these holds:
- OTP_MASTER_CODE is set: the fixed test login signs in any customer. A
  test server built from the production image must say so with
  TEST_SERVER_ALLOW_MASTER_CODE=yes.
- JWT_CUSTOMER_SECRET is missing, equal to JWT_SECRET, or under 32
  characters.
- JWT_SECRET is missing, the published dev key, or under 32 characters.
- CORS_ORIGINS is empty, `*`, or not full origins. It no longer falls
  back to allow-all.

Checked live: a misconfigured start lists all four problems; a correct
one starts, answers the allowed origin, and blocks any other.
docs/SERVER-SETUP-CHECKLIST.md is the operator's list. DELIVERY.md,
docker-compose.yml and .env.example are updated to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 17:00:17 +03:30
s.esmaeiliandClaude Opus 5.5 d91dcf197b feat(admin): birth date on the customer forms; permission saves limited to known rids (PERS-03, SEC-07)
- 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>
2026-10-08 16:45:41 +03:30
s.esmaeiliandClaude Opus 5.5 7bb5c2e00f chore(apps): remove unused code and packages (DEAD-01, DEAD-02, DEAD-03)
Owner approved the deletions. Found by walking imports from each app's
main.ts; sw.ts stays (the PWA plugin's entry).
- user-app: 8 unreachable components, loadingStore.
- user-app-v2: 11 unreachable components, loadingStore.
- both: the unused validators and price helpers, and the gsap and
  @ant-design/icons-vue packages.
- the stray root user-app-v2/ folder (an old package.json and
  vite.config.ts that nothing referenced).
Both apps type-check clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:40:10 +03:30
s.esmaeiliandClaude Opus 5.5 2d74f53d0a feat(admin): D16 (b)(c) — Excel order-item report, document-confirm SMS, %name in bulk SMS
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>
2026-10-08 16:34:45 +03:30
s.esmaeiliandClaude Opus 5.5 fbe4f38e06 feat(cities): cities are a tree again, as legacy's; old dashboard permissions merged (PERS-02, PERM-01)
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>
2026-10-08 16:31:12 +03:30
s.esmaeiliandClaude Opus 5.5 fbe7b98ada feat(admin): «اجرت» screen — edit the making-charge formula, check a sample, approve
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>
2026-10-08 11:26:56 +03:30
s.esmaeiliandClaude Opus 5.5 699657e483 feat(pricing): the server prices app orders with legacy's making-charge formula (MON-08)
Legacy: final % = variant % + customer % − the largest active promotion
on the product, its category or its brand; rial charge 0. The apps sent
the variant's own % only, so the new app undercharged by the customer's
percent (usually 3) and ignored promotions, and the server trusted
whatever the app sent.

- pricing/wage-formula.ts: one formula, legacy by default, with
  coefficients, promotion pick, constant, floor/cap and rounding so the
  admin screen can change it; validated
- an app order is priced by it; the app's wage, discount and rial charge
  are ignored; an operator's manual numbers are kept
- POST customer/me/wage-quote; both apps show that figure
- admin/pricing/wage-formula (config.wage): read, preview, approve with
  the sample it was checked on, last 20 versions kept; the setting is
  kept off the generic const routes

Owner approval: 2026-10-08.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 11:26:55 +03:30
s.esmaeiliandClaude Opus 5.5 4b7b2473fc feat(orders): «عدم سفارش» blocks app orders; import starts everyone allowed (MON-05)
Legacy stored customers.sts but no code read it, so the switch never
stopped an order, and 313 active customers carry it, mostly as the
sign-up default. Owner option 2: enforce it from now on, for orders
placed from the app only (an operator can still order for the
customer), and import every customer as allowed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 11:16:29 +03:30
s.esmaeiliandClaude Opus 5.5 1c850216e9 fix(security): no dev key, demo seed or open CORS in production (SEC-04, SEC-05)
- production refuses to boot without JWT_SECRET instead of signing
  admin tokens with the published development key
- CORS_ORIGINS restricts browser callers; blank keeps allow-all with a
  production warning so a running deployment is not cut off
- the boot-time seed creates only roles and permissions in production;
  the demo customers, orders, debt and known-password accounts need
  SEED_DEMO_DATA=true

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 11:00:45 +03:30
s.esmaeiliandClaude Opus 5.5 fb25b2251b fix(ledger): an order's debt follows its status, as legacy recomputed it (ACC-03)
Legacy never posted order debt; updateRemain recomputed it as
SUM(orders.input) WHERE sts > 4. The port posted it once on entering
DELIVERED_TO_AGENT, guarded on deliveredAt, which:
- posted it twice when an order reached that status again
- posted nothing for an order sent straight to DELIVERED_TO_CUSTOMER
- kept it after a rollback or cancellation

syncOrderDebt compares what the order should owe with the confirmed
ORDER_STOCK entries on it and posts only the difference (append-only,
idempotent). Status removal runs it too, so removing «تحویل به نماینده»
is allowed again, as in legacy. Existing data: 0 orders out of line.

Owner approval: 2026-10-08.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:57:38 +03:30
s.esmaeiliandClaude Opus 5.5 5d386bf752 feat(favorites): keep customer favourites on the server, as legacy did (FAV-01)
Both apps kept favourites in memory only, and the API had no route, so
the 874 favourites imported from legacy major_favorites never showed.

- GET customer/me/favorites, POST customer/me/favorites/toggle
- apps load them on sign-in, clear on sign-out, toggle optimistically
  and roll back on failure

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:54:17 +03:30
s.esmaeiliandClaude Opus 5.5 5480398a93 docs: issues triage plan; close items verified fixed
MON-01/V2-01, V2-02/APP-02 are fixed in code; MON-03 has no effect
(Prisma.Decimal(4.331802) is exactly 4.331802); PERS-03 has its column.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:54:17 +03:30
s.esmaeiliandClaude Opus 5.5 53b61ae80d docs: migration map §10.9 — order expert credit and the owner's rulings
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:40:22 +03:30
s.esmaeiliandClaude Opus 5.5 38ce3d2488 fix(admin): Jalali dates in grids and legacy order-status tags
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>
2026-10-08 10:40:22 +03:30
s.esmaeiliandClaude Opus 5.5 29084904e9 feat(orders): credit each order to the expert who placed it, as legacy did
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>
2026-10-08 10:40:21 +03:30
mahmood dcb74603ad feat add some for users in iam 2026-10-08 00:10:10 +03:30
mahmood 1b02780881 add ennv need in server for iam? 2026-10-07 22:02:53 +03:30
mahmood 40a28d5605 Feat build new 2026-10-07 21:26:54 +03:30
s.esmaeili 8c5d971d1f migration 2026-10-07 20:20:27 +03:30
s.esmaeiliandClaude Opus 5.5 92831ed33e docs: migration map — verified data facts, corrections, import tool
DATA-MIGRATION-MAP.md:
- §10.7 is rewritten from the full dump loaded into MySQL. The legacy
  debt formula is confirmed (1,590/1,591 balances). The earlier claims
  that new_city is a cities5 id, that birthdays are clean and that
  types 3/4/5 are unknown were wrong and are corrected in a table.
  Favourites come from major_favorites, not fav. The role-number clash
  (legacy 86 «حسابدار» vs target 86 «کارشناس فروش») is recorded.
- §10.8 adds the tool's usage, safety rules, test result, and what it
  deliberately does not migrate.
- §11 and §5.5 are brought up to date. The rejected count is 7, not 14.

migration-precheck-3.sql: two bugs that would have failed on the server
are fixed: the reserved word `both` and a collation clash between
cities.title and customers.new_city.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:35:19 +03:30
s.esmaeiliandClaude Opus 5.5 be5ca829d2 feat(import): henza → gold-bank data import tool
apps/api/scripts/legacy-import/ reads the legacy MySQL (read only) and
merges into the target Postgres. Every rule is from
docs/DATA-MIGRATION-MAP.md, and each step cites its section. It covers
cities, roles and permissions, branches, admin users, sales experts,
workshops, customers, brands, categories, products, variants, product
images, orders, the ledger, favourites, custom orders and a
reconciliation report.

Key rules:
- Debt is rebuilt from legacy's own formula (updateRemain): delivered
  orders minus confirmed type 1-4 documents. Type 9 «سفارش» documents
  mirror orders and are skipped, or every order's debt would count twice.
  A balance the records cannot explain is carried as one ADJUSTMENT entry.
  In the 2026-10-04 dump that is only customer #125 (owner ruling).
- Merge, not import: rows already in the target are matched by mobile or
  product code, and only their empty fields are filled. Never written:
  accounting* columns, ProductVariant.imageId, Accounting* tables. No
  ProductImage is ever deleted.
- Roles map by title, not number. Legacy 86 is «حسابدار» but target 86
  is «کارشناس فروش». An expert sharing a mobile with an admin never
  demotes the admin.
- Soft-deleted variants and products that live order lines still point
  at (473 of 610 lines) come over inactive, so order history stays whole.
- Owner rulings 2026-10-07: birthdays filtered (541 of 915), typed city
  kept plus exact-match cityId, the 7 legacy rejections get the sentinel
  reason, OTP-only accounts get OTP_ONLY_PASSWORD_HASH.
- Tehran wall-clock timestamps are converted with ICU rules, including
  Iran's pre-2022 DST. Jalali receipt dates are converted with the
  jalaali-js arithmetic (truncating division; verified on 8 known dates).

Safety: --target is required and never read from .env. Nothing is
written without --yes, and a target not named *import*/*staging* also
needs --confirm-db. The id map lives in its own schema, legacy_import,
so a re-run updates instead of duplicating. Writes bypass the API
services, so no SMS is sent and nothing is queued to accounting.

Tested against goldbank_import_test, a clone of the dev DB, in three
runs:
- 1,503/1,503 created customers match legacy gold and rial balances
  (±0.0001 g / ±1 rial)
- accounting data is byte-identical before and after
- 0 imported variants have an imageId
- runs 2 and 3 created zero duplicate rows
- 6,196 image files copied, 3 missing in the legacy storage

New devDependency: mysql2 ^3.24.5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:35:18 +03:30
s.esmaeiliandClaude Opus 5.5 7019cad932 refactor(auth): move the password-hash helpers into their own module
compareLocalHash, OTP_ONLY_PASSWORD_HASH and a new isPasswordHash now
live in password-hash.ts, which depends on bcryptjs only. The data-import
script needs them, and importing auth.service would load NestJS and the
ESM-only @nestjs/jwt.

auth.service re-exports the first two, so callers and tests are
unchanged. Its own bcrypt import, now unused, is dropped. No behaviour
change: 21/21 auth tests, 178/178 overall.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:35:02 +03:30
s.esmaeiliandClaude Opus 5.5 3ac9e92e9f feat(db): Customer.cityNameRaw for the city typed at legacy sign-up
Additive and nullable. Owner ruling 2026-10-07: keep the typed text
verbatim, and set cityId as well only on an exact match.

Legacy customers.new_city is free text: all 172 live values are names,
none are ids. The legacy city table has only 15 rows and 43 of the names
match no city, so dropping the text would lose where those customers are.
Legacy sign-up also forced city_id = 5 (Tehran) on everyone
(Major/CustomerController.php:21). 91 of the 92 customers with both
values carry that forced Tehran, so the typed name is the truth.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:35:01 +03:30
s.esmaeiliandClaude Opus 5.5 5bacade748 chore: keep the live henza data dump out of git
oapi.sql is the owner's full data dump of the legacy database: customers'
names, mobiles and addresses. It was untracked but not ignored, so a single
`git add .` would have committed it and a push would have sent it to the
remote. Only the structure-only oapi-struncture.sql belongs in the repo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:35:01 +03:30
s.esmaeiliandClaude Opus 5.5 4d3b15c3b9 docs: data migration map — precheck results, owner rulings, run status
DATA-MIGRATION-MAP.md now records:
- the precheck numbers from the live henza DB and the assumptions they
  overturned: verify steps are 1/2/3, not boolean; reagent_id points at
  customers, not users; cashes.remain/rial_remain are NULL on every row
- owner rulings: mobile as the match key, sales expert wins conflicts,
  merge rather than import, allow-list columns only, retail
  orders_orders deferred, fav and birth_date kept, token_mobile and
  marriage_date dropped
- run status of the prep migration and the REJECTED consumer audit
- a process hazard: on the dev machine, a file in prisma/migrations is
  applied by the watch-mode server on the next src/ edit
- two open owner questions about the live verify() flow

migration-precheck-3.sql: read-only (COUNT only). Checks where new_city
should land and whether birth_date holds Jalali years.

PROGRESS.md: workstream row pointing here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:37:34 +03:30
s.esmaeiliandClaude Opus 5.5 884413513d feat(auth): refuse non-bcrypt password hashes before calling bcrypt
compareLocalHash() returns false for any passwordHash that does not
start with `$2`: '' (accounts created without a password) and the new
OTP_ONLY_PASSWORD_HASH sentinel the data import writes for ~1,590
OTP-only legacy customers. It is used on both local-compare paths of
verifyPassword(), IAM off and IAM fallback.

bcryptjs 3 already returns false for these. The guard keeps the result
from depending on that: a future version or a native binding that
throws would turn a wrong password into a 500 (owner ruling 2026-10-07).

Behaviour-neutral for existing data. Real hashes start with $2a$, $2b$
or PHP's $2y$, and every row that fails the prefix already failed in
bcrypt. Verified live: wrong password 401, correct password 200.

Tests: refusal for both values with IAM off and on, bcrypt never
called for them, and legacy $2y$ hashes still verify. 21/21.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:37:26 +03:30
s.esmaeiliandClaude Opus 5.5 e020e6a633 fix(customers): handle REJECTED verification explicitly
verifyStepOne() sent REJECTED to `default`, which returns 2
("accepted"). The admin list unlocks the order/account switches when
step one is 2, so a rejected customer would have had them unlocked.
REJECTED now maps to 3, matching legacy, where a step-one reject also
set active = -1.

Both switches are now exhaustive with no `default`, so a future enum
value fails the build instead of falling through silently.

Both user apps' ProfileView label it «رد شده». Before this they showed
the raw English value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:37:19 +03:30
s.esmaeiliandClaude Opus 5.5 45633b5aa0 feat(db): additive prep migration for the henza data import
Purely additive; nothing dropped, renamed or retyped. Owner rulings of
2026-10-06/07, recorded in docs/DATA-MIGRATION-MAP.md.

- Customer.nationalCode is now optional. All 1,595 live legacy customers
  have none, and fabricating one is a data violation. Still @unique:
  NULLs do not collide in Postgres, so the import must write NULL, not ''.
- Customer.birthDate added, empty. Must not be backfilled until the
  Jalali-vs-Gregorian check (precheck-3) is in.
- VerificationStep.REJECTED plus verificationRejectionReason/RejectedAt.
  Legacy has a third "rejected" state that the enum could not represent.
- CustomerFavorite for legacy `fav` (251 rows), which had no home.

accounting-sync customerPayload() now takes `nationalCode: string | null`.
validNationalCode() already handled null, so Tahesab/Zarrin receive
exactly what they did before. Caught by tsc after the column change.

Not touched: accounting* columns, ProductVariant.imageId, the
Accounting* tables, SMS (PROTECTED.md §B, §G).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:37:12 +03:30
Montech 5223de313a fix some for iam 2026-10-07 16:45:36 +03:30
s.esmaeiliandClaude Opus 4.6 19d490e790 feat: accounting integration (D27), SMS relay driver (D26), rebuild user apps
- Add Tahesab/Zarrin accounting sync module with admin panel UI
- Add SMS relay driver as default provider, refactor SMS into driver pattern
- Update customer services, orders, products, and catalog queries
- Rebuild user-app and user-app-v2 dist assets
- Add Prisma migration for accounting tables
- Update docker-compose and env examples
- Update parity docs (DECISIONS, ISSUES, PROGRESS)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-06 16:48:36 +03:30
s.esmaeiliandClaude Opus 5.5 a72fa607a5 Add CartView build asset for user-app-v2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:19:15 +03:30
s.esmaeili e54511fa94 md 2026-10-04 20:16:30 +03:30
s.esmaeili 6ae48d417d md app 2026-10-04 20:11:09 +03:30
Montech dfc70b6757 fix some for docker 2 2026-10-04 17:25:00 +03:30
Montech f3928fe228 fix some for docker 2026-10-04 17:23:30 +03:30
Montech eecd91c9be fix docker 2026-10-04 17:20:04 +03:30
s.esmaeiliandClaude Opus 5.5 b5b3cd8bef build(user-apps): rebuild user-app and user-app-v2 dist
Production build of both customer apps for f441ca8 (appearance settings,
dark mode, reduced motion, product morph, filter sheet). The API,
admin-panel and shared-types builds are unchanged: none of their sources
changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 16:31:50 +03:30
s.esmaeili f441ca8289 feat(user-apps): appearance settings (dark mode + reduced motion), product morph, filter sheet
Applies to both user-app and user-app-v2. Logic and structure are shared;
each app keeps its own look.

Appearance settings
- New "تنظیمات ظاهری" page (/settings/appearance), opened from the
  dashboard and available without login. It offers a light/dark/system
  theme and a "fast & light" motion switch.
- appearanceStore saves the choice to localStorage. A small inline boot
  script in index.html applies it before first paint so there is no
  white flash. The "system" option follows OS changes live.
- New ToggleSwitch component.

Dark mode
- Theme LESS tokens now compile to CSS custom properties. Light/dark
  values come from a generated virtual:palette.css (themeConfig/
  appearance.ts, with colors.json + new colors.dark.json).
- New LESS alpha() plugin replaces fade() on theme tokens.
- New semantic tokens: page, onInk, heroSurface, hairlineStrong,
  photoMat, photoStage.
- The dark palette is layered (page < card < panel < border) so
  surfaces separate without shadows.
- Hard-coded colors were replaced by tokens according to their role.
- antd inputs/text get dark overrides; vant uses its own
  .van-theme-dark.
- Light mode looks the same as before.

Reduced motion
- With html.motion-reduced, transitions are capped at 120ms, keyframe
  loops stop, and backdrop blurs and permanent will-change are dropped.
- Spinners and the toast progress bar (which closes the toast) are
  exempt via .keep-motion.
- Carousel autoplay, the v1 Lottie and the product morph honor the
  setting.

Product detail
- Opening a product is now a container-transform morph from the tapped
  card (ProductMorph). It replaces the bottom Drawer and uses composited
  transform/opacity only.
- The detail view is seeded with the card's data, so it no longer
  opens blank and jumps.

Filters and sheets
- Filters are now a full-width bottom sheet. Selections stay a draft
  until applied, so the result list no longer re-renders while
  dragging the slider.
- DropDown collapse animates grid-template-rows instead of a capped
  max-height.
- v2 IslandSheet is restyled to the v2 design language (API unchanged).

Fixes
- filtersStore.reset() shallow-copied shared arrays, so clearing the
  filters never cleared the selected brands.
2026-10-04 16:18:56 +03:30
s.esmaeiliandClaude Sonnet 5 c05352d6fd refactor(ui): replace modal dialogs with toast/sheet components
Remove DialogVant/NotifyVant and add Toast and Sheet components with
useToast, useReveal, and usePageTransition composables. Rework
Navigation, RegisterView, and styles across both apps; rebuild dist assets.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-04 13:57:25 +03:30
mahmood 148b99c4a6 feat build 2026-09-25 19:12:39 +03:30
mahmood 36f239e02c Feat add iam auth 2026-09-25 19:04:48 +03:30
s.esmaeiliandClaude Opus 5 2de90c6b35 docs(delivery): add a step-by-step quick start and a full pre-release section
1. "0. Quick start — run these in order" at the top of DELIVERY.md: five steps
   from an empty machine to a verified running stack, copy-pasteable, with no
   decisions to make. Covers which env vars must have a value (and how to
   generate the two JWT secrets), `pnpm build:release` BEFORE
   `docker compose build`, what the boot log should say, which banner is
   expected and which means a step was missed, and how to run the end-to-end
   check. Every command in it was executed verbatim to confirm it works.

2. "Before the real release" replaces the old three-item list:
   - A. blocking config: real JWT_SECRET and a SEPARATE JWT_CUSTOMER_SECRET,
     remove OTP_MASTER_CODE, configure the SMS credentials
   - B. credentials the reference leaked and that must be rotated (SEC-12,
     SEC-06) — same live accounts, never copied into this repo
   - C. parity work not finished: admin (financial documents, users and
     permissions incl. the still-open PERM-01, reports) and apps (MON-08 first,
     then STUB-05 SMS recipients, STUB-04, STUB-03, analytics), pointing at
     ISSUES.md for the rest

Also removed the obsolete `version:` key from docker-compose.yml, which printed
a deprecation warning on every compose command and made the checklist's output
noisy. Config re-validated.

Verified after these changes: container bundle is byte-identical to the local
release build (sha256 match), and that container was re-tested end to end:
compose e2e 32/32, complete-profile 38/38, report 40/40, expert login 36/36.
Locally: jest 54/54, build 5/5, typecheck clean on all four apps.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 02:38:00 +03:30
s.esmaeiliandClaude Opus 5 24b7a4c395 fix(api): treat blank env vars as unset; verify the whole flow in Docker Compose
Found by running the delivery flow against the actual built container rather
than a local dev process.

DEPLOY-02 (Critical) — ADMIN LOGIN RETURNED 500 FOR EVERY USER IN DOCKER.
docker-compose.yml passes each optional variable as ${X:-}, so an unconfigured
one arrives as "" rather than undefined. The code read them with
`process.env.X ?? 'default'`, which only falls back on undefined, so the empty
string reached jsonwebtoken and it threw

  "expiresIn" should be a number of seconds or string representing a timespan

on every sign. Invisible to every existing suite, all of which run against a
local process where the variable is genuinely absent.

Added src/common/env.ts (envOr / envOrNull), which treat blank as unset, and
applied it to every auth, port, upload and SMS read. Three were not cosmetic:
a blank UPLOAD_DIR would have served static files from the FILESYSTEM ROOT, a
blank PORT would have bound a random ephemeral port (Number('') === 0), and a
blank JWT_SECRET would have become the verifying key. Recorded as D25, with a
standing rule to use these readers instead of `??`.

Pinned by src/common/env.spec.ts (9 tests), which reproduces the original throw
against the real jsonwebtoken and asserts the reader can no longer produce it.

DEPLOY-03 (process, documented) — the API image is `COPY release/ ./`, so it
ships a bundle pre-built on the developer machine. `docker compose build` will
happily produce an image from a STALE release/app.js and report success; at the
start of this run that bundle was four days old and the rebuilt container was
missing every route added since. DELIVERY.md's deploy section now leads with
`pnpm build:release`.

New: scripts/test-compose-e2e.cjs — the whole delivery flow against the built
container, over HTTP only, as a black box: a sales expert and an ordinary
customer both sign in with the master code, both place an order, both orders
appear in the admin grid and open there, and the three token boundaries still
hold in the deployed image. 32/32.

Verified: compose e2e 32/32 · jest 54/54 · build 5/5 ·
against the CONTAINER: complete-profile 38/38, report 40/40, expert login 36/36 ·
locally: apps smoke 43/43, admin smoke 43/43, B2B scan 34 ok/0 broken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 02:27:30 +03:30
s.esmaeiliandClaude Opus 5 3f549f1749 parity(apps+api): report + report/detail on the internal ledger [ref: henza_pwa views/report]
Ports "گزارش" end to end (D24), the last missing app page.

In the reference this feature computes nothing: Customer\TahesabController
proxies an external accounting system (hesab.henzagold.com and a bare IP at
31.7.68.147:3030) with TLS verification disabled, a bearer token hardcoded in
the source and a URL-embedded secret, keyed on Customer.mcode. The target has
no such system and no mcode, so per the owner's option الف this is built on our
own ledger with no external connection.

API — customer/me/report{,/detail,/balances,/:id}
- one row per CONFIRMED ledger entry with the running gold and rial balance
  after it, newest first as the reference displayed it
- only confirmed entries count, the same rule the cached
  Customer.goldDebtGrams/fiatDebtRial follow, so the statement's closing
  balance agrees with the figure shown everywhere else in the app
- fields the target has no source for (foreign currency, نام کارساخته,
  جنس فلز, the external system's internal ids) are null/zero and the UI OMITS
  the row rather than rendering a fabricated value (REP-01)

SEC-16, NOT ported: the reference's detail($id) took the factor code from the
URL and performed no ownership check at all — it never consults auth() — so any
signed-in customer could read any other customer's document by guessing a code.
Here the id is only a filter on top of a customerId scope from the token.
Tested in both directions.

Apps (both, template+script byte-identical, per-app styling only)
- ReportView with the reference's two tabs, its card rows and its «جزئیات»
  modal; ReportDocument for one document
- routes /report and /report/detail, the latter opening the second tab
- dashboard entry (v1 tile, v2 row), shown to every signed-in customer since
  the reference's hasMCode() gate has no target equivalent

Verified: build 5/5 · jest 45/45 · report 40/40 (new) ·
complete-profile 38/38 · apps smoke 43/43 · typecheck clean on all four apps.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 01:49:49 +03:30
s.esmaeiliandClaude Opus 5 5ad1083aee parity(apps+api): complete_profile in both apps [ref: henza_server Major/CustomerController::complete]
Ports "تکمیل پروفایل" end to end (D22).

API
- POST customer/me/complete-profile — three required document ids, the
  customer taken from the token, verification step set to the reference's
  LIVE value (approved outright; its `= 1` line is commented out at :83 in
  favour of `= 2` at :85). Both SMS messages byte-identical and non-blocking
  per D20.
- POST customer/me/documents — the reference let the app upload through the
  same file/create the admin used, inside its auth:api-customer group
  (routes/api.php:95); the target's copy is admin-guarded, so the page could
  not have uploaded at all. Reuses AdminUploadsService unchanged, so both
  paths share one size limit, MIME whitelist and naming scheme (UPL-01).
- PROFILE_ADMIN_MOBILES replaces the reference's four hardcoded personal
  numbers; blank means nobody is notified and the upload still succeeds.

Three reference behaviours deliberately NOT ported, all in ISSUES.md:
- SEC-14: `$customer->update($request->all())` mass-assigned the whole request
  body, letting the app write its own credit limit, making charge, gold debt,
  active flag or mobile. Replaced with a three-field whitelist.
- SEC-15: four real personal mobiles hardcoded in the source.
- APP-03: refresh()'s unterminated setInterval page reload.

Apps (both, template+script byte-identical, per-app styling only)
- CompleteProfileView + SingleImageUploader
- route /complete_profile, redirecting home once the documents are in, as the
  reference's guard did (router/index.ts:266)
- the entry point on the profile screen, shown only while they are missing
  (profile.vue:96-102)

Also (D23): JWT_CUSTOMER_SECRET now warns loudly at boot when unset. It falls
back to JWT_SECRET, which signs app and admin tokens with the same key and
leaves the `typ` check as the only barrier — and an expert's app token carries
their AdminUser id in `sub`. Warn rather than refuse to boot (D19, lowest
risk); DELIVERY.md lists it as the first pre-release step (REL-03).

Verified: build 5/5 · jest 45/45 · complete-profile 38/38 (new) ·
expert login 36/36 · apps smoke 43/43 · admin smoke 43/43 ·
B2B scan 34 ok/0 broken · typecheck clean on all four apps.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 01:42:07 +03:30
s.esmaeiliandClaude Opus 5 f71fd5bfe1 feat(apps): sales-expert screens in both apps + v2 admin home slider (D21, APP-02)
Both expert screens are built in user-app and user-app-v2, with byte-identical
template and script and a per-app style block only — verified by diffing
everything above <style>:

  ChildOrdersView  "سفارش مشتریان"  — legacy ChildOrders.vue. Keeps the
    reference's title, search, infinite paging, and per-card item count, total
    weight, customer name and the fixed «تحویل مسئول فروش» chip (the endpoint
    only returns that status, ApiMobileController:766).
  CustomerChoiceView "لیست مشتریان" — legacy customerChoice.vue. Keeps its
    title, per-search re-query and the reference's choice to LIST inactive
    customers (its active() scope is commented out at :187).

Reachable from inside the app, not just by URL: routes carry the reference's own
paths and names, the dashboard grows the two entries only for an expert (v1 as
tiles, v2 as its own rows), and a router guard sends an ordinary customer back
to their dashboard. None of that is relied on for safety — the server scopes
both endpoints on the AdminUser id in the token and answers 403 otherwise.

Not ported, both recorded in D21: the act-as api_token minting (:197-201) and
the child-order edit drawer, which recomputed an order's money from client-sent
values. The card opens a read-only summary instead.

APP-02 fixed: user-app-v2 gained getHomeSlider and now prefers admin-configured
slides, keeping its category-derived hero as the fallback and its own design.

Session stores stay byte-identical across the apps.
build 5/5, jest 45/45, expert login 36/36, apps smoke 43/43, typecheck clean.

[ref: henza_pwa/src/views/user/src/components/ChildOrders.vue, customerChoice.vue]
[ref: henza_pwa/src/router/index.ts:171-188]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 01:12:53 +03:30
s.esmaeiliandClaude Opus 5 69f4ba2b29 feat(api): sales-expert login to the customer apps (D21)
A sales expert can now sign into the apps with their AdminUser account, without
reversing D17 — the expert remains an AdminUser and is never turned back into a
Customer.

The app token carries a `typ` of 'customer' or 'sales-expert'; the strategy
resolves both to one principal whose `id` is always a Customer id, so every
route already written for a customer works unchanged for an expert. That is what
the reference gave them, where an agent was a customer with extra screens.

Each expert gets ONE linked trading Customer row (additive migration, dry-run
clean and rolled back before applying) created at first sign-in, seeded from the
two numbers the reference read off the agent's own row: SalesExpertProfile
.gramLevel -> goldCreditLimitGrams and AdminUser.wagePercent ->
makingChargeOffsetPercent. Orders the expert places land there, exactly as the
reference's single row did.

New expert-only routes scope on the AdminUser id from the token:
POST expert/child-orders (legacy /order/child, ApiMobileController:759, keeping
its sts=5 filter as DELIVERED_TO_AGENT), POST expert/my-customers (legacy
/my-child, :184) and GET expert/whoami so the apps can hide the entries.

Security: jwt.strategy now rejects any token carrying an app `typ`. An expert's
app token holds their AdminUser id in `sub`, and JWT_CUSTOMER_SECRET falls back
to JWT_SECRET when unset — in that configuration the token would otherwise have
validated as an admin with full panel permissions.

NOT ported: myChild minted an api_token per listed customer (:197-201), letting
an agent act AS that customer. Recorded in D21 rather than silently dropped.

Tests: new test-sales-expert-app-login.cjs 36/36 — expert sign-in, trading
account seeding, own-customers-only (with both negatives), child orders, the
expert ordering for themselves, an ordinary customer refused both screens with
their own app unaffected, and all four token boundaries including immediate
lockout on deactivation. jest 45/45, admin smoke 43/43, apps smoke 43/43,
B2B scan 34/34, B2B writes 25/25, auth-smoke 32/32.

[ref: henza_server/app/Http/Controllers/Api/ApiMobileController.php:184-195,534-535,759-781]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 01:05:36 +03:30
s.esmaeiliandClaude Opus 5 53436ec855 chore(release): prepare release/delivery + fix docker env passthrough
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>
2026-09-23 00:30:09 +03:30
s.esmaeiliandClaude Opus 5 6d0f5c5359 test(api): B2B admin scan + write-path smoke, both green
scan-admin-b2b.cjs (34 ok, 0 broken) hits every read endpoint the wholesale desk
uses daily. smoke-admin-b2b-writes.cjs (25/25) covers the money-touching writes:
admin order placement and a cash payment document, asserting the ledger and the
customer balance rather than the HTTP status.

Verified: the making charge computes exactly, a document is created unconfirmed,
confirming moves the gold debt, and confirming again toggles back and restores
the balance to the byte.

Retail/storefront surfaces were deliberately not scanned (D13).

No B2B breakage found. Three first-draft failures were test bugs, corrected
against the real callers: cities/list -> admin/cities/list, by-ids takes
products_id, and document type 4 is cash (2 is a transfer).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 00:25:53 +03:30
s.esmaeiliandClaude Opus 5 8b7da90f51 test(apps): customer-flow smoke test for both apps + 4-digit master-code guard
smoke-apps.cjs walks login (master OTP), catalogue, cart cap, order placement
and order history, with the empty-order and unauthenticated negatives: 43/43.
The cart rules are executed from BOTH apps' own source so the result cannot be
carried by v1 alone.

No behavioural divergence found: all 9 stores are byte-identical, and the router,
API client and composables match except v1's protected variant-image picker.

APP-01: both apps' OTP box is exactly 4 inputs and only submits at 4, so a longer
OTP_MASTER_CODE cannot be typed into them. The login UI was left alone; main.ts
now warns at boot when the code is not 4 digits and .env.example states the rule.

APP-02 logged, not fixed: user-app-v2 has no getHomeSlider, so admin-configured
home slides show in v1 only. Changing v2's hero during a delivery is not worth
the risk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 00:22:50 +03:30
s.esmaeiliandClaude Opus 5 0983c42788 fix(admin-panel): retail order status editing posted a body with no orderId
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>
2026-09-22 21:30:58 +03:30
s.esmaeiliandClaude Opus 5 676ec3ed72 test(api): live D20 verification + document every env var
Two live scripts against a running API, covering both halves of the master-OTP
contract:
  * with OTP_MASTER_CODE set (8/8): signs in, the JWT works on customer/me,
    Persian digits accepted, and the RULE E1 negatives — near-miss code,
    unknown mobile, inactive customer — all still rejected.
  * without it (6/6): the same code returns 401, no boot banner, a real code
    still logs in, and replaying it fails.

Also confirmed from the boot log that with no SMS credentials the server starts
normally and declines to send instead of erroring (D20).

.env.example gained the auth, SMS and upload variables, names only, no values,
with OTP_MASTER_CODE marked delivery-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 21:28:17 +03:30
s.esmaeiliandClaude Opus 5 460cf276b6 parity(api,apps): delivery-mode gaps — master OTP, non-blocking SMS, faToEn, v2 cart cap
D20: OTP_MASTER_CODE login bypass read only from the environment, disabled when
unset, audited on every use, with a boot banner while armed. SMS no longer
aborts the boot in production — it warns and declines silently, so no message
can stop an operation.

D19: port Utility::faTOen onto admin-entered mobiles (before the uniqueness
lookup, which could previously be bypassed with Persian digits), and the
create-only welcome SMS, byte-identical to the reference and fire-and-forget.

user-app-v2 was missing the cart stock ceiling entirely: capQty/maxQty, the
un-orderable guard and the qty clamp are ported, leaving v2's own design intact.
The two cart stores are now byte-identical. isVariantOrderable and the
orderable-first default selection ported too.

jest 45/45; typecheck clean on api, admin-panel, user-app, user-app-v2.

[ref: henza_server/app/Http/Controllers/Adm/AdminController.php:333,348,364,378-387]
[ref: henza_server/app/Models/Utility.php:26]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 18:35:33 +03:30
s.esmaeiliandClaude Opus 5 2194b3d4d9 parity(admin): D17 — SalesExpertProfile, AdminUser name parts, agent grid fields
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>
2026-09-22 17:50:47 +03:30
s.esmaeiliandClaude Opus 5 e3af31948e parity(docs): D16 — stop PRICE-01 as storefront, reorder Phase 1, verify SEC-02
Owner ruling: computing and displaying price / price_with_discount is retail
storefront work, so it falls under D13 and is marked out of scope. The B2B
app works in making charge only.

variant-price.ts and its 11 hand-checked tests are kept and still run, just
unused. Nothing needed reverting: computeVariantPrice was never called from
outside its own file and spec, verified by grep over apps/api/src and
apps/admin-panel/src, so no price had been wired into any screen. PRICE-01
is re-marked in ISSUES.md rather than deleted so the evidence survives if
the storefront is ever taken up.

Making-charge work stays fully in scope: percentage_pay,
makingChargeOffsetPercent, and wage display on variants and order lines.

New Phase 1 order: Persons -> products/categories/variants/order detail ->
financial documents -> users and permissions -> admin SMS -> promotions,
notifications, cities, branches, workshops -> reports -> gate. Per section:
implement the gaps, then field-by-field test that section, and show a gap
summary before fixing.

SEC-02 re-verified live rather than quoted: 9/9 assertions, plus 32/32 in
auth-smoke.sh. POST /orders is 401 without a token; a body customerId of
"customer-2" sent with a customer-dev-test token still lands the order on
customer-dev-test and is warn-logged; a foreign order id returns 404 rather
than 403 so ids stay unenumerable; the admin read rejects a customer token.
Client-supplied wagePercent/discountPercent/flatRialCharge remain as MON-08,
already scheduled as Phase 2 item #1.

Also records why the dev OTP log briefly looked broken: a stale API process
still held port 3077, so the new instance died with EADDRINUSE and the
tailed log belonged to the old process. Not a regression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 16:43:11 +03:30
s.esmaeiliandClaude Opus 5 64e76a8616 parity(admin): start the Phase 1 matrix — rid mapping, price and order sections
Answers D1's 172-vs-174 question with real counts: the catalogues hold 176
(reference) and 174 (target) unique keys, and the ENTIRE difference is the
dashboard. The reference has `dashboard` + `dashboard.omde` + `dashboard.tak`;
the target replaces all three with `dashboard.view`, because the admin
Dashboard is a PROTECTED zone that merged the two reference dashboards.
176 - 3 + 1 = 174. All 173 other keys match exactly, including the same 5
keys that legitimately appear twice in the tree.

Route-guard rids are 57 vs 57, differing only by that dashboard
consolidation plus one target-only route reusing an existing catalogue rid.
All 67 reachable reference admin routes exist in the target at the same
paths; 2 target-only routes are additive.

Three findings logged:

PRICE-01 (critical) — no product price is computed or shown anywhere in the
admin panel. The reference appends price/price_with_discount to every
variant and renders it in three places in its product list; the target's
variantRow() returns neither, nothing fetches a gold rate for the admin
list, and computeVariantPrice() — ported and unit-tested earlier today — is
called from nowhere. This is fix #1.

PERM-01 (medium) — dashboard.omde and dashboard.tak are seeded into the
permission table but absent from the permission UI, so they are grantable
yet unmanageable; the DB holds 178 distinct rids against the UI's 174. The
fix deletes permission rows, so it waits for the owner's approval.

PRICE-02 (info) — rial_pay belongs only to the superseded commented-out
formula; the target correctly omits it. Recorded so it is not "restored".

Contrary to the Phase 0 expectation, the order module is in good shape: the
gram_level credit limit matches the reference exactly including the strict
'>' comparison, and debt recognition fires at the same status.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 16:17:31 +03:30
s.esmaeiliandClaude Opus 5 654b98520d parity(api): declare _goldbank_migrations as an ignored model; drift now zero
Prisma migrate diff proposed DROP TABLE "_goldbank_migrations" because the
table exists in the database but was not declared in the schema. Declaring
it with @@ignore removes the proposal without exposing it to Prisma Client,
and the table and all its rows are kept.

Correcting the earlier DEAD-07 note: this is not a leftover from the
pre-Prisma port. It is the tracking table of the application's own
migration runner (src/prisma/migrate.ts:5), which exists because the Docker
deploy image is copy-only and ships no Prisma CLI, so migrate deploy cannot
run in the container (migrate.ts:7-13). The project therefore keeps two
ledgers deliberately: _prisma_migrations for the CLI in development, and
_goldbank_migrations for the runtime runner in the container. That runner
already filtered out any DROP of its own table (migrate.ts:190-196); the
schema declaration removes the cause rather than relying on that guard.

The practical consequence, recorded in DECISIONS.md D15: a new migration
must be safe under both paths, so its SQL stays idempotent.

The ledger grew 12 -> 14 rows while testing, with nothing deleted: starting
the API ran prepareDatabase(), which appended the two migrations applied
earlier today. Its $$-aware statement splitter handled the PL/pgSQL trigger
body correctly — trigger and function are both present and the 9 trigger
assertions pass against the live database.

Also records the UTF-8 testing rule: Persian test input must come from a
file, never a shell argument, because the Windows shell replaces Persian
characters with '?' and a test can then pass while storing '????'.

Verified: migrate diff -> "This is an empty migration" (zero drift);
migrate status -> up to date; tsc clean; 32 jest; 9 trigger assertions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 08:25:03 +03:30
s.esmaeiliandClaude Opus 5 8133a2bdf4 parity(api,admin): split customer name, add making-charge offset, wire SMS
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>
2026-09-22 08:08:50 +03:30
s.esmaeiliandClaude Opus 5 7b598e9e8e parity(api): port the reference price formula with tests; SMS + MON-08 reports
Adds computeVariantPrice(), a verbatim port of the live formula at
henza_server/app/Models/ProductVariant.php:183-190, with the per-customer
making-charge offset as an explicit input defaulting to 0. The three
superseded formulas commented out above it apply VAT to the whole price
and are deliberately not ported (D4).

11 unit tests assert against hand calculations derived from the PHP source
rather than from the implementation, covering: with and without the
offset, VAT on the non-metal portion only, floor-to-nearest-1000, the
extraPercent default of 7, and the equivalence between the app folding the
offset into percent_pay and the server deriving it. 18 tests pass overall.

Also adds the D0 name-split review report script (read-only) and two
investigations the owner asked for:

- SMS: the reference uses the Trez panel (smspanel.trez.ir, Sms.php:69,97)
  with production credentials committed to git (SEC-12 — rotate them). All
  12 message templates catalogued; env var names proposed; nothing
  implemented pending approval.
- MON-08: henza_pwa computes percent_pay, discount and rial_pay rather
  than asking the user (cart.vue:170-179, Mixins/product/index.ts:69-75),
  so Phase 2 item #1 is to derive them server-side and ignore the client's
  values. rash_weight stays read from the DB as a recorded deviation.

Records the owner's two new standing rules: migrations only through the
standard Prisma workflow, and every data deletion logged in CHANGELOG.

[ref: henza_server/app/Models/{ProductVariant,Sms}.php, henza_pwa/src/views/cart.vue]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 07:40:28 +03:30
s.esmaeiliandClaude Opus 5 c71574b1d2 parity(docs): record D13 — retail storefront preserved but out of scope
The owner ruled that the reference also ran a retail storefront (B2C
single-customer shop) and that the wallet, cashback and
ConfigStatic['percent'] belong to it. These are preserved exactly as they
are: nothing deleted or disabled on either side, no parity work, listed in
the matrices with status "preserved — out of current scope" and no gap or
work row. Shared surfaces (product, variant, price, order, customer) need
the owner's approval before a change that could reach the storefront.

Classifying the endpoints for that split surfaced a counting error in the
Phase 0 inventory: routes/admin.php has 151 live explicit routes, not the
117 recorded, so the reference total is 412 and not 369 (logged DOC-01).
get-analytics-tak reads SingleCustomerOrders, so it is a storefront route
despite its neutral name (logged DOC-02).

Of the 412 reference endpoints, 109 are retail storefront, 15 are web.php
dev/ops closures, and 288 remain in scope (218 admin + 70 B2B app).

[ref: henza_server/routes/{admin,api,customer,site,web}.php]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 07:23:48 +03:30
s.esmaeiliandClaude Opus 5 024e3dd045 parity(api,apps): close SEC-02 - orders take the customer from the token
The user declined to defer this to Phase 2, correctly: a public POST /orders
with customerId in the body let anyone place an order on any customer's
account, and an order lands on that customer's gold debt and ledger.

POST /orders is now behind CustomerAuth. OrdersService.createOrder takes the
customer id as a separate argument that the controller fills from the token;
the body's customerId is ignored and a mismatch is logged. The field stays
optional in the DTO and in shared-types only so both apps keep validating
while they still send it -- they drop it in Phase 2. The admin builder is
unaffected: it legitimately places orders for a chosen customer and passes
that id explicitly, behind the admin guard.

GET /orders/:id is scoped to the owner. A foreign or nonexistent id both
return 404, never 403, so ids stay unenumerable. Operators read through the
new GET /orders/admin/:id behind rid order.list, declared before the :id
route because Nest matches in declaration order.

Both apps' CartView now gates on session.isLoggedIn rather than a remembered
customerId, which is no longer a credential.

OTP audit (requested): no master code, no bypass, no hardcoded code anywhere;
one code path issues a token and only after bcrypt.compare. No SMS provider is
wired up -- in development the code is logged once, guarded by
NODE_ENV !== 'production'.

Fixed while auditing (SEC-11): deliver() persisted the real message text, code
included, into SmsMessage.body, which the admin SMS grid lists and full-text
searches. Any operator with the sms rid could read live codes, defeating the
hashing of codeHash. The stored row is now a redacted audit line.

Also logged MON-08: wagePercent/discountPercent/flatRialCharge are still taken
from the client on order creation. The reference is worse -- it takes
rash_weight from the client too -- so this is already a partial improvement;
logged rather than changed, per D4.

Tests: auth-smoke.sh now 53 passed, 0 failed, including a spoofed body
customerId being ignored in favour of the token. Typecheck clean on api,
user-app, user-app-v2 and shared-types.

[ref: henza_server ApiMobileController.php:533,536-545,615-632]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 07:09:33 +03:30
s.esmaeiliandClaude Opus 5 4efe543d62 parity(api,apps): security hotfix stage 2 - real customer sessions, close IDOR
Stage 1 narrowed GET /customers but did not close the leak: the response still
carried every customer id, and /customers/:id, /:id/orders and
/:id/ledger-entries were still public, so the full customer book was still two
requests away. The user caught this. Stage 2 deletes the id-bearing surface.

Customer JWT, separate from the admin one (T1):
  - new src/modules/customer-auth/ with its own passport strategy,
    CustomerJwtGuard, @CustomerAuth() and @CurrentCustomer()
  - signed with JWT_CUSTOMER_SECRET, and every token carries typ:'customer'
    which the strategy verifies; both checks are present deliberately
  - customer state re-read per request, so deactivating takes effect at once
  - mirrors the reference's own split: auth:api vs auth('api-customer')

Real OTP (D6), porting ApiMobileController::userAuth/userVerfiy (:39-64):
  - new CustomerOtp table, additive migration, nothing existing touched
  - parity behaviour: rand(1000,9999), active-customer filter, faTOen digit
    normalisation on both number and code, rejection on mismatch
  - approved improvements over the reference: bcrypt-hashed code, 2-minute
    expiry, 5 attempts then lockout, single use, send throttle (60s per
    number, 5/number and 20/IP per 10 min), and no number enumeration

Routes removed, replaced by token-derived equivalents:
  GET customers, customers/:id, /:id/orders, /:id/ledger-entries
    -> GET customer/me, customer/me/orders, customer/me/ledger-entries
No route accepts a customer id any more, so the IDOR is closed structurally
rather than by a check. The admin panel was unaffected (admin/customers-omde/*).

Both apps migrated in the same commit per the v1/v2 parity rule: token-based
session store, Bearer header, 401 -> clear session and route to login, real
two-step OTP login. Each app keeps its own markup and styling; only logic moved.

Tests: auth-smoke.sh extended with the stage-2 assertions, 46 passed 0 failed
against a live server. jest installed dev-only (user-approved) so pnpm test
works for the first time: 7 passed covering faToEn, which moved to its own
digits.ts to be testable without booting Nest. Typecheck clean on api,
user-app and user-app-v2; both apps build.

[ref: henza_server ApiMobileController.php:39-78]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 01:20:00 +03:30
s.esmaeiliandClaude Opus 5 fdf833565f parity(api,apps): security hotfix stage 1 - close unauthenticated write paths
Authorised by rulings D7 and T1-T3 in docs/parity/DECISIONS.md. This is the
CLAUDE.md section 3 carve-out: matching the reference here would keep a money
write path open to anonymous callers.

Closed, each behind the rid the legacy vocabulary already had:
  GET   accounting/customers/:id/balance    -> wallet.list
  POST  accounting/customers/:id/reconcile  -> wallet.charge
  POST  accounting/repayments/gold          -> document.create
  POST  accounting/repayments/rial          -> document.create
  POST  accounting/entries/:id/confirm      -> document.list.confirm
  PATCH orders/:id/status                   -> order.list.sts.submit

Four of the accounting routes WRITE to the ledger. The order-status route was
worse in kind: its DTO takes a client-supplied actorAdminUserId, so an
anonymous caller could move any order and pin it on any admin. It had no
caller - the admin panel posts to admin/orders/status.

GET /customers kept its public status (both apps' login still matches the
phone number in the browser) but now selects only id/mobile/isActive/
isArchived. It previously returned every active customer's full name, national
code, both debts, credit limit and the URLs of their identity-document scans.
CustomerSummary narrowed in BOTH user apps in the same commit, per the
v1/v2 parity rule.

Not changed: /uploads stays public per ruling T2. The filename check requested
alongside it passed unchanged - md5(time + name + uuid), no directory listing.

Tests: apps/api/scripts/auth-smoke.sh asserts 401 without a token, 403 with a
low-privilege token (ut 88, 5 unrelated rids) and pass-through for a super
admin on every closed route, plus that the public routes still answer 200 and
that GET /customers exposes only the four login fields. 23 passed, 0 failed
against a live server. Typecheck clean on api, user-app and user-app-v2.

[ref: henza_server/routes/admin.php:12,272; ApiMobileController.php:39-78]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 00:43:11 +03:30
s.esmaeiliandClaude Opus 5 004a3945c2 parity(docs): record user decisions D0-D12 and urgent investigation results
Phase 0 gate passed. All rulings recorded in docs/parity/DECISIONS.md and
applied to PROTECTED.md, PROGRESS.md and ISSUES.md. No source code changed.

Urgent investigations requested by the user, all evidence-cited:

- percent trace: the target's implementation is WRONG. `percent` is the
  customer's OWN column in the reference (customer_add.php:32), a per-customer
  making-charge offset used to shift catalogue filter bounds
  (ApiMobileController.php:391-396,76-77). The target reads the referring
  agent's wagePercent instead (admin-customers.service.ts:245) and never
  writes the field at all. It is NOT part of the price formula -- that extra
  percent comes from a global ConfigStatic row (ProductVariant.php:50,183).
  Two unrelated `percent` keys exist and must not be confused with it.

- @Public audit: 15 routes across 7 controllers, grouped by risk. Group A is
  5 ledger routes (4 writes) that contradict decision D1; group B is 3 order
  routes incl. a public status change accepting a client-supplied
  actorAdminUserId; group C is 8 reads, of which only the catalogue, pricing,
  slider and uploads are legitimately public.

- Login leak: it is in the TARGET. GET /customers is public and returns every
  active customer with national ID numbers, debts and ID-document URLs.
  passwordHash is correctly excluded. Not possible in the reference.

Also logged 5 customer-module defects found in the deep check the user asked
for (CUST-01..05), including the fullName collapse the user reported.

Skill updated per D12: Step C gains rule C0 (field-by-field matrix, never
page-by-page) and Step E gains rule E0 (mandatory 4-step round-trip test).

[ref: henza_server, henza_admin]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 23:26:35 +03:30
s.esmaeiliandClaude Opus 5 71192ba2ba parity(docs): Phase 0 inventory, mapping and draft PROTECTED list
Read-only inventory of all 7 projects (3 reference + 4 target) produced by
parallel subagents, every claim cited path:line. No source code changed.

- 00-INVENTORY.md  consolidated mapping: stack, backend, admin, user apps
- PROTECTED.md     draft protected list (awaiting user confirmation)
- ISSUES.md        26 logged issues (security, money/stock, validation, dead code)
- CHANGELOG.md     change log scaffold
- PROGRESS.md      resumable state + Q1-Q5 blocking Phase 1
- _inv-*.md        7 raw per-project inventories (8111 lines)

Key finding: the reference is a gold-jewellery B2B/B2C e-commerce system, not a
gold bank. Buy/sell, deposit/withdraw, transfer, KYC levels, payment callbacks,
cron and websockets do not exist in it, so they cannot be parity gaps.

[ref: henza_server, henza_admin, henza_pwa]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 18:09:46 +03:30
s.esmaeili a743f6f93c update admin 2026-09-21 17:37:26 +03:30
s.esmaeili ef1d4638cc admin user and ui panel 2026-09-21 08:32:31 +03:30
Montech 79226c9f13 fix some for api download pic 2026-09-19 15:44:00 +03:30
s.esmaeili 7227e9c61b build 2026-09-19 13:12:58 +03:30
s.esmaeili cb07e7c969 dashbird beter ui 2026-09-19 13:08:27 +03:30
s.esmaeili 7f20c6333e cheng dashbord data and ui 2026-09-19 12:42:17 +03:30
918 changed files with 161994 additions and 14442 deletions
+86
View File
@@ -0,0 +1,86 @@
---
description: اتصال ورود ادمین به سامانه هویت متمرکز (IAM) — مرزها و قواعدی که نباید شکسته شوند
globs:
alwaysApply: true
---
# اتصال به سامانه هویت متمرکز (Central IAM)
مستندها:
- `docs/iam-connection.md` — **نحوه ارتباط**: سه کانال داده و رفتار در خطا
- `docs/iam-sso.md` — پیکربندی، ورود مشترک با همکار، ابطال نشست
- مرجع تصویری اندپوینت‌های IAM: `<IAM>/docs` (Scalar)
## اصل غیرقابل‌مذاکره
این قابلیت **افزایشی** است. با `IAM_ENABLED=false` (پیش‌فرض) سامانه باید دقیقاً
مثل قبل رفتار کند: یک `bcrypt.compare` محلی و هیچ درخواست شبکه‌ای.
- فرم ورود عوض نمی‌شود. پاسخ ورود همان `api_token` قبلی است، به‌اضافهٔ
`iamBootstrapUrl` فقط وقتی IAM بلیت مرورگر داده باشد.
- وقتی IAM روشن است و `IAM_ADMIN_REDIRECT_URI` (یا یکی از
`IAM_ADMIN_REDIRECT_URIS`) با آدرس خودِ پنل یکی است، بار اول بدون توکن یک
پرش `prompt=none` به `/auth/iam/callback` می‌رود. اپ کاربر همین کار را با
`IAM_REDIRECT_URI` و `IAM_REDIRECT_URIS` می‌کند. آدرس ناشناس جایگزین آدرس
ثبت‌شده نمی‌شود و پاسخ `enabled: false` می‌ماند.
- IAM خاموش، یا redirect ادمین خالی، یعنی این پرش شروع نمی‌شود و خروج آدرس
مرکزی برنمی‌گرداند.
## نقشه فایل‌ها
| مسیر | نقش |
| --- | --- |
| `apps/api/src/modules/iam/iam.service.ts` | ارتباط با IAM و نگاشت خطاها |
| `apps/api/src/modules/iam/iam.module.ts` | ماژول Global (بی‌اثر تا زمان روشن شدن) |
| `apps/api/src/modules/auth/auth.service.ts` | متد `verifyPassword` — تنها نقطه اتصال ورود |
| `apps/api/src/modules/sessions/admin-session.service.ts` | نشست‌های قابل ابطال (`jti`) |
| `apps/api/src/modules/iam/iam-logout.service.ts` | تأیید توکن خروج سراسری (JWKS، RS256) |
| `apps/api/src/modules/iam/iam.controller.ts` | `POST /api/iam/backchannel-logout` |
## قواعد سخت
1. **هر نتیجه مبهم باید به `bcrypt` محلی برگردد.** سرویس در دسترس نیست، کاربر
آنجا نیست، یا ۴۲۹ گرفتیم ⇒ رمز محلی بررسی شود. فقط `exclusive` اجازه رد
کردن دارد. خرابی IAM هرگز نباید ادمین‌ها را بیرون بیندازد.
2. **حساب قفل‌شده، «نتیجه قطعی» است نه خطا.** `account_locked` نباید به
`bcrypt` محلی برگردد وگرنه قفلِ IAM بی‌اثر می‌شود.
3. **`client_ip` همیشه فرستاده می‌شود.** بدون آن، محدودیت نرخِ IAM روی IP همین
سرور اعمال می‌شود و بعد از چند ورود کل پنل ادمین ۴۲۹ می‌گیرد. اگر API پشت
پراکسی رفت، `trust proxy` را تنظیم کن وگرنه `@Ip()` همیشه IP پراکسی است.
4. **هیچ جدول موجودی تغییر نمی‌کند.** نگاشت کاربر با همان `mobile` انجام می‌شود و
`iamSub`/`iamSessionId` روی جدول **جدید** `AdminSession` می‌نشینند، نه روی
`AdminUser`. اگر روزی چیزی از IAM لازم شد، همین الگو را نگه دار.
5. **ابطال نشست دو شرط دارد و هر دو لازم است:** توکن باید `jti` داشته باشد و ردیف
`AdminSession` باید موجود و باطل‌نشده باشد. با خاموش بودن ردیابی
(`AUTH_SESSION_TRACKING=false` یا IAM خاموش) نه `jti` صادر می‌شود، نه کوئری
اضافه‌ای در مسیر احراز هویت اجرا می‌شود — یعنی دقیقاً رفتار و هزینه قبلی.
توکن بدون `jti` هرگز نباید رد شود، وگرنه روشن کردن این کلید همه را بیرون می‌اندازد.
6. **اندپوینت خروج سراسری بدون احراز هویت است و باید بماند.** اعتبارش از امضای
`logout_token` می‌آید: `iss`، `aud`، `events`، نبودن `nonce` و `exp` همه بررسی
می‌شوند. هیچ‌یک از این بررسی‌ها را برای «راحتی تست» حذف نکن. پاسخ همیشه ۲۰۰ است
تا IdP بی‌نهایت تلاش مجدد نکند؛ دلیل رد شدن فقط لاگ می‌شود.
7. **فرم ورود این پنل مرحله «ورود کد» ندارد**، پس حساب دو مرحله‌ای در IAM از این
مسیر قابل تکمیل نیست و با پیام روشن رد می‌شود. تا وقتی آن مرحله ساخته نشده،
تلاش برای تکمیل 2FA در بک‌اند بی‌معناست.
8. **ورود اپ کاربر رمز ندارد**، پس از `verifyCredentials` استفاده نمی‌شود؛ بعد از
بررسی موفق کد، `IamService.attestOtpLogin` فراخوانی می‌شود. قاعده خطا: «کاربر
آنجا نیست» و «سرویس در دسترس نیست» ⇒ ورود ادامه می‌یابد؛ «قفل/غیرفعال/دو
مرحله‌ای» ⇒ ورود رد می‌شود. کد اضطراری تحویل (`OTP_MASTER_CODE`) هرگز به IAM
اعلام نمی‌شود.
9. پیکربندی ناقص = خاموش. `IamService.enabled` وجود `issuer`, `client_id` و
`client_secret` را هم شرط می‌کند (با `envOr` که مقدار خالی را unset می‌شمارد).
## قبل از تحویل
```bash
cd apps/api && npx jest src/modules/iam src/modules/auth src/modules/sessions
# و در استقرار: prisma migrate deploy (جدول AdminSession)
```
+131
View File
@@ -23,3 +23,134 @@ USER_APP_V2_PORT=5182
# API Configuration (kept for any tooling that still reads bare PORT)
PORT=3000
# ---------------------------------------------------------------------------
# Authentication
# ---------------------------------------------------------------------------
# Admin JWT signing secret. REQUIRED in any real deployment — without it the
# API falls back to a well-known development string.
JWT_SECRET=
# Admin token lifetime (default 12h).
JWT_EXPIRES_IN=
# Customer JWT signing secret. Deliberately SEPARATE from JWT_SECRET so an
# admin token can never validate as a customer token (T1).
JWT_CUSTOMER_SECRET=
# Customer token lifetime (default 30d).
JWT_CUSTOMER_EXPIRES_IN=
# ---------------------------------------------------------------------------
# Accounting software integration (Tahesab / Zarrin, D27). Everything else —
# which software, address, token, mappings — is set in the admin panel
# («پیکر بندی ← اتصال حسابداری»), and the integration is off until then.
# ---------------------------------------------------------------------------
# Encrypts the accounting token at rest. Set a long random value. Blank falls
# back to a key derived from JWT_SECRET (with a boot warning); changing that
# key later means re-entering the token in the admin panel.
ACCOUNTING_SECRET_KEY=
# "off" keeps this API instance out of the background sync (only needed when
# several instances run and you want exactly one doing it; a lease already
# stops two from running the same job).
ACCOUNTING_WORKER=
# ---------------------------------------------------------------------------
# Central IAM — delegated admin login (docs/iam-sso.md)
# ---------------------------------------------------------------------------
# OFF by default. When off, nothing changes: admin login stays a local bcrypt
# check and no request ever leaves this server.
#
# When on, the admin's phone + password are verified against the central
# identity service instead. There is NO UI change: no SSO button, no redirect.
# If the identity service is unreachable or does not know the account, the
# local password check still runs (see IAM_LOGIN_DELEGATION).
IAM_ENABLED=false
IAM_ISSUER=
# The client must be registered there as confidential AND trusted.
IAM_CLIENT_ID=
IAM_CLIENT_SECRET=
# One of the two; tenant_id wins when both are set.
IAM_TENANT_ID=
IAM_TENANT_SLUG=
# off | fallback (default, safest) | exclusive (local password no longer accepted)
IAM_LOGIN_DELEGATION=fallback
IAM_HTTP_TIMEOUT_MS=8000
# صفحه callback اپ کاربر. باید در کلاینت IAM دقیقاً ثبت شود.
IAM_REDIRECT_URI=
# callbackهای اضافه (دولوپ و دامنه دیگر) با کاما. آدرس ناشناس جایگزین نمی‌شود.
IAM_REDIRECT_URIS=
# بعد از ورود محلی، مرورگر برای کوکی نشست مشترک به اینجا برمی‌گردد.
IAM_BROWSER_RETURN_URL=
# خروج سراسری فقط اگر این آدرس در post_logout کلاینت ثبت شده باشد برمی‌گردد.
IAM_POST_LOGOUT_REDIRECT_URI=
# مقصدهای مجاز خروج اپ کاربر، با کاما. باید عیناً در post_logout کلاینت باشند.
IAM_ALLOWED_RETURN_URLS=
# پنل ادمین. خالی یعنی بررسی بی‌صدای ورود در پنل انجام نمی‌شود.
IAM_ADMIN_REDIRECT_URI=
IAM_ADMIN_REDIRECT_URIS=
IAM_ADMIN_BROWSER_RETURN_URL=
IAM_ADMIN_POST_LOGOUT_REDIRECT_URI=
IAM_ADMIN_ALLOWED_RETURN_URLS=
# Revocable admin sessions (AdminSession table).
#
# An admin token is self-contained and lives for JWT_EXPIRES_IN (12h), so until
# this existed nothing could end a session early — not disabling the account, not
# a central sign-out. Every issued token now carries a `jti` recorded in the
# database, and a revoked row is refused on the next request.
#
# Unset = follows IAM_ENABLED (on with the IAM, so central revocation reaches
# here). `true` turns it on without the IAM; `false` opts out of the per-request
# lookup and central revocation then cannot end a session here.
AUTH_SESSION_TRACKING=
# To let the IAM end sessions here, register this path on the client there as its
# backchannel_logout_uri:
# https://<api-host>/api/iam/backchannel-logout
# ---------------------------------------------------------------------------
# ⚠ DELIVERY ONLY — REMOVE BEFORE THE REAL RELEASE (D20 / ISSUES REL-01)
# ---------------------------------------------------------------------------
# A fixed OTP that signs in ANY active customer, alongside the real code.
# MUST BE EXACTLY 4 DIGITS: the apps' login box has four single-character
# inputs and only submits at four (LoginView.vue:43). A longer code still works
# over the API but cannot be typed into either app.
# Leave it EMPTY or unset to disable it completely — that is the correct state
# for production. While it is set, the API prints a warning banner at boot.
OTP_MASTER_CODE=
# ---------------------------------------------------------------------------
# SMS gateway. The default "relay" driver works with everything blank (D26).
# A failed SMS never stops the server or an operation (D20).
# ---------------------------------------------------------------------------
# "relay" (default), "trez", or "none" to disable explicitly.
SMS_PROVIDER=
# relay — the owner's createsms gateway. All four are optional.
# URL and secret have built-in defaults.
SMS_RELAY_URL=
SMS_RELAY_SECRET=
# The "host" field. Blank sends this machine's first external IPv4 — inside
# Docker that is the container's address.
SMS_RELAY_HOST=
# "kave" (default, Kavenegar) or "default" to let the gateway pick the line.
SMS_RELAY_PLATFORM=
# trez — only used with SMS_PROVIDER=trez. Never commit real values.
# Override only for staging; defaults to the provider's live endpoint.
SMS_TREZ_URL=
SMS_TREZ_USERNAME=
SMS_TREZ_PASSWORD=
SMS_TREZ_SENDER_LINE=
# ---------------------------------------------------------------------------
# Uploads
# ---------------------------------------------------------------------------
# Absolute path for uploaded files; defaults to <api>/storage/uploads.
UPLOAD_DIR=
# ---------------------------------------------------------------------------
# Profile completion ("تکمیل پروفایل")
# ---------------------------------------------------------------------------
# Comma-separated admin mobiles told when a customer uploads their documents.
# The reference hardcoded four numbers in its source
# (henza_server Major/CustomerController::sendSmsToAdmin); they live here
# instead so no personal number is committed. Blank means nobody is notified —
# the upload and the customer's own SMS still go through (D20).
PROFILE_ADMIN_MOBILES=
+4
View File
@@ -15,3 +15,7 @@ apps/api/storage/uploads/*
# apps/api/release/app.js + apps/api/release/vendor/ (Prisma)
# apps/admin-panel/dist/
# apps/user-app/dist/
# Live henza data dump (customers' personal data). Never commit.
# Only the structure-only oapi-struncture.sql belongs in the repo.
/oapi.sql
+427
View File
@@ -0,0 +1,427 @@
# DELIVERY — gold-banking-monorepo
Branch: **`release/delivery`** · Prepared 2026-09-23
Contains everything from `parity/security-hotfix` and `parity/phase-1-admin`.
Both merged with **no conflicts** (`phase-1-admin` already contained
`security-hotfix`, so the merge was a fast-forward). `main` was not touched.
---
## 0. Quick start — run these in order
Copy-paste, top to bottom. Nothing here needs a decision.
### Step 1 — fill in the environment
Create `.env` in the repo root. These **must** have a value:
```dotenv
# Database
POSTGRES_USER=postgres
POSTGRES_PASSWORD=<choose a strong password>
POSTGRES_DB=goldbank
DATABASE_URL=postgresql://postgres:<the same password>@db:5432/goldbank?schema=public
# Auth — two DIFFERENT random values. Generate each with:
# node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))"
JWT_SECRET=<random value 1, 32+ characters>
JWT_CUSTOMER_SECRET=<random value 2, 32+ characters, NOT the same as above>
# Every origin the panel and the two apps are opened from, comma-separated.
CORS_ORIGINS=https://adm.example.ir,https://app.example.ir,https://app2.example.ir
```
The API **refuses to start** in production until all three are right, and
prints every problem at once (REL-01, REL-03, SEC-04 —
docs/SERVER-SETUP-CHECKLIST.md). A random value:
`node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`.
Inside Docker the database host is `db`, not `localhost`.
Everything else is optional and may be left blank. Two you may want now:
```dotenv
# Delivery only — a fixed OTP that signs in ANY active customer.
# Exactly 4 digits, or it cannot be typed into the apps. REMOVE before the
# real release (see "Before the real release" below). The production image
# refuses to start with it unless the server is marked as a test server:
OTP_MASTER_CODE=1379
TEST_SERVER_ALLOW_MASTER_CODE=yes
# SMS goes through the "relay" gateway with nothing to set (D26).
# SMS_PROVIDER=none turns sending off; the server still starts either way.
SMS_PROVIDER=
```
### Step 2 — build
```bash
pnpm install
pnpm build:release
```
**Do not skip `pnpm build:release`, and never run `docker compose build`
without it first.** The API image copies a bundle built here on the machine; it
does not build inside the image. Skipping this step produces an image from
**old code** and still reports success. This is real — it happened during this
delivery's own test run, with a bundle four days stale. (DEPLOY-03)
### Step 3 — start
```bash
docker compose up -d --build
```
Database migrations and seeding run **automatically inside the API container**
at boot. There is no manual migration step. Never run `prisma migrate reset`.
### Step 4 — check it came up
```bash
docker compose ps # db healthy, api up
docker compose logs api | tail -40
```
In the log you should see `Nest application successfully started`, and
`Applied migrations: …` (first run) or `No pending migrations` (later runs).
An `OTP_MASTER_CODE IS ACTIVE` banner is **expected** while that variable is
set — it is a reminder, not an error, and it disappears once you remove the
variable before the real release.
A `JWT_CUSTOMER_SECRET IS NOT SET` banner means **Step 1 was not done**. That
one is a real security problem, not a warning to ignore — go back and set it.
### Step 5 — prove the whole flow works
```bash
# Publishes the API on host port 3100 so the test can reach it.
docker compose -f docker-compose.yml -f docker-compose.delivery-test.yml up -d api
node apps/api/scripts/test-compose-e2e.cjs http://localhost:3100/api <OTP_MASTER_CODE>
```
Expect **32 passed, 0 failed**. It signs a sales expert and an ordinary
customer into the app, has both place an order, confirms both orders appear in
the admin panel, and re-checks the token boundaries — all against the real
container.
`docker-compose.delivery-test.yml` exists **only** for that check. The normal
deployment uses `docker-compose.yml` alone, where the API has no host port and
Dokploy/Traefik routes to it by domain.
---
## ⚠ Before the real release
Tonight's build is a **delivery build**. These are the things that must change
before it is a production release. The first three are blocking.
### A. Configuration — blocking
| # | Do this | Why it matters |
|---|---|---|
| **1** | **Set `JWT_CUSTOMER_SECRET` to its own random value**, different from `JWT_SECRET`. | Not a cosmetic default. Unset, the API signs customer and sales-expert **app** tokens with `JWT_SECRET` — *the same key admin tokens use* — and the two token realms stop being cryptographically separate. The only remaining barrier is the `typ` check in `jwt.strategy.ts`, and a sales expert's app token carries their **AdminUser id** in `sub`. Two independent defences collapse into one. A loud boot banner fires while it is missing. **REL-03 (Critical).** |
| **2** | **Remove `OTP_MASTER_CODE` and `TEST_SERVER_ALLOW_MASTER_CODE` from the environment.** The API now refuses to start with the code set unless that flag says this is a test server. | While set, **any active customer signs in with that one fixed code**. Unset or empty disables it completely; the API prints a banner at boot for as long as it is armed. **REL-01 (Critical).** |
| **3** | **Set real `JWT_SECRET` and `CORS_ORIGINS`.** The API refuses to start without them. | Without the key anyone could forge an admin token; without the origin list any website could call the API from a visitor's browser. |
| **4** | **Confirm SMS is going out.** The default `relay` gateway needs no credentials (D26); check `docker compose logs api` for `SMS provider: relay` and send yourself a login code. | If the gateway refuses the `host` value (inside Docker it is the container's IP), set `SMS_RELAY_HOST`. A failed send is only a log line — the server still boots (D20). **REL-02.** |
| **5** | **Accounting integration (D27), only if it will be switched on:** set `ACCOUNTING_SECRET_KEY`; count products with no image (they disappear from the app — query in ISSUES ACC-05) and photograph them first; for Zarrin, run the trial on temporary documents first (ACC-01). | The integration itself is off until an admin turns it on in «اتصال حسابداری»; nothing changes before that except the no-image rule, which applies to every product. |
After changing any of these: `docker compose up -d --build` and confirm the
`OTP_MASTER_CODE` banner is **gone** from `docker compose logs api`.
### B. Credentials to rotate — the reference leaked them
These are committed in plaintext in `henza_server` and must be assumed
compromised by anyone who has ever had repo access. None were copied into this
repository, but they are **the same live accounts**.
- **SMS panel** username + password + sender line (`app/Models/Sms.php:73-77`) — **SEC-12 (Critical)**
- PayPing bearer token, Tahesab bearer token + IP allowlist, wallet hash pepper — **SEC-06**
### C. Parity work not finished
Phase 1 stopped after the Persons section when delivery mode began (D19). The
**Phase 1 gate still stands** as the next milestone.
**Admin — not yet audited field-by-field.** Each has had only the breakage scan
(`scan-admin-b2b.cjs`, 34 endpoints, 0 broken), not a parity audit:
- **Financial documents** (اسناد) — the money path; highest priority of the three
- **Users & permissions** (کاربران و دسترسی‌ها) — includes **PERM-01**, still
open: the database holds **178** distinct permission ids against the UI's
**174**. Unresolved because fixing it means **deleting rows**, which needs
your approval.
- **Reports** (گزارش‌ها)
- Also untouched: products/categories/variants, SMS templates, promotions,
cities, branches, workshops.
**Apps — known gaps.**
| ID | What |
|---|---|
| **MON-08** | `wagePercent`, `discountPercent` and `flatRialCharge` are taken from the **client** on order creation, so a caller can set their own making charge. `rashWeightGrams` is correctly read from the variant. The reference is *worse* (it trusts client `rash_weight` too), so this is already a partial improvement — but it is still a money path trusting client input. **Fix this first in Phase 2.** |
| **STUB-05** | Admin SMS recipients are hardcoded mobile numbers scattered across six places in the reference. Port as `SMS_ADMIN_RECIPIENTS` (comma-separated), the way `PROFILE_ADMIN_MOBILES` already works — not by copying the literals. Staff changes should not need a deploy. |
| **STUB-04** | Live gold price is **pull-on-mount only** — no socket, no polling. `quotedAt` renders as a Jalali **date**, so a stale rate looks current. |
| **STUB-03** | All list filtering and paging is **client-side** over one flat `GET /catalog/products`; no server `page`/`limit`. |
| **Analytics** | The reference PWA calls `useCustomAnalytics` on every route change (`henza_pwa/src/router/index.ts:249`); neither target app has any analytics. Deliberate — it needs a provider decision from you, not a port. |
**`docs/parity/ISSUES.md`** holds the full list with evidence and file
references — currently **40+ entries**, including everything above plus the
reference bugs deliberately not ported (SEC-14, SEC-16, APP-03, and the two
D21 items). Phase 3 is the pass over that file.
---
## 1. Environment variables
Names only — no values here, and none are committed. The full annotated list is
in `.env.example`; `docker-compose.yml` passes each one through to the API
container.
### Required
| Variable | Purpose |
|---|---|
| `DATABASE_URL` | PostgreSQL connection string. Inside Docker the host is `db`. |
| `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` | Credentials for the bundled `db` service. |
| `JWT_SECRET` | Signs **admin** tokens. Without it the API falls back to a well-known development string — set it. |
| `JWT_CUSTOMER_SECRET` | Signs **customer and sales-expert app** tokens. Must be its own value — see the warning at the top of this file. Unset, it falls back to `JWT_SECRET` and the two token realms merge. |
### Optional
| Variable | Default | Purpose |
|---|---|---|
| `PORT` / `API_PORT` | `3000` | API listen port. |
| `JWT_EXPIRES_IN` | `12h` | Admin token lifetime. |
| `JWT_CUSTOMER_EXPIRES_IN` | `30d` | Customer token lifetime. |
| `UPLOAD_DIR` | `<api>/storage/uploads` | Where uploaded files are written. |
| `ADMIN_PANEL_PORT`, `USER_APP_PORT`, `USER_APP_V2_PORT` | 5180–5182 | Dev servers only. |
| `RUN_MIGRATIONS` | `true` | Set `false` to skip the boot-time migration run. |
| `RUN_SEED` | `true` | Set `false` to skip seeding. |
| `ACCOUNTING_SECRET_KEY` | *(derived from `JWT_SECRET`)* | Encrypts the accounting-software token the admin enters (D27). Set its own long random value; changing it later means re-entering the token. |
| `ACCOUNTING_WORKER` | `on` | `off` keeps this instance out of the accounting background sync. |
| `PROFILE_ADMIN_MOBILES` | *(blank)* | Comma-separated admin mobiles told when a customer completes their profile. The reference hardcoded four numbers; blank means nobody is notified and the upload still succeeds. |
### SMS — all optional
`SMS_PROVIDER`: `relay` (default), `trez`, or `none` (D26).
- **relay**: `SMS_RELAY_URL`, `SMS_RELAY_SECRET` (both have built-in
defaults), `SMS_RELAY_HOST` (blank = the machine's first external IPv4),
`SMS_RELAY_PLATFORM` (`kave` default, or `default` to let the gateway pick).
- **trez**: `SMS_TREZ_URL`, `SMS_TREZ_USERNAME`, `SMS_TREZ_PASSWORD`,
`SMS_TREZ_SENDER_LINE`.
The server **always starts**; a misconfigured provider is a warning, and a
failed or skipped SMS never blocks an operation, including customer creation.
### ⚠ `OTP_MASTER_CODE` — delivery only
A fixed OTP accepted for **any active customer**, alongside the real code.
- Read only from the environment; never hardcoded.
- Unset or empty → completely disabled.
- **Must be exactly 4 digits.** Both apps' login box is four single-character
inputs and only submits at four, so a longer code works over the API but
cannot be typed into either app.
- Every login through it is logged with the customer's mobile.
- The real OTP keeps working unchanged while it is armed.
---
## 2. Running it
### Development
```bash
pnpm install
pnpm dev # all apps via turbo
```
Ports come from the repo-root `.env` (single source of truth):
API 3000, admin-panel 5180, user-app 5181, user-app-v2 5182.
### Build
```bash
pnpm build # all workspaces
pnpm build:release # the production/Docker build
```
### Deploy (Docker)
```bash
pnpm build:release # ⚠ REQUIRED FIRST — see below
docker compose up -d --build
```
> **⚠ `pnpm build:release` must be run before `docker compose build`.**
> The API image is `COPY release/ ./` — it ships a bundle built on the
> developer machine, not one built inside the image. So `docker compose build`
> will happily produce an image from a **stale** `release/app.js` and report
> success. During this delivery's compose run that bundle was four days old and
> the container was missing every route added since. Tracked as **DEPLOY-03**.
Services: `db` (Postgres 15), `api` (port 3000), `admin-panel`, `user-app`,
`user-app-v2` (each nginx on port 80). The apps expose no host ports —
Dokploy/Traefik routes to them by domain. Uploads persist in the `api_uploads`
volume; without it every previously uploaded image 404s after a redeploy.
---
## 3. How migrations run
**Automatically, inside the API container, at boot.** No manual step.
`src/prisma/bootstrap.ts` calls `runMigrations()` before Nest starts. That
runner (`src/prisma/migrate.ts`) applies the `.sql` files from
`prisma/migrations` and records each one in its own tracking table,
**`_goldbank_migrations`**, so every file runs exactly once.
It exists because **the deploy image is copy-only and ships no Prisma CLI**, so
`prisma migrate deploy` is not available there. This is why the database has two
migration ledgers — `_prisma_migrations` (written by the Prisma CLI in
development) and `_goldbank_migrations` (written by the runtime runner). Both
are intentional. `_goldbank_migrations` is declared in `schema.prisma` with
`@@ignore` so drift checks do not propose dropping it.
**Never** run `prisma migrate reset`, and never edit either table by hand.
In development, migrations are created with `prisma migrate dev`; in production
the container runner applies them.
Seeding runs once after migrations, gated on a marker row, and is skipped when
no seed script is present.
---
## 4. What was verified on this branch
All green on `release/delivery`:
| Suite | Result |
|---|---|
| `pnpm build` (5 workspaces) | ✅ all pass |
| jest (api unit) | **54/54** |
| `smoke-admin.cjs` — admin flows | **43/43** |
| `smoke-apps.cjs` — customer flows, both apps | **43/43** |
| `scan-admin-b2b.cjs` — every B2B read endpoint | **34 ok, 0 broken** |
| `smoke-admin-b2b-writes.cjs` — order + document writes | **25/25** |
| `auth-smoke.sh` — permissions/401/403 | **32/32** |
| `test-sales-expert-roundtrip.cjs` | **33/33** |
| `test-sales-expert-app-login.cjs` — expert app login (D21) | **36/36** |
| `test-complete-profile.cjs` — «تکمیل پروفایل» | **38/38** |
| `test-report.cjs` — «گزارش» | **40/40** |
| `test-compose-e2e.cjs` — **the whole flow inside Docker Compose** | **32/32** |
| `test-name-trigger.cjs` | **9/9** |
| typecheck (api, admin-panel, user-app, user-app-v2) | ✅ clean |
### Verified inside Docker Compose, not just locally
The whole delivery flow was run against the **built container**, which is what
actually ships: `test-compose-e2e.cjs` signs a **sales expert** and an
**ordinary customer** into the app with the master code, has **both place an
order**, and confirms **both orders appear in the admin panel** and open there.
It also re-checks the three token boundaries in the deployed image. **32/32.**
The three page suites were re-run against the container too — complete-profile
38/38, report 40/40, expert login 36/36.
That run found two deployment problems that no local test could have caught:
- **DEPLOY-02 (fixed):** admin login returned **500 for every user in Docker**.
Compose passes optional variables as `${X:-}`, so an unconfigured one arrives
as `""`, not `undefined`; the code used `?? 'default'`, which only falls back
on `undefined`, so the empty string reached `jsonwebtoken` and it threw on
every sign. Fixed with `src/common/env.ts`, applied to every auth, port,
upload and SMS read, and pinned by a test that reproduces the original throw.
- **DEPLOY-03 (process):** the API image ships a **pre-built** bundle, so
`docker compose build` can silently produce an image from stale code. See the
warning in the deploy section above.
Flows walked end to end: admin login, customer creation, sales-expert
create/edit, product and variant create/edit, order list/detail/status change,
wholesale order placement from the admin panel, cash document creation +
confirm + un-confirm; and in **both** customer apps: OTP login with the master
code, product browsing, cart, order placement, order history.
Money paths were checked against the reference's own formulas, not just for a
200: making charge `rash × (1 + (wage − discount)/100)` reproduced exactly, and
the cash-document conversion `price / (price_per_gram / 4.331802)` moved the
customer's gold debt by the expected amount and restored it exactly on
un-confirm.
Every test script deletes only the rows it created; the write test also asserts
the customer's balances end exactly as they started.
---
## 5. Known issues — not done tonight
Nothing here blocks the delivery. Full detail in `docs/parity/ISSUES.md`.
### Missing app pages
**None.** Every page the reference PWA has now exists in both apps.
### Delivered tonight
| Feature | State |
|---|---|
| **Sales-expert app login (D21)** | ✅ Done. An expert signs into both apps with their AdminUser account and the ordinary OTP flow; the master code works for them too. They get the whole app — browsing, cart, ordering, order history — plus their own two screens. |
| **«سفارش مشتریان» (ChildOrder)** | ✅ Done in both apps. Their referred customers' orders at «تحویل مسئول فروش», searchable, infinite-scrolled. |
| **«مشتریان من» (customerChoice)** | ✅ Done in both apps. Their referred customers, searchable, with a read-only detail sheet. |
| **APP-02 — admin home slider in v2** | ✅ Fixed. v2 now prefers admin-configured slides and keeps its category-derived hero as the fallback. |
| **«تکمیل پروفایل» (complete_profile)** | ✅ Done in both apps. Three document uploads (جواز کسب، سردر مغازه، کارت ملی), then the profile is marked verified. Reached from the profile screen, and only while the documents are missing — exactly the reference's condition. |
| **«گزارش» (report + report/detail)** | ✅ Done in both apps. Two tabs («کلیات حساب» / «جزئیات حساب»), a running gold and rial balance per document, and a per-document detail sheet. Built on the **internal ledger** (owner's option الف) — no connection to the external accounting system the reference proxied. See D24. |
An expert's own order lands on their **own trading account**, with the making
charge from `AdminUser.wagePercent` and the credit limit from
`SalesExpertProfile.gramLevel` — the same two numbers the reference read off
the agent's own customer row. See D21.
**Two reference behaviours were deliberately not ported**, both money/session
risks and both recorded in D21:
1. `myChild` minted an `api_token` per listed customer so the agent could act
**as** them — a session-hijack primitive.
2. The child-order edit drawer (`PUT /order/child/{id}`) recalculated an
order's weights and prices from client-sent values. The card opens a
read-only summary instead.
### Behavioural gaps
| ID | What |
|---|---|
| **APP-02** | `user-app-v2` has no `getHomeSlider`, so slides configured in the admin panel appear in **user-app only**; v2 always derives its hero from categories. Not fixed because v2's home is its own design and rewiring it during a delivery risks the page being demonstrated. |
| **PERS-02** | The reference's cities are a province → city **tree** (provinces not selectable); the target flattened this to a `province` string and a flat select on all three Persons forms. The city itself is still captured correctly. |
| **PERS-03** | The reference collects a Jalali **birth date** on the customer forms; the target has no column for it. |
| **PERS-01** | The reference makes a customer's **mobile immutable on edit**; the target allows editing it. Owner ruled this NOT ported — the target keeps its current behaviour. |
| **PERF-01** | `listReagents()` runs one aggregate per row (N+1) for the per-agent sales total. Harmless at the current agent count. |
### Parity work not reached
Phase 1 was not completed: the field-by-field audit stopped after the Persons
section, and products/categories/variants, financial documents, users and
permissions, SMS templates, promotions, cities, branches, workshops and reports
have had only the breakage scan above, not a full parity audit. The
**Phase 1 gate was lifted for tonight only** (D19) and still stands as the next
milestone.
`PERM-01` (the database holds 178 distinct permission ids against the UI's 174)
is still awaiting a decision, because resolving it means deleting rows.
---
## 6. Decisions that shaped this build
Recorded in `docs/parity/DECISIONS.md`:
- **D13** — the retail storefront (`single*`, wallet, comments, `customer.php`,
`site.php`) is **preserved but out of scope**. Nothing there was changed.
- **D19** — delivery mode: implement the main gaps, do not stop to ask, choose
the lowest-risk option, change nothing protected.
- **D20** — SMS can never block a boot or an operation; `OTP_MASTER_CODE` for
the delivery.
- **D26** — SMS goes through the owner's `relay` gateway by default; Trez is
kept as a selectable driver.
- **D11 is cancelled** — superseded by D13.
+1 -1
View File
@@ -1,4 +1,4 @@
FROM docker.arvancloud.ir/nginx:latest
FROM nginx:latest
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY dist /usr/share/nginx/html
@@ -0,0 +1 @@
import{c as O,I as x,d as j,v as B,x as m,o as C,a as A,w as v,f as $,t as E,h as P,i as L,l as u,Z as y,M as b,z as M,r as N}from"./index-DDKWZYPX.js";var z={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M168 504.2c1-43.7 10-86.1 26.9-126 17.3-41 42.1-77.7 73.7-109.4S337 212.3 378 195c42.4-17.9 87.4-27 133.9-27s91.5 9.1 133.8 27A341.5 341.5 0 01755 268.8c9.9 9.9 19.2 20.4 27.8 31.4l-60.2 47a8 8 0 003 14.1l175.7 43c5 1.2 9.9-2.6 9.9-7.7l.8-180.9c0-6.7-7.7-10.5-12.9-6.3l-56.4 44.1C765.8 155.1 646.2 92 511.8 92 282.7 92 96.3 275.6 92 503.8a8 8 0 008 8.2h60c4.4 0 7.9-3.5 8-7.8zm756 7.8h-60c-4.4 0-7.9 3.5-8 7.8-1 43.7-10 86.1-26.9 126-17.3 41-42.1 77.8-73.7 109.4A342.45 342.45 0 01512.1 856a342.24 342.24 0 01-243.2-100.8c-9.9-9.9-19.2-20.4-27.8-31.4l60.2-47a8 8 0 00-3-14.1l-175.7-43c-5-1.2-9.9 2.6-9.9 7.7l-.7 181c0 6.7 7.7 10.5 12.9 6.3l56.4-44.1C258.2 868.9 377.8 932 512.2 932c229.2 0 415.5-183.7 419.8-411.8a8 8 0 00-8-8.2z"}}]},name:"sync",theme:"outlined"};function g(o){for(var t=1;t<arguments.length;t++){var n=arguments[t]!=null?Object(arguments[t]):{},r=Object.keys(n);typeof Object.getOwnPropertySymbols=="function"&&(r=r.concat(Object.getOwnPropertySymbols(n).filter(function(c){return Object.getOwnPropertyDescriptor(n,c).enumerable}))),r.forEach(function(c){V(o,c,n[c])})}return o}function V(o,t,n){return t in o?Object.defineProperty(o,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):o[t]=n,o}var d=function(t,n){var r=g({},t,n.attrs);return O(x,g({},r,{icon:z}),null)};d.displayName="SyncOutlined";d.inheritAttrs=!1;const I=j({__name:"AccountingSyncButton",props:{endpoint:{}},emits:["done"],setup(o,{emit:t}){const n=o,r=t,c=u(!1),p=u(null),i=u(!1),h={customers:"مشتریان",balances:"مانده‌ها",products:"کارساخته‌ها و موجودی"},_={scanned:"بررسی‌شده",inScope:"در گروه‌های انتخابی",refreshed:"به‌روز",linked:"لینک‌شده",imported:"وارد شده",issues:"مغایرت",updated:"به‌روز",items:"کارساخته",stockErrors:"خطای موجودی"},S=e=>e&&typeof e=="object"?Object.entries(e).map(([s,a])=>`${_[s]??s}: ${a}`).join("، "):"";B(async()=>{try{const{data:e}=await m.get("admin/accounting-status");c.value=!!(e!=null&&e.connected),p.value=(e==null?void 0:e.provider)??null}catch{c.value=!1}});async function w(){var e,s;i.value=!0;try{const{data:a}=await m.post(n.endpoint),k=Object.entries(a.results).map(([f,l])=>y("p",{style:l.ok?"":"color:#d4380d"},`${h[f]??f}: ${l.ok?S(l.summary)||"انجام شد":`خطا — ${l.error}`}`));(a.ok?b.success:b.warning)({title:a.ok?"سینک با حسابداری انجام شد":"سینک با حسابداری کامل انجام نشد",content:y("div",k),okText:"باشه"}),r("done")}catch(a){M.error(((s=(e=a==null?void 0:a.response)==null?void 0:e.data)==null?void 0:s.message)||"سینک با حسابداری انجام نشد")}finally{i.value=!1}}return(e,s)=>{const a=N("a-button");return c.value?(C(),A(a,{key:0,loading:i.value,onClick:w},{icon:v(()=>[O(P(d))]),default:v(()=>[$(" سینک با "+E(p.value||"حسابداری"),1)]),_:1},8,["loading"])):L("",!0)}}});export{I as _};
-1
View File
@@ -1 +0,0 @@
import{_ as o}from"./Add.vue_vue_type_script_setup_true_lang-vc2KlLTI.js";import"./index-DuH3OOPw.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-zAo7Oyp5.js";import"./ImageUploadInput.vue_vue_type_script_setup_true_lang-BTbAO94-.js";export{o as default};
+1
View File
@@ -0,0 +1 @@
import{_ as o}from"./Add.vue_vue_type_script_setup_true_lang-CjPh6jcZ.js";import"./index-DDKWZYPX.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-YIXFkb8N.js";import"./ImageUploadInput.vue_vue_type_script_setup_true_lang-DHQxooHi.js";export{o as default};
+1
View File
@@ -0,0 +1 @@
import{_ as o}from"./AddBranches.vue_vue_type_script_setup_true_lang-CQi_1IC6.js";import"./index-DDKWZYPX.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-YIXFkb8N.js";export{o as default};
-1
View File
@@ -1 +0,0 @@
import{_ as o}from"./AddBranches.vue_vue_type_script_setup_true_lang-BQagavGv.js";import"./index-DuH3OOPw.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-zAo7Oyp5.js";export{o as default};
@@ -1 +1 @@
import{d as q,v as G,x as c,a as v,w as l,h as R,j as H,l as S,y as j,r as i,o as p,n as C,c as n,b as O,e as $,F as z,f as g,A as I,z as M,k as J,t as N}from"./index-DuH3OOPw.js";import{_ as K}from"./BaseFormModal.vue_vue_type_script_setup_true_lang-zAo7Oyp5.js";const P={class:"grid-2"},Q={class:"grid-2"},Y=q({__name:"AddBranches",props:{open:{type:Boolean},id:{}},emits:["update:open","success"],setup(h,{emit:V}){const y=h,w=V,f=y.open!==void 0,A=H(),E=J(),d=f?y.id:A.params.id,m=S(!1),_=S([]),t=j({name:"",phone:"",address:"",user_id:void 0}),r=j({});function k(s,e){var o,u;return String(((u=(o=e==null?void 0:e.children)==null?void 0:o[0])==null?void 0:u.children)??"").includes(s)}async function b(){var s,e;if(Object.keys(r).forEach(o=>delete r[o]),!t.name){r.name="لطفا مقدار را وارد کنید";return}try{m.value=!0,d?await c.put(`branch/${d}`,t):await c.post("branch",t),M.info("اطلاعات با موفقیت به ثبت رسید."),d||Object.assign(t,{name:"",phone:"",address:"",user_id:void 0}),f?(w("success"),w("update:open",!1)):E.push("/branch/list")}catch(o){M.error(((e=(s=o==null?void 0:o.response)==null?void 0:s.data)==null?void 0:e.message)??"خطا در ثبت اطلاعات.")}finally{m.value=!1}}return G(async()=>{try{const{data:s}=await c.get("user/list");_.value=s}catch(s){console.log(s)}if(d)try{const{data:s}=await c.get(`branch/${d}`);Object.assign(t,{name:s.name??"",phone:s.phone??"",address:s.address??"",user_id:s.user_id??void 0})}catch(s){console.log(s)}}),(s,e)=>{const o=i("a-input"),u=i("a-form-item"),U=i("a-select-option"),x=i("a-select"),B=i("a-textarea"),F=i("a-button"),D=i("a-card"),L=i("a-form"),T=i("a-spin");return f?(p(),v(K,{key:0,open:!!h.open,id:R(d),"add-title":"افزودن شعبه","edit-title":"ویرایش شعبه",loading:m.value,submitting:m.value,"onUpdate:open":e[4]||(e[4]=a=>s.$emit("update:open",a)),onSubmit:b},{default:l(()=>[C("div",P,[n(u,{label:"نام شعبه","validate-status":r.name?"error":"",help:r.name},{default:l(()=>[n(o,{value:t.name,"onUpdate:value":e[0]||(e[0]=a=>t.name=a)},null,8,["value"])]),_:1},8,["validate-status","help"]),n(u,{label:"تلفن شعبه"},{default:l(()=>[n(o,{value:t.phone,"onUpdate:value":e[1]||(e[1]=a=>t.phone=a)},null,8,["value"])]),_:1}),n(u,{label:"مدیر"},{default:l(()=>[n(x,{value:t.user_id,"onUpdate:value":e[2]||(e[2]=a=>t.user_id=a),"show-search":"","allow-clear":"",placeholder:"انتخاب کنید","filter-option":k},{default:l(()=>[(p(!0),O(z,null,$(_.value,a=>(p(),v(U,{key:a.id,value:a.id},{default:l(()=>[g(N(a.name),1)]),_:2},1032,["value"]))),128))]),_:1},8,["value"])]),_:1})]),n(u,{label:"آدرس"},{default:l(()=>[n(B,{value:t.address,"onUpdate:value":e[3]||(e[3]=a=>t.address=a),"auto-size":{minRows:2,maxRows:5}},null,8,["value"])]),_:1})]),_:1},8,["open","id","loading","submitting"])):(p(),v(T,{key:1,spinning:m.value},{default:l(()=>[n(L,{layout:"vertical",onSubmit:I(b,["prevent"])},{default:l(()=>[n(D,{title:R(d)?"ویرایش شعبه":"افزودن شعبه",class:"mb-base"},{default:l(()=>[C("div",Q,[n(u,{label:"نام شعبه","validate-status":r.name?"error":"",help:r.name},{default:l(()=>[n(o,{value:t.name,"onUpdate:value":e[5]||(e[5]=a=>t.name=a)},null,8,["value"])]),_:1},8,["validate-status","help"]),n(u,{label:"تلفن شعبه"},{default:l(()=>[n(o,{value:t.phone,"onUpdate:value":e[6]||(e[6]=a=>t.phone=a)},null,8,["value"])]),_:1}),n(u,{label:"مدیر"},{default:l(()=>[n(x,{value:t.user_id,"onUpdate:value":e[7]||(e[7]=a=>t.user_id=a),"show-search":"","allow-clear":"",placeholder:"انتخاب کنید","filter-option":k},{default:l(()=>[(p(!0),O(z,null,$(_.value,a=>(p(),v(U,{key:a.id,value:a.id},{default:l(()=>[g(N(a.name),1)]),_:2},1032,["value"]))),128))]),_:1},8,["value"])]),_:1})]),n(u,{label:"آدرس"},{default:l(()=>[n(B,{value:t.address,"onUpdate:value":e[8]||(e[8]=a=>t.address=a),"auto-size":{minRows:2,maxRows:5}},null,8,["value"])]),_:1}),n(F,{type:"primary",onClick:b},{default:l(()=>[...e[9]||(e[9]=[g("ثبت اطلاعات",-1)])]),_:1})]),_:1},8,["title"])]),_:1})]),_:1},8,["spinning"]))}}});export{Y as _};
import{d as q,v as G,x as c,a as v,w as l,h as R,j as H,l as S,y as j,r as i,o as p,n as C,c as n,b as O,e as $,F as z,f as g,A as I,z as M,k as J,t as N}from"./index-DDKWZYPX.js";import{_ as K}from"./BaseFormModal.vue_vue_type_script_setup_true_lang-YIXFkb8N.js";const P={class:"grid-2"},Q={class:"grid-2"},Y=q({__name:"AddBranches",props:{open:{type:Boolean},id:{}},emits:["update:open","success"],setup(h,{emit:V}){const y=h,w=V,f=y.open!==void 0,A=H(),E=J(),d=f?y.id:A.params.id,m=S(!1),_=S([]),t=j({name:"",phone:"",address:"",user_id:void 0}),r=j({});function k(s,e){var o,u;return String(((u=(o=e==null?void 0:e.children)==null?void 0:o[0])==null?void 0:u.children)??"").includes(s)}async function b(){var s,e;if(Object.keys(r).forEach(o=>delete r[o]),!t.name){r.name="لطفا مقدار را وارد کنید";return}try{m.value=!0,d?await c.put(`branch/${d}`,t):await c.post("branch",t),M.info("اطلاعات با موفقیت به ثبت رسید."),d||Object.assign(t,{name:"",phone:"",address:"",user_id:void 0}),f?(w("success"),w("update:open",!1)):E.push("/branch/list")}catch(o){M.error(((e=(s=o==null?void 0:o.response)==null?void 0:s.data)==null?void 0:e.message)??"خطا در ثبت اطلاعات.")}finally{m.value=!1}}return G(async()=>{try{const{data:s}=await c.get("user/list");_.value=s}catch(s){console.log(s)}if(d)try{const{data:s}=await c.get(`branch/${d}`);Object.assign(t,{name:s.name??"",phone:s.phone??"",address:s.address??"",user_id:s.user_id??void 0})}catch(s){console.log(s)}}),(s,e)=>{const o=i("a-input"),u=i("a-form-item"),U=i("a-select-option"),x=i("a-select"),B=i("a-textarea"),F=i("a-button"),D=i("a-card"),L=i("a-form"),T=i("a-spin");return f?(p(),v(K,{key:0,open:!!h.open,id:R(d),"add-title":"افزودن شعبه","edit-title":"ویرایش شعبه",loading:m.value,submitting:m.value,"onUpdate:open":e[4]||(e[4]=a=>s.$emit("update:open",a)),onSubmit:b},{default:l(()=>[C("div",P,[n(u,{label:"نام شعبه","validate-status":r.name?"error":"",help:r.name},{default:l(()=>[n(o,{value:t.name,"onUpdate:value":e[0]||(e[0]=a=>t.name=a)},null,8,["value"])]),_:1},8,["validate-status","help"]),n(u,{label:"تلفن شعبه"},{default:l(()=>[n(o,{value:t.phone,"onUpdate:value":e[1]||(e[1]=a=>t.phone=a)},null,8,["value"])]),_:1}),n(u,{label:"مدیر"},{default:l(()=>[n(x,{value:t.user_id,"onUpdate:value":e[2]||(e[2]=a=>t.user_id=a),"show-search":"","allow-clear":"",placeholder:"انتخاب کنید","filter-option":k},{default:l(()=>[(p(!0),O(z,null,$(_.value,a=>(p(),v(U,{key:a.id,value:a.id},{default:l(()=>[g(N(a.name),1)]),_:2},1032,["value"]))),128))]),_:1},8,["value"])]),_:1})]),n(u,{label:"آدرس"},{default:l(()=>[n(B,{value:t.address,"onUpdate:value":e[3]||(e[3]=a=>t.address=a),"auto-size":{minRows:2,maxRows:5}},null,8,["value"])]),_:1})]),_:1},8,["open","id","loading","submitting"])):(p(),v(T,{key:1,spinning:m.value},{default:l(()=>[n(L,{layout:"vertical",onSubmit:I(b,["prevent"])},{default:l(()=>[n(D,{title:R(d)?"ویرایش شعبه":"افزودن شعبه",class:"mb-base"},{default:l(()=>[C("div",Q,[n(u,{label:"نام شعبه","validate-status":r.name?"error":"",help:r.name},{default:l(()=>[n(o,{value:t.name,"onUpdate:value":e[5]||(e[5]=a=>t.name=a)},null,8,["value"])]),_:1},8,["validate-status","help"]),n(u,{label:"تلفن شعبه"},{default:l(()=>[n(o,{value:t.phone,"onUpdate:value":e[6]||(e[6]=a=>t.phone=a)},null,8,["value"])]),_:1}),n(u,{label:"مدیر"},{default:l(()=>[n(x,{value:t.user_id,"onUpdate:value":e[7]||(e[7]=a=>t.user_id=a),"show-search":"","allow-clear":"",placeholder:"انتخاب کنید","filter-option":k},{default:l(()=>[(p(!0),O(z,null,$(_.value,a=>(p(),v(U,{key:a.id,value:a.id},{default:l(()=>[g(N(a.name),1)]),_:2},1032,["value"]))),128))]),_:1},8,["value"])]),_:1})]),n(u,{label:"آدرس"},{default:l(()=>[n(B,{value:t.address,"onUpdate:value":e[8]||(e[8]=a=>t.address=a),"auto-size":{minRows:2,maxRows:5}},null,8,["value"])]),_:1}),n(F,{type:"primary",onClick:b},{default:l(()=>[...e[9]||(e[9]=[g("ثبت اطلاعات",-1)])]),_:1})]),_:1},8,["title"])]),_:1})]),_:1},8,["spinning"]))}}});export{Y as _};
+1
View File
@@ -0,0 +1 @@
import{_ as d}from"./bulkCustomer.vue_vue_type_script_setup_true_lang-DE79zKza.js";import{R as u}from"./ReagentCustomer-6o67xE2d.js";import{_ as l}from"./singleCustomer.vue_vue_type_script_setup_true_lang-yVXlCX4v.js";import{d as f,u as b,b as k,n as h,c as e,w as t,r,o as a,a as n,i as c}from"./index-DDKWZYPX.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-YIXFkb8N.js";import"./ImageUploadInput.vue_vue_type_script_setup_true_lang-DHQxooHi.js";import"./CityTreeSelect.vue_vue_type_script_setup_true_lang-BRAJxq2_.js";import"./DateInput-BFcDZlW_.js";import"./index-FKca-jp6.js";import"./_plugin-vue_export-helper-DlAUqK2U.js";const C={id:"page-user-view"},B={id:"user-data"},S=f({__name:"AddCustomer",setup(v){const m=b(),s=_=>!!m.hasPermission(_);return(_,y)=>{const o=r("a-tab-pane"),i=r("a-tabs"),p=r("a-card");return a(),k("div",C,[h("div",B,[e(p,{class:"mb-base"},{default:t(()=>[s("persons.create")?(a(),n(i,{key:0},{default:t(()=>[s("persons.create.client")?(a(),n(o,{key:"omde",tab:"مشتری عمده"},{default:t(()=>[e(d)]),_:1})):c("",!0),s("persons.create.seller")?(a(),n(o,{key:"reagent",tab:"کارشناس فروش"},{default:t(()=>[e(u)]),_:1})):c("",!0),e(o,{key:"tak",tab:"مشتری تک"},{default:t(()=>[e(l)]),_:1})]),_:1})):c("",!0)]),_:1})])])}}});export{S as default};
-1
View File
@@ -1 +0,0 @@
import{_ as p}from"./bulkCustomer.vue_vue_type_script_setup_true_lang-DgWWO9pL.js";import{d as u,u as l,b as f,n as b,c as n,w as e,r as a,o as s,a as r,i as c}from"./index-DuH3OOPw.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-zAo7Oyp5.js";const h={id:"page-user-view"},k={id:"user-data"},N=u({__name:"AddCustomer",setup(B){const _=l(),t=o=>!!_.hasPermission(o);return(o,C)=>{const d=a("a-tab-pane"),m=a("a-tabs"),i=a("a-card");return s(),f("div",h,[b("div",k,[n(i,{class:"mb-base"},{default:e(()=>[t("persons.create")?(s(),r(m,{key:0},{default:e(()=>[t("persons.create.client")?(s(),r(d,{key:"omde",tab:"مشتری عمده"},{default:e(()=>[n(p)]),_:1})):c("",!0)]),_:1})):c("",!0)]),_:1})])])}}});export{N as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
.grid-1[data-v-7f6fdb0f]{display:grid;grid-template-columns:1fr}.image-grid[data-v-7f6fdb0f]{display:flex;flex-wrap:wrap;gap:8px}.flex-1[data-v-7f6fdb0f]{flex:1}.mb-8[data-v-7f6fdb0f]{margin-bottom:2rem}.overflow-auto[data-v-7f6fdb0f]{overflow:auto}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.grid-1[data-v-b0cf24f2]{display:grid;grid-template-columns:1fr}.image-grid[data-v-b0cf24f2]{display:flex;flex-wrap:wrap;gap:8px}.variant-thumb[data-v-b0cf24f2]{width:40px;height:40px;object-fit:cover;border-radius:4px;display:inline-block}.variant-thumb--empty[data-v-b0cf24f2]{display:inline-flex;align-items:center;justify-content:center;border:1px dashed #d9d9d9;color:#bfbfbf}.variant-picker[data-v-b0cf24f2]{display:flex;flex-wrap:wrap;gap:8px}.variant-picker__item[data-v-b0cf24f2]{width:64px;height:64px;padding:2px;border:2px solid #d9d9d9;border-radius:6px;background:#fff;cursor:pointer;overflow:hidden}.variant-picker__item img[data-v-b0cf24f2]{width:100%;height:100%;object-fit:cover;border-radius:4px;display:block}.variant-picker__item.is-on[data-v-b0cf24f2]{border-color:#1890ff}.variant-picker__none[data-v-b0cf24f2]{display:flex;align-items:center;justify-content:center;width:100%;height:100%;font-size:11px;color:#8c8c8c;line-height:1.3}.flex-1[data-v-b0cf24f2]{flex:1}.mb-8[data-v-b0cf24f2]{margin-bottom:2rem}.overflow-auto[data-v-b0cf24f2]{overflow:auto}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{_ as o}from"./AddWorkshop.vue_vue_type_script_setup_true_lang-CVuc-ZrE.js";import"./index-DDKWZYPX.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-YIXFkb8N.js";export{o as default};
-1
View File
@@ -1 +0,0 @@
import{_ as o}from"./AddWorkshop.vue_vue_type_script_setup_true_lang-Ddx471SB.js";import"./index-DuH3OOPw.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-zAo7Oyp5.js";export{o as default};
@@ -1 +1 @@
import{d as N,v as O,x as v,a as b,w as n,h as g,j as S,l as V,y,r as p,o as U,n as x,c as o,f as A,A as E,z as c,k as T}from"./index-DuH3OOPw.js";import{_ as W}from"./BaseFormModal.vue_vue_type_script_setup_true_lang-zAo7Oyp5.js";const q={class:"grid-3"},D={class:"grid-3"},H=N({__name:"AddWorkshop",props:{open:{type:Boolean},id:{}},emits:["update:open","success"],setup(f,{emit:R}){const h=f,w=R,_=h.open!==void 0,B=S(),j=T(),d=_?h.id:B.params.id,i=V(!1),e=y({workshop_name:"",workshop_phone:"",workshop_address:"",user_fname:"",user_lname:"",user_phone:""}),u=y({});async function m(){var t,a;if(Object.keys(u).forEach(l=>delete u[l]),e.workshop_name||(u.workshop_name="لطفا مقدار را وارد کنید"),(e.user_fname||e.user_lname)&&!e.user_phone&&(u.user_phone="موبایل سرپرست کارگاه الزامی است"),!Object.keys(u).length)try{i.value=!0,d?await v.put(`workshop/${d}`,e):await v.post("workshop",e),c.info("اطلاعات با موفقیت به ثبت رسید."),_?(w("success"),w("update:open",!1)):j.push("/workshop/list")}catch(l){c.error(((a=(t=l==null?void 0:l.response)==null?void 0:t.data)==null?void 0:a.message)??"خطا در ثبت اطلاعات.")}finally{i.value=!1}}return O(async()=>{if(d)try{const{data:t}=await v.get(`workshop/${d}`);Object.assign(e,{workshop_name:t.workshop_name??t.name??"",workshop_phone:t.workshop_phone??t.phone??"",workshop_address:t.workshop_address??t.address??"",user_fname:t.user_fname??"",user_lname:t.user_lname??"",user_phone:t.user_phone??""})}catch(t){console.log(t)}}),(t,a)=>{const l=p("a-input"),r=p("a-form-item"),k=p("a-textarea"),C=p("a-button"),$=p("a-card"),z=p("a-form"),M=p("a-spin");return _?(U(),b(W,{key:0,open:!!f.open,id:g(d),"add-title":"افزودن کارگاه","edit-title":"ویرایش کارگاه",loading:i.value,submitting:i.value,width:"lg","onUpdate:open":a[6]||(a[6]=s=>t.$emit("update:open",s)),onSubmit:m},{default:n(()=>[x("div",q,[o(r,{label:"نام کارگاه","validate-status":u.workshop_name?"error":"",help:u.workshop_name},{default:n(()=>[o(l,{value:e.workshop_name,"onUpdate:value":a[0]||(a[0]=s=>e.workshop_name=s)},null,8,["value"])]),_:1},8,["validate-status","help"]),o(r,{label:"تلفن کارگاه"},{default:n(()=>[o(l,{value:e.workshop_phone,"onUpdate:value":a[1]||(a[1]=s=>e.workshop_phone=s)},null,8,["value"])]),_:1}),o(r,{label:"نام سرپرست کارگاه"},{default:n(()=>[o(l,{value:e.user_fname,"onUpdate:value":a[2]||(a[2]=s=>e.user_fname=s)},null,8,["value"])]),_:1}),o(r,{label:"نام خانوادگی سرپرست کارگاه"},{default:n(()=>[o(l,{value:e.user_lname,"onUpdate:value":a[3]||(a[3]=s=>e.user_lname=s)},null,8,["value"])]),_:1}),o(r,{label:"موبایل سرپرست کارگاه","validate-status":u.user_phone?"error":"",help:u.user_phone},{default:n(()=>[o(l,{value:e.user_phone,"onUpdate:value":a[4]||(a[4]=s=>e.user_phone=s)},null,8,["value"])]),_:1},8,["validate-status","help"])]),o(r,{label:"آدرس"},{default:n(()=>[o(k,{value:e.workshop_address,"onUpdate:value":a[5]||(a[5]=s=>e.workshop_address=s),"auto-size":{minRows:2,maxRows:5}},null,8,["value"])]),_:1})]),_:1},8,["open","id","loading","submitting"])):(U(),b(M,{key:1,spinning:i.value},{default:n(()=>[o(z,{layout:"vertical",onSubmit:E(m,["prevent"])},{default:n(()=>[o($,{title:g(d)?"ویرایش کارگاه":"افزودن کارگاه",class:"mb-base"},{default:n(()=>[x("div",D,[o(r,{label:"نام کارگاه","validate-status":u.workshop_name?"error":"",help:u.workshop_name},{default:n(()=>[o(l,{value:e.workshop_name,"onUpdate:value":a[7]||(a[7]=s=>e.workshop_name=s)},null,8,["value"])]),_:1},8,["validate-status","help"]),o(r,{label:"تلفن کارگاه"},{default:n(()=>[o(l,{value:e.workshop_phone,"onUpdate:value":a[8]||(a[8]=s=>e.workshop_phone=s)},null,8,["value"])]),_:1}),o(r,{label:"نام سرپرست کارگاه"},{default:n(()=>[o(l,{value:e.user_fname,"onUpdate:value":a[9]||(a[9]=s=>e.user_fname=s)},null,8,["value"])]),_:1}),o(r,{label:"نام خانوادگی سرپرست کارگاه"},{default:n(()=>[o(l,{value:e.user_lname,"onUpdate:value":a[10]||(a[10]=s=>e.user_lname=s)},null,8,["value"])]),_:1}),o(r,{label:"موبایل سرپرست کارگاه","validate-status":u.user_phone?"error":"",help:u.user_phone},{default:n(()=>[o(l,{value:e.user_phone,"onUpdate:value":a[11]||(a[11]=s=>e.user_phone=s)},null,8,["value"])]),_:1},8,["validate-status","help"])]),o(r,{label:"آدرس"},{default:n(()=>[o(k,{value:e.workshop_address,"onUpdate:value":a[12]||(a[12]=s=>e.workshop_address=s),"auto-size":{minRows:2,maxRows:5}},null,8,["value"])]),_:1}),o(C,{type:"primary",onClick:m},{default:n(()=>[...a[13]||(a[13]=[A("ثبت اطلاعات",-1)])]),_:1})]),_:1},8,["title"])]),_:1})]),_:1},8,["spinning"]))}}});export{H as _};
import{d as N,v as O,x as v,a as b,w as n,h as g,j as S,l as V,y,r as p,o as U,n as x,c as o,f as A,A as E,z as c,k as T}from"./index-DDKWZYPX.js";import{_ as W}from"./BaseFormModal.vue_vue_type_script_setup_true_lang-YIXFkb8N.js";const q={class:"grid-3"},D={class:"grid-3"},H=N({__name:"AddWorkshop",props:{open:{type:Boolean},id:{}},emits:["update:open","success"],setup(f,{emit:R}){const h=f,w=R,_=h.open!==void 0,B=S(),j=T(),d=_?h.id:B.params.id,i=V(!1),e=y({workshop_name:"",workshop_phone:"",workshop_address:"",user_fname:"",user_lname:"",user_phone:""}),u=y({});async function m(){var t,a;if(Object.keys(u).forEach(l=>delete u[l]),e.workshop_name||(u.workshop_name="لطفا مقدار را وارد کنید"),(e.user_fname||e.user_lname)&&!e.user_phone&&(u.user_phone="موبایل سرپرست کارگاه الزامی است"),!Object.keys(u).length)try{i.value=!0,d?await v.put(`workshop/${d}`,e):await v.post("workshop",e),c.info("اطلاعات با موفقیت به ثبت رسید."),_?(w("success"),w("update:open",!1)):j.push("/workshop/list")}catch(l){c.error(((a=(t=l==null?void 0:l.response)==null?void 0:t.data)==null?void 0:a.message)??"خطا در ثبت اطلاعات.")}finally{i.value=!1}}return O(async()=>{if(d)try{const{data:t}=await v.get(`workshop/${d}`);Object.assign(e,{workshop_name:t.workshop_name??t.name??"",workshop_phone:t.workshop_phone??t.phone??"",workshop_address:t.workshop_address??t.address??"",user_fname:t.user_fname??"",user_lname:t.user_lname??"",user_phone:t.user_phone??""})}catch(t){console.log(t)}}),(t,a)=>{const l=p("a-input"),r=p("a-form-item"),k=p("a-textarea"),C=p("a-button"),$=p("a-card"),z=p("a-form"),M=p("a-spin");return _?(U(),b(W,{key:0,open:!!f.open,id:g(d),"add-title":"افزودن کارگاه","edit-title":"ویرایش کارگاه",loading:i.value,submitting:i.value,width:"lg","onUpdate:open":a[6]||(a[6]=s=>t.$emit("update:open",s)),onSubmit:m},{default:n(()=>[x("div",q,[o(r,{label:"نام کارگاه","validate-status":u.workshop_name?"error":"",help:u.workshop_name},{default:n(()=>[o(l,{value:e.workshop_name,"onUpdate:value":a[0]||(a[0]=s=>e.workshop_name=s)},null,8,["value"])]),_:1},8,["validate-status","help"]),o(r,{label:"تلفن کارگاه"},{default:n(()=>[o(l,{value:e.workshop_phone,"onUpdate:value":a[1]||(a[1]=s=>e.workshop_phone=s)},null,8,["value"])]),_:1}),o(r,{label:"نام سرپرست کارگاه"},{default:n(()=>[o(l,{value:e.user_fname,"onUpdate:value":a[2]||(a[2]=s=>e.user_fname=s)},null,8,["value"])]),_:1}),o(r,{label:"نام خانوادگی سرپرست کارگاه"},{default:n(()=>[o(l,{value:e.user_lname,"onUpdate:value":a[3]||(a[3]=s=>e.user_lname=s)},null,8,["value"])]),_:1}),o(r,{label:"موبایل سرپرست کارگاه","validate-status":u.user_phone?"error":"",help:u.user_phone},{default:n(()=>[o(l,{value:e.user_phone,"onUpdate:value":a[4]||(a[4]=s=>e.user_phone=s)},null,8,["value"])]),_:1},8,["validate-status","help"])]),o(r,{label:"آدرس"},{default:n(()=>[o(k,{value:e.workshop_address,"onUpdate:value":a[5]||(a[5]=s=>e.workshop_address=s),"auto-size":{minRows:2,maxRows:5}},null,8,["value"])]),_:1})]),_:1},8,["open","id","loading","submitting"])):(U(),b(M,{key:1,spinning:i.value},{default:n(()=>[o(z,{layout:"vertical",onSubmit:E(m,["prevent"])},{default:n(()=>[o($,{title:g(d)?"ویرایش کارگاه":"افزودن کارگاه",class:"mb-base"},{default:n(()=>[x("div",D,[o(r,{label:"نام کارگاه","validate-status":u.workshop_name?"error":"",help:u.workshop_name},{default:n(()=>[o(l,{value:e.workshop_name,"onUpdate:value":a[7]||(a[7]=s=>e.workshop_name=s)},null,8,["value"])]),_:1},8,["validate-status","help"]),o(r,{label:"تلفن کارگاه"},{default:n(()=>[o(l,{value:e.workshop_phone,"onUpdate:value":a[8]||(a[8]=s=>e.workshop_phone=s)},null,8,["value"])]),_:1}),o(r,{label:"نام سرپرست کارگاه"},{default:n(()=>[o(l,{value:e.user_fname,"onUpdate:value":a[9]||(a[9]=s=>e.user_fname=s)},null,8,["value"])]),_:1}),o(r,{label:"نام خانوادگی سرپرست کارگاه"},{default:n(()=>[o(l,{value:e.user_lname,"onUpdate:value":a[10]||(a[10]=s=>e.user_lname=s)},null,8,["value"])]),_:1}),o(r,{label:"موبایل سرپرست کارگاه","validate-status":u.user_phone?"error":"",help:u.user_phone},{default:n(()=>[o(l,{value:e.user_phone,"onUpdate:value":a[11]||(a[11]=s=>e.user_phone=s)},null,8,["value"])]),_:1},8,["validate-status","help"])]),o(r,{label:"آدرس"},{default:n(()=>[o(k,{value:e.workshop_address,"onUpdate:value":a[12]||(a[12]=s=>e.workshop_address=s),"auto-size":{minRows:2,maxRows:5}},null,8,["value"])]),_:1}),o(C,{type:"primary",onClick:m},{default:n(()=>[...a[13]||(a[13]=[A("ثبت اطلاعات",-1)])]),_:1})]),_:1},8,["title"])]),_:1})]),_:1},8,["spinning"]))}}});export{H as _};
+1
View File
@@ -0,0 +1 @@
.stats[data-v-726ef090]{display:flex;gap:.5rem}.mt-4[data-v-726ef090]{margin-top:1rem}
+1
View File
@@ -0,0 +1 @@
import{d as D,v as S,x as A,a as x,w as a,j as F,l as y,r as d,o as i,c as o,f as l,t as s,n as E,b as p,F as f,h as I,i as k}from"./index-DDKWZYPX.js";import{j}from"./filters-CqJCHW3n.js";import{_ as M}from"./_plugin-vue_export-helper-DlAUqK2U.js";import"./index-FKca-jp6.js";const R={class:"stats mt-4"},T=D({__name:"AgentDetail",setup($){const h=F().params.id,m=y(!1),n=y(void 0),C=[{title:"نام",dataIndex:"name"},{title:"موبایل",dataIndex:"mobile"},{title:"شهر",dataIndex:"city"},{title:"نوع",dataIndex:"type_str"},{title:"وضعیت",dataIndex:"active"},{title:"تاریخ ثبت",dataIndex:"caj"}],w=[{title:"شماره سفارش",dataIndex:"name"},{title:"مشتری",dataIndex:"customer"},{title:"وزن",dataIndex:"w"},{title:"وضعیت",dataIndex:"status"},{title:"تاریخ",dataIndex:"caj"}];return S(async()=>{try{m.value=!0;const{data:_}=await A.get(`admin/customers-omde/reagent/${h}`);n.value=_}catch(_){console.log(_)}finally{m.value=!1}}),(_,G)=>{const c=d("a-descriptions-item"),u=d("a-tag"),z=d("a-descriptions"),B=d("a-card"),v=d("a-table"),g=d("a-tab-pane"),N=d("a-tabs"),V=d("a-spin");return i(),x(V,{spinning:m.value},{default:a(()=>[o(B,{title:"مشاهده اطلاعات کارشناس فروش"},{default:a(()=>[o(z,{bordered:"",size:"small",column:3},{default:a(()=>[o(c,{label:"نام"},{default:a(()=>{var e;return[l(s(((e=n.value)==null?void 0:e.name)??"-"),1)]}),_:1}),o(c,{label:"موبایل"},{default:a(()=>{var e;return[l(s(((e=n.value)==null?void 0:e.mobile)??"-"),1)]}),_:1}),o(c,{label:"شعبه"},{default:a(()=>{var e,t;return[l(s(((t=(e=n.value)==null?void 0:e.branch)==null?void 0:t.name)??"-"),1)]}),_:1}),o(c,{label:"درصد کمیسیون"},{default:a(()=>{var e;return[l(s(((e=n.value)==null?void 0:e.percent)??"-"),1)]}),_:1}),o(c,{label:"وضعیت"},{default:a(()=>{var e;return[o(u,{color:((e=n.value)==null?void 0:e.active)===1?"green":"red"},{default:a(()=>{var t;return[l(s(((t=n.value)==null?void 0:t.active)===1?"فعال":"غیر فعال"),1)]}),_:1},8,["color"])]}),_:1}),o(c,{label:"میزان فروش (گرم)"},{default:a(()=>{var e;return[l(s(((e=n.value)==null?void 0:e.grams_sell)??0),1)]}),_:1})]),_:1}),E("div",R,[o(u,{color:"blue"},{default:a(()=>{var e,t;return[l("مشتریان معرفی‌شده: "+s(((t=(e=n.value)==null?void 0:e.referred_customers)==null?void 0:t.length)??0),1)]}),_:1}),o(u,{color:"green"},{default:a(()=>{var e,t;return[l("سفارش‌های ثبت‌شده: "+s(((t=(e=n.value)==null?void 0:e.orders)==null?void 0:t.length)??0),1)]}),_:1})])]),_:1}),o(N,{class:"mt-4","default-active-key":"referred"},{default:a(()=>[o(g,{key:"referred",tab:"مشتریان معرفی‌شده"},{default:a(()=>{var e;return[o(v,{size:"small","data-source":(e=n.value)==null?void 0:e.referred_customers,columns:C,pagination:{pageSize:10},"row-key":"id"},{bodyCell:a(({column:t,record:r})=>{var b;return[t.dataIndex==="active"?(i(),x(u,{key:0,color:r.active===1?"green":"red"},{default:a(()=>[l(s(r.active===1?"فعال":"غیر فعال"),1)]),_:2},1032,["color"])):t.dataIndex==="city"?(i(),p(f,{key:1},[l(s(((b=r.city)==null?void 0:b.title)??"-"),1)],64)):t.dataIndex==="caj"?(i(),p(f,{key:2},[l(s(I(j)(r.caj)),1)],64)):k("",!0)]}),_:1},8,["data-source"])]}),_:1}),o(g,{key:"orders",tab:"سفارش‌های مشتریان"},{default:a(()=>{var e;return[o(v,{size:"small","data-source":(e=n.value)==null?void 0:e.orders,columns:w,pagination:{pageSize:10},"row-key":"id"},{bodyCell:a(({column:t,record:r})=>[t.dataIndex==="caj"?(i(),p(f,{key:0},[l(s(I(j)(r.caj)),1)],64)):k("",!0)]),_:1},8,["data-source"])]}),_:1})]),_:1})]),_:1},8,["spinning"])}}}),O=M(T,[["__scopeId","data-v-726ef090"]]);export{O as default};
+1
View File
@@ -0,0 +1 @@
import{b as e,O as i,_ as m}from"./omdeListModel-DBaFXcO5.js";import{d as p,a,h as t,o as n}from"./index-DDKWZYPX.js";import"./model-DfYFtEav.js";import"./filters-CqJCHW3n.js";import"./index-FKca-jp6.js";import"./SettingOutlined-Bf9_Xgtn.js";import"./_plugin-vue_export-helper-DlAUqK2U.js";import"./ListPageHeader.vue_vue_type_script_setup_true_lang-D1N_8dtP.js";import"./AccountingSyncButton.vue_vue_type_script_setup_true_lang-NaZvq9ko.js";import"./bulkCustomer.vue_vue_type_script_setup_true_lang-DE79zKza.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-YIXFkb8N.js";import"./ImageUploadInput.vue_vue_type_script_setup_true_lang-DHQxooHi.js";import"./CityTreeSelect.vue_vue_type_script_setup_true_lang-BRAJxq2_.js";import"./DateInput-BFcDZlW_.js";import"./UnorderedListOutlined-_lhSZvn8.js";const D=p({__name:"BankerList",setup(s){const o=i.banker.title,r=e("banker");return(l,_)=>(n(),a(m,{title:t(o),model:t(r)},null,8,["title","model"]))}});export{D as default};
-1
View File
@@ -1 +0,0 @@
import{b as r,O as i,_ as m}from"./omdeListModel-B4MG3nAI.js";import{d as a,a as n,h as t,o as s}from"./index-DuH3OOPw.js";import"./model-DWeeFWcT.js";import"./SettingOutlined-MJAt5cqt.js";import"./_plugin-vue_export-helper-DlAUqK2U.js";import"./ListPageHeader.vue_vue_type_script_setup_true_lang-BaaZTuAN.js";import"./bulkCustomer.vue_vue_type_script_setup_true_lang-DgWWO9pL.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-zAo7Oyp5.js";import"./UnorderedListOutlined-7Y4Gu4Ut.js";const x=a({__name:"BankerList",setup(p){const e=i.banker.title,o=r("banker");return(l,_)=>(s(),n(m,{title:t(e),model:t(o)},null,8,["title","model"]))}});export{x as default};
@@ -1 +1 @@
import{d as r,a as g,w as o,r as a,o as k,c as l,V as x}from"./index-DuH3OOPw.js";const B=r({__name:"BaseFormModal",props:{open:{type:Boolean},id:{default:void 0},addTitle:{},editTitle:{},loading:{type:Boolean,default:!1},submitting:{type:Boolean,default:!1},width:{default:"md"},okText:{default:"ثبت اطلاعات"}},emits:["update:open","submit","cancel"],setup(e,{emit:d}){const n=d,s={sm:520,md:720,lg:900,xl:1140};function m(t){n("update:open",t)}function c(){n("update:open",!1),n("cancel")}return(t,i)=>{const f=a("a-form"),u=a("a-spin"),p=a("a-modal");return k(),g(p,{open:e.open,title:e.id?e.editTitle:e.addTitle,width:s[e.width],"confirm-loading":e.submitting,"ok-text":e.okText,"cancel-text":"انصراف","mask-closable":!1,"onUpdate:open":m,onOk:i[0]||(i[0]=T=>t.$emit("submit")),onCancel:c},{default:o(()=>[l(u,{spinning:e.loading},{default:o(()=>[l(f,{layout:"vertical"},{default:o(()=>[x(t.$slots,"default")]),_:3})]),_:3},8,["spinning"])]),_:3},8,["open","title","width","confirm-loading","ok-text"])}}});export{B as _};
import{d as r,a as g,w as o,r as a,o as k,c as l,Y as x}from"./index-DDKWZYPX.js";const B=r({__name:"BaseFormModal",props:{open:{type:Boolean},id:{default:void 0},addTitle:{},editTitle:{},loading:{type:Boolean,default:!1},submitting:{type:Boolean,default:!1},width:{default:"md"},okText:{default:"ثبت اطلاعات"}},emits:["update:open","submit","cancel"],setup(e,{emit:d}){const n=d,s={sm:520,md:720,lg:900,xl:1140};function m(t){n("update:open",t)}function c(){n("update:open",!1),n("cancel")}return(t,i)=>{const f=a("a-form"),u=a("a-spin"),p=a("a-modal");return k(),g(p,{open:e.open,title:e.id?e.editTitle:e.addTitle,width:s[e.width],"confirm-loading":e.submitting,"ok-text":e.okText,"cancel-text":"انصراف","mask-closable":!1,"onUpdate:open":m,onOk:i[0]||(i[0]=T=>t.$emit("submit")),onCancel:c},{default:o(()=>[l(u,{spinning:e.loading},{default:o(()=>[l(f,{layout:"vertical"},{default:o(()=>[x(t.$slots,"default")]),_:3})]),_:3},8,["spinning"])]),_:3},8,["open","title","width","confirm-loading","ok-text"])}}});export{B as _};
@@ -1 +1 @@
import{d as P,v as S,x as u,b as m,c as o,w as l,l as _,r as c,y as T,o as d,n as $,F as w,e as C,f as g,z,M as k,a as x,t as B}from"./index-DuH3OOPw.js";const D={id:"page-user-list"},E={class:"grid-3"},A=P({__name:"ChangePercent",setup(j){const y=_([]),b=_([]),p=_("-"),f=_(!1),a=T({category_id:void 0,brand_id:void 0,percent:void 0});function h(t,e){var r,s;return String(((s=(r=e==null?void 0:e.children)==null?void 0:r[0])==null?void 0:s.children)??"").toLowerCase().indexOf(t.toLowerCase())>=0}async function v(){try{const{data:t}=await u.post("change/percent/count",{category_id:a.category_id,brand_id:a.brand_id});p.value=t.count}catch(t){console.log("e ==> ",t)}}async function L(){if(a.percent===void 0||a.percent===null){z.error("لطفا اجرت درصدی را وارد کنید");return}try{f.value=!0;const{data:t}=await u.post("change/all",{percent:a.percent,category_id:a.category_id,brand_id:a.brand_id});k.success({title:"تغییرات با موفقیت اعمال شد",okText:"بستن"}),p.value=t.updated}catch(t){k.success({title:"خطا در اعمال تغییرات"}),console.log(t)}finally{f.value=!1}}return S(async()=>{try{const[t,e]=await Promise.all([u.get("brand"),u.post("category")]);y.value=t.data,b.value=(e.data.rows??e.data).map(r=>({id:r.id,title:r.type_str?`${r.title} (${r.type_str}) `:r.title}))}catch(t){console.log(t)}v()}),(t,e)=>{const r=c("a-select-option"),s=c("a-select"),i=c("a-form-item"),M=c("a-input"),N=c("a-input-number"),U=c("a-button"),V=c("a-popconfirm"),F=c("a-form"),O=c("a-card");return d(),m("div",D,[o(O,{title:"تغییر درصد محصولات"},{default:l(()=>[o(F,{layout:"vertical"},{default:l(()=>[$("div",E,[o(i,{label:"دسته بندی"},{default:l(()=>[o(s,{value:a.category_id,"onUpdate:value":e[0]||(e[0]=n=>a.category_id=n),"allow-clear":"","show-search":"",placeholder:"انتخاب کنید","filter-option":h,onChange:v},{default:l(()=>[(d(!0),m(w,null,C(b.value,n=>(d(),x(r,{key:n.id,value:n.id},{default:l(()=>[g(B(n.title),1)]),_:2},1032,["value"]))),128))]),_:1},8,["value"])]),_:1}),o(i,{label:"برند"},{default:l(()=>[o(s,{value:a.brand_id,"onUpdate:value":e[1]||(e[1]=n=>a.brand_id=n),"allow-clear":"","show-search":"",placeholder:"انتخاب کنید","filter-option":h,onChange:v},{default:l(()=>[(d(!0),m(w,null,C(y.value,n=>(d(),x(r,{key:n.id,value:n.id},{default:l(()=>[g(B(n.title),1)]),_:2},1032,["value"]))),128))]),_:1},8,["value"])]),_:1}),o(i,{label:"تعداد یافت شده"},{default:l(()=>[o(M,{value:p.value,disabled:""},null,8,["value"])]),_:1}),o(i,{label:"اجرت درصدی"},{default:l(()=>[o(N,{value:a.percent,"onUpdate:value":e[2]||(e[2]=n=>a.percent=n),class:"w-full",step:.1},null,8,["value"])]),_:1})]),o(V,{title:"آیا از اعمال تغییرات مطمئن هستید؟","ok-text":"بله","cancel-text":"خیر",onConfirm:L},{default:l(()=>[o(U,{type:"primary",loading:f.value},{default:l(()=>[...e[3]||(e[3]=[g("اعمال تغییرات",-1)])]),_:1},8,["loading"])]),_:1})]),_:1})]),_:1})])}}});export{A as default};
import{d as P,v as S,x as u,b as m,c as o,w as l,l as _,r as c,y as T,o as d,n as $,F as w,e as C,f as g,z,M as k,a as x,t as B}from"./index-DDKWZYPX.js";const D={id:"page-user-list"},E={class:"grid-3"},A=P({__name:"ChangePercent",setup(j){const y=_([]),b=_([]),p=_("-"),f=_(!1),a=T({category_id:void 0,brand_id:void 0,percent:void 0});function h(t,e){var r,s;return String(((s=(r=e==null?void 0:e.children)==null?void 0:r[0])==null?void 0:s.children)??"").toLowerCase().indexOf(t.toLowerCase())>=0}async function v(){try{const{data:t}=await u.post("change/percent/count",{category_id:a.category_id,brand_id:a.brand_id});p.value=t.count}catch(t){console.log("e ==> ",t)}}async function L(){if(a.percent===void 0||a.percent===null){z.error("لطفا اجرت درصدی را وارد کنید");return}try{f.value=!0;const{data:t}=await u.post("change/all",{percent:a.percent,category_id:a.category_id,brand_id:a.brand_id});k.success({title:"تغییرات با موفقیت اعمال شد",okText:"بستن"}),p.value=t.updated}catch(t){k.success({title:"خطا در اعمال تغییرات"}),console.log(t)}finally{f.value=!1}}return S(async()=>{try{const[t,e]=await Promise.all([u.get("brand"),u.post("category")]);y.value=t.data,b.value=(e.data.rows??e.data).map(r=>({id:r.id,title:r.type_str?`${r.title} (${r.type_str}) `:r.title}))}catch(t){console.log(t)}v()}),(t,e)=>{const r=c("a-select-option"),s=c("a-select"),i=c("a-form-item"),M=c("a-input"),N=c("a-input-number"),U=c("a-button"),V=c("a-popconfirm"),F=c("a-form"),O=c("a-card");return d(),m("div",D,[o(O,{title:"تغییر درصد محصولات"},{default:l(()=>[o(F,{layout:"vertical"},{default:l(()=>[$("div",E,[o(i,{label:"دسته بندی"},{default:l(()=>[o(s,{value:a.category_id,"onUpdate:value":e[0]||(e[0]=n=>a.category_id=n),"allow-clear":"","show-search":"",placeholder:"انتخاب کنید","filter-option":h,onChange:v},{default:l(()=>[(d(!0),m(w,null,C(b.value,n=>(d(),x(r,{key:n.id,value:n.id},{default:l(()=>[g(B(n.title),1)]),_:2},1032,["value"]))),128))]),_:1},8,["value"])]),_:1}),o(i,{label:"برند"},{default:l(()=>[o(s,{value:a.brand_id,"onUpdate:value":e[1]||(e[1]=n=>a.brand_id=n),"allow-clear":"","show-search":"",placeholder:"انتخاب کنید","filter-option":h,onChange:v},{default:l(()=>[(d(!0),m(w,null,C(y.value,n=>(d(),x(r,{key:n.id,value:n.id},{default:l(()=>[g(B(n.title),1)]),_:2},1032,["value"]))),128))]),_:1},8,["value"])]),_:1}),o(i,{label:"تعداد یافت شده"},{default:l(()=>[o(M,{value:p.value,disabled:""},null,8,["value"])]),_:1}),o(i,{label:"اجرت درصدی"},{default:l(()=>[o(N,{value:a.percent,"onUpdate:value":e[2]||(e[2]=n=>a.percent=n),class:"w-full",step:.1},null,8,["value"])]),_:1})]),o(V,{title:"آیا از اعمال تغییرات مطمئن هستید؟","ok-text":"بله","cancel-text":"خیر",onConfirm:L},{default:l(()=>[o(U,{type:"primary",loading:f.value},{default:l(()=>[...e[3]||(e[3]=[g("اعمال تغییرات",-1)])]),_:1},8,["loading"])]),_:1})]),_:1})]),_:1})])}}});export{A as default};
@@ -0,0 +1 @@
import{d as m,o as v,a as g,p as w,r as k}from"./index-DDKWZYPX.js";const _=m({__name:"CityTreeSelect",props:{value:{},cities:{},parentPicker:{type:Boolean},excludeId:{},placeholder:{}},emits:["update:value","change"],setup(i,{emit:u}){const n=i,d=u,h=w(()=>{const l=new Set(n.cities.map(e=>e.id)),o=new Map;for(const e of n.cities){const a=e.parent&&l.has(e.parent)&&e.parent!==e.id?e.parent:null;o.has(a)||o.set(a,[]),o.get(a).push(e)}const r=new Set,p=(e,a)=>(o.get(e)??[]).filter(t=>!r.has(t.id)&&!(n.parentPicker&&t.id===n.excludeId)).map(t=>{r.add(t.id);const s=p(t.id,a+1),c={value:t.id,key:t.id,title:t.title};return s.length&&(c.children=s),!n.parentPicker&&a===0&&s.length&&(c.disabled=!0),c});return p(null,0)});function f(l){d("update:value",l??void 0),d("change",l??void 0)}return(l,o)=>{const r=k("a-tree-select");return v(),g(r,{value:i.value??void 0,"tree-data":h.value,"show-search":"","allow-clear":"","tree-default-expand-all":"","tree-node-filter-prop":"title","dropdown-style":{maxHeight:"400px",overflow:"auto"},placeholder:i.placeholder??"شهر را انتخاب کنید",class:"w-full","onUpdate:value":f},null,8,["value","tree-data","placeholder"])}}});export{_};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.row[data-v-580880e4]{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap}.tags[data-v-580880e4]{display:flex;gap:.5rem;flex-wrap:wrap}.slide[data-v-580880e4]{border:1px solid #f0f0f0;border-radius:6px;padding:.75rem;margin-bottom:.75rem}.mobile-c[data-v-580880e4]{margin:0 0 .5rem}.mt-4[data-v-580880e4]{margin-top:1rem}
-1
View File
@@ -1 +0,0 @@
.row[data-v-60fd2863]{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap}.tags[data-v-60fd2863]{display:flex;gap:.5rem;flex-wrap:wrap}.slide[data-v-60fd2863]{border:1px solid #f0f0f0;border-radius:6px;padding:.75rem;margin-bottom:.75rem}.mobile-c[data-v-60fd2863]{margin:0 0 .5rem}.mt-4[data-v-60fd2863]{margin-top:1rem}
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{c as u,I as l}from"./index-DuH3OOPw.js";var d={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-792 72h752v120H136V232zm752 560H136V440h752v352zm-237-64h165c4.4 0 8-3.6 8-8v-72c0-4.4-3.6-8-8-8H651c-4.4 0-8 3.6-8 8v72c0 4.4 3.6 8 8 8z"}}]},name:"credit-card",theme:"outlined"};function c(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){o(r,a,e[a])})}return r}function o(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var i=function(t,e){var n=c({},t,e.attrs);return u(l,c({},n,{icon:d}),null)};i.displayName="CreditCardOutlined";i.inheritAttrs=!1;export{i as C};
import{c as u,I as l}from"./index-DDKWZYPX.js";var d={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-792 72h752v120H136V232zm752 560H136V440h752v352zm-237-64h165c4.4 0 8-3.6 8-8v-72c0-4.4-3.6-8-8-8H651c-4.4 0-8 3.6-8 8v72c0 4.4 3.6 8 8 8z"}}]},name:"credit-card",theme:"outlined"};function c(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){o(r,a,e[a])})}return r}function o(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var i=function(t,e){var n=c({},t,e.attrs);return u(l,c({},n,{icon:d}),null)};i.displayName="CreditCardOutlined";i.inheritAttrs=!1;export{i as C};
@@ -0,0 +1 @@
.mt-4[data-v-3185cd59]{margin-top:1rem}.mt-2[data-v-3185cd59]{margin-top:.5rem}.ms-2[data-v-3185cd59]{margin-inline-start:.5rem}.muted[data-v-3185cd59]{color:#888}.small[data-v-3185cd59]{font-size:12px}.warn[data-v-3185cd59]{color:#d4380d}.debt[data-v-3185cd59]{color:#cf1322}.credit[data-v-3185cd59]{color:#389e0d}.stats[data-v-6d44e777]{display:flex;gap:.5rem}.mt-4[data-v-6d44e777]{margin-top:1rem}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
.stats[data-v-9dee124b]{display:flex;gap:.5rem}.mt-4[data-v-9dee124b]{margin-top:1rem}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.stat-card[data-v-2ccaa937]{height:100%}.stat-row[data-v-2ccaa937]{display:flex;align-items:center;gap:1rem}.stat-icon[data-v-2ccaa937]{display:inline-flex;align-items:center;justify-content:center;width:48px;height:48px;border-radius:50%;font-size:22px;flex-shrink:0}.icon-primary[data-v-2ccaa937]{background:#1c2d561a;color:#1c2d56}.icon-warning[data-v-2ccaa937]{background:#ff9f431f;color:#ff9f43}.icon-success[data-v-2ccaa937]{background:#28c76f1f;color:#28c76f}.icon-danger[data-v-2ccaa937]{background:#ea54551f;color:#ea5455}.stat-value[data-v-2ccaa937]{margin:0;font-size:20px;color:#1c2d56}.stat-title[data-v-2ccaa937]{font-size:12px;color:#888}.mt-4[data-v-585a18f1]{margin-top:1rem}.mt-3[data-v-585a18f1]{margin-top:.75rem}
@@ -1 +0,0 @@
import{j as y}from"./filters-CWowq3Uw.js";import{d as b,v as k,x as C,a as l,w as t,l as I,y as j,r as e,o,c as d,f as i,t as _,b as v,h as w,F as h,i as B}from"./index-DuH3OOPw.js";import"./index-BHIiPvAE.js";const F=b({__name:"DashboardAnalytics",setup(N){const n=I(!1),r=j({lastOrder:[]}),m=[{title:"شماره سفارش",dataIndex:"name"},{title:"مشتری",dataIndex:"customer"},{title:"وزن",dataIndex:"w"},{title:"وضعیت",dataIndex:"sts_str"},{title:"تاریخ",dataIndex:"caj"}];function p(a){return{success:"green",warning:"orange",danger:"red",primary:"blue"}[a]??""}return k(async()=>{try{n.value=!0;const{data:a}=await C.get("get-analytics-tak");Object.assign(r,a)}catch(a){console.error(a)}finally{n.value=!1}}),(a,O)=>{const u=e("a-tag"),g=e("a-table"),f=e("a-card"),x=e("a-spin");return o(),l(x,{spinning:n.value},{default:t(()=>[d(f,{title:"سفارشات هفته جاری"},{default:t(()=>[d(g,{size:"small","data-source":r.lastOrder,columns:m,pagination:{pageSize:15},"row-key":"id"},{bodyCell:t(({column:c,record:s})=>[c.dataIndex==="sts_str"?(o(),l(u,{key:0,color:p(s.statusColor)},{default:t(()=>[i(_(s.sts_str),1)]),_:2},1032,["color"])):c.dataIndex==="caj"?(o(),v(h,{key:1},[i(_(w(y)(s.caj)),1)],64)):B("",!0)]),_:1},8,["data-source"])]),_:1})]),_:1},8,["spinning"])}}});export{F as default};
@@ -1 +0,0 @@
import{d as y,o as d,a as _,w as e,n,s as N,g as D,t as c,p as k,r as o,v as $,x as j,l as L,y as z,c as a,f as u,b as T,h as V,F as E,i as M}from"./index-DuH3OOPw.js";import{B as v,D as F,S as U,T as q}from"./TeamOutlined-CEYpODLG.js";import{_ as O}from"./_plugin-vue_export-helper-DlAUqK2U.js";import{j as A}from"./filters-CWowq3Uw.js";import"./index-BHIiPvAE.js";const G={class:"stat-row"},H={class:"stat-value"},J={class:"stat-title"},K=y({__name:"StatisticsCardLine",props:{icon:{},statistic:{},statisticTitle:{},color:{default:"primary"},hideChart:{type:Boolean,default:!0}},setup(i){const l=i,t={UsersIcon:q,ShoppingBagIcon:U,DollarSignIcon:F,BarChartIcon:v},p=k(()=>t[l.icon??""]??v);return(C,I)=>{const s=o("a-card");return d(),_(s,{class:"stat-card"},{default:e(()=>[n("div",G,[n("span",{class:N(["stat-icon",`icon-${i.color}`])},[(d(),_(D(p.value)))],2),n("div",null,[n("h2",H,c(i.statistic),1),n("span",J,c(i.statisticTitle),1)])])]),_:1})}}}),h=O(K,[["__scopeId","data-v-ebefdc7b"]]),P={class:"grid-3"},Q={class:"grid-2 mt-4"},R=y({__name:"DashboardECommerce",setup(i){const l=L(!1),t=z({customerCount:0,allgram:0,ordercount:0,reagent:[],lastOrder:[],sumLastOrder:0,sumLastOrderMonth:0}),p=[{title:"کارشناس فروش",dataIndex:"name"},{title:"میزان فروش (گرم)",dataIndex:"grams_sell"},{title:"تعداد سفارش",dataIndex:"order_count"}],C=[{title:"شماره سفارش",dataIndex:"name"},{title:"مشتری",dataIndex:"customer"},{title:"وزن",dataIndex:"w"},{title:"وضعیت",dataIndex:"sts_str"},{title:"تاریخ",dataIndex:"caj"}];function I(s){return{success:"green",warning:"orange",danger:"red",primary:"blue"}[s]??""}return $(async()=>{try{l.value=!0;const{data:s}=await j.get("get-analytics");Object.assign(t,s),t.reagent.sort((m,r)=>Number(r.grams_sell)-Number(m.grams_sell)),t.lastOrder.sort((m,r)=>Number(r.w)-Number(m.w))}catch(s){console.error(s)}finally{l.value=!1}}),(s,m)=>{const r=o("a-table"),g=o("a-card"),b=o("a-descriptions-item"),w=o("a-descriptions"),B=o("a-tag"),S=o("a-spin");return d(),_(S,{spinning:l.value},{default:e(()=>[n("div",P,[a(h,{icon:"UsersIcon",color:"warning",statistic:`${t.customerCount} نفر `,"statistic-title":"تعداد مشتریان ثبت نامی در 30 روز گذشته"},null,8,["statistic"]),a(h,{icon:"ShoppingBagIcon",statistic:`${t.allgram} گرم `,"statistic-title":"طلای فروش رفته در 30 روز گذشته"},null,8,["statistic"]),a(h,{icon:"ShoppingBagIcon",color:"warning",statistic:`${t.ordercount} سفارش `,"statistic-title":"تعداد سفارشات 30 روز گذشته"},null,8,["statistic"])]),n("div",Q,[a(g,{title:"عملکرد کارشناسان فروش"},{default:e(()=>[a(r,{size:"small","data-source":t.reagent,columns:p,pagination:!1,"row-key":"id"},null,8,["data-source"])]),_:1}),a(g,{title:"میزان فروش"},{default:e(()=>[a(w,{bordered:"",size:"small",column:1},{default:e(()=>[a(b,{label:"تعداد سفارش (۳۰ روز)"},{default:e(()=>[u(c(t.sumLastOrder),1)]),_:1}),a(b,{label:"مجموع وزن (۳۰ روز)"},{default:e(()=>[u(c(t.sumLastOrderMonth)+" گرم ",1)]),_:1})]),_:1})]),_:1})]),a(g,{title:"سفارشات هفته جاری",class:"mt-4"},{default:e(()=>[a(r,{size:"small","data-source":t.lastOrder,columns:C,pagination:{pageSize:10},"row-key":"id"},{bodyCell:e(({column:x,record:f})=>[x.dataIndex==="sts_str"?(d(),_(B,{key:0,color:I(f.statusColor)},{default:e(()=>[u(c(f.sts_str),1)]),_:2},1032,["color"])):x.dataIndex==="caj"?(d(),T(E,{key:1},[u(c(V(A)(f.caj)),1)],64)):M("",!0)]),_:1},8,["data-source"])]),_:1})]),_:1},8,["spinning"])}}}),at=O(R,[["__scopeId","data-v-a6b0d1e2"]]);export{at as default};
@@ -1 +0,0 @@
.stat-card[data-v-ebefdc7b]{height:100%}.stat-row[data-v-ebefdc7b]{display:flex;align-items:center;gap:1rem}.stat-icon[data-v-ebefdc7b]{display:inline-flex;align-items:center;justify-content:center;width:48px;height:48px;border-radius:50%;font-size:22px;flex-shrink:0}.icon-primary[data-v-ebefdc7b]{background:#1c2d561a;color:#1c2d56}.icon-warning[data-v-ebefdc7b]{background:#ff9f431f;color:#ff9f43}.icon-success[data-v-ebefdc7b]{background:#28c76f1f;color:#28c76f}.icon-danger[data-v-ebefdc7b]{background:#ea54551f;color:#ea5455}.stat-value[data-v-ebefdc7b]{margin:0;font-size:20px;color:#1c2d56}.stat-title[data-v-ebefdc7b]{font-size:12px;color:#888}.mt-4[data-v-a6b0d1e2]{margin-top:1rem}
@@ -1 +1 @@
import{m}from"./index-BHIiPvAE.js";import{d as _,m as V,o as r,a as Y,w as h,c as D,b,t as j,i as M,l as i,p as x,r as d}from"./index-DuH3OOPw.js";import{_ as y}from"./_plugin-vue_export-helper-DlAUqK2U.js";const k={key:0,class:"hint"},C=_({__name:"DateInput",props:{label:{},modelValue:{},placeholder:{default:"مثال: ۱۴۰۳/۰۵/۱۲"}},emits:["update:modelValue"],setup(o,{emit:p}){const n=o,u=p,t=i(void 0),a=i(""),f=x(()=>{if(t.value!==void 0)return t.value;if(!n.modelValue)return"";const e=m(n.modelValue);return e.isValid()?e.format("jYYYY/jMM/jDD"):""});V(()=>n.modelValue,()=>{t.value=void 0});function v(e){if(t.value=e,!e){a.value="",u("update:modelValue",void 0);return}const c=e.replace(/[۰-۹]/g,s=>String("۰۱۲۳۴۵۶۷۸۹".indexOf(s))),l=m(c,"jYYYY/jMM/jDD");l.isValid()?(a.value="",u("update:modelValue",l.format("YYYY-MM-DD"))):a.value="تاریخ معتبر نیست"}return(e,c)=>{const l=d("a-input"),s=d("a-form-item");return r(),Y(s,{label:o.label},{default:h(()=>[D(l,{value:f.value,placeholder:o.placeholder,"allow-clear":"","onUpdate:value":v},null,8,["value","placeholder"]),a.value?(r(),b("small",k,j(a.value),1)):M("",!0)]),_:1},8,["label"])}}}),w=y(C,[["__scopeId","data-v-42b77d70"]]);export{w as D};
import{m}from"./index-FKca-jp6.js";import{d as _,m as V,o as r,a as Y,w as h,c as D,b,t as j,i as M,l as i,p as x,r as d}from"./index-DDKWZYPX.js";import{_ as y}from"./_plugin-vue_export-helper-DlAUqK2U.js";const k={key:0,class:"hint"},C=_({__name:"DateInput",props:{label:{},modelValue:{},placeholder:{default:"مثال: ۱۴۰۳/۰۵/۱۲"}},emits:["update:modelValue"],setup(o,{emit:p}){const n=o,c=p,t=i(void 0),a=i(""),f=x(()=>{if(t.value!==void 0)return t.value;if(!n.modelValue)return"";const e=m(n.modelValue);return e.isValid()?e.format("jYYYY/jMM/jDD"):""});V(()=>n.modelValue,()=>{t.value=void 0});function v(e){if(t.value=e,!e){a.value="",c("update:modelValue",void 0);return}const u=e.replace(/[۰-۹]/g,s=>String("۰۱۲۳۴۵۶۷۸۹".indexOf(s))),l=m(u,"jYYYY/jMM/jDD");l.isValid()?(a.value="",c("update:modelValue",l.format("YYYY-MM-DD"))):a.value="تاریخ معتبر نیست"}return(e,u)=>{const l=d("a-input"),s=d("a-form-item");return r(),Y(s,{label:o.label},{default:h(()=>[D(l,{value:f.value,placeholder:o.placeholder,"allow-clear":"","onUpdate:value":v},null,8,["value","placeholder"]),a.value?(r(),b("small",k,j(a.value),1)):M("",!0)]),_:1},8,["label"])}}}),w=y(C,[["__scopeId","data-v-3863c3bc"]]);export{w as D};
+1
View File
@@ -0,0 +1 @@
.hint[data-v-3863c3bc]{color:#ea5455;font-size:11px}
-1
View File
@@ -1 +0,0 @@
.hint[data-v-42b77d70]{color:#ea5455;font-size:11px}
+1
View File
@@ -0,0 +1 @@
import{_ as o}from"./Add.vue_vue_type_script_setup_true_lang-CjPh6jcZ.js";import{d as t,a as e,o as r}from"./index-DDKWZYPX.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-YIXFkb8N.js";import"./ImageUploadInput.vue_vue_type_script_setup_true_lang-DHQxooHi.js";const i=t({__name:"Edit",setup(a){return(_,m)=>(r(),e(o))}});export{i as default};
-1
View File
@@ -1 +0,0 @@
import{_ as o}from"./Add.vue_vue_type_script_setup_true_lang-vc2KlLTI.js";import{d as t,a as e,o as r}from"./index-DuH3OOPw.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-zAo7Oyp5.js";import"./ImageUploadInput.vue_vue_type_script_setup_true_lang-BTbAO94-.js";const i=t({__name:"Edit",setup(a){return(_,m)=>(r(),e(o))}});export{i as default};
-1
View File
@@ -1 +0,0 @@
import{_ as e}from"./AddBranches.vue_vue_type_script_setup_true_lang-BQagavGv.js";import{d as o,a,o as r}from"./index-DuH3OOPw.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-zAo7Oyp5.js";const p=o({__name:"EditBranches",setup(t){return(n,s)=>(r(),a(e))}});export{p as default};
+1
View File
@@ -0,0 +1 @@
import{_ as e}from"./AddBranches.vue_vue_type_script_setup_true_lang-CQi_1IC6.js";import{d as o,a,o as r}from"./index-DDKWZYPX.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-YIXFkb8N.js";const p=o({__name:"EditBranches",setup(t){return(n,s)=>(r(),a(e))}});export{p as default};
@@ -1 +0,0 @@
import{_ as o}from"./bulkCustomer.vue_vue_type_script_setup_true_lang-DgWWO9pL.js";import{d as e,a as t,o as r}from"./index-DuH3OOPw.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-zAo7Oyp5.js";const p=e({__name:"EditBulkCustomer",setup(a){return(s,_)=>(r(),t(o))}});export{p as default};
@@ -0,0 +1 @@
import{_ as o}from"./bulkCustomer.vue_vue_type_script_setup_true_lang-DE79zKza.js";import{d as t,a as r,o as m}from"./index-DDKWZYPX.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-YIXFkb8N.js";import"./ImageUploadInput.vue_vue_type_script_setup_true_lang-DHQxooHi.js";import"./CityTreeSelect.vue_vue_type_script_setup_true_lang-BRAJxq2_.js";import"./DateInput-BFcDZlW_.js";import"./index-FKca-jp6.js";import"./_plugin-vue_export-helper-DlAUqK2U.js";const l=t({__name:"EditBulkCustomer",setup(e){return(p,a)=>(m(),r(o))}});export{l as default};
-1
View File
@@ -1 +0,0 @@
import o from"./AddInvoice-CkxeyCUi.js";import{d as t,a as e,o as r}from"./index-DuH3OOPw.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-zAo7Oyp5.js";import"./model-DWeeFWcT.js";import"./SettingOutlined-MJAt5cqt.js";import"./_plugin-vue_export-helper-DlAUqK2U.js";const f=t({__name:"EditInvoice",setup(p){return(i,m)=>(r(),e(o))}});export{f as default};
+1
View File
@@ -0,0 +1 @@
import o from"./AddInvoice-CThPsK78.js";import{d as t,a as r,o as e}from"./index-DDKWZYPX.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-YIXFkb8N.js";import"./model-DfYFtEav.js";import"./filters-CqJCHW3n.js";import"./index-FKca-jp6.js";import"./SettingOutlined-Bf9_Xgtn.js";import"./_plugin-vue_export-helper-DlAUqK2U.js";const l=t({__name:"EditInvoice",setup(p){return(i,m)=>(e(),r(o))}});export{l as default};
+1
View File
@@ -0,0 +1 @@
import e from"./AddProduct-BSEl8ECX.js";import{d as o,b as t,c as r,o as c}from"./index-DDKWZYPX.js";import"./ImageUploadInput.vue_vue_type_script_setup_true_lang-DHQxooHi.js";import"./_plugin-vue_export-helper-DlAUqK2U.js";const _=o({__name:"EditProduct",setup(a){return(n,d)=>(c(),t("div",null,[r(e)]))}});export{_ as default};
-1
View File
@@ -1 +0,0 @@
import e from"./AddProduct-Dx2YzUG6.js";import{d as o,b as t,c as r,o as c}from"./index-DuH3OOPw.js";import"./ImageUploadInput.vue_vue_type_script_setup_true_lang-BTbAO94-.js";import"./_plugin-vue_export-helper-DlAUqK2U.js";const _=o({__name:"EditProduct",setup(a){return(n,d)=>(c(),t("div",null,[r(e)]))}});export{_ as default};
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{R as e}from"./ReagentCustomer-6o67xE2d.js";import{d as t,a as o,o as r}from"./index-DDKWZYPX.js";import"./CityTreeSelect.vue_vue_type_script_setup_true_lang-BRAJxq2_.js";import"./ImageUploadInput.vue_vue_type_script_setup_true_lang-DHQxooHi.js";import"./_plugin-vue_export-helper-DlAUqK2U.js";const f=t({__name:"EditReagent",setup(a){return(m,n)=>(r(),o(e))}});export{f as default};
-1
View File
@@ -1 +0,0 @@
.avatar-cell[data-v-03ed8085]{grid-row:span 2;display:flex;justify-content:center}
@@ -1 +0,0 @@
import{d as C,j as N,k as V,v as D,x as m,r,o as p,a as _,w as i,c as s,A as F,h as M,n as z,b as A,F as I,e as L,f as b,t as Q,l as w,y as k,z as x}from"./index-DuH3OOPw.js";const T={class:"grid-3"},f="لطفا مقدار را وارد کنید",q=C({__name:"singleCustomer",setup(S){const v=N(),h=V(),c=v.params.id,d=w(!1),y=w([]),a=k({fname:"",lname:"",phone:"",city_id:void 0}),l=k({});function j(t,e){var n,u;return String(((u=(n=e==null?void 0:e.children)==null?void 0:n[0])==null?void 0:u.children)??"").includes(t)}async function g(){var t,e;if(Object.keys(l).forEach(n=>delete l[n]),a.fname||(l.fname=f),a.lname||(l.lname=f),a.phone||(l.phone=f),!Object.keys(l).length)try{d.value=!0;const n={...a,name:`${a.fname} ${a.lname}`.trim(),mobile:a.phone};c?await m.put(`single/${c}`,n):await m.post("single",n),x.info("اطلاعات با موفقیت به ثبت رسید."),h.go(-1)}catch(n){x.error(((e=(t=n==null?void 0:n.response)==null?void 0:t.data)==null?void 0:e.message)??"خطا در ثبت اطلاعات.")}finally{d.value=!1}}return D(async()=>{try{const{data:t}=await m.post("cities/list");y.value=t.rows??[]}catch(t){console.log(t)}if(c)try{const{data:t}=await m.get(`customer/${c}`),e=String(t.name??"").trim().split(" ");Object.assign(a,{fname:t.fname||e[0]||"",lname:t.lname||e.slice(1).join(" "),phone:t.mobile??"",city_id:t.city_id??void 0})}catch(t){console.log(t)}}),(t,e)=>{const n=r("a-input"),u=r("a-form-item"),E=r("a-select-option"),U=r("a-select"),$=r("a-button"),B=r("a-card"),O=r("a-form"),R=r("a-spin");return p(),_(R,{spinning:d.value},{default:i(()=>[s(O,{layout:"vertical",onSubmit:F(g,["prevent"])},{default:i(()=>[s(B,{title:M(c)?"ویرایش مشتری":"افزودن مشتری",class:"mb-base"},{default:i(()=>[z("div",T,[s(u,{label:"نام ","validate-status":l.fname?"error":"",help:l.fname},{default:i(()=>[s(n,{value:a.fname,"onUpdate:value":e[0]||(e[0]=o=>a.fname=o)},null,8,["value"])]),_:1},8,["validate-status","help"]),s(u,{label:"نام خانوادگی","validate-status":l.lname?"error":"",help:l.lname},{default:i(()=>[s(n,{value:a.lname,"onUpdate:value":e[1]||(e[1]=o=>a.lname=o)},null,8,["value"])]),_:1},8,["validate-status","help"]),s(u,{label:"شماره تلفن همراه","validate-status":l.phone?"error":"",help:l.phone},{default:i(()=>[s(n,{value:a.phone,"onUpdate:value":e[2]||(e[2]=o=>a.phone=o)},null,8,["value"])]),_:1},8,["validate-status","help"]),s(u,{label:"شهر"},{default:i(()=>[s(U,{value:a.city_id,"onUpdate:value":e[3]||(e[3]=o=>a.city_id=o),"show-search":"","allow-clear":"",placeholder:"انتخاب کنید","filter-option":j},{default:i(()=>[(p(!0),A(I,null,L(y.value,o=>(p(),_(E,{key:o.id,value:o.id},{default:i(()=>[b(Q(o.title),1)]),_:2},1032,["value"]))),128))]),_:1},8,["value"])]),_:1})]),s($,{type:"primary",onClick:g},{default:i(()=>[...e[4]||(e[4]=[b("ثبت اطلاعات",-1)])]),_:1})]),_:1},8,["title"])]),_:1})]),_:1},8,["spinning"])}}}),H=C({__name:"EditSingleCustomer",setup(S){return(v,h)=>(p(),_(q))}});export{H as default};
@@ -0,0 +1 @@
import{_ as o}from"./singleCustomer.vue_vue_type_script_setup_true_lang-yVXlCX4v.js";import{d as t,a as r,o as e}from"./index-DDKWZYPX.js";import"./CityTreeSelect.vue_vue_type_script_setup_true_lang-BRAJxq2_.js";import"./DateInput-BFcDZlW_.js";import"./index-FKca-jp6.js";import"./_plugin-vue_export-helper-DlAUqK2U.js";const u=t({__name:"EditSingleCustomer",setup(m){return(a,p)=>(e(),r(o))}});export{u as default};
-1
View File
@@ -1 +0,0 @@
import{_ as o}from"./AddWorkshop.vue_vue_type_script_setup_true_lang-Ddx471SB.js";import{d as e,a as r,o as t}from"./index-DuH3OOPw.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-zAo7Oyp5.js";const m=e({__name:"EditWorkshop",setup(a){return(s,_)=>(t(),r(o))}});export{m as default};
+1
View File
@@ -0,0 +1 @@
import{_ as o}from"./AddWorkshop.vue_vue_type_script_setup_true_lang-CVuc-ZrE.js";import{d as e,a as r,o as t}from"./index-DDKWZYPX.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-YIXFkb8N.js";const m=e({__name:"EditWorkshop",setup(a){return(s,_)=>(t(),r(o))}});export{m as default};
@@ -1 +1 @@
import{_}from"./_plugin-vue_export-helper-DlAUqK2U.js";import{b as c,c as e,w as o,f as l,r as s,o as p}from"./index-DuH3OOPw.js";const u={},d={class:"full-page-msg"};function i(r,t){const n=s("a-button"),a=s("a-result");return p(),c("div",d,[e(a,{status:"404",title:"صفحه مورد نظر یافت نشد","sub-title":"آدرسی که وارد کرده‌اید وجود ندارد."},{extra:o(()=>[e(n,{type:"primary",onClick:t[0]||(t[0]=m=>r.$router.push("/"))},{default:o(()=>[...t[1]||(t[1]=[l("بازگشت به خانه",-1)])]),_:1})]),_:1})])}const b=_(u,[["render",i],["__scopeId","data-v-4851d61e"]]);export{b as default};
import{_}from"./_plugin-vue_export-helper-DlAUqK2U.js";import{b as c,c as e,w as o,f as l,r as s,o as p}from"./index-DDKWZYPX.js";const u={},i={class:"full-page-msg"};function m(r,t){const a=s("a-button"),n=s("a-result");return p(),c("div",i,[e(n,{status:"404",title:"صفحه مورد نظر یافت نشد","sub-title":"آدرسی که وارد کرده‌اید وجود ندارد."},{extra:o(()=>[e(a,{type:"primary",onClick:t[0]||(t[0]=d=>r.$router.push("/"))},{default:o(()=>[...t[1]||(t[1]=[l("بازگشت به خانه",-1)])]),_:1})]),_:1})])}const b=_(u,[["render",m],["__scopeId","data-v-82a95474"]]);export{b as default};
-1
View File
@@ -1 +0,0 @@
.full-page-msg[data-v-4851d61e]{min-height:100vh;display:flex;align-items:center;justify-content:center}
+1
View File
@@ -0,0 +1 @@
.full-page-msg[data-v-82a95474]{min-height:100vh;display:flex;align-items:center;justify-content:center}
-1
View File
@@ -1 +0,0 @@
.full-page-msg[data-v-abbac884]{min-height:100vh;display:flex;align-items:center;justify-content:center}
+1
View File
@@ -0,0 +1 @@
.full-page-msg[data-v-1080291a]{min-height:100vh;display:flex;align-items:center;justify-content:center}
@@ -1 +1 @@
import{_}from"./_plugin-vue_export-helper-DlAUqK2U.js";import{b as c,c as e,w as o,f as l,r as s,o as p}from"./index-DuH3OOPw.js";const u={},i={class:"full-page-msg"};function m(r,t){const a=s("a-button"),n=s("a-result");return p(),c("div",i,[e(n,{status:"500",title:"خطای سرور","sub-title":"مشکلی در سرور رخ داده است. لطفاً بعداً تلاش کنید."},{extra:o(()=>[e(a,{type:"primary",onClick:t[0]||(t[0]=d=>r.$router.push("/"))},{default:o(()=>[...t[1]||(t[1]=[l("بازگشت به خانه",-1)])]),_:1})]),_:1})])}const x=_(u,[["render",m],["__scopeId","data-v-abbac884"]]);export{x as default};
import{_}from"./_plugin-vue_export-helper-DlAUqK2U.js";import{b as c,c as e,w as o,f as l,r as s,o as p}from"./index-DDKWZYPX.js";const u={},i={class:"full-page-msg"};function m(r,t){const a=s("a-button"),n=s("a-result");return p(),c("div",i,[e(n,{status:"500",title:"خطای سرور","sub-title":"مشکلی در سرور رخ داده است. لطفاً بعداً تلاش کنید."},{extra:o(()=>[e(a,{type:"primary",onClick:t[0]||(t[0]=d=>r.$router.push("/"))},{default:o(()=>[...t[1]||(t[1]=[l("بازگشت به خانه",-1)])]),_:1})]),_:1})])}const b=_(u,[["render",m],["__scopeId","data-v-1080291a"]]);export{b as default};
@@ -1 +0,0 @@
import{d as E,a as i,w as l,r as C,o as s,c as d,W as P,n as w,h as r,E as h,K as B,J as L,V as S,l as p,x as V,z as y,k as A}from"./index-DuH3OOPw.js";import{X as I}from"./model-DWeeFWcT.js";import{_ as N}from"./ListPageHeader.vue_vue_type_script_setup_true_lang-BaaZTuAN.js";const X={class:"flex gap-2"},K=E({__name:"FacilityList",props:{title:{},model:{},editPath:{},activePath:{},addRid:{},addLabel:{}},setup(a){const k=a,$=A(),f=p(null),c=p(!1),m=p(void 0);function _(){m.value=void 0,c.value=!0}function b(e){m.value=e,c.value=!0}function g(e){c.value=e}function O(){var e;(e=f.value)==null||e.fetch()}function R(e){$.push(`${k.editPath}/${e}`)}async function x(e){var o,n,u;try{await V.get(`${k.activePath}/${e}`),y.success("تغییرات ثبت شد"),(o=f.value)==null||o.fetch()}catch(t){y.error(((u=(n=t==null?void 0:t.response)==null?void 0:n.data)==null?void 0:u.message)??"خطا در تغییر وضعیت")}}return(e,o)=>{const n=C("a-tooltip"),u=C("a-card");return s(),i(u,{title:a.title},{default:l(()=>[d(I,{ref_key:"tbl",ref:f,model:a.model},P({actions:l(({record:t})=>[w("div",X,[a.addRid?(s(),i(n,{key:0,title:"ویرایش"},{default:l(()=>[d(r(h),{class:"cursor-pointer mx-1 text-primary",onClick:v=>b(t.id)},null,8,["onClick"])]),_:2},1024)):(s(),i(n,{key:1,title:"ویرایش"},{default:l(()=>[d(r(h),{class:"cursor-pointer mx-1 text-primary",onClick:v=>R(t.id)},null,8,["onClick"])]),_:2},1024)),d(n,{title:"وضعیت"},{default:l(()=>[t.is_active?(s(),i(r(B),{key:0,class:"cursor-pointer mx-1 text-success",onClick:v=>x(t.id)},null,8,["onClick"])):(s(),i(r(L),{key:1,class:"cursor-pointer mx-1 text-danger",onClick:v=>x(t.id)},null,8,["onClick"]))]),_:2},1024)])]),_:2},[a.addRid?{name:"add",fn:l(()=>[d(N,{rid:a.addRid,"add-label":a.addLabel,onAdd:o[0]||(o[0]=t=>_())},null,8,["rid","add-label"])]),key:"0"}:void 0]),1032,["model"]),S(e.$slots,"form",{open:c.value,editingId:m.value,onUpdateOpen:g,onSuccess:O})]),_:3},8,["title"])}}});export{K as _};
@@ -0,0 +1 @@
import{d as E,a as i,w as l,r as C,o as s,c as d,_ as L,n as P,h as r,E as h,N as w,L as B,Y as N,l as p,x as S,z as y,k as A}from"./index-DDKWZYPX.js";import{X as I}from"./model-DfYFtEav.js";import{_ as V}from"./ListPageHeader.vue_vue_type_script_setup_true_lang-D1N_8dtP.js";const X={class:"flex gap-2"},U=E({__name:"FacilityList",props:{title:{},model:{},editPath:{},activePath:{},addRid:{},addLabel:{}},setup(a){const k=a,_=A(),f=p(null),c=p(!1),m=p(void 0);function $(){m.value=void 0,c.value=!0}function b(e){m.value=e,c.value=!0}function g(e){c.value=e}function O(){var e;(e=f.value)==null||e.fetch()}function R(e){_.push(`${k.editPath}/${e}`)}async function x(e){var o,n,u;try{await S.get(`${k.activePath}/${e}`),y.success("تغییرات ثبت شد"),(o=f.value)==null||o.fetch()}catch(t){y.error(((u=(n=t==null?void 0:t.response)==null?void 0:n.data)==null?void 0:u.message)??"خطا در تغییر وضعیت")}}return(e,o)=>{const n=C("a-tooltip"),u=C("a-card");return s(),i(u,{title:a.title},{default:l(()=>[d(I,{ref_key:"tbl",ref:f,model:a.model},L({actions:l(({record:t})=>[P("div",X,[a.addRid?(s(),i(n,{key:0,title:"ویرایش"},{default:l(()=>[d(r(h),{class:"cursor-pointer mx-1 text-primary",onClick:v=>b(t.id)},null,8,["onClick"])]),_:2},1024)):(s(),i(n,{key:1,title:"ویرایش"},{default:l(()=>[d(r(h),{class:"cursor-pointer mx-1 text-primary",onClick:v=>R(t.id)},null,8,["onClick"])]),_:2},1024)),d(n,{title:"وضعیت"},{default:l(()=>[t.is_active?(s(),i(r(w),{key:0,class:"cursor-pointer mx-1 text-success",onClick:v=>x(t.id)},null,8,["onClick"])):(s(),i(r(B),{key:1,class:"cursor-pointer mx-1 text-danger",onClick:v=>x(t.id)},null,8,["onClick"]))]),_:2},1024)])]),_:2},[a.addRid?{name:"add",fn:l(()=>[d(V,{rid:a.addRid,"add-label":a.addLabel,onAdd:o[0]||(o[0]=t=>$())},null,8,["rid","add-label"])]),key:"0"}:void 0]),1032,["model"]),N(e.$slots,"form",{open:c.value,editingId:m.value,onUpdateOpen:g,onSuccess:O})]),_:3},8,["title"])}}});export{U as _};
@@ -1 +1 @@
import{_ as o}from"./_plugin-vue_export-helper-DlAUqK2U.js";import{b as t,c as r,r as c,o as s}from"./index-DuH3OOPw.js";const n={},a={class:"layout--full-page"};function _(l,f){const e=c("router-view");return s(),t("div",a,[r(e)])}const p=o(n,[["render",_]]);export{p as default};
import{_ as o}from"./_plugin-vue_export-helper-DlAUqK2U.js";import{b as t,c as r,r as c,o as s}from"./index-DDKWZYPX.js";const n={},a={class:"layout--full-page"};function _(l,f){const e=c("router-view");return s(),t("div",a,[r(e)])}const p=o(n,[["render",_]]);export{p as default};
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
import{d as c,v as r,x as n,b as i,k as d,o as l}from"./index-DDKWZYPX.js";import{u as m}from"./auth-DYHJI02F.js";const u={style:{"min-height":"100vh",display:"grid","place-items":"center"}},g=c({__name:"IamCallback",setup(p){const t=d(),o=m();return r(async()=>{const e=new URLSearchParams(window.location.search);if(sessionStorage.setItem("gb_admin_iam_sso_checked","1"),e.get("error")||!e.get("code")||!e.get("state")){await t.replace("/auth/login");return}try{const{data:s}=await n.post("admin/auth/sso/callback",{code:e.get("code"),state:e.get("state")}),a={...s};delete a.roles,delete a.ut,o.inject(a),await t.replace("/dashboard/ecommerce")}catch{await t.replace("/auth/login")}}),(e,s)=>(l(),i("div",u,"در حال ورود..."))}});export{g as default};
@@ -1 +1 @@
import{d as B,m as P,o as u,b as r,c as m,w as h,a as N,h as b,P as z,n as g,t as D,i as I,l as i,p as R,x as S,z as U,r as f}from"./index-DuH3OOPw.js";const q={class:"image-upload"},E={key:0},F={key:1},L={style:{"margin-top":"8px"}},O=["src"],j=B({__name:"ImageUploadInput",props:{modelValue:{default:null},purpose:{default:void 0},placeholder:{default:"بارگذاری"}},emits:["update:modelValue","ok"],setup(w,{emit:y}){const l=w,p=y,c=i(!1),d=i(!1),v=i(""),o=i([]);P(()=>l.modelValue,e=>{var a;e?((a=o.value[0])==null?void 0:a.url)!==e&&(o.value=[{uid:e,name:e.split("/").pop()??"file",status:"done",url:e}]):o.value=[]},{immediate:!0});const V=R(()=>l.placeholder);async function k({file:e}){var s,n;const a=new FormData;a.append("file",e),l.purpose&&a.append("purpose",l.purpose),c.value=!0;try{const{data:t}=await S.post("file/create",a);p("update:modelValue",t.url),p("ok",t)}catch(t){const _=((n=(s=t==null?void 0:t.response)==null?void 0:s.data)==null?void 0:n.message)??"بارگذاری فایل ناموفق بود";U.error(_)}finally{c.value=!1}}function x(){return p("update:modelValue",null),!0}function C(e){v.value=e.url,d.value=!0}return(e,a)=>{const s=f("a-spin"),n=f("a-upload"),t=f("a-modal");return u(),r("div",q,[m(n,{"file-list":o.value,"list-type":"picture-card","max-count":1,accept:"image/*","custom-request":k,onRemove:x,onPreview:C},{default:h(()=>[o.value.length===0?(u(),r("div",E,[c.value?(u(),N(s,{key:0,size:"small"})):(u(),r("div",F,[m(b(z)),g("div",L,D(V.value),1)]))])):I("",!0)]),_:1},8,["file-list"]),m(t,{open:d.value,footer:null,onCancel:a[0]||(a[0]=_=>d.value=!1)},{default:h(()=>[g("img",{src:v.value,style:{width:"100%"},alt:""},null,8,O)]),_:1},8,["open"])])}}});export{j as _};
import{d as B,m as N,o as u,b as r,c as m,w as h,a as P,h as S,S as b,n as g,t as z,i as D,l as i,p as I,x as R,z as U,r as f}from"./index-DDKWZYPX.js";const q={class:"image-upload"},E={key:0},F={key:1},L={style:{"margin-top":"8px"}},O=["src"],j=B({__name:"ImageUploadInput",props:{modelValue:{default:null},purpose:{default:void 0},placeholder:{default:"بارگذاری"}},emits:["update:modelValue","ok"],setup(w,{emit:y}){const l=w,p=y,c=i(!1),d=i(!1),v=i(""),o=i([]);N(()=>l.modelValue,e=>{var a;e?((a=o.value[0])==null?void 0:a.url)!==e&&(o.value=[{uid:e,name:e.split("/").pop()??"file",status:"done",url:e}]):o.value=[]},{immediate:!0});const V=I(()=>l.placeholder);async function k({file:e}){var s,n;const a=new FormData;a.append("file",e),l.purpose&&a.append("purpose",l.purpose),c.value=!0;try{const{data:t}=await R.post("file/create",a);p("update:modelValue",t.url),p("ok",t)}catch(t){const _=((n=(s=t==null?void 0:t.response)==null?void 0:s.data)==null?void 0:n.message)??"بارگذاری فایل ناموفق بود";U.error(_)}finally{c.value=!1}}function x(){return p("update:modelValue",null),!0}function C(e){v.value=e.url,d.value=!0}return(e,a)=>{const s=f("a-spin"),n=f("a-upload"),t=f("a-modal");return u(),r("div",q,[m(n,{"file-list":o.value,"list-type":"picture-card","max-count":1,accept:"image/*","custom-request":k,onRemove:x,onPreview:C},{default:h(()=>[o.value.length===0?(u(),r("div",E,[c.value?(u(),P(s,{key:0,size:"small"})):(u(),r("div",F,[m(S(b)),g("div",L,z(V.value),1)]))])):D("",!0)]),_:1},8,["file-list"]),m(t,{open:d.value,footer:null,onCancel:a[0]||(a[0]=_=>d.value=!1)},{default:h(()=>[g("img",{src:v.value,style:{width:"100%"},alt:""},null,8,O)]),_:1},8,["open"])])}}});export{j as _};
-1
View File
@@ -1 +0,0 @@
.flex-col[data-v-7e21cbc2]{flex-direction:column}.flex-1[data-v-7e21cbc2]{flex:1}.justify-between[data-v-7e21cbc2]{justify-content:space-between}.mr-3[data-v-7e21cbc2]{margin-left:.75rem}
-1
View File
@@ -1 +0,0 @@
import{a as $,b as s,X as B}from"./model-DWeeFWcT.js";import{_ as E}from"./ListPageHeader.vue_vue_type_script_setup_true_lang-BaaZTuAN.js";import{_ as D}from"./Add.vue_vue_type_script_setup_true_lang-vc2KlLTI.js";import{d as X,u as M,b as O,c as i,w as o,r as p,o as c,h as m,a as f,E as T,i as b,D as V,n as w,l as _}from"./index-DuH3OOPw.js";import{u as A}from"./useDeleteHandler-DPZrExAz.js";import"./SettingOutlined-MJAt5cqt.js";import"./_plugin-vue_export-helper-DlAUqK2U.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-zAo7Oyp5.js";import"./ImageUploadInput.vue_vue_type_script_setup_true_lang-BTbAO94-.js";function I(){const t=new $("admin/categories/list","لیست دسته بندی");return t.add(new s("id","شماره").filterNumber()),t.add(new s("image","تصویر").noSort().renderSlot("image")),t.add(new s("type_str","نوع نمایش")),t.add(new s("title","عنوان دسته").filterNumber()),t.add(new s("parent.title","پدر دسته").filterNumber()),t.add(new s("opt","عملیات").noSort().renderSlot("opt")),t}const P={id:"page-user-list"},U=["src"],j="/img/no-items.png",R=X({__name:"List",setup(t){const k=M(),v=n=>!!k.hasPermission(n),y=I(),u=_(null),l=_(!1),r=_(void 0),{remove:x}=A({onSuccess:()=>{var n;return(n=u.value)==null?void 0:n.fetch()},successMessage:"دسته حذف شد",successType:"info",fallbackErrorMessage:"خطا در حذف دسته"});function C(){r.value=void 0,l.value=!0}function S(n){r.value=n,l.value=!0}return(n,a)=>{const g=p("a-tooltip"),h=p("a-popconfirm"),N=p("a-card");return c(),O("div",P,[i(N,{title:"لیست دسته بندی ها"},{default:o(()=>[i(B,{ref_key:"tbl",ref:u,model:m(y)},{add:o(()=>[i(E,{rid:"collection.create","add-label":"افزودن دسته",onAdd:C})]),image:o(({text:e})=>[w("img",{width:"90px",height:"90px",style:{"object-fit":"scale-down"},src:e&&e.image?e.image:j},null,8,U)]),opt:o(({record:e})=>[v("collection.list.edit")?(c(),f(g,{key:0,title:"ویرایش"},{default:o(()=>[i(m(T),{class:"cursor-pointer mx-1 text-primary",onClick:d=>S(e.id)},null,8,["onClick"])]),_:2},1024)):b("",!0),v("collection.list.remove")?(c(),f(g,{key:1,title:"حذف"},{default:o(()=>[i(h,{placement:"top","ok-text":"بله","cancel-text":"خیر",onConfirm:d=>m(x)(`category/${e.id}`)},{title:o(()=>[...a[2]||(a[2]=[w("p",null,"آیا مطمئن به حذف دسته مورد نظر هستید ؟",-1)])]),default:o(()=>[i(m(V),{class:"cursor-pointer mx-1 text-danger"})]),_:1},8,["onConfirm"])]),_:2},1024)):b("",!0)]),_:1},8,["model"]),(c(),f(D,{key:r.value??"new",open:l.value,id:r.value,"onUpdate:open":a[0]||(a[0]=e=>l.value=e),onSuccess:a[1]||(a[1]=e=>{var d;return(d=u.value)==null?void 0:d.fetch()})},null,8,["open","id"]))]),_:1})])}}});export{R as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.flex-col[data-v-a0837a14]{flex-direction:column}.flex-1[data-v-a0837a14]{flex:1}.justify-between[data-v-a0837a14]{justify-content:space-between}.mr-3[data-v-a0837a14]{margin-left:.75rem}
+1
View File
@@ -0,0 +1 @@
import{a as $,b as s,X as B}from"./model-DfYFtEav.js";import{_ as E}from"./ListPageHeader.vue_vue_type_script_setup_true_lang-D1N_8dtP.js";import{_ as D}from"./Add.vue_vue_type_script_setup_true_lang-CjPh6jcZ.js";import{d as X,u as M,b as O,c as i,w as o,r as u,o as c,h as m,a as f,E as T,i as b,D as V,n as w,l as _}from"./index-DDKWZYPX.js";import{u as A}from"./useDeleteHandler-DPRijnf4.js";import"./filters-CqJCHW3n.js";import"./index-FKca-jp6.js";import"./SettingOutlined-Bf9_Xgtn.js";import"./_plugin-vue_export-helper-DlAUqK2U.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-YIXFkb8N.js";import"./ImageUploadInput.vue_vue_type_script_setup_true_lang-DHQxooHi.js";function I(){const t=new $("admin/categories/list","لیست دسته بندی");return t.add(new s("id","شماره").filterNumber()),t.add(new s("image","تصویر").noSort().renderSlot("image")),t.add(new s("type_str","نوع نمایش")),t.add(new s("title","عنوان دسته").filterNumber()),t.add(new s("parent.title","پدر دسته").filterNumber()),t.add(new s("opt","عملیات").noSort().renderSlot("opt")),t}const P={id:"page-user-list"},U=["src"],j="/img/no-items.png",Y=X({__name:"List",setup(t){const k=M(),v=n=>!!k.hasPermission(n),y=I(),p=_(null),r=_(!1),l=_(void 0),{remove:x}=A({onSuccess:()=>{var n;return(n=p.value)==null?void 0:n.fetch()},successMessage:"دسته حذف شد",successType:"info",fallbackErrorMessage:"خطا در حذف دسته"});function C(){l.value=void 0,r.value=!0}function S(n){l.value=n,r.value=!0}return(n,a)=>{const g=u("a-tooltip"),h=u("a-popconfirm"),N=u("a-card");return c(),O("div",P,[i(N,{title:"لیست دسته بندی ها"},{default:o(()=>[i(B,{ref_key:"tbl",ref:p,model:m(y)},{add:o(()=>[i(E,{rid:"collection.create","add-label":"افزودن دسته",onAdd:C})]),image:o(({text:e})=>[w("img",{width:"90px",height:"90px",style:{"object-fit":"scale-down"},src:e&&e.image?e.image:j},null,8,U)]),opt:o(({record:e})=>[v("collection.list.edit")?(c(),f(g,{key:0,title:"ویرایش"},{default:o(()=>[i(m(T),{class:"cursor-pointer mx-1 text-primary",onClick:d=>S(e.id)},null,8,["onClick"])]),_:2},1024)):b("",!0),v("collection.list.remove")?(c(),f(g,{key:1,title:"حذف"},{default:o(()=>[i(h,{placement:"top","ok-text":"بله","cancel-text":"خیر",onConfirm:d=>m(x)(`category/${e.id}`)},{title:o(()=>[...a[2]||(a[2]=[w("p",null,"آیا مطمئن به حذف دسته مورد نظر هستید ؟",-1)])]),default:o(()=>[i(m(V),{class:"cursor-pointer mx-1 text-danger"})]),_:1},8,["onConfirm"])]),_:2},1024)):b("",!0)]),_:1},8,["model"]),(c(),f(D,{key:l.value??"new",open:r.value,id:l.value,"onUpdate:open":a[0]||(a[0]=e=>r.value=e),onSuccess:a[1]||(a[1]=e=>{var d;return(d=p.value)==null?void 0:d.fetch()})},null,8,["open","id"]))]),_:1})])}}});export{Y as default};
-1
View File
@@ -1 +0,0 @@
import{_ as c}from"./FacilityList.vue_vue_type_script_setup_true_lang-BOM9j7TC.js";import{_ as m}from"./AddBranches.vue_vue_type_script_setup_true_lang-BQagavGv.js";import{a as p,b as t}from"./model-DWeeFWcT.js";import{d as l,a as n,w as f,h as _,o}from"./index-DuH3OOPw.js";import"./ListPageHeader.vue_vue_type_script_setup_true_lang-BaaZTuAN.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-zAo7Oyp5.js";import"./SettingOutlined-MJAt5cqt.js";import"./_plugin-vue_export-helper-DlAUqK2U.js";function u(){const e=new p("admin/branches/list","لیست شعب");return e.add(new t("name","نام").filterText()),e.add(new t("address","آدرس").filterText()),e.add(new t("phone","شماره تلفن").filterNumber()),e.add(new t("user.name","مدیر شعبه").filterNumber().noSort()),e.add(new t("op","عملیات").noSort().renderSlot("actions")),e}const L=l({__name:"ListBranches",setup(e){const r=u();return(h,b)=>(o(),n(c,{title:"لیست شعب",model:_(r),"edit-path":"/branch/edit","active-path":"branch/active","add-rid":"branch.create","add-label":"افزودن شعبه"},{form:f(({open:d,editingId:a,onUpdateOpen:i,onSuccess:s})=>[(o(),n(m,{key:a??"new",open:d,id:a,"onUpdate:open":i,onSuccess:s},null,8,["open","id","onUpdate:open","onSuccess"]))]),_:1},8,["model"]))}});export{L as default};
+1
View File
@@ -0,0 +1 @@
import{_ as m}from"./FacilityList.vue_vue_type_script_setup_true_lang-C5XIrWWu.js";import{_ as c}from"./AddBranches.vue_vue_type_script_setup_true_lang-CQi_1IC6.js";import{a as p,b as t}from"./model-DfYFtEav.js";import{d as l,a as o,w as f,h as _,o as r}from"./index-DDKWZYPX.js";import"./ListPageHeader.vue_vue_type_script_setup_true_lang-D1N_8dtP.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-YIXFkb8N.js";import"./filters-CqJCHW3n.js";import"./index-FKca-jp6.js";import"./SettingOutlined-Bf9_Xgtn.js";import"./_plugin-vue_export-helper-DlAUqK2U.js";function u(){const e=new p("admin/branches/list","لیست شعب");return e.add(new t("name","نام").filterText()),e.add(new t("address","آدرس").filterText()),e.add(new t("phone","شماره تلفن").filterNumber()),e.add(new t("user.name","مدیر شعبه").filterNumber().noSort()),e.add(new t("op","عملیات").noSort().renderSlot("actions")),e}const U=l({__name:"ListBranches",setup(e){const n=u();return(h,b)=>(r(),o(m,{title:"لیست شعب",model:_(n),"edit-path":"/branch/edit","active-path":"branch/active","add-rid":"branch.create","add-label":"افزودن شعبه"},{form:f(({open:i,editingId:a,onUpdateOpen:d,onSuccess:s})=>[(r(),o(c,{key:a??"new",open:i,id:a,"onUpdate:open":d,onSuccess:s},null,8,["open","id","onUpdate:open","onSuccess"]))]),_:1},8,["model"]))}});export{U as default};
+1
View File
@@ -0,0 +1 @@
import{a as M,b as d,X as V}from"./model-DfYFtEav.js";import{_ as h}from"./ListPageHeader.vue_vue_type_script_setup_true_lang-D1N_8dtP.js";import z from"./AddInvoice-CThPsK78.js";import{j as K}from"./filters-CqJCHW3n.js";import{u as U}from"./useDeleteHandler-DPRijnf4.js";import{d as H,b as y,c as e,w as t,r as s,o as x,h as p,n as g,T as R,E as q,D as F,f as r,t as u,a as C,l as _,x as G}from"./index-DDKWZYPX.js";import{_ as J}from"./_plugin-vue_export-helper-DlAUqK2U.js";import"./SettingOutlined-Bf9_Xgtn.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-YIXFkb8N.js";import"./index-FKca-jp6.js";function P(){const n=new M("admin/invoices/list","لیست فاکتورها");return n.add(new d("id","ردیف").filterNumber()),n.add(new d("name","نام مشتری").filterKey("branch.name").filterText()),n.add(new d("sum_weight","جمع کل سفارش").filterNumber().noSort()),n.add(new d("count_item","تعداد آیتم ها").filterNumber().noSort()),n.add(new d("caj","تاریخ ثبت").filterKey("createdAt").filterDate().noSort()),n.add(new d("allocator_user.name","ثبت کننده").renderSlot("user").noSort()),n.add(new d("note","توضیحات").filterText().noSort()),n.add(new d("op","عملیات").noSort().renderSlot("actions")),n}const Q={id:"page-user-list"},W={class:"flex gap-2"},Y={key:0},Z=H({__name:"ListInvoice",setup(n){const S=P(),w=_(null),f=_(!1),l=_(void 0),v=_(!1),b=_(void 0);function I(){b.value=void 0,v.value=!0}function $(c){b.value=c,v.value=!0}const D=[{title:"کد محصول",dataIndex:"product_code"},{title:"نام محصول",dataIndex:"title"},{title:"وزن",dataIndex:"grams"}];async function E(c){const{data:o}=await G.get(`branch/product/${c.id}`);l.value=o,f.value=!0}const{remove:N}=U({onSuccess:()=>{var c;return(c=w.value)==null?void 0:c.fetch()},successMessage:"فاکتور حذف شد",fallbackErrorMessage:"خطا در حذف فاکتور"});return(c,o)=>{const k=s("a-tooltip"),T=s("a-popconfirm"),m=s("a-descriptions-item"),j=s("a-descriptions"),A=s("a-table"),B=s("a-empty"),O=s("a-button"),X=s("a-modal"),L=s("a-card");return x(),y("div",Q,[e(L,{title:"لیست فاکتورها"},{default:t(()=>[e(V,{ref_key:"tbl",ref:w,model:p(S)},{add:t(()=>[e(h,{rid:"invoice.create","add-label":"افزودن فاکتور",onAdd:I})]),user:t(({record:a})=>{var i;return[r(u(((i=a.allocator_user)==null?void 0:i.name)??"-"),1)]}),actions:t(({record:a})=>[g("div",W,[e(k,{title:"مشاهده"},{default:t(()=>[e(p(R),{class:"cursor-pointer mx-1 text-success",onClick:i=>E(a)},null,8,["onClick"])]),_:2},1024),e(k,{title:"ویرایش"},{default:t(()=>[e(p(q),{class:"cursor-pointer mx-1 text-primary",onClick:i=>$(a.id)},null,8,["onClick"])]),_:2},1024),e(k,{title:"حذف"},{default:t(()=>[e(T,{placement:"top","ok-text":"بله","cancel-text":"خیر",onConfirm:i=>p(N)(`branch/product/${a.id}`)},{title:t(()=>[...o[4]||(o[4]=[g("p",null,"آیا مطمئن به حذف فاکتور مورد نظر هستید ؟",-1)])]),default:t(()=>[e(p(F),{class:"cursor-pointer mx-1 text-danger"})]),_:1},8,["onConfirm"])]),_:2},1024)])]),_:1},8,["model"]),e(X,{open:f.value,"onUpdate:open":o[1]||(o[1]=a=>f.value=a),title:"مشاهده فاکتور",width:"70%"},{footer:t(()=>[e(O,{onClick:o[0]||(o[0]=a=>f.value=!1)},{default:t(()=>[...o[5]||(o[5]=[r("بستن",-1)])]),_:1})]),default:t(()=>[l.value?(x(),y("div",Y,[e(j,{bordered:"",size:"small",column:2},{default:t(()=>[e(m,{label:"شعبه"},{default:t(()=>[r(u(l.value.name),1)]),_:1}),e(m,{label:"ثبت کننده"},{default:t(()=>{var a;return[r(u(((a=l.value.allocator_user)==null?void 0:a.name)??"-"),1)]}),_:1}),e(m,{label:"تاریخ ثبت"},{default:t(()=>[r(u(p(K)(l.value.caj)),1)]),_:1}),e(m,{label:"تعداد آیتم"},{default:t(()=>[r(u(l.value.count_item),1)]),_:1}),e(m,{label:"جمع وزن"},{default:t(()=>[r(u(l.value.sum_weight),1)]),_:1}),e(m,{label:"توضیحات"},{default:t(()=>[r(u(l.value.note??"-"),1)]),_:1})]),_:1}),e(A,{class:"mt-4",size:"small","data-source":l.value.products,columns:D,pagination:!1,"row-key":"id"},null,8,["data-source"])])):(x(),C(B,{key:1}))]),_:1},8,["open"]),(x(),C(z,{key:b.value??"new",open:v.value,id:b.value,"onUpdate:open":o[2]||(o[2]=a=>v.value=a),onSuccess:o[3]||(o[3]=a=>{var i;return(i=w.value)==null?void 0:i.fetch()})},null,8,["open","id"]))]),_:1})])}}}),ce=J(Z,[["__scopeId","data-v-6cda7919"]]);export{ce as default};
-1
View File
@@ -1 +0,0 @@
import{a as M,b as d,X as V}from"./model-DWeeFWcT.js";import{_ as h}from"./ListPageHeader.vue_vue_type_script_setup_true_lang-BaaZTuAN.js";import z from"./AddInvoice-CkxeyCUi.js";import{j as K}from"./filters-CWowq3Uw.js";import{u as U}from"./useDeleteHandler-DPZrExAz.js";import{d as H,b as y,c as e,w as t,r as s,o as x,h as p,n as g,Q,E as R,D as q,f as r,t as u,a as C,l as _,x as F}from"./index-DuH3OOPw.js";import{_ as G}from"./_plugin-vue_export-helper-DlAUqK2U.js";import"./SettingOutlined-MJAt5cqt.js";import"./BaseFormModal.vue_vue_type_script_setup_true_lang-zAo7Oyp5.js";import"./index-BHIiPvAE.js";function J(){const n=new M("admin/invoices/list","لیست فاکتورها");return n.add(new d("id","ردیف").filterNumber()),n.add(new d("name","نام مشتری").filterKey("branch.name").filterText()),n.add(new d("sum_weight","جمع کل سفارش").filterNumber().noSort()),n.add(new d("count_item","تعداد آیتم ها").filterNumber().noSort()),n.add(new d("caj","تاریخ ثبت").filterKey("createdAt").filterDate().noSort()),n.add(new d("allocator_user.name","ثبت کننده").renderSlot("user").noSort()),n.add(new d("note","توضیحات").filterText().noSort()),n.add(new d("op","عملیات").noSort().renderSlot("actions")),n}const P={id:"page-user-list"},W={class:"flex gap-2"},Y={key:0},Z=H({__name:"ListInvoice",setup(n){const S=J(),w=_(null),f=_(!1),l=_(void 0),v=_(!1),b=_(void 0);function I(){b.value=void 0,v.value=!0}function $(c){b.value=c,v.value=!0}const D=[{title:"کد محصول",dataIndex:"product_code"},{title:"نام محصول",dataIndex:"title"},{title:"وزن",dataIndex:"grams"}];async function E(c){const{data:o}=await F.get(`branch/product/${c.id}`);l.value=o,f.value=!0}const{remove:N}=U({onSuccess:()=>{var c;return(c=w.value)==null?void 0:c.fetch()},successMessage:"فاکتور حذف شد",fallbackErrorMessage:"خطا در حذف فاکتور"});return(c,o)=>{const k=s("a-tooltip"),T=s("a-popconfirm"),m=s("a-descriptions-item"),j=s("a-descriptions"),A=s("a-table"),B=s("a-empty"),O=s("a-button"),X=s("a-modal"),L=s("a-card");return x(),y("div",P,[e(L,{title:"لیست فاکتورها"},{default:t(()=>[e(V,{ref_key:"tbl",ref:w,model:p(S)},{add:t(()=>[e(h,{rid:"invoice.create","add-label":"افزودن فاکتور",onAdd:I})]),user:t(({record:a})=>{var i;return[r(u(((i=a.allocator_user)==null?void 0:i.name)??"-"),1)]}),actions:t(({record:a})=>[g("div",W,[e(k,{title:"مشاهده"},{default:t(()=>[e(p(Q),{class:"cursor-pointer mx-1 text-success",onClick:i=>E(a)},null,8,["onClick"])]),_:2},1024),e(k,{title:"ویرایش"},{default:t(()=>[e(p(R),{class:"cursor-pointer mx-1 text-primary",onClick:i=>$(a.id)},null,8,["onClick"])]),_:2},1024),e(k,{title:"حذف"},{default:t(()=>[e(T,{placement:"top","ok-text":"بله","cancel-text":"خیر",onConfirm:i=>p(N)(`branch/product/${a.id}`)},{title:t(()=>[...o[4]||(o[4]=[g("p",null,"آیا مطمئن به حذف فاکتور مورد نظر هستید ؟",-1)])]),default:t(()=>[e(p(q),{class:"cursor-pointer mx-1 text-danger"})]),_:1},8,["onConfirm"])]),_:2},1024)])]),_:1},8,["model"]),e(X,{open:f.value,"onUpdate:open":o[1]||(o[1]=a=>f.value=a),title:"مشاهده فاکتور",width:"70%"},{footer:t(()=>[e(O,{onClick:o[0]||(o[0]=a=>f.value=!1)},{default:t(()=>[...o[5]||(o[5]=[r("بستن",-1)])]),_:1})]),default:t(()=>[l.value?(x(),y("div",Y,[e(j,{bordered:"",size:"small",column:2},{default:t(()=>[e(m,{label:"شعبه"},{default:t(()=>[r(u(l.value.name),1)]),_:1}),e(m,{label:"ثبت کننده"},{default:t(()=>{var a;return[r(u(((a=l.value.allocator_user)==null?void 0:a.name)??"-"),1)]}),_:1}),e(m,{label:"تاریخ ثبت"},{default:t(()=>[r(u(p(K)(l.value.caj)),1)]),_:1}),e(m,{label:"تعداد آیتم"},{default:t(()=>[r(u(l.value.count_item),1)]),_:1}),e(m,{label:"جمع وزن"},{default:t(()=>[r(u(l.value.sum_weight),1)]),_:1}),e(m,{label:"توضیحات"},{default:t(()=>[r(u(l.value.note??"-"),1)]),_:1})]),_:1}),e(A,{class:"mt-4",size:"small","data-source":l.value.products,columns:D,pagination:!1,"row-key":"id"},null,8,["data-source"])])):(x(),C(B,{key:1}))]),_:1},8,["open"]),(x(),C(z,{key:b.value??"new",open:v.value,id:b.value,"onUpdate:open":o[2]||(o[2]=a=>v.value=a),onSuccess:o[3]||(o[3]=a=>{var i;return(i=w.value)==null?void 0:i.fetch()})},null,8,["open","id"]))]),_:1})])}}}),ce=G(Z,[["__scopeId","data-v-6cda7919"]]);export{ce as default};
@@ -1 +1 @@
import{d,u as i,o as u,a as m,w as s,f as c,t as l,c as f,h as p,P as C,i as P,r as b}from"./index-DuH3OOPw.js";const _=d({__name:"ListPageHeader",props:{rid:{default:void 0},addLabel:{default:"افزودن"}},emits:["add"],setup(e){const o=i(),n=t=>!!o.hasPermission(t);return(t,a)=>{const r=b("a-button");return!e.rid||n(e.rid)?(u(),m(r,{key:0,type:"primary",onClick:a[0]||(a[0]=k=>t.$emit("add"))},{icon:s(()=>[f(p(C))]),default:s(()=>[c(" "+l(e.addLabel),1)]),_:1})):P("",!0)}}});export{_};
import{d,u as i,o as u,a as m,w as s,f as c,t as l,c as f,h as p,S as C,i as b,r as k}from"./index-DDKWZYPX.js";const _=d({__name:"ListPageHeader",props:{rid:{default:void 0},addLabel:{default:"افزودن"}},emits:["add"],setup(e){const o=i(),n=t=>!!o.hasPermission(t);return(t,a)=>{const r=k("a-button");return!e.rid||n(e.rid)?(u(),m(r,{key:0,type:"primary",onClick:a[0]||(a[0]=y=>t.$emit("add"))},{icon:s(()=>[f(p(C))]),default:s(()=>[c(" "+l(e.addLabel),1)]),_:1})):b("",!0)}}});export{_};

Some files were not shown because too many files have changed in this diff Show More