Tekijä | SHA1 | Viesti | Päivämäärä |
---|---|---|---|
|
71a80fb80a | Merge branch 'master' of https://git.2fi-solutions.com/derek/FPSMS-frontend | 2 kuukautta sitten |
|
7cb62c365c | Update i18n wrapper | 2 kuukautta sitten |
|
6beca4fc3c | Add production process | 2 kuukautta sitten |
@@ -1,5 +1,6 @@ | |||
import { preloadClaims } from "@/app/api/claims"; | |||
import ClaimSearch from "@/components/ClaimSearch"; | |||
import ProductionProcess from "@/components/ProductionProcess"; | |||
import { getServerI18n } from "@/i18n"; | |||
import Add from "@mui/icons-material/Add"; | |||
import Button from "@mui/material/Button"; | |||
@@ -38,7 +39,7 @@ const production: React.FC = async () => { | |||
</Button> | |||
</Stack> | |||
<Suspense fallback={<ClaimSearch.Loading />}> | |||
<ClaimSearch /> | |||
<ProductionProcess /> | |||
</Suspense> | |||
</> | |||
); | |||
@@ -89,7 +89,7 @@ const NavigationContent: React.FC = () => { | |||
{ | |||
icon: <RequestQuote />, | |||
label: "Job Order", | |||
path: "", | |||
path: "/production", | |||
}, | |||
{ | |||
icon: <RequestQuote />, | |||
@@ -485,10 +485,10 @@ function PoInputGrid({ | |||
// marginRight: 1, | |||
}} | |||
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 | |||
// 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; | |||
} |