Quellcode durchsuchen

added some purchase chart

MergeProblem1^2
PC-20260115JRSN\Administrator vor 14 Stunden
Ursprung
Commit
7415dbe4b6
3 geänderte Dateien mit 1438 neuen und 27 gelöschten Zeilen
  1. +54
    -0
      src/app/(main)/chart/purchase/exportPurchaseChartMaster.ts
  2. +1080
    -23
      src/app/(main)/chart/purchase/page.tsx
  3. +304
    -4
      src/app/api/chart/client.ts

+ 54
- 0
src/app/(main)/chart/purchase/exportPurchaseChartMaster.ts Datei anzeigen

@@ -0,0 +1,54 @@
/**
* Multi-sheet 總表 export for the 採購 chart page — mirrors on-screen charts and drill-down data.
*/
import { exportMultiSheetToXlsx, type MultiSheetSpec } from "../_components/exportChartToXlsx";

export type PurchaseChartMasterExportPayload = {
/** ISO timestamp for audit */
exportedAtIso: string;
/** 篩選與情境 — key-value rows */
metaRows: Record<string, unknown>[];
/** 預計送貨 donut (依預計到貨日、上方篩選) */
estimatedDonutRows: Record<string, unknown>[];
/** 實際已送貨 donut (依訂單日期、上方篩選) */
actualStatusDonutRows: Record<string, unknown>[];
/** 貨品摘要表 (當前 drill) */
itemSummaryRows: Record<string, unknown>[];
/** 供應商分佈 (由採購單明細彙總) */
supplierDistributionRows: Record<string, unknown>[];
/** 採購單列表 */
purchaseOrderListRows: Record<string, unknown>[];
/** 全量採購單行明細 (每張 PO 所有行) */
purchaseOrderLineRows: Record<string, unknown>[];
};

function sheetOrPlaceholder(name: string, rows: Record<string, unknown>[], emptyMessage: string): MultiSheetSpec {
if (rows.length > 0) return { name, rows };
return {
name,
rows: [{ 說明: emptyMessage }],
};
}

/**
* Build worksheet specs (used by {@link exportPurchaseChartMasterToFile}).
*/
export function buildPurchaseChartMasterSheets(payload: PurchaseChartMasterExportPayload): MultiSheetSpec[] {
return [
{ name: "篩選條件與情境", rows: payload.metaRows },
sheetOrPlaceholder("預計送貨", payload.estimatedDonutRows, "無資料(請確認訂單日期與篩選)"),
sheetOrPlaceholder("實際已送貨", payload.actualStatusDonutRows, "無資料"),
sheetOrPlaceholder("貨品摘要", payload.itemSummaryRows, "無資料(可能為篩選交集為空或未載入)"),
sheetOrPlaceholder("供應商分佈", payload.supplierDistributionRows, "無資料"),
sheetOrPlaceholder("採購單列表", payload.purchaseOrderListRows, "無採購單明細可匯出"),
sheetOrPlaceholder("採購單行明細", payload.purchaseOrderLineRows, "無行資料(採購單列表為空)"),
];
}

export function exportPurchaseChartMasterToFile(
payload: PurchaseChartMasterExportPayload,
filenameBase: string
): void {
const sheets = buildPurchaseChartMasterSheets(payload);
exportMultiSheetToXlsx(sheets, filenameBase);
}

+ 1080
- 23
src/app/(main)/chart/purchase/page.tsx
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


+ 304
- 4
src/app/api/chart/client.ts Datei anzeigen

@@ -29,6 +29,81 @@ export interface PurchaseOrderByStatusRow {
count: number;
}

/** Multi-select filters for purchase charts (repeated `supplierId` / `itemCode` / `purchaseOrderNo` query params). */
export type PurchaseOrderChartFilters = {
supplierIds?: number[];
itemCodes?: string[];
purchaseOrderNos?: string[];
/** Single supplier code (drill when row has no supplier id); not used with `supplierIds`. */
supplierCode?: string;
};

