FPSMS-frontend
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

actions.ts 19 KiB

3ヶ月前
3ヶ月前
2週間前
3ヶ月前
2週間前
2週間前
2週間前
3ヶ月前
2週間前
3ヶ月前
1日前
3ヶ月前
1日前
3ヶ月前
3ヶ月前
1日前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
2週間前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
1週間前
1週間前
1週間前
1週間前
1週間前
1週間前
1週間前
1週間前
1週間前
1週間前
1週間前
2週間前
2週間前
2週間前
1週間前
2週間前
1週間前
2週間前
2週間前
2週間前
1週間前
2週間前
1週間前
2週間前
2週間前
2週間前
3ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
2週間前
3ヶ月前
2週間前
3ヶ月前
2週間前
2週間前
3ヶ月前
2週間前
3ヶ月前
3ヶ月前
1週間前
1週間前
3ヶ月前
2週間前
3ヶ月前
2週間前
2週間前
2週間前
3ヶ月前
1日前
3ヶ月前
3ヶ月前
1日前
3ヶ月前
3ヶ月前
3ヶ月前
2週間前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  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 const saveApproverStockTakeRecord = async (
  376. request: SaveApproverStockTakeRecordRequest,
  377. stockTakeId: number
  378. ) => {
  379. try {
  380. const result = await serverFetchJson<any>(
  381. `${BASE_API_URL}/stockTakeRecord/saveApproverStockTakeRecord?stockTakeId=${stockTakeId}`,
  382. {
  383. method: "POST",
  384. headers: {
  385. "Content-Type": "application/json",
  386. },
  387. body: JSON.stringify(request),
  388. },
  389. );
  390. return result;
  391. } catch (error: any) {
  392. if (error?.response) {
  393. try {
  394. const errorData = await error.response.json();
  395. const errorWithMessage = new Error(errorData.message || errorData.error || "Failed to save approver stock take record");
  396. (errorWithMessage as any).response = error.response;
  397. throw errorWithMessage;
  398. } catch {
  399. throw error;
  400. }
  401. }
  402. throw error;
  403. }
  404. }
  405. export const batchSaveApproverStockTakeRecords = cache(async (data: BatchSaveApproverStockTakeRecordRequest) => {
  406. return serverFetchJson<BatchSaveApproverStockTakeRecordResponse>(
  407. `${BASE_API_URL}/stockTakeRecord/batchSaveApproverStockTakeRecords`,
  408. {
  409. method: "POST",
  410. body: JSON.stringify(data),
  411. headers: { "Content-Type": "application/json" },
  412. }
  413. )
  414. }
  415. )
  416. export const batchSaveApproverStockTakeRecordsAll = cache(async (data: BatchSaveApproverStockTakeAllRequest) => {
  417. const r = await serverFetchJson<BatchSaveApproverStockTakeRecordResponse>(
  418. `${BASE_API_URL}/stockTakeRecord/batchSaveApproverStockTakeRecordsAll`,
  419. {
  420. method: "POST",
  421. body: JSON.stringify(data),
  422. headers: { "Content-Type": "application/json" },
  423. }
  424. )
  425. return r
  426. })
  427. export const updateStockTakeRecordStatusToNotMatch = async (
  428. stockTakeRecordId: number
  429. ) => {
  430. try {
  431. const result = await serverFetchJson<any>(
  432. `${BASE_API_URL}/stockTakeRecord/updateStockTakeRecordStatusToNotMatch?stockTakeRecordId=${stockTakeRecordId}`,
  433. {
  434. method: "POST",
  435. },
  436. );
  437. return result;
  438. } catch (error: any) {
  439. if (error?.response) {
  440. try {
  441. const errorData = await error.response.json();
  442. const errorWithMessage = new Error(errorData.message || errorData.error || "Failed to update stock take record status");
  443. (errorWithMessage as any).response = error.response;
  444. throw errorWithMessage;
  445. } catch {
  446. throw error;
  447. }
  448. }
  449. throw error;
  450. }
  451. }
  452. export const getInventoryLotDetailsBySectionNotMatch = async (
  453. stockTakeSection: string,
  454. stockTakeId?: number | null,
  455. pageNum: number = 0,
  456. pageSize: number = 10,
  457. stockTakeRoundId?: number | null
  458. ) => {
  459. const encodedSection = encodeURIComponent(stockTakeSection);
  460. let url = `${BASE_API_URL}/stockTakeRecord/inventoryLotDetailsBySectionNotMatch?stockTakeSection=${encodedSection}&pageNum=${pageNum}`;
  461. // Only add pageSize if it's not "all" (which would be a large number)
  462. if (pageSize < 100000) {
  463. url += `&pageSize=${pageSize}`;
  464. }
  465. // If pageSize is large (meaning "all"), don't send it - backend will return all
  466. if (stockTakeId != null && stockTakeId > 0) {
  467. url += `&stockTakeId=${stockTakeId}`;
  468. }
  469. if (stockTakeRoundId != null && stockTakeRoundId > 0) {
  470. url += `&stockTakeRoundId=${stockTakeRoundId}`;
  471. }
  472. const response = await serverFetchJson<RecordsRes<InventoryLotDetailResponse>>(
  473. url,
  474. {
  475. method: "GET",
  476. },
  477. );
  478. return response;
  479. }
  480. export interface SearchStockTransactionResult {
  481. records: StockTransactionResponse[];
  482. total: number;
  483. }
  484. export interface SearchStockTransactionRequest {
  485. startDate: string | null;
  486. endDate: string | null;
  487. itemCode: string | null;
  488. itemName: string | null;
  489. type: string | null;
  490. pageNum: number;
  491. pageSize: number;
  492. }
  493. export interface StockTransactionResponse {
  494. id: number;
  495. transactionType: string;
  496. itemId: number;
  497. itemCode: string | null;
  498. itemName: string | null;
  499. uomId?: number | null;
  500. uomDesc?: string | null;
  501. balanceQty: number | null;
  502. qty: number;
  503. type: string | null;
  504. status: string;
  505. transactionDate: string | null;
  506. date: string | null; // 添加这个字段
  507. lotNo: string | null;
  508. stockInId: number | null;
  509. stockOutId: number | null;
  510. remarks: string | null;
  511. }
  512. export interface StockTransactionListResponse {
  513. records: RecordsRes<StockTransactionResponse>;
  514. }
  515. export const searchStockTransactions = cache(async (request: SearchStockTransactionRequest) => {
  516. const params = new URLSearchParams();
  517. if (request.itemCode) params.append("itemCode", request.itemCode);
  518. if (request.itemName) params.append("itemName", request.itemName);
  519. if (request.type) params.append("type", request.type);
  520. if (request.startDate) params.append("startDate", request.startDate);
  521. if (request.endDate) params.append("endDate", request.endDate);
  522. params.append("pageNum", String(request.pageNum || 0));
  523. params.append("pageSize", String(request.pageSize || 100));
  524. const queryString = params.toString();
  525. const url = `${BASE_API_URL}/stockTakeRecord/searchStockTransactions${queryString ? `?${queryString}` : ''}`;
  526. const response = await serverFetchJson<RecordsRes<StockTransactionResponse>>(
  527. url,
  528. {
  529. method: "GET",
  530. next: { tags: ["Stock Transaction List"] },
  531. }
  532. );
  533. // 回傳 records 與 total,供分頁正確顯示
  534. return {
  535. records: response?.records || [],
  536. total: response?.total ?? 0,
  537. };
  538. });