Enhance Dewar handling and display in logistics system

Added new fields and enriched data representations in DewarStatusTab, backend schemas, and APIs to improve dewar tracking and management. Introduced new API endpoint `/dewar/table` for simplified data rendering. Applied logging and validations for missing relationships.
This commit is contained in:
GotthardG 2025-02-05 21:43:17 +01:00
parent 25673ae05c
commit 43d67b1044
3 changed files with 243 additions and 38 deletions

View File

@ -7,7 +7,14 @@ from ..models import (
Slot as SlotModel,
LogisticsEvent as LogisticsEventModel,
)
from ..schemas import LogisticsEventCreate, SlotSchema, Dewar as DewarSchema
from ..schemas import (
LogisticsEventCreate,
SlotSchema,
Dewar as DewarSchema,
DewarTable,
ContactMinimal,
AddressMinimal,
)
from ..database import get_db
import logging
from datetime import datetime, timedelta
@ -342,6 +349,64 @@ async def get_all_dewars(db: Session = Depends(get_db)):
return dewars
@router.get("/dewar/table", response_model=List[DewarTable])
async def get_all_dewars_table(db: Session = Depends(get_db)):
dewars = db.query(DewarModel).all()
# Flatten relationships for simplified frontend rendering
response = []
for dewar in dewars:
response.append(
DewarTable(
id=dewar.id,
shipment_id=dewar.shipment_id
if hasattr(dewar, "shipment_id")
else None,
dewar_name=dewar.dewar_name,
shipment_name=dewar.shipment.shipment_name if dewar.shipment else "N/A",
# Use the most recent event if available
status=dewar.events[-1].event_type if dewar.events else "No Events",
tracking_number=dewar.tracking_number or "N/A",
slot_id=dewar.slot[0].id
if dewar.slot
else None, # Use first slot if available
contact=[
ContactMinimal(
firstname=dewar.contact.firstname,
lastname=dewar.contact.lastname,
email=dewar.contact.email,
id=dewar.contact.id,
)
]
if dewar.contact
else [],
address=[
AddressMinimal(
house_number=dewar.return_address.house_number,
street=dewar.return_address.street,
city=dewar.return_address.city,
state=dewar.return_address.state,
country=dewar.return_address.country,
zipcode=dewar.return_address.zipcode,
id=dewar.return_address.id,
)
]
if dewar.return_address
else [],
events=dewar.events[-1].slot_id if dewar.events else "No Events",
last_updated=dewar.events[-1].timestamp if dewar.events else None,
)
)
# Add logging for missing relationships
if not hasattr(dewar, "pgroups"):
logger.warning(f"Dewar {dewar.id} is missing 'pgroups'")
if not hasattr(dewar, "shipment_id"):
logger.warning(f"Dewar {dewar.id} is missing 'shipment_id'")
return response
@router.get("/dewar/{unique_id}", response_model=DewarSchema)
async def get_dewar_by_unique_id(unique_id: str, db: Session = Depends(get_db)):
logger.info(f"Received request for dewar with unique_id: {unique_id}")

View File

@ -1,4 +1,4 @@
from typing import List, Optional
from typing import List, Optional, Union
from datetime import datetime
from pydantic import BaseModel, EmailStr, constr, Field, field_validator
from datetime import date
@ -410,6 +410,13 @@ class ContactUpdate(BaseModel):
email: Optional[EmailStr] = None
class ContactMinimal(BaseModel):
firstname: str
lastname: str
email: EmailStr
id: int
class AddressCreate(BaseModel):
pgroups: str
house_number: Optional[str] = None
@ -438,6 +445,16 @@ class AddressUpdate(BaseModel):
country: Optional[str] = None
class AddressMinimal(BaseModel):
house_number: str
street: str
city: str
state: Optional[str] = None
zipcode: str
country: str
id: int
class Sample(BaseModel):
id: int
sample_name: str
@ -578,6 +595,28 @@ class DewarSchema(BaseModel):
# Tracking will also become an event
class DewarTable(BaseModel):
id: int
pgroups: Optional[str] = None # Make "pgroups" optional
shipment_id: Optional[int] = None # Make "shipment_id" optional
shipment_name: str
dewar_name: str
tracking_number: Optional[str] = None
dewar_type_id: Optional[int] = None
dewar_serial_number_id: Optional[int] = None
unique_id: Optional[str] = None
status: Optional[str] = None
contact: Optional[List[ContactMinimal]] = None
address: Optional[List[AddressMinimal]] = None
event_id: Optional[int] = None
slot_id: Optional[int] = None
events: Optional[Union[str, int]] = None
last_updated: Optional[datetime] = None
class Config:
from_attributes = True
class Proposal(BaseModel):
id: int
number: str

View File

