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.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
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(dewar.router, prefix="/dewars", tags=["dewars"])
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__":
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 {
ContactPerson,
Address,
Dewar, ContactsService, AddressesService, ShipmentsService, DewarsService,
Dewar, ContactsService, AddressesService, ShipmentsService,
} from '../../openapi';
import Unipuck from '../components/Unipuck';
interface DewarDetailsProps {
dewar: Dewar;
@ -252,10 +253,10 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
shipment_date: existingShipment.shipment_date,
shipment_status: existingShipment.shipment_status,
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,
proposal_id: existingShipment.proposal?.id,
dewars: [updatedDewar], // Updated dewars array
dewars: [updatedDewar],
};
console.log('Payload for update:', JSON.stringify(payload, null, 2));
@ -292,18 +293,19 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
sx={{ width: '300px', 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 ? (
<QRCode value={dewar.qrcode} size={70} />
) : (
<Typography>No QR code available</Typography>
)}
<Button variant="contained" sx={{ marginTop: 1 }} onClick={() => { /** Add logic to generate QR Code */ }}>
Generate QR Code
</Button>
</Box>
<Button variant="contained" onClick={() => { /** Add logic to generate QR Code */ }}>
Generate QR Code
</Button>
</Box>
<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">Current Contact Person:</Typography>
<Select

View File

@ -23,14 +23,11 @@ interface ShipmentDetailsProps {
}
const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
isCreatingShipment,
sx,
selectedShipment,
selectedDewar,
setSelectedDewar,
setSelectedShipment,
refreshShipments,
defaultContactPerson
}) => {
const [localSelectedDewar, setLocalSelectedDewar] = useState<Dewar | null>(null);
const [isAddingDewar, setIsAddingDewar] = useState<boolean>(false);
@ -200,146 +197,152 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
</Box>
)}
{
selectedShipment
? (
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Box sx={{ marginTop: 2, marginBottom: 2 }}>
<Typography variant="h5">{selectedShipment.shipment_name}</Typography>
<Typography variant="body1" color="textSecondary">
Main contact person: {contactPerson ? `${contactPerson.firstname} ${contactPerson.lastname}` : 'N/A'}
</Typography>
<Typography variant="body1">Number of Pucks: {totalPucks}</Typography>
<Typography variant="body1">Number of Samples: {totalSamples}</Typography>
<Typography variant="body1">Shipment Date: {selectedShipment.shipment_date}</Typography>
</Box>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ position: 'relative' }}>
<TextField
label="Comments"
fullWidth
multiline
rows={4}
value={comments}
onChange={(e) => setComments(e.target.value)}
sx={{
marginBottom: 2,
'& .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}
/>
<Box sx={{ position: 'absolute', bottom: 8, right: 8, display: 'flex', gap: 1 }}>
<IconButton
color="primary"
onClick={handleSaveComments}
disabled={comments.length > MAX_COMMENTS_LENGTH}
>
<CheckIcon />
</IconButton>
<IconButton color="secondary" onClick={handleCancelEdit}>
<CloseIcon />
</IconButton>
</Box>
</Box>
</Grid>
</Grid>
)
: <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}
/>
{selectedShipment ? (
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Box sx={{ marginTop: 2, marginBottom: 2 }}>
<Typography variant="h5">{selectedShipment.shipment_name}</Typography>
<Typography variant="body1" color="textSecondary">
Main contact person: {contactPerson ? `${contactPerson.firstname} ${contactPerson.lastname}` : 'N/A'}
</Typography>
<Typography variant="body1">Number of Pucks: {totalPucks}</Typography>
<Typography variant="body1">Number of Samples: {totalSamples}</Typography>
<Typography variant="body1">Shipment Date: {selectedShipment.shipment_date}</Typography>
</Box>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ position: 'relative' }}>
<TextField
label="Comments"
fullWidth
multiline
rows={4}
value={comments}
onChange={(e) => setComments(e.target.value)}
sx={{
marginBottom: 2,
'& .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}
/>
<Box sx={{ position: 'absolute', bottom: 8, right: 8, display: 'flex', gap: 1 }}>
<IconButton
color="primary"
onClick={handleSaveComments}
disabled={comments.length > MAX_COMMENTS_LENGTH}
>
<CheckIcon />
</IconButton>
<IconButton color="secondary" onClick={handleCancelEdit}>
<CloseIcon />
</IconButton>
</Box>
</Box>
</Grid>
</Grid>
) : (
<Typography variant="h5" color="error">No shipment selected</Typography>
)}
<Stack spacing={1}>
{selectedShipment?.dewars?.map((dewar) => (
<Button
key={dewar.id}
onClick={() => handleDewarSelection(dewar)}
variant="outlined"
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: 1,
border: '1px solid #ccc',
borderRadius: 1,
backgroundColor: localSelectedDewar?.id === dewar.id ? '#f0f0f0' : '#fff',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', marginRight: 2 }}>
{dewar.qrcode ? (
<QRCode value={dewar.qrcode} size={70} />
) : (
<Box
sx={{
width: 70,
height: 70,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '1px dashed #ccc',
borderRadius: 1,
color: 'text.secondary'
}}
>
<Typography variant="body2">No QR Code</Typography>
</Box>
)}
</Box>
<React.Fragment key={dewar.id}>
<Button
onClick={() => handleDewarSelection(dewar)}
variant="outlined"
sx={{
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
padding: localSelectedDewar?.id === dewar.id ? 2 : 1,
textTransform: 'none',
width: '100%',
backgroundColor: localSelectedDewar?.id === dewar.id ? '#f0f0f0' : '#fff',
transition: 'all 0.3s',
overflow: 'hidden',
border: localSelectedDewar?.id === dewar.id ? '2px solid #000' : undefined,
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', marginRight: 2 }}>
{dewar.qrcode ? (
<QRCode value={dewar.qrcode} size={70} />
) : (
<Box
sx={{
width: 70,
height: 70,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '1px dashed #ccc',
borderRadius: 1,
color: 'text.secondary'
}}
>
<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 && (
<Button
onClick={() => handleDeleteDewar(dewar.id)}
<IconButton
onClick={(e) => {
e.stopPropagation();
handleDeleteDewar(dewar.id);
}}
color="error"
sx={{
minWidth: '40px',
height: '40px',
marginLeft: 2,
padding: 0,
alignSelf: 'center',
}}
>
<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>
</Box>

View File

@ -33,7 +33,7 @@ const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
sx,
shipments,
refreshShipments,
error
error,
}) => {
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
@ -160,27 +160,29 @@ const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
</Box>
</Box>
<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 && (
<IconButton
onClick={(event) => {
event.stopPropagation();
console.log('Delete button clicked'); // debug log
handleDeleteShipment();
}}
color="error"
title="Delete Shipment"
sx={{ marginLeft: 1 }}
>
<DeleteIcon />
</IconButton>
<>
<IconButton
onClick={openUploadDialog}
color="primary"
title="Upload Sample Data Sheet"
sx={{ marginLeft: 1 }}
>
<UploadFileIcon />
</IconButton>
<IconButton
onClick={(event) => {
event.stopPropagation();
console.log('Delete button clicked'); // debug log
handleDeleteShipment();
}}
color="error"
title="Delete Shipment"
sx={{ marginLeft: 1 }}
>
<DeleteIcon />
</IconButton>
</>
)}
</Box>
</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;