Przeglądaj źródła

Merge branch 'production_process'

master
MSI\2Fi 1 miesiąc temu
rodzic
commit
6f02dfda7e
13 zmienionych plików z 947 dodań i 118 usunięć
  1. +46
    -0
      src/app/api/jo/actions.ts
  2. +14
    -0
      src/app/api/jo/index.ts
  3. +158
    -0
      src/components/ProductionProcess/DefectsSection.tsx
  4. +26
    -15
      src/components/ProductionProcess/MachineScanner.tsx
  5. +14
    -20
      src/components/ProductionProcess/MaterialLotScanner.tsx
  6. +42
    -32
      src/components/ProductionProcess/OperatorScanner.tsx
  7. +172
    -20
      src/components/ProductionProcess/ProductionProcess.tsx
  8. +12
    -3
      src/components/ProductionProcess/ProductionProcessWrapper.tsx
  9. +9
    -17
      src/components/ProductionProcess/ProductionRecordingModal.tsx
  10. +289
    -0
      src/components/ProductionProcess/QualityCheckModal.tsx
  11. +74
    -0
      src/components/ProductionProcess/dummyData.ts
  12. +57
    -11
      src/components/ProductionProcess/types.ts
  13. +34
    -0
      src/components/ProductionProcess/utils/QualityCheckHelper.tsx

+ 46
- 0
src/app/api/jo/actions.ts Wyświetl plik

@@ -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
}

+ 14
- 0
src/app/api/jo/index.ts Wyświetl plik

@@ -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;
}

+ 158
- 0
src/components/ProductionProcess/DefectsSection.tsx Wyświetl plik

@@ -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;

+ 26
- 15
src/components/ProductionProcess/MachineScanner.tsx Wyświetl plik

@@ -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>
)}



+ 14
- 20
src/components/ProductionProcess/MaterialLotScanner.tsx Wyświetl plik

@@ -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.)');
}
}
}


+ 42
- 32
src/components/ProductionProcess/OperatorScanner.tsx Wyświetl plik

@@ -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 />


+ 172
- 20
src/components/ProductionProcess/ProductionProcess.tsx Wyświetl plik

@@ -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>
);
};


+ 12
- 3
src/components/ProductionProcess/ProductionProcessWrapper.tsx Wyświetl plik

@@ -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;


+ 9
- 17
src/components/ProductionProcess/ProductionRecordingModal.tsx Wyświetl plik

@@ -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: []


+ 289
- 0
src/components/ProductionProcess/QualityCheckModal.tsx Wyświetl plik

@@ -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;

+ 74
- 0
src/components/ProductionProcess/dummyData.ts Wyświetl plik

@@ -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' }
];

+ 57
- 11
src/components/ProductionProcess/types.ts Wyświetl plik

@@ -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;
}

+ 34
- 0
src/components/ProductionProcess/utils/QualityCheckHelper.tsx Wyświetl plik

@@ -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';
}
};

Ładowanie…
Anuluj
Zapisz