3 次程式碼提交

共有 12 個文件被更改,包括 878 次插入6 次删除
分割檢視
  1. +2
    -1
      src/app/(main)/production/page.tsx
  2. +1
    -1
      src/components/NavigationContent/NavigationContent.tsx
  3. +4
    -4
      src/components/PoDetail/PoInputGrid.tsx
  4. +138
    -0
      src/components/ProductionProcess/MachineScanner.tsx
  5. +204
    -0
      src/components/ProductionProcess/MaterialLotScanner.tsx
  6. +138
    -0
      src/components/ProductionProcess/OperatorScanner.tsx
  7. +29
    -0
      src/components/ProductionProcess/ProductionProcess.tsx
  8. +40
    -0
      src/components/ProductionProcess/ProductionProcessLoading.tsx
  9. +15
    -0
      src/components/ProductionProcess/ProductionProcessWrapper.tsx
  10. +257
    -0
      src/components/ProductionProcess/ProductionRecordingModal.tsx
  11. +1
    -0
      src/components/ProductionProcess/index.ts
  12. +49
    -0
      src/components/ProductionProcess/types.ts

+ 2
- 1
src/app/(main)/production/page.tsx 查看文件

@@ -1,5 +1,6 @@
import { preloadClaims } from "@/app/api/claims";
import ClaimSearch from "@/components/ClaimSearch";
import ProductionProcess from "@/components/ProductionProcess";
import { getServerI18n } from "@/i18n";
import Add from "@mui/icons-material/Add";
import Button from "@mui/material/Button";
@@ -38,7 +39,7 @@ const production: React.FC = async () => {
</Button>
</Stack>
<Suspense fallback={<ClaimSearch.Loading />}>
<ClaimSearch />
<ProductionProcess />
</Suspense>
</>
);


+ 1
- 1
src/components/NavigationContent/NavigationContent.tsx 查看文件

