Autor | SHA1 | Nachricht | Datum |
---|---|---|---|
|
6f02dfda7e | Merge branch 'production_process' | vor 1 Monat |
|
ebc8d6f6d3 | add 2 modals to manage production process and qc | vor 2 Monaten |
@@ -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<T> { | |||
id: number | null; | |||
name: string; | |||
code: string; | |||
type?: string | |||
message: string | null; | |||
errorPosition: string | keyof T; | |||
entity: T | |||
} | |||
export interface isCorrectMachineUsedResponse<T> { | |||
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<IsOperatorExistResponse<Operator>>(`${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<isCorrectMachineUsedResponse<Machine>>(`${BASE_API_URL}/jop/isCorrectMachineUsed`, { | |||
method: "POST", | |||
body: JSON.stringify({ machineCode }), | |||
headers: { "Content-Type": "application/json" }, | |||
}); | |||
revalidateTag("po"); | |||
return isExist | |||
} |
@@ -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; | |||
} |
@@ -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<DefectsSectionProps> = ({ | |||
defects, | |||
onDefectsChange, | |||
error | |||
}) => { | |||
const [newDefect, setNewDefect] = useState<QualityCheckItem | null>(null); | |||
const [inputValue, setInputValue] = useState<string>(''); | |||
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<HTMLInputElement>): void => { | |||
if (e.key === 'Enter') { | |||
e.preventDefault(); | |||
handleAddDefect(); | |||
} | |||
}; | |||
return ( | |||
<Box> | |||
<Typography variant="subtitle1" fontWeight={600} mb={1}> | |||
Defects Found | |||
</Typography> | |||
<Stack direction="row" spacing={2} mb={2}> | |||
<Autocomplete | |||
value={newDefect} | |||
onChange={(event, value) => { | |||
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) => ( | |||
<Box component="li" {...props} key={option.id}> | |||
<Box> | |||
<Typography variant="body2" fontWeight={500}> | |||
{option.code} - {option.name} | |||
</Typography> | |||
<Typography variant="caption" color="text.secondary"> | |||
{option.description} | |||
</Typography> | |||
</Box> | |||
</Box> | |||
)} | |||
renderInput={(params) => ( | |||
<TextField | |||
{...params} | |||
size="small" | |||
label="Enter defect description" | |||
variant="outlined" | |||
onKeyDown={handleKeyPress} | |||
error={!!error} | |||
/> | |||
)} | |||
/> | |||
<Button | |||
variant="contained" | |||
color="primary" | |||
onClick={handleAddDefect} | |||
> | |||
Add | |||
</Button> | |||
</Stack> | |||
{error && ( | |||
<Typography variant="caption" color="error" sx={{ mb: 1, display: 'block' }}> | |||
{error} | |||
</Typography> | |||
)} | |||
{defects.length > 0 && ( | |||
<Stack spacing={1}> | |||
<Typography variant="body2" color="text.secondary" mb={1}> | |||
{defects.length} defect(s) found | |||
</Typography> | |||
{defects.map((defect: QualityCheckItem, index: number) => ( | |||
<Paper | |||
key={`${defect.id}-${index}`} | |||
sx={{ | |||
display: 'flex', | |||
alignItems: 'center', | |||
justifyContent: 'space-between', | |||
bgcolor: 'red.50', | |||
border: '1px solid', | |||
borderColor: 'red.200', | |||
p: 1.5 | |||
}} | |||
elevation={0} | |||
> | |||
<Box> | |||
<Typography color="error" sx={{ wordBreak: 'break-word', fontWeight: 500 }}> | |||
{defect.code && `${defect.code} - `}{defect.name} | |||
</Typography> | |||
{defect.description && ( | |||
<Typography variant="caption" color="error" sx={{ opacity: 0.7 }}> | |||
{defect.description} | |||
</Typography> | |||
)} | |||
</Box> | |||
<IconButton | |||
onClick={() => handleRemoveDefect(index)} | |||
color="error" | |||
size="small" | |||
> | |||
<X fontSize="small" /> | |||
</IconButton> | |||
</Paper> | |||
))} | |||
</Stack> | |||
)} | |||
</Box> | |||
); | |||
}; | |||
export default DefectsSection; |
@@ -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<MachineScannerProps> = ({ | |||
error | |||
}) => { | |||
const [scanningMode, setScanningMode] = useState<boolean>(false); | |||
const [scanError, setScanError] = useState<string | null>(null); | |||
const machineScanRef = useRef<HTMLInputElement>(null); | |||
const { t } = useTranslation() | |||
const startScanning = (): void => { | |||
setScanningMode(true); | |||
@@ -39,25 +44,27 @@ const MachineScanner: React.FC<MachineScannerProps> = ({ | |||
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 | |||
); | |||
const handleMachineScan = async (e: React.KeyboardEvent<HTMLInputElement>): Promise<void> => { | |||
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<MachineScannerProps> = ({ | |||
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> | |||
{scanError ? ( | |||
<Typography variant="body2" color="error" mt={1}>{scanError}</Typography> | |||
) : ( | |||
<Typography variant="body2" color="success.main" mt={1}> | |||
{t("Position the QR code scanner and scan, or type the machine code manually")} | |||
</Typography> | |||
)} | |||
</Paper> | |||
)} | |||
@@ -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<MaterialLotScannerProps> = ({ | |||
materials, | |||
@@ -42,17 +30,22 @@ const MaterialLotScanner: React.FC<MaterialLotScannerProps> = ({ | |||
setMaterialScanInput(e.target.value.trim()); | |||
}; | |||
const handleMaterialInputKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => { | |||
const handleMaterialInputKeyPress = async (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 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<MaterialLotScannerProps> = ({ | |||
}); | |||
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.)'); | |||
} | |||
} | |||
} | |||
@@ -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<OperatorScannerProps> = ({ | |||
operators, | |||
onOperatorsChange, | |||
error | |||
}) => { | |||
const [scanningMode, setScanningMode] = useState<boolean>(false); | |||
const [scanError, setScanError] = useState<string | null>(null); | |||
const operatorScanRef = useRef<HTMLInputElement>(null); | |||
const startScanning = (): void => { | |||
setScanningMode(true); | |||
setScanError(null); | |||
setTimeout(() => { | |||
if (operatorScanRef.current) { | |||
operatorScanRef.current.focus(); | |||
@@ -37,27 +34,35 @@ const OperatorScanner: React.FC<OperatorScannerProps> = ({ | |||
const stopScanning = (): void => { | |||
setScanningMode(false); | |||
setScanError(null); | |||
}; | |||
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); | |||
const handleOperatorScan = async (e: React.KeyboardEvent<HTMLInputElement>): Promise<void> => { | |||
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<OperatorScannerProps> = ({ | |||
<TextField | |||
inputRef={operatorScanRef} | |||
type="text" | |||
onKeyPress={handleOperatorScan} | |||
onKeyDown={handleOperatorScan} | |||
fullWidth | |||
label="Scan operator ID card or enter manually..." | |||
variant="outlined" | |||
size="small" | |||
sx={{ bgcolor: 'white' }} | |||
onInput={handleOperatorScan} | |||
/> | |||
<Button | |||
variant="contained" | |||
@@ -103,9 +109,13 @@ const OperatorScanner: React.FC<OperatorScannerProps> = ({ | |||
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> | |||
{scanError ? ( | |||
<Typography variant="body2" color="error" mt={1}>{scanError}</Typography> | |||
) : ( | |||
<Typography variant="body2" color="success.main" mt={1}> | |||
Position the ID card scanner and scan, or type the employee ID manually | |||
</Typography> | |||
)} | |||
</Paper> | |||
)} | |||
@@ -116,7 +126,7 @@ const OperatorScanner: React.FC<OperatorScannerProps> = ({ | |||
<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> | |||
<Typography variant="body2" color="primary.main">{operator.username}</Typography> | |||
</Box> | |||
<IconButton onClick={() => removeOperator(operator.id)} color="error"> | |||
<CloseIcon /> | |||
@@ -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<ProductionProcessProps> = ({ processes }) => { | |||
const [isModalOpen, setIsModalOpen] = React.useState<boolean>(false); | |||
const [selectedProcess, setSelectedProcess] = React.useState<ProcessData | null>(null); | |||
const [isQCModalOpen, setIsQCModalOpen] = React.useState<boolean>(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<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)} | |||
<Box sx={{ p: 3 }}> | |||
<Typography variant="h4" component="h1" gutterBottom sx={{ mb: 3 }}> | |||
Production Process Recording | |||
</Typography> | |||
<TableContainer component={Paper} sx={{ boxShadow: 2 }}> | |||
<Table sx={{ minWidth: 650 }}> | |||
<TableHead> | |||
<TableRow sx={{ bgcolor: 'grey.50' }}> | |||
<TableCell sx={{ fontWeight: 'bold' }}>Process ID</TableCell> | |||
<TableCell sx={{ fontWeight: 'bold' }}>Process Name</TableCell> | |||
<TableCell sx={{ fontWeight: 'bold' }}>Status</TableCell> | |||
<TableCell sx={{ fontWeight: 'bold' }}>Action</TableCell> | |||
</TableRow> | |||
</TableHead> | |||
<TableBody> | |||
{processes.map((process) => ( | |||
<TableRow | |||
key={process.id} | |||
sx={{ | |||
'&:last-child td, &:last-child th': { border: 0 }, | |||
'&:hover': { bgcolor: 'grey.50' } | |||
}} | |||
> | |||
<TableCell component="th" scope="row"> | |||
{process.id} | |||
</TableCell> | |||
<TableCell>{process.processName}</TableCell> | |||
<TableCell> | |||
<Chip | |||
label={process.status} | |||
color={getStatusColor(process.status)} | |||
size="small" | |||
/> | |||
</TableCell> | |||
<TableCell> | |||
<Button | |||
variant="contained" | |||
color="primary" | |||
size="small" | |||
onClick={() => handleOpenModal(process)} | |||
sx={{ | |||
fontWeight: 500, | |||
textTransform: 'none' | |||
}} | |||
> | |||
{process.status === 'Completed' ? 'View/Edit Data' : 'Record Data'} | |||
</Button> | |||
<Button | |||
variant="contained" | |||
color="primary" | |||
size="small" | |||
onClick={() => handleQCOpenModal(process)} | |||
sx={{ | |||
fontWeight: 500, | |||
textTransform: 'none' | |||
}} | |||
> | |||
{t("Quality Check")} | |||
</Button> | |||
</TableCell> | |||
</TableRow> | |||
))} | |||
</TableBody> | |||
</Table> | |||
</TableContainer> | |||
{selectedProcess && ( | |||
<ProductionRecordingModal | |||
isOpen={isModalOpen} | |||
onClose={handleCloseModal} | |||
jobProcess={selectedProcess} | |||
onDataRecorded={handleDataRecorded} | |||
/> | |||
)} | |||
{ | |||
<QualityCheckModal | |||
isOpen={isQCModalOpen} | |||
onClose={() => handleQCCloseModal()} | |||
item={sampleItem} | |||
/> | |||
</> | |||
} | |||
</Box> | |||
); | |||
}; | |||
@@ -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 <ProductionProcess />; | |||
const ProductionProcessWrapper: React.FC & SubComponents = async () => { | |||
return ( | |||
<> | |||
<h3>p</h3> | |||
<ProductionProcess | |||
processes={processData} | |||
/> | |||
</> | |||
) | |||
}; | |||
ProductionProcessWrapper.Loading = ProductionProcessLoading; | |||
@@ -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<ProductionRecordingModalProps> = ({ | |||
isOpen, | |||
onClose | |||
onClose, | |||
jobProcess | |||
}) => { | |||
const { | |||
control, | |||
@@ -45,7 +36,8 @@ const ProductionRecordingModal: React.FC<ProductionRecordingModalProps> = ({ | |||
notes: '', | |||
operators: [], | |||
machines: [], | |||
materials: initialMaterials | |||
materials: jobProcess.materials || [] | |||
} | |||
}); | |||
@@ -127,7 +119,7 @@ const ProductionRecordingModal: React.FC<ProductionRecordingModalProps> = ({ | |||
notes: '', | |||
operators: [], | |||
machines: [], | |||
materials: initialMaterials.map(material => ({ | |||
materials: jobProcess.materials.map(material => ({ | |||
...material, | |||
isUsed: false, | |||
lotNumbers: [] | |||
@@ -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<QualityCheckModalProps> = ({ | |||
isOpen, | |||
onClose, | |||
item = null | |||
}) => { | |||
const { | |||
control, | |||
handleSubmit, | |||
reset, | |||
watch, | |||
setValue, | |||
setError, | |||
clearErrors, | |||
formState: { errors, isValid } | |||
} = useForm<QualityCheckData>({ | |||
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 ( | |||
<Dialog open={isOpen} onClose={handleClose} maxWidth="md" fullWidth> | |||
<DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> | |||
<Box> | |||
<Typography variant="h5" fontWeight={700}>Quality Check</Typography> | |||
{item && ( | |||
<Typography variant="body2" color="text.secondary" mt={0.5}> | |||
Item: {item.name} (ID: {item.id}) | |||
</Typography> | |||
)} | |||
</Box> | |||
<IconButton onClick={handleClose} size="large"> | |||
<X /> | |||
</IconButton> | |||
</DialogTitle> | |||
<DialogContent dividers sx={{ p: 3 }}> | |||
<Stack spacing={4}> | |||
{/* Inspector and Date */} | |||
<Stack direction={{ xs: 'column', md: 'row' }} spacing={2}> | |||
<Box sx={{ flex: 1 }}> | |||
<Typography variant="body2" fontWeight={600} mb={1}> | |||
Inspector * | |||
</Typography> | |||
<OperatorScanner | |||
operators={watchedInspectors || []} | |||
onOperatorsChange={(operators) => { | |||
setValue('inspectors', operators); | |||
if (operators.length > 0) { | |||
clearErrors('inspectors'); | |||
} | |||
}} | |||
error={errors.inspectors?.message} | |||
/> | |||
</Box> | |||
</Stack> | |||
<Stack direction={{ xs: 'column', md: 'row' }} spacing={2}> | |||
<Box sx={{ flex: 1 }}> | |||
<Typography variant="body2" fontWeight={600} mb={1}> | |||
Check Date * | |||
</Typography> | |||
<Controller | |||
name="checkDate" | |||
control={control} | |||
rules={{ required: 'Check date is required' }} | |||
render={({ field }) => ( | |||
<input | |||
{...field} | |||
type="date" | |||
style={{ | |||
width: '100%', | |||
padding: '8px 12px', | |||
border: errors.checkDate ? '1px solid #f44336' : '1px solid #ccc', | |||
borderRadius: '4px', | |||
fontSize: '14px', | |||
outline: 'none' | |||
}} | |||
/> | |||
)} | |||
/> | |||
{errors.checkDate && ( | |||
<Typography variant="caption" color="error" sx={{ mt: 0.5 }}> | |||
{errors.checkDate.message} | |||
</Typography> | |||
)} | |||
</Box> | |||
</Stack> | |||
{/* Quality Status */} | |||
<Box> | |||
<Typography variant="body2" fontWeight={600} mb={2}> | |||
Quality Status * | |||
</Typography> | |||
<Controller | |||
name="status" | |||
control={control} | |||
rules={{ required: 'Please select a quality status' }} | |||
render={({ field }) => ( | |||
<Stack direction="row" spacing={2}> | |||
{statusOptions.map((statusOption) => { | |||
const IconComponent = getStatusIcon(statusOption); | |||
const isSelected = field.value === statusOption; | |||
return ( | |||
<Button | |||
key={statusOption} | |||
variant={isSelected ? "contained" : "outlined"} | |||
onClick={() => field.onChange(statusOption)} | |||
startIcon={ | |||
<IconComponent | |||
sx={{ | |||
fontSize: 20, | |||
color: statusOption === 'passed' ? 'success.main' : | |||
statusOption === 'failed' ? 'error.main' : 'warning.main' | |||
}} | |||
/> | |||
} | |||
sx={{ | |||
flex: 1, | |||
textTransform: 'capitalize', | |||
py: 1.5, | |||
backgroundColor: isSelected ? | |||
(statusOption === 'passed' ? 'success.main' : | |||
statusOption === 'failed' ? 'error.main' : 'warning.main') : | |||
'transparent', | |||
borderColor: statusOption === 'passed' ? 'success.main' : | |||
statusOption === 'failed' ? 'error.main' : 'warning.main', | |||
color: isSelected ? 'white' : | |||
(statusOption === 'passed' ? 'success.main' : | |||
statusOption === 'failed' ? 'error.main' : 'warning.main'), | |||
'&:hover': { | |||
backgroundColor: isSelected ? | |||
(statusOption === 'passed' ? 'success.dark' : | |||
statusOption === 'failed' ? 'error.dark' : 'warning.dark') : | |||
(statusOption === 'passed' ? 'success.light' : | |||
statusOption === 'failed' ? 'error.light' : 'warning.light') | |||
} | |||
}} | |||
> | |||
{statusOption} | |||
</Button> | |||
); | |||
})} | |||
</Stack> | |||
)} | |||
/> | |||
{errors.status && ( | |||
<Typography variant="caption" color="error" sx={{ mt: 0.5 }}> | |||
{errors.status.message} | |||
</Typography> | |||
)} | |||
</Box> | |||
{/* Defects Section */} | |||
<DefectsSection | |||
defects={watchedDefects || []} | |||
onDefectsChange={(defects) => { | |||
setValue('defects', defects); | |||
clearErrors('defects'); | |||
}} | |||
error={errors.defects?.message} | |||
/> | |||
{/* Additional Notes */} | |||
<Controller | |||
name="notes" | |||
control={control} | |||
render={({ field }) => ( | |||
<Box> | |||
<Typography variant="body2" fontWeight={600} mb={1}> | |||
Additional Notes | |||
</Typography> | |||
<textarea | |||
{...field} | |||
rows={4} | |||
style={{ | |||
width: '100%', | |||
padding: '12px', | |||
border: '1px solid #ccc', | |||
borderRadius: '4px', | |||
fontSize: '14px', | |||
fontFamily: 'inherit', | |||
outline: 'none', | |||
resize: 'vertical' | |||
}} | |||
placeholder="Enter any additional observations or notes..." | |||
/> | |||
</Box> | |||
)} | |||
/> | |||
</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 />} | |||
disabled={!isValid} | |||
> | |||
Save Quality Check | |||
</Button> | |||
</DialogActions> | |||
</Dialog> | |||
); | |||
}; | |||
export default QualityCheckModal; |
@@ -0,0 +1,74 @@ | |||
import { Operator } from "@/app/api/jo"; | |||
import { Material, MaterialDatabase, ProcessData, QualityCheckItem } from "./types"; | |||
export 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 operatorDatabase: Operator[] = [ | |||
{ id: 1, name: 'John Smith', username: 'EMP001', }, | |||
{ id: 2, name: 'Maria Garcia', username: 'EMP002',} , | |||
{ id: 3, name: 'David Chen', username: 'EMP003', }, | |||
{ id: 4, name: 'Sarah Johnson', username: 'EMP004', }, | |||
{ id: 5, name: 'Mike Wilson', username: 'EMP005', } | |||
]; | |||
const initialMaterial1: 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 }, | |||
]; | |||
const initialMaterial2: Material[] = [ | |||
{ 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 }, | |||
]; | |||
const initialMaterial3: Material[] = [ | |||
{ 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 } | |||
]; | |||
export const processData: ProcessData[] = [ | |||
{ id: '1', processName: 'Material Preparation', status: 'Not Started', materials: initialMaterial1 }, | |||
{ id: '2', processName: 'Assembly Line 1', status: 'Not Started', materials: initialMaterial2 }, | |||
{ id: '3', processName: 'Quality Control', status: 'Not Started', materials: initialMaterial3 } | |||
]; | |||
export const sampleItem: QualityCheckItem = { | |||
id: 1, | |||
code: 'QC-001', | |||
name: 'Steel Beam Section A', | |||
description: 'testing', | |||
category: 'Structural Steel' | |||
}; | |||
export const defectOptions: QualityCheckItem[] = [ | |||
{ id: 1, code: 'DEF001', name: 'Crack in wall', description: 'Visible cracks in wall surface' }, | |||
{ id: 2, code: 'DEF002', name: 'Paint peeling', description: 'Paint coming off surfaces' }, | |||
{ id: 3, code: 'DEF003', name: 'Loose tiles', description: 'Tiles not properly secured' }, | |||
{ id: 4, code: 'DEF004', name: 'Water damage', description: 'Damage caused by water exposure' }, | |||
{ id: 5, code: 'DEF005', name: 'Electrical issue', description: 'Problems with electrical systems' }, | |||
{ id: 6, code: 'DEF006', name: 'Plumbing leak', description: 'Water leaking from pipes or fixtures' }, | |||
{ id: 7, code: 'DEF007', name: 'Door not closing properly', description: 'Door alignment or hardware issues' }, | |||
{ id: 8, code: 'DEF008', name: 'Window seal broken', description: 'Damaged or missing window seals' }, | |||
{ id: 9, code: 'DEF009', name: 'Missing screws', description: 'Hardware missing from fixtures' }, | |||
{ id: 10, code: 'DEF010', name: 'Damaged flooring', description: 'Floor surface damage or wear' }, | |||
{ id: 11, code: 'DEF011', name: 'Ceiling stain', description: 'Discoloration on ceiling surface' }, | |||
{ id: 12, code: 'DEF012', name: 'Broken fixture', description: 'Non-functioning or damaged fixtures' }, | |||
{ id: 13, code: 'DEF013', name: 'Poor ventilation', description: 'Inadequate air circulation' }, | |||
{ id: 14, code: 'DEF014', name: 'Noise issue', description: 'Excessive or unwanted noise' }, | |||
{ id: 15, code: 'DEF015', name: 'Temperature control problem', description: 'HVAC or insulation issues' } | |||
]; |
@@ -1,15 +1,13 @@ | |||
export interface Operator { | |||
id: number; | |||
name: string; | |||
employeeId: string; | |||
cardId: string; | |||
import { Machine, Operator } from "@/app/api/jo"; | |||
export interface OperatorQrCode{ | |||
username: string | |||
} | |||
export interface Machine { | |||
id: number; | |||
name: string; | |||
code: string; | |||
qrCode: string; | |||
export interface MachineQrCode{ | |||
id?: string, | |||
code: string, | |||
name?: string, | |||
} | |||
export interface Material { | |||
@@ -46,4 +44,52 @@ export interface ValidationRule { | |||
export interface ValidationRules { | |||
[key: string]: ValidationRule; | |||
} | |||
} | |||
export 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[]; | |||
} | |||
/// QC related, will be moved to api level when fetching from backend | |||
export interface QualityCheckItem { | |||
id: number; | |||
code: string; | |||
name: string; | |||
description: string; | |||
category?: string; | |||
} | |||
export interface QualityCheckData { | |||
inspectors: Operator[]; | |||
checkDate: string; | |||
status: 'pending' | 'passed' | 'failed'; | |||
notes: string; | |||
defects: QualityCheckItem[]; | |||
} | |||
export interface QualityCheckRecord extends QualityCheckData { | |||
itemId?: string; | |||
itemName?: string; | |||
timestamp: string; | |||
} | |||
export interface QualityCheckModalProps { | |||
isOpen: boolean; | |||
onClose: () => void; | |||
item?: QualityCheckItem | null; | |||
} | |||
export interface QualityCheckModalFormProps { | |||
item?: QualityCheckItem | null; | |||
} | |||
export interface DefectsSectionProps { | |||
defects: QualityCheckItem[]; | |||
onDefectsChange: (defects: QualityCheckItem[]) => void; | |||
error?: string; | |||
} |
@@ -0,0 +1,34 @@ | |||
import { CheckCircle, HighlightOff, ErrorOutline } from '@mui/icons-material'; | |||
export const getStatusIcon = (status: 'pending' | 'passed' | 'failed') => { | |||
switch (status) { | |||
case 'passed': | |||
return CheckCircle; | |||
case 'failed': | |||
return HighlightOff; | |||
default: | |||
return ErrorOutline; | |||
} | |||
}; | |||
export const getStatusColor = (status: 'pending' | 'passed' | 'failed'): string => { | |||
switch (status) { | |||
case 'passed': | |||
return 'bg-green-100 text-green-800 border-green-300'; | |||
case 'failed': | |||
return 'bg-red-100 text-red-800 border-red-300'; | |||
default: | |||
return 'bg-yellow-100 text-yellow-800 border-yellow-300'; | |||
} | |||
}; | |||
export const getStatusColorSimple = (status: 'pending' | 'passed' | 'failed'): string => { | |||
switch (status) { | |||
case 'passed': | |||
return 'bg-green-100 text-green-800'; | |||
case 'failed': | |||
return 'bg-red-100 text-red-800'; | |||
default: | |||
return 'bg-yellow-100 text-yellow-800'; | |||
} | |||
}; |