feat: paginate customer orders and polish the dashboard hero

Add paged orders API and infinite scroll, short order numbers, and align the guest invite card with the logged-in hero shell.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
s.esmaeili
2026-10-17 13:11:42 +03:30
co-authored by Cursor
parent f550172567
commit afb068c643
89 changed files with 4518 additions and 1325 deletions
@@ -0,0 +1,16 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
const row = await prisma.setting.findUnique({ where: { key: 'accounting.config' } })
console.log('accounting.config:', row ? JSON.stringify(row.value, null, 2) : 'MISSING')
const links = await prisma.accountingLink.count({ where: { entityType: 'PRODUCT' } })
console.log('PRODUCT accounting links:', links)
await prisma.$disconnect()
}
main().catch((e) => {
console.error(e)
process.exit(1)
})
+15
View File
@@ -0,0 +1,15 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
const a = await prisma.$queryRaw<Array<{ n: bigint }>>`SELECT nextval('order_number_seq') AS n`
const b = await prisma.$queryRaw<Array<{ n: bigint }>>`SELECT nextval('order_number_seq') AS n`
console.log(JSON.stringify({ first: String(a[0]!.n), second: String(b[0]!.n) }))
await prisma.$disconnect()
}
main().catch((e) => {
console.error(e)
process.exit(1)
})
+27
View File
@@ -0,0 +1,27 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
const counts = await prisma.order.groupBy({
by: ['status'],
_count: { _all: true },
where: { orderNumber: { startsWith: 'MOCK-ORD' } },
})
const total = counts.reduce((s, c) => s + c._count._all, 0)
const sample = await prisma.order.findFirst({
where: { orderNumber: { startsWith: 'MOCK-ORD' } },
select: {
orderNumber: true,
status: true,
customer: { select: { mobile: true, fullName: true } },
},
})
console.log(JSON.stringify({ total, byStatus: Object.fromEntries(counts.map((c) => [c.status, c._count._all])), sample }, null, 2))
await prisma.$disconnect()
}
main().catch((e) => {
console.error(e)
process.exit(1)
})
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Legacy system (MySQL `oapi`) → zartaj (PostgreSQL) data import.
* Legacy system (MySQL `oapi`) → tehran-taj (PostgreSQL) data import.
*
* The rules live in docs/DATA-MIGRATION-MAP.md; every step cites its section.
*
@@ -56,7 +56,7 @@ export class Report {
}
render(header: string[]): string {
const out: string[] = ['# legacy (oapi) → zartaj import report', '', ...header, ''];
const out: string[] = ['# legacy (oapi) → tehran-taj import report', '', ...header, ''];
for (const [step, s] of this.sections) {
out.push(`## ${step}`, '');
if (s.counts.size) {
+53
View File
@@ -0,0 +1,53 @@
/**
* Move all MOCK-ORD-* orders to a target customer mobile.
* Run: npx tsx scripts/reassign-mock-orders.ts
*/
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const TARGET_MOBILE = process.env.MOCK_ORDERS_MOBILE?.trim() || '09382507041'
const TAG = 'MOCK-ORD'
async function main() {
const customer = await prisma.customer.findUnique({
where: { mobile: TARGET_MOBILE },
select: { id: true, mobile: true, fullName: true },
})
if (!customer) {
throw new Error(`Customer not found for mobile=${TARGET_MOBILE}`)
}
const before = await prisma.order.count({ where: { orderNumber: { startsWith: TAG } } })
const result = await prisma.order.updateMany({
where: { orderNumber: { startsWith: TAG } },
data: { customerId: customer.id },
})
const byStatus = await prisma.order.groupBy({
by: ['status'],
_count: { _all: true },
where: { orderNumber: { startsWith: TAG }, customerId: customer.id },
})
console.log(
JSON.stringify(
{
target: { mobile: customer.mobile, fullName: customer.fullName },
matched: before,
updated: result.count,
byStatus: Object.fromEntries(byStatus.map((c) => [c.status, c._count._all])),
},
null,
2,
),
)
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})
+199
View File
@@ -0,0 +1,199 @@
/**
* Dev helper: seed >15 mock orders per OrderStatus so the Orders UI can be inspected.
* Run: npx tsx scripts/seed-mock-orders.ts
*/
import {
OrderChannel,
OrderStatus,
Prisma,
PrismaClient,
} from '@prisma/client'
const prisma = new PrismaClient()
const STATUSES: OrderStatus[] = [
OrderStatus.SUSPENDED,
OrderStatus.PLACED,
OrderStatus.ADMIN_APPROVED,
OrderStatus.SOURCING,
OrderStatus.PREPARING,
OrderStatus.DELIVERED_TO_AGENT,
OrderStatus.DELIVERED_TO_CUSTOMER,
OrderStatus.CANCELLED,
]
const PER_STATUS = 18
const TAG = 'MOCK-ORD'
function d(n: number | string) {
return new Prisma.Decimal(n)
}
function orderNumber(status: OrderStatus, i: number) {
// Unique, easy to spot/delete later
return `${TAG}-${status.slice(0, 4)}-${String(i).padStart(3, '0')}-${Date.now().toString(36)}`
}
async function main() {
const mobileOverride = process.env.MOCK_ORDERS_MOBILE?.trim()
const customer = mobileOverride
? await prisma.customer.findUnique({
where: { mobile: mobileOverride },
select: { id: true, mobile: true, fullName: true, referredByAgentId: true },
})
: await prisma.customer.findFirst({
orderBy: { updatedAt: 'desc' },
select: { id: true, mobile: true, fullName: true, referredByAgentId: true },
})
if (!customer) {
throw new Error(
mobileOverride
? `No customer with mobile=${mobileOverride}`
: 'No customers in DB — create/login a customer first',
)
}
const variants = await prisma.productVariant.findMany({
take: 30,
where: { isActive: true },
include: { product: { select: { title: true } } },
})
if (variants.length === 0) {
throw new Error('No product variants in DB — seed catalog first')
}
const branch = await prisma.branch.findFirst({ select: { id: true, name: true } })
console.log(
`Seeding ${PER_STATUS} orders × ${STATUSES.length} statuses for customer ${customer.mobile} (${customer.fullName}), ${variants.length} variants` +
(branch ? `, branch=${branch.name}` : ''),
)
let created = 0
for (const status of STATUSES) {
for (let i = 1; i <= PER_STATUS; i++) {
const variant = variants[i % variants.length]!
const qty = 1 + (i % 3)
const rash = Number(variant.rashWeightGrams ?? 1.25)
const wagePct = Number(variant.defaultWagePercent ?? 7)
const discountPct = 0
const computed = rash * (1 + (wagePct - discountPct) / 100)
const totalGold = computed * qty
const wageWeight = totalGold - rash * qty
const flatRial = i % 4 === 0 ? 150_000 * i : 0
const daysAgo = (STATUSES.indexOf(status) * PER_STATUS + i) % 40
const createdAt = new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000 - i * 60_000)
const deliveredAt =
status === OrderStatus.DELIVERED_TO_CUSTOMER
? new Date(createdAt.getTime() + 2 * 24 * 60 * 60 * 1000)
: null
// 1–2 line items so list/detail both look realistic
const second = variants[(i + 3) % variants.length]!
const useSecond = i % 3 === 0
const order = await prisma.order.create({
data: {
orderNumber: orderNumber(status, i),
customerId: customer.id,
branchId: branch?.id ?? null,
channel: i % 5 === 0 ? OrderChannel.WHOLESALE : OrderChannel.RETAIL,
status,
totalGoldWeightGrams: d(
(useSecond
? totalGold + Number(second.rashWeightGrams) * (1 + Number(second.defaultWagePercent) / 100)
: totalGold
).toFixed(4),
),
wageWeightGrams: d(Math.max(0, wageWeight).toFixed(4)),
flatRialCharge: d(flatRial),
isSuspendedOverCreditLimit: status === OrderStatus.SUSPENDED,
deliveredAt,
salesAgentId: customer.referredByAgentId ?? null,
createdAt,
updatedAt: createdAt,
items: {
create: [
{
productVariantId: variant.id,
quantity: qty,
rashWeightGramsSnapshot: d(rash.toFixed(4)),
wagePercentSnapshot: d(wagePct.toFixed(3)),
discountPercentSnapshot: d(discountPct.toFixed(3)),
computedWeightGrams: d(computed.toFixed(4)),
flatRialCharge: d(flatRial),
},
...(useSecond
? [
{
productVariantId: second.id,
quantity: 1,
rashWeightGramsSnapshot: d(Number(second.rashWeightGrams).toFixed(4)),
wagePercentSnapshot: d(Number(second.defaultWagePercent).toFixed(3)),
discountPercentSnapshot: d(0),
computedWeightGrams: d(
(
Number(second.rashWeightGrams) *
(1 + Number(second.defaultWagePercent) / 100)
).toFixed(4),
),
flatRialCharge: d(0),
},
]
: []),
],
},
statusEvents: {
create: [
{
fromStatus: null,
toStatus: OrderStatus.PLACED,
note: 'mock seed: placed',
createdAt,
},
...(status !== OrderStatus.PLACED
? [
{
fromStatus: OrderStatus.PLACED,
toStatus: status,
note: `mock seed: set to ${status}`,
createdAt: new Date(createdAt.getTime() + 60_000),
},
]
: []),
],
},
},
})
created++
if (i === 1) {
console.log(` ${status}: first=${order.orderNumber}`)
}
}
console.log(` ${status}: +${PER_STATUS}`)
}
const counts = await prisma.order.groupBy({
by: ['status'],
_count: { _all: true },
where: { orderNumber: { startsWith: TAG } },
})
console.log(`Done. Created ${created} mock orders (tag ${TAG}).`)
console.log(
'Mock counts:',
Object.fromEntries(counts.map((c) => [c.status, c._count._all])),
)
console.log(`Login as mobile ${customer.mobile} to see them in the app.`)
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})