@@ -89,7 +89,7 @@ const NavigationContent: React.FC = () => {
{
icon: <RequestQuote />,
label: "Job Order",
path: "",
path: "/production",
},
{
icon: <RequestQuote />,


+ 4
- 4
src/components/PoDetail/PoInputGrid.tsx 查看文件

@@ -485,10 +485,10 @@ function PoInputGrid({
// marginRight: 1,
}}
disabled={
// stockInLineStatusMap[status] === 9 ||
// stockInLineStatusMap[status] <= 2 ||
// stockInLineStatusMap[status] >= 7 ||
((stockInLineStatusMap[status] >= 3 && stockInLineStatusMap[status] <= 5) ? !session?.user?.abilities?.includes("APPROVAL"): false)
stockInLineStatusMap[status] === 9 ||
stockInLineStatusMap[status] <= 2 ||
stockInLineStatusMap[status] >= 7 ||
((stockInLineStatusMap[status] >= 3 && stockInLineStatusMap[status] <= 5) && !session?.user?.abilities?.includes("APPROVAL"))
}
// set _isNew to false after posting
// or check status


+ 138
- 0
src/components/ProductionProcess/MachineScanner.tsx 查看文件

@@ -0,0 +1,138 @@
"use client";
import React, { useState, useRef } from 'react';
import { Machine } from './types';
import { Button, TextField, Typography, Paper, Box, IconButton, Stack } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';

interface MachineScannerProps {
machines: Machine[];
onMachinesChange: (machines: Machine[]) => void;
error?: string;
}

const machineDatabase: Machine[] = [
{ id: 1, name: 'CNC Mill #1', code: 'CNC001', qrCode: 'QR-CNC001' },
{ id: 2, name: 'Lathe #2', code: 'LAT002', qrCode: 'QR-LAT002' },
{ id: 3, name: 'Press #3', code: 'PRS003', qrCode: 'QR-PRS003' },
{ id: 4, name: 'Welder #4', code: 'WLD004', qrCode: 'QR-WLD004' },
{ id: 5, name: 'Drill Press #5', code: 'DRL005', qrCode: 'QR-DRL005' }
];

const MachineScanner: React.FC<MachineScannerProps> = ({
machines,
onMachinesChange,
error
}) => {
const [scanningMode, setScanningMode] = useState<boolean>(false);
const machineScanRef = useRef<HTMLInputElement>(null);

const startScanning = (): void => {
setScanningMode(true);
setTimeout(() => {
if (machineScanRef.current) {
machineScanRef.current.focus();
}
}, 100);
};

const stopScanning = (): void => {
setScanningMode(false);
};

const handleMachineScan = (e: React.KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Enter') {
const target = e.target as HTMLInputElement;
const scannedCode = target.value.trim();
const machine = machineDatabase.find(m =>
m.qrCode === scannedCode || m.code === scannedCode
);
if (machine) {
const isAlreadyAdded = machines.some(m => m.id === machine.id);
if (!isAlreadyAdded) {
onMachinesChange([...machines, machine]);
}
target.value = '';
stopScanning();
} else {
alert('Machine not found. Please check the code and try again.');
target.value = '';
}
}
};

const removeMachine = (machineId: number): void => {
onMachinesChange(machines.filter(m => m.id !== machineId));
};

return (
<Box>
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={2}>
<Typography variant="h6" fontWeight={600}>
Machines *
</Typography>
<Button
variant={scanningMode ? 'contained' : 'outlined'}
color={scanningMode ? 'success' : 'primary'}
onClick={startScanning}
>
{scanningMode ? 'Scanning...' : 'Scan QR Code'}
</Button>
</Stack>

{scanningMode && (
<Paper elevation={2} sx={{ mb: 2, p: 2, bgcolor: 'green.50', border: '1px solid', borderColor: 'green.200' }}>
<Stack direction="row" alignItems="center" spacing={2}>
<TextField
inputRef={machineScanRef}
type="text"
onKeyPress={handleMachineScan}
fullWidth
label="Scan machine QR code or enter manually..."
variant="outlined"
size="small"
sx={{ bgcolor: 'white' }}
/>
<Button
variant="contained"
color="inherit"
onClick={stopScanning}
>
Cancel
</Button>
</Stack>
<Typography variant="body2" color="success.main" mt={1}>
Position the QR code scanner and scan, or type the machine code manually
</Typography>
</Paper>
)}

{error && <Typography color="error" variant="body2" mb={1}>{error}</Typography>}

<Stack spacing={1}>
{machines.map((machine) => (
<Paper key={machine.id} sx={{ p: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between', bgcolor: 'green.50', border: '1px solid', borderColor: 'green.200' }}>
<Box>
<Typography fontWeight={500} color="success.dark">{machine.name}</Typography>
<Typography variant="body2" color="success.main">{machine.code}</Typography>
</Box>
<IconButton onClick={() => removeMachine(machine.id)} color="error">
<CloseIcon />
</IconButton>
</Paper>
))}
{machines.length === 0 && (
<Paper sx={{ p: 2, bgcolor: 'grey.100', border: '1px solid', borderColor: 'grey.200' }}>
<Typography color="text.secondary" fontStyle="italic" variant="body2">
No machines added yet. Use the scan button to add machines.
</Typography>
</Paper>
)}
</Stack>
</Box>
);
};

export default MachineScanner;

+ 204
- 0
src/components/ProductionProcess/MaterialLotScanner.tsx 查看文件

@@ -0,0 +1,204 @@
"use client";
import React, { useState, useRef, useEffect } from 'react';
import { Material, MaterialDatabase } from './types';
import { Box, Typography, TextField, Paper, Stack, Button, IconButton, Chip } from '@mui/material';
import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from '@mui/icons-material/Close';

interface MaterialLotScannerProps {
materials: Material[];
onMaterialsChange: (materials: Material[]) => void;
error?: string;
}

const materialDatabase: MaterialDatabase[] = [
{ name: 'Steel Sheet', lotPattern: /^SS-\d{6}-\d{3}$/, required: true },
{ name: 'Aluminum Rod', lotPattern: /^AL-\d{6}-\d{3}$/, required: false },
{ name: 'Plastic Pellets', lotPattern: /^PP-\d{6}-\d{3}$/, required: false },
{ name: 'Copper Wire', lotPattern: /^CW-\d{6}-\d{3}$/, required: false },
{ name: 'Rubber Gasket', lotPattern: /^RG-\d{6}-\d{3}$/, required: false },
{ name: 'Glass Panel', lotPattern: /^GP-\d{6}-\d{3}$/, required: false },
{ name: 'Hydraulic Oil', lotPattern: /^HO-\d{6}-\d{3}$/, required: false },
{ name: 'Cutting Fluid', lotPattern: /^CF-\d{6}-\d{3}$/, required: false },
{ name: 'Welding Rod', lotPattern: /^WR-\d{6}-\d{3}$/, required: false },
{ name: 'Adhesive', lotPattern: /^AD-\d{6}-\d{3}$/, required: false }
];

const MaterialLotScanner: React.FC<MaterialLotScannerProps> = ({
materials,
onMaterialsChange,
error
}) => {
const [materialScanInput, setMaterialScanInput] = useState<string>('');
const materialScanRef = useRef<HTMLInputElement>(null);

useEffect(() => {
if (materialScanRef.current) {
materialScanRef.current.focus();
}
}, []);

const handleMaterialInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setMaterialScanInput(e.target.value.trim());
};

const handleMaterialInputKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
const target = e.target as HTMLInputElement;
const scannedLot = target.value.trim();
if (scannedLot) {
const matchedMaterial = materialDatabase.find(material =>
material.lotPattern.test(scannedLot)
);
if (matchedMaterial) {
const updatedMaterials = materials.map(material => {
if (material.name === matchedMaterial.name) {
const isAlreadyAdded = material.lotNumbers.includes(scannedLot);
if (!isAlreadyAdded) {
return {
...material,
isUsed: true,
lotNumbers: [...material.lotNumbers, scannedLot]
};
}
}
return material;
});
onMaterialsChange(updatedMaterials);
setMaterialScanInput('');
} else {
alert('Invalid lot number format or material not recognized.');
setMaterialScanInput('');
}
}
}
};

const removeLotNumber = (materialName: string, lotNumber: string): void => {
const updatedMaterials = materials.map(material => {
if (material.name === materialName) {
const updatedLotNumbers = material.lotNumbers.filter(lot => lot !== lotNumber);
return {
...material,
lotNumbers: updatedLotNumbers,
isUsed: updatedLotNumbers.length > 0
};
}
return material;
});
onMaterialsChange(updatedMaterials);
};

const requiredMaterials = materials.filter(m => m.required);
const optionalMaterials = materials.filter(m => !m.required);

return (
<Box>
<Typography variant="h6" fontWeight={600} mb={2}>
Material Lot Numbers
</Typography>
<Paper elevation={2} sx={{ mb: 2, p: 2, bgcolor: 'yellow.50', border: '1px solid', borderColor: 'yellow.200' }}>
<TextField
inputRef={materialScanRef}
type="text"
value={materialScanInput}
onChange={handleMaterialInputChange}
onKeyPress={handleMaterialInputKeyPress}
fullWidth
label="Scan or enter material lot number (e.g., SS-240616-001)"
variant="outlined"
size="small"
sx={{ bgcolor: 'white' }}
/>
<Box mt={2}>
<Typography variant="body2" color="warning.main">
<strong>Lot Number Formats:</strong>
</Typography>
<Typography variant="body2" color="warning.main">
Steel Sheet: SS-YYMMDD-XXX | Aluminum: AL-YYMMDD-XXX | Plastic: PP-YYMMDD-XXX
</Typography>
<Typography variant="body2" color="warning.main">
Copper Wire: CW-YYMMDD-XXX | Rubber: RG-YYMMDD-XXX | Glass: GP-YYMMDD-XXX
</Typography>
</Box>
</Paper>
{error && <Typography color="error" variant="body2" mb={2}>{error}</Typography>}
<Stack spacing={3}>
{/* Required Materials */}
<Box>
<Stack direction="row" alignItems="center" spacing={1} mb={1}>
<Typography variant="subtitle1" fontWeight={600} color="error.main">
Required Materials
</Typography>
<Chip label="Must scan lot numbers" color="error" size="small" />
</Stack>
<Stack spacing={1}>
{requiredMaterials.map((material) => (
<Paper key={material.id} sx={{ p: 2, bgcolor: 'red.50', border: '1px solid', borderColor: 'red.200' }}>
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={1}>
<Stack direction="row" alignItems="center" spacing={1}>
{material.isUsed && <CheckIcon fontSize="small" color="success" />}
<Typography fontWeight={500}>{material.name}</Typography>
</Stack>
<Typography variant="body2" color="text.secondary">
{material.lotNumbers.length} lot(s)
</Typography>
</Stack>
{material.lotNumbers.length > 0 && (
<Stack spacing={1}>
{material.lotNumbers.map((lotNumber, index) => (
<Paper key={index} sx={{ p: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between', bgcolor: 'white', border: '1px solid', borderColor: 'grey.200' }}>
<Typography variant="body2" fontFamily="monospace">{lotNumber}</Typography>
<IconButton onClick={() => removeLotNumber(material.name, lotNumber)} color="error" size="small">
<CloseIcon fontSize="small" />
</IconButton>
</Paper>
))}
</Stack>
)}
</Paper>
))}
</Stack>
</Box>
{/* Optional Materials */}
<Box>
<Stack direction="row" alignItems="center" spacing={1} mb={1}>
<Typography variant="subtitle1" fontWeight={600} color="primary.main">
Optional Materials
</Typography>
<Chip label="Lot numbers recommended" color="primary" size="small" />
</Stack>
<Stack spacing={1}>
{optionalMaterials.map((material) => (
<Paper key={material.id} sx={{ p: 2, bgcolor: 'blue.50', border: '1px solid', borderColor: 'blue.200' }}>
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={1}>
<Stack direction="row" alignItems="center" spacing={1}>
{material.isUsed && <CheckIcon fontSize="small" color="success" />}
<Typography fontWeight={500}>{material.name}</Typography>
</Stack>
<Typography variant="body2" color="text.secondary">
{material.lotNumbers.length} lot(s)
</Typography>
</Stack>
{material.lotNumbers.length > 0 && (
<Stack spacing={1}>
{material.lotNumbers.map((lotNumber, index) => (
<Paper key={index} sx={{ p: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between', bgcolor: 'white', border: '1px solid', borderColor: 'grey.200' }}>
<Typography variant="body2" fontFamily="monospace">{lotNumber}</Typography>
<IconButton onClick={() => removeLotNumber(material.name, lotNumber)} color="error" size="small">
<CloseIcon fontSize="small" />
</IconButton>
</Paper>
))}
</Stack>
)}
</Paper>
))}
</Stack>
</Box>
</Stack>
</Box>
);
};

export default MaterialLotScanner;

+ 138
- 0
src/components/ProductionProcess/OperatorScanner.tsx 查看文件

@@ -0,0 +1,138 @@
"use client";
import React, { useState, useRef } from 'react';
import { Operator } from './types';
import { Button, TextField, Typography, Paper, Box, IconButton, Stack } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';

interface OperatorScannerProps {
operators: Operator[];
onOperatorsChange: (operators: Operator[]) => void;
error?: string;
}

const operatorDatabase: Operator[] = [
{ id: 1, name: 'John Smith', employeeId: 'EMP001', cardId: '12345678' },
{ id: 2, name: 'Maria Garcia', employeeId: 'EMP002', cardId: '23456789' },
{ id: 3, name: 'David Chen', employeeId: 'EMP003', cardId: '34567890' },
{ id: 4, name: 'Sarah Johnson', employeeId: 'EMP004', cardId: '45678901' },
{ id: 5, name: 'Mike Wilson', employeeId: 'EMP005', cardId: '56789012' }
];

const OperatorScanner: React.FC<OperatorScannerProps> = ({
operators,
onOperatorsChange,
error
}) => {
const [scanningMode, setScanningMode] = useState<boolean>(false);
const operatorScanRef = useRef<HTMLInputElement>(null);

const startScanning = (): void => {
setScanningMode(true);
setTimeout(() => {
if (operatorScanRef.current) {
operatorScanRef.current.focus();
}
}, 100);
};

const stopScanning = (): void => {
setScanningMode(false);
};

const handleOperatorScan = (e: React.KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Enter') {
const target = e.target as HTMLInputElement;
const scannedId = target.value.trim();
const operator = operatorDatabase.find(op =>
op.cardId === scannedId || op.employeeId === scannedId
);
if (operator) {
const isAlreadyAdded = operators.some(op => op.id === operator.id);
if (!isAlreadyAdded) {
onOperatorsChange([...operators, operator]);
}
target.value = '';
stopScanning();
} else {
alert('Operator not found. Please check the ID and try again.');
target.value = '';
}
}
};

const removeOperator = (operatorId: number): void => {
onOperatorsChange(operators.filter(op => op.id !== operatorId));
};

return (
<Box>
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={2}>
<Typography variant="h6" fontWeight={600}>
Operators *
</Typography>
<Button
variant={scanningMode ? 'contained' : 'outlined'}
color={scanningMode ? 'success' : 'primary'}
onClick={startScanning}
>
{scanningMode ? 'Scanning...' : 'Scan ID Card'}
</Button>
</Stack>

{scanningMode && (
<Paper elevation={2} sx={{ mb: 2, p: 2, bgcolor: 'green.50', border: '1px solid', borderColor: 'green.200' }}>
<Stack direction="row" alignItems="center" spacing={2}>
<TextField
inputRef={operatorScanRef}
type="text"
onKeyPress={handleOperatorScan}
fullWidth
label="Scan operator ID card or enter manually..."
variant="outlined"
size="small"
sx={{ bgcolor: 'white' }}
/>
<Button
variant="contained"
color="inherit"
onClick={stopScanning}
>
Cancel
</Button>
</Stack>
<Typography variant="body2" color="success.main" mt={1}>
Position the ID card scanner and scan, or type the employee ID manually
</Typography>
</Paper>
)}

{error && <Typography color="error" variant="body2" mb={1}>{error}</Typography>}

<Stack spacing={1}>
{operators.map((operator) => (
<Paper key={operator.id} sx={{ p: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between', bgcolor: 'blue.50', border: '1px solid', borderColor: 'blue.200' }}>
<Box>
<Typography fontWeight={500} color="primary.dark">{operator.name}</Typography>
<Typography variant="body2" color="primary.main">{operator.employeeId}</Typography>
</Box>
<IconButton onClick={() => removeOperator(operator.id)} color="error">
<CloseIcon />
</IconButton>
</Paper>
))}
{operators.length === 0 && (
<Paper sx={{ p: 2, bgcolor: 'grey.100', border: '1px solid', borderColor: 'grey.200' }}>
<Typography color="text.secondary" fontStyle="italic" variant="body2">
No operators added yet. Use the scan button to add operators.
</Typography>
</Paper>
)}
</Stack>
</Box>
);
};

export default OperatorScanner;

+ 29
- 0
src/components/ProductionProcess/ProductionProcess.tsx 查看文件

@@ -0,0 +1,29 @@
"use client";
import React, { useState } from 'react';
import ProductionRecordingModal from './ProductionRecordingModal';
import { Box, Button } from '@mui/material';

const ProductionProcess: React.FC = () => {
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
console.log("prodcution process");
return (
<>
<Button
variant="contained"
color="primary"
size="large"
onClick={() => setIsModalOpen(true)}
sx={{ fontWeight: 500, borderRadius: 2, mb: 2 }}
>
Open Production Recording Modal
</Button>

<ProductionRecordingModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
/>
</>
);
};

export default ProductionProcess;

+ 40
- 0
src/components/ProductionProcess/ProductionProcessLoading.tsx 查看文件

@@ -0,0 +1,40 @@
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Skeleton from "@mui/material/Skeleton";
import Stack from "@mui/material/Stack";
import React from "react";

// Can make this nicer
export const ProductionProcessLoading: React.FC = () => {
return (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton
variant="rounded"
height={50}
width={100}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
</CardContent>
</Card>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
</Stack>
</CardContent>
</Card>
</>
);
};

export default ProductionProcessLoading;

+ 15
- 0
src/components/ProductionProcess/ProductionProcessWrapper.tsx 查看文件

@@ -0,0 +1,15 @@
import ProductionProcess from "./ProductionProcess";
import ProductionProcessLoading from "./ProductionProcessLoading";

interface SubComponents {
Loading: typeof ProductionProcessLoading;
}

const ProductionProcessWrapper: React.FC & SubComponents = () => {
console.log("Testing")
return <ProductionProcess />;
};

ProductionProcessWrapper.Loading = ProductionProcessLoading;
export default ProductionProcessWrapper;

+ 257
- 0
src/components/ProductionProcess/ProductionRecordingModal.tsx 查看文件

@@ -0,0 +1,257 @@
"use client";
import React from 'react';
import { X, Save } from '@mui/icons-material';
import { Dialog, DialogTitle, DialogContent, DialogActions, IconButton, Typography, TextField, Stack, Box, Button } from '@mui/material';
import { useForm, Controller } from 'react-hook-form';
import { Operator, Machine, Material, FormData, ProductionRecord } from './types';
import OperatorScanner from './OperatorScanner';
import MachineScanner from './MachineScanner';
import MaterialLotScanner from './MaterialLotScanner';

interface ProductionRecordingModalProps {
isOpen: boolean;
onClose: () => void;
}

const initialMaterials: Material[] = [
{ id: 1, name: 'Steel Sheet', isUsed: false, lotNumbers: [], required: true },
{ id: 2, name: 'Aluminum Rod', isUsed: false, lotNumbers: [], required: false },
{ id: 3, name: 'Plastic Pellets', isUsed: false, lotNumbers: [], required: false },
{ id: 4, name: 'Copper Wire', isUsed: false, lotNumbers: [], required: false },
{ id: 5, name: 'Rubber Gasket', isUsed: false, lotNumbers: [], required: false },
{ id: 6, name: 'Glass Panel', isUsed: false, lotNumbers: [], required: false },
{ id: 7, name: 'Hydraulic Oil', isUsed: false, lotNumbers: [], required: false },
{ id: 8, name: 'Cutting Fluid', isUsed: false, lotNumbers: [], required: false },
{ id: 9, name: 'Welding Rod', isUsed: false, lotNumbers: [], required: false },
{ id: 10, name: 'Adhesive', isUsed: false, lotNumbers: [], required: false }
];

const ProductionRecordingModal: React.FC<ProductionRecordingModalProps> = ({
isOpen,
onClose
}) => {
const {
control,
handleSubmit,
reset,
watch,
setValue,
setError,
clearErrors,
formState: { errors }
} = useForm<FormData>({
defaultValues: {
productionDate: new Date().toISOString().split('T')[0],
notes: '',
operators: [],
machines: [],
materials: initialMaterials
}
});

const watchedOperators = watch('operators');
const watchedMachines = watch('machines');
const watchedMaterials = watch('materials');

const validateForm = (): boolean => {
let isValid = true;

// Clear previous errors
clearErrors();

// Validate operators
if (!watchedOperators || watchedOperators.length === 0) {
setError('operators', {
type: 'required',
message: 'At least one operator is required'
});
isValid = false;
}

// Validate machines
if (!watchedMachines || watchedMachines.length === 0) {
setError('machines', {
type: 'required',
message: 'At least one machine is required'
});
isValid = false;
}

// Validate materials
console.log(watchedMaterials)
if (watchedMaterials) {
const missingLotNumbers = watchedMaterials
.filter(m => m.required && m.isUsed && (!m.lotNumbers || m.lotNumbers.length === 0))
.length;
if (missingLotNumbers > 0) {
setError('materials', {
type: 'custom',
message: `${missingLotNumbers} required material(s) missing lot numbers`
});
isValid = false;
}
}

return isValid;
};

const onSubmit = (data: FormData) => {
if (!validateForm()) {
return;
}

const usedMaterials = data.materials
.filter(material => material.isUsed)
.map(material => ({
id: material.id,
name: material.name,
lotNumbers: material.lotNumbers,
required: material.required,
isUsed: false
}));

const recordData: ProductionRecord = {
...data,
materials: usedMaterials,
timestamp: new Date().toISOString()
};

console.log('Production record saved:', recordData);
handleClose();
};

const handleClose = (): void => {
reset({
productionDate: new Date().toISOString().split('T')[0],
notes: '',
operators: [],
machines: [],
materials: initialMaterials.map(material => ({
...material,
isUsed: false,
lotNumbers: []
}))
});
onClose();
};

const getUsedMaterialsCount = (): number =>
watchedMaterials?.filter(m => m.isUsed).length || 0;

if (!isOpen) return null;

return (
<Dialog open={isOpen} onClose={handleClose} maxWidth="xl" fullWidth>
<DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box>
<Typography variant="h5" fontWeight={700}>Production Recording</Typography>
<Typography variant="body2" color="text.secondary" mt={0.5}>
{watchedOperators?.length || 0} operator(s), {watchedMachines?.length || 0} machine(s), {getUsedMaterialsCount()} materials
</Typography>
</Box>
<IconButton onClick={handleClose} size="large">
<X />
</IconButton>
</DialogTitle>
<DialogContent dividers sx={{ p: 3 }}>
<Stack spacing={4}>
{/* Basic Information */}
<Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>
<Controller
name="productionDate"
control={control}
rules={{
required: 'Production date is required'
}}
render={({ field }) => (
<TextField
{...field}
type="date"
label="Production Date"
InputLabelProps={{ shrink: true }}
fullWidth
size="small"
error={!!errors.productionDate}
helperText={errors.productionDate?.message}
/>
)}
/>
</Stack>

{/* Operator Scanner */}
<OperatorScanner
operators={watchedOperators || []}
onOperatorsChange={(operators) => {
setValue('operators', operators);
if (operators.length > 0) {
clearErrors('operators');
}
}}
error={errors.operators?.message}
/>

{/* Machine Scanner */}
<MachineScanner
machines={watchedMachines || []}
onMachinesChange={(machines) => {
setValue('machines', machines);
if (machines.length > 0) {
clearErrors('machines');
}
}}
error={errors.machines?.message}
/>

{/* Material Lot Scanner */}
<MaterialLotScanner
materials={watchedMaterials || []}
onMaterialsChange={(materials) => {
setValue('materials', materials);
clearErrors('materials');
}}
error={errors.materials?.message}
/>

{/* Additional Notes */}
<Controller
name="notes"
control={control}
render={({ field }) => (
<TextField
{...field}
label="Additional Notes"
multiline
minRows={3}
fullWidth
size="small"
placeholder="Enter any additional production notes..."
error={!!errors.notes}
helperText={errors.notes?.message}
/>
)}
/>
</Stack>
</DialogContent>
<DialogActions sx={{ p: 3 }}>
<Button
variant="outlined"
color="inherit"
onClick={handleClose}
>
Cancel
</Button>
<Button
variant="contained"
color="primary"
onClick={handleSubmit(onSubmit)}
startIcon={<Save />}
>
Save Record
</Button>
</DialogActions>
</Dialog>
);
};

export default ProductionRecordingModal;

+ 1
- 0
src/components/ProductionProcess/index.ts 查看文件

@@ -0,0 +1 @@
export { default } from "./ProductionProcessWrapper";

+ 49
- 0
src/components/ProductionProcess/types.ts 查看文件

@@ -0,0 +1,49 @@
export interface Operator {
id: number;
name: string;
employeeId: string;
cardId: string;
}

export interface Machine {
id: number;
name: string;
code: string;
qrCode: string;
}

export interface Material {
id: number;
name: string;
isUsed: boolean;
lotNumbers: string[];
required: boolean;
}

export interface MaterialDatabase {
name: string;
lotPattern: RegExp;
required: boolean;
}

export interface FormData {
productionDate: string;
notes: string;
operators: Operator[];
machines: Machine[];
materials: Material[];
}

export interface ProductionRecord extends FormData {
timestamp: string;
}

export interface ValidationRule {
required?: boolean;
message?: string;
custom?: (value: any, allValues?: any) => string | null;
}

export interface ValidationRules {
[key: string]: ValidationRule;
}

Loading…
取消
儲存