added update to comments with characters counter
This commit is contained in:
parent
0becdf9337
commit
a9b8925be8
@ -3,7 +3,7 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from app.routers import address, contact, proposal, dewar, shipment
|
from app.routers import address, contact, proposal, dewar, shipment, upload
|
||||||
from app.database import Base, engine, SessionLocal, load_sample_data
|
from app.database import Base, engine, SessionLocal, load_sample_data
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
@ -35,6 +35,7 @@ app.include_router(address.router, prefix="/addresses", tags=["addresses"])
|
|||||||
app.include_router(proposal.router, prefix="/proposals", tags=["proposals"])
|
app.include_router(proposal.router, prefix="/proposals", tags=["proposals"])
|
||||||
app.include_router(dewar.router, prefix="/dewars", tags=["dewars"])
|
app.include_router(dewar.router, prefix="/dewars", tags=["dewars"])
|
||||||
app.include_router(shipment.router, prefix="/shipments", tags=["shipments"])
|
app.include_router(shipment.router, prefix="/shipments", tags=["shipments"])
|
||||||
|
app.include_router(upload.router, tags=["upload"]) # Removed the trailing '/' from the prefix
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
35
backend/app/routers/upload.py
Normal file
35
backend/app/routers/upload.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# app/routers/upload.py
|
||||||
|
|
||||||
|
from fastapi import APIRouter, UploadFile, File, HTTPException
|
||||||
|
import os
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/upload")
|
||||||
|
async def upload_file(file: UploadFile = File(...)):
|
||||||
|
if not file.filename.endswith('.xlsx'):
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid file format. Please upload an .xlsx file.")
|
||||||
|
|
||||||
|
save_path = os.path.join("uploads", file.filename)
|
||||||
|
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
||||||
|
with open(save_path, "wb") as buffer:
|
||||||
|
buffer.write(await file.read())
|
||||||
|
|
||||||
|
# Validate the file (add your validation logic here)
|
||||||
|
is_valid, summary, error = validate_file(save_path)
|
||||||
|
if not is_valid:
|
||||||
|
raise HTTPException(status_code=400, detail=error)
|
||||||
|
|
||||||
|
return summary
|
||||||
|
|
||||||
|
|
||||||
|
def validate_file(file_path: str):
|
||||||
|
# Implement your file validation logic here
|
||||||
|
# For demo purpose, assuming it always succeeds
|
||||||
|
summary = {
|
||||||
|
"dewars": 5,
|
||||||
|
"pucks": 10,
|
||||||
|
"samples": 100,
|
||||||
|
}
|
||||||
|
return True, summary, None
|
@ -12,8 +12,9 @@ import QRCode from 'react-qr-code';
|
|||||||
import {
|
import {
|
||||||
ContactPerson,
|
ContactPerson,
|
||||||
Address,
|
Address,
|
||||||
Dewar, ContactsService, AddressesService, ShipmentsService, DewarsService,
|
Dewar, ContactsService, AddressesService, ShipmentsService,
|
||||||
} from '../../openapi';
|
} from '../../openapi';
|
||||||
|
import Unipuck from '../components/Unipuck';
|
||||||
|
|
||||||
interface DewarDetailsProps {
|
interface DewarDetailsProps {
|
||||||
dewar: Dewar;
|
dewar: Dewar;
|
||||||
@ -252,10 +253,10 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
|
|||||||
shipment_date: existingShipment.shipment_date,
|
shipment_date: existingShipment.shipment_date,
|
||||||
shipment_status: existingShipment.shipment_status,
|
shipment_status: existingShipment.shipment_status,
|
||||||
comments: existingShipment.comments,
|
comments: existingShipment.comments,
|
||||||
contact_person_id: existingShipment.contact_person.id, // Keep main shipment contact person
|
contact_person_id: existingShipment.contact_person.id,
|
||||||
return_address_id: selectedReturnAddress,
|
return_address_id: selectedReturnAddress,
|
||||||
proposal_id: existingShipment.proposal?.id,
|
proposal_id: existingShipment.proposal?.id,
|
||||||
dewars: [updatedDewar], // Updated dewars array
|
dewars: [updatedDewar],
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Payload for update:', JSON.stringify(payload, null, 2));
|
console.log('Payload for update:', JSON.stringify(payload, null, 2));
|
||||||
@ -292,18 +293,19 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
|
|||||||
sx={{ width: '300px', marginBottom: 2 }}
|
sx={{ width: '300px', marginBottom: 2 }}
|
||||||
/>
|
/>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', marginBottom: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', marginBottom: 2 }}>
|
||||||
<Box sx={{ width: 80, height: 80, backgroundColor: '#e0e0e0', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column' }}>
|
||||||
{dewar.qrcode ? (
|
{dewar.qrcode ? (
|
||||||
<QRCode value={dewar.qrcode} size={70} />
|
<QRCode value={dewar.qrcode} size={70} />
|
||||||
) : (
|
) : (
|
||||||
<Typography>No QR code available</Typography>
|
<Typography>No QR code available</Typography>
|
||||||
)}
|
)}
|
||||||
|
<Button variant="contained" sx={{ marginTop: 1 }} onClick={() => { /** Add logic to generate QR Code */ }}>
|
||||||
|
Generate QR Code
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
<Button variant="contained" onClick={() => { /** Add logic to generate QR Code */ }}>
|
|
||||||
Generate QR Code
|
|
||||||
</Button>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Typography variant="body1">Number of Pucks: {dewar.number_of_pucks}</Typography>
|
<Typography variant="body1">Number of Pucks: {dewar.number_of_pucks}</Typography>
|
||||||
|
<Unipuck pucks={dewar.number_of_pucks ?? 0} />
|
||||||
<Typography variant="body1">Number of Samples: {dewar.number_of_samples}</Typography>
|
<Typography variant="body1">Number of Samples: {dewar.number_of_samples}</Typography>
|
||||||
<Typography variant="body1">Current Contact Person:</Typography>
|
<Typography variant="body1">Current Contact Person:</Typography>
|
||||||
<Select
|
<Select
|
||||||
|
@ -23,14 +23,11 @@ interface ShipmentDetailsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
|
const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
|
||||||
isCreatingShipment,
|
|
||||||
sx,
|
sx,
|
||||||
selectedShipment,
|
selectedShipment,
|
||||||
selectedDewar,
|
|
||||||
setSelectedDewar,
|
setSelectedDewar,
|
||||||
setSelectedShipment,
|
setSelectedShipment,
|
||||||
refreshShipments,
|
refreshShipments,
|
||||||
defaultContactPerson
|
|
||||||
}) => {
|
}) => {
|
||||||
const [localSelectedDewar, setLocalSelectedDewar] = useState<Dewar | null>(null);
|
const [localSelectedDewar, setLocalSelectedDewar] = useState<Dewar | null>(null);
|
||||||
const [isAddingDewar, setIsAddingDewar] = useState<boolean>(false);
|
const [isAddingDewar, setIsAddingDewar] = useState<boolean>(false);
|
||||||
@ -200,146 +197,152 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{
|
{selectedShipment ? (
|
||||||
selectedShipment
|
<Grid container spacing={2}>
|
||||||
? (
|
<Grid item xs={12} md={6}>
|
||||||
<Grid container spacing={2}>
|
<Box sx={{ marginTop: 2, marginBottom: 2 }}>
|
||||||
<Grid item xs={12} md={6}>
|
<Typography variant="h5">{selectedShipment.shipment_name}</Typography>
|
||||||
<Box sx={{ marginTop: 2, marginBottom: 2 }}>
|
<Typography variant="body1" color="textSecondary">
|
||||||
<Typography variant="h5">{selectedShipment.shipment_name}</Typography>
|
Main contact person: {contactPerson ? `${contactPerson.firstname} ${contactPerson.lastname}` : 'N/A'}
|
||||||
<Typography variant="body1" color="textSecondary">
|
</Typography>
|
||||||
Main contact person: {contactPerson ? `${contactPerson.firstname} ${contactPerson.lastname}` : 'N/A'}
|
<Typography variant="body1">Number of Pucks: {totalPucks}</Typography>
|
||||||
</Typography>
|
<Typography variant="body1">Number of Samples: {totalSamples}</Typography>
|
||||||
<Typography variant="body1">Number of Pucks: {totalPucks}</Typography>
|
<Typography variant="body1">Shipment Date: {selectedShipment.shipment_date}</Typography>
|
||||||
<Typography variant="body1">Number of Samples: {totalSamples}</Typography>
|
</Box>
|
||||||
<Typography variant="body1">Shipment Date: {selectedShipment.shipment_date}</Typography>
|
</Grid>
|
||||||
</Box>
|
<Grid item xs={12} md={6}>
|
||||||
</Grid>
|
<Box sx={{ position: 'relative' }}>
|
||||||
<Grid item xs={12} md={6}>
|
<TextField
|
||||||
<Box sx={{ position: 'relative' }}>
|
label="Comments"
|
||||||
<TextField
|
fullWidth
|
||||||
label="Comments"
|
multiline
|
||||||
fullWidth
|
rows={4}
|
||||||
multiline
|
value={comments}
|
||||||
rows={4}
|
onChange={(e) => setComments(e.target.value)}
|
||||||
value={comments}
|
sx={{
|
||||||
onChange={(e) => setComments(e.target.value)}
|
marginBottom: 2,
|
||||||
sx={{
|
'& .MuiInputBase-root': {
|
||||||
marginBottom: 2,
|
color: isCommentsEdited ? 'inherit' : 'rgba(0, 0, 0, 0.6)',
|
||||||
'& .MuiInputBase-root': {
|
},
|
||||||
color: isCommentsEdited ? 'inherit' : 'rgba(0, 0, 0, 0.6)',
|
}}
|
||||||
},
|
helperText={`${MAX_COMMENTS_LENGTH - comments.length} characters remaining`}
|
||||||
}}
|
error={comments.length > MAX_COMMENTS_LENGTH}
|
||||||
helperText={`${MAX_COMMENTS_LENGTH - comments.length} characters remaining`}
|
/>
|
||||||
error={comments.length > MAX_COMMENTS_LENGTH}
|
<Box sx={{ position: 'absolute', bottom: 8, right: 8, display: 'flex', gap: 1 }}>
|
||||||
/>
|
<IconButton
|
||||||
<Box sx={{ position: 'absolute', bottom: 8, right: 8, display: 'flex', gap: 1 }}>
|
color="primary"
|
||||||
<IconButton
|
onClick={handleSaveComments}
|
||||||
color="primary"
|
disabled={comments.length > MAX_COMMENTS_LENGTH}
|
||||||
onClick={handleSaveComments}
|
>
|
||||||
disabled={comments.length > MAX_COMMENTS_LENGTH}
|
<CheckIcon />
|
||||||
>
|
</IconButton>
|
||||||
<CheckIcon />
|
<IconButton color="secondary" onClick={handleCancelEdit}>
|
||||||
</IconButton>
|
<CloseIcon />
|
||||||
<IconButton color="secondary" onClick={handleCancelEdit}>
|
</IconButton>
|
||||||
<CloseIcon />
|
</Box>
|
||||||
</IconButton>
|
</Box>
|
||||||
</Box>
|
</Grid>
|
||||||
</Box>
|
</Grid>
|
||||||
</Grid>
|
) : (
|
||||||
</Grid>
|
<Typography variant="h5" color="error">No shipment selected</Typography>
|
||||||
)
|
|
||||||
: <Typography variant="h5" color="error">No shipment selected</Typography>
|
|
||||||
}
|
|
||||||
|
|
||||||
{localSelectedDewar && !isAddingDewar && (
|
|
||||||
<DewarDetails
|
|
||||||
dewar={localSelectedDewar}
|
|
||||||
trackingNumber={localSelectedDewar.tracking_number || ''}
|
|
||||||
setTrackingNumber={(value) => {
|
|
||||||
setLocalSelectedDewar((prev) => (prev ? { ...prev, tracking_number: value as string } : prev));
|
|
||||||
}}
|
|
||||||
initialContactPersons={selectedShipment?.contact_person ? [selectedShipment.contact_person] : []}
|
|
||||||
initialReturnAddresses={selectedShipment?.return_address ? [selectedShipment.return_address] : []}
|
|
||||||
defaultContactPerson={contactPerson}
|
|
||||||
defaultReturnAddress={selectedShipment?.return_address}
|
|
||||||
shipmentId={selectedShipment?.shipment_id || ''}
|
|
||||||
refreshShipments={refreshShipments}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
{selectedShipment?.dewars?.map((dewar) => (
|
{selectedShipment?.dewars?.map((dewar) => (
|
||||||
<Button
|
<React.Fragment key={dewar.id}>
|
||||||
key={dewar.id}
|
<Button
|
||||||
onClick={() => handleDewarSelection(dewar)}
|
onClick={() => handleDewarSelection(dewar)}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'flex-start',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: 1,
|
padding: localSelectedDewar?.id === dewar.id ? 2 : 1,
|
||||||
border: '1px solid #ccc',
|
textTransform: 'none',
|
||||||
borderRadius: 1,
|
width: '100%',
|
||||||
backgroundColor: localSelectedDewar?.id === dewar.id ? '#f0f0f0' : '#fff',
|
backgroundColor: localSelectedDewar?.id === dewar.id ? '#f0f0f0' : '#fff',
|
||||||
}}
|
transition: 'all 0.3s',
|
||||||
>
|
overflow: 'hidden',
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', marginRight: 2 }}>
|
border: localSelectedDewar?.id === dewar.id ? '2px solid #000' : undefined,
|
||||||
{dewar.qrcode ? (
|
}}
|
||||||
<QRCode value={dewar.qrcode} size={70} />
|
>
|
||||||
) : (
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', marginRight: 2 }}>
|
||||||
<Box
|
{dewar.qrcode ? (
|
||||||
sx={{
|
<QRCode value={dewar.qrcode} size={70} />
|
||||||
width: 70,
|
) : (
|
||||||
height: 70,
|
<Box
|
||||||
display: 'flex',
|
sx={{
|
||||||
alignItems: 'center',
|
width: 70,
|
||||||
justifyContent: 'center',
|
height: 70,
|
||||||
border: '1px dashed #ccc',
|
display: 'flex',
|
||||||
borderRadius: 1,
|
alignItems: 'center',
|
||||||
color: 'text.secondary'
|
justifyContent: 'center',
|
||||||
}}
|
border: '1px dashed #ccc',
|
||||||
>
|
borderRadius: 1,
|
||||||
<Typography variant="body2">No QR Code</Typography>
|
color: 'text.secondary'
|
||||||
</Box>
|
}}
|
||||||
)}
|
>
|
||||||
</Box>
|
<Typography variant="body2">No QR Code</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
|
<Typography variant="body1">{dewar.dewar_name}</Typography>
|
||||||
|
<Typography variant="body2">Number of Pucks: {dewar.number_of_pucks || 0}</Typography>
|
||||||
|
<Typography variant="body2">Number of Samples: {dewar.number_of_samples || 0}</Typography>
|
||||||
|
<Typography variant="body2">Tracking Number: {dewar.tracking_number}</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
Contact Person: {dewar.contact_person?.firstname ? `${dewar.contact_person.firstname} ${dewar.contact_person.lastname}` : 'N/A'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
}}>
|
||||||
|
<CustomStepper dewar={dewar} />
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ flexGrow: 1, marginRight: 0 }}>
|
|
||||||
<Typography variant="body1">{dewar.dewar_name}</Typography>
|
|
||||||
<Typography variant="body2">Number of Pucks: {dewar.number_of_pucks || 0}</Typography>
|
|
||||||
<Typography variant="body2">Number of Samples: {dewar.number_of_samples || 0}</Typography>
|
|
||||||
<Typography variant="body2">Tracking Number: {dewar.tracking_number}</Typography>
|
|
||||||
<Typography variant="body2">
|
|
||||||
Contact Person: {dewar.contact_person?.firstname ? `${dewar.contact_person.firstname} ${dewar.contact_person.lastname}` : 'N/A'}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box sx={{
|
|
||||||
flexGrow: 1,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between'
|
|
||||||
}}>
|
|
||||||
<CustomStepper dewar={dewar} />
|
|
||||||
{localSelectedDewar?.id === dewar.id && (
|
{localSelectedDewar?.id === dewar.id && (
|
||||||
<Button
|
<IconButton
|
||||||
onClick={() => handleDeleteDewar(dewar.id)}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteDewar(dewar.id);
|
||||||
|
}}
|
||||||
color="error"
|
color="error"
|
||||||
sx={{
|
sx={{
|
||||||
minWidth: '40px',
|
minWidth: '40px',
|
||||||
height: '40px',
|
height: '40px',
|
||||||
marginLeft: 2,
|
|
||||||
padding: 0,
|
|
||||||
alignSelf: 'center',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
</Button>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Button>
|
||||||
</Button>
|
|
||||||
|
{localSelectedDewar?.id === dewar.id && (
|
||||||
|
<Box sx={{ padding: 2, border: '1px solid #ccc', borderRadius: '4px' }}>
|
||||||
|
<DewarDetails
|
||||||
|
dewar={localSelectedDewar}
|
||||||
|
trackingNumber={localSelectedDewar.tracking_number || ''}
|
||||||
|
setTrackingNumber={(value) => {
|
||||||
|
setLocalSelectedDewar((prev) => (prev ? { ...prev, tracking_number: value as string } : prev));
|
||||||
|
}}
|
||||||
|
initialContactPersons={selectedShipment?.contact_person ? [selectedShipment.contact_person] : []}
|
||||||
|
initialReturnAddresses={selectedShipment?.return_address ? [selectedShipment.return_address] : []}
|
||||||
|
defaultContactPerson={contactPerson}
|
||||||
|
defaultReturnAddress={selectedShipment?.return_address}
|
||||||
|
shipmentId={selectedShipment?.shipment_id || ''}
|
||||||
|
refreshShipments={refreshShipments}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -33,7 +33,7 @@ const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
|
|||||||
sx,
|
sx,
|
||||||
shipments,
|
shipments,
|
||||||
refreshShipments,
|
refreshShipments,
|
||||||
error
|
error,
|
||||||
}) => {
|
}) => {
|
||||||
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
|
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
|
||||||
|
|
||||||
@ -160,27 +160,29 @@ const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<IconButton
|
|
||||||
onClick={openUploadDialog}
|
|
||||||
color="primary"
|
|
||||||
title="Upload Sample Data Sheet"
|
|
||||||
sx={{ marginLeft: 1 }}
|
|
||||||
>
|
|
||||||
<UploadFileIcon />
|
|
||||||
</IconButton>
|
|
||||||
{selectedShipment?.shipment_id === shipment.shipment_id && (
|
{selectedShipment?.shipment_id === shipment.shipment_id && (
|
||||||
<IconButton
|
<>
|
||||||
onClick={(event) => {
|
<IconButton
|
||||||
event.stopPropagation();
|
onClick={openUploadDialog}
|
||||||
console.log('Delete button clicked'); // debug log
|
color="primary"
|
||||||
handleDeleteShipment();
|
title="Upload Sample Data Sheet"
|
||||||
}}
|
sx={{ marginLeft: 1 }}
|
||||||
color="error"
|
>
|
||||||
title="Delete Shipment"
|
<UploadFileIcon />
|
||||||
sx={{ marginLeft: 1 }}
|
</IconButton>
|
||||||
>
|
<IconButton
|
||||||
<DeleteIcon />
|
onClick={(event) => {
|
||||||
</IconButton>
|
event.stopPropagation();
|
||||||
|
console.log('Delete button clicked'); // debug log
|
||||||
|
handleDeleteShipment();
|
||||||
|
}}
|
||||||
|
color="error"
|
||||||
|
title="Delete Shipment"
|
||||||
|
sx={{ marginLeft: 1 }}
|
||||||
|
>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Button>
|
</Button>
|
||||||
|
42
frontend/src/components/Unipuck.tsx
Normal file
42
frontend/src/components/Unipuck.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
// app/components/Unipuck.tsx
|
||||||
|
import React from 'react';
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
|
||||||
|
interface UnipuckProps {
|
||||||
|
pucks: number; // Number of pucks, assuming each puck follows the same layout
|
||||||
|
}
|
||||||
|
|
||||||
|
const Unipuck: React.FC<UnipuckProps> = ({ pucks }) => {
|
||||||
|
const renderPuck = () => {
|
||||||
|
const puckSVG = (
|
||||||
|
<svg width="100" height="100" viewBox="0 0 100 100">
|
||||||
|
<circle cx="50" cy="50" r="45" stroke="black" strokeWidth="2" fill="none" />
|
||||||
|
{[...Array(11)].map((_, index) => {
|
||||||
|
const angle = (index * (360 / 11)) * (Math.PI / 180);
|
||||||
|
const x = 50 + 35 * Math.cos(angle);
|
||||||
|
const y = 50 + 35 * Math.sin(angle);
|
||||||
|
return <circle key={index} cx={x} cy={y} r="5" fill="black" />;
|
||||||
|
})}
|
||||||
|
{[...Array(5)].map((_, index) => {
|
||||||
|
const angle = (index * (360 / 5) + 36) * (Math.PI / 180);
|
||||||
|
const x = 50 + 15 * Math.cos(angle);
|
||||||
|
const y = 50 + 15 * Math.sin(angle);
|
||||||
|
return <circle key={index} cx={x} cy={y} r="5" fill="black" />;
|
||||||
|
})}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
return puckSVG;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: 2 }}>
|
||||||
|
{[...Array(pucks)].map((_, index) => (
|
||||||
|
<Box key={index} sx={{ margin: 1 }}>
|
||||||
|
{renderPuck()}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Unipuck;
|
Loading…
x
Reference in New Issue
Block a user