diff --git a/backend/app/routers/logistics.py b/backend/app/routers/logistics.py index cdbebdd..8c527e6 100644 --- a/backend/app/routers/logistics.py +++ b/backend/app/routers/logistics.py @@ -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}") diff --git a/backend/app/schemas.py b/backend/app/schemas.py index ca68430..c76c21a 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -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 diff --git a/logistics/src/pages/DewarStatusTab.tsx b/logistics/src/pages/DewarStatusTab.tsx index 00b5554..146704c 100644 --- a/logistics/src/pages/DewarStatusTab.tsx +++ b/logistics/src/pages/DewarStatusTab.tsx @@ -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([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(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 ( - 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 /> )}