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:
@@ -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)
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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,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) {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
Reference in New Issue
Block a user