134 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
s.esmaeili 81987ceae8 admin panel beter ui 2026-09-19 12:05:59 +03:30
s.esmaeili 80ccf1a2aa feat build 2026-09-18 19:58:29 +03:30
s.esmaeili e8c67bead0 fix(user-app): rebuild product gallery with fullscreen preview, fix icon reactivity bug
- Extract product image gallery into a dedicated GallerySlider component
  with swipe/tap support
- Add a teleported fullscreen image preview (tap to open, thumbnail strip,
  body scroll lock) to SingleProduct
- Restore the Trust section that was accidentally commented out
- Swap back/favorite button positions and recenter the image counter
- Fix Icon.vue: tempIcon was a static ref set once at setup, so it never
  updated on prop change when Vue reused the component instance (e.g. cart
  qty steppers toggling trash/minus icons); switched to computed + watch
- Rename brand text from "منتزر" to "هنزاگلد"
- Fix a CSS typo in user-app-v2 SingleProduct (rgba alpha value)
- Rebuild dist assets
2026-09-18 19:54:26 +03:30
mahmood 062fe80a68 feat build 2026-09-18 17:30:32 +03:30
mahmood ffd952d754 Merge remote-tracking branch 'origin/main' 2026-09-18 17:16:44 +03:30
mahmood 7a2b8b3109 fix add new version of user in app 2026-09-18 17:16:36 +03:30
s.esmaeili b80fa9bcfb Redesign product detail page and fix icon/cart reactivity bugs
- SingleProduct.vue: rebuild gallery/header/action bar as a self-scrolling
  layout with a sticky photo, solid-on-scroll header matching PageBar, and
  a full-screen image preview (blurred overlay, centered thumbnail strip)
- Add GallerySlider.vue: pointer-based swipe carousel isolated in an
  LTR subtree, fixing RTL/transform sign mismatch that caused slide jumps
- Fix Icon.vue: icon was read into a plain ref once at setup and never
  updated on prop change; switch to computed so icon swaps (e.g. cart
  qty stepper trash/minus toggle) actually re-render
- Fix quantity stepper DOM order in SingleProduct.vue/CartItem.vue so
  plus/minus render correctly under RTL flex
- prisma/assign-downloaded-images.js: seed real product photos (FEATURED/
  FEATURED_MOBILE/GALLERY/GALLERY_MOBILE) onto the 5 placeholder products
2026-09-18 16:22:31 +03:30
s.esmaeili 53a304b5d5 SearchView: Removed the dynamic collection-tile grid (e.g. "پرفروش‌های این هفته") and the "نتایج / N محصول" section header from the UI. Underlying data/filtering logic untouched.
DashboardView ("میز کار"): Rebuilt from scratch with a new structure, distinct from the old card+grid layout but visually aligned with HomeView (same blurred bar, @ink/@wine palette, staged reveal animation on mount):

Single hero card (ink→wine gradient) showing balance, gold/fiat debt, and identity — no quick-action icons inside it anymore.

Live gold rate folded into a slim inline row at the bottom instead of a titled section.
Iterated shadow/spacing on the hero card per feedback (softer, less clipped).
DialogVant / DialogVantOneButton (shared modal components, used in 7+ call sites: Dashboard, Cart, SingleProduct, Register, App): Replaced Vant's default dialog chrome with the app's own rounded-card language — circular icon badge for the illustration, pill-shaped primary/secondary buttons. No call-site markup changed.

LoginView: Rebuilt from scratch — full-bleed dark hero (brand + step copy) overlapped by a floating white form sheet, animated crossfade transition between the mobile-number and OTP steps, same staged entrance animation as Home/Dashboard. All auth logic (API check, OTP stub, session login) unchanged.
2026-09-18 12:54:45 +03:30
Montech 05fe780c89 Merge branch 'dev'
# Conflicts:
#	apps/admin-panel/dist/index.html
#	apps/admin-panel/nginx.conf
#	apps/admin-panel/vite.config.ts
#	apps/api/release/app.js
2026-09-16 15:45:23 +03:30
mahmoodandCursor e51a068039 chore: include full admin-panel dist assets for Dokploy copy-only deploy
index.html was already in git pointing at hashed bundles, but the assets folder was missing from the image so nginx 404ed every JS/CSS request.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 12:10:55 +03:30
mahmood 4743cfb89f fix admin and montzar 2026-09-16 11:59:35 +03:30
mahmood 262122cb3a fix build 2026-09-16 10:44:58 +03:30
s.esmaeili 2c48faf30d Merge branch 'dev' of https://get.feep.ir/s.esmaeili/gold-bank into dev
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
2026-09-16 01:54:22 +03:30
s.esmaeili e756fbaf85 Redesign orders/cart/search flows and fix RTL/auth edge cases in user-app
- Fix silent redirect-to-home for unauthenticated users on protected routes;
  now redirects to login with a return path
- Restyle OrderCard, SearchInput, Filters, DropDown, and product card image
  sizing/centering to match the app's LESS-token design language
- Fix ant-design Slider handle placement/drag under RTL and seed a valid
  default range so handles actually render
- Fix chip-row edge padding and top spacing across Orders/Cart/Search lists
- Add pull-to-refresh to the dashboard and fix its header/padding to match
  the fixed PageBar pattern
- Remove the floating WhatsApp button from index.html
2026-09-16 01:15:16 +03:30
mahmood 2448627e1b fix: stop drift migration from dropping tracking table 2026-09-15 22:40:00 +03:30
mahmood 1f734bedca FIx some for nginx 2026-09-15 22:30:39 +03:30
Montech 341c7a5095 fix some for db 2026-09-15 17:25:12 +03:30
Montech 938b1c0b76 Fix some for admin 2026-09-15 17:06:27 +03:30
Montech a7e13fe64e fix update build 2026-09-15 15:41:47 +03:30
s.esmaeiliandClaude Opus 5 0e5f9ccd7b user-app: extend the redesign across the account screens
Phase 2, completing the app-wide rollout of the home/product-detail design
language (LESS colour tokens, BEM class names, hairline-bordered cards,
SectionHead, dashed empty states) onto the account side: dashboard, orders,
transactions, profile, login and register.

Reuse over re-implementation: the dashboard's live rate block is now
components/home/RateStrip.vue rather than a second hand-built strip, and
every screen uses components/ui/PageBar.vue for its chrome.

OrderCard and OrderItemCard are restyled to match. OrderItemCard drops its
always-empty note row: our OrderItem schema has no note field (legacy's
per-item note was a real column there; ours is not), so it could only ever
print a dash.

OrdersView swaps ant Tabs for the same scrolling chip rail the rest of the
redesign uses, and its full-screen Modal with hand-written CSS overrides for
a bottom Drawer, matching how every other detail surface opens.

Deleted AppHeader.vue, orphaned once every screen moved to PageBar. Its
shutter animation was driven by transitionFlag, which nothing reads any
more — the router afterEach comment claiming otherwise is corrected rather
than left asserting something untrue, and the now-inert half is flagged in
place. ScrollFetchSearch loses its isFav prop: it gated two empty-state
branches that rendered the identical image, and its only caller
(FavoritesView) now renders its own.

Behaviour is preserved throughout. Orders still reads GET
/customers/:id/orders and filters client-side; transactions still reads
GET /customers/:id/ledger-entries with legacy's CREDIT/DEBIT colour meaning
intact; profile stays read-only over real fields; login keeps the real
customer lookup plus the documented OTP stub; register stays stubbed with
copy that does not claim an account was created.

Verified in a real browser against the running API and native database:
dashboard 4 tiles with a real balance, orders 1 real order card, profile 4
real spec rows, transactions the correct empty state for a customer with no
ledger rows, and login/register confirmed with the session cleared (the
guard bounces a signed-in user away from /login, which is why they needed a
separate pass). No console errors, no horizontal overflow. vue-tsc and vite
build both clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 01:34:30 +03:30
s.esmaeiliandClaude Opus 5 cc62975dd0 user-app: extend the redesign across the purchase path
Applies the home/product-detail design language (LESS colour tokens, BEM
class names, hairline-bordered cards, SectionHead, dashed empty states) to
the screens a shopper actually walks through: search, collection, newest,
cart and favorites.

Two shared pieces are extracted so the language stays consistent rather than
re-typed per screen:
  - components/ui/PageBar.vue — the sticky translucent bar with blur and a
    hairline, lifted from the home screen's own header so every other screen
    gets identical chrome instead of the old dark AppHeader.
  - components/product/ProductCard.vue — the catalogue workhorse card. It is
    deliberately a sibling of home/ShowcaseCard (which keeps its editorial "wide"
    variant and badge) and additionally surfaces stock, which the
    catalogue needs and the home screen does not.

Deleted as orphaned once nothing imported them: ProductList,
ProductVerticalFull, ProductVertical, ProductHorizontal, CollectionCard —
all Tailwind utility soup carried over from the legacy PWA.

Behaviour is preserved throughout; only chrome and cards change. The cart
still posts to the real POST /orders with wagePercent from each item's
pinned variant, still gates on a real session, and still keeps the note
drawer and success step. Search keeps its client-side filtering, the Filters
sheet, category chips and collection tiles. No rial price is printed
anywhere, for the same reason as the rest of the redesign: a finished
piece's price is the live gold rate x weight plus wage, computed at order
time and never stored on the product.

FIXED a regression this redesign introduced before it shipped: dropping
ScrollFetchSearch from the search grid meant all 205 catalogue products
mounted at once (205 cards, 205 images). Both search and newest now render
a 20-item window that grows as the user reaches the end.

