FPSMS-frontend
Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 

593 righe
20 KiB

  1. // actions.ts
  2. "use server";
  3. import { cache } from 'react';
  4. import { serverFetchJson } from "@/app/utils/fetchUtil"; // 改为 serverFetchJson
  5. import { BASE_API_URL } from "@/config/api";
  6. //import { stockTakeDebugLog } from "@/components/StockTakeManagement/stockTakeDebugLog";
  7. export interface RecordsRes<T> {
  8. records: T[];
  9. total: number;
  10. }
  11. export interface InventoryLotDetailResponse {
  12. id: number;
  13. inventoryLotId: number;
  14. itemId: number;
  15. itemCode: string;
  16. itemName: string;
  17. lotNo: string;
  18. expiryDate: string;
  19. productionDate: string;
  20. stockInDate: string;
  21. inQty: number;
  22. outQty: number;
  23. holdQty: number;
  24. availableQty: number;
  25. uom: string;
  26. warehouseCode: string;
  27. warehouseName: string;
  28. warehouseSlot: string;
  29. warehouseArea: string;
  30. warehouse: string;
  31. varianceQty: number | null;
  32. status: string;
  33. remarks: string | null;
  34. stockTakeRecordStatus: string;
  35. stockTakeRecordId: number | null;
  36. firstStockTakeQty: number | null;
  37. secondStockTakeQty: number | null;
  38. firstBadQty: number | null;
  39. secondBadQty: number | null;
  40. approverQty: number | null;
  41. approverBadQty: number | null;
  42. finalQty: number | null;
  43. bookQty: number | null;
  44. lastSelect?: number | null;
  45. stockTakeSection?: string | null;
  46. stockTakeSectionDescription?: string | null;
  47. stockTakerName?: string | null;
  48. /** ISO string or backend LocalDateTime array */
  49. stockTakeEndTime?: string | string[] | null;
  50. /** ISO string or backend LocalDateTime array */
  51. approverTime?: string | string[] | null;
  52. }
  53. /**
  54. * `approverInventoryLotDetailsAll*`:
  55. * - `total` = 全域 `inventory_lot_line` 中 `status = available` 筆數(與 DB COUNT 一致)
  56. * - `filteredRecordCount` = 目前 tab/篩選後筆數(分頁用)
  57. */
  58. export interface ApproverInventoryLotDetailsRecordsRes extends RecordsRes<InventoryLotDetailResponse> {
  59. filteredRecordCount?: number;
  60. totalWaitingForApprover?: number;
  61. totalApproved?: number;
  62. }
  63. function normalizeApproverInventoryLotDetailsRes(
  64. raw: ApproverInventoryLotDetailsRecordsRes
  65. ): ApproverInventoryLotDetailsRecordsRes {
  66. const waiting = Number(raw.totalWaitingForApprover ?? 0) || 0;
  67. const approved = Number(raw.totalApproved ?? 0) || 0;
  68. return {
  69. records: Array.isArray(raw.records) ? raw.records : [],
  70. total: Number(raw.total ?? 0) || 0,
  71. filteredRecordCount: Number(raw.filteredRecordCount ?? 0) || 0,
  72. totalWaitingForApprover: waiting,
  73. totalApproved: approved,
  74. };
  75. }
  76. export const getInventoryLotDetailsBySection = async (
  77. stockTakeSection: string,
  78. stockTakeId?: number | null,
  79. pageNum?: number,
  80. pageSize?: number,
  81. stockTakeRoundId?: number | null
  82. ) => {
  83. console.log('🌐 [API] getInventoryLotDetailsBySection called with:', {
  84. stockTakeSection,
  85. stockTakeId,
  86. stockTakeRoundId,
  87. pageNum,
  88. pageSize
  89. });
  90. const encodedSection = encodeURIComponent(stockTakeSection);
  91. let url = `${BASE_API_URL}/stockTakeRecord/inventoryLotDetailsBySection?stockTakeSection=${encodedSection}&pageNum=${pageNum}&pageSize=${pageSize}`;
  92. if (stockTakeId != null && stockTakeId > 0) {
  93. url += `&stockTakeId=${stockTakeId}`;
  94. }
  95. if (stockTakeRoundId != null && stockTakeRoundId > 0) {
  96. url += `&stockTakeRoundId=${stockTakeRoundId}`;
  97. }
  98. console.log(' [API] Full URL:', url);
  99. const response = await serverFetchJson<RecordsRes<InventoryLotDetailResponse>>(
  100. url,
  101. {
  102. method: "GET",
  103. },
  104. );
  105. console.log('[API] Response received:', response);
  106. return response;
  107. }
  108. export interface SaveStockTakeRecordRequest {
  109. stockTakeRecordId?: number | null;
  110. inventoryLotLineId: number;
  111. qty: number;
  112. badQty: number;
  113. //stockTakerName: string;
  114. remark?: string | null;
  115. }
  116. export interface AllPickedStockTakeListReponse {
  117. id: number;
  118. stockTakeSession: string;
  119. lastStockTakeDate: string | null;
  120. status: string|null;
  121. approverName: string | null;
  122. currentStockTakeItemNumber: number;
  123. totalInventoryLotNumber: number;
  124. stockTakeId: number;
  125. stockTakeRoundId: number | null;
  126. stockTakerName: string | null;
  127. totalItemNumber: number;
  128. startTime: string | null;
  129. endTime: string | null;
  130. planStartDate: string | null;
  131. stockTakeSectionDescription: string | null;
  132. reStockTakeTrueFalse: boolean;
  133. }
  134. /** 與 Picker 列表一致:區域描述、盤點區域、貨品編號/名稱(逗號多關鍵字由後端解析) */
  135. export type ApproverInventoryLotDetailsQuery = {
  136. itemKeyword?: string | null;
  137. //itemKeyword?: string | null;
  138. sectionDescription?: string | null;
  139. stockTakeSections?: string | null;
  140. warehouseKeyword?: string | null;
  141. };
  142. function appendApproverInventoryLotQueryParams(
  143. params: URLSearchParams,
  144. query?: ApproverInventoryLotDetailsQuery | null
  145. ) {
  146. if (!query) return;
  147. if (query.itemKeyword != null && query.itemKeyword.trim() !== "") {
  148. params.append("itemKeyword", query.itemKeyword.trim());
  149. }
  150. if (query.warehouseKeyword != null && query.warehouseKeyword.trim() !== "") {
  151. params.append("warehouseKeyword", query.warehouseKeyword.trim());
  152. }
  153. if (
  154. query.sectionDescription != null &&
  155. query.sectionDescription !== "" &&
  156. query.sectionDescription !== "All"
  157. ) {
  158. params.append("sectionDescription", query.sectionDescription.trim());
  159. }
  160. if (query.stockTakeSections != null && query.stockTakeSections.trim() !== "") {
  161. params.append("stockTakeSections", query.stockTakeSections.trim());
  162. }
  163. }
  164. export const getApproverInventoryLotDetailsAll = async (
  165. stockTakeId?: number | null,
  166. pageNum: number = 0,
  167. pageSize: number = 100,
  168. query?: ApproverInventoryLotDetailsQuery | null
  169. ) => {
  170. const params = new URLSearchParams();
  171. params.append("pageNum", String(pageNum));
  172. params.append("pageSize", String(pageSize));
  173. if (stockTakeId != null && stockTakeId > 0) {
  174. params.append("stockTakeId", String(stockTakeId));
  175. }
  176. appendApproverInventoryLotQueryParams(params, query);
  177. const url = `${BASE_API_URL}/stockTakeRecord/approverInventoryLotDetailsAll?${params.toString()}`;
  178. const response = await serverFetchJson<ApproverInventoryLotDetailsRecordsRes>(
  179. url,
  180. {
  181. method: "GET",
  182. },
  183. );
  184. return normalizeApproverInventoryLotDetailsRes(response);
  185. }
  186. export const getApproverInventoryLotDetailsAllPending = async (
  187. stockTakeId?: number | null,
  188. pageNum: number = 0,
  189. pageSize: number = 50,
  190. query?: ApproverInventoryLotDetailsQuery | null
  191. ) => {
  192. const params = new URLSearchParams();
  193. params.append("pageNum", String(pageNum));
  194. params.append("pageSize", String(pageSize));
  195. if (stockTakeId != null && stockTakeId > 0) {
  196. params.append("stockTakeId", String(stockTakeId));
  197. }
  198. appendApproverInventoryLotQueryParams(params, query);
  199. const url = `${BASE_API_URL}/stockTakeRecord/approverInventoryLotDetailsAllPending?${params.toString()}`;
  200. const response = await serverFetchJson<ApproverInventoryLotDetailsRecordsRes>(url, { method: "GET" });
  201. return normalizeApproverInventoryLotDetailsRes(response);
  202. }
  203. export const getApproverInventoryLotDetailsAllApproved = async (
  204. stockTakeId?: number | null,
  205. pageNum: number = 0,
  206. pageSize: number = 50,
  207. query?: ApproverInventoryLotDetailsQuery | null
  208. ) => {
  209. const params = new URLSearchParams();
  210. params.append("pageNum", String(pageNum));
  211. params.append("pageSize", String(pageSize));
  212. if (stockTakeId != null && stockTakeId > 0) {
  213. params.append("stockTakeId", String(stockTakeId));
  214. }
  215. appendApproverInventoryLotQueryParams(params, query);
  216. const url = `${BASE_API_URL}/stockTakeRecord/approverInventoryLotDetailsAllApproved?${params.toString()}`;
  217. const response = await serverFetchJson<ApproverInventoryLotDetailsRecordsRes>(url, { method: "GET" });
  218. return normalizeApproverInventoryLotDetailsRes(response);
  219. }
  220. export const importStockTake = async (data: FormData) => {
  221. const importStockTake = await serverFetchJson<string>(
  222. `${BASE_API_URL}/stockTake/import`,
  223. {
  224. method: "POST",
  225. body: data,
  226. },
  227. );
  228. return importStockTake;
  229. }
  230. export const getStockTakeRecords = async () => {
  231. const stockTakeRecords = await serverFetchJson<AllPickedStockTakeListReponse[]>( // 改为 serverFetchJson
  232. `${BASE_API_URL}/stockTakeRecord/AllPickedStockOutRecordList`,
  233. {
  234. method: "GET",
  235. },
  236. );
  237. return stockTakeRecords;
  238. }
  239. export const getStockTakeRecordsPaged = async (
  240. pageNum: number,
  241. pageSize: number,
  242. params?: { sectionDescription?: string; stockTakeSections?: string }
  243. ) => {
  244. const searchParams = new URLSearchParams();
  245. searchParams.set("pageNum", String(pageNum));
  246. searchParams.set("pageSize", String(pageSize));
  247. if (params?.sectionDescription && params.sectionDescription !== "All") {
  248. searchParams.set("sectionDescription", params.sectionDescription);
  249. }
  250. if (params?.stockTakeSections?.trim()) {
  251. searchParams.set("stockTakeSections", params.stockTakeSections.trim());
  252. }
  253. const url = `${BASE_API_URL}/stockTakeRecord/AllPickedStockOutRecordList?${searchParams.toString()}`;
  254. const res = await serverFetchJson<RecordsRes<AllPickedStockTakeListReponse>>(url, { method: "GET" });
  255. return res;
  256. };
  257. export const getApproverStockTakeRecords = async () => {
  258. const stockTakeRecords = await serverFetchJson<AllPickedStockTakeListReponse[]>( // 改为 serverFetchJson
  259. `${BASE_API_URL}/stockTakeRecord/AllApproverStockTakeList`,
  260. {
  261. method: "GET",
  262. },
  263. );
  264. return stockTakeRecords;
  265. }
  266. export const getLatestApproverStockTakeHeader = async () => {
  267. return serverFetchJson<AllPickedStockTakeListReponse>(
  268. `${BASE_API_URL}/stockTakeRecord/LatestApproverStockTakeHeader`,
  269. { method: "GET" }
  270. );
  271. }
  272. export const createStockTakeForSections = async () => {
  273. const createStockTakeForSections = await serverFetchJson<Map<string, string>>(
  274. `${BASE_API_URL}/stockTake/createForSections`,
  275. {
  276. method: "POST",
  277. },
  278. );
  279. return createStockTakeForSections;
  280. }
  281. export const saveStockTakeRecord = async (
  282. request: SaveStockTakeRecordRequest,
  283. stockTakeId: number,
  284. stockTakerId: number
  285. ) => {
  286. try {
  287. const result = await serverFetchJson<any>(
  288. `${BASE_API_URL}/stockTakeRecord/saveStockTakeRecord?stockTakeId=${stockTakeId}&stockTakerId=${stockTakerId}`,
  289. {
  290. method: "POST",
  291. headers: {
  292. "Content-Type": "application/json",
  293. },
  294. body: JSON.stringify(request),
  295. },
  296. );
  297. console.log('saveStockTakeRecord: request:', request);
  298. console.log('saveStockTakeRecord: stockTakeId:', stockTakeId);
  299. console.log('saveStockTakeRecord: stockTakerId:', stockTakerId);
  300. return result;
  301. } catch (error: any) {
  302. // 尝试从错误响应中提取消息
  303. if (error?.response) {
  304. try {
  305. const errorData = await error.response.json();
  306. const errorWithMessage = new Error(errorData.message || errorData.error || "Failed to save stock take record");
  307. (errorWithMessage as any).response = error.response;
  308. throw errorWithMessage;
  309. } catch {
  310. throw error;
  311. }
  312. }
  313. throw error;
  314. }
  315. }
  316. export interface BatchSaveStockTakeRecordRequest {
  317. stockTakeId: number;
  318. stockTakeSection: string;
  319. stockTakerId: number;
  320. //stockTakerName: string;
  321. }
  322. export interface BatchSaveStockTakeRecordResponse {
  323. successCount: number;
  324. errorCount: number;
  325. errors: string[];
  326. }
  327. export const batchSaveStockTakeRecords = cache(async (data: BatchSaveStockTakeRecordRequest) => {
  328. const r = await serverFetchJson<BatchSaveStockTakeRecordResponse>(`${BASE_API_URL}/stockTakeRecord/batchSaveStockTakeRecords`,
  329. {
  330. method: "POST",
  331. body: JSON.stringify(data),
  332. headers: { "Content-Type": "application/json" },
  333. })
  334. return r
  335. })
  336. // Add these interfaces and functions
  337. export interface SaveApproverStockTakeRecordRequest {
  338. stockTakeRecordId?: number | null;
  339. qty: number;
  340. badQty: number;
  341. approverId?: number | null;
  342. approverQty?: number | null;
  343. approverBadQty?: number | null;
  344. lastSelect?: number | null;
  345. }
  346. export interface BatchSaveApproverStockTakeRecordRequest {
  347. stockTakeId: number;
  348. stockTakeSection: string;
  349. approverId: number;
  350. variancePercentTolerance?: number | null;
  351. }
  352. export interface BatchSaveApproverStockTakeRecordResponse {
  353. successCount: number;
  354. errorCount: number;
  355. errors: string[];
  356. }
  357. /*
  358. export interface BatchSaveApproverStockTakeAllRequest {
  359. stockTakeId: number;
  360. approverId: number;
  361. variancePercentTolerance?: number | null;
  362. }
  363. */
  364. export interface BatchSaveApproverStockTakeAllRequest {
  365. stockTakeId: number;
  366. approverId: number;
  367. // UI 用,batch 不應該用它來 skip
  368. variancePercentTolerance?: number | null;
  369. // 新增:讓 batch 只處理搜尋結果那批
  370. itemKeyword?: string | null;
  371. warehouseKeyword?: string | null;
  372. sectionDescription?: string | null;
  373. stockTakeSections?: string | null; // 逗號字串
  374. }
  375. export interface BatchSaveApproverStockTakeByIdsRequest {
  376. stockTakeId: number;
  377. approverId: number;
  378. recordIds: number[];
  379. }
  380. export const saveApproverStockTakeRecord = async (
  381. request: SaveApproverStockTakeRecordRequest,
  382. stockTakeId: number
  383. ) => {
  384. try {
  385. const result = await serverFetchJson<any>(
  386. `${BASE_API_URL}/stockTakeRecord/saveApproverStockTakeRecord?stockTakeId=${stockTakeId}`,
  387. {
  388. method: "POST",
  389. headers: {
  390. "Content-Type": "application/json",
  391. },
  392. body: JSON.stringify(request),
  393. },
  394. );
  395. return result;
  396. } catch (error: any) {
  397. if (error?.response) {
  398. try {
  399. const errorData = await error.response.json();
  400. const errorWithMessage = new Error(errorData.message || errorData.error || "Failed to save approver stock take record");
  401. (errorWithMessage as any).response = error.response;
  402. throw errorWithMessage;
  403. } catch {
  404. throw error;
  405. }
  406. }
  407. throw error;
  408. }
  409. }
  410. export const batchSaveApproverStockTakeRecords = cache(async (data: BatchSaveApproverStockTakeRecordRequest) => {
  411. return serverFetchJson<BatchSaveApproverStockTakeRecordResponse>(
  412. `${BASE_API_URL}/stockTakeRecord/batchSaveApproverStockTakeRecords`,
  413. {
  414. method: "POST",
  415. body: JSON.stringify(data),
  416. headers: { "Content-Type": "application/json" },
  417. }
  418. )
  419. }
  420. )
  421. export const batchSaveApproverStockTakeRecordsAll = cache(async (data: BatchSaveApproverStockTakeAllRequest) => {
  422. const r = await serverFetchJson<BatchSaveApproverStockTakeRecordResponse>(
  423. `${BASE_API_URL}/stockTakeRecord/batchSaveApproverStockTakeRecordsAll`,
  424. {
  425. method: "POST",
  426. body: JSON.stringify(data),
  427. headers: { "Content-Type": "application/json" },
  428. }
  429. )
  430. return r
  431. })
  432. export const batchSaveApproverStockTakeRecordsByIds = cache(async (data: BatchSaveApproverStockTakeByIdsRequest) => {
  433. const r = await serverFetchJson<BatchSaveApproverStockTakeRecordResponse>(
  434. `${BASE_API_URL}/stockTakeRecord/batchSaveApproverStockTakeRecordsByIds`,
  435. {
  436. method: "POST",
  437. body: JSON.stringify(data),
  438. headers: { "Content-Type": "application/json" },
  439. }
  440. )
  441. return r
  442. })
  443. export const updateStockTakeRecordStatusToNotMatch = async (
  444. stockTakeRecordId: number
  445. ) => {
  446. try {
  447. const result = await serverFetchJson<any>(
  448. `${BASE_API_URL}/stockTakeRecord/updateStockTakeRecordStatusToNotMatch?stockTakeRecordId=${stockTakeRecordId}`,
  449. {
  450. method: "POST",
  451. },
  452. );
  453. return result;
  454. } catch (error: any) {
  455. if (error?.response) {
  456. try {
  457. const errorData = await error.response.json();
  458. const errorWithMessage = new Error(errorData.message || errorData.error || "Failed to update stock take record status");
  459. (errorWithMessage as any).response = error.response;
  460. throw errorWithMessage;
  461. } catch {
  462. throw error;
  463. }
  464. }
  465. throw error;
  466. }
  467. }
  468. export const getInventoryLotDetailsBySectionNotMatch = async (
  469. stockTakeSection: string,
  470. stockTakeId?: number | null,
  471. pageNum: number = 0,
  472. pageSize: number = 10,
  473. stockTakeRoundId?: number | null
  474. ) => {
  475. const encodedSection = encodeURIComponent(stockTakeSection);
  476. let url = `${BASE_API_URL}/stockTakeRecord/inventoryLotDetailsBySectionNotMatch?stockTakeSection=${encodedSection}&pageNum=${pageNum}`;
  477. // Only add pageSize if it's not "all" (which would be a large number)
  478. if (pageSize < 100000) {
  479. url += `&pageSize=${pageSize}`;
  480. }
  481. // If pageSize is large (meaning "all"), don't send it - backend will return all
  482. if (stockTakeId != null && stockTakeId > 0) {
  483. url += `&stockTakeId=${stockTakeId}`;
  484. }
  485. if (stockTakeRoundId != null && stockTakeRoundId > 0) {
  486. url += `&stockTakeRoundId=${stockTakeRoundId}`;
  487. }
  488. const response = await serverFetchJson<RecordsRes<InventoryLotDetailResponse>>(
  489. url,
  490. {
  491. method: "GET",
  492. },
  493. );
  494. return response;
  495. }
  496. export interface SearchStockTransactionResult {
  497. records: StockTransactionResponse[];
  498. total: number;
  499. }
  500. export interface SearchStockTransactionRequest {
  501. startDate: string | null;
  502. endDate: string | null;
  503. itemCode: string | null;
  504. itemName: string | null;
  505. type: string | null;
  506. pageNum: number;
  507. pageSize: number;
  508. }
  509. export interface StockTransactionResponse {
  510. id: number;
  511. transactionType: string;
  512. itemId: number;
  513. itemCode: string | null;
  514. itemName: string | null;
  515. uomId?: number | null;
  516. uomDesc?: string | null;
  517. balanceQty: number | null;
  518. qty: number;
  519. type: string | null;
  520. status: string;
  521. transactionDate: string | null;
  522. date: string | null; // 添加这个字段
  523. lotNo: string | null;
  524. stockInId: number | null;
  525. stockOutId: number | null;
  526. remarks: string | null;
  527. }
  528. export interface StockTransactionListResponse {
  529. records: RecordsRes<StockTransactionResponse>;
  530. }
  531. export const searchStockTransactions = cache(async (request: SearchStockTransactionRequest) => {
  532. const params = new URLSearchParams();
  533. if (request.itemCode) params.append("itemCode", request.itemCode);
  534. if (request.itemName) params.append("itemName", request.itemName);
  535. if (request.type) params.append("type", request.type);
  536. if (request.startDate) params.append("startDate", request.startDate);
  537. if (request.endDate) params.append("endDate", request.endDate);
  538. params.append("pageNum", String(request.pageNum || 0));
  539. params.append("pageSize", String(request.pageSize || 100));
  540. const queryString = params.toString();
  541. const url = `${BASE_API_URL}/stockTakeRecord/searchStockTransactions${queryString ? `?${queryString}` : ''}`;
  542. const response = await serverFetchJson<RecordsRes<StockTransactionResponse>>(
  543. url,
  544. {
  545. method: "GET",
  546. next: { tags: ["Stock Transaction List"] },
  547. }
  548. );
  549. // 回傳 records 與 total,供分頁正確顯示
  550. return {
  551. records: response?.records || [],
  552. total: response?.total ?? 0,
  553. };
  554. });