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

355 lines
12 KiB

  1. "use client";
  2. import React, { useCallback, useEffect, useMemo, useState } from "react";
  3. import {
  4. Box,
  5. Button,
  6. Dialog,
  7. DialogActions,
  8. DialogContent,
  9. DialogTitle,
  10. Stack,
  11. TextField,
  12. Typography,
  13. CircularProgress,
  14. IconButton,
  15. TableContainer,
  16. Table,
  17. TableHead,
  18. TableRow,
  19. TableCell,
  20. TableBody,
  21. } from "@mui/material";
  22. import Delete from "@mui/icons-material/Delete";
  23. import Add from "@mui/icons-material/Add";
  24. import { useTranslation } from "react-i18next";
  25. import { Edit } from "@mui/icons-material";
  26. import SearchBox, { Criterion } from "@/components/SearchBox/SearchBox";
  27. import SearchResults, { Column } from "@/components/SearchResults/SearchResults";
  28. import {
  29. fetchStockTakeSections,
  30. updateSectionDescription,
  31. clearWarehouseSection,
  32. getWarehousesBySection,
  33. searchWarehousesForAddToSection,
  34. editWarehouse,
  35. } from "@/app/api/warehouse/actions";
  36. import { WarehouseResult } from "@/app/api/warehouse";
  37. import { StockTakeSectionInfo } from "@/app/api/warehouse";
  38. import { deleteDialog, successDialog } from "@/components/Swal/CustomAlerts";
  39. type SearchKey = "stockTakeSection" | "stockTakeSectionDescription";
  40. export default function TabStockTakeSectionMapping() {
  41. const { t } = useTranslation(["warehouse", "common"]);
  42. const [sections, setSections] = useState<StockTakeSectionInfo[]>([]);
  43. const [filteredSections, setFilteredSections] = useState<StockTakeSectionInfo[]>([]);
  44. const [selectedSection, setSelectedSection] = useState<StockTakeSectionInfo | null>(null);
  45. const [warehousesInSection, setWarehousesInSection] = useState<WarehouseResult[]>([]);
  46. const [loading, setLoading] = useState(true);
  47. const [openDialog, setOpenDialog] = useState(false);
  48. const [editDesc, setEditDesc] = useState("");
  49. const [savingDesc, setSavingDesc] = useState(false);
  50. const [warehouseList, setWarehouseList] = useState<WarehouseResult[]>([]);
  51. const [openAddDialog, setOpenAddDialog] = useState(false);
  52. const [addStoreId, setAddStoreId] = useState("");
  53. const [addWarehouse, setAddWarehouse] = useState("");
  54. const [addArea, setAddArea] = useState("");
  55. const [addSlot, setAddSlot] = useState("");
  56. const [addSearchResults, setAddSearchResults] = useState<WarehouseResult[]>([]);
  57. const [addSearching, setAddSearching] = useState(false);
  58. const [addingWarehouseId, setAddingWarehouseId] = useState<number | null>(null);
  59. const loadSections = useCallback(async () => {
  60. setLoading(true);
  61. try {
  62. const data = await fetchStockTakeSections();
  63. const withId = (data ?? []).map((s) => ({
  64. ...s,
  65. id: s.stockTakeSection,
  66. }));
  67. setSections(withId);
  68. setFilteredSections(withId);
  69. } catch (e) {
  70. console.error(e);
  71. setSections([]);
  72. setFilteredSections([]);
  73. } finally {
  74. setLoading(false);
  75. }
  76. }, []);
  77. useEffect(() => {
  78. loadSections();
  79. }, [loadSections]);
  80. const handleViewSection = useCallback(async (section: StockTakeSectionInfo) => {
  81. setSelectedSection(section);
  82. setEditDesc(section.stockTakeSectionDescription ?? "");
  83. setOpenDialog(true);
  84. try {
  85. const list = await getWarehousesBySection(section.stockTakeSection);
  86. setWarehousesInSection(list ?? []);
  87. } catch (e) {
  88. console.error(e);
  89. setWarehousesInSection([]);
  90. }
  91. }, []);
  92. const criteria: Criterion<SearchKey>[] = useMemo(
  93. () => [
  94. { type: "text", label: "Stock Take Section", paramName: "stockTakeSection", placeholder: "" },
  95. { type: "text", label: "Stock Take Section Description", paramName: "stockTakeSectionDescription", placeholder: "" },
  96. ],
  97. []
  98. );
  99. const handleSearch = useCallback((inputs: Record<SearchKey | `${SearchKey}To`, string>) => {
  100. const section = (inputs.stockTakeSection ?? "").trim().toLowerCase();
  101. const desc = (inputs.stockTakeSectionDescription ?? "").trim().toLowerCase();
  102. setFilteredSections(
  103. sections.filter(
  104. (s) =>
  105. (!section || (s.stockTakeSection ?? "").toLowerCase().includes(section)) &&
  106. (!desc || (s.stockTakeSectionDescription ?? "").toLowerCase().includes(desc))
  107. )
  108. );
  109. }, [sections]);
  110. const handleReset = useCallback(() => {
  111. setFilteredSections(sections);
  112. }, [sections]);
  113. const handleSaveDescription = useCallback(async () => {
  114. if (!selectedSection) return;
  115. setSavingDesc(true);
  116. try {
  117. await updateSectionDescription(selectedSection.stockTakeSection, editDesc || null);
  118. await loadSections();
  119. if (selectedSection) {
  120. setSelectedSection((prev) => (prev ? { ...prev, stockTakeSectionDescription: editDesc || null } : null));
  121. }
  122. successDialog(t("Saved"), t);
  123. } catch (e) {
  124. console.error(e);
  125. } finally {
  126. setSavingDesc(false);
  127. }
  128. }, [selectedSection, editDesc, loadSections, t]);
  129. const handleRemoveWarehouse = useCallback(
  130. (warehouse: WarehouseResult) => {
  131. deleteDialog(async () => {
  132. try {
  133. await clearWarehouseSection(warehouse.id);
  134. setWarehousesInSection((prev) => prev.filter((w) => w.id !== warehouse.id));
  135. successDialog(t("Delete Success"), t);
  136. } catch (e) {
  137. console.error(e);
  138. }
  139. }, t);
  140. },
  141. [t]
  142. );
  143. const handleOpenAddWarehouse = useCallback(() => {
  144. setAddStoreId("");
  145. setAddWarehouse("");
  146. setAddArea("");
  147. setAddSlot("");
  148. setAddSearchResults([]);
  149. setOpenAddDialog(true);
  150. }, []);
  151. const handleAddSearch = useCallback(async () => {
  152. if (!selectedSection) return;
  153. setAddSearching(true);
  154. try {
  155. const params: { store_id?: string; warehouse?: string; area?: string; slot?: string } = {};
  156. if (addStoreId.trim()) params.store_id = addStoreId.trim();
  157. if (addWarehouse.trim()) params.warehouse = addWarehouse.trim();
  158. if (addArea.trim()) params.area = addArea.trim();
  159. if (addSlot.trim()) params.slot = addSlot.trim();
  160. const list = await searchWarehousesForAddToSection(params, selectedSection.stockTakeSection);
  161. setAddSearchResults(list ?? []);
  162. } catch (e) {
  163. console.error(e);
  164. setAddSearchResults([]);
  165. } finally {
  166. setAddSearching(false);
  167. }
  168. }, [selectedSection, addStoreId, addWarehouse, addArea, addSlot]);
  169. const handleAddWarehouseToSection = useCallback(
  170. async (w: WarehouseResult) => {
  171. if (!selectedSection) return;
  172. setAddingWarehouseId(w.id);
  173. try {
  174. await editWarehouse(w.id, {
  175. stockTakeSection: selectedSection.stockTakeSection,
  176. stockTakeSectionDescription: selectedSection.stockTakeSectionDescription ?? undefined,
  177. });
  178. setWarehousesInSection((prev) => [...prev, w]);
  179. setAddSearchResults((prev) => prev.filter((x) => x.id !== w.id));
  180. successDialog(t("Add Success") ?? t("Saved"), t);
  181. } catch (e) {
  182. console.error(e);
  183. } finally {
  184. setAddingWarehouseId(null);
  185. }
  186. },
  187. [selectedSection, t]
  188. );
  189. const columns = useMemo<Column<StockTakeSectionInfo>[]>(
  190. () => [
  191. { name: "stockTakeSection", label: t("stockTakeSection"), align: "left", sx: { width: "25%" } },
  192. { name: "stockTakeSectionDescription", label: t("stockTakeSectionDescription"), align: "left", sx: { width: "35%" } },
  193. {
  194. name: "id",
  195. label: t("Edit"),
  196. onClick: (row) => handleViewSection(row),
  197. buttonIcon: <Edit />,
  198. buttonIcons: {} as Record<keyof StockTakeSectionInfo, React.ReactNode>,
  199. color: "primary",
  200. sx: { width: "20%" },
  201. },
  202. ],
  203. [t, handleViewSection]
  204. );
  205. if (loading) {
  206. return (
  207. <Box sx={{ display: "flex", justifyContent: "center", minHeight: 200, alignItems: "center" }}>
  208. <CircularProgress />
  209. </Box>
  210. );
  211. }
  212. return (
  213. <Box>
  214. <SearchBox<SearchKey> criteria={criteria} onSearch={handleSearch} onReset={handleReset} />
  215. <SearchResults<StockTakeSectionInfo> items={filteredSections} columns={columns} />
  216. <Dialog open={openDialog} onClose={() => setOpenDialog(false)} maxWidth="md" fullWidth sx={{ zIndex: 1000 }}>
  217. <DialogTitle>
  218. {t("Mapping Details")} - {selectedSection?.stockTakeSection} ({selectedSection?.stockTakeSectionDescription ?? ""})
  219. </DialogTitle>
  220. <DialogContent>
  221. <Stack direction="row" alignItems="center" spacing={2} sx={{ mb: 1, minHeight: 40 }}>
  222. <Typography variant="body2" sx={{ display: "flex", alignItems: "center" }}>
  223. {t("stockTakeSectionDescription")}
  224. </Typography>
  225. <TextField size="small" value={editDesc} onChange={(e) => setEditDesc(e.target.value)} sx={{ minWidth: 200 }} />
  226. <Button variant="contained" size="small" disabled={savingDesc} onClick={handleSaveDescription}>
  227. {t("Save")}
  228. </Button>
  229. <Box sx={{ flex: 1 }} />
  230. <Button variant="contained" startIcon={<Add />} onClick={handleOpenAddWarehouse}>
  231. {t("Add Warehouse")}
  232. </Button>
  233. </Stack>
  234. <TableContainer>
  235. <Table size="small">
  236. <TableHead>
  237. <TableRow>
  238. <TableCell>{t("code")}</TableCell>
  239. <TableCell>{t("Actions")}</TableCell>
  240. </TableRow>
  241. </TableHead>
  242. <TableBody>
  243. {warehousesInSection.length === 0 ? (
  244. <TableRow><TableCell colSpan={3} align="center">{t("No warehouses")}</TableCell></TableRow>
  245. ) : (
  246. warehousesInSection.map((w) => (
  247. <TableRow key={w.id}>
  248. <TableCell>{w.code}</TableCell>
  249. <TableCell>
  250. <IconButton color="error" size="small" onClick={() => handleRemoveWarehouse(w)}>
  251. <Delete />
  252. </IconButton>
  253. </TableCell>
  254. </TableRow>
  255. ))
  256. )}
  257. </TableBody>
  258. </Table>
  259. </TableContainer>
  260. </DialogContent>
  261. <DialogActions>
  262. <Button onClick={() => setOpenDialog(false)}>{t("Cancel")}</Button>
  263. </DialogActions>
  264. </Dialog>
  265. <Dialog open={openAddDialog} onClose={() => setOpenAddDialog(false)} maxWidth="sm" fullWidth sx={{ zIndex: 1000 }}>
  266. <DialogTitle>{t("Add Warehouse")}</DialogTitle>
  267. <DialogContent>
  268. <Stack spacing={2} sx={{ pt: 1 }}>
  269. <TextField
  270. size="small"
  271. label={t("Store ID")}
  272. value={addStoreId}
  273. onChange={(e) => setAddStoreId(e.target.value)}
  274. fullWidth
  275. />
  276. <TextField
  277. size="small"
  278. label={t("warehouse")}
  279. value={addWarehouse}
  280. onChange={(e) => setAddWarehouse(e.target.value)}
  281. fullWidth
  282. />
  283. <TextField
  284. size="small"
  285. label={t("area")}
  286. value={addArea}
  287. onChange={(e) => setAddArea(e.target.value)}
  288. fullWidth
  289. />
  290. <TextField
  291. size="small"
  292. label={t("slot")}
  293. value={addSlot}
  294. onChange={(e) => setAddSlot(e.target.value)}
  295. fullWidth
  296. />
  297. <Button variant="contained" onClick={handleAddSearch} disabled={addSearching}>
  298. {addSearching ? <CircularProgress size={20} /> : t("Search")}
  299. </Button>
  300. <TableContainer>
  301. <Table size="small">
  302. <TableHead>
  303. <TableRow>
  304. <TableCell>{t("code")}</TableCell>
  305. <TableCell>{t("name")}</TableCell>
  306. <TableCell>{t("Actions")}</TableCell>
  307. </TableRow>
  308. </TableHead>
  309. <TableBody>
  310. {addSearchResults
  311. .filter((w) => !warehousesInSection.some((inc) => inc.id === w.id))
  312. .map((w) => (
  313. <TableRow key={w.id}>
  314. <TableCell>{w.code}</TableCell>
  315. <TableCell>{w.name}</TableCell>
  316. <TableCell>
  317. <Button
  318. size="small"
  319. variant="outlined"
  320. disabled={addingWarehouseId === w.id}
  321. onClick={() => handleAddWarehouseToSection(w)}
  322. >
  323. {addingWarehouseId === w.id ? <CircularProgress size={16} /> : t("Add")}
  324. </Button>
  325. </TableCell>
  326. </TableRow>
  327. ))}
  328. </TableBody>
  329. </Table>
  330. </TableContainer>
  331. </Stack>
  332. </DialogContent>
  333. <DialogActions>
  334. <Button onClick={() => setOpenAddDialog(false)}>{t("Cancel")}</Button>
  335. </DialogActions>
  336. </Dialog>
  337. </Box>
  338. );
  339. }