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