function appendPurchaseOrderListParams(p: URLSearchParams, filters?: PurchaseOrderChartFilters) {
(filters?.supplierIds ?? []).forEach((id) => {
if (Number.isFinite(id) && id > 0) p.append("supplierId", String(id));
});
(filters?.itemCodes ?? []).forEach((c) => {
const t = String(c).trim();
if (t) p.append("itemCode", t);
});
(filters?.purchaseOrderNos ?? []).forEach((n) => {
const t = String(n).trim();
if (t) p.append("purchaseOrderNo", t);
});
const sc = filters?.supplierCode?.trim();
if (sc) p.set("supplierCode", sc);
}

export interface PoFilterSupplierOption {
supplierId: number;
code: string;
name: string;
}

export interface PoFilterItemOption {
itemCode: string;
itemName: string;
}

export interface PoFilterPoNoOption {
poNo: string;
}

export interface PurchaseOrderFilterOptions {
suppliers: PoFilterSupplierOption[];
items: PoFilterItemOption[];
poNos: PoFilterPoNoOption[];
}

export interface PurchaseOrderEstimatedArrivalRow {
bucket: string;
count: number;
}

export interface PurchaseOrderDetailByStatusRow {
purchaseOrderId: number;
purchaseOrderNo: string;
status: string;
orderDate: string;
estimatedArrivalDate: string;
/** Shop / supplier FK; use for grouping when code is blank */
supplierId: number | null;
supplierCode: string;
supplierName: string;
itemCount: number;
totalQty: number;
}

export interface PurchaseOrderItemRow {
purchaseOrderLineId: number;
itemCode: string;
itemName: string;
orderedQty: number;
uom: string;
receivedQty: number;
pendingQty: number;
}

export interface StockInOutByDateRow {
date: string;
inQty: number;
@@ -317,11 +392,13 @@ export async function fetchDeliveryOrderByDate(
}

export async function fetchPurchaseOrderByStatus(
targetDate?: string
targetDate?: string,
filters?: PurchaseOrderChartFilters
): Promise<PurchaseOrderByStatusRow[]> {
const q = targetDate
? buildParams({ targetDate })
: "";
const p = new URLSearchParams();
if (targetDate) p.set("targetDate", targetDate);
appendPurchaseOrderListParams(p, filters);
const q = p.toString();
const res = await clientAuthFetch(
q ? `${BASE}/purchase-order-by-status?${q}` : `${BASE}/purchase-order-by-status`
);
@@ -333,6 +410,229 @@ export async function fetchPurchaseOrderByStatus(
}));
}

export async function fetchPurchaseOrderFilterOptions(
targetDate?: string
): Promise<PurchaseOrderFilterOptions> {
const p = new URLSearchParams();
if (targetDate) p.set("targetDate", targetDate);
const q = p.toString();
const res = await clientAuthFetch(
q ? `${BASE}/purchase-order-filter-options?${q}` : `${BASE}/purchase-order-filter-options`
);
if (!res.ok) throw new Error("Failed to fetch purchase order filter options");
const data = await res.json();
const row = (data ?? {}) as Record<string, unknown>;
const suppliers = (Array.isArray(row.suppliers) ? row.suppliers : []) as Record<string, unknown>[];
const items = (Array.isArray(row.items) ? row.items : []) as Record<string, unknown>[];
const poNos = (Array.isArray(row.poNos) ? row.poNos : []) as Record<string, unknown>[];
return {
suppliers: suppliers.map((r) => ({
supplierId: Number(r.supplierId ?? r.supplierid ?? 0),
code: String(r.code ?? ""),
name: String(r.name ?? ""),
})),
items: items.map((r) => ({
itemCode: String(r.itemCode ?? r.itemcode ?? ""),
itemName: String(r.itemName ?? r.itemname ?? ""),
})),
poNos: poNos.map((r) => ({
poNo: String(r.poNo ?? r.pono ?? ""),
})),
};
}

