FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

352 lines
11 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. Grid,
  11. Stack,
  12. Table,
  13. TableBody,
  14. TableCell,
  15. TableContainer,
  16. TableHead,
  17. TableRow,
  18. TextField,
  19. Typography,
  20. IconButton,
  21. CircularProgress,
  22. } from "@mui/material";
  23. import { useTranslation } from "react-i18next";
  24. import { Add, Delete, Edit } from "@mui/icons-material";
  25. import SearchBox, { Criterion } from "../SearchBox/SearchBox";
  26. import SearchResults, { Column } from "../SearchResults/SearchResults";
  27. import {
  28. saveItemQcCategoryMapping,
  29. deleteItemQcCategoryMapping,
  30. getItemQcCategoryMappings,
  31. fetchQcCategoriesForAll,
  32. fetchItemsForAll,
  33. getItemByCode,
  34. } from "@/app/api/settings/qcItemAll/actions";
  35. import {
  36. QcCategoryResult,
  37. ItemsResult,
  38. } from "@/app/api/settings/qcItemAll";
  39. import { ItemQcCategoryMappingInfo } from "@/app/api/settings/qcItemAll";
  40. import {
  41. deleteDialog,
  42. errorDialogWithContent,
  43. submitDialog,
  44. successDialog,
  45. } from "../Swal/CustomAlerts";
  46. type SearchQuery = Partial<Omit<QcCategoryResult, "id">>;
  47. type SearchParamNames = keyof SearchQuery;
  48. const Tab0ItemQcCategoryMapping: React.FC = () => {
  49. const { t } = useTranslation("qcItemAll");
  50. const [qcCategories, setQcCategories] = useState<QcCategoryResult[]>([]);
  51. const [filteredQcCategories, setFilteredQcCategories] = useState<QcCategoryResult[]>([]);
  52. const [selectedCategory, setSelectedCategory] = useState<QcCategoryResult | null>(null);
  53. const [mappings, setMappings] = useState<ItemQcCategoryMappingInfo[]>([]);
  54. const [openDialog, setOpenDialog] = useState(false);
  55. const [openAddDialog, setOpenAddDialog] = useState(false);
  56. const [itemCode, setItemCode] = useState<string>("");
  57. const [validatedItem, setValidatedItem] = useState<ItemsResult | null>(null);
  58. const [itemCodeError, setItemCodeError] = useState<string>("");
  59. const [validatingItemCode, setValidatingItemCode] = useState<boolean>(false);
  60. const [selectedType, setSelectedType] = useState<string>("IQC");
  61. const [loading, setLoading] = useState(true);
  62. useEffect(() => {
  63. const loadData = async () => {
  64. setLoading(true);
  65. try {
  66. // Only load categories list (same as Tab 2) - fast!
  67. const categories = await fetchQcCategoriesForAll();
  68. setQcCategories(categories || []);
  69. setFilteredQcCategories(categories || []);
  70. } catch (error) {
  71. console.error("Tab0: Error loading data:", error);
  72. setQcCategories([]);
  73. setFilteredQcCategories([]);
  74. if (error instanceof Error) {
  75. errorDialogWithContent(t("Error"), error.message, t);
  76. }
  77. } finally {
  78. setLoading(false);
  79. }
  80. };
  81. loadData();
  82. }, []);
  83. const handleViewMappings = useCallback(async (category: QcCategoryResult) => {
  84. setSelectedCategory(category);
  85. const mappingData = await getItemQcCategoryMappings(category.id);
  86. setMappings(mappingData);
  87. setOpenDialog(true);
  88. }, []);
  89. const handleAddMapping = useCallback(() => {
  90. if (!selectedCategory) return;
  91. setItemCode("");
  92. setValidatedItem(null);
  93. setItemCodeError("");
  94. setOpenAddDialog(true);
  95. }, [selectedCategory]);
  96. const handleItemCodeChange = useCallback(async (code: string) => {
  97. setItemCode(code);
  98. setValidatedItem(null);
  99. setItemCodeError("");
  100. if (!code || code.trim() === "") {
  101. return;
  102. }
  103. setValidatingItemCode(true);
  104. try {
  105. const item = await getItemByCode(code.trim());
  106. if (item) {
  107. setValidatedItem(item);
  108. setItemCodeError("");
  109. } else {
  110. setValidatedItem(null);
  111. setItemCodeError(t("Item code not found"));
  112. }
  113. } catch (error) {
  114. setValidatedItem(null);
  115. setItemCodeError(t("Error validating item code"));
  116. } finally {
  117. setValidatingItemCode(false);
  118. }
  119. }, [t]);
  120. const handleSaveMapping = useCallback(async () => {
  121. if (!selectedCategory || !validatedItem) return;
  122. await submitDialog(async () => {
  123. try {
  124. await saveItemQcCategoryMapping(
  125. validatedItem.id as number,
  126. selectedCategory.id,
  127. selectedType
  128. );
  129. // Close add dialog first
  130. setOpenAddDialog(false);
  131. setItemCode("");
  132. setValidatedItem(null);
  133. setItemCodeError("");
  134. // Reload mappings to update the view
  135. const mappingData = await getItemQcCategoryMappings(selectedCategory.id);
  136. setMappings(mappingData);
  137. // Show success message after closing dialogs
  138. await successDialog(t("Submit Success"), t);
  139. // Keep the view dialog open to show updated data
  140. } catch (error) {
  141. errorDialogWithContent(t("Submit Error"), String(error), t);
  142. }
  143. }, t);
  144. }, [selectedCategory, validatedItem, selectedType, t]);
  145. const handleDeleteMapping = useCallback(
  146. async (mappingId: number) => {
  147. if (!selectedCategory) return;
  148. deleteDialog(async () => {
  149. try {
  150. await deleteItemQcCategoryMapping(mappingId);
  151. await successDialog(t("Delete Success"), t);
  152. // Reload mappings
  153. const mappingData = await getItemQcCategoryMappings(selectedCategory.id);
  154. setMappings(mappingData);
  155. // No need to reload categories list - it doesn't change
  156. } catch (error) {
  157. errorDialogWithContent(t("Delete Error"), String(error), t);
  158. }
  159. }, t);
  160. },
  161. [selectedCategory, t]
  162. );
  163. const typeOptions = ["IQC", "IPQC", "OQC", "FQC"];
  164. const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
  165. () => [
  166. { label: t("Code"), paramName: "code", type: "text" },
  167. { label: t("Name"), paramName: "name", type: "text" },
  168. ],
  169. [t]
  170. );
  171. const onReset = useCallback(() => {
  172. setFilteredQcCategories(qcCategories);
  173. }, [qcCategories]);
  174. const columnWidthSx = (width = "10%") => {
  175. return { width: width, whiteSpace: "nowrap" };
  176. };
  177. const columns = useMemo<Column<QcCategoryResult>[]>(
  178. () => [
  179. { name: "code", label: t("Qc Category Code"), sx: columnWidthSx("20%") },
  180. { name: "name", label: t("Qc Category Name"), sx: columnWidthSx("40%") },
  181. {
  182. name: "id",
  183. label: t("Actions"),
  184. onClick: (category) => handleViewMappings(category),
  185. buttonIcon: <Edit />,
  186. buttonIcons: {} as any,
  187. sx: columnWidthSx("10%"),
  188. },
  189. ],
  190. [t, handleViewMappings]
  191. );
  192. if (loading) {
  193. return (
  194. <Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", minHeight: "200px" }}>
  195. <CircularProgress />
  196. </Box>
  197. );
  198. }
  199. return (
  200. <Box>
  201. <SearchBox
  202. criteria={searchCriteria}
  203. onSearch={(query) => {
  204. setFilteredQcCategories(
  205. qcCategories.filter(
  206. (qc) =>
  207. (!query.code || qc.code.toLowerCase().includes(query.code.toLowerCase())) &&
  208. (!query.name || qc.name.toLowerCase().includes(query.name.toLowerCase()))
  209. )
  210. );
  211. }}
  212. onReset={onReset}
  213. />
  214. <SearchResults<QcCategoryResult>
  215. items={filteredQcCategories}
  216. columns={columns}
  217. />
  218. {/* View Mappings Dialog */}
  219. <Dialog open={openDialog} onClose={() => setOpenDialog(false)} maxWidth="md" fullWidth>
  220. <DialogTitle>
  221. {t("Mapping Details")} - {selectedCategory?.name}
  222. </DialogTitle>
  223. <DialogContent>
  224. <Stack spacing={2} sx={{ mt: 1 }}>
  225. <Box sx={{ display: "flex", justifyContent: "flex-end" }}>
  226. <Button
  227. variant="contained"
  228. startIcon={<Add />}
  229. onClick={handleAddMapping}
  230. >
  231. {t("Add Mapping")}
  232. </Button>
  233. </Box>
  234. <TableContainer>
  235. <Table>
  236. <TableHead>
  237. <TableRow>
  238. <TableCell>{t("Item Code")}</TableCell>
  239. <TableCell>{t("Item Name")}</TableCell>
  240. <TableCell>{t("Type")}</TableCell>
  241. <TableCell>{t("Actions")}</TableCell>
  242. </TableRow>
  243. </TableHead>
  244. <TableBody>
  245. {mappings.length === 0 ? (
  246. <TableRow>
  247. <TableCell colSpan={4} align="center">
  248. {t("No mappings found")}
  249. </TableCell>
  250. </TableRow>
  251. ) : (
  252. mappings.map((mapping) => (
  253. <TableRow key={mapping.id}>
  254. <TableCell>{mapping.itemCode}</TableCell>
  255. <TableCell>{mapping.itemName}</TableCell>
  256. <TableCell>{mapping.type}</TableCell>
  257. <TableCell>
  258. <IconButton
  259. color="error"
  260. size="small"
  261. onClick={() => handleDeleteMapping(mapping.id)}
  262. >
  263. <Delete />
  264. </IconButton>
  265. </TableCell>
  266. </TableRow>
  267. ))
  268. )}
  269. </TableBody>
  270. </Table>
  271. </TableContainer>
  272. </Stack>
  273. </DialogContent>
  274. <DialogActions>
  275. <Button onClick={() => setOpenDialog(false)}>{t("Cancel")}</Button>
  276. </DialogActions>
  277. </Dialog>
  278. {/* Add Mapping Dialog */}
  279. <Dialog open={openAddDialog} onClose={() => setOpenAddDialog(false)} maxWidth="sm" fullWidth>
  280. <DialogTitle>{t("Add Mapping")}</DialogTitle>
  281. <DialogContent>
  282. <Stack spacing={2} sx={{ mt: 2 }}>
  283. <TextField
  284. label={t("Item Code")}
  285. value={itemCode}
  286. onChange={(e) => handleItemCodeChange(e.target.value)}
  287. error={!!itemCodeError}
  288. helperText={itemCodeError || (validatedItem ? `${validatedItem.code} - ${validatedItem.name}` : t("Enter item code to validate"))}
  289. fullWidth
  290. disabled={validatingItemCode}
  291. InputProps={{
  292. endAdornment: validatingItemCode ? <CircularProgress size={20} /> : null,
  293. }}
  294. />
  295. <TextField
  296. select
  297. label={t("Select Type")}
  298. value={selectedType}
  299. onChange={(e) => setSelectedType(e.target.value)}
  300. SelectProps={{
  301. native: true,
  302. }}
  303. fullWidth
  304. >
  305. {typeOptions.map((type) => (
  306. <option key={type} value={type}>
  307. {type}
  308. </option>
  309. ))}
  310. </TextField>
  311. </Stack>
  312. </DialogContent>
  313. <DialogActions>
  314. <Button onClick={() => setOpenAddDialog(false)}>{t("Cancel")}</Button>
  315. <Button
  316. variant="contained"
  317. onClick={handleSaveMapping}
  318. disabled={!validatedItem}
  319. >
  320. {t("Save")}
  321. </Button>
  322. </DialogActions>
  323. </Dialog>
  324. </Box>
  325. );
  326. };
  327. export default Tab0ItemQcCategoryMapping;