FPSMS-frontend
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 

315 řádky
10 KiB

  1. "use client";
  2. import React, { useState, useRef, useEffect } from "react";
  3. import { Material, MaterialDatabase } from "./types";
  4. import {
  5. Box,
  6. Typography,
  7. TextField,
  8. Paper,
  9. Stack,
  10. Button,
  11. IconButton,
  12. Chip,
  13. } from "@mui/material";
  14. import CheckIcon from "@mui/icons-material/Check";
  15. import CloseIcon from "@mui/icons-material/Close";
  16. interface MaterialLotScannerProps {
  17. materials: Material[];
  18. onMaterialsChange: (materials: Material[]) => void;
  19. error?: string;
  20. }
  21. const MaterialLotScanner: React.FC<MaterialLotScannerProps> = ({
  22. materials,
  23. onMaterialsChange,
  24. error,
  25. }) => {
  26. const [materialScanInput, setMaterialScanInput] = useState<string>("");
  27. const materialScanRef = useRef<HTMLInputElement>(null);
  28. useEffect(() => {
  29. if (materialScanRef.current) {
  30. materialScanRef.current.focus();
  31. }
  32. }, []);
  33. const handleMaterialInputChange = (
  34. e: React.ChangeEvent<HTMLInputElement>,
  35. ) => {
  36. setMaterialScanInput(e.target.value.trim());
  37. };
  38. const handleMaterialInputKeyPress = async (
  39. e: React.KeyboardEvent<HTMLInputElement>,
  40. ) => {
  41. if (e.key === "Enter") {
  42. const target = e.target as HTMLInputElement;
  43. const scannedLot = target.value.trim();
  44. if (scannedLot) {
  45. const response = await fetch(
  46. "http://your-backend-url.com/validateLot",
  47. {
  48. method: "POST",
  49. headers: {
  50. "Content-Type": "application/json",
  51. },
  52. body: JSON.stringify({ lot: scannedLot }),
  53. },
  54. );
  55. const data = await response.json();
  56. if (data.suggestedLot) {
  57. const updatedMaterials = materials.map((material) => {
  58. if (material.name === data.matchedMaterial.name) {
  59. const isAlreadyAdded = material.lotNumbers.includes(scannedLot);
  60. if (!isAlreadyAdded) {
  61. return {
  62. ...material,
  63. isUsed: true,
  64. lotNumbers: [...material.lotNumbers, scannedLot],
  65. };
  66. }
  67. }
  68. return material;
  69. });
  70. onMaterialsChange(updatedMaterials);
  71. setMaterialScanInput("");
  72. } else if (data.matchedMaterial) {
  73. setMaterialScanInput(
  74. scannedLot +
  75. " (Invalid lot number format. Suggested lot number: " +
  76. data.suggestedLot +
  77. ")",
  78. );
  79. } else {
  80. setMaterialScanInput(
  81. scannedLot +
  82. " (Invalid lot number format or material not recognized.)",
  83. );
  84. }
  85. }
  86. }
  87. };
  88. const removeLotNumber = (materialName: string, lotNumber: string): void => {
  89. const updatedMaterials = materials.map((material) => {
  90. if (material.name === materialName) {
  91. const updatedLotNumbers = material.lotNumbers.filter(
  92. (lot) => lot !== lotNumber,
  93. );
  94. return {
  95. ...material,
  96. lotNumbers: updatedLotNumbers,
  97. isUsed: updatedLotNumbers.length > 0,
  98. };
  99. }
  100. return material;
  101. });
  102. onMaterialsChange(updatedMaterials);
  103. };
  104. const requiredMaterials = materials.filter((m) => m.required);
  105. const optionalMaterials = materials.filter((m) => !m.required);
  106. return (
  107. <Box>
  108. <Typography variant="h6" fontWeight={600} mb={2}>
  109. Material Lot Numbers
  110. </Typography>
  111. <Paper
  112. elevation={2}
  113. sx={{
  114. mb: 2,
  115. p: 2,
  116. bgcolor: "yellow.50",
  117. border: "1px solid",
  118. borderColor: "yellow.200",
  119. }}
  120. >
  121. <TextField
  122. inputRef={materialScanRef}
  123. type="text"
  124. value={materialScanInput}
  125. onChange={handleMaterialInputChange}
  126. onKeyPress={handleMaterialInputKeyPress}
  127. fullWidth
  128. label="Scan or enter material lot number (e.g., SS-240616-001)"
  129. variant="outlined"
  130. size="small"
  131. sx={{ bgcolor: "white" }}
  132. />
  133. <Box mt={2}>
  134. <Typography variant="body2" color="warning.main">
  135. <strong>Lot Number Formats:</strong>
  136. </Typography>
  137. <Typography variant="body2" color="warning.main">
  138. Steel Sheet: SS-YYMMDD-XXX | Aluminum: AL-YYMMDD-XXX | Plastic:
  139. PP-YYMMDD-XXX
  140. </Typography>
  141. <Typography variant="body2" color="warning.main">
  142. Copper Wire: CW-YYMMDD-XXX | Rubber: RG-YYMMDD-XXX | Glass:
  143. GP-YYMMDD-XXX
  144. </Typography>
  145. </Box>
  146. </Paper>
  147. {error && (
  148. <Typography color="error" variant="body2" mb={2}>
  149. {error}
  150. </Typography>
  151. )}
  152. <Stack spacing={3}>
  153. {/* Required Materials */}
  154. <Box>
  155. <Stack direction="row" alignItems="center" spacing={1} mb={1}>
  156. <Typography variant="subtitle1" fontWeight={600} color="error.main">
  157. Required Materials
  158. </Typography>
  159. <Chip label="Must scan lot numbers" color="error" size="small" />
  160. </Stack>
  161. <Stack spacing={1}>
  162. {requiredMaterials.map((material) => (
  163. <Paper
  164. key={material.id}
  165. sx={{
  166. p: 2,
  167. bgcolor: "red.50",
  168. border: "1px solid",
  169. borderColor: "red.200",
  170. }}
  171. >
  172. <Stack
  173. direction="row"
  174. alignItems="center"
  175. justifyContent="space-between"
  176. mb={1}
  177. >
  178. <Stack direction="row" alignItems="center" spacing={1}>
  179. {material.isUsed && (
  180. <CheckIcon fontSize="small" color="success" />
  181. )}
  182. <Typography fontWeight={500}>{material.name}</Typography>
  183. </Stack>
  184. <Typography variant="body2" color="text.secondary">
  185. {material.lotNumbers.length} lot(s)
  186. </Typography>
  187. </Stack>
  188. {material.lotNumbers.length > 0 && (
  189. <Stack spacing={1}>
  190. {material.lotNumbers.map((lotNumber, index) => (
  191. <Paper
  192. key={index}
  193. sx={{
  194. p: 1,
  195. display: "flex",
  196. alignItems: "center",
  197. justifyContent: "space-between",
  198. bgcolor: "white",
  199. border: "1px solid",
  200. borderColor: "grey.200",
  201. }}
  202. >
  203. <Typography variant="body2" fontFamily="monospace">
  204. {lotNumber}
  205. </Typography>
  206. <IconButton
  207. onClick={() =>
  208. removeLotNumber(material.name, lotNumber)
  209. }
  210. color="error"
  211. size="small"
  212. >
  213. <CloseIcon fontSize="small" />
  214. </IconButton>
  215. </Paper>
  216. ))}
  217. </Stack>
  218. )}
  219. </Paper>
  220. ))}
  221. </Stack>
  222. </Box>
  223. {/* Optional Materials */}
  224. <Box>
  225. <Stack direction="row" alignItems="center" spacing={1} mb={1}>
  226. <Typography
  227. variant="subtitle1"
  228. fontWeight={600}
  229. color="primary.main"
  230. >
  231. Optional Materials
  232. </Typography>
  233. <Chip
  234. label="Lot numbers recommended"
  235. color="primary"
  236. size="small"
  237. />
  238. </Stack>
  239. <Stack spacing={1}>
  240. {optionalMaterials.map((material) => (
  241. <Paper
  242. key={material.id}
  243. sx={{
  244. p: 2,
  245. bgcolor: "blue.50",
  246. border: "1px solid",
  247. borderColor: "blue.200",
  248. }}
  249. >
  250. <Stack
  251. direction="row"
  252. alignItems="center"
  253. justifyContent="space-between"
  254. mb={1}
  255. >
  256. <Stack direction="row" alignItems="center" spacing={1}>
  257. {material.isUsed && (
  258. <CheckIcon fontSize="small" color="success" />
  259. )}
  260. <Typography fontWeight={500}>{material.name}</Typography>
  261. </Stack>
  262. <Typography variant="body2" color="text.secondary">
  263. {material.lotNumbers.length} lot(s)
  264. </Typography>
  265. </Stack>
  266. {material.lotNumbers.length > 0 && (
  267. <Stack spacing={1}>
  268. {material.lotNumbers.map((lotNumber, index) => (
  269. <Paper
  270. key={index}
  271. sx={{
  272. p: 1,
  273. display: "flex",
  274. alignItems: "center",
  275. justifyContent: "space-between",
  276. bgcolor: "white",
  277. border: "1px solid",
  278. borderColor: "grey.200",
  279. }}
  280. >
  281. <Typography variant="body2" fontFamily="monospace">
  282. {lotNumber}
  283. </Typography>
  284. <IconButton
  285. onClick={() =>
  286. removeLotNumber(material.name, lotNumber)
  287. }
  288. color="error"
  289. size="small"
  290. >
  291. <CloseIcon fontSize="small" />
  292. </IconButton>
  293. </Paper>
  294. ))}
  295. </Stack>
  296. )}
  297. </Paper>
  298. ))}
  299. </Stack>
  300. </Box>
  301. </Stack>
  302. </Box>
  303. );
  304. };
  305. export default MaterialLotScanner;