Verified in a real browser against the running API and native database, not
just a green build: home 13 cards, search 20 -> 40 -> 60 -> 80 on scroll,
newest 20 -> 40 -> 60, cart empty state, favorites 3 cards behind a seeded
session. All images loaded (0 broken), no horizontal overflow, no console
errors beyond pre-existing vue3-lazyload noise that predates this work.
vue-tsc and vite build both clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 01:01:19 +03:30
s.esmaeiliandClaude Opus 5 f2d83c53df user-app: redesigned home screen and product detail
Design language introduced here and reused across both screens:
  - LESS colour tokens (@ink/@inkSoft/@inkMuted/@inkFaint, @wine/@wineSoft,
    @canvas/@canvasSoft, @hairline/@hairlineSoft) added to
    themeConfig/colors.json and injected globally via vite's modifyVars, so
    scoped <style lang="less"> blocks resolve them with no import
  - BEM-ish scoped class names, hairline-bordered cards at 14-18px radii,
    ink surfaces with a wine accent, dashed-border empty states
  - components/home/: SectionHead, ShowcaseCard, ShowcaseSlider, CategoryRail,
    RateStrip, AccountStrip

Data honesty is deliberate throughout: no rial price is printed for a piece,
because a finished item's price is the live gold rate x weight plus wage,
computed at order time and never stored on the product. Only genuinely
present fields are shown (weight, making-charge percent, stock, category,
brand). Categories and the hero are derived from the real catalogue, so an
empty catalogue renders an empty state rather than placeholder tiles.

Also fixes a real bug in the product detail view: it ran, at module scope,
history.pushState + window.onpopstate = () => history.go(1). Because the
drawer mounts its slot eagerly, that fired for every product card on the
home screen and permanently hijacked the back button for the session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 23:05:29 +03:30
s.esmaeiliandClaude Opus 5 a12fb0450e api: don't replay the baseline migration on pre-fix databases
The folder-discovery fix in the previous commit surfaced a second problem.
Databases migrated BEFORE that fix recorded the baseline under the name of
the hand-added flat duplicate that used to sit beside it
(prisma/migrations/0001_init.sql). Once the runner started discovering the
real folder (20260910113720_first), it saw a name it had never recorded and
tried to apply the baseline again — against a database that already had it:

  Raw query failed. Code: 42710.
  ERROR: type "AdminRole" already exists

The two files are identical SQL (verified byte-for-byte after normalising
line endings; they differ only by one trailing blank line), so the recorded
legacy name genuinely means that migration is applied. A LEGACY_NAME_ALIASES
map now treats '0001_init.sql' as an already-applied marker for
'20260910113720_first', so upgrading an existing database is a no-op instead
of a crash.

Verified against the local dev database this actually broke on
(postgresql://localhost:5432/gold_banking, which had recorded only the old
flat name): 'npm run dev' in apps/api now logs 'No pending migrations' and
'Nest application successfully started' instead of dying. That database's
tracking table was also reconciled to record both real migration names, and
it was confirmed to already contain both migrations' schema (AdminRole,
City.percent, user_type, CustomerKind all present) — nothing was re-run
against it. The dockerised database is unaffected: it already records the
real names, so the alias is inert there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 21:44:14 +03:30
s.esmaeiliandClaude Sonnet 5 2b6795d007 api: fix migration runner, serve uploads correctly, add product photos
Three real bugs found and fixed while wiring up product images end-to-end:

1. src/prisma/migrate.ts only discovered flat prisma/migrations/<name>.sql
   files, never Prisma's own folder-per-migration layout
   (<timestamp>_<name>/migration.sql). Every migration created the normal
   way (via 'prisma migrate dev') was silently skipped; only a hand-added
   flat duplicate (prisma/migrations/0001_init.sql, now removed) ever ran.
   This meant schema.prisma had drifted well ahead of what any real deploy
   database actually had (missing columns on City, Customer, Branch,
   Product, several new tables). Fixed to discover both layouts; the
   drift is closed with a new 20260914165255_sync_schema_drift migration.

2. Neither nginx.conf (production) nor vite.config.ts (dev) proxied
   /uploads/ to the API — only /api/. UploadedFile/ProductImage URLs
   (/uploads/<file>, served by main.ts's useStaticAssets OUTSIDE the /api
   prefix) 404'd through both. Both now proxy /uploads/ the same way /api/
   already was.

3. docker-compose.yml's api service had no volume for storage/uploads, so
   every uploaded file — and by extension every ProductImage row pointing
   at one — was lost on the next container recreate/redeploy. Added a
   named volume.

Also: main.ts's static file serving now sets Content-Type: image/avif
explicitly (serve-static's bundled mime table predates .avif and served it
as application/octet-stream; browsers rendered it anyway by sniffing, but
a stricter client would not have).

Product photos: copied 15 real jewelry photos into the uploads volume,
registered them as UploadedFile rows, and assigned each of the 5 existing
Product rows a randomly picked one (duplicates allowed, per request),
replacing their /img/no-items.png placeholder ProductImage row. Verified
end-to-end in a real browser against the real containerized API+DB: logged
in, opened the admin product list, confirmed all 5 rows render their photo
(naturalWidth/Height populated, network 200s, no console errors).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 20:56:17 +03:30
s.esmaeiliandClaude Sonnet 5 a2ab9ba192 admin-panel: drop six more redundant add entries from the sidebar
Each now has an add button at the top of its own list page, so the nav
entry duplicated it:
  - 'افزودن دسته بندی'          (/category/add)
  - 'وارد کردن سند مالی مشتری'  (/add/findoc)
  - 'تعریف تخفیف  تک'           (/major/promotion/tak/add)
  - 'تعریف تخفیف  عمده'         (/major/promotion/add)
  - 'افزودن فاکتور'             (/invoice/add)
  - 'افزودن شهر'                (/city/create)

Only adminMenu is touched; all six routes still exist and still work as a
direct-link fallback. Parent submenus keep their remaining children
('تخفیف اتوماتیک' and 'لیست تخفیفات ' both still sit under 'تخفیفات', for
instance).

Verified in a real browser: menu items 26 -> 20, none of the six labels
render, all five parent submenus (دسته بندی ها، اسناد مالی، تخفیفات، فاکتور
انتقال، شهر ها) still present with their list children, icons unchanged at
16, no console errors. Committed dist/ artifact rebuilt to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 19:43:40 +03:30
s.esmaeiliandClaude Opus 5 d15129168a admin-panel: drop the four redundant add entries from the sidebar
Each of these now has an add button at the top of its own list page, so the
nav entry duplicated it:
  - 'اضافه کردن شخص'  (/customer/add)
  - 'افزودن شعب'      (/branch/add)
  - 'افزودن کارگاه'    (/workshop/add)
  - 'افزودن مدیر'      (/user/create)

Only the adminMenu tree is touched; branchMenu keeps its own /customer/add
entry, since the branch build has no converted add-modal. All four routes
still exist and still work as a direct-link fallback, and the parent
submenus keep their remaining children (including 'دسترسی ها' under مدیران).

