Fix wholesale app orders, polish admin/user chrome, and harden order detail UX.
Customer placements now store WHOLESALE (with a migration for prior RETAIL rows), order status opens in a right drawer, logout asks for confirm, and the dashboard profile nudge plus IRANSans panel typeface are tightened for readability. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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';
|
||||
@@ -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 ──
|
||||
|
||||
@@ -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' } },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user