@ -2,12 +2,18 @@ import React, { useEffect, useState } from "react";
import DataGrid from "react-data-grid";
import { Box, Typography, Snackbar, Alert, CircularProgress } from "@mui/material";
import { LogisticsService } from "../../../frontend/openapi";
import "react-data-grid/lib/styles.css";
import dayjs from 'dayjs'; // Import dayjs library
interface Dewar {
id: string;
dewar_name: string;
shipment_name: string; // Added new field
slot_id: string; // Added new field
status: string;
location: string;
beamline_location: string;
timestamp: string; // You can change this type based on your API response
}
@ -15,54 +21,149 @@ const DewarStatusTab: React.FC = () => {
const [dewars, setDewars] = useState<Dewar[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const columns = [
{ key: "dewar_name", name: "Dewar Name", resizable: true },
{
key: "status",
name: "Status",
editable: true,
resizable: true,
editor: (props: { row: any; column: any; onRowChange: any }) => {
return (
<input
type="text"
value={props.row[props.column.key]}
onChange={(e) => props.onRowChange({ ...props.row, [props.column.key]: e.target.value })}
style={{
border: "none",
outline: "none",
padding: "4px",
}}
/>
);
},
},
{ key: "location", name: "Location", resizable: true },
{ key: "timestamp", name: "Last Updated", resizable: true },
const slotQRCodes = [
"A1-X06SA",
"A2-X06SA",
"A3-X06SA",
"A4-X06SA",
"A5-X06SA",
"B1-X06SA",
"B2-X06SA",
"B3-X06SA",
"B4-X06SA",
"B5-X06SA",
"C1-X06SA",
"C2-X06SA",
"C3-X06SA",
"C4-X06SA",
"C5-X06SA",
"D1-X06SA",
"D2-X06SA",
"D3-X06SA",
"D4-X06SA",
"D5-X06SA",
"A1-X10SA",
"A2-X10SA",
"A3-X10SA",
"A4-X10SA",
"A5-X10SA",
"B1-X10SA",
"B2-X10SA",
"B3-X10SA",
"B4-X10SA",
"B5-X10SA",
"C1-X10SA",
"C2-X10SA",
"C3-X10SA",
"C4-X10SA",
"C5-X10SA",
"D1-X10SA",
"D2-X10SA",
"D3-X10SA",
"D4-X10SA",
"D5-X10SA",
"NB1",
"NB2",
"NB3",
"NB4",
"NB5",
"NB6",
"X10SA-Beamline",
"X06SA-Beamline",
"X06DA-Beamline",
"Outgoing X10SA",
"Outgoing X06SA",
];
// Fetch dewars when component mounts
useEffect(() => {
fetchDewarData();
}, []);
// Updated columns array
const columns = [
{ key: "shipment_name", name: "Shipment Name", resizable: true },
{ key: "dewar_name", name: "Dewar Name", resizable: true },
{ key: "slot_id", name: "Storage", resizable: true },
{ key: "status", name: "Status", editable: true, resizable: true },
{ key: "beamline_location", name: "Location", resizable: true },
{ key: "last_updated", name: "Last Updated", resizable: true },
{ key: "local_contact", name: "Local Contact", resizable: true }, // Now a string
{ key: "contact", name: "Contact", resizable: true }, // Now a string
{ key: "address", name: "Return Address", resizable: true }, // Now a string
];
const fetchDewarData = async () => {
setLoading(true);
try {
const dewarData = await LogisticsService.getAllDewarsLogisticsDewarsGet(); // Use your real API call
setDewars(dewarData);
// Fetch data from API
const dewarData = await LogisticsService.getAllDewarsTableLogisticsDewarTableGet();
// Log the raw data for debugging
console.log("Fetched dewarData:", dewarData);
// Flatten and enrich data
const enrichedData = dewarData.map((dewar: any) => {
// Format address into a single string
const returnAddress = dewar.address && dewar.address.length > 0
? `${dewar.address[0].house_number || ""} ${dewar.address[0].street || ""}, ${dewar.address[0].city || ""}, ${dewar.address[0].state || ""}, ${dewar.address[0].zipcode || ""}, ${dewar.address[0].country || ""}`.trim()
: "N/A";
// Format contact into a single string
const contact = dewar.contact && dewar.contact.length > 0
? `${dewar.contact[0].firstname || "N/A"} ${dewar.contact[0].lastname || "N/A"} (${dewar.contact[0].email || "N/A"})`
: "N/A";
// Format local_contact into a single string
const localContact = dewar.local_contact
? `${dewar.local_contact.firstname || "N/A"} ${dewar.local_contact.lastname || "N/A"} (${dewar.local_contact.phone_number || "N/A"})`
: "N/A";
const beamline_location = dewar.events.slot_id || "N/A";
console.log("Beamline location:", beamline_location);
// Log any fields that are missing or appear incorrect
if (!dewar.local_contact) console.warn("Missing local_contact for dewar:", dewar);
if (!dewar.contact) console.warn("Missing contact for dewar:", dewar);
if (!dewar.address) console.warn("Missing address for dewar:", dewar);
return {
...dewar,
local_contact: localContact,
contact: contact,
address: returnAddress, // Replace `address` object with single formatted string
beamline_location: dewar.events !== undefined && slotQRCodes[dewar.events]
? slotQRCodes[dewar.events -1]
: "",
slot_id: dewar.slot_id !== undefined && slotQRCodes[dewar.slot_id]
? slotQRCodes[dewar.slot_id -1]
: "", // Convert slot_id to descriptive label
last_updated: dewar.last_updated
? new Date(dewar.last_updated).toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
hour12: true,
})
: ""
};
});
setDewars(enrichedData);
console.log("Final enrichedData:", enrichedData);
} catch (e) {
console.error("Failed to fetch or process dewar data:", e);
setError("Failed to fetch dewar data");
} finally {
setLoading(false);
}
};
const onRowsChange = async (updatedRow: Dewar[]) => {
setDewars(updatedRow);
useEffect(() => {
fetchDewarData();
}, []);
const onRowsChange = async (updatedRows: Dewar[]) => {
setDewars(updatedRows);
try {
const updatedDewar = updatedRow[updatedRow.length - 1]; // Get the last edited row
const updatedDewar = updatedRows[updatedRows.length - 1]; // Get the last edited row
await LogisticsService.updateDewarStatus({ ...updatedDewar }); // Mock API update
} catch (err) {
setError("Error updating dewar");
@ -87,7 +188,7 @@ const DewarStatusTab: React.FC = () => {
columns={columns}
rows={dewars}
onRowsChange={onRowsChange}
style={{ height: 600, width: "100%" }}
style={{ height: 600, width: "100%" }} // Make sure height and width are set
/>
)}
</Box>