Verified in a real browser: menu items 30 -> 26, none of the four labels
render, all four parent submenus still present with their list children,
icons unchanged at 16, no console errors. Committed dist/ artifact rebuilt
to match (force-added, as it is gitignored but deliberately tracked for
Dokploy).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:45:30 +03:30
s.esmaeiliandClaude Opus 5 4b038c0518 admin-panel: rebuild the committed deploy artifact from current source
dist/ is gitignored by apps/admin-panel/.gitignore but is deliberately
committed for Dokploy copy-only deploys (see root .gitignore's note), so it
is force-added. The previous artifact was a pre-port prototype build; the
previous commit removed its two stale assets without being able to add the
new ones, leaving index.html pointing at files absent from git. This restores
a self-consistent artifact built from current source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:35:13 +03:30
s.esmaeiliandClaude Opus 5 9b6b54dc71 admin-panel: build into the folder the Docker image actually serves
The Dockerfile does `COPY dist /usr/share/nginx/html` and nginx serves that
folder's root, but vite's outDir was 'dist/export' (carried over from legacy's
vue.config.js). So the deployed root kept serving whatever index.html had been
committed at dist/ — a stale pre-port prototype build (lang="en", title
"Gold Bank — Admin Panel", no ant-design-vue at all, committed 2026-09-10 in
cea3b80). That artifact, not the current source, renders as an unstyled stack
of bullets and bare tables.

outDir is now 'dist' (and 'branchDist'), matching what the image serves and
matching apps/user-app's convention.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:30:04 +03:30
s.esmaeiliandClaude Opus 5 acc41c21d9 admin-panel: import ant-design-vue 4 reset.css (fixes unstyled page)
The ADV4 upgrade dropped the old `ant-design-vue/dist/antd.less` import but
never added its replacement. ADV4 injects each COMPONENT's styles at runtime
via CSS-in-JS, but NOT the global baseline — that ships separately as
dist/reset.css and must be imported explicitly. Without it the page lost
box-sizing, the body margin reset and the browser default list/heading
resets, so the layout rendered unstyled.

Verified in a real headless browser this time, not just a green build:
reset.css now resolves through LESS and its rules are present in the built
CSS, the CSS-in-JS engine injects component styles (14 <style> tags on the
login page), and buttons/inputs/cards compute real ant metrics with dir=rtl.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:22:46 +03:30
s.esmaeiliandClaude Sonnet 5 0e1dd0c36a admin-panel: convert wholesale-customer add form into a modal
bulkCustomer.vue (reached today only via /customer/add's AddCustomer.vue tab
wrapper, and via /bulkCustomer/edit/:id from 5 different list variants) gets
open/id modal support; both routes stay as a fallback.

OmdeCustomerList.vue is shared by 5 list pages (OmdeList, BankerList,
ShopkeeperList, OmdeNonVerifiedList, OmdeArchivedList). Per user decision, the
add button and modal-based edit are wired only on OmdeList.vue (the one this
form conceptually belongs to) via a new showAddButton prop, default false —
the other four keep navigating to the edit route unchanged.

singleCustomer.vue and ReagentCustomer.vue were also planned as add-form
candidates but have no create entry point anywhere in the app (edit-only,
reachable solely via /singleCustomer/edit/:id and /reagent/edit/:id) — left
untouched, nothing to convert. offersIgnore-add-edit.vue is a standalone
settings page with no list counterpart, not a create form — left as a page.

Verified: pnpm build passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 17:05:42 +03:30
s.esmaeiliandClaude Sonnet 5 1bb9802d1f admin-panel: convert promotion add/edit forms into modals
PromotionForm.vue (shared by the omde and tak variants) gains open/id modal
support; both thin wrappers (promotion_add_edit.vue, promotion_tak_add_edit.vue)
pass the props/emits through unchanged. promotion_list.vue gets two
ListPageHeader buttons (offers.create / offers.create.tak, matching the two
routes' rids) and picks which variant's modal to open based on the edited
row's type_customer, exactly as the old toEdit() picked which route to
navigate to.

Verified: pnpm build passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 16:56:30 +03:30
s.esmaeiliandClaude Sonnet 5 3345c86d18 admin-panel: convert financial document add/edit form into a modal
Same open/id pattern as the previous conversions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 16:53:02 +03:30
s.esmaeiliandClaude Sonnet 5 3e24fe3fc7 admin-panel: convert invoice add/edit form into a modal
Same open/id pattern, kept at width=xl since it embeds its own product-picker
XTable plus a running cart table. Also deduped ListInvoice's confirmDelete
onto the shared useDeleteHandler composable (missed in the earlier delete-
handler pass since it wasn't byte-identical to the other 6 — it used
message.success + a different fallback string, both preserved).

Verified: pnpm build passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 16:51:16 +03:30
s.esmaeiliandClaude Sonnet 5 a8988c18a2 admin-panel: convert user and category add/edit forms into modals
Same pattern as branch/workshop/city: open/id props switch UserCreate.vue and
apps/category/Add.vue into BaseFormModal mode; the /user/create,
/user/edit/:id and /category/add/:id? routes are unchanged as a fallback.
ListUser.vue's pre-existing add button (previously a router.push, added
outside the original port and left there per its own comment) now opens the
modal via ListPageHeader instead.

Verified: pnpm build passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 16:49:24 +03:30
s.esmaeiliandClaude Sonnet 5 1ac3b03ad5 admin-panel: convert branch and workshop add/edit forms into modals
Both share FacilityList.vue, which now accepts an addRid/addLabel pair and a
#form slot: when addRid is supplied, edit opens the slot's modal instead of
navigating, and a ListPageHeader add button appears above the list. Branch
and workshop each plug their own AddBranches/AddWorkshop instance into that
slot, keyed by editingId so switching rows remounts the form (fresh
onMounted load) rather than needing new watch-based reactivity.

The /branch/add, /branch/edit/:id, /workshop/add, /workshop/edit/:id routes
are unchanged and still work as a route-based fallback.

Verified: pnpm build passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 16:46:14 +03:30
s.esmaeiliandClaude Sonnet 5 12c8195090 admin-panel: convert city create form into a modal (pilot)
Establishes the pattern the remaining 15 add/edit forms will follow:
- The form component accepts open?/id? props. When open is defined it's
  used as-is; asModal is fixed at mount (not reactive) since the parent
  decides once whether this instance is a modal or a route page.
- As a modal it renders through BaseFormModal (header=title, footer=
  buttons, body=form) and emits success + update:open instead of
  navigating; as a route page (the old /city/create still exists and still
  works) it falls back to the original <a-card> + button layout unchanged.
- The list page gets a ListPageHeader add button above the list, gated on
  the exact rid the route used to gate on (cities.create), opening the
  modal instead of navigating.

Verified: pnpm build passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 15:22:54 +03:30
s.esmaeiliandClaude Sonnet 5 a6d8993d01 admin-panel: render sidebar icons, fix openKeys, persist collapse state
- VerticalNavMenu.vue now renders the icon: values that navMenuItems.ts
  already carried on 33 nodes but were never displayed, via the shared
  resolveIcon() map. Sub-menus and leaf items get a #icon slot each; ADV4's
  Menu shows icons only (labels hidden) once :inline-collapsed is true, which
  is now wired to the collapsed prop.
- Fixed openKeys: it previously started at [] and was only ever updated by
  user clicks, so a direct link or page refresh landed with the active
  item's parent submenu collapsed. It's now derived from the current route
  on load and on every navigation.
- Sidebar collapse state moves from a local ref in Main.vue to
  stores/layout.ts's existing (previously unused) sidebarCollapsed, persisted
  to localStorage so it survives a reload.

Verified: pnpm build passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 15:20:47 +03:30
s.esmaeiliandClaude Sonnet 5 02d92fc465 admin-panel: add shared modal/header/icon/delete-handler components
New src/components/common/:
- BaseFormModal.vue — the standard shell every converted add/edit modal will
  use: header=title, footer=buttons, body=form. Replaces the ad-hoc
  60%/70%/80% <a-modal> widths used across the codebase with 4 named sizes.
- ListPageHeader.vue — the add button that goes above every list page,
  gated on the same rid the create route used (modelled on the one place
  this already existed, ListUser.vue's add-user button).

New src/components/icons.ts: the legacy-Feather-name -> ant-icon map,
extracted out of TooltipBtn.vue so the sidebar can resolve the same names
through the same map instead of a second copy.

New src/composables/useDeleteHandler.ts: the delete-then-refetch-then-toast
handler that was copy-pasted verbatim in 6 list views (city/cityList.vue,
user/ListUser.vue, apps/category/List.vue, finical/list.vue,
apps/product/ProductList.vue, manage/Const.vue). Preserves each site's exact
success-message wording and message.success/info choice, and ProductList's
irregular POST-based delete endpoint.

Verified: pnpm build passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 15:18:17 +03:30
s.esmaeiliandClaude Sonnet 5 f0de478940 admin-panel: fix CSS bugs and dedupe scoped grid utilities
Approved departure from the port's byte-faithful rule (docs/ADMIN-PORT-PLAN.md
bug rows 4-6), fixed and logged rather than silently changed:
- farsi.less: bold/700 @font-face blocks now load iran-yekan-700.woff2
  instead of the 400 file, so bold text actually renders bold. No source
  file exists for weight 300, so that face is dropped rather than faked.
- format("woff") corrected to format("woff2") on all faces.
- Removed 'font-weight: inherit' from the body,* reset, which was flattening
  every heading's weight.

Also deduped 24 view files that each redeclared .grid-2/.grid-3/.grid-4 in
a scoped <style> block with drifted minmax values, though utilities.less
already defines all three globally. Removed the scoped copies; kept
PromotionForm's genuinely different 0.5rem gap as a small .grid-4--tight
modifier instead of a full redeclaration.

Verified: pnpm build passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 15:09:11 +03:30
s.esmaeiliandClaude Sonnet 5 7e30701570 admin-panel: upgrade to ant-design-vue 4.2.6
- Bump ant-design-vue 3.2.20 -> 4.2.6, @ant-design/icons-vue 6.1.0 -> 7.0.1.
- ADV4 removed LESS theming (modifyVars) for CSS-in-JS design tokens: drop
  the vite.config.ts modifyVars block and the antd.less import; move the
  three tokens ant actually consumed (primary/link/border-radius) to
  App.vue's <a-config-provider :theme>.
- Delete src/assets/styles/ant.less (426 lines of hand-written RTL flips):
  ADV4 renders RTL natively via CSS logical properties given direction="rtl",
  which App.vue already sets. Preserve only the small residue ADV4 doesn't
  cover natively (upload filename LTR, select dropdown z-index, tabs nav
  direction) in a new rtl-overrides.css.
- Clean up the stale --primary/--accent token comment in utilities.less now
  that ant.less is gone.
- Verified: pnpm build (vue-tsc -b && vite build) passes with zero errors;
  the codebase already used v-model:open everywhere (no ADV3 visible prop
  usage to migrate).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 15:05:05 +03:30
s.esmaeili 8fac6a3f36 merge: integrate origin/main with admin panel and api migrations 2026-09-13 21:05:20 +03:30
s.esmaeili 267fc7b7d1 feat: complete admin panel and backend admin api migration 2026-09-13 21:02:25 +03:30
mahmood f19f49daeb feat: auto-run SQL migrations and one-time seed on boot 2026-09-10 17:15:17 +03:30
mahmood a4c5c9259f Fix some for docker ' 2026-09-10 14:59:25 +03:30
mahmood 1b675804ea fix: nginx dns re-resolve, db healthcheck, explicit bind 2026-09-10 14:51:57 +03:30
mahmood 7d93507089 fix: run API on bookworm with debian Prisma engine 2026-09-10 14:45:47 +03:30
mahmood 88bb7b1fbf fix: use bookworm + debian Prisma engine 2026-09-10 13:58:50 +03:30
mahmood bbbe6a5202 fix: add linux-musl Prisma engines for Alpine 2026-09-10 13:00:05 +03:30
mahmood f0f82d20ec fix: vendor Prisma into release for Docker 2026-09-10 12:53:23 +03:30
mahmood 9e8d4a017d Fix port docker 2026-09-10 12:43:30 +03:30
mahmood cea3b808d8 Fix some for production 2026-09-10 12:17:47 +03:30
mahmood 78e148a6b5 IFx some for eror on build docker 2026-09-10 01:00:00 +03:30
mahmood 3e3cb4b3c0 fix some for mirror 2026-09-10 00:48:06 +03:30
mahmood 56caedb8b2 fix use mirror 2026-09-10 00:25:43 +03:30
mahmood 05a9837695 fix some for build 2026-09-10 00:18:53 +03:30
mahmood c59ca8e1dc Fix some for deploy 2026-09-10 00:13:03 +03:30
mahmood d65c67e440 Fix db external port 2026-09-09 23:31:31 +03:30
s.esmaeili 03119cd655 docs: add comprehensive Docker usage documentation (DOCKER.md)
Includes explanation of multi-stage Turborepo builds, Nginx SPA routing, and docker-compose deployment steps.
2026-09-09 19:49:27 +03:30
s.esmaeili 78a68b3567 chore: add docker configuration 2026-09-09 19:39:13 +03:30
s.esmaeili 0ed6bed028 feat: initial commit 2026-09-09 06:46:27 +03:30
1100 changed files with 25468 additions and 21000 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)
```
+5 -22
View File
@@ -1,10 +1,9 @@
# Database configuration # Database configuration
POSTGRES_USER=postgres POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres POSTGRES_PASSWORD=postgres
POSTGRES_DB=hedenagold POSTGRES_DB=goldbank
# Host port for PostgreSQL. Each copy of this stack on the same machine # Host port exposed for PostgreSQL (used by docker-compose db service)
# must use a different port (the apps talk to hostname "db", not this port).
EXTERNAL_DB_PORT=5432 EXTERNAL_DB_PORT=5432
# Prisma connection string (inside Docker network use hostname "db") # Prisma connection string (inside Docker network use hostname "db")
@@ -20,6 +19,7 @@ DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGR
API_PORT=3000 API_PORT=3000
ADMIN_PANEL_PORT=5180 ADMIN_PANEL_PORT=5180
USER_APP_PORT=5181 USER_APP_PORT=5181
USER_APP_V2_PORT=5182
# API Configuration (kept for any tooling that still reads bare PORT) # API Configuration (kept for any tooling that still reads bare PORT)
PORT=3000 PORT=3000
@@ -130,6 +130,8 @@ SMS_RELAY_SECRET=
# The "host" field. Blank sends this machine's first external IPv4 — inside # The "host" field. Blank sends this machine's first external IPv4 — inside
# Docker that is the container's address. # Docker that is the container's address.
SMS_RELAY_HOST= 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. # trez — only used with SMS_PROVIDER=trez. Never commit real values.
# Override only for staging; defaults to the provider's live endpoint. # Override only for staging; defaults to the provider's live endpoint.
SMS_TREZ_URL= SMS_TREZ_URL=
@@ -152,22 +154,3 @@ UPLOAD_DIR=
# instead so no personal number is committed. Blank means nobody is notified — # instead so no personal number is committed. Blank means nobody is notified —
# the upload and the customer's own SMS still go through (D20). # the upload and the customer's own SMS still go through (D20).
PROFILE_ADMIN_MOBILES= PROFILE_ADMIN_MOBILES=
# ---------------------------------------------------------------------------
# Order placement
# ---------------------------------------------------------------------------
# Comma-separated admin mobiles told when a customer places an order. The
# reference hardcoded four numbers (henza_server
# Api/ApiMobileController::setOrder:740-743). Blank means nobody is notified —
# the order, the customer's SMS and the sales expert's SMS still go through.
ORDER_ADMIN_MOBILES=
# ---------------------------------------------------------------------------
# Brand
# ---------------------------------------------------------------------------
# The name at the top of every SMS («هدنا گلد\n…») and the site link in the
# welcome SMS. Blank = the defaults in apps/api/src/common/brand.ts. The
# frontends take their brand from the repo-root brand.ts instead; the full
# rebrand checklist is docs/BRANDING.md.
BRAND_NAME=
BRAND_SITE_URL=
+1 -8
View File
@@ -6,8 +6,6 @@ node_modules/
*.log *.log
*.tsbuildinfo *.tsbuildinfo
.DS_Store .DS_Store
.idea/
.cursor/
apps/api/generated/ apps/api/generated/
apps/api/dist/ apps/api/dist/
apps/api/storage/uploads/* apps/api/storage/uploads/*
@@ -18,11 +16,6 @@ apps/api/storage/uploads/*
# apps/admin-panel/dist/ # apps/admin-panel/dist/
# apps/user-app/dist/ # apps/user-app/dist/
# Live legacy data dump (customers' personal data). Never commit. # Live henza data dump (customers' personal data). Never commit.
# Only the structure-only oapi-struncture.sql belongs in the repo. # Only the structure-only oapi-struncture.sql belongs in the repo.
/oapi.sql /oapi.sql
# Legacy Henza reference trees — keep locally, never push to Hedena Gold.
/henza_admin/
/henza_pwa/
/henza_server/
+10
View File
@@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/gold-bank.iml" filepath="$PROJECT_DIR$/.idea/gold-bank.iml" />
</modules>
</component>
</project>
Generated
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCSFixerOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCodeSnifferOptionsConfiguration">
<option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" />
</component>
<component name="PhpStanOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PsalmOptionsConfiguration">
<option name="transferred" value="true" />
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+2 -2
View File
@@ -208,8 +208,8 @@ container.
`SMS_PROVIDER`: `relay` (default), `trez`, or `none` (D26). `SMS_PROVIDER`: `relay` (default), `trez`, or `none` (D26).
- **relay**: `SMS_RELAY_URL`, `SMS_RELAY_SECRET` (both have built-in - **relay**: `SMS_RELAY_URL`, `SMS_RELAY_SECRET` (both have built-in
defaults), `SMS_RELAY_HOST` (blank = the machine's first external IPv4). defaults), `SMS_RELAY_HOST` (blank = the machine's first external IPv4),
No `platform` is sent; the gateway picks the line. `SMS_RELAY_PLATFORM` (`kave` default, or `default` to let the gateway pick).
- **trez**: `SMS_TREZ_URL`, `SMS_TREZ_USERNAME`, `SMS_TREZ_PASSWORD`, - **trez**: `SMS_TREZ_URL`, `SMS_TREZ_USERNAME`, `SMS_TREZ_PASSWORD`,
`SMS_TREZ_SENDER_LINE`. `SMS_TREZ_SENDER_LINE`.
+6 -11
View File
@@ -8,7 +8,7 @@ Dokploy only builds thin images that copy those files (no `pnpm install` on the
```bash ```bash
pnpm install pnpm install
pnpm build:release pnpm build:release
git add -f apps/api/release apps/admin-panel/dist apps/user-app/dist git add -f apps/api/release apps/admin-panel/dist apps/user-app/dist apps/user-app-v2/dist
git commit -m "chore: refresh release artifacts" git commit -m "chore: refresh release artifacts"
git push git push
``` ```
@@ -20,31 +20,26 @@ What this produces:
| API | `apps/api/release/app.js` (+ Prisma under `release/vendor`) | `docker.arvancloud.ir/node:20-bookworm` | | API | `apps/api/release/app.js` (+ Prisma under `release/vendor`) | `docker.arvancloud.ir/node:20-bookworm` |
| Admin | `apps/admin-panel/dist/` | `docker.arvancloud.ir/nginx:latest` | | Admin | `apps/admin-panel/dist/` | `docker.arvancloud.ir/nginx:latest` |
| User | `apps/user-app/dist/` | `docker.arvancloud.ir/nginx:latest` | | User | `apps/user-app/dist/` | `docker.arvancloud.ir/nginx:latest` |
| User v2 | `apps/user-app-v2/dist/` | `docker.arvancloud.ir/nginx:latest` |
## Dokploy Environment ## Dokploy Environment
```env ```env
POSTGRES_USER=hedenagold POSTGRES_USER=goldbank
POSTGRES_PASSWORD=... POSTGRES_PASSWORD=...
POSTGRES_DB=hedenagold POSTGRES_DB=goldbank
EXTERNAL_DB_PORT=5433 EXTERNAL_DB_PORT=5433
DATABASE_URL=postgresql://hedenagold:...@db:5432/hedenagold?schema=public DATABASE_URL=postgresql://goldbank:...@db:5432/goldbank?schema=public
PORT=3000 PORT=3000
``` ```
A second copy on the **same server** is a separate Dokploy application (its own
domains, passwords, and `JWT_SECRET`). Do not set `container_name` in this file:
those names are global in Docker, and a fixed `hedenagold_db` collides with the
first copy. Give the second app its own `EXTERNAL_DB_PORT` (for example `5434`
if `5433` is already taken). Volumes stay separate because Dokploy uses a
different Compose project name for each application.
## Domains (container ports) ## Domains (container ports)
| Service | Domain | Port | | Service | Domain | Port |
|---|---|---| |---|---|---|
| `admin-panel` | admin.example.com | **80** | | `admin-panel` | admin.example.com | **80** |
| `user-app` | app.example.com | **80** | | `user-app` | app.example.com | **80** |
| `user-app-v2` | app-v2.example.com | **80** |
| `api` | (optional) | **3000** | | `api` | (optional) | **3000** |
Frontends call `/api` on their own domain; nginx proxies to `api:3000`. Frontends call `/api` on their own domain; nginx proxies to `api:3000`.
+10
View File
@@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/apps.iml" filepath="$PROJECT_DIR$/.idea/apps.iml" />
</modules>
</component>
</project>
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCSFixerOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCodeSnifferOptionsConfiguration">
<option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" />
</component>
<component name="PhpStanOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PsalmOptionsConfiguration">
<option name="transferred" value="true" />
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
@@ -1 +0,0 @@
import{I as h}from"./Icon-B9FXOmlS.js";import{d as w,v as B,x as l,o as C,c as S,w as u,e as $,t as A,p as E,f as L,k as c,P as p,M as d,A as j,r as M}from"./index-QQcP5-AC.js";const V=w({__name:"AccountingSyncButton",props:{endpoint:{}},emits:["done"],setup(m,{emit:f}){const v=m,_=f,s=c(!1),r=c(null),a=c(!1),k={customers:"مشتریان",balances:"مانده‌ها",products:"کارساخته‌ها و موجودی"},y={scanned:"بررسی‌شده",inScope:"در گروه‌های انتخابی",refreshed:"به‌روز",linked:"لینک‌شده",imported:"وارد شده",issues:"مغایرت",updated:"به‌روز",items:"کارساخته",stockErrors:"خطای موجودی"},g=e=>e&&typeof e=="object"?Object.entries(e).map(([o,t])=>`${y[o]??o}: ${t}`).join("، "):"";B(async()=>{try{const{data:e}=await l.get("admin/accounting-status");s.value=!!(e!=null&&e.connected),r.value=(e==null?void 0:e.provider)??null}catch{s.value=!1}});async function x(){var e,o;a.value=!0;try{const{data:t}=await l.post(v.endpoint),b=Object.entries(t.results).map(([i,n])=>p("p",{style:n.ok?"":"color:#d4380d"},`${k[i]??i}: ${n.ok?g(n.summary)||"انجام شد":`خطا — ${n.error}`}`));(t.ok?d.success:d.warning)({title:t.ok?"سینک با حسابداری انجام شد":"سینک با حسابداری کامل انجام نشد",content:p("div",b),okText:"باشه"}),_("done")}catch(t){j.error(((o=(e=t==null?void 0:t.response)==null?void 0:e.data)==null?void 0:o.message)||"سینک با حسابداری انجام نشد")}finally{a.value=!1}}return(e,o)=>{const t=M("a-button");return s.value?(C(),S(t,{key:0,loading:a.value,onClick:x},{icon:u(()=>[E(h,{icon:"vuesax-linear:refresh-2",size:"16",class:"icon-spin"})]),default:u(()=>[$(" سینک با "+A(r.value||"حسابداری"),1)]),_:1},8,["loading"])):L("",!0)}}});export{V as _};
@@ -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
@@ -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
@@ -1 +0,0 @@
import{_ as o}from"./Add.vue_vue_type_script_setup_true_lang-C2L5ywNo.js";import"./index-QQcP5-AC.js";import"./BaseFormModal.vue_vue_type_style_index_0_lang-BOlb0dJf.js";import"./ImageUploadInput.vue_vue_type_script_setup_true_lang-gPDguZj2.js";import"./Icon-B9FXOmlS.js";export{o 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 @@
import{_ as o}from"./AddBranches.vue_vue_type_script_setup_true_lang-Du71-Nbh.js";import"./index-QQcP5-AC.js";import"./BaseFormModal.vue_vue_type_style_index_0_lang-BOlb0dJf.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};
@@ -0,0 +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-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 +0,0 @@
import{d as T,v as G,x as c,c as v,w as l,q as R,i as H,k as S,y as j,r as i,o as p,m as C,p as n,a as O,b as $,e as g,t as M,F as N,B as I,A as V,j as J}from"./index-QQcP5-AC.js";import{_ as K}from"./BaseFormModal.vue_vue_type_style_index_0_lang-BOlb0dJf.js";const P={class:"grid-2"},Q={class:"grid-2"},Y=T({__name:"AddBranches",props:{open:{type:Boolean},id:{}},emits:["update:open","success"],setup(y,{emit:z}){const h=y,w=z,f=h.open!==void 0,A=H(),E=J(),r=f?h.id:A.params.id,m=S(!1),_=S([]),t=j({name:"",phone:"",address:"",user_id:void 0}),d=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(d).forEach(o=>delete d[o]),!t.name){d.name="لطفا مقدار را وارد کنید";return}try{m.value=!0,r?await c.put(`branch/${r}`,t):await c.post("branch",t),V.info("اطلاعات با موفقیت به ثبت رسید."),r||Object.assign(t,{name:"",phone:"",address:"",user_id:void 0}),f?(w("success"),w("update:open",!1)):E.push("/branch/list")}catch(o){V.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(r)try{const{data:s}=await c.get(`branch/${r}`);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"),q=i("a-card"),D=i("a-form"),L=i("a-spin");return f?(p(),v(K,{key:0,open:!!y.open,id:R(r),"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":d.name?"error":"",help:d.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(N,null,$(_.value,a=>(p(),v(U,{key:a.id,value:a.id},{default:l(()=>[g(M(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(L,{key:1,spinning:m.value},{default:l(()=>[n(D,{layout:"vertical",onSubmit:I(b,["prevent"])},{default:l(()=>[n(q,{title:R(r)?"ویرایش شعبه":"افزودن شعبه",class:"mb-base page-card"},{default:l(()=>[C("div",Q,[n(u,{label:"نام شعبه","validate-status":d.name?"error":"",help:d.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(N,null,$(_.value,a=>(p(),v(U,{key:a.id,value:a.id},{default:l(()=>[g(M(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",class:"form-submit",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{I as c}from"./Icon-B9FXOmlS.js";import{d as b,u as g,k as x,a as n,m as s,p as l,w,F as _,b as B,c as h,a1 as S,f as u,i as R,g as z,r as F,o,j as A,n as I,t as k,_ as N}from"./index-QQcP5-AC.js";import{_ as V}from"./bulkCustomer.vue_vue_type_script_setup_true_lang-UhBsQkgG.js";import{R as D}from"./ReagentCustomer-DkAyHy8l.js";import{S as P}from"./singleCustomer-XwT9-VyX.js";import"./BaseFormModal.vue_vue_type_style_index_0_lang-BOlb0dJf.js";import"./ImageUploadInput.vue_vue_type_script_setup_true_lang-gPDguZj2.js";import"./CityTreeSelect.vue_vue_type_script_setup_true_lang-qW84W-31.js";import"./DateInput-COU3oLhL.js";import"./index-DT4Uze1z.js";const $={id:"page-user-view",class:"pf-page"},j={class:"pf-head"},q={class:"kind-picker",role:"tablist"},E=["aria-selected","onClick"],L={class:"kind-card__icon"},M={class:"kind-card__text"},O=b({__name:"AddCustomer",setup(T){const v=R(),d=A(),p=g(),t=x(v.query.tab||"omde"),r=a=>!!p.hasPermission(a),i=z(()=>{const a=[];return r("persons.create.client")&&a.push({key:"omde",title:"مشتری عمده",hint:"مغازه‌دار، بنکدار و خریدار عمده",icon:"vuesax-linear:shop"}),r("persons.create.seller")&&a.push({key:"reagent",title:"کارشناس فروش",hint:"کارشناس با کمیسیون و سقف اعتبار",icon:"vuesax-linear:personalcard"}),p.isSuperAdmin&&a.push({key:"tak",title:"مشتری تک",hint:"مشتری خرده‌فروشی حضوری",icon:"vuesax-linear:user"}),a}),f={omde:V,reagent:D,tak:P};function y(){window.history.length>1?d.back():d.push("/customer/omde-list")}return(a,m)=>{const C=F("a-button");return o(),n("div",$,[s("header",j,[l(C,{class:"pf-head__back",shape:"circle",onClick:y},{icon:w(()=>[l(c,{icon:"vuesax-linear:arrow-right",size:"16"})]),_:1}),m[0]||(m[0]=s("div",{class:"pf-head__text"},[s("h1",null,"افزودن شخص"),s("p",null,"نوع شخص را انتخاب و اطلاعات او را تکمیل کنید.")],-1))]),r("persons.create")&&i.value.length?(o(),n(_,{key:0},[s("div",q,[(o(!0),n(_,null,B(i.value,e=>(o(),n("button",{key:e.key,type:"button",role:"tab",class:I(["kind-card",{"is-on":t.value===e.key}]),"aria-selected":t.value===e.key,onClick:U=>t.value=e.key},[s("span",L,[l(c,{icon:e.icon,size:"22"},null,8,["icon"])]),s("span",M,[s("b",null,k(e.title),1),s("small",null,k(e.hint),1)]),t.value===e.key?(o(),h(c,{key:0,icon:"vuesax-linear:tick-circle",size:"16",class:"kind-card__check"})):u("",!0)],10,E))),128))]),i.value.some(e=>e.key===t.value)?(o(),h(S(f[t.value]),{key:t.value})):u("",!0)],64)):u("",!0)])}}}),se=N(O,[["__scopeId","data-v-84479c56"]]);export{se as default};
-1
View File
@@ -1 +0,0 @@
b[data-v-84479c56]{font-weight:500}.kind-picker[data-v-84479c56]{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;margin-bottom:20px}.kind-card[data-v-84479c56]{position:relative;display:flex;align-items:center;gap:12px;padding:14px 16px;border:1.5px solid #eef0f5;border-radius:16px;background:#fff;text-align:start;cursor:pointer;font:inherit;transition:border-color .2s ease,box-shadow .2s ease,background .2s ease}.kind-card[data-v-84479c56]:hover{border-color:#c9d0dd}.kind-card.is-on[data-v-84479c56]{border-color:#1d4ed8;background:#1d4ed808;box-shadow:0 0 0 3px #1d4ed814}.kind-card__icon[data-v-84479c56]{width:42px;height:42px;flex-shrink:0;display:grid;place-items:center;border-radius:12px;font-size:16px;color:#8a94a6;background:#f2f4f7;transition:color .2s ease,background .2s ease}.kind-card.is-on .kind-card__icon[data-v-84479c56]{color:#fff;background:#1d4ed8}.kind-card__text[data-v-84479c56]{display:flex;flex-direction:column;gap:2px;min-width:0}.kind-card__text b[data-v-84479c56]{font-size:14px;color:#101828}.kind-card__text small[data-v-84479c56]{font-size:12px;color:#8a94a6}.kind-card__check[data-v-84479c56]{margin-inline-start:auto;font-size:16px;color:#1d4ed8}@media (max-width: 575px){.kind-picker[data-v-84479c56]{grid-template-columns:minmax(0,1fr);gap:8px;margin-bottom:12px}.kind-card[data-v-84479c56]{padding:10px 12px}.kind-card__icon[data-v-84479c56]{width:36px;height:36px;font-size:16px}}
+1
View File
@@ -0,0 +1 @@
.mt-4[data-v-f195ac0c]{margin-top:1rem}.mt-5[data-v-f195ac0c]{margin-top:1.25rem}
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
.mt-4[data-v-93be6d1e]{margin-top:1rem}.mt-5[data-v-93be6d1e]{margin-top:1.25rem}
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
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}
+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-C26658Z1.js";import"./index-QQcP5-AC.js";import"./BaseFormModal.vue_vue_type_style_index_0_lang-BOlb0dJf.js";export{o as default};
@@ -1 +0,0 @@
import{d as O,v as S,x as v,c as b,w as n,q as g,i as V,k as z,y,r as p,o as U,m as x,p as o,e as A,B as q,A as c,j as E}from"./index-QQcP5-AC.js";import{_ as T}from"./BaseFormModal.vue_vue_type_style_index_0_lang-BOlb0dJf.js";const W={class:"grid-3"},D={class:"grid-3"},H=O({__name:"AddWorkshop",props:{open:{type:Boolean},id:{}},emits:["update:open","success"],setup(f,{emit:B}){const h=f,w=B,_=h.open!==void 0,R=V(),j=E(),d=_?h.id:R.params.id,i=z(!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 S(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"),M=p("a-form"),N=p("a-spin");return _?(U(),b(T,{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",W,[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(N,{key:1,spinning:i.value},{default:n(()=>[o(M,{layout:"vertical",onSubmit:q(m,["prevent"])},{default:n(()=>[o($,{title:g(d)?"ویرایش کارگاه":"افزودن کارگاه",class:"mb-base page-card"},{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",class:"form-submit",onClick:m},{default:n(()=>[...a[13]||(a[13]=[A("ثبت اطلاعات",-1)])]),_:1})]),_:1},8,["title"])]),_:1})]),_:1},8,["spinning"]))}}});export{H as _};
@@ -0,0 +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-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
@@ -1 +0,0 @@
.stats[data-v-eef9d79c]{display:flex;flex-wrap:wrap;gap:.5rem}.mt-4[data-v-eef9d79c]{margin-top:1rem}@media (max-width: 575px){.mt-4[data-v-eef9d79c]{margin-top:12px}.stats[data-v-eef9d79c] .ant-tag{margin:0}.detail-tabs[data-v-eef9d79c] .ant-tabs-nav{margin-bottom:8px}.detail-tabs[data-v-eef9d79c] .ant-table-content{overflow-x:auto}.detail-tabs[data-v-eef9d79c] .ant-table-content>table{min-width:max-content}}
+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
@@ -1 +0,0 @@
import{d as D,v as S,x as A,c as b,w as a,i as F,k as y,r as d,o as i,p as l,e as n,t as s,m as q,a as p,F as f,q as I,f as k,_ as E}from"./index-QQcP5-AC.js";import{j}from"./filters-B9z4I-pb.js";import"./index-DT4Uze1z.js";const M={class:"stats mt-4"},R=D({__name:"AgentDetail",setup(T){const C=F().params.id,m=y(!1),o=y(void 0),h=[{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/${C}`);o.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"),x=d("a-tab-pane"),N=d("a-tabs"),V=d("a-spin");return i(),b(V,{spinning:m.value},{default:a(()=>[l(B,{title:"مشاهده اطلاعات کارشناس فروش",class:"page-card"},{default:a(()=>[l(z,{bordered:"",size:"small",column:{xxl:3,xl:3,lg:3,md:2,sm:2,xs:1}},{default:a(()=>[l(c,{label:"نام"},{default:a(()=>{var e;return[n(s(((e=o.value)==null?void 0:e.name)??"-"),1)]}),_:1}),l(c,{label:"موبایل"},{default:a(()=>{var e;return[n(s(((e=o.value)==null?void 0:e.mobile)??"-"),1)]}),_:1}),l(c,{label:"شعبه"},{default:a(()=>{var e,t;return[n(s(((t=(e=o.value)==null?void 0:e.branch)==null?void 0:t.name)??"-"),1)]}),_:1}),l(c,{label:"درصد کمیسیون"},{default:a(()=>{var e;return[n(s(((e=o.value)==null?void 0:e.percent)??"-"),1)]}),_:1}),l(c,{label:"وضعیت"},{default:a(()=>{var e;return[l(u,{color:((e=o.value)==null?void 0:e.active)===1?"green":"red"},{default:a(()=>{var t;return[n(s(((t=o.value)==null?void 0:t.active)===1?"فعال":"غیر فعال"),1)]}),_:1},8,["color"])]}),_:1}),l(c,{label:"میزان فروش (گرم)"},{default:a(()=>{var e;return[n(s(((e=o.value)==null?void 0:e.grams_sell)??0),1)]}),_:1})]),_:1}),q("div",M,[l(u,{color:"blue"},{default:a(()=>{var e,t;return[n("مشتریان معرفی‌شده: "+s(((t=(e=o.value)==null?void 0:e.referred_customers)==null?void 0:t.length)??0),1)]}),_:1}),l(u,{color:"green"},{default:a(()=>{var e,t;return[n("سفارش‌های ثبت‌شده: "+s(((t=(e=o.value)==null?void 0:e.orders)==null?void 0:t.length)??0),1)]}),_:1})])]),_:1}),l(N,{class:"mt-4 detail-tabs","default-active-key":"referred"},{default:a(()=>[l(x,{key:"referred",tab:"مشتریان معرفی‌شده"},{default:a(()=>{var e;return[l(v,{size:"small","data-source":(e=o.value)==null?void 0:e.referred_customers,columns:h,pagination:{pageSize:10},"row-key":"id"},{bodyCell:a(({column:t,record:r})=>{var g;return[t.dataIndex==="active"?(i(),b(u,{key:0,color:r.active===1?"green":"red"},{default:a(()=>[n(s(r.active===1?"فعال":"غیر فعال"),1)]),_:2},1032,["color"])):t.dataIndex==="city"?(i(),p(f,{key:1},[n(s(((g=r.city)==null?void 0:g.title)??"-"),1)],64)):t.dataIndex==="caj"?(i(),p(f,{key:2},[n(s(I(j)(r.caj)),1)],64)):k("",!0)]}),_:1},8,["data-source"])]}),_:1}),l(x,{key:"orders",tab:"سفارش‌های مشتریان"},{default:a(()=>{var e;return[l(v,{size:"small","data-source":(e=o.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},[n(s(I(j)(r.caj)),1)],64)):k("",!0)]),_:1},8,["data-source"])]}),_:1})]),_:1})]),_:1},8,["spinning"])}}}),L=E(R,[["__scopeId","data-v-eef9d79c"]]);export{L as default};
+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 m,a as i}from"./OmdeCustomerList-fAbvz4EO.js";import{d as p,c as s,q as t,o as a}from"./index-QQcP5-AC.js";import"./Icon-B9FXOmlS.js";import"./index-DT4Uze1z.js";import"./model-m8OsHE_l.js";import"./filters-B9z4I-pb.js";import"./ListPageHeader.vue_vue_type_script_setup_true_lang-BBMCN2Q8.js";import"./AccountingSyncButton.vue_vue_type_script_setup_true_lang-Ce4J8HI3.js";import"./bulkCustomer.vue_vue_type_script_setup_true_lang-UhBsQkgG.js";import"./BaseFormModal.vue_vue_type_style_index_0_lang-BOlb0dJf.js";import"./ImageUploadInput.vue_vue_type_script_setup_true_lang-gPDguZj2.js";import"./CityTreeSelect.vue_vue_type_script_setup_true_lang-qW84W-31.js";import"./DateInput-COU3oLhL.js";const S=p({__name:"BankerList",setup(n){const o=m.banker.title,e=r("banker");return(l,c)=>(a(),s(i,{title:t(o),model:t(e)},null,8,["title","model"]))}});export{S as default};
@@ -1 +0,0 @@
@media (max-width: 575px){.base-form-modal .ant-modal{width:100vw!important;max-width:100vw;top:0;margin:0;padding-bottom:0}.base-form-modal .ant-modal-content{display:flex;flex-direction:column;min-height:calc(var(--vh, 1vh) * 100);border-radius:0;padding:14px 16px 0}.base-form-modal .ant-modal-header{margin-bottom:12px}.base-form-modal .ant-modal-body{flex:1}.base-form-modal .ant-modal-footer{position:sticky;bottom:0;display:flex;gap:8px;margin:0 -16px;padding:10px 16px;background:#fff;border-top:1px solid #f0f0f0}.base-form-modal .ant-modal-footer>.ant-btn{flex:1;margin:0!important}}
@@ -0,0 +1 @@
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 +0,0 @@
import{d as r,c as g,w as o,r as a,o as k,p as i,O as w}from"./index-QQcP5-AC.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,l)=>{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,"wrap-class-name":"base-form-modal","onUpdate:open":m,onOk:l[0]||(l[0]=x=>t.$emit("submit")),onCancel:c},{default:o(()=>[i(u,{spinning:e.loading},{default:o(()=>[i(f,{layout:"vertical"},{default:o(()=>[w(t.$slots,"default")]),_:3})]),_:3},8,["spinning"])]),_:3},8,["open","title","width","confirm-loading","ok-text"])}}});export{b as _};
@@ -1 +0,0 @@
import{d as P,v as S,x as u,a as v,p as n,w as l,k as _,r as s,y as T,o as d,m as $,F as w,b as C,e as g,A,M as k,c as x,t as B}from"./index-QQcP5-AC.js";const D={id:"page-user-list"},E={class:"grid-3"},z=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,c;return String(((c=(r=e==null?void 0:e.children)==null?void 0:r[0])==null?void 0:c.children)??"").toLowerCase().indexOf(t.toLowerCase())>=0}async function m(){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){A.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)}m()}),(t,e)=>{const r=s("a-select-option"),c=s("a-select"),i=s("a-form-item"),M=s("a-input"),N=s("a-input-number"),U=s("a-button"),V=s("a-popconfirm"),F=s("a-form"),O=s("a-card");return d(),v("div",D,[n(O,{title:"تغییر درصد محصولات",class:"page-card"},{default:l(()=>[n(F,{layout:"vertical"},{default:l(()=>[$("div",E,[n(i,{label:"دسته بندی"},{default:l(()=>[n(c,{value:a.category_id,"onUpdate:value":e[0]||(e[0]=o=>a.category_id=o),"allow-clear":"","show-search":"",placeholder:"انتخاب کنید","filter-option":h,onChange:m},{default:l(()=>[(d(!0),v(w,null,C(b.value,o=>(d(),x(r,{key:o.id,value:o.id},{default:l(()=>[g(B(o.title),1)]),_:2},1032,["value"]))),128))]),_:1},8,["value"])]),_:1}),n(i,{label:"برند"},{default:l(()=>[n(c,{value:a.brand_id,"onUpdate:value":e[1]||(e[1]=o=>a.brand_id=o),"allow-clear":"","show-search":"",placeholder:"انتخاب کنید","filter-option":h,onChange:m},{default:l(()=>[(d(!0),v(w,null,C(y.value,o=>(d(),x(r,{key:o.id,value:o.id},{default:l(()=>[g(B(o.title),1)]),_:2},1032,["value"]))),128))]),_:1},8,["value"])]),_:1}),n(i,{label:"تعداد یافت شده"},{default:l(()=>[n(M,{value:p.value,disabled:""},null,8,["value"])]),_:1}),n(i,{label:"اجرت درصدی"},{default:l(()=>[n(N,{value:a.percent,"onUpdate:value":e[2]||(e[2]=o=>a.percent=o),class:"w-full",step:.1},null,8,["value"])]),_:1})]),n(V,{title:"آیا از اعمال تغییرات مطمئن هستید؟","ok-text":"بله","cancel-text":"خیر",onConfirm:L},{default:l(()=>[n(U,{type:"primary",class:"form-submit",loading:f.value},{default:l(()=>[...e[3]||(e[3]=[g("اعمال تغییرات",-1)])]),_:1},8,["loading"])]),_:1})]),_:1})]),_:1})])}}});export{z as default};
@@ -0,0 +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-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};
@@ -1 +1 @@
import{d as m,o as v,c as g,g as w,r as k}from"./index-QQcP5-AC.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 c=p(t.id,a+1),s={value:t.id,key:t.id,title:t.title};return c.length&&(s.children=c),!n.parentPicker&&a===0&&c.length&&(s.disabled=!0),s});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{_}; 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
+1
View File
@@ -0,0 +1 @@
.mr-3[data-v-6332a204]{margin-left:.75rem}.comments-list ul[data-v-6332a204]{list-style:none;padding-right:0}.comment-item[data-v-6332a204]{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:1rem}.comment-item .img[data-v-6332a204]{width:65px;height:65px;display:flex;align-items:center;justify-content:center}.comment-body[data-v-6332a204]{flex:1;min-width:240px}.comment-body .time[data-v-6332a204]{font-size:12px;color:#888}.border[data-v-6332a204]{border:1px solid grey;border-radius:50%}
-1
View File
@@ -1 +0,0 @@
.mr-3[data-v-2efe4af0]{margin-left:.75rem}.comments-list ul[data-v-2efe4af0]{list-style:none;padding-right:0}.comment-item[data-v-2efe4af0]{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:1rem}.comment-item .img[data-v-2efe4af0]{width:65px;height:65px;display:flex;align-items:center;justify-content:center}.comment-body[data-v-2efe4af0]{flex:1;min-width:240px}.comment-body .time[data-v-2efe4af0]{font-size:12px;color:#888}.border[data-v-2efe4af0]{border:1px solid grey;border-radius:50%}@media (max-width: 575px){.comment-item[data-v-2efe4af0]{gap:10px;flex-wrap:nowrap;margin-bottom:12px}.comment-item .img[data-v-2efe4af0]{width:40px;height:40px;flex-shrink:0}.comment-body[data-v-2efe4af0]{min-width:0}}
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
.row[data-v-787cb7a2]{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap}.tags[data-v-787cb7a2]{display:flex;gap:.5rem;flex-wrap:wrap}.slider-toolbar[data-v-787cb7a2]{display:flex;justify-content:space-between;align-items:center;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem}.slider-count[data-v-787cb7a2]{color:#00000073}.slider-grid[data-v-787cb7a2]{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,220px),1fr));gap:16px}.slide-card[data-v-787cb7a2]{display:flex;flex-direction:column;gap:10px;min-width:0;padding:12px;border:1px solid #f0f0f0;border-radius:8px;background:#fff}.slide-card__head[data-v-787cb7a2]{display:flex;justify-content:space-between;align-items:center}.slide-card__title[data-v-787cb7a2]{font-weight:600}.slide-card__media[data-v-787cb7a2]{height:140px}.slide-card__media[data-v-787cb7a2] .image-upload,.slide-card__media[data-v-787cb7a2] .ant-upload-wrapper,.slide-card__media[data-v-787cb7a2] .ant-upload-list,.slide-card__media[data-v-787cb7a2] .ant-upload-list-item-container,.slide-card__media[data-v-787cb7a2] .ant-upload.ant-upload-select{width:100%!important;height:100%!important;margin:0!important}.slide-card__media[data-v-787cb7a2] .ant-upload-wrapper{display:block}.slide-card__media[data-v-787cb7a2] .ant-upload-list-item-thumbnail img{object-fit:cover}.mt-4[data-v-787cb7a2]{margin-top:1rem}@media (max-width: 575px){.row>.ant-input[data-v-787cb7a2]{flex:1 1 140px;max-width:none!important}.mt-4[data-v-787cb7a2]{margin-top:12px}}
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}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
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};
File diff suppressed because one or more lines are too long
@@ -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
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}
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 @@
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
@@ -1 +0,0 @@
import{m}from"./index-DT4Uze1z.js";import{d as _,l as V,o as r,c as Y,w as h,p as D,a as j,t as M,f as b,k as d,g as k,r as i,_ as x}from"./index-QQcP5-AC.js";const y={key:0,class:"hint"},g=_({__name:"DateInput",props:{label:{},modelValue:{},placeholder:{default:"مثال: ۱۴۰۳/۰۵/۱۲"}},emits:["update:modelValue"],setup(o,{emit:p}){const n=o,c=p,t=d(void 0),a=d(""),f=k(()=>{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=i("a-input"),s=i("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(),j("small",y,M(a.value),1)):b("",!0)]),_:1},8,["label"])}}}),I=x(g,[["__scopeId","data-v-3863c3bc"]]);export{I as D};
+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-C2L5ywNo.js";import{d as t,c as r,o as e}from"./index-QQcP5-AC.js";import"./BaseFormModal.vue_vue_type_style_index_0_lang-BOlb0dJf.js";import"./ImageUploadInput.vue_vue_type_script_setup_true_lang-gPDguZj2.js";import"./Icon-B9FXOmlS.js";const f=t({__name:"Edit",setup(a){return(m,p)=>(e(),r(o))}});export{f as default};
-1
View File
@@ -1 +0,0 @@
import{_ as e}from"./AddBranches.vue_vue_type_script_setup_true_lang-Du71-Nbh.js";import{d as o,c as r,o as t}from"./index-QQcP5-AC.js";import"./BaseFormModal.vue_vue_type_style_index_0_lang-BOlb0dJf.js";const p=o({__name:"EditBranches",setup(a){return(c,n)=>(t(),r(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};
@@ -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 +0,0 @@
import{_ as o}from"./bulkCustomer.vue_vue_type_script_setup_true_lang-UhBsQkgG.js";import{d as r,c as t,o as e}from"./index-QQcP5-AC.js";import"./Icon-B9FXOmlS.js";import"./BaseFormModal.vue_vue_type_style_index_0_lang-BOlb0dJf.js";import"./ImageUploadInput.vue_vue_type_script_setup_true_lang-gPDguZj2.js";import"./CityTreeSelect.vue_vue_type_script_setup_true_lang-qW84W-31.js";import"./DateInput-COU3oLhL.js";import"./index-DT4Uze1z.js";const l=r({__name:"EditBulkCustomer",setup(m){return(p,a)=>(e(),t(o,{class:"page-card"}))}});export{l as default};
-1
View File
@@ -1 +0,0 @@
import o from"./AddInvoice-C5JsFepN.js";import{d as t,c as r,o as e}from"./index-QQcP5-AC.js";import"./Icon-B9FXOmlS.js";import"./BaseFormModal.vue_vue_type_style_index_0_lang-BOlb0dJf.js";import"./model-m8OsHE_l.js";import"./filters-B9z4I-pb.js";import"./index-DT4Uze1z.js";const u=t({__name:"EditInvoice",setup(p){return(i,m)=>(e(),r(o))}});export{u 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-CjWrD0FL.js";import{d as o,a as t,p as r,o as a}from"./index-QQcP5-AC.js";import"./Icon-B9FXOmlS.js";import"./ImageUploadInput.vue_vue_type_script_setup_true_lang-gPDguZj2.js";const _=o({__name:"EditProduct",setup(c){return(n,p)=>(a(),t("div",null,[r(e)]))}});export{_ as default};
+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 @@
import{R as e}from"./ReagentCustomer-DkAyHy8l.js";import{d as t,c as o,o as r}from"./index-QQcP5-AC.js";import"./Icon-B9FXOmlS.js";import"./CityTreeSelect.vue_vue_type_script_setup_true_lang-qW84W-31.js";import"./ImageUploadInput.vue_vue_type_script_setup_true_lang-gPDguZj2.js";const d=t({__name:"EditReagent",setup(a){return(p,s)=>(r(),o(e,{class:"page-card"}))}});export{d as default};
@@ -1 +0,0 @@
import{S as o}from"./singleCustomer-XwT9-VyX.js";import{d as r,c as e,o as t}from"./index-QQcP5-AC.js";import"./Icon-B9FXOmlS.js";import"./CityTreeSelect.vue_vue_type_script_setup_true_lang-qW84W-31.js";import"./DateInput-COU3oLhL.js";import"./index-DT4Uze1z.js";const u=r({__name:"EditSingleCustomer",setup(m){return(a,p)=>(t(),e(o,{class:"page-card"}))}});export{u 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-C26658Z1.js";import{d as e,c as r,o as t}from"./index-QQcP5-AC.js";import"./BaseFormModal.vue_vue_type_style_index_0_lang-BOlb0dJf.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
View File
@@ -0,0 +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-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 @@
import{_,a as l,p as e,w as o,e as c,r as s,o as p}from"./index-QQcP5-AC.js";const u={},d={class:"full-page-msg"};function i(r,t){const a=s("a-button"),n=s("a-result");return p(),l("div",d,[e(n,{status:"404",title:"صفحه مورد نظر یافت نشد","sub-title":"آدرسی که وارد کرده‌اید وجود ندارد."},{extra:o(()=>[e(a,{type:"primary",onClick:t[0]||(t[0]=m=>r.$router.push("/"))},{default:o(()=>[...t[1]||(t[1]=[c("بازگشت به خانه",-1)])]),_:1})]),_:1})])}const x=_(u,[["render",i],["__scopeId","data-v-82a95474"]]);export{x as default};
+1
View File
@@ -0,0 +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-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
View File
@@ -1 +0,0 @@
import{_,a as l,p as e,w as o,e as c,r as s,o as p}from"./index-QQcP5-AC.js";const u={},d={class:"full-page-msg"};function i(r,t){const a=s("a-button"),n=s("a-result");return p(),l("div",d,[e(n,{status:"500",title:"خطای سرور","sub-title":"مشکلی در سرور رخ داده است. لطفاً بعداً تلاش کنید."},{extra:o(()=>[e(a,{type:"primary",onClick:t[0]||(t[0]=m=>r.$router.push("/"))},{default:o(()=>[...t[1]||(t[1]=[c("بازگشت به خانه",-1)])]),_:1})]),_:1})])}const x=_(u,[["render",i],["__scopeId","data-v-1080291a"]]);export{x as default};
@@ -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 +0,0 @@
import{I as u}from"./Icon-B9FXOmlS.js";import{d as z,c as l,w as n,r as y,o as s,p as c,a0 as I,m as O,O as P,k as p,x as w,A as C,j as A}from"./index-QQcP5-AC.js";import{X as B}from"./model-m8OsHE_l.js";import{_ as L}from"./ListPageHeader.vue_vue_type_script_setup_true_lang-BBMCN2Q8.js";const S={class:"flex gap-2"},j=z({__name:"FacilityList",props:{title:{},model:{},editPath:{},activePath:{},addRid:{},addLabel:{}},setup(a){const k=a,$=A(),m=p(null),d=p(!1),f=p(void 0);function _(){f.value=void 0,d.value=!0}function h(e){f.value=e,d.value=!0}function b(e){d.value=e}function g(){var e;(e=m.value)==null||e.fetch()}function R(e){$.push(`${k.editPath}/${e}`)}async function x(e){var o,i,r;try{await w.get(`${k.activePath}/${e}`),C.success("تغییرات ثبت شد"),(o=m.value)==null||o.fetch()}catch(t){C.error(((r=(i=t==null?void 0:t.response)==null?void 0:i.data)==null?void 0:r.message)??"خطا در تغییر وضعیت")}}return(e,o)=>{const i=y("a-tooltip"),r=y("a-card");return s(),l(r,{title:a.title,class:"page-card"},{default:n(()=>[c(B,{ref_key:"tbl",ref:m,model:a.model,"mobile-cards":""},I({actions:n(({record:t})=>[O("div",S,[a.addRid?(s(),l(i,{key:0,title:"ویرایش"},{default:n(()=>[c(u,{icon:"vuesax-linear:edit-2",size:"16",class:"cursor-pointer mx-1 text-primary",onClick:v=>h(t.id)},null,8,["onClick"])]),_:2},1024)):(s(),l(i,{key:1,title:"ویرایش"},{default:n(()=>[c(u,{icon:"vuesax-linear:edit-2",size:"16",class:"cursor-pointer mx-1 text-primary",onClick:v=>R(t.id)},null,8,["onClick"])]),_:2},1024)),c(i,{title:"وضعیت"},{default:n(()=>[t.is_active?(s(),l(u,{key:0,icon:"vuesax-linear:tick-circle",size:"16",class:"cursor-pointer mx-1 text-success",onClick:v=>x(t.id)},null,8,["onClick"])):(s(),l(u,{key:1,icon:"vuesax-linear:close-circle",size:"16",class:"cursor-pointer mx-1 text-danger",onClick:v=>x(t.id)},null,8,["onClick"]))]),_:2},1024)])]),_:2},[a.addRid?{name:"add",fn:n(()=>[c(L,{rid:a.addRid,"add-label":a.addLabel,onAdd:o[0]||(o[0]=t=>_())},null,8,["rid","add-label"])]),key:"0"}:void 0]),1032,["model"]),P(e.$slots,"form",{open:d.value,editingId:f.value,onUpdateOpen:b,onSuccess:g})]),_:3},8,["title"])}}});export{j as _};
-1
View File
@@ -1 +0,0 @@
import{_ as o,a as t,p as r,r as c,o as s}from"./index-QQcP5-AC.js";const a={},n={class:"layout--full-page"};function _(l,p){const e=c("router-view");return s(),t("div",n,[r(e)])}const f=o(a,[["render",_]]);export{f as default};
+1
View File
@@ -0,0 +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-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};
-1
View File
@@ -1 +0,0 @@
import{d as r,v as c,x as n,a as i,j as m,o as d}from"./index-QQcP5-AC.js";import{u as _}from"./auth-CP16SSo9.js";const l={style:{"min-height":"100vh",display:"grid","place-items":"center"}},h=r({__name:"IamCallback",setup(p){const t=m(),o=_();return c(async()=>{const e=new URLSearchParams(window.location.search);if(sessionStorage.removeItem("gb_admin_iam_sso_checked"),e.get("error")||!e.get("code")||!e.get("state")){sessionStorage.setItem("gb_admin_iam_sso_skip","1"),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{sessionStorage.setItem("gb_admin_iam_sso_skip","1"),await t.replace("/auth/login")}}),(e,s)=>(d(),i("div",l,"در حال ورود..."))}});export{h as default};
+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
View File
@@ -1 +0,0 @@
.liman-icons[data-v-a55ea85c]{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;line-height:0;vertical-align:middle}.liman-icons[data-v-a55ea85c] svg path{stroke:currentColor}
@@ -0,0 +1 @@
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 +0,0 @@
import{I as B}from"./Icon-B9FXOmlS.js";import{d as I,l as N,o as i,a as r,p as m,w as h,c as z,m as g,t as D,f as P,k as p,g as R,x as S,A as U,r as f}from"./index-QQcP5-AC.js";const b={class:"image-upload"},q={key:0},A={key:1},E={style:{"margin-top":"8px"}},F=["src"],j=I({__name:"ImageUploadInput",props:{modelValue:{default:null},purpose:{default:void 0},placeholder:{default:"بارگذاری"}},emits:["update:modelValue","ok"],setup(w,{emit:y}){const s=w,u=y,c=p(!1),d=p(!1),v=p(""),t=p([]);N(()=>s.modelValue,e=>{var a;e?((a=t.value[0])==null?void 0:a.url)!==e&&(t.value=[{uid:e,name:e.split("/").pop()??"file",status:"done",url:e}]):t.value=[]},{immediate:!0});const k=R(()=>s.placeholder);async function V({file:e}){var l,n;const a=new FormData;a.append("file",e),s.purpose&&a.append("purpose",s.purpose),c.value=!0;try{const{data:o}=await S.post("file/create",a);u("update:modelValue",o.url),u("ok",o)}catch(o){const _=((n=(l=o==null?void 0:o.response)==null?void 0:l.data)==null?void 0:n.message)??"بارگذاری فایل ناموفق بود";U.error(_)}finally{c.value=!1}}function x(){return u("update:modelValue",null),!0}function C(e){v.value=e.url,d.value=!0}return(e,a)=>{const l=f("a-spin"),n=f("a-upload"),o=f("a-modal");return i(),r("div",b,[m(n,{"file-list":t.value,"list-type":"picture-card","max-count":1,accept:"image/*","custom-request":V,onRemove:x,onPreview:C},{default:h(()=>[t.value.length===0?(i(),r("div",q,[c.value?(i(),z(l,{key:0,size:"small"})):(i(),r("div",A,[m(B,{icon:"vuesax-linear:add",size:"16"}),g("div",E,D(k.value),1)]))])):P("",!0)]),_:1},8,["file-list"]),m(o,{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,F)]),_:1},8,["open"])])}}});export{j as _};

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