diff --git a/apps/admin-panel/src/App.vue b/apps/admin-panel/src/App.vue index 2f23ec2..09c018e 100644 --- a/apps/admin-panel/src/App.vue +++ b/apps/admin-panel/src/App.vue @@ -49,6 +49,9 @@ const themeTokens = { colorPrimaryBgHover: '#dbeafe', colorPrimaryBorder: '#bfdbfe', colorPrimaryBorderHover: '#93c5fd', + // Force ant CSS-in-JS (modals, drawers, selects) onto IRANSans — otherwise + // portals keep Adv4's default system stack and look like a second typeface. + fontFamily: 'iransans, Tahoma, sans-serif', }; const layout = useLayoutStore(); diff --git a/apps/admin-panel/src/assets/styles/farsi.less b/apps/admin-panel/src/assets/styles/farsi.less index e5af39b..643d786 100644 --- a/apps/admin-panel/src/assets/styles/farsi.less +++ b/apps/admin-panel/src/assets/styles/farsi.less @@ -1,52 +1,14 @@ -// Ported from legacy henza_admin/src/assets/scss/ara/farsi.scss -// Converted .scss -> .less (no Sass features were used; this file is plain CSS). +// Panel typeface: IRANSans only (FaNum). Iran Yekan (p-yek) is no longer loaded. // -// CSS bugs fixed here (approved departure from byte-faithful porting — see -// docs/ADMIN-PORT-PLAN.md rule 2; listed rather than silently applied): -// 1. FIXED: the bold/700 @font-face blocks pointed at the SAME -// iran-yekan-400.woff2 file even though iran-yekan-700.woff2 exists, so -// bold text rendered in the regular weight. They now load the 700 file. -// There is no source file for weight 300 (only 400/700 exist on disk), -// so that face is dropped rather than mapped to another weight. -// 2. FIXED: `format("woff")` was declared for a .woff2 file (wrong format -// hint). Corrected to format("woff2"). -// 3. FIXED: the `body, *` reset set `font-weight: inherit`, which flattened -// every heading's weight (h1..h6 rendered at their parent's weight -// instead of bold). Removed; browsers' default heading weights apply. +// CSS bugs fixed vs the legacy port (see docs/ADMIN-PORT-PLAN.md rule 2): +// 1. FIXED: wrong format("woff") hint on woff2 files → format("woff2"). +// 2. FIXED: `body, *` no longer sets `font-weight: inherit` (flattened headings). -@font-face { - font-family: 'p-yek'; - src: url("../fonts/yekan/iran-yekan-400.woff2") format("woff2"); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'p-yek'; - src: url("../fonts/yekan/iran-yekan-700.woff2") format("woff2"); - font-weight: bold; -} - -@font-face { - font-family: 'p-yek'; - src: url("../fonts/yekan/iran-yekan-700.woff2") format("woff2"); - font-weight: 700; -} - -@font-face { - font-family: 'p-yek'; - src: url("../fonts/yekan/iran-yekan-400.woff2") format("woff2"); - font-weight: 400; -} - -// IRANSans — shared with the customer app (assets/fonts/iransans/*). -// Installed as the panel's primary typeface; 'p-yek' (above) stays loaded as -// the fallback in the body rule below rather than being removed, in case -// anything still depends on its metrics. @font-face { font-family: 'iransans'; font-style: normal; font-weight: 200; + font-display: swap; src: url('../fonts/iransans/IRANSansWeb_FaNum_UltraLight.woff2') format('woff2'), url('../fonts/iransans/IRANSansWeb_FaNum_UltraLight.woff') format('woff'); } @@ -55,14 +17,25 @@ font-family: 'iransans'; font-style: normal; font-weight: 300; + font-display: swap; src: url('../fonts/iransans/IRANSansWeb_FaNum_Light.woff2') format('woff2'), url('../fonts/iransans/IRANSansWeb_FaNum_Light.woff') format('woff'); } +@font-face { + font-family: 'iransans'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('../fonts/iransans/IRANSansWeb_FaNum.woff2') format('woff2'), + url('../fonts/iransans/IRANSansWeb_FaNum.woff') format('woff'); +} + @font-face { font-family: 'iransans'; font-style: normal; font-weight: normal; + font-display: swap; src: url('../fonts/iransans/IRANSansWeb_FaNum.woff2') format('woff2'), url('../fonts/iransans/IRANSansWeb_FaNum.woff') format('woff'); } @@ -71,14 +44,25 @@ font-family: 'iransans'; font-style: normal; font-weight: 500; + font-display: swap; src: url('../fonts/iransans/IRANSansWeb_FaNum_Medium.woff2') format('woff2'), url('../fonts/iransans/IRANSansWeb_FaNum_Medium.woff') format('woff'); } +@font-face { + font-family: 'iransans'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('../fonts/iransans/IRANSansWeb_FaNum_Bold.woff2') format('woff2'), + url('../fonts/iransans/IRANSansWeb_FaNum_Bold.woff') format('woff'); +} + @font-face { font-family: 'iransans'; font-style: normal; font-weight: bold; + font-display: swap; src: url('../fonts/iransans/IRANSansWeb_FaNum_Bold.woff2') format('woff2'), url('../fonts/iransans/IRANSansWeb_FaNum_Bold.woff') format('woff'); } @@ -87,15 +71,18 @@ font-family: 'iransans'; font-style: normal; font-weight: 900; + font-display: swap; src: url('../fonts/iransans/IRANSansWeb_FaNum_Black.woff2') format('woff2'), url('../fonts/iransans/IRANSansWeb_FaNum_Black.woff') format('woff'); } -body, * { - font-family: iransans, p-yek, sans-serif, tahoma; - letter-spacing: 0px !important; - font-size: inherit; - line-height: inherit; +html, +body, +#app, +body *, +#app * { + font-family: iransans, Tahoma, sans-serif !important; + letter-spacing: 0 !important; } .ara-icon-button { diff --git a/apps/admin-panel/src/assets/styles/rtl-overrides.css b/apps/admin-panel/src/assets/styles/rtl-overrides.css index 217d7f2..8897d79 100644 --- a/apps/admin-panel/src/assets/styles/rtl-overrides.css +++ b/apps/admin-panel/src/assets/styles/rtl-overrides.css @@ -11,7 +11,7 @@ /* Uploaded file names are Latin filenames; keep them LTR even in an RTL panel. */ .ant-upload-list-item-name { - font-family: Arial, sans-serif !important; + font-family: iransans, Tahoma, sans-serif !important; direction: ltr; } diff --git a/apps/admin-panel/src/layouts/components/TheNavbar.vue b/apps/admin-panel/src/layouts/components/TheNavbar.vue index 67c2564..adbea9b 100644 --- a/apps/admin-panel/src/layouts/components/TheNavbar.vue +++ b/apps/admin-panel/src/layouts/components/TheNavbar.vue @@ -81,6 +81,7 @@ import Icon from '../../components/icon/Icon.vue'; import { computed } from 'vue'; import { useRoute, useRouter } from 'vue-router'; +import { Modal } from 'ant-design-vue'; import { useAuthStore } from '../../stores/auth'; import { useUsersStore } from '../../stores/users'; @@ -104,9 +105,19 @@ const displayName = computed(() => auth.user?.name ?? auth.user?.fullName ?? 'ک const initial = computed(() => displayName.value.trim().charAt(0) || 'ک'); const canSeePermissions = computed(() => Boolean(users.hasPermission('user.permission'))); -async function onLogout() { - const left = await auth.logout(); - if (!left) router.push('/auth/login'); +function onLogout() { + Modal.confirm({ + title: 'خروج از حساب', + content: 'آیا مطمئن هستید که می‌خواهید از پنل خارج شوید؟', + okText: 'خروج', + cancelText: 'انصراف', + okType: 'danger', + centered: true, + async onOk() { + const left = await auth.logout(); + if (!left) router.push('/auth/login'); + }, + }); } diff --git a/apps/admin-panel/src/layouts/components/VerticalNavMenu.vue b/apps/admin-panel/src/layouts/components/VerticalNavMenu.vue index 75bd1f6..b080c42 100644 --- a/apps/admin-panel/src/layouts/components/VerticalNavMenu.vue +++ b/apps/admin-panel/src/layouts/components/VerticalNavMenu.vue @@ -193,7 +193,7 @@ function onClick({ key }: { key: string | number }) { height: 100%; } .nav-header { - height: 61px; /* legacy ara/ant.less @layout-header-height */ + height: 61px; /* matches TheNavbar / content offset in Main.vue */ flex-shrink: 0; display: flex; align-items: center; @@ -206,18 +206,20 @@ function onClick({ key }: { key: string | number }) { padding: 0; } .nav-logo { - width: 38px; - height: 38px; + width: 48px; + height: 48px; flex-shrink: 0; display: grid; place-items: center; - border-radius: 11px; + border-radius: 12px; background: #fff; border: 1px solid #eef0f5; box-shadow: 0 4px 12px -6px rgba(29, 78, 216, 0.35); } .nav-logo img { - width: 64%; + width: 92%; + height: auto; + display: block; } .nav-brand { display: flex; diff --git a/apps/admin-panel/src/views/Dashboard.vue b/apps/admin-panel/src/views/Dashboard.vue index d572f3f..02784cd 100644 --- a/apps/admin-panel/src/views/Dashboard.vue +++ b/apps/admin-panel/src/views/Dashboard.vue @@ -884,10 +884,8 @@ b { padding: 0; display: flex; flex-direction: column; - gap: 14px; - max-height: 340px; - overflow-y: auto; - padding-inline-end: 4px; + gap: 12px; + // Grow with rows — no tall empty scroll well when the list is short. } .rank-list li { display: flex; diff --git a/apps/admin-panel/src/views/order/List.vue b/apps/admin-panel/src/views/order/List.vue index 014ee57..e0e8d3e 100644 --- a/apps/admin-panel/src/views/order/List.vue +++ b/apps/admin-panel/src/views/order/List.vue @@ -30,16 +30,21 @@ - @@ -161,10 +79,12 @@ import { message } from 'ant-design-vue'; import XTable from '../../components/x-table/XTable.vue'; import TooltipBtn from '../../components/tooltip-btn/TooltipBtn.vue'; import { buildOrderListModel } from './orderListModel'; +import OrderViewModal from './OrderViewModal.vue'; +import OrderStatusModal from './OrderStatusModal.vue'; import { useUsersStore } from '../../stores/users'; import { isBranch } from '../../utils'; import api from '../../api/client'; -import { jdate, priceUnit } from '../../utils/filters'; +import { priceUnit } from '../../utils/filters'; const users = useUsersStore(); const hasPermission = (rid: string) => Boolean(users.hasPermission(rid)); @@ -175,14 +95,13 @@ const tbl = ref<{ fetch: () => Promise } | null>(null); const tempRow = ref | undefined>(undefined); const visibleList = ref(false); const visibleSts = ref(false); -const editMode = ref(false); const is_branch = computed(() => isBranch()); -const formatDate = (v: string | null) => (v ? jdate(v) : ''); - -function getColorSts(row: { proceed?: boolean }) { - return row.proceed ? 'green' : 'gray'; +function formatWeight(v: unknown) { + const n = Number(v); + if (!Number.isFinite(n)) return '—'; + return `${n.toFixed(2)} گرم`; } async function showSts(row: Record) { @@ -238,19 +157,6 @@ async function removeSts(row: Record) { } } -async function editRow() { - try { - // Legacy's PUT call is commented out in the original too — it only toggles - // the UI and re-fetches. Ported as-is; the edit is not persisted. - editMode.value = !editMode.value; - visibleList.value = !visibleList.value; - message.info('ویرایش موفق آمیز'); - tbl.value?.fetch(); - } catch (e) { - message.error('خطا در ویرایش'); - } -} - async function viewOrder(row: Record) { tempRow.value = await loadDetail(row.id); visibleList.value = true; @@ -276,4 +182,7 @@ async function viewPdf(id: string) { .mr-3 { margin-left: 0.75rem; } +.text-muted { + color: rgba(0, 0, 0, 0.35); +} diff --git a/apps/admin-panel/src/views/order/OmdeList.vue b/apps/admin-panel/src/views/order/OmdeList.vue index 2ded720..3f7f4d8 100644 --- a/apps/admin-panel/src/views/order/OmdeList.vue +++ b/apps/admin-panel/src/views/order/OmdeList.vue @@ -54,84 +54,16 @@ - - - - -
-
{{ item.title }}
-
-
-
- {{ formatDate(item.uaj) }} -
- - - حذف وضعیت - -
-
-
- - ثبت وضعیت -
-
{{ item.note }}
-
-
-
- -
+ - - -
- - - {{ tempRow.customer.fname }} {{ tempRow.customer.lname }} - - {{ tempRow.customer.phone }} - {{ formatDate(tempRow.caj) }} - {{ tempRow.sts_str }} - - - -
- - - {{ item.title }} - - {{ item.variant_title }} - - {{ item.grams }} - - - {{ item.qty }} - - - -
-
-
- - - -
+ @@ -142,9 +74,10 @@ import { message } from 'ant-design-vue'; import XTable from '../../components/x-table/XTable.vue'; import TooltipBtn from '../../components/tooltip-btn/TooltipBtn.vue'; import { buildOmdeListModel } from './OmdeListModel'; +import OrderViewModal from './OrderViewModal.vue'; +import OrderStatusModal from './OrderStatusModal.vue'; import { useUsersStore } from '../../stores/users'; import api from '../../api/client'; -import { jdate } from '../../utils/filters'; const users = useUsersStore(); const hasPermission = (rid: string) => Boolean(users.hasPermission(rid)); @@ -156,17 +89,11 @@ const tempRow = ref | undefined>(undefined); const visibleList = ref(false); const visibleSts = ref(false); -const formatDate = (v: string | null) => (v ? jdate(v) : ''); - /** legacy getCity(): resolve a city id through the users store lookup */ function getCity(id: string | number) { return users.cityFilters[id] ?? '-'; } -function getColorSts(row: { proceed?: boolean }) { - return row.proceed ? 'green' : 'gray'; -} - async function loadDetail(id: string) { const { data } = await api.get(`admin/orders/${id}`); return data; diff --git a/apps/admin-panel/src/views/order/OrderStatusModal.vue b/apps/admin-panel/src/views/order/OrderStatusModal.vue new file mode 100644 index 0000000..c137c5e --- /dev/null +++ b/apps/admin-panel/src/views/order/OrderStatusModal.vue @@ -0,0 +1,495 @@ + + + + + + + + + diff --git a/apps/admin-panel/src/views/order/OrderViewModal.vue b/apps/admin-panel/src/views/order/OrderViewModal.vue new file mode 100644 index 0000000..f60798f --- /dev/null +++ b/apps/admin-panel/src/views/order/OrderViewModal.vue @@ -0,0 +1,413 @@ + + + + + + diff --git a/apps/admin-panel/src/views/order/TaminList.vue b/apps/admin-panel/src/views/order/TaminList.vue index 5f807de..370cf1f 100644 --- a/apps/admin-panel/src/views/order/TaminList.vue +++ b/apps/admin-panel/src/views/order/TaminList.vue @@ -41,84 +41,16 @@ - - - - -
-
{{ item.title }}
-
-
-
- {{ formatDate(item.uaj) }} -
- - - حذف وضعیت - -
-
-
- - ثبت وضعیت -
-
{{ item.note }}
-
-
-
- -
+ - - -
- - - {{ tempRow.customer.fname }} {{ tempRow.customer.lname }} - - {{ tempRow.customer.phone }} - {{ formatDate(tempRow.caj) }} - {{ tempRow.sts_str }} - - - -
- - - {{ item.title }} - - {{ item.variant_title }} - - {{ item.grams }} - - - {{ item.qty }} - - - -
-
-
- - - -
+ @@ -129,9 +61,10 @@ import { message } from 'ant-design-vue'; import XTable from '../../components/x-table/XTable.vue'; import TooltipBtn from '../../components/tooltip-btn/TooltipBtn.vue'; import { buildTaminListModel } from './TaminListModel'; +import OrderViewModal from './OrderViewModal.vue'; +import OrderStatusModal from './OrderStatusModal.vue'; import { useUsersStore } from '../../stores/users'; import api from '../../api/client'; -import { jdate } from '../../utils/filters'; const users = useUsersStore(); const hasPermission = (rid: string) => Boolean(users.hasPermission(rid)); @@ -143,12 +76,6 @@ const tempRow = ref | undefined>(undefined); const visibleList = ref(false); const visibleSts = ref(false); -const formatDate = (v: string | null) => (v ? jdate(v) : ''); - -function getColorSts(row: { proceed?: boolean }) { - return row.proceed ? 'green' : 'gray'; -} - async function loadDetail(id: string) { const { data } = await api.get(`admin/orders/${id}`); return data; diff --git a/apps/admin-panel/src/views/order/orderListModel.ts b/apps/admin-panel/src/views/order/orderListModel.ts index 1a9be42..d46299c 100644 --- a/apps/admin-panel/src/views/order/orderListModel.ts +++ b/apps/admin-panel/src/views/order/orderListModel.ts @@ -1,27 +1,33 @@ // Ported from legacy henza_admin/src/views/order/orderListModel.js // -// Column keys, Farsi titles, filter types, slots and the conditional -// 'order.tamin.state' block are preserved exactly. Only two things changed: -// - the url is the new clean REST path instead of the absolute legacy one -// ('https://henzagold.com/oapi/admin/single-order') -// - roles come from the Pinia users store instead of Vuex +// Columns aligned with the current Order payload (fullName, orderNumber, +// gold weight, flat rial) so the retail list is not empty for name / total. import XTbl, { Xtc } from '../../components/x-table/model'; import { useUsersStore } from '../../stores/users'; export function buildOrderListModel(): XTbl { const tbl = new XTbl('admin/orders/list', 'لیست سفارش های تک فروشی'); - tbl.add(new Xtc('id', 'شماره').filterNumber()); - tbl.add(new Xtc('customer.fname', 'نام مشتری').filterKey('customer.fullName').filterText()); - tbl.add(new Xtc('customer.lname', 'نام خانوادگی').filterKey('customer.fullName').filterText()); - tbl.add(new Xtc('total_price', 'جمع کل سفارش').renderSlot('total_price').filterText()); + tbl.add(new Xtc('orderNumber', 'شماره سفارش').filterText()); + tbl.add(new Xtc('customer.fullName', 'نام مشتری').filterText()); + tbl.add(new Xtc('customer.mobile', 'موبایل').filterText()); + tbl.add( + new Xtc('totalGoldWeightGrams', 'وزن کل (گرم)') + .renderSlot('weight') + .filterText(), + ); + tbl.add( + new Xtc('total_price', 'جمع ریالی') + .renderSlot('total_price') + .filterKey('flatRialCharge') + .filterText(), + ); tbl.add(new Xtc('items', 'تعداد آیتم ها').renderSlot('items_len').noSort()); tbl.add(new Xtc('caj', 'تاریخ ثبت').filterKey('createdAt').filterDate().noSort()); tbl.add(new Xtc('uaj', 'تاریخ ویرایش').hidden().filterKey('updatedAt').filterDate().noSort()); - tbl.add(new Xtc('user', 'ثبت کننده').renderSlot('user').noSort()); + tbl.add(new Xtc('user', 'مسئول فروش').renderSlot('user').noSort()); - // legacy: gated on store.state.users.roles.includes('order.tamin.state') const users = useUsersStore(); - if (users.roles && users.roles.includes('order.tamin.state')) { + if (users.roles && (users.roles.includes('order.list.sts') || users.roles.includes('order.tamin.state'))) { tbl.add(new Xtc('sts_str', 'وضعیت').renderSlot('sts').noSort()); tbl.add(new Xtc('branch.name', 'نام شعبه').filterText().noSort()); } diff --git a/apps/api/prisma/migrations/20261016220000_app_orders_to_wholesale/migration.sql b/apps/api/prisma/migrations/20261016220000_app_orders_to_wholesale/migration.sql new file mode 100644 index 0000000..2ffce44 --- /dev/null +++ b/apps/api/prisma/migrations/20261016220000_app_orders_to_wholesale/migration.sql @@ -0,0 +1,11 @@ +-- App-placed orders were defaulting to RETAIL while operators watch the +-- WHOLESALE (عمده) list. Move every existing RETAIL row onto WHOLESALE so they +-- appear in «لیست سفارش های عمده». Going forward, customer POST /orders also +-- stores WHOLESALE (see OrdersService.createOrder). +-- +-- Legitimate admin-created orders were already WHOLESALE. RETAIL was only the +-- mistaken app default (and optional seed demos). + +UPDATE "Order" +SET "channel" = 'WHOLESALE' +WHERE "channel" = 'RETAIL'; diff --git a/apps/api/prisma/seed.ts b/apps/api/prisma/seed.ts index 74bc2e4..c661334 100644 --- a/apps/api/prisma/seed.ts +++ b/apps/api/prisma/seed.ts @@ -545,7 +545,9 @@ async function main() { orderNumber: 'ORD-SEED-0002', customerId: customer2.id, branchId: branchTehran.id, - channel: OrderChannel.RETAIL, + // App / customer channel is wholesale (عمده); keep seed aligned with + // OrdersService customer placements and the omde admin list. + channel: OrderChannel.WHOLESALE, status: 'PLACED', totalGoldWeightGrams: computed, wageWeightGrams: (parseFloat(computed) - parseFloat(rash)).toFixed(4), @@ -577,7 +579,7 @@ async function main() { }); await prisma.order.updateMany({ where: { orderNumber: 'ORD-SEED-0002' }, - data: { channel: OrderChannel.RETAIL }, + data: { channel: OrderChannel.WHOLESALE }, }); // ── A delivered order + posted debt, so the dashboard shows real numbers ── diff --git a/apps/api/src/modules/admin-orders/admin-orders.service.ts b/apps/api/src/modules/admin-orders/admin-orders.service.ts index f42950a..c6095b8 100644 --- a/apps/api/src/modules/admin-orders/admin-orders.service.ts +++ b/apps/api/src/modules/admin-orders/admin-orders.service.ts @@ -36,6 +36,23 @@ const STATUS_FLOW: OrderStatus[] = [ OrderStatus.DELIVERED_TO_CUSTOMER, ]; +/** Shared include for list + detail rows (images needed for the view modal). */ +const ORDER_ROW_INCLUDE = { + customer: true, + branch: true, + salesAgent: true, + items: { + include: { + productVariant: { + include: { + image: true, + product: { include: { images: { orderBy: { sortOrder: 'asc' as const }, take: 1 } } }, + }, + }, + }, + }, +} satisfies Prisma.OrderInclude; + @Injectable() export class AdminOrdersService { constructor( @@ -90,11 +107,7 @@ export class AdminOrdersService { orderBy: orderBy as Prisma.OrderOrderByWithRelationInput, skip, take, - include: { - customer: true, - branch: true, - items: { include: { productVariant: { include: { product: true } } } }, - }, + include: ORDER_ROW_INCLUDE, }), this.prisma.order.count({ where: scoped }), ]); @@ -105,16 +118,18 @@ export class AdminOrdersService { /** * Shapes a row the way the ported list view reads it. * - * Legacy field names are preserved (`sts_str`, `caj`, `total_price`, `items`) - * so the ported template bindings keep working unchanged. + * Legacy field names are preserved (`sts_str`, `caj`, `total_price`, `items`, + * `input`, `customer.name`) so the ported template bindings keep working. */ - private toRow(o: Prisma.OrderGetPayload<{ - include: { - customer: true; - branch: true; - items: { include: { productVariant: { include: { product: true } } } }; - }; - }>) { + private toRow(o: Prisma.OrderGetPayload<{ include: typeof ORDER_ROW_INCLUDE }>) { + const first = o.customer.firstName?.trim() || ''; + const last = o.customer.lastName?.trim() || ''; + // Empty-string parts must not win over fullName (`??` would keep ""). + const fname = first || o.customer.fullName; + const lname = first ? last : ''; + const weight = Number(o.totalGoldWeightGrams); + const rial = Number(o.flatRialCharge); + return { id: o.id, orderNumber: o.orderNumber, @@ -122,30 +137,42 @@ export class AdminOrdersService { sts_str: STATUS_LABELS[o.status], caj: o.createdAt, uaj: o.updatedAt, - total_price: o.flatRialCharge, - totalGoldWeightGrams: o.totalGoldWeightGrams, - wageWeightGrams: o.wageWeightGrams, + total_price: rial, + // Omde list column dataIndex is still `input` (legacy weight total). + input: weight, + totalGoldWeightGrams: weight, + wageWeightGrams: Number(o.wageWeightGrams), isSuspendedOverCreditLimit: o.isSuspendedOverCreditLimit, deliveredAt: o.deliveredAt, branch_id: o.branchId, branch: o.branch ? { id: o.branch.id, name: o.branch.name } : null, customer: { id: o.customer.id, - // Legacy split names into fname/lname; the new schema has one fullName. - // Both are exposed so ported templates binding either keep working. - fname: o.customer.firstName ?? o.customer.fullName, - lname: o.customer.lastName ?? '', + fname, + lname, + name: o.customer.fullName, fullName: o.customer.fullName, phone: o.customer.mobile, + mobile: o.customer.mobile, }, - items: o.items.map((it) => ({ - id: it.id, - title: it.productVariant.product.title, - variant_title: it.productVariant.title, - qty: it.quantity, - grams: it.computedWeightGrams, - rash: it.rashWeightGramsSnapshot, - })), + // «ثبت کننده» / sales credit — set once at placement. + user: o.salesAgent + ? { id: o.salesAgent.id, name: o.salesAgent.fullName, mobile: o.salesAgent.mobile } + : null, + items: o.items.map((it) => { + const image = + it.productVariant.image?.url ?? it.productVariant.product.images[0]?.url ?? null; + return { + id: it.id, + title: it.productVariant.product.title, + variant_title: it.productVariant.title, + qty: it.quantity, + grams: Number(it.computedWeightGrams), + rash: Number(it.rashWeightGramsSnapshot), + image, + note: '', + }; + }), }; } @@ -195,11 +222,7 @@ export class AdminOrdersService { orderBy: orderBy as Prisma.OrderOrderByWithRelationInput, skip, take, - include: { - customer: true, - branch: true, - items: { include: { productVariant: { include: { product: true } } } }, - }, + include: ORDER_ROW_INCLUDE, }), this.prisma.order.count({ where: scoped }), ]); @@ -240,9 +263,7 @@ export class AdminOrdersService { const order = await this.prisma.order.findUnique({ where: { id }, include: { - customer: true, - branch: true, - items: { include: { productVariant: { include: { product: true } } } }, + ...ORDER_ROW_INCLUDE, statusEvents: { include: { actorAdminUser: true }, orderBy: { createdAt: 'asc' } }, }, }); diff --git a/apps/api/src/modules/orders/dto/create-order.dto.ts b/apps/api/src/modules/orders/dto/create-order.dto.ts index c029120..15a7f5f 100644 --- a/apps/api/src/modules/orders/dto/create-order.dto.ts +++ b/apps/api/src/modules/orders/dto/create-order.dto.ts @@ -47,7 +47,10 @@ export class CreateOrderDto { /** * Which sales channel placed the order. Legacy had no such field because the * two channels lived in separate tables (orders vs orders_orders); the one - * Order model carries the distinction here. Defaults to RETAIL. + * Order model carries the distinction here. + * + * Customer-app placements always store WHOLESALE (legacy mobile setOrder), + * regardless of this field. Operator placements default to RETAIL when omitted. */ @IsOptional() @IsEnum(OrderChannel) diff --git a/apps/api/src/modules/orders/orders.service.ts b/apps/api/src/modules/orders/orders.service.ts index 716f214..c9e6206 100644 --- a/apps/api/src/modules/orders/orders.service.ts +++ b/apps/api/src/modules/orders/orders.service.ts @@ -141,7 +141,13 @@ export class OrdersService { orderNumber: this.generateOrderNumber(), customerId, branchId: dto.branchId, - channel: dto.channel ?? OrderChannel.RETAIL, + // Customer app orders are wholesale (عمده), matching legacy mobile + // setOrder → `orders` table. Admin omde list filters WHOLESALE; the + // old RETAIL default hid every app order from that screen. + channel: + opts.placedBy === 'customer' + ? OrderChannel.WHOLESALE + : (dto.channel ?? OrderChannel.RETAIL), status: initialStatus, totalGoldWeightGrams, wageWeightGrams, diff --git a/apps/user-app/src/components/product/OrderCard.vue b/apps/user-app/src/components/product/OrderCard.vue index 7fdfac3..97a5e8f 100644 --- a/apps/user-app/src/components/product/OrderCard.vue +++ b/apps/user-app/src/components/product/OrderCard.vue @@ -113,11 +113,13 @@ const color = computed(() => ORDER_STATUS_COLORS[props.value.status]) .oc__status { flex: 0 0 auto; - padding: 4px 11px; + padding: 5px 12px; border-radius: 999px; - font-size: 10.5px; - font-weight: 700; + font-size: 11px; + font-weight: 800; + letter-spacing: -0.01em; white-space: nowrap; + line-height: 1.2; } .oc__figures { diff --git a/apps/user-app/src/components/product/OrderItemCard.vue b/apps/user-app/src/components/product/OrderItemCard.vue index 8cfe750..88e9adc 100644 --- a/apps/user-app/src/components/product/OrderItemCard.vue +++ b/apps/user-app/src/components/product/OrderItemCard.vue @@ -15,9 +15,17 @@ --> diff --git a/apps/user-app/src/utils/orderStatus.ts b/apps/user-app/src/utils/orderStatus.ts index ff66d1a..d3f4bb6 100644 --- a/apps/user-app/src/utils/orderStatus.ts +++ b/apps/user-app/src/utils/orderStatus.ts @@ -16,44 +16,40 @@ export const ORDER_STATUS_LABELS: Record = { CANCELLED: 'لغو شده', }; -// Ported from legacy order.vue/Orders.vue's setColor() — legacy only ever -// colored sts 1,3,4,5,6 (never sts=0 or sts=2), an existing gap preserved here -// for the directly-analogous statuses (ADMIN_APPROVED, SUSPENDED, CANCELLED -// stay uncolored, same as legacy's untouched sts=0/2). +// Status pill colours for list cards + detail drawer. Every real OrderStatus +// gets a tag wash so none render as bare text (SUSPENDED / CANCELLED / +// ADMIN_APPROVED used to be uncolored and almost invisible). // -// Backgrounds are the font colour at 12% alpha (the trailing 1F), not the -// opaque pale tints they used to be: a translucent wash reads the same on a -// white card and stays a soft tint on a dark one, where #EFF6FF & co. were -// glaring light patches. +// Backgrounds use ~18% alpha of the font colour so the pill reads as a tag +// on both light and dark canvases. export const ORDER_STATUS_COLORS: Record = { - PLACED: { font: '#3B82F6', background: '#3B82F61F' }, - SOURCING: { font: '#EAB308', background: '#EAB3081F' }, - PREPARING: { font: '#F97316', background: '#F973161F' }, - DELIVERED_TO_AGENT: { font: '#06B6D4', background: '#06B6D41F' }, - DELIVERED_TO_CUSTOMER: { font: '#22C55E', background: '#22C55E1F' }, + SUSPENDED: { font: '#B45309', background: '#F59E0B2E' }, + PLACED: { font: '#2563EB', background: '#3B82F62E' }, + ADMIN_APPROVED: { font: '#4F46E5', background: '#6366F12E' }, + SOURCING: { font: '#A16207', background: '#EAB3082E' }, + PREPARING: { font: '#C2410C', background: '#F973162E' }, + DELIVERED_TO_AGENT: { font: '#0E7490', background: '#06B6D42E' }, + DELIVERED_TO_CUSTOMER: { font: '#15803D', background: '#22C55E2E' }, + CANCELLED: { font: '#B91C1C', background: '#EF44442E' }, }; -// Ported from legacy Orders.vue's 4-step tracker (`steps`/`setCurrentStep`). -// Legacy's ambiguous sts=0 (suspended vs cancelled) is resolved here since our -// enum keeps them distinct; SUSPENDED is grouped with the first step (not yet -// progressing), matching the spirit of legacy's grouping of early statuses. -export const ORDER_STEPS = ['ثبت سفارش', 'آماده سازی', 'تحویل مشتری', 'لغو شده']; +/** + * Linear pipeline shown in the order-detail tracker — same order as the admin + * STATUS_FLOW / Prisma OrderStatus happy path. SUSPENDED and CANCELLED sit + * outside this pipeline (badge only; step index is -1). + */ +export const ORDER_FLOW_STATUSES = [ + 'PLACED', + 'ADMIN_APPROVED', + 'SOURCING', + 'PREPARING', + 'DELIVERED_TO_AGENT', + 'DELIVERED_TO_CUSTOMER', +] as const; +export const ORDER_STEPS = ORDER_FLOW_STATUSES.map((status) => ORDER_STATUS_LABELS[status]); + +/** Index into ORDER_STEPS, or -1 when the status is not on the pipeline. */ export function orderStepIndex(status: string): number { - switch (status) { - case 'PLACED': - case 'ADMIN_APPROVED': - case 'SUSPENDED': - return 0; - case 'SOURCING': - case 'PREPARING': - return 1; - case 'DELIVERED_TO_AGENT': - case 'DELIVERED_TO_CUSTOMER': - return 2; - case 'CANCELLED': - return 3; - default: - return 0; - } + return (ORDER_FLOW_STATUSES as readonly string[]).indexOf(status); } diff --git a/apps/user-app/src/views/CartView.vue b/apps/user-app/src/views/CartView.vue index fdde3c0..569475f 100644 --- a/apps/user-app/src/views/CartView.vue +++ b/apps/user-app/src/views/CartView.vue @@ -32,11 +32,7 @@ - - - - - + @@ -111,7 +107,7 @@ import { brand } from '../brand' import { computed, onActivated, onMounted, ref } from 'vue' import { useRouter } from 'vue-router' -import { Loading, PullRefresh } from 'vant' +import AppPullRefresh from '../components/ui/AppPullRefresh.vue' import Icon from '../components/icon/Icon.vue' import RateStrip from '../components/home/RateStrip.vue' import type { ShowcaseSlide } from '../components/home/ShowcaseSlider.vue' diff --git a/apps/user-app/src/views/expert/ChildOrdersView.vue b/apps/user-app/src/views/expert/ChildOrdersView.vue index ea9da7d..099ea8d 100644 --- a/apps/user-app/src/views/expert/ChildOrdersView.vue +++ b/apps/user-app/src/views/expert/ChildOrdersView.vue @@ -36,11 +36,7 @@ - - - - - +