FPSMS-frontend
25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

211 lines
7.3 KiB

  1. "use client";
  2. import { NEXT_PUBLIC_API_URL } from "@/config/api";
  3. import { clientAuthFetch } from "@/app/utils/clientAuthFetch";
  4. import { exportChartToXlsx } from "@/app/(main)/chart/_components/exportChartToXlsx";
  5. import { reportExcelT as tx } from "./reportI18n";
  6. import type { TFunction } from "i18next";
  7. export interface ShopOrderReplenishmentReportRow {
  8. shopNo?: string;
  9. shopName?: string;
  10. shopOrderDate?: string;
  11. shopOrderNo?: string;
  12. itemNo?: string;
  13. itemName?: string;
  14. firstOrderQty?: number | string;
  15. firstOrderActualPickQty?: number | string;
  16. firstOrderPickerHandler?: string;
  17. reorderQty?: number | string;
  18. reorderDate?: string;
  19. reason?: string;
  20. actualDeliveredQty?: number | string;
  21. actualDeliveredHandler?: string;
  22. deliveredDate?: string;
  23. [key: string]: unknown;
  24. }
  25. export interface ShopOrderReplenishmentReportResponse {
  26. rows: ShopOrderReplenishmentReportRow[];
  27. }
  28. function shopReplenishmentLabels(t?: TFunction) {
  29. return {
  30. sheetName: tx(t, "excel.shopReplenishment.sheetName", "店鋪訂單補貨記錄"),
  31. noData: tx(t, "excel.noData", "(篩選範圍內無資料)"),
  32. shopCode: tx(t, "excel.shopReplenishment.shopCode", "店鋪編號"),
  33. shopName: tx(t, "excel.shopReplenishment.shopName", "店鋪名稱"),
  34. shopOrderDate: tx(t, "excel.shopReplenishment.shopOrderDate", "店鋪訂單日期"),
  35. shopOrderNo: tx(t, "excel.shopReplenishment.shopOrderNo", "店鋪訂單編號"),
  36. itemCode: tx(t, "excel.shopReplenishment.itemCode", "貨品編號"),
  37. itemName: tx(t, "excel.shopReplenishment.itemName", "貨品名稱"),
  38. firstOrderQty: tx(t, "excel.shopReplenishment.firstOrderQty", "原訂單數量"),
  39. firstOrderActualPickQty: tx(
  40. t,
  41. "excel.shopReplenishment.firstOrderActualPickQty",
  42. "原單實際提料數量",
  43. ),
  44. firstOrderPicker: tx(t, "excel.shopReplenishment.firstOrderPicker", "原單提料人"),
  45. reorderQty: tx(t, "excel.shopReplenishment.reorderQty", "補貨數量"),
  46. reorderDate: tx(t, "excel.shopReplenishment.reorderDate", "補貨日期"),
  47. reason: tx(t, "excel.shopReplenishment.reason", "補貨原因"),
  48. actualDeliveredQty: tx(t, "excel.shopReplenishment.actualDeliveredQty", "實際補貨數量"),
  49. actualDeliveredHandler: tx(
  50. t,
  51. "excel.shopReplenishment.actualDeliveredHandler",
  52. "實際補貨提料人",
  53. ),
  54. deliveredDate: tx(t, "excel.shopReplenishment.deliveredDate", "送貨日期"),
  55. reasonQuality: tx(t, "excel.shopReplenishment.reasonQuality", "質素問題"),
  56. reasonOutOfStock: tx(t, "excel.shopReplenishment.reasonOutOfStock", "缺貨"),
  57. reasonOther: tx(t, "excel.shopReplenishment.reasonOther", "其他"),
  58. };
  59. }
  60. type ShopReplenishmentLabels = ReturnType<typeof shopReplenishmentLabels>;
  61. function emptySheetRow(
  62. L: ShopReplenishmentLabels,
  63. note?: string,
  64. ): Record<string, unknown> {
  65. return {
  66. [L.shopCode]: note ?? L.noData,
  67. [L.shopName]: "",
  68. [L.shopOrderDate]: "",
  69. [L.shopOrderNo]: "",
  70. [L.itemCode]: "",
  71. [L.itemName]: "",
  72. [L.firstOrderQty]: "",
  73. [L.firstOrderActualPickQty]: "",
  74. [L.firstOrderPicker]: "",
  75. [L.reorderQty]: "",
  76. [L.reorderDate]: "",
  77. [L.reason]: "",
  78. [L.actualDeliveredQty]: "",
  79. [L.actualDeliveredHandler]: "",
  80. [L.deliveredDate]: "",
  81. };
  82. }
  83. function formatReason(
  84. reason: string | undefined,
  85. L: ShopReplenishmentLabels,
  86. ): string {
  87. switch ((reason ?? "").trim()) {
  88. case "quality_issue":
  89. return L.reasonQuality;
  90. case "out_of_stock":
  91. return L.reasonOutOfStock;
  92. case "other":
  93. return L.reasonOther;
  94. default:
  95. return reason ?? "";
  96. }
  97. }
  98. function formatDateCell(value: unknown): string {
  99. if (value == null || value === "") return "";
  100. if (typeof value === "number" && Number.isFinite(value)) {
  101. // Jackson may serialize java.sql.Date as epoch millis or seconds
  102. const ms = value > 1e12 ? value : value * 1000;
  103. const d = new Date(ms);
  104. if (Number.isNaN(d.getTime())) return String(value);
  105. return d.toISOString().slice(0, 10);
  106. }
  107. const s = String(value).trim();
  108. if (/^\d{10,13}$/.test(s)) {
  109. const n = Number(s);
  110. const ms = s.length >= 13 ? n : n * 1000;
  111. const d = new Date(ms);
  112. if (!Number.isNaN(d.getTime())) return d.toISOString().slice(0, 10);
  113. }
  114. // JDBC / string date: "2026-08-01" or "2026-08-01 00:00:00.0"
  115. return s.length >= 10 ? s.slice(0, 10) : s;
  116. }
  117. function formatQty(value: unknown): string | number {
  118. if (value === null || value === undefined || value === "") return "";
  119. const n = typeof value === "number" ? value : Number(value);
  120. if (!Number.isFinite(n)) return String(value);
  121. return n;
  122. }
  123. function toExcelRow(
  124. r: ShopOrderReplenishmentReportRow,
  125. L: ShopReplenishmentLabels,
  126. ): Record<string, unknown> {
  127. return {
  128. ...emptySheetRow(L, ""),
  129. [L.shopCode]: r.shopNo ?? "",
  130. [L.shopName]: r.shopName ?? "",
  131. [L.shopOrderDate]: formatDateCell(r.shopOrderDate),
  132. [L.shopOrderNo]: r.shopOrderNo ?? "",
  133. [L.itemCode]: r.itemNo ?? "",
  134. [L.itemName]: r.itemName ?? "",
  135. [L.firstOrderQty]: formatQty(r.firstOrderQty),
  136. [L.firstOrderActualPickQty]: formatQty(r.firstOrderActualPickQty),
  137. [L.firstOrderPicker]: r.firstOrderPickerHandler ?? "",
  138. [L.reorderQty]: formatQty(r.reorderQty),
  139. [L.reorderDate]: formatDateCell(r.reorderDate),
  140. [L.reason]: formatReason(r.reason, L),
  141. [L.actualDeliveredQty]: formatQty(r.actualDeliveredQty),
  142. [L.actualDeliveredHandler]: r.actualDeliveredHandler ?? "",
  143. [L.deliveredDate]: formatDateCell(r.deliveredDate),
  144. };
  145. }
  146. export async function fetchShopOrderReplenishmentReportData(
  147. criteria: Record<string, string>,
  148. ): Promise<ShopOrderReplenishmentReportRow[]> {
  149. const queryParams = new URLSearchParams(criteria).toString();
  150. const url = `${NEXT_PUBLIC_API_URL}/report/shop-order-replenishment?${queryParams}`;
  151. const response = await clientAuthFetch(url, {
  152. method: "GET",
  153. headers: { Accept: "application/json" },
  154. });
  155. if (response.status === 401 || response.status === 403)
  156. throw new Error("Unauthorized");
  157. if (!response.ok)
  158. throw new Error(`HTTP error! status: ${response.status}`);
  159. const data = (await response.json()) as
  160. | ShopOrderReplenishmentReportResponse
  161. | ShopOrderReplenishmentReportRow[];
  162. if (Array.isArray(data)) return data;
  163. return data.rows ?? [];
  164. }
  165. /**
  166. * FP-MTMS Version Checklist | Functions Ref. No. 49 | v1.0.1 | 2026-08-10
  167. * Generate and download Shop Orders Replenishment Records as Excel.
  168. */
  169. export async function generateShopOrderReplenishmentReportExcel(
  170. criteria: Record<string, string>,
  171. reportTitle: string = "店鋪訂單補貨記錄",
  172. t?: TFunction,
  173. ): Promise<void> {
  174. const L = shopReplenishmentLabels(t);
  175. const rows = await fetchShopOrderReplenishmentReportData(criteria);
  176. const excelRows =
  177. rows.length > 0 ? rows.map((r) => toExcelRow(r, L)) : [emptySheetRow(L)];
  178. const dateCandidates = [
  179. criteria.reorderDateStart,
  180. criteria.reorderDateEnd,
  181. criteria.shopOrderDateStart,
  182. criteria.shopOrderDateEnd,
  183. criteria.deliveredDateStart,
  184. criteria.deliveredDateEnd,
  185. ].filter((v) => (v ?? "").trim().length > 0);
  186. const datePart =
  187. dateCandidates.length > 0
  188. ? dateCandidates.join("_")
  189. : new Date().toISOString().slice(0, 10);
  190. const safeDatePart = datePart.replace(/[^\d\-_/]/g, "");
  191. const filename = `${reportTitle}_${safeDatePart}`;
  192. exportChartToXlsx(excelRows, filename, L.sheetName);
  193. }