562 lines
24 KiB
TypeScript
562 lines
24 KiB
TypeScript
import { Elysia, t } from "elysia";
|
|
import prisma from "../lib/prisma";
|
|
import { adminOnly } from "../middleware/auth";
|
|
import { THAILAND_PROVINCES, findProvince, cleanDistrictName } from "../utils/thaiProvinces";
|
|
|
|
const toNum = (val: any): number => {
|
|
if (val === null || val === undefined || val === "") return 0;
|
|
const s = String(val).replace(/[^0-9.\-]/g, "");
|
|
const n = parseFloat(s);
|
|
return isNaN(n) ? 0 : n;
|
|
};
|
|
|
|
function extractShopeeAddress(order: any): { province: string; district: string } {
|
|
const raw = (order.items?.[0]?.rawData as any) || {};
|
|
const province = String(raw["จังหวัด"] || raw["Province"] || raw["City"] || "").trim();
|
|
const district = String(raw["เขต/อำเภอ"] || raw["อำเภอ/เขต"] || raw["อำเภอ"] || raw["District"] || "").trim();
|
|
return { province, district };
|
|
}
|
|
|
|
export const shopeeRoute = new Elysia({ prefix: "/shopee" })
|
|
.use(adminOnly)
|
|
// ── Import Orders ─────────────────────────────────────────────────────────
|
|
.post(
|
|
"/import/orders",
|
|
async ({ body, set }) => {
|
|
try {
|
|
const { rows, shop } = body as { rows: any[]; shop?: string };
|
|
const shopName = shop || "hom-aroma";
|
|
|
|
console.log(`[Shopee] Received ${rows.length} raw rows for shop: ${shopName}`);
|
|
|
|
const orderMap = new Map<string, { meta: any; items: any[] }>();
|
|
for (const row of rows) {
|
|
const orderId = String(row["หมายเลขคำสั่งซื้อ"] || "").trim();
|
|
if (!orderId) continue;
|
|
if (!orderMap.has(orderId)) orderMap.set(orderId, { meta: row, items: [] });
|
|
orderMap.get(orderId)!.items.push(row);
|
|
}
|
|
|
|
console.log(`[Shopee] Grouped into ${orderMap.size} unique orders`);
|
|
|
|
const CHUNK_SIZE = 30;
|
|
const entries = Array.from(orderMap.entries());
|
|
let upserted = 0;
|
|
|
|
for (let i = 0; i < entries.length; i += CHUNK_SIZE) {
|
|
const chunk = entries.slice(i, i + CHUNK_SIZE);
|
|
await prisma.$transaction(async (tx) => {
|
|
for (const [orderId, { meta, items }] of chunk) {
|
|
await tx.shopeeOrderItem.deleteMany({ where: { orderId } });
|
|
const itemsData = items.map((item) => ({
|
|
parentSku: String(item["เลขอ้างอิง Parent SKU"] || ""),
|
|
productName: String(item["ชื่อสินค้า"] || ""),
|
|
skuRef: String(item["เลขอ้างอิง SKU (SKU Reference No.)"] || ""),
|
|
variantName: String(item["ชื่อตัวเลือก"] || ""),
|
|
originalPrice: toNum(item["ราคาตั้งต้น"]),
|
|
dealPrice: toNum(item["ราคาขาย"]),
|
|
quantity: Math.max(1, Math.round(toNum(item["จำนวน"]))),
|
|
totalPayment: toNum(item["ยอดชำระเงิน"]),
|
|
rawData: item,
|
|
}));
|
|
const orderData = {
|
|
shop: shopName,
|
|
status: String(meta["สถานะการสั่งซื้อ"] || ""),
|
|
buyerUsername: String(meta["ชื่อผู้ใช้ (ผู้ซื้อ)"] || ""),
|
|
orderCreatedAt: String(meta["วันที่ทำการสั่งซื้อ"] || ""),
|
|
paymentTime: String(meta["เวลาการชำระสินค้า"] || ""),
|
|
paymentMethod: String(meta["ช่องทางการชำระเงิน"] || ""),
|
|
trackingNo: String(meta["*หมายเลขติดตามพัสดุ"] || ""),
|
|
totalAmount: toNum(meta["จำนวนเงินทั้งหมด"]),
|
|
buyerPaidAmount: toNum(meta["ราคาสินค้าที่ชำระโดยผู้ซื้อ (THB)"]),
|
|
shippingFee: toNum(meta["ค่าจัดส่งที่ชำระโดยผู้ซื้อ"]),
|
|
commission: toNum(meta["ค่าคอมมิชชั่น"]),
|
|
completedAt: String(meta["เวลาที่ทำการสั่งซื้อสำเร็จ"] || ""),
|
|
};
|
|
await tx.shopeeOrder.upsert({
|
|
where: { orderId },
|
|
update: { ...orderData, items: { create: itemsData } },
|
|
create: { orderId, ...orderData, items: { create: itemsData } },
|
|
});
|
|
upserted++;
|
|
}
|
|
}, { timeout: 30000 });
|
|
console.log(`[Shopee] Progress: ${Math.min(i + CHUNK_SIZE, entries.length)}/${entries.length}`);
|
|
}
|
|
|
|
console.log(`[Shopee] ✅ Done — ${upserted} orders upserted`);
|
|
return { success: true, message: `นำเข้าสำเร็จ ${upserted} คำสั่งซื้อ (จาก ${rows.length} แถว)`, ordersCount: upserted, rowsCount: rows.length };
|
|
} catch (error: any) {
|
|
console.error(`[Shopee] ❌ Error:`, error.message);
|
|
set.status = 500;
|
|
return { success: false, message: error.message };
|
|
}
|
|
},
|
|
{ body: t.Object({ rows: t.Array(t.Any()), shop: t.Optional(t.String()) }) }
|
|
)
|
|
|
|
// ── GET Single Order ──────────────────────────────────────────────────────
|
|
.get("/orders/:orderId", async ({ params, set }) => {
|
|
const { orderId } = params;
|
|
const order = await prisma.shopeeOrder.findUnique({ where: { orderId }, include: { items: true } });
|
|
if (!order) { set.status = 404; return { error: "Order not found" }; }
|
|
return order;
|
|
})
|
|
|
|
// ── GET Orders (paginated) ────────────────────────────────────────────────
|
|
.get("/orders", async ({ query }) => {
|
|
const page = Number(query.page ?? 1);
|
|
const limit = Number(query.limit ?? 30);
|
|
const skip = (page - 1) * limit;
|
|
const shop = query.shop || "hom-aroma";
|
|
const where = { shop };
|
|
const [items, total] = await Promise.all([
|
|
prisma.shopeeOrder.findMany({ where, include: { items: true }, orderBy: { orderCreatedAt: "desc" }, skip, take: limit }),
|
|
prisma.shopeeOrder.count({ where }),
|
|
]);
|
|
return { items, total, page, limit, totalPages: Math.ceil(total / limit) };
|
|
})
|
|
|
|
// ── GET Analytics (with date range filter) ───────────────────────────────
|
|
.get("/analytics", async ({ query }) => {
|
|
const shop = query.shop || "hom-aroma";
|
|
const dateFrom = query.dateFrom as string | undefined; // "YYYY-MM-DD"
|
|
const dateTo = query.dateTo as string | undefined;
|
|
|
|
// Build where — orderCreatedAt stored as "YYYY-MM-DD HH:mm" string
|
|
const where: any = { shop };
|
|
if (dateFrom || dateTo) {
|
|
where.orderCreatedAt = {};
|
|
if (dateFrom) where.orderCreatedAt.gte = dateFrom;
|
|
if (dateTo) where.orderCreatedAt.lte = dateTo + " 23:59";
|
|
}
|
|
|
|
const allOrders = await prisma.shopeeOrder.findMany({
|
|
where,
|
|
select: {
|
|
orderId: true, status: true, orderCreatedAt: true,
|
|
totalAmount: true, buyerPaidAmount: true, commission: true,
|
|
shippingFee: true, paymentMethod: true,
|
|
items: { select: { quantity: true, totalPayment: true } },
|
|
},
|
|
});
|
|
|
|
// Monthly aggregation
|
|
const monthMap = new Map<string, { month: string; revenue: number; orders: number; commission: number; cancelled: number; items: number }>();
|
|
for (const o of allOrders) {
|
|
const month = (o.orderCreatedAt ?? "").slice(0, 7) || "Unknown";
|
|
if (!monthMap.has(month)) monthMap.set(month, { month, revenue: 0, orders: 0, commission: 0, cancelled: 0, items: 0 });
|
|
const m = monthMap.get(month)!;
|
|
m.orders += 1;
|
|
m.revenue += o.totalAmount;
|
|
m.commission += o.commission;
|
|
if (o.status.includes("ยกเลิก")) m.cancelled += 1;
|
|
m.items += o.items.reduce((s, i) => s + i.quantity, 0);
|
|
}
|
|
const monthly = Array.from(monthMap.values()).sort((a, b) => a.month.localeCompare(b.month));
|
|
|
|
// Status breakdown
|
|
const statusMap = new Map<string, number>();
|
|
for (const o of allOrders) {
|
|
let status = o.status;
|
|
if (status.includes("สำเร็จแล้ว")) status = "สำเร็จแล้ว";
|
|
else if (status.includes("ได้รับสินค้าแล้ว")) status = "ได้รับสินค้าแล้ว";
|
|
else if (status.includes("ยกเลิก")) status = "ยกเลิก";
|
|
statusMap.set(status, (statusMap.get(status) ?? 0) + 1);
|
|
}
|
|
const statusBreakdown = Array.from(statusMap.entries())
|
|
.map(([status, count]) => ({ status, count }))
|
|
.sort((a, b) => b.count - a.count);
|
|
|
|
// Payment method breakdown
|
|
const paymentMap = new Map<string, { count: number; revenue: number }>();
|
|
for (const o of allOrders) {
|
|
const key = o.paymentMethod || "Unknown";
|
|
if (!paymentMap.has(key)) paymentMap.set(key, { count: 0, revenue: 0 });
|
|
const p = paymentMap.get(key)!;
|
|
p.count += 1;
|
|
p.revenue += o.buyerPaidAmount;
|
|
}
|
|
const paymentBreakdown = Array.from(paymentMap.entries())
|
|
.map(([method, v]) => ({ method, ...v }))
|
|
.sort((a, b) => b.count - a.count);
|
|
|
|
// Top products (from filtered order IDs)
|
|
const filteredOrderIds = allOrders.map(o => o.orderId);
|
|
const allItems = filteredOrderIds.length > 0
|
|
? await prisma.shopeeOrderItem.findMany({
|
|
where: { orderId: { in: filteredOrderIds } },
|
|
select: { productName: true, variantName: true, quantity: true, totalPayment: true, dealPrice: true },
|
|
})
|
|
: [];
|
|
const productMap = new Map<string, { name: string; variant: string; qty: number; revenue: number; orders: number }>();
|
|
for (const item of allItems) {
|
|
const key = `${item.productName}__${item.variantName ?? ""}`;
|
|
if (!productMap.has(key)) productMap.set(key, { name: item.productName, variant: item.variantName ?? "", qty: 0, revenue: 0, orders: 0 });
|
|
const p = productMap.get(key)!;
|
|
p.qty += item.quantity;
|
|
p.revenue += item.totalPayment;
|
|
p.orders += 1;
|
|
}
|
|
const topProducts = Array.from(productMap.values()).sort((a, b) => b.revenue - a.revenue).slice(0, 10);
|
|
|
|
// Summary
|
|
const totalRevenue = allOrders.reduce((s, o) => s + o.totalAmount, 0);
|
|
const totalCommission = allOrders.reduce((s, o) => s + o.commission, 0);
|
|
const totalShipping = allOrders.reduce((s, o) => s + o.shippingFee, 0);
|
|
const totalOrders = allOrders.length;
|
|
const completedOrders = allOrders.filter(o => !o.status.includes("ยกเลิก")).length;
|
|
const cancelledOrders = allOrders.filter(o => o.status.includes("ยกเลิก")).length;
|
|
const cancelRate = totalOrders > 0 ? (cancelledOrders / totalOrders) * 100 : 0;
|
|
const netRevenue = totalRevenue - totalCommission - totalShipping;
|
|
const aov = completedOrders > 0 ? totalRevenue / completedOrders : 0;
|
|
|
|
return {
|
|
monthly, statusBreakdown, paymentBreakdown, topProducts,
|
|
summary: {
|
|
totalRevenue, totalCommission, totalShipping, netRevenue,
|
|
totalOrders, completedOrders, cancelledOrders, cancelRate, aov,
|
|
commissionRate: totalRevenue > 0 ? (totalCommission / totalRevenue) * 100 : 0,
|
|
},
|
|
};
|
|
})
|
|
|
|
// ── GET Analytics Overview (ALL shops combined) ──────────────────────────
|
|
.get("/analytics-overview", async ({ query }) => {
|
|
const dateFrom = query.dateFrom as string | undefined;
|
|
const dateTo = query.dateTo as string | undefined;
|
|
|
|
const where: any = {};
|
|
if (dateFrom || dateTo) {
|
|
where.orderCreatedAt = {};
|
|
if (dateFrom) where.orderCreatedAt.gte = dateFrom;
|
|
if (dateTo) where.orderCreatedAt.lte = dateTo + " 23:59";
|
|
}
|
|
|
|
const allOrders = await prisma.shopeeOrder.findMany({
|
|
where,
|
|
select: {
|
|
orderId: true, shop: true, status: true, orderCreatedAt: true,
|
|
totalAmount: true, buyerPaidAmount: true, commission: true,
|
|
shippingFee: true,
|
|
items: { select: { quantity: true, totalPayment: true } },
|
|
},
|
|
|
|
});
|
|
|
|
// Per-shop summary
|
|
const shopMap = new Map<string, {
|
|
shop: string; orders: number; completed: number; cancelled: number;
|
|
revenue: number; commission: number; shippingFee: number; items: number;
|
|
}>();
|
|
for (const o of allOrders) {
|
|
if (!shopMap.has(o.shop)) {
|
|
shopMap.set(o.shop, { shop: o.shop, orders: 0, completed: 0, cancelled: 0, revenue: 0, commission: 0, shippingFee: 0, items: 0 });
|
|
}
|
|
const s = shopMap.get(o.shop)!;
|
|
s.orders += 1;
|
|
s.revenue += o.totalAmount;
|
|
s.commission += o.commission;
|
|
s.shippingFee += o.shippingFee;
|
|
s.items += o.items.reduce((acc, i) => acc + i.quantity, 0);
|
|
if (o.status.includes("ยกเลิก")) s.cancelled += 1; else s.completed += 1;
|
|
}
|
|
const byShop = Array.from(shopMap.values()).map(s => ({
|
|
...s,
|
|
net: s.revenue - s.commission - s.shippingFee,
|
|
cancelRate: s.orders > 0 ? (s.cancelled / s.orders) * 100 : 0,
|
|
aov: s.completed > 0 ? s.revenue / s.completed : 0,
|
|
})).sort((a, b) => b.revenue - a.revenue);
|
|
|
|
// Monthly stacked by shop
|
|
const monthShopMap = new Map<string, Map<string, { revenue: number; orders: number; commission: number }>>();
|
|
for (const o of allOrders) {
|
|
const month = (o.orderCreatedAt ?? "").slice(0, 7) || "Unknown";
|
|
if (!monthShopMap.has(month)) monthShopMap.set(month, new Map());
|
|
const sm = monthShopMap.get(month)!;
|
|
if (!sm.has(o.shop)) sm.set(o.shop, { revenue: 0, orders: 0, commission: 0 });
|
|
const ms = sm.get(o.shop)!;
|
|
ms.revenue += o.totalAmount;
|
|
ms.orders += 1;
|
|
ms.commission += o.commission;
|
|
}
|
|
const shops = Array.from(shopMap.keys());
|
|
const monthly = Array.from(monthShopMap.entries())
|
|
.map(([month, sm]) => {
|
|
const row: any = { month };
|
|
for (const shop of shops) {
|
|
const d = sm.get(shop) ?? { revenue: 0, orders: 0, commission: 0 };
|
|
row[`${shop}_revenue`] = d.revenue;
|
|
row[`${shop}_orders`] = d.orders;
|
|
}
|
|
row.totalRevenue = Array.from(sm.values()).reduce((s, v) => s + v.revenue, 0);
|
|
row.totalOrders = Array.from(sm.values()).reduce((s, v) => s + v.orders, 0);
|
|
return row;
|
|
})
|
|
.sort((a, b) => a.month.localeCompare(b.month));
|
|
|
|
// Top products across all shops (filtered by date range via orderId)
|
|
const filteredOrderIds = allOrders.map(o => o.orderId);
|
|
const allItems = filteredOrderIds.length > 0 ? await prisma.shopeeOrderItem.findMany({
|
|
where: { orderId: { in: filteredOrderIds } },
|
|
select: { productName: true, variantName: true, quantity: true, totalPayment: true, order: { select: { shop: true } } },
|
|
}) : [];
|
|
|
|
const productMap = new Map<string, { name: string; variant: string; qty: number; revenue: number }>();
|
|
for (const item of allItems) {
|
|
const key = `${item.productName}__${item.variantName ?? ""}`;
|
|
if (!productMap.has(key)) productMap.set(key, { name: item.productName, variant: item.variantName ?? "", qty: 0, revenue: 0 });
|
|
const p = productMap.get(key)!;
|
|
p.qty += item.quantity;
|
|
p.revenue += item.totalPayment;
|
|
}
|
|
const topProducts = Array.from(productMap.values()).sort((a, b) => b.revenue - a.revenue).slice(0, 10);
|
|
|
|
// Grand totals
|
|
const totalRevenue = allOrders.reduce((s, o) => s + o.totalAmount, 0);
|
|
const totalCommission = allOrders.reduce((s, o) => s + o.commission, 0);
|
|
const totalShipping = allOrders.reduce((s, o) => s + o.shippingFee, 0);
|
|
const totalOrders = allOrders.length;
|
|
const totalCancelled = allOrders.filter(o => o.status.includes("ยกเลิก")).length;
|
|
|
|
return {
|
|
byShop, monthly, topProducts, shops,
|
|
totals: {
|
|
totalRevenue, totalCommission, totalShipping,
|
|
netRevenue: totalRevenue - totalCommission - totalShipping,
|
|
totalOrders, totalCancelled,
|
|
cancelRate: totalOrders > 0 ? (totalCancelled / totalOrders) * 100 : 0,
|
|
aov: (totalOrders - totalCancelled) > 0 ? totalRevenue / (totalOrders - totalCancelled) : 0,
|
|
commissionRate: totalRevenue > 0 ? (totalCommission / totalRevenue) * 100 : 0,
|
|
},
|
|
};
|
|
})
|
|
|
|
// ── GET Summary ───────────────────────────────────────────────────────────
|
|
.get("/summary", async ({ query }) => {
|
|
const shop = query.shop || "hom-aroma";
|
|
const where = { shop };
|
|
const [orderCount, agg, itemAgg] = await Promise.all([
|
|
prisma.shopeeOrder.count({ where }),
|
|
prisma.shopeeOrder.aggregate({ where, _sum: { totalAmount: true, buyerPaidAmount: true, commission: true, shippingFee: true } }),
|
|
prisma.shopeeOrderItem.aggregate({ where: { order: where }, _sum: { quantity: true, totalPayment: true } }),
|
|
]);
|
|
return {
|
|
orderCount,
|
|
totalAmount: agg._sum.totalAmount ?? 0,
|
|
buyerPaidAmount: agg._sum.buyerPaidAmount ?? 0,
|
|
totalCommission: agg._sum.commission ?? 0,
|
|
totalShipping: agg._sum.shippingFee ?? 0,
|
|
totalItems: itemAgg._sum.quantity ?? 0,
|
|
totalItemRevenue: itemAgg._sum.totalPayment ?? 0,
|
|
};
|
|
})
|
|
|
|
// ── GET Thailand Sales Map (Shopee 77 Provinces & Top 20) ─────────────────
|
|
.get("/sales-map", async ({ query }) => {
|
|
const shop = (query.shop as string) || "hom-aroma";
|
|
const dateFrom = query.dateFrom as string | undefined;
|
|
const dateTo = query.dateTo as string | undefined;
|
|
|
|
const where: any = {};
|
|
if (shop && shop !== "all") {
|
|
where.shop = shop;
|
|
}
|
|
if (dateFrom || dateTo) {
|
|
where.orderCreatedAt = {};
|
|
if (dateFrom) where.orderCreatedAt.gte = dateFrom;
|
|
if (dateTo) where.orderCreatedAt.lte = dateTo + " 23:59";
|
|
}
|
|
|
|
const orders = await prisma.shopeeOrder.findMany({
|
|
where,
|
|
include: {
|
|
items: {
|
|
select: {
|
|
rawData: true,
|
|
},
|
|
take: 1,
|
|
},
|
|
},
|
|
});
|
|
|
|
const provSalesMap = new Map<string, { code: string; nameTh: string; nameEn: string; region: string; regionTh: string; revenue: number; orders: number }>();
|
|
|
|
for (const p of THAILAND_PROVINCES) {
|
|
provSalesMap.set(p.nameTh, {
|
|
code: p.code,
|
|
nameTh: p.nameTh,
|
|
nameEn: p.nameEn,
|
|
region: p.region,
|
|
regionTh: p.regionTh,
|
|
revenue: 0,
|
|
orders: 0,
|
|
});
|
|
}
|
|
|
|
let totalRevenue = 0;
|
|
let totalOrders = orders.length;
|
|
let unmappedOrders = 0;
|
|
let unmappedRevenue = 0;
|
|
|
|
for (const o of orders) {
|
|
totalRevenue += o.totalAmount;
|
|
const { province } = extractShopeeAddress(o);
|
|
const matched = findProvince(province);
|
|
if (matched) {
|
|
const p = provSalesMap.get(matched.nameTh)!;
|
|
p.revenue += o.totalAmount;
|
|
p.orders += 1;
|
|
} else {
|
|
unmappedOrders += 1;
|
|
unmappedRevenue += o.totalAmount;
|
|
}
|
|
}
|
|
|
|
const provinceList = Array.from(provSalesMap.values());
|
|
const activeProvinces = provinceList.filter(p => p.orders > 0);
|
|
const coverageCount = activeProvinces.length;
|
|
|
|
// Top 20 provinces by revenue
|
|
const topProvinces = [...provinceList]
|
|
.sort((a, b) => b.revenue - a.revenue)
|
|
.slice(0, 20)
|
|
.map((p, idx) => ({
|
|
rank: idx + 1,
|
|
...p,
|
|
aov: p.orders > 0 ? p.revenue / p.orders : 0,
|
|
}));
|
|
|
|
// Region Breakdown
|
|
const regionMap = new Map<string, { region: string; regionTh: string; revenue: number; orders: number }>();
|
|
for (const p of provinceList) {
|
|
if (!regionMap.has(p.region)) {
|
|
regionMap.set(p.region, { region: p.region, regionTh: p.regionTh, revenue: 0, orders: 0 });
|
|
}
|
|
const r = regionMap.get(p.region)!;
|
|
r.revenue += p.revenue;
|
|
r.orders += p.orders;
|
|
}
|
|
const regionBreakdown = Array.from(regionMap.values())
|
|
.map(r => ({
|
|
...r,
|
|
percentage: totalRevenue > 0 ? (r.revenue / totalRevenue) * 100 : 0,
|
|
}))
|
|
.sort((a, b) => b.revenue - a.revenue);
|
|
|
|
const topRegion = regionBreakdown[0] || { region: 'Central', regionTh: 'ภาคกลาง', revenue: 0, orders: 0 };
|
|
const aov = totalOrders > 0 ? totalRevenue / totalOrders : 0;
|
|
|
|
return {
|
|
kpi: {
|
|
totalRevenue,
|
|
totalOrders,
|
|
coverageCount,
|
|
coverageTotal: 77,
|
|
coveragePercent: (coverageCount / 77) * 100,
|
|
aov,
|
|
topRegion: {
|
|
name: topRegion.regionTh,
|
|
orders: topRegion.orders,
|
|
revenue: topRegion.revenue,
|
|
},
|
|
unmappedOrders,
|
|
unmappedRevenue,
|
|
},
|
|
provinces: provinceList,
|
|
topProvinces,
|
|
regionBreakdown,
|
|
};
|
|
})
|
|
|
|
// ── GET District Level Details & Orders (Shopee) ──────────────────────────
|
|
.get("/sales-map/district", async ({ query, set }) => {
|
|
const provinceQuery = query.province as string;
|
|
if (!provinceQuery) {
|
|
set.status = 400;
|
|
return { error: "Province is required" };
|
|
}
|
|
|
|
const targetProv = findProvince(provinceQuery);
|
|
const provNameTh = targetProv ? targetProv.nameTh : provinceQuery;
|
|
|
|
const shop = query.shop as string | undefined;
|
|
const dateFrom = query.dateFrom as string | undefined;
|
|
const dateTo = query.dateTo as string | undefined;
|
|
|
|
const where: any = {};
|
|
if (shop && shop !== "all") {
|
|
where.shop = shop;
|
|
}
|
|
if (dateFrom || dateTo) {
|
|
where.orderCreatedAt = {};
|
|
if (dateFrom) where.orderCreatedAt.gte = dateFrom;
|
|
if (dateTo) where.orderCreatedAt.lte = dateTo + " 23:59";
|
|
}
|
|
|
|
const allOrders = await prisma.shopeeOrder.findMany({
|
|
where,
|
|
include: {
|
|
items: true,
|
|
},
|
|
orderBy: { orderCreatedAt: "desc" },
|
|
});
|
|
|
|
// Filter in JS by matching extracted address
|
|
const matchedOrders = allOrders.filter(o => {
|
|
const { province } = extractShopeeAddress(o);
|
|
const p = findProvince(province);
|
|
return p ? p.nameTh === provNameTh : (province?.includes(provNameTh) || provNameTh.includes(province || ''));
|
|
});
|
|
|
|
const districtMap = new Map<string, { districtName: string; revenue: number; orders: number }>();
|
|
let provRevenue = 0;
|
|
|
|
for (const o of matchedOrders) {
|
|
provRevenue += o.totalAmount;
|
|
const { district } = extractShopeeAddress(o);
|
|
const dName = cleanDistrictName(district) || "ไม่ระบุอำเภอ";
|
|
if (!districtMap.has(dName)) {
|
|
districtMap.set(dName, { districtName: dName, revenue: 0, orders: 0 });
|
|
}
|
|
const d = districtMap.get(dName)!;
|
|
d.revenue += o.totalAmount;
|
|
d.orders += 1;
|
|
}
|
|
|
|
const districts = Array.from(districtMap.values()).sort((a, b) => b.revenue - a.revenue);
|
|
|
|
return {
|
|
province: targetProv || { nameTh: provNameTh, nameEn: provNameTh, region: 'Central', regionTh: 'ภาคกลาง', code: '' },
|
|
summary: {
|
|
totalRevenue: provRevenue,
|
|
totalOrders: matchedOrders.length,
|
|
districtCount: districts.length,
|
|
},
|
|
districts,
|
|
orders: matchedOrders.map(o => {
|
|
const { district, province } = extractShopeeAddress(o);
|
|
return {
|
|
orderId: o.orderId,
|
|
shop: o.shop,
|
|
status: o.status,
|
|
buyerUsername: o.buyerUsername,
|
|
createdTime: o.orderCreatedAt,
|
|
orderAmount: o.totalAmount,
|
|
shippingFee: o.shippingFee,
|
|
province: province || 'ไม่ระบุจังหวัด',
|
|
district: cleanDistrictName(district) || district || 'ไม่ระบุอำเภอ',
|
|
itemsCount: o.items.reduce((s, i) => s + i.quantity, 0),
|
|
items: o.items.map(i => ({
|
|
productName: i.productName,
|
|
variation: i.variantName,
|
|
sellerSku: i.skuRef || i.parentSku,
|
|
quantity: i.quantity,
|
|
subtotalAfterDiscount: i.totalPayment,
|
|
unitOriginalPrice: i.originalPrice || i.dealPrice,
|
|
})),
|
|
};
|
|
}),
|
|
};
|
|
});
|