export async function fetchPurchaseOrderEstimatedArrivalSummary(
targetDate?: string,
filters?: PurchaseOrderChartFilters
): Promise<PurchaseOrderEstimatedArrivalRow[]> {
const p = new URLSearchParams();
if (targetDate) p.set("targetDate", targetDate);
appendPurchaseOrderListParams(p, filters);
const q = p.toString();
const res = await clientAuthFetch(
q
? `${BASE}/purchase-order-estimated-arrival-summary?${q}`
: `${BASE}/purchase-order-estimated-arrival-summary`
);
if (!res.ok) throw new Error("Failed to fetch estimated arrival summary");
const data = await res.json();
return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
bucket: String(r.bucket ?? ""),
count: Number(r.count ?? 0),
}));
}

export interface EstimatedArrivalBreakdownSupplierRow {
supplierId: number | null;
supplierCode: string;
supplierName: string;
poCount: number;
}

export interface EstimatedArrivalBreakdownItemRow {
itemCode: string;
itemName: string;
poCount: number;
totalQty: number;
}

export interface EstimatedArrivalBreakdownPoRow {
purchaseOrderId: number;
purchaseOrderNo: string;
status: string;
orderDate: string;
supplierId: number | null;
supplierCode: string;
supplierName: string;
}

export interface PurchaseOrderEstimatedArrivalBreakdown {
suppliers: EstimatedArrivalBreakdownSupplierRow[];
items: EstimatedArrivalBreakdownItemRow[];
purchaseOrders: EstimatedArrivalBreakdownPoRow[];
}

/** Related suppliers / items / POs for one 預計送貨 bucket (same bar filters as the donut). */
export async function fetchPurchaseOrderEstimatedArrivalBreakdown(
targetDate: string,
estimatedArrivalBucket: string,
filters?: PurchaseOrderChartFilters
): Promise<PurchaseOrderEstimatedArrivalBreakdown> {
const p = new URLSearchParams();
p.set("targetDate", targetDate);
p.set("estimatedArrivalBucket", estimatedArrivalBucket.trim().toLowerCase());
appendPurchaseOrderListParams(p, filters);
const res = await clientAuthFetch(`${BASE}/purchase-order-estimated-arrival-breakdown?${p.toString()}`);
if (!res.ok) throw new Error("Failed to fetch estimated arrival breakdown");
const data = await res.json();
const row = (data ?? {}) as Record<string, unknown>;
const suppliers = (Array.isArray(row.suppliers) ? row.suppliers : []) as Record<string, unknown>[];
const items = (Array.isArray(row.items) ? row.items : []) as Record<string, unknown>[];
const purchaseOrders = (Array.isArray(row.purchaseOrders) ? row.purchaseOrders : []) as Record<string, unknown>[];
return {
suppliers: suppliers.map((r) => ({
supplierId: (() => {
const v = r.supplierId ?? r.supplierid;
if (v == null || v === "") return null;
const n = Number(v);
return Number.isFinite(n) ? n : null;
})(),
supplierCode: String(r.supplierCode ?? r.suppliercode ?? ""),
supplierName: String(r.supplierName ?? r.suppliername ?? ""),
poCount: Number(r.poCount ?? r.pocount ?? 0),
})),
items: items.map((r) => ({
itemCode: String(r.itemCode ?? r.itemcode ?? ""),
itemName: String(r.itemName ?? r.itemname ?? ""),
poCount: Number(r.poCount ?? r.pocount ?? 0),
totalQty: Number(r.totalQty ?? r.totalqty ?? 0),
})),
purchaseOrders: purchaseOrders.map((r) => ({
purchaseOrderId: Number(r.purchaseOrderId ?? r.purchaseorderid ?? 0),
purchaseOrderNo: String(r.purchaseOrderNo ?? r.purchaseorderno ?? ""),
status: String(r.status ?? ""),
orderDate: String(r.orderDate ?? r.orderdate ?? ""),
supplierId: (() => {
const v = r.supplierId ?? r.supplierid;
if (v == null || v === "") return null;
const n = Number(v);
return Number.isFinite(n) ? n : null;
})(),
supplierCode: String(r.supplierCode ?? r.suppliercode ?? ""),
supplierName: String(r.supplierName ?? r.suppliername ?? ""),
})),
};
}

