From 6beca4fc3c1498ed9d62906f1ccfe4b982ce68c0 Mon Sep 17 00:00:00 2001 From: "MSI\\2Fi" Date: Tue, 17 Jun 2025 17:21:48 +0800 Subject: [PATCH] Add production process --- src/app/(main)/production/page.tsx | 3 +- .../NavigationContent/NavigationContent.tsx | 2 +- src/components/PoDetail/PoInputGrid.tsx | 8 +- .../ProductionProcess/MachineScanner.tsx | 138 ++++++++++ .../ProductionProcess/MaterialLotScanner.tsx | 204 ++++++++++++++ .../ProductionProcess/OperatorScanner.tsx | 138 ++++++++++ .../ProductionProcess/ProductionProcess.tsx | 29 ++ .../ProductionProcessLoading.tsx | 40 +++ .../ProductionProcessWrapper.tsx | 15 + .../ProductionRecordingModal.tsx | 257 ++++++++++++++++++ src/components/ProductionProcess/index.ts | 1 + src/components/ProductionProcess/types.ts | 49 ++++ 12 files changed, 878 insertions(+), 6 deletions(-) create mode 100644 src/components/ProductionProcess/MachineScanner.tsx create mode 100644 src/components/ProductionProcess/MaterialLotScanner.tsx create mode 100644 src/components/ProductionProcess/OperatorScanner.tsx create mode 100644 src/components/ProductionProcess/ProductionProcess.tsx create mode 100644 src/components/ProductionProcess/ProductionProcessLoading.tsx create mode 100644 src/components/ProductionProcess/ProductionProcessWrapper.tsx create mode 100644 src/components/ProductionProcess/ProductionRecordingModal.tsx create mode 100644 src/components/ProductionProcess/index.ts create mode 100644 src/components/ProductionProcess/types.ts diff --git a/src/app/(main)/production/page.tsx b/src/app/(main)/production/page.tsx index 180b348..59093d2 100644 --- a/src/app/(main)/production/page.tsx +++ b/src/app/(main)/production/page.tsx @@ -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 () => { }> - + ); diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 4817500..3470431 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -89,7 +89,7 @@ const NavigationContent: React.FC = () => { { icon: , label: "Job Order", - path: "", + path: "/production", }, { icon: , diff --git a/src/components/PoDetail/PoInputGrid.tsx b/src/components/PoDetail/PoInputGrid.tsx index 09b54ee..54a4711 100644 --- a/src/components/PoDetail/PoInputGrid.tsx +++ b/src/components/PoDetail/PoInputGrid.tsx @@ -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 diff --git a/src/components/ProductionProcess/MachineScanner.tsx b/src/components/ProductionProcess/MachineScanner.tsx new file mode 100644 index 0000000..9b49d42 --- /dev/null +++ b/src/components/ProductionProcess/MachineScanner.tsx @@ -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 = ({ + machines, + onMachinesChange, + error +}) => { + const [scanningMode, setScanningMode] = useState(false); + const machineScanRef = useRef(null); + + const startScanning = (): void => { + setScanningMode(true); + setTimeout(() => { + if (machineScanRef.current) { + machineScanRef.current.focus(); + } + }, 100); + }; + + const stopScanning = (): void => { + setScanningMode(false); + }; + + const handleMachineScan = (e: React.KeyboardEvent): 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 ( + + + + Machines * + + + + + {scanningMode && ( + + + + + + + Position the QR code scanner and scan, or type the machine code manually + + + )} + + {error && {error}} + + + {machines.map((machine) => ( + + + {machine.name} + {machine.code} + + removeMachine(machine.id)} color="error"> + + + + ))} + {machines.length === 0 && ( + + + No machines added yet. Use the scan button to add machines. + + + )} + + + ); +}; + +export default MachineScanner; \ No newline at end of file diff --git a/src/components/ProductionProcess/MaterialLotScanner.tsx b/src/components/ProductionProcess/MaterialLotScanner.tsx new file mode 100644 index 0000000..c8bbb9c --- /dev/null +++ b/src/components/ProductionProcess/MaterialLotScanner.tsx @@ -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 = ({ + materials, + onMaterialsChange, + error +}) => { + const [materialScanInput, setMaterialScanInput] = useState(''); + const materialScanRef = useRef(null); + + useEffect(() => { + if (materialScanRef.current) { + materialScanRef.current.focus(); + } + }, []); + + const handleMaterialInputChange = (e: React.ChangeEvent) => { + setMaterialScanInput(e.target.value.trim()); + }; + + const handleMaterialInputKeyPress = (e: React.KeyboardEvent) => { + 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 ( + + + Material Lot Numbers + + + + + + Lot Number Formats: + + + Steel Sheet: SS-YYMMDD-XXX | Aluminum: AL-YYMMDD-XXX | Plastic: PP-YYMMDD-XXX + + + Copper Wire: CW-YYMMDD-XXX | Rubber: RG-YYMMDD-XXX | Glass: GP-YYMMDD-XXX + + + + {error && {error}} + + {/* Required Materials */} + + + + Required Materials + + + + + {requiredMaterials.map((material) => ( + + + + {material.isUsed && } + {material.name} + + + {material.lotNumbers.length} lot(s) + + + {material.lotNumbers.length > 0 && ( + + {material.lotNumbers.map((lotNumber, index) => ( + + {lotNumber} + removeLotNumber(material.name, lotNumber)} color="error" size="small"> + + + + ))} + + )} + + ))} + + + {/* Optional Materials */} + + + + Optional Materials + + + + + {optionalMaterials.map((material) => ( + + + + {material.isUsed && } + {material.name} + + + {material.lotNumbers.length} lot(s) + + + {material.lotNumbers.length > 0 && ( + + {material.lotNumbers.map((lotNumber, index) => ( + + {lotNumber} + removeLotNumber(material.name, lotNumber)} color="error" size="small"> + + + + ))} + + )} + + ))} + + + + + ); +}; + +export default MaterialLotScanner; \ No newline at end of file diff --git a/src/components/ProductionProcess/OperatorScanner.tsx b/src/components/ProductionProcess/OperatorScanner.tsx new file mode 100644 index 0000000..ce521f5 --- /dev/null +++ b/src/components/ProductionProcess/OperatorScanner.tsx @@ -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 = ({ + operators, + onOperatorsChange, + error +}) => { + const [scanningMode, setScanningMode] = useState(false); + const operatorScanRef = useRef(null); + + const startScanning = (): void => { + setScanningMode(true); + setTimeout(() => { + if (operatorScanRef.current) { + operatorScanRef.current.focus(); + } + }, 100); + }; + + const stopScanning = (): void => { + setScanningMode(false); + }; + + const handleOperatorScan = (e: React.KeyboardEvent): 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 ( + + + + Operators * + + + + + {scanningMode && ( + + + + + + + Position the ID card scanner and scan, or type the employee ID manually + + + )} + + {error && {error}} + + + {operators.map((operator) => ( + + + {operator.name} + {operator.employeeId} + + removeOperator(operator.id)} color="error"> + + + + ))} + {operators.length === 0 && ( + + + No operators added yet. Use the scan button to add operators. + + + )} + + + ); +}; + +export default OperatorScanner; \ No newline at end of file diff --git a/src/components/ProductionProcess/ProductionProcess.tsx b/src/components/ProductionProcess/ProductionProcess.tsx new file mode 100644 index 0000000..f5d22ca --- /dev/null +++ b/src/components/ProductionProcess/ProductionProcess.tsx @@ -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(false); + console.log("prodcution process"); + return ( + <> + + + setIsModalOpen(false)} + /> + + ); +}; + +export default ProductionProcess; \ No newline at end of file diff --git a/src/components/ProductionProcess/ProductionProcessLoading.tsx b/src/components/ProductionProcess/ProductionProcessLoading.tsx new file mode 100644 index 0000000..caef2c1 --- /dev/null +++ b/src/components/ProductionProcess/ProductionProcessLoading.tsx @@ -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 ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default ProductionProcessLoading; diff --git a/src/components/ProductionProcess/ProductionProcessWrapper.tsx b/src/components/ProductionProcess/ProductionProcessWrapper.tsx new file mode 100644 index 0000000..6d00afb --- /dev/null +++ b/src/components/ProductionProcess/ProductionProcessWrapper.tsx @@ -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 ; + }; + + ProductionProcessWrapper.Loading = ProductionProcessLoading; + + export default ProductionProcessWrapper; \ No newline at end of file diff --git a/src/components/ProductionProcess/ProductionRecordingModal.tsx b/src/components/ProductionProcess/ProductionRecordingModal.tsx new file mode 100644 index 0000000..28a9544 --- /dev/null +++ b/src/components/ProductionProcess/ProductionRecordingModal.tsx @@ -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 = ({ + isOpen, + onClose +}) => { + const { + control, + handleSubmit, + reset, + watch, + setValue, + setError, + clearErrors, + formState: { errors } + } = useForm({ + 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 ( + + + + Production Recording + + {watchedOperators?.length || 0} operator(s), {watchedMachines?.length || 0} machine(s), {getUsedMaterialsCount()} materials + + + + + + + + + {/* Basic Information */} + + ( + + )} + /> + + + {/* Operator Scanner */} + { + setValue('operators', operators); + if (operators.length > 0) { + clearErrors('operators'); + } + }} + error={errors.operators?.message} + /> + + {/* Machine Scanner */} + { + setValue('machines', machines); + if (machines.length > 0) { + clearErrors('machines'); + } + }} + error={errors.machines?.message} + /> + + {/* Material Lot Scanner */} + { + setValue('materials', materials); + clearErrors('materials'); + }} + error={errors.materials?.message} + /> + + {/* Additional Notes */} + ( + + )} + /> + + + + + + + + ); +}; + +export default ProductionRecordingModal; \ No newline at end of file diff --git a/src/components/ProductionProcess/index.ts b/src/components/ProductionProcess/index.ts new file mode 100644 index 0000000..c022da4 --- /dev/null +++ b/src/components/ProductionProcess/index.ts @@ -0,0 +1 @@ +export { default } from "./ProductionProcessWrapper"; \ No newline at end of file diff --git a/src/components/ProductionProcess/types.ts b/src/components/ProductionProcess/types.ts new file mode 100644 index 0000000..addfeb6 --- /dev/null +++ b/src/components/ProductionProcess/types.ts @@ -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; +} \ No newline at end of file