| @@ -1,5 +1,6 @@ | |||||
| import { preloadClaims } from "@/app/api/claims"; | import { preloadClaims } from "@/app/api/claims"; | ||||
| import ClaimSearch from "@/components/ClaimSearch"; | import ClaimSearch from "@/components/ClaimSearch"; | ||||
| import ProductionProcess from "@/components/ProductionProcess"; | |||||
| import { getServerI18n } from "@/i18n"; | import { getServerI18n } from "@/i18n"; | ||||
| import Add from "@mui/icons-material/Add"; | import Add from "@mui/icons-material/Add"; | ||||
| import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||
| @@ -38,7 +39,7 @@ const production: React.FC = async () => { | |||||
| </Button> | </Button> | ||||
| </Stack> | </Stack> | ||||
| <Suspense fallback={<ClaimSearch.Loading />}> | <Suspense fallback={<ClaimSearch.Loading />}> | ||||
| <ClaimSearch /> | |||||
| <ProductionProcess /> | |||||
| </Suspense> | </Suspense> | ||||
| </> | </> | ||||
| ); | ); | ||||
| @@ -89,7 +89,7 @@ const NavigationContent: React.FC = () => { | |||||
| { | { | ||||
| icon: <RequestQuote />, | icon: <RequestQuote />, | ||||
| label: "Job Order", | label: "Job Order", | ||||
| path: "", | |||||
| path: "/production", | |||||
| }, | }, | ||||
| { | { | ||||
| icon: <RequestQuote />, | icon: <RequestQuote />, | ||||
| @@ -485,10 +485,10 @@ function PoInputGrid({ | |||||
| // marginRight: 1, | // marginRight: 1, | ||||
| }} | }} | ||||
| disabled={ | disabled={ | ||||
| // stockInLineStatusMap[status] === 9 || | |||||
| // stockInLineStatusMap[status] <= 2 || | |||||
| // stockInLineStatusMap[status] >= 7 || | |||||
| ((stockInLineStatusMap[status] >= 3 && stockInLineStatusMap[status] <= 5) ? !session?.user?.abilities?.includes("APPROVAL"): false) | |||||
| stockInLineStatusMap[status] === 9 || | |||||
| stockInLineStatusMap[status] <= 2 || | |||||
| stockInLineStatusMap[status] >= 7 || | |||||
| ((stockInLineStatusMap[status] >= 3 && stockInLineStatusMap[status] <= 5) && !session?.user?.abilities?.includes("APPROVAL")) | |||||
| } | } | ||||
| // set _isNew to false after posting | // set _isNew to false after posting | ||||
| // or check status | // or check status | ||||
| @@ -0,0 +1,138 @@ | |||||
| "use client"; | |||||
| import React, { useState, useRef } from 'react'; | |||||
| import { Machine } from './types'; | |||||
| import { Button, TextField, Typography, Paper, Box, IconButton, Stack } from '@mui/material'; | |||||
| import CloseIcon from '@mui/icons-material/Close'; | |||||
| interface MachineScannerProps { | |||||
| machines: Machine[]; | |||||
| onMachinesChange: (machines: Machine[]) => void; | |||||
| error?: string; | |||||
| } | |||||
| const machineDatabase: Machine[] = [ | |||||
| { id: 1, name: 'CNC Mill #1', code: 'CNC001', qrCode: 'QR-CNC001' }, | |||||
| { id: 2, name: 'Lathe #2', code: 'LAT002', qrCode: 'QR-LAT002' }, | |||||
| { id: 3, name: 'Press #3', code: 'PRS003', qrCode: 'QR-PRS003' }, | |||||
| { id: 4, name: 'Welder #4', code: 'WLD004', qrCode: 'QR-WLD004' }, | |||||
| { id: 5, name: 'Drill Press #5', code: 'DRL005', qrCode: 'QR-DRL005' } | |||||
| ]; | |||||
| const MachineScanner: React.FC<MachineScannerProps> = ({ | |||||
| machines, | |||||
| onMachinesChange, | |||||
| error | |||||
| }) => { | |||||
| const [scanningMode, setScanningMode] = useState<boolean>(false); | |||||
| const machineScanRef = useRef<HTMLInputElement>(null); | |||||
| const startScanning = (): void => { | |||||
| setScanningMode(true); | |||||
| setTimeout(() => { | |||||
| if (machineScanRef.current) { | |||||
| machineScanRef.current.focus(); | |||||
| } | |||||
| }, 100); | |||||
| }; | |||||
| const stopScanning = (): void => { | |||||
| setScanningMode(false); | |||||
| }; | |||||
| const handleMachineScan = (e: React.KeyboardEvent<HTMLInputElement>): void => { | |||||
| if (e.key === 'Enter') { | |||||
| const target = e.target as HTMLInputElement; | |||||
| const scannedCode = target.value.trim(); | |||||
| const machine = machineDatabase.find(m => | |||||
| m.qrCode === scannedCode || m.code === scannedCode | |||||
| ); | |||||
| if (machine) { | |||||
| const isAlreadyAdded = machines.some(m => m.id === machine.id); | |||||
| if (!isAlreadyAdded) { | |||||
| onMachinesChange([...machines, machine]); | |||||
| } | |||||
| target.value = ''; | |||||
| stopScanning(); | |||||
| } else { | |||||
| alert('Machine not found. Please check the code and try again.'); | |||||
| target.value = ''; | |||||
| } | |||||
| } | |||||
| }; | |||||
| const removeMachine = (machineId: number): void => { | |||||
| onMachinesChange(machines.filter(m => m.id !== machineId)); | |||||
| }; | |||||
| return ( | |||||
| <Box> | |||||
| <Stack direction="row" alignItems="center" justifyContent="space-between" mb={2}> | |||||
| <Typography variant="h6" fontWeight={600}> | |||||
| Machines * | |||||
| </Typography> | |||||
| <Button | |||||
| variant={scanningMode ? 'contained' : 'outlined'} | |||||
| color={scanningMode ? 'success' : 'primary'} | |||||
| onClick={startScanning} | |||||
| > | |||||
| {scanningMode ? 'Scanning...' : 'Scan QR Code'} | |||||
| </Button> | |||||
| </Stack> | |||||
| {scanningMode && ( | |||||
| <Paper elevation={2} sx={{ mb: 2, p: 2, bgcolor: 'green.50', border: '1px solid', borderColor: 'green.200' }}> | |||||
| <Stack direction="row" alignItems="center" spacing={2}> | |||||
| <TextField | |||||
| inputRef={machineScanRef} | |||||
| type="text" | |||||
| onKeyPress={handleMachineScan} | |||||
| fullWidth | |||||
| label="Scan machine QR code or enter manually..." | |||||
| variant="outlined" | |||||
| size="small" | |||||
| sx={{ bgcolor: 'white' }} | |||||
| /> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="inherit" | |||||
| onClick={stopScanning} | |||||
| > | |||||
| Cancel | |||||
| </Button> | |||||
| </Stack> | |||||
| <Typography variant="body2" color="success.main" mt={1}> | |||||
| Position the QR code scanner and scan, or type the machine code manually | |||||
| </Typography> | |||||
| </Paper> | |||||
| )} | |||||
| {error && <Typography color="error" variant="body2" mb={1}>{error}</Typography>} | |||||
| <Stack spacing={1}> | |||||
| {machines.map((machine) => ( | |||||
| <Paper key={machine.id} sx={{ p: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between', bgcolor: 'green.50', border: '1px solid', borderColor: 'green.200' }}> | |||||
| <Box> | |||||
| <Typography fontWeight={500} color="success.dark">{machine.name}</Typography> | |||||
| <Typography variant="body2" color="success.main">{machine.code}</Typography> | |||||
| </Box> | |||||
| <IconButton onClick={() => removeMachine(machine.id)} color="error"> | |||||
| <CloseIcon /> | |||||
| </IconButton> | |||||
| </Paper> | |||||
| ))} | |||||
| {machines.length === 0 && ( | |||||
| <Paper sx={{ p: 2, bgcolor: 'grey.100', border: '1px solid', borderColor: 'grey.200' }}> | |||||
| <Typography color="text.secondary" fontStyle="italic" variant="body2"> | |||||
| No machines added yet. Use the scan button to add machines. | |||||
| </Typography> | |||||
| </Paper> | |||||
| )} | |||||
| </Stack> | |||||
| </Box> | |||||
| ); | |||||
| }; | |||||
| export default MachineScanner; | |||||
| @@ -0,0 +1,204 @@ | |||||
| "use client"; | |||||
| import React, { useState, useRef, useEffect } from 'react'; | |||||
| import { Material, MaterialDatabase } from './types'; | |||||
| import { Box, Typography, TextField, Paper, Stack, Button, IconButton, Chip } from '@mui/material'; | |||||
| import CheckIcon from '@mui/icons-material/Check'; | |||||
| import CloseIcon from '@mui/icons-material/Close'; | |||||
| interface MaterialLotScannerProps { | |||||
| materials: Material[]; | |||||
| onMaterialsChange: (materials: Material[]) => void; | |||||
| error?: string; | |||||
| } | |||||
| const materialDatabase: MaterialDatabase[] = [ | |||||
| { name: 'Steel Sheet', lotPattern: /^SS-\d{6}-\d{3}$/, required: true }, | |||||
| { name: 'Aluminum Rod', lotPattern: /^AL-\d{6}-\d{3}$/, required: false }, | |||||
| { name: 'Plastic Pellets', lotPattern: /^PP-\d{6}-\d{3}$/, required: false }, | |||||
| { name: 'Copper Wire', lotPattern: /^CW-\d{6}-\d{3}$/, required: false }, | |||||
| { name: 'Rubber Gasket', lotPattern: /^RG-\d{6}-\d{3}$/, required: false }, | |||||
| { name: 'Glass Panel', lotPattern: /^GP-\d{6}-\d{3}$/, required: false }, | |||||
| { name: 'Hydraulic Oil', lotPattern: /^HO-\d{6}-\d{3}$/, required: false }, | |||||
| { name: 'Cutting Fluid', lotPattern: /^CF-\d{6}-\d{3}$/, required: false }, | |||||
| { name: 'Welding Rod', lotPattern: /^WR-\d{6}-\d{3}$/, required: false }, | |||||
| { name: 'Adhesive', lotPattern: /^AD-\d{6}-\d{3}$/, required: false } | |||||
| ]; | |||||
| const MaterialLotScanner: React.FC<MaterialLotScannerProps> = ({ | |||||
| materials, | |||||
| onMaterialsChange, | |||||
| error | |||||
| }) => { | |||||
| const [materialScanInput, setMaterialScanInput] = useState<string>(''); | |||||
| const materialScanRef = useRef<HTMLInputElement>(null); | |||||
| useEffect(() => { | |||||
| if (materialScanRef.current) { | |||||
| materialScanRef.current.focus(); | |||||
| } | |||||
| }, []); | |||||
| const handleMaterialInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { | |||||
| setMaterialScanInput(e.target.value.trim()); | |||||
| }; | |||||
| const handleMaterialInputKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => { | |||||
| if (e.key === 'Enter') { | |||||
| const target = e.target as HTMLInputElement; | |||||
| const scannedLot = target.value.trim(); | |||||
| if (scannedLot) { | |||||
| const matchedMaterial = materialDatabase.find(material => | |||||
| material.lotPattern.test(scannedLot) | |||||
| ); | |||||
| if (matchedMaterial) { | |||||
| const updatedMaterials = materials.map(material => { | |||||
| if (material.name === matchedMaterial.name) { | |||||
| const isAlreadyAdded = material.lotNumbers.includes(scannedLot); | |||||
| if (!isAlreadyAdded) { | |||||
| return { | |||||
| ...material, | |||||
| isUsed: true, | |||||
| lotNumbers: [...material.lotNumbers, scannedLot] | |||||
| }; | |||||
| } | |||||
| } | |||||
| return material; | |||||
| }); | |||||
| onMaterialsChange(updatedMaterials); | |||||
| setMaterialScanInput(''); | |||||
| } else { | |||||
| alert('Invalid lot number format or material not recognized.'); | |||||
| setMaterialScanInput(''); | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| const removeLotNumber = (materialName: string, lotNumber: string): void => { | |||||
| const updatedMaterials = materials.map(material => { | |||||
| if (material.name === materialName) { | |||||
| const updatedLotNumbers = material.lotNumbers.filter(lot => lot !== lotNumber); | |||||
| return { | |||||
| ...material, | |||||
| lotNumbers: updatedLotNumbers, | |||||
| isUsed: updatedLotNumbers.length > 0 | |||||
| }; | |||||
| } | |||||
| return material; | |||||
| }); | |||||
| onMaterialsChange(updatedMaterials); | |||||
| }; | |||||
| const requiredMaterials = materials.filter(m => m.required); | |||||
| const optionalMaterials = materials.filter(m => !m.required); | |||||
| return ( | |||||
| <Box> | |||||
| <Typography variant="h6" fontWeight={600} mb={2}> | |||||
| Material Lot Numbers | |||||
| </Typography> | |||||
| <Paper elevation={2} sx={{ mb: 2, p: 2, bgcolor: 'yellow.50', border: '1px solid', borderColor: 'yellow.200' }}> | |||||
| <TextField | |||||
| inputRef={materialScanRef} | |||||
| type="text" | |||||
| value={materialScanInput} | |||||
| onChange={handleMaterialInputChange} | |||||
| onKeyPress={handleMaterialInputKeyPress} | |||||
| fullWidth | |||||
| label="Scan or enter material lot number (e.g., SS-240616-001)" | |||||
| variant="outlined" | |||||
| size="small" | |||||
| sx={{ bgcolor: 'white' }} | |||||
| /> | |||||
| <Box mt={2}> | |||||
| <Typography variant="body2" color="warning.main"> | |||||
| <strong>Lot Number Formats:</strong> | |||||
| </Typography> | |||||
| <Typography variant="body2" color="warning.main"> | |||||
| Steel Sheet: SS-YYMMDD-XXX | Aluminum: AL-YYMMDD-XXX | Plastic: PP-YYMMDD-XXX | |||||
| </Typography> | |||||
| <Typography variant="body2" color="warning.main"> | |||||
| Copper Wire: CW-YYMMDD-XXX | Rubber: RG-YYMMDD-XXX | Glass: GP-YYMMDD-XXX | |||||
| </Typography> | |||||
| </Box> | |||||
| </Paper> | |||||
| {error && <Typography color="error" variant="body2" mb={2}>{error}</Typography>} | |||||
| <Stack spacing={3}> | |||||
| {/* Required Materials */} | |||||
| <Box> | |||||
| <Stack direction="row" alignItems="center" spacing={1} mb={1}> | |||||
| <Typography variant="subtitle1" fontWeight={600} color="error.main"> | |||||
| Required Materials | |||||
| </Typography> | |||||
| <Chip label="Must scan lot numbers" color="error" size="small" /> | |||||
| </Stack> | |||||
| <Stack spacing={1}> | |||||
| {requiredMaterials.map((material) => ( | |||||
| <Paper key={material.id} sx={{ p: 2, bgcolor: 'red.50', border: '1px solid', borderColor: 'red.200' }}> | |||||
| <Stack direction="row" alignItems="center" justifyContent="space-between" mb={1}> | |||||
| <Stack direction="row" alignItems="center" spacing={1}> | |||||
| {material.isUsed && <CheckIcon fontSize="small" color="success" />} | |||||
| <Typography fontWeight={500}>{material.name}</Typography> | |||||
| </Stack> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {material.lotNumbers.length} lot(s) | |||||
| </Typography> | |||||
| </Stack> | |||||
| {material.lotNumbers.length > 0 && ( | |||||
| <Stack spacing={1}> | |||||
| {material.lotNumbers.map((lotNumber, index) => ( | |||||
| <Paper key={index} sx={{ p: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between', bgcolor: 'white', border: '1px solid', borderColor: 'grey.200' }}> | |||||
| <Typography variant="body2" fontFamily="monospace">{lotNumber}</Typography> | |||||
| <IconButton onClick={() => removeLotNumber(material.name, lotNumber)} color="error" size="small"> | |||||
| <CloseIcon fontSize="small" /> | |||||
| </IconButton> | |||||
| </Paper> | |||||
| ))} | |||||
| </Stack> | |||||
| )} | |||||
| </Paper> | |||||
| ))} | |||||
| </Stack> | |||||
| </Box> | |||||
| {/* Optional Materials */} | |||||
| <Box> | |||||
| <Stack direction="row" alignItems="center" spacing={1} mb={1}> | |||||
| <Typography variant="subtitle1" fontWeight={600} color="primary.main"> | |||||
| Optional Materials | |||||
| </Typography> | |||||
| <Chip label="Lot numbers recommended" color="primary" size="small" /> | |||||
| </Stack> | |||||
| <Stack spacing={1}> | |||||
| {optionalMaterials.map((material) => ( | |||||
| <Paper key={material.id} sx={{ p: 2, bgcolor: 'blue.50', border: '1px solid', borderColor: 'blue.200' }}> | |||||
| <Stack direction="row" alignItems="center" justifyContent="space-between" mb={1}> | |||||
| <Stack direction="row" alignItems="center" spacing={1}> | |||||
| {material.isUsed && <CheckIcon fontSize="small" color="success" />} | |||||
| <Typography fontWeight={500}>{material.name}</Typography> | |||||
| </Stack> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {material.lotNumbers.length} lot(s) | |||||
| </Typography> | |||||
| </Stack> | |||||
| {material.lotNumbers.length > 0 && ( | |||||
| <Stack spacing={1}> | |||||
| {material.lotNumbers.map((lotNumber, index) => ( | |||||
| <Paper key={index} sx={{ p: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between', bgcolor: 'white', border: '1px solid', borderColor: 'grey.200' }}> | |||||
| <Typography variant="body2" fontFamily="monospace">{lotNumber}</Typography> | |||||
| <IconButton onClick={() => removeLotNumber(material.name, lotNumber)} color="error" size="small"> | |||||
| <CloseIcon fontSize="small" /> | |||||
| </IconButton> | |||||
| </Paper> | |||||
| ))} | |||||
| </Stack> | |||||
| )} | |||||
| </Paper> | |||||
| ))} | |||||
| </Stack> | |||||
| </Box> | |||||
| </Stack> | |||||
| </Box> | |||||
| ); | |||||
| }; | |||||
| export default MaterialLotScanner; | |||||
| @@ -0,0 +1,138 @@ | |||||
| "use client"; | |||||
| import React, { useState, useRef } from 'react'; | |||||
| import { Operator } from './types'; | |||||
| import { Button, TextField, Typography, Paper, Box, IconButton, Stack } from '@mui/material'; | |||||
| import CloseIcon from '@mui/icons-material/Close'; | |||||
| interface OperatorScannerProps { | |||||
| operators: Operator[]; | |||||
| onOperatorsChange: (operators: Operator[]) => void; | |||||
| error?: string; | |||||
| } | |||||
| const operatorDatabase: Operator[] = [ | |||||
| { id: 1, name: 'John Smith', employeeId: 'EMP001', cardId: '12345678' }, | |||||
| { id: 2, name: 'Maria Garcia', employeeId: 'EMP002', cardId: '23456789' }, | |||||
| { id: 3, name: 'David Chen', employeeId: 'EMP003', cardId: '34567890' }, | |||||
| { id: 4, name: 'Sarah Johnson', employeeId: 'EMP004', cardId: '45678901' }, | |||||
| { id: 5, name: 'Mike Wilson', employeeId: 'EMP005', cardId: '56789012' } | |||||
| ]; | |||||
| const OperatorScanner: React.FC<OperatorScannerProps> = ({ | |||||
| operators, | |||||
| onOperatorsChange, | |||||
| error | |||||
| }) => { | |||||
| const [scanningMode, setScanningMode] = useState<boolean>(false); | |||||
| const operatorScanRef = useRef<HTMLInputElement>(null); | |||||
| const startScanning = (): void => { | |||||
| setScanningMode(true); | |||||
| setTimeout(() => { | |||||
| if (operatorScanRef.current) { | |||||
| operatorScanRef.current.focus(); | |||||
| } | |||||
| }, 100); | |||||
| }; | |||||
| const stopScanning = (): void => { | |||||
| setScanningMode(false); | |||||
| }; | |||||
| const handleOperatorScan = (e: React.KeyboardEvent<HTMLInputElement>): void => { | |||||
| if (e.key === 'Enter') { | |||||
| const target = e.target as HTMLInputElement; | |||||
| const scannedId = target.value.trim(); | |||||
| const operator = operatorDatabase.find(op => | |||||
| op.cardId === scannedId || op.employeeId === scannedId | |||||
| ); | |||||
| if (operator) { | |||||
| const isAlreadyAdded = operators.some(op => op.id === operator.id); | |||||
| if (!isAlreadyAdded) { | |||||
| onOperatorsChange([...operators, operator]); | |||||
| } | |||||
| target.value = ''; | |||||
| stopScanning(); | |||||
| } else { | |||||
| alert('Operator not found. Please check the ID and try again.'); | |||||
| target.value = ''; | |||||
| } | |||||
| } | |||||
| }; | |||||
| const removeOperator = (operatorId: number): void => { | |||||
| onOperatorsChange(operators.filter(op => op.id !== operatorId)); | |||||
| }; | |||||
| return ( | |||||
| <Box> | |||||
| <Stack direction="row" alignItems="center" justifyContent="space-between" mb={2}> | |||||
| <Typography variant="h6" fontWeight={600}> | |||||
| Operators * | |||||
| </Typography> | |||||
| <Button | |||||
| variant={scanningMode ? 'contained' : 'outlined'} | |||||
| color={scanningMode ? 'success' : 'primary'} | |||||
| onClick={startScanning} | |||||
| > | |||||
| {scanningMode ? 'Scanning...' : 'Scan ID Card'} | |||||
| </Button> | |||||
| </Stack> | |||||
| {scanningMode && ( | |||||
| <Paper elevation={2} sx={{ mb: 2, p: 2, bgcolor: 'green.50', border: '1px solid', borderColor: 'green.200' }}> | |||||
| <Stack direction="row" alignItems="center" spacing={2}> | |||||
| <TextField | |||||
| inputRef={operatorScanRef} | |||||
| type="text" | |||||
| onKeyPress={handleOperatorScan} | |||||
| fullWidth | |||||
| label="Scan operator ID card or enter manually..." | |||||
| variant="outlined" | |||||
| size="small" | |||||
| sx={{ bgcolor: 'white' }} | |||||
| /> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="inherit" | |||||
| onClick={stopScanning} | |||||
| > | |||||
| Cancel | |||||
| </Button> | |||||
| </Stack> | |||||
| <Typography variant="body2" color="success.main" mt={1}> | |||||
| Position the ID card scanner and scan, or type the employee ID manually | |||||
| </Typography> | |||||
| </Paper> | |||||
| )} | |||||
| {error && <Typography color="error" variant="body2" mb={1}>{error}</Typography>} | |||||
| <Stack spacing={1}> | |||||
| {operators.map((operator) => ( | |||||
| <Paper key={operator.id} sx={{ p: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between', bgcolor: 'blue.50', border: '1px solid', borderColor: 'blue.200' }}> | |||||
| <Box> | |||||
| <Typography fontWeight={500} color="primary.dark">{operator.name}</Typography> | |||||
| <Typography variant="body2" color="primary.main">{operator.employeeId}</Typography> | |||||
| </Box> | |||||
| <IconButton onClick={() => removeOperator(operator.id)} color="error"> | |||||
| <CloseIcon /> | |||||
| </IconButton> | |||||
| </Paper> | |||||
| ))} | |||||
| {operators.length === 0 && ( | |||||
| <Paper sx={{ p: 2, bgcolor: 'grey.100', border: '1px solid', borderColor: 'grey.200' }}> | |||||
| <Typography color="text.secondary" fontStyle="italic" variant="body2"> | |||||
| No operators added yet. Use the scan button to add operators. | |||||
| </Typography> | |||||
| </Paper> | |||||
| )} | |||||
| </Stack> | |||||
| </Box> | |||||
| ); | |||||
| }; | |||||
| export default OperatorScanner; | |||||
| @@ -0,0 +1,29 @@ | |||||
| "use client"; | |||||
| import React, { useState } from 'react'; | |||||
| import ProductionRecordingModal from './ProductionRecordingModal'; | |||||
| import { Box, Button } from '@mui/material'; | |||||
| const ProductionProcess: React.FC = () => { | |||||
| const [isModalOpen, setIsModalOpen] = useState<boolean>(false); | |||||
| console.log("prodcution process"); | |||||
| return ( | |||||
| <> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="primary" | |||||
| size="large" | |||||
| onClick={() => setIsModalOpen(true)} | |||||
| sx={{ fontWeight: 500, borderRadius: 2, mb: 2 }} | |||||
| > | |||||
| Open Production Recording Modal | |||||
| </Button> | |||||
| <ProductionRecordingModal | |||||
| isOpen={isModalOpen} | |||||
| onClose={() => setIsModalOpen(false)} | |||||
| /> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default ProductionProcess; | |||||
| @@ -0,0 +1,40 @@ | |||||
| import Card from "@mui/material/Card"; | |||||
| import CardContent from "@mui/material/CardContent"; | |||||
| import Skeleton from "@mui/material/Skeleton"; | |||||
| import Stack from "@mui/material/Stack"; | |||||
| import React from "react"; | |||||
| // Can make this nicer | |||||
| export const ProductionProcessLoading: React.FC = () => { | |||||
| return ( | |||||
| <> | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Stack spacing={2}> | |||||
| <Skeleton variant="rounded" height={60} /> | |||||
| <Skeleton variant="rounded" height={60} /> | |||||
| <Skeleton variant="rounded" height={60} /> | |||||
| <Skeleton | |||||
| variant="rounded" | |||||
| height={50} | |||||
| width={100} | |||||
| sx={{ alignSelf: "flex-end" }} | |||||
| /> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Stack spacing={2}> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default ProductionProcessLoading; | |||||
| @@ -0,0 +1,15 @@ | |||||
| import ProductionProcess from "./ProductionProcess"; | |||||
| import ProductionProcessLoading from "./ProductionProcessLoading"; | |||||
| interface SubComponents { | |||||
| Loading: typeof ProductionProcessLoading; | |||||
| } | |||||
| const ProductionProcessWrapper: React.FC & SubComponents = () => { | |||||
| console.log("Testing") | |||||
| return <ProductionProcess />; | |||||
| }; | |||||
| ProductionProcessWrapper.Loading = ProductionProcessLoading; | |||||
| export default ProductionProcessWrapper; | |||||
| @@ -0,0 +1,257 @@ | |||||
| "use client"; | |||||
| import React from 'react'; | |||||
| import { X, Save } from '@mui/icons-material'; | |||||
| import { Dialog, DialogTitle, DialogContent, DialogActions, IconButton, Typography, TextField, Stack, Box, Button } from '@mui/material'; | |||||
| import { useForm, Controller } from 'react-hook-form'; | |||||
| import { Operator, Machine, Material, FormData, ProductionRecord } from './types'; | |||||
| import OperatorScanner from './OperatorScanner'; | |||||
| import MachineScanner from './MachineScanner'; | |||||
| import MaterialLotScanner from './MaterialLotScanner'; | |||||
| interface ProductionRecordingModalProps { | |||||
| isOpen: boolean; | |||||
| onClose: () => void; | |||||
| } | |||||
| const initialMaterials: Material[] = [ | |||||
| { id: 1, name: 'Steel Sheet', isUsed: false, lotNumbers: [], required: true }, | |||||
| { id: 2, name: 'Aluminum Rod', isUsed: false, lotNumbers: [], required: false }, | |||||
| { id: 3, name: 'Plastic Pellets', isUsed: false, lotNumbers: [], required: false }, | |||||
| { id: 4, name: 'Copper Wire', isUsed: false, lotNumbers: [], required: false }, | |||||
| { id: 5, name: 'Rubber Gasket', isUsed: false, lotNumbers: [], required: false }, | |||||
| { id: 6, name: 'Glass Panel', isUsed: false, lotNumbers: [], required: false }, | |||||
| { id: 7, name: 'Hydraulic Oil', isUsed: false, lotNumbers: [], required: false }, | |||||
| { id: 8, name: 'Cutting Fluid', isUsed: false, lotNumbers: [], required: false }, | |||||
| { id: 9, name: 'Welding Rod', isUsed: false, lotNumbers: [], required: false }, | |||||
| { id: 10, name: 'Adhesive', isUsed: false, lotNumbers: [], required: false } | |||||
| ]; | |||||
| const ProductionRecordingModal: React.FC<ProductionRecordingModalProps> = ({ | |||||
| isOpen, | |||||
| onClose | |||||
| }) => { | |||||
| const { | |||||
| control, | |||||
| handleSubmit, | |||||
| reset, | |||||
| watch, | |||||
| setValue, | |||||
| setError, | |||||
| clearErrors, | |||||
| formState: { errors } | |||||
| } = useForm<FormData>({ | |||||
| defaultValues: { | |||||
| productionDate: new Date().toISOString().split('T')[0], | |||||
| notes: '', | |||||
| operators: [], | |||||
| machines: [], | |||||
| materials: initialMaterials | |||||
| } | |||||
| }); | |||||
| const watchedOperators = watch('operators'); | |||||
| const watchedMachines = watch('machines'); | |||||
| const watchedMaterials = watch('materials'); | |||||
| const validateForm = (): boolean => { | |||||
| let isValid = true; | |||||
| // Clear previous errors | |||||
| clearErrors(); | |||||
| // Validate operators | |||||
| if (!watchedOperators || watchedOperators.length === 0) { | |||||
| setError('operators', { | |||||
| type: 'required', | |||||
| message: 'At least one operator is required' | |||||
| }); | |||||
| isValid = false; | |||||
| } | |||||
| // Validate machines | |||||
| if (!watchedMachines || watchedMachines.length === 0) { | |||||
| setError('machines', { | |||||
| type: 'required', | |||||
| message: 'At least one machine is required' | |||||
| }); | |||||
| isValid = false; | |||||
| } | |||||
| // Validate materials | |||||
| console.log(watchedMaterials) | |||||
| if (watchedMaterials) { | |||||
| const missingLotNumbers = watchedMaterials | |||||
| .filter(m => m.required && m.isUsed && (!m.lotNumbers || m.lotNumbers.length === 0)) | |||||
| .length; | |||||
| if (missingLotNumbers > 0) { | |||||
| setError('materials', { | |||||
| type: 'custom', | |||||
| message: `${missingLotNumbers} required material(s) missing lot numbers` | |||||
| }); | |||||
| isValid = false; | |||||
| } | |||||
| } | |||||
| return isValid; | |||||
| }; | |||||
| const onSubmit = (data: FormData) => { | |||||
| if (!validateForm()) { | |||||
| return; | |||||
| } | |||||
| const usedMaterials = data.materials | |||||
| .filter(material => material.isUsed) | |||||
| .map(material => ({ | |||||
| id: material.id, | |||||
| name: material.name, | |||||
| lotNumbers: material.lotNumbers, | |||||
| required: material.required, | |||||
| isUsed: false | |||||
| })); | |||||
| const recordData: ProductionRecord = { | |||||
| ...data, | |||||
| materials: usedMaterials, | |||||
| timestamp: new Date().toISOString() | |||||
| }; | |||||
| console.log('Production record saved:', recordData); | |||||
| handleClose(); | |||||
| }; | |||||
| const handleClose = (): void => { | |||||
| reset({ | |||||
| productionDate: new Date().toISOString().split('T')[0], | |||||
| notes: '', | |||||
| operators: [], | |||||
| machines: [], | |||||
| materials: initialMaterials.map(material => ({ | |||||
| ...material, | |||||
| isUsed: false, | |||||
| lotNumbers: [] | |||||
| })) | |||||
| }); | |||||
| onClose(); | |||||
| }; | |||||
| const getUsedMaterialsCount = (): number => | |||||
| watchedMaterials?.filter(m => m.isUsed).length || 0; | |||||
| if (!isOpen) return null; | |||||
| return ( | |||||
| <Dialog open={isOpen} onClose={handleClose} maxWidth="xl" fullWidth> | |||||
| <DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> | |||||
| <Box> | |||||
| <Typography variant="h5" fontWeight={700}>Production Recording</Typography> | |||||
| <Typography variant="body2" color="text.secondary" mt={0.5}> | |||||
| {watchedOperators?.length || 0} operator(s), {watchedMachines?.length || 0} machine(s), {getUsedMaterialsCount()} materials | |||||
| </Typography> | |||||
| </Box> | |||||
| <IconButton onClick={handleClose} size="large"> | |||||
| <X /> | |||||
| </IconButton> | |||||
| </DialogTitle> | |||||
| <DialogContent dividers sx={{ p: 3 }}> | |||||
| <Stack spacing={4}> | |||||
| {/* Basic Information */} | |||||
| <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}> | |||||
| <Controller | |||||
| name="productionDate" | |||||
| control={control} | |||||
| rules={{ | |||||
| required: 'Production date is required' | |||||
| }} | |||||
| render={({ field }) => ( | |||||
| <TextField | |||||
| {...field} | |||||
| type="date" | |||||
| label="Production Date" | |||||
| InputLabelProps={{ shrink: true }} | |||||
| fullWidth | |||||
| size="small" | |||||
| error={!!errors.productionDate} | |||||
| helperText={errors.productionDate?.message} | |||||
| /> | |||||
| )} | |||||
| /> | |||||
| </Stack> | |||||
| {/* Operator Scanner */} | |||||
| <OperatorScanner | |||||
| operators={watchedOperators || []} | |||||
| onOperatorsChange={(operators) => { | |||||
| setValue('operators', operators); | |||||
| if (operators.length > 0) { | |||||
| clearErrors('operators'); | |||||
| } | |||||
| }} | |||||
| error={errors.operators?.message} | |||||
| /> | |||||
| {/* Machine Scanner */} | |||||
| <MachineScanner | |||||
| machines={watchedMachines || []} | |||||
| onMachinesChange={(machines) => { | |||||
| setValue('machines', machines); | |||||
| if (machines.length > 0) { | |||||
| clearErrors('machines'); | |||||
| } | |||||
| }} | |||||
| error={errors.machines?.message} | |||||
| /> | |||||
| {/* Material Lot Scanner */} | |||||
| <MaterialLotScanner | |||||
| materials={watchedMaterials || []} | |||||
| onMaterialsChange={(materials) => { | |||||
| setValue('materials', materials); | |||||
| clearErrors('materials'); | |||||
| }} | |||||
| error={errors.materials?.message} | |||||
| /> | |||||
| {/* Additional Notes */} | |||||
| <Controller | |||||
| name="notes" | |||||
| control={control} | |||||
| render={({ field }) => ( | |||||
| <TextField | |||||
| {...field} | |||||
| label="Additional Notes" | |||||
| multiline | |||||
| minRows={3} | |||||
| fullWidth | |||||
| size="small" | |||||
| placeholder="Enter any additional production notes..." | |||||
| error={!!errors.notes} | |||||
| helperText={errors.notes?.message} | |||||
| /> | |||||
| )} | |||||
| /> | |||||
| </Stack> | |||||
| </DialogContent> | |||||
| <DialogActions sx={{ p: 3 }}> | |||||
| <Button | |||||
| variant="outlined" | |||||
| color="inherit" | |||||
| onClick={handleClose} | |||||
| > | |||||
| Cancel | |||||
| </Button> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="primary" | |||||
| onClick={handleSubmit(onSubmit)} | |||||
| startIcon={<Save />} | |||||
| > | |||||
| Save Record | |||||
| </Button> | |||||
| </DialogActions> | |||||
| </Dialog> | |||||
| ); | |||||
| }; | |||||
| export default ProductionRecordingModal; | |||||
| @@ -0,0 +1 @@ | |||||
| export { default } from "./ProductionProcessWrapper"; | |||||
| @@ -0,0 +1,49 @@ | |||||
| export interface Operator { | |||||
| id: number; | |||||
| name: string; | |||||
| employeeId: string; | |||||
| cardId: string; | |||||
| } | |||||
| export interface Machine { | |||||
| id: number; | |||||
| name: string; | |||||
| code: string; | |||||
| qrCode: string; | |||||
| } | |||||
| export interface Material { | |||||
| id: number; | |||||
| name: string; | |||||
| isUsed: boolean; | |||||
| lotNumbers: string[]; | |||||
| required: boolean; | |||||
| } | |||||
| export interface MaterialDatabase { | |||||
| name: string; | |||||
| lotPattern: RegExp; | |||||
| required: boolean; | |||||
| } | |||||
| export interface FormData { | |||||
| productionDate: string; | |||||
| notes: string; | |||||
| operators: Operator[]; | |||||
| machines: Machine[]; | |||||
| materials: Material[]; | |||||
| } | |||||
| export interface ProductionRecord extends FormData { | |||||
| timestamp: string; | |||||
| } | |||||
| export interface ValidationRule { | |||||
| required?: boolean; | |||||
| message?: string; | |||||
| custom?: (value: any, allValues?: any) => string | null; | |||||
| } | |||||
| export interface ValidationRules { | |||||
| [key: string]: ValidationRule; | |||||
| } | |||||