-
-
-
- باشه
-
-
+
@@ -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 @@
-->
-
-
-
+
+
+
@@ -43,11 +51,29 @@
+
+
+
+
+
+
+
+
+
+
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 @@
-
-
-
-
-
+
-
+
@@ -153,7 +149,7 @@
import { onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Drawer, Form, FormItem, Textarea } from 'ant-design-vue'
-import { Loading, PullRefresh } from 'vant'
+import AppPullRefresh from '../components/ui/AppPullRefresh.vue'
import dialogEmpty4 from '../assets/img/empty/dialogEmpty4.svg'
import loginDialogImage from '../assets/img/empty/dialogLogin.svg'
import Icon from '../components/icon/Icon.vue'
@@ -218,6 +214,9 @@ const submitOrder = async () => {
// Ignored by the server, which takes the customer from the token; still
// sent so older builds keep validating. Removed in Phase 2.
customerId: session.customerId ?? undefined,
+ // Wholesale (عمده) — same as legacy mobile; server also forces this for
+ // customer placements so admin omde list shows the order.
+ channel: 'WHOLESALE',
items: cart.items.map((item) => {
const variant = cart.variant(item)
return {
diff --git a/apps/user-app/src/views/HomeView.vue b/apps/user-app/src/views/HomeView.vue
index 0740926..e40beb3 100644
--- a/apps/user-app/src/views/HomeView.vue
+++ b/apps/user-app/src/views/HomeView.vue
@@ -34,11 +34,7 @@
-
-
-
-
-
+
@@ -103,7 +99,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 @@
-
-
-
-
-
+
@@ -99,7 +95,7 @@
-
+
import { onBeforeUnmount, onMounted, ref } from 'vue'
import { Drawer } from 'ant-design-vue'
-import { Loading, PullRefresh } from 'vant'
+import AppPullRefresh from '../../components/ui/AppPullRefresh.vue'
import Icon from '../../components/icon/Icon.vue'
import PageBar from '../../components/ui/PageBar.vue'
import Spinner from '../../components/Spinner.vue'
diff --git a/apps/user-app/src/views/expert/CustomerChoiceView.vue b/apps/user-app/src/views/expert/CustomerChoiceView.vue
index 52f5103..4772417 100644
--- a/apps/user-app/src/views/expert/CustomerChoiceView.vue
+++ b/apps/user-app/src/views/expert/CustomerChoiceView.vue
@@ -40,11 +40,7 @@
-
-
-
-
-
+