FPSMS-frontend
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 

281 wiersze
9.3 KiB

  1. "use client";
  2. import { Box, Tab, Tabs, Snackbar, Alert, CircularProgress, Typography, TextField, Button, Stack } from "@mui/material";
  3. import { useState, useCallback, useEffect, useRef } from "react";
  4. import { useSession } from "next-auth/react";
  5. import { useTranslation } from "react-i18next";
  6. import { AUTH } from "@/authorities";
  7. import { SessionWithTokens } from "@/config/authConfig";
  8. import { AllPickedStockTakeListReponse, getLatestApproverStockTakeHeader } from "@/app/api/stockTake/actions";
  9. import PickerCardList from "./PickerCardList";
  10. import type { PickerCardListFilters } from "./PickerCardList";
  11. import PickerStockTake from "./PickerStockTake";
  12. import PickerReStockTake from "./PickerReStockTake";
  13. import ApproverStockTakeAll from "./ApproverStockTakeAll";
  14. import { useStockTakeQtyGapWarnPercent } from "./useStockTakeQtyGapWarnPercent";
  15. import {
  16. parseQtyGapWarnPercent,
  17. saveStockTakeQtyGapWarnPercent,
  18. } from "./qtyGapWarnSettingClient";
  19. import { STOCK_TAKE_QTY_GAP_WARN_PERCENT } from "./stockTakeQtyGapWarning";
  20. type ViewScope = "picker" | "approver-all";
  21. const DEFAULT_PICKER_CARD_LIST_FILTERS: PickerCardListFilters = {
  22. sectionDescription: "All",
  23. stockTakeSession: "",
  24. status: "All",
  25. area: "",
  26. storeId: "All",
  27. };
  28. const StockTakeTab: React.FC = () => {
  29. const { t } = useTranslation(["stockTake", "common"]);
  30. const { data: session } = useSession() as { data: SessionWithTokens | null };
  31. const isAdmin = (session?.abilities ?? session?.user?.abilities ?? []).some(
  32. (ability) => String(ability).trim() === AUTH.ADMIN,
  33. );
  34. const qtyGapWarnPercent = useStockTakeQtyGapWarnPercent();
  35. const [qtyGapDraft, setQtyGapDraft] = useState(String(STOCK_TAKE_QTY_GAP_WARN_PERCENT));
  36. const [qtyGapSaving, setQtyGapSaving] = useState(false);
  37. const qtyGapSaveLock = useRef(false);
  38. const [tabValue, setTabValue] = useState(0);
  39. const [selectedSession, setSelectedSession] = useState<AllPickedStockTakeListReponse | null>(null);
  40. const [viewMode, setViewMode] = useState<"details" | "reStockTake">("details");
  41. const [viewScope, setViewScope] = useState<ViewScope>("picker");
  42. const [approverSession, setApproverSession] = useState<AllPickedStockTakeListReponse | null>(null);
  43. const [approverLoading, setApproverLoading] = useState(false);
  44. /** 從卡片列表進入明細後返回時保留分頁 */
  45. const [pickerListPage, setPickerListPage] = useState(0);
  46. const [pickerListPageSize] = useState(6);
  47. const [pickerSearchFilters, setPickerSearchFilters] = useState<PickerCardListFilters>(DEFAULT_PICKER_CARD_LIST_FILTERS);
  48. const [pickerAppliedFilters, setPickerAppliedFilters] = useState<PickerCardListFilters>(DEFAULT_PICKER_CARD_LIST_FILTERS);
  49. const [snackbar, setSnackbar] = useState<{
  50. open: boolean;
  51. message: string;
  52. severity: "success" | "error" | "warning"
  53. }>({
  54. open: false,
  55. message: "",
  56. severity: "success",
  57. });
  58. const handleCardClick = useCallback((session: AllPickedStockTakeListReponse) => {
  59. setSelectedSession(session);
  60. setViewMode("details");
  61. }, []);
  62. const handleReStockTakeClick = useCallback((session: AllPickedStockTakeListReponse) => {
  63. setSelectedSession(session);
  64. setViewMode("reStockTake");
  65. setViewScope("picker");
  66. }, []);
  67. const handleBackToList = useCallback(() => {
  68. setSelectedSession(null);
  69. setViewMode("details");
  70. }, []);
  71. const handleSnackbar = useCallback((message: string, severity: "success" | "error" | "warning") => {
  72. setSnackbar({
  73. open: true,
  74. message,
  75. severity,
  76. });
  77. }, []);
  78. useEffect(() => {
  79. setQtyGapDraft(String(qtyGapWarnPercent));
  80. }, [qtyGapWarnPercent]);
  81. const saveQtyGapWarnPercent = useCallback(async () => {
  82. if (!isAdmin || qtyGapSaveLock.current) return;
  83. const parsed = Number(qtyGapDraft.trim());
  84. if (!Number.isInteger(parsed) || parsed < 0 || parsed > 1000) {
  85. handleSnackbar(t("qtyGapWarnPercentInvalid"), "warning");
  86. return;
  87. }
  88. qtyGapSaveLock.current = true;
  89. setQtyGapSaving(true);
  90. try {
  91. await saveStockTakeQtyGapWarnPercent(parsed);
  92. setQtyGapDraft(String(parseQtyGapWarnPercent(String(parsed))));
  93. handleSnackbar(t("qtyGapWarnPercentSaved"), "success");
  94. } catch (e) {
  95. handleSnackbar(e instanceof Error ? e.message : t("qtyGapWarnPercentInvalid"), "error");
  96. } finally {
  97. qtyGapSaveLock.current = false;
  98. setQtyGapSaving(false);
  99. }
  100. }, [handleSnackbar, isAdmin, qtyGapDraft, t]);
  101. useEffect(() => {
  102. if (tabValue !== 1 && tabValue !== 2) return;
  103. setApproverLoading(true);
  104. getLatestApproverStockTakeHeader()
  105. .then((header) => {
  106. setApproverSession(header ?? null);
  107. })
  108. .catch((e) => {
  109. console.error(e);
  110. setApproverSession(null);
  111. })
  112. .finally(() => setApproverLoading(false));
  113. }, [tabValue]);
  114. if (selectedSession && viewScope === "picker") {
  115. return (
  116. <Box>
  117. {viewScope === "picker" && (
  118. tabValue === 0 ? (
  119. viewMode === "reStockTake" ? (
  120. <PickerReStockTake
  121. selectedSession={selectedSession}
  122. onBack={handleBackToList}
  123. onSnackbar={handleSnackbar}
  124. />
  125. ) : (
  126. <PickerStockTake
  127. selectedSession={selectedSession}
  128. onBack={handleBackToList}
  129. onSnackbar={handleSnackbar}
  130. />
  131. )
  132. ) : null
  133. )}
  134. <Snackbar
  135. open={snackbar.open}
  136. autoHideDuration={6000}
  137. onClose={() => setSnackbar({ ...snackbar, open: false })}
  138. >
  139. <Alert onClose={() => setSnackbar({ ...snackbar, open: false })} severity={snackbar.severity}>
  140. {snackbar.message}
  141. </Alert>
  142. </Snackbar>
  143. </Box>
  144. );
  145. }
  146. return (
  147. <Box>
  148. {isAdmin && (
  149. <Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1 }}>
  150. <Typography
  151. component="label"
  152. htmlFor="stock-take-qty-gap-warn"
  153. sx={{ m: 0, height: 40, fontSize: 18, fontWeight: 500, lineHeight: "40px" }}
  154. >
  155. {t("qtyGapWarnPercent")}
  156. </Typography>
  157. <TextField
  158. id="stock-take-qty-gap-warn"
  159. size="small"
  160. type="number"
  161. value={qtyGapDraft}
  162. onChange={(e) => setQtyGapDraft(e.target.value.replace(/[^\d]/g, ""))}
  163. inputProps={{ min: 0, max: 1000, inputMode: "numeric" }}
  164. sx={{
  165. width: 88,
  166. m: 0,
  167. "& .MuiFilledInput-root": { height: 40 },
  168. "& .MuiFilledInput-input.MuiInputBase-inputSizeSmall": {
  169. height: 40,
  170. boxSizing: "border-box",
  171. paddingTop: 0,
  172. paddingBottom: 0,
  173. lineHeight: "40px",
  174. },
  175. }}
  176. />
  177. <Button
  178. size="small"
  179. variant="outlined"
  180. disabled={qtyGapSaving}
  181. onClick={saveQtyGapWarnPercent}
  182. sx={{ height: 40 }}
  183. >
  184. {t("Save")}
  185. </Button>
  186. </Stack>
  187. )}
  188. <Tabs
  189. value={tabValue}
  190. onChange={(e, newValue) => {
  191. setTabValue(newValue);
  192. if (newValue === 0) {
  193. setViewScope("picker");
  194. } else {
  195. setViewScope("approver-all");
  196. }
  197. }}
  198. sx={{ mb: 2 }}
  199. >
  200. <Tab label={t("Picker")} />
  201. <Tab label={t("Approver Pending")} />
  202. <Tab label={t("Approver Approved")} />
  203. </Tabs>
  204. {tabValue === 0 && (
  205. <PickerCardList
  206. page={pickerListPage}
  207. pageSize={pickerListPageSize}
  208. onListPageChange={setPickerListPage}
  209. searchFilters={pickerSearchFilters}
  210. appliedFilters={pickerAppliedFilters}
  211. onSearchFiltersChange={setPickerSearchFilters}
  212. onAppliedFiltersChange={setPickerAppliedFilters}
  213. onCardClick={(session) => {
  214. setViewScope("picker");
  215. handleCardClick(session);
  216. }}
  217. onReStockTakeClick={handleReStockTakeClick}
  218. />
  219. )}
  220. {tabValue === 1 && (
  221. <Box>
  222. {approverLoading ? (
  223. <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
  224. <CircularProgress />
  225. </Box>
  226. ) : approverSession ? (
  227. <ApproverStockTakeAll
  228. selectedSession={approverSession}
  229. mode="pending"
  230. onSnackbar={handleSnackbar}
  231. />
  232. ) : (
  233. <Typography variant="body2" color="text.secondary">
  234. {t("No data")}
  235. </Typography>
  236. )}
  237. </Box>
  238. )}
  239. {tabValue === 2 && (
  240. <Box>
  241. {approverSession ? (
  242. <ApproverStockTakeAll
  243. selectedSession={approverSession}
  244. mode="approved"
  245. onSnackbar={handleSnackbar}
  246. />
  247. ) : (
  248. <Typography variant="body2" color="text.secondary">
  249. {t("No data")}
  250. </Typography>
  251. )}
  252. </Box>
  253. )}
  254. <Snackbar
  255. open={snackbar.open}
  256. autoHideDuration={6000}
  257. onClose={() => setSnackbar({ ...snackbar, open: false })}
  258. >
  259. <Alert onClose={() => setSnackbar({ ...snackbar, open: false })} severity={snackbar.severity}>
  260. {snackbar.message}
  261. </Alert>
  262. </Snackbar>
  263. </Box>
  264. );
  265. };
  266. export default StockTakeTab;