added update to comments with characters counter

This commit is contained in:
GotthardG 2024-11-03 21:42:42 +01:00
parent 0becdf9337
commit a9b8925be8
6 changed files with 242 additions and 157 deletions

View File

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

View 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

View File

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

View File

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

View File

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

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