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, Slot as SlotModel,
LogisticsEvent as LogisticsEventModel, 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 from ..database import get_db
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -342,6 +349,64 @@ async def get_all_dewars(db: Session = Depends(get_db)):
return dewars 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) @router.get("/dewar/{unique_id}", response_model=DewarSchema)
async def get_dewar_by_unique_id(unique_id: str, db: Session = Depends(get_db)): 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}") 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 datetime import datetime
from pydantic import BaseModel, EmailStr, constr, Field, field_validator from pydantic import BaseModel, EmailStr, constr, Field, field_validator
from datetime import date from datetime import date
@ -410,6 +410,13 @@ class ContactUpdate(BaseModel):
email: Optional[EmailStr] = None email: Optional[EmailStr] = None
class ContactMinimal(BaseModel):
firstname: str
lastname: str
email: EmailStr
id: int
class AddressCreate(BaseModel): class AddressCreate(BaseModel):
pgroups: str pgroups: str
house_number: Optional[str] = None house_number: Optional[str] = None
@ -438,6 +445,16 @@ class AddressUpdate(BaseModel):
country: Optional[str] = None 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): class Sample(BaseModel):
id: int id: int
sample_name: str sample_name: str
@ -578,6 +595,28 @@ class DewarSchema(BaseModel):
# Tracking will also become an event # 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): class Proposal(BaseModel):
id: int id: int
number: str number: str

View File

@ -2,12 +2,18 @@ import React, { useEffect, useState } from "react";
import DataGrid from "react-data-grid"; import DataGrid from "react-data-grid";
import { Box, Typography, Snackbar, Alert, CircularProgress } from "@mui/material"; import { Box, Typography, Snackbar, Alert, CircularProgress } from "@mui/material";
import { LogisticsService } from "../../../frontend/openapi"; import { LogisticsService } from "../../../frontend/openapi";
import "react-data-grid/lib/styles.css";
import dayjs from 'dayjs'; // Import dayjs library
interface Dewar { interface Dewar {
id: string; id: string;
dewar_name: string; dewar_name: string;
shipment_name: string; // Added new field
slot_id: string; // Added new field
status: string; status: string;
location: string; beamline_location: string;
timestamp: string; // You can change this type based on your API response 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 [dewars, setDewars] = useState<Dewar[]>([]);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const slotQRCodes = [
const columns = [ "A1-X06SA",
{ key: "dewar_name", name: "Dewar Name", resizable: true }, "A2-X06SA",
{ "A3-X06SA",
key: "status", "A4-X06SA",
name: "Status", "A5-X06SA",
editable: true, "B1-X06SA",
resizable: true, "B2-X06SA",
editor: (props: { row: any; column: any; onRowChange: any }) => { "B3-X06SA",
return ( "B4-X06SA",
<input "B5-X06SA",
type="text" "C1-X06SA",
value={props.row[props.column.key]} "C2-X06SA",
onChange={(e) => props.onRowChange({ ...props.row, [props.column.key]: e.target.value })} "C3-X06SA",
style={{ "C4-X06SA",
border: "none", "C5-X06SA",
outline: "none", "D1-X06SA",
padding: "4px", "D2-X06SA",
}} "D3-X06SA",
/> "D4-X06SA",
); "D5-X06SA",
}, "A1-X10SA",
}, "A2-X10SA",
{ key: "location", name: "Location", resizable: true }, "A3-X10SA",
{ key: "timestamp", name: "Last Updated", resizable: true }, "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 // Updated columns array
useEffect(() => { const columns = [
fetchDewarData(); { 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 () => { const fetchDewarData = async () => {
setLoading(true); setLoading(true);
try { try {
const dewarData = await LogisticsService.getAllDewarsLogisticsDewarsGet(); // Use your real API call // Fetch data from API
setDewars(dewarData); 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) { } catch (e) {
console.error("Failed to fetch or process dewar data:", e);
setError("Failed to fetch dewar data"); setError("Failed to fetch dewar data");
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const onRowsChange = async (updatedRow: Dewar[]) => {
setDewars(updatedRow); useEffect(() => {
fetchDewarData();
}, []);
const onRowsChange = async (updatedRows: Dewar[]) => {
setDewars(updatedRows);
try { 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 await LogisticsService.updateDewarStatus({ ...updatedDewar }); // Mock API update
} catch (err) { } catch (err) {
setError("Error updating dewar"); setError("Error updating dewar");
@ -87,7 +188,7 @@ const DewarStatusTab: React.FC = () => {
columns={columns} columns={columns}
rows={dewars} rows={dewars}
onRowsChange={onRowsChange} onRowsChange={onRowsChange}
style={{ height: 600, width: "100%" }} style={{ height: 600, width: "100%" }} // Make sure height and width are set
/> />
)} )}
</Box> </Box>