export type PurchaseOrderDrillQuery = PurchaseOrderChartFilters & {
/** order = PO order date; complete = PO complete date (for received/completed on a day) */
dateFilter?: "order" | "complete";
/** delivered | not_delivered | cancelled | other — same as 預計送貨 donut buckets */
estimatedArrivalBucket?: string;
};

export async function fetchPurchaseOrderDetailsByStatus(
status: string,
targetDate?: string,
opts?: PurchaseOrderDrillQuery
): Promise<PurchaseOrderDetailByStatusRow[]> {
const p = new URLSearchParams();
p.set("status", status.trim().toLowerCase());
if (targetDate) p.set("targetDate", targetDate);
if (opts?.dateFilter) p.set("dateFilter", opts.dateFilter);
if (opts?.estimatedArrivalBucket?.trim()) {
p.set("estimatedArrivalBucket", opts.estimatedArrivalBucket.trim().toLowerCase());
}
appendPurchaseOrderListParams(p, opts);
const q = p.toString();
const res = await clientAuthFetch(`${BASE}/purchase-order-details-by-status?${q}`);
if (!res.ok) throw new Error("Failed to fetch purchase order details by status");
const data = await res.json();
return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
purchaseOrderId: Number(r.purchaseOrderId ?? 0),
purchaseOrderNo: String(r.purchaseOrderNo ?? ""),
status: String(r.status ?? ""),
orderDate: String(r.orderDate ?? ""),
estimatedArrivalDate: String(r.estimatedArrivalDate ?? ""),
supplierId: (() => {
const v = r.supplierId;
if (v == null || v === "") return null;
const n = Number(v);
return Number.isFinite(n) && n > 0 ? n : null;
})(),
supplierCode: String(r.supplierCode ?? ""),
supplierName: String(r.supplierName ?? ""),
itemCount: Number(r.itemCount ?? 0),
totalQty: Number(r.totalQty ?? 0),
}));
}

export async function fetchPurchaseOrderItems(
purchaseOrderId: number
): Promise<PurchaseOrderItemRow[]> {
const q = buildParams({ purchaseOrderId });
const res = await clientAuthFetch(`${BASE}/purchase-order-items?${q}`);
if (!res.ok) throw new Error("Failed to fetch purchase order items");
const data = await res.json();
return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
purchaseOrderLineId: Number(r.purchaseOrderLineId ?? 0),
itemCode: String(r.itemCode ?? ""),
itemName: String(r.itemName ?? ""),
orderedQty: Number(r.orderedQty ?? 0),
uom: String(r.uom ?? ""),
receivedQty: Number(r.receivedQty ?? 0),
pendingQty: Number(r.pendingQty ?? 0),
}));
}

export async function fetchPurchaseOrderItemsByStatus(
status: string,
targetDate?: string,
opts?: PurchaseOrderDrillQuery
): Promise<PurchaseOrderItemRow[]> {
const p = new URLSearchParams();
p.set("status", status.trim().toLowerCase());
if (targetDate) p.set("targetDate", targetDate);
if (opts?.dateFilter) p.set("dateFilter", opts.dateFilter);
if (opts?.estimatedArrivalBucket?.trim()) {
p.set("estimatedArrivalBucket", opts.estimatedArrivalBucket.trim().toLowerCase());
}
appendPurchaseOrderListParams(p, opts);
const q = p.toString();
const res = await clientAuthFetch(`${BASE}/purchase-order-items-by-status?${q}`);
if (!res.ok) throw new Error("Failed to fetch purchase order items by status");
const data = await res.json();
return ((Array.isArray(data) ? data : []) as Record<string, unknown>[]).map((r: Record<string, unknown>) => ({
purchaseOrderLineId: 0,
itemCode: String(r.itemCode ?? ""),
itemName: String(r.itemName ?? ""),
orderedQty: Number(r.orderedQty ?? 0),
uom: String(r.uom ?? ""),
receivedQty: Number(r.receivedQty ?? 0),
pendingQty: Number(r.pendingQty ?? 0),
}));
}

export async function fetchStockInOutByDate(
startDate?: string,
endDate?: string


Laden…
Abbrechen
Speichern