diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts new file mode 100644 index 0000000..85eda7a --- /dev/null +++ b/src/app/api/jo/actions.ts @@ -0,0 +1,46 @@ +'use server' +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { Machine, Operator } from "."; +import { BASE_API_URL } from "@/config/api"; +import { revalidateTag } from "next/cache"; + +export interface IsOperatorExistResponse { + id: number | null; + name: string; + code: string; + type?: string + message: string | null; + errorPosition: string | keyof T; + entity: T +} + +export interface isCorrectMachineUsedResponse { + id: number | null; + name: string; + code: string; + type?: string + message: string | null; + errorPosition: string | keyof T; + entity: T +} + + +export const isOperatorExist = async (username: string) => { + const isExist = await serverFetchJson>(`${BASE_API_URL}/jop/isOperatorExist`, { + method: "POST", + body: JSON.stringify({ username }), + headers: { "Content-Type": "application/json" }, + }); + revalidateTag("po"); + return isExist +} + +export const isCorrectMachineUsed = async (machineCode: string) => { + const isExist = await serverFetchJson>(`${BASE_API_URL}/jop/isCorrectMachineUsed`, { + method: "POST", + body: JSON.stringify({ machineCode }), + headers: { "Content-Type": "application/json" }, + }); + revalidateTag("po"); + return isExist +} \ No newline at end of file diff --git a/src/app/api/jo/index.ts b/src/app/api/jo/index.ts new file mode 100644 index 0000000..5633d65 --- /dev/null +++ b/src/app/api/jo/index.ts @@ -0,0 +1,14 @@ +'server=only' + +export interface Operator { + id: number; + name: string; + username: string; +} + +export interface Machine { + id: number; + name: string; + code: string; + qrCode: string; +} \ No newline at end of file diff --git a/src/components/ProductionProcess/DefectsSection.tsx b/src/components/ProductionProcess/DefectsSection.tsx new file mode 100644 index 0000000..6470e29 --- /dev/null +++ b/src/components/ProductionProcess/DefectsSection.tsx @@ -0,0 +1,158 @@ +import React, { useState } from 'react'; +import { X } from '@mui/icons-material'; +import { DefectsSectionProps, QualityCheckItem } from './types'; +import { Autocomplete, TextField, Button, Typography, Box, Stack, Paper, IconButton } from '@mui/material'; +import { defectOptions } from './dummyData'; + + +const DefectsSection: React.FC = ({ + defects, + onDefectsChange, + error +}) => { + const [newDefect, setNewDefect] = useState(null); + const [inputValue, setInputValue] = useState(''); + + const handleAddDefect = (): void => { + let defectToAdd: QualityCheckItem | null = null; + + if (newDefect) { + // If user selected from options + defectToAdd = { + id: newDefect.id, + code: newDefect.code, + name: newDefect.name, + description: newDefect.description + }; + } + + if (defectToAdd) { + // Check for duplicate code (skip if code is empty) + const isDuplicate = defectToAdd.code && defects.some(d => d.code === defectToAdd.code); + if (!isDuplicate) { + const updatedDefects = [...defects, defectToAdd]; + onDefectsChange(updatedDefects); + } + setNewDefect(null); + setInputValue(''); + } + }; + + const handleRemoveDefect = (index: number): void => { + const updatedDefects = defects.filter((_, i) => i !== index); + onDefectsChange(updatedDefects); + }; + + const handleKeyPress = (e: React.KeyboardEvent): void => { + if (e.key === 'Enter') { + e.preventDefault(); + handleAddDefect(); + } + }; + + return ( + + + Defects Found + + + { + if (typeof value === 'string') { + setNewDefect({ id: 0, code: '', name: value, description: '' }); + } else { + setNewDefect(value); + } + }} + inputValue={inputValue} + onInputChange={(event, newInputValue: string) => { + setInputValue(newInputValue); + }} + options={defectOptions} + getOptionLabel={(option) => typeof option === 'string' ? option : `${option.code} - ${option.name}`} + isOptionEqualToValue={(option, value) => option.id === value.id} + fullWidth + renderOption={(props, option) => ( + + + + {option.code} - {option.name} + + + {option.description} + + + + )} + renderInput={(params) => ( + + )} + /> + + + + {error && ( + + {error} + + )} + + {defects.length > 0 && ( + + + {defects.length} defect(s) found + + {defects.map((defect: QualityCheckItem, index: number) => ( + + + + {defect.code && `${defect.code} - `}{defect.name} + + {defect.description && ( + + {defect.description} + + )} + + handleRemoveDefect(index)} + color="error" + size="small" + > + + + + ))} + + )} + + ); +}; + +export default DefectsSection; \ No newline at end of file diff --git a/src/components/ProductionProcess/MachineScanner.tsx b/src/components/ProductionProcess/MachineScanner.tsx index 9b49d42..4316fae 100644 --- a/src/components/ProductionProcess/MachineScanner.tsx +++ b/src/components/ProductionProcess/MachineScanner.tsx @@ -1,8 +1,11 @@ "use client"; import React, { useState, useRef } from 'react'; -import { Machine } from './types'; +import { MachineQrCode } from './types'; import { Button, TextField, Typography, Paper, Box, IconButton, Stack } from '@mui/material'; import CloseIcon from '@mui/icons-material/Close'; +import { isCorrectMachineUsed } from '@/app/api/jo/actions'; +import { Machine } from '@/app/api/jo'; +import { useTranslation } from 'react-i18next'; interface MachineScannerProps { machines: Machine[]; @@ -24,7 +27,9 @@ const MachineScanner: React.FC = ({ error }) => { const [scanningMode, setScanningMode] = useState(false); + const [scanError, setScanError] = useState(null); const machineScanRef = useRef(null); + const { t } = useTranslation() const startScanning = (): void => { setScanningMode(true); @@ -39,25 +44,27 @@ const MachineScanner: React.FC = ({ 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 - ); + const handleMachineScan = async (e: React.KeyboardEvent): Promise => { + const target = e.target as HTMLInputElement; + const scannedCodeJSON = target.value.trim(); + + if (e.key === 'Enter' || scannedCodeJSON.endsWith('}') ) { + const scannedObj: MachineQrCode = JSON.parse(scannedCodeJSON) + + const response = await isCorrectMachineUsed(scannedObj?.code) - if (machine) { - const isAlreadyAdded = machines.some(m => m.id === machine.id); + if (response.message === "Success") { + const isAlreadyAdded = machines.some(m => m.code === response.entity.code); if (!isAlreadyAdded) { - onMachinesChange([...machines, machine]); + onMachinesChange([...machines, response.entity]); } target.value = ''; - stopScanning(); + // stopScanning(); } else { alert('Machine not found. Please check the code and try again.'); + setScanError('An error occurred while checking the operator. Please try again.'); target.value = ''; } } @@ -103,9 +110,13 @@ const MachineScanner: React.FC = ({ Cancel - - Position the QR code scanner and scan, or type the machine code manually - + {scanError ? ( + {scanError} + ) : ( + + {t("Position the QR code scanner and scan, or type the machine code manually")} + + )} )} diff --git a/src/components/ProductionProcess/MaterialLotScanner.tsx b/src/components/ProductionProcess/MaterialLotScanner.tsx index c8bbb9c..40c2693 100644 --- a/src/components/ProductionProcess/MaterialLotScanner.tsx +++ b/src/components/ProductionProcess/MaterialLotScanner.tsx @@ -11,18 +11,6 @@ interface MaterialLotScannerProps { 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, @@ -42,17 +30,22 @@ const MaterialLotScanner: React.FC = ({ setMaterialScanInput(e.target.value.trim()); }; - const handleMaterialInputKeyPress = (e: React.KeyboardEvent) => { + const handleMaterialInputKeyPress = async (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 response = await fetch('http://your-backend-url.com/validateLot', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ lot: scannedLot }) + }); + const data = await response.json(); + if (data.suggestedLot) { const updatedMaterials = materials.map(material => { - if (material.name === matchedMaterial.name) { + if (material.name === data.matchedMaterial.name) { const isAlreadyAdded = material.lotNumbers.includes(scannedLot); if (!isAlreadyAdded) { return { @@ -66,9 +59,10 @@ const MaterialLotScanner: React.FC = ({ }); onMaterialsChange(updatedMaterials); setMaterialScanInput(''); + } else if (data.matchedMaterial) { + setMaterialScanInput(scannedLot + ' (Invalid lot number format. Suggested lot number: ' + data.suggestedLot + ')'); } else { - alert('Invalid lot number format or material not recognized.'); - setMaterialScanInput(''); + setMaterialScanInput(scannedLot + ' (Invalid lot number format or material not recognized.)'); } } } diff --git a/src/components/ProductionProcess/OperatorScanner.tsx b/src/components/ProductionProcess/OperatorScanner.tsx index ce521f5..5894067 100644 --- a/src/components/ProductionProcess/OperatorScanner.tsx +++ b/src/components/ProductionProcess/OperatorScanner.tsx @@ -1,8 +1,10 @@ "use client"; -import React, { useState, useRef } from 'react'; -import { Operator } from './types'; +import React, { useState, useRef, useEffect } from 'react'; +import { Operator } from "@/app/api/jo"; import { Button, TextField, Typography, Paper, Box, IconButton, Stack } from '@mui/material'; import CloseIcon from '@mui/icons-material/Close'; +import { isOperatorExist } from '@/app/api/jo/actions'; +import { OperatorQrCode } from './types'; interface OperatorScannerProps { operators: Operator[]; @@ -10,24 +12,19 @@ interface OperatorScannerProps { 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 [scanError, setScanError] = useState(null); const operatorScanRef = useRef(null); + const startScanning = (): void => { setScanningMode(true); + setScanError(null); setTimeout(() => { if (operatorScanRef.current) { operatorScanRef.current.focus(); @@ -37,27 +34,35 @@ const OperatorScanner: React.FC = ({ const stopScanning = (): void => { setScanningMode(false); + setScanError(null); }; - 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); + const handleOperatorScan = async (e: React.KeyboardEvent): Promise => { + const target = e.target as HTMLInputElement; + const usernameJSON: string = target.value.trim(); + + if (e.key === 'Enter' || usernameJSON.endsWith('}') ) { + console.log(usernameJSON) + try { + const usernameObj: OperatorQrCode = JSON.parse(usernameJSON) + + const response = await isOperatorExist(usernameObj.username) - if (!isAlreadyAdded) { - onOperatorsChange([...operators, operator]); + if (response.message === "Success") { + const isAlreadyAdded = operators.some(op => op.username === response.entity.username); + if (!isAlreadyAdded) { + onOperatorsChange([...operators, response.entity]); + } + target.value = ''; + setScanError(null); + // stopScanning(); + } else { + setScanError('Operator not found. Please check the ID and try again.'); + target.value = ''; } - - target.value = ''; - stopScanning(); - } else { - alert('Operator not found. Please check the ID and try again.'); + } catch (error) { + console.error('Error checking operator:', error); + setScanError('An error occurred while checking the operator. Please try again.'); target.value = ''; } } @@ -88,12 +93,13 @@ const OperatorScanner: React.FC = ({ - - Position the ID card scanner and scan, or type the employee ID manually - + {scanError ? ( + {scanError} + ) : ( + + Position the ID card scanner and scan, or type the employee ID manually + + )} )} @@ -116,7 +126,7 @@ const OperatorScanner: React.FC = ({ {operator.name} - {operator.employeeId} + {operator.username} removeOperator(operator.id)} color="error"> diff --git a/src/components/ProductionProcess/ProductionProcess.tsx b/src/components/ProductionProcess/ProductionProcess.tsx index f5d22ca..0b3c86e 100644 --- a/src/components/ProductionProcess/ProductionProcess.tsx +++ b/src/components/ProductionProcess/ProductionProcess.tsx @@ -1,28 +1,180 @@ "use client"; -import React, { useState } from 'react'; +import React, { useTransition } from 'react'; import ProductionRecordingModal from './ProductionRecordingModal'; -import { Box, Button } from '@mui/material'; +import { + Box, + Button, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Typography, + Chip +} from '@mui/material'; +import { Material } from './types'; +import { useTranslation } from 'react-i18next'; +import QualityCheckModal from './QualityCheckModal'; +import { sampleItem } from './dummyData'; + +interface ProcessData { + id: string; + processName: string; + status: 'Not Started' | 'In Progress' | 'Completed'; + recordedData?: any; // You can type this based on your actual data structure + materials: Material[] +} + +interface ProductionProcessProps { + processes: ProcessData[]; +} + +interface ProductionRecordingModalProps { + isOpen: boolean; + onClose: () => void; + process?: ProcessData | null; + onDataRecorded?: (processId: string, data: any) => void; +} + +const ProductionProcess: React.FC = ({ processes }) => { + const [isModalOpen, setIsModalOpen] = React.useState(false); + const [selectedProcess, setSelectedProcess] = React.useState(null); + + const [isQCModalOpen, setIsQCModalOpen] = React.useState(false); + + const { t } = useTranslation() + + console.log("production process"); + + const handleOpenModal = (process: ProcessData) => { + setSelectedProcess(process); + setIsModalOpen(true); + }; + + const handleCloseModal = () => { + setIsModalOpen(false); + setSelectedProcess(null); + }; + + const handleQCOpenModal = (process: ProcessData) => { + setSelectedProcess(process); + setIsQCModalOpen(true); + } + + const handleQCCloseModal = () => { + setIsQCModalOpen(false); + setSelectedProcess(null); + }; + + const handleDataRecorded = (processId: string, data: any) => { + const updated = processes.map(process => + process.id === processId + ? { ...process, status: 'Completed', recordedData: data } + : process + ); + console.log(updated) + }; + + const getStatusColor = (status: ProcessData['status']) => { + switch (status) { + case 'Not Started': + return 'default'; + case 'In Progress': + return 'warning'; + case 'Completed': + return 'success'; + default: + return 'default'; + } + }; -const ProductionProcess: React.FC = () => { - const [isModalOpen, setIsModalOpen] = useState(false); - console.log("prodcution process"); return ( - <> - - - setIsModalOpen(false)} + + + Production Process Recording + + + + + + + Process ID + Process Name + Status + Action + + + + {processes.map((process) => ( + + + {process.id} + + {process.processName} + + + + + + + + + ))} + +
+
+ + {selectedProcess && ( + + )} + + { + handleQCCloseModal()} + item={sampleItem} /> - + } +
); }; diff --git a/src/components/ProductionProcess/ProductionProcessWrapper.tsx b/src/components/ProductionProcess/ProductionProcessWrapper.tsx index 6d00afb..1809722 100644 --- a/src/components/ProductionProcess/ProductionProcessWrapper.tsx +++ b/src/components/ProductionProcess/ProductionProcessWrapper.tsx @@ -1,3 +1,4 @@ +import { processData } from "./dummyData"; import ProductionProcess from "./ProductionProcess"; import ProductionProcessLoading from "./ProductionProcessLoading"; @@ -5,9 +6,17 @@ interface SubComponents { Loading: typeof ProductionProcessLoading; } -const ProductionProcessWrapper: React.FC & SubComponents = () => { - console.log("Testing") - return ; +const ProductionProcessWrapper: React.FC & SubComponents = async () => { + + return ( + <> +

p

+ + + ) + }; ProductionProcessWrapper.Loading = ProductionProcessLoading; diff --git a/src/components/ProductionProcess/ProductionRecordingModal.tsx b/src/components/ProductionProcess/ProductionRecordingModal.tsx index 28a9544..672de69 100644 --- a/src/components/ProductionProcess/ProductionRecordingModal.tsx +++ b/src/components/ProductionProcess/ProductionRecordingModal.tsx @@ -3,7 +3,8 @@ 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 { Material, FormData, ProductionRecord, ProcessData } from './types'; +import { Operator, Machine } from "@/app/api/jo"; import OperatorScanner from './OperatorScanner'; import MachineScanner from './MachineScanner'; import MaterialLotScanner from './MaterialLotScanner'; @@ -11,24 +12,14 @@ import MaterialLotScanner from './MaterialLotScanner'; interface ProductionRecordingModalProps { isOpen: boolean; onClose: () => void; + jobProcess: ProcessData; + onDataRecorded?: (processId: string, data: any) => 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 + onClose, + jobProcess }) => { const { control, @@ -45,7 +36,8 @@ const ProductionRecordingModal: React.FC = ({ notes: '', operators: [], machines: [], - materials: initialMaterials + materials: jobProcess.materials || [] + } }); @@ -127,7 +119,7 @@ const ProductionRecordingModal: React.FC = ({ notes: '', operators: [], machines: [], - materials: initialMaterials.map(material => ({ + materials: jobProcess.materials.map(material => ({ ...material, isUsed: false, lotNumbers: [] diff --git a/src/components/ProductionProcess/QualityCheckModal.tsx b/src/components/ProductionProcess/QualityCheckModal.tsx new file mode 100644 index 0000000..9dc917e --- /dev/null +++ b/src/components/ProductionProcess/QualityCheckModal.tsx @@ -0,0 +1,289 @@ +"use client"; +import React from 'react'; +import { X, Save } from '@mui/icons-material'; +import { Dialog, DialogTitle, DialogContent, DialogActions, IconButton, Typography, Button, Box, Stack } from '@mui/material'; +import { useForm, Controller } from 'react-hook-form'; +import { QualityCheckModalProps, QualityCheckData, QualityCheckRecord, QualityCheckItem } from './types'; +import { getStatusIcon, getStatusColor } from './utils/QualityCheckHelper'; +import DefectsSection from './DefectsSection'; +import OperatorScanner from './OperatorScanner'; + +const QualityCheckModal: React.FC = ({ + isOpen, + onClose, + item = null +}) => { + const { + control, + handleSubmit, + reset, + watch, + setValue, + setError, + clearErrors, + formState: { errors, isValid } + } = useForm({ + defaultValues: { + inspectors: [], + checkDate: new Date().toISOString().split('T')[0], + status: 'pending', + notes: '', + defects: [] + }, + mode: 'onChange' + }); + + const watchedDefects = watch('defects'); + const watchedInspectors = watch('inspectors'); + + const validateForm = (): boolean => { + let isValid = true; + + // Clear previous errors + clearErrors(); + + // Validate inspectors + if (!watchedInspectors || watchedInspectors.length === 0) { + setError('inspectors', { + type: 'required', + message: 'At least one inspector is required' + }); + isValid = false; + } + + return isValid; + }; + + + const onSubmit = (data: QualityCheckData): void => { + if (!validateForm()) { + return; + } + + console.log(data); + const qualityRecord: QualityCheckRecord = { + ...data, + itemId: item?.id?.toString(), + itemName: item?.name, + timestamp: new Date().toISOString() + }; + + // Save to localStorage or your preferred storage method + // const existingRecords = JSON.parse(localStorage.getItem('qualityCheckRecords') || '[]'); + // const updatedRecords = [...existingRecords, qualityRecord]; + // localStorage.setItem('qualityCheckRecords', JSON.stringify(updatedRecords)); + + + // Close modal and reset form + handleClose(); + }; + + const handleClose = (): void => { + reset({ + inspectors: [], + checkDate: new Date().toISOString().split('T')[0], + status: 'pending', + notes: '', + defects: [] + }); + onClose(); + }; + + const statusOptions: Array<'pending' | 'passed' | 'failed'> = ['pending', 'passed', 'failed']; + + if (!isOpen) return null; + + return ( + + + + Quality Check + {item && ( + + Item: {item.name} (ID: {item.id}) + + )} + + + + + + + + {/* Inspector and Date */} + + + + Inspector * + + { + setValue('inspectors', operators); + if (operators.length > 0) { + clearErrors('inspectors'); + } + }} + error={errors.inspectors?.message} + /> + + + + + + Check Date * + + ( + + )} + /> + {errors.checkDate && ( + + {errors.checkDate.message} + + )} + + + + {/* Quality Status */} + + + Quality Status * + + ( + + {statusOptions.map((statusOption) => { + const IconComponent = getStatusIcon(statusOption); + const isSelected = field.value === statusOption; + + return ( + + ); + })} + + )} + /> + {errors.status && ( + + {errors.status.message} + + )} + + + {/* Defects Section */} + { + setValue('defects', defects); + clearErrors('defects'); + }} + error={errors.defects?.message} + /> + + {/* Additional Notes */} + ( + + + Additional Notes + +