diff --git a/backend/app/data/data.py b/backend/app/data/data.py index ffdb068..c58154f 100644 --- a/backend/app/data/data.py +++ b/backend/app/data/data.py @@ -682,9 +682,16 @@ dewar_to_beamtime = { dewar.id: random.choice([1, 2]) for dewar in dewars # Or use actual beamtime ids } +# Update dewars and their pucks with consistent beamtime +for dewar in dewars: + dewar.beamtime_id = dewar_to_beamtime[dewar.id] + for puck in pucks: dewar_id = puck.dewar_id # Assuming puck has dewar_id assigned_beamtime = dewar_to_beamtime[dewar_id] + puck.beamtime_id = ( + assigned_beamtime # Associate puck to the same beamtime as its dewar + ) positions_with_samples = random.randint(1, 16) occupied_positions = random.sample(range(1, 17), positions_with_samples) @@ -696,7 +703,7 @@ for puck in pucks: sample_name=f"Sample{sample_id_counter:03}", position=pos, puck_id=puck.id, - beamtime_id=assigned_beamtime, # IMPORTANT: Use the dewar's beamtime + beamtime_id=assigned_beamtime, ) samples.append(sample) sample_id_counter += 1 diff --git a/backend/app/routers/dewar.py b/backend/app/routers/dewar.py index ce7299e..4fc2824 100644 --- a/backend/app/routers/dewar.py +++ b/backend/app/routers/dewar.py @@ -718,3 +718,13 @@ async def get_single_shipment(id: int, db: Session = Depends(get_db)): except SQLAlchemyError as e: logging.error(f"Database error occurred: {e}") raise HTTPException(status_code=500, detail="Internal server error") + + +@dewar_router.get( + "/by-beamtime/{beamtime_id}", + response_model=List[DewarSchema], + operation_id="get_dewars_by_beamtime", +) +async def get_dewars_by_beamtime(beamtime_id: int, db: Session = Depends(get_db)): + """List all dewars assigned to a given beamtime.""" + return db.query(DewarModel).filter(DewarModel.beamtime_id == beamtime_id).all() diff --git a/backend/app/routers/puck.py b/backend/app/routers/puck.py index 5da02dd..cc85d05 100644 --- a/backend/app/routers/puck.py +++ b/backend/app/routers/puck.py @@ -683,3 +683,13 @@ async def assign_beamtime_to_puck( sample.beamtime_id = None if beamtime_id == 0 else beamtime_id db.commit() return {"status": "success", "puck_id": puck.id, "beamtime_id": beamtime_id} + + +@router.get( + "/by-beamtime/{beamtime_id}", + response_model=List[PuckSchema], + operation_id="get_pucks_by_beamtime", +) +async def get_pucks_by_beamtime(beamtime_id: int, db: Session = Depends(get_db)): + """List all pucks assigned to a given beamtime.""" + return db.query(PuckModel).filter(PuckModel.beamtime_id == beamtime_id).all() diff --git a/backend/app/routers/sample.py b/backend/app/routers/sample.py index b76bcee..5927bd0 100644 --- a/backend/app/routers/sample.py +++ b/backend/app/routers/sample.py @@ -471,3 +471,13 @@ async def get_results_for_run_and_sample( ] return formatted_results + + +@router.get( + "/by-beamtime/{beamtime_id}", + response_model=List[SampleSchema], + operation_id="get_samples_by_beamtime", +) +async def get_samples_by_beamtime(beamtime_id: int, db: Session = Depends(get_db)): + """List all samples assigned to a given beamtime.""" + return db.query(SampleModel).filter(SampleModel.beamtime_id == beamtime_id).all() diff --git a/frontend/src/components/Calendar.tsx b/frontend/src/components/Calendar.tsx index cbd6945..d7e6391 100644 --- a/frontend/src/components/Calendar.tsx +++ b/frontend/src/components/Calendar.tsx @@ -6,34 +6,31 @@ import interactionPlugin from '@fullcalendar/interaction'; import '../styles/Calendar.css'; import { BeamtimesService, DewarsService, PucksService } from '../../openapi'; -// Define colors for each beamline const beamlineColors: { [key: string]: string } = { X06SA: '#FF5733', X10SA: '#33FF57', X06DA: '#3357FF', - Unknown: '#CCCCCC', // Gray color for unknown beamlines + Unknown: '#CCCCCC', }; -// Custom event interface interface CustomEvent extends EventInput { beamline: string; beamtime_shift: string; - isSubmitted?: boolean; // Track if information is submitted + beamtime_id?: number; + isSubmitted?: boolean; } -// Define experiment modes const experimentModes = ['SDU-Scheduled', 'SDU-queued', 'Remote', 'In-person']; -// Utility function to darken a hex color const darkenColor = (color: string, percent: number): string => { - const num = parseInt(color.slice(1), 16); // Convert hex to number - const amt = Math.round(2.55 * percent); // Calculate amount to darken - const r = (num >> 16) + amt; // Red - const g = (num >> 8 & 0x00FF) + amt; // Green - const b = (num & 0x0000FF) + amt; // Blue - - // Ensure values stay within 0-255 range - const newColor = (0x1000000 + (r < 255 ? (r < 0 ? 0 : r) : 255) * 0x10000 + (g < 255 ? (g < 0 ? 0 : g) : 255) * 0x100 + (b < 255 ? (b < 0 ? 0 : b) : 255)).toString(16).slice(1); + const num = parseInt(color.slice(1), 16); + const amt = Math.round(2.55 * percent); + const r = (num >> 16) + amt; + const g = (num >> 8 & 0x00FF) + amt; + const b = (num & 0x0000FF) + amt; + const newColor = (0x1000000 + (r < 255 ? (r < 0 ? 0 : r) : 255) * 0x10000 + + (g < 255 ? (g < 0 ? 0 : g) : 255) * 0x100 + + (b < 255 ? (b < 0 ? 0 : b) : 255)).toString(16).slice(1); return `#${newColor}`; }; @@ -43,7 +40,8 @@ const Calendar: React.FC = () => { const [fetchError, setFetchError] = useState(null); const [selectedEventId, setSelectedEventId] = useState(null); const [eventDetails, setEventDetails] = useState(null); - const [eventAssociations, setEventAssociations] = useState<{ [eventId: string]: {dewars: string[], pucks: string[]} }>({}); + // eventId => { dewars: [dewar_id], pucks: [puck_id] } + const [eventAssociations, setEventAssociations] = useState<{ [eventId: string]: { dewars: string[], pucks: string[] } }>({}); const [userDetails, setUserDetails] = useState({ name: '', firstName: '', @@ -52,103 +50,94 @@ const Calendar: React.FC = () => { extAccount: '', experimentMode: experimentModes[0], }); - const [shipments, setShipments] = useState([]); // State for shipments - const [selectedDewars, setSelectedDewars] = useState([]); // Track selected dewars for the experiment + const [shipments, setShipments] = useState([]); - const fetchBeamtimes = async () => { - setIsLoading(true); - setFetchError(null); - - try { - const beamtimeData = await BeamtimesService.getMyBeamtimesProtectedBeamtimesMyBeamtimesGet(); // Replace with actual API function - const formattedEvents: CustomEvent[] = beamtimeData.map((beamtime: any) => ({ - id: `${beamtime.beamline}-${beamtime.start_date}`, - title: `${beamtime.beamline}: ${beamtime.shift}`, - start: beamtime.start_date, - end: beamtime.end_date, - beamtime_id: beamtime.id, - beamline: beamtime.beamline || 'Unknown', - beamtime_shift: beamtime.shift || 'Unknown', - backgroundColor: beamlineColors[beamtime.beamline] || beamlineColors.Unknown, - borderColor: '#000', - textColor: '#fff', - })); - - setEvents(formattedEvents); - } catch (error) { - console.error('Failed to fetch beamtime data:', error); - setFetchError('Failed to load beamtime data. Please try again later.'); - } finally { - setIsLoading(false); - } - }; - - // Fetch beamtimes on component mount + // Load all beamtime events AND their current associations (on mount) useEffect(() => { - fetchBeamtimes(); + const fetchAll = async () => { + setIsLoading(true); + setFetchError(null); + try { + const beamtimes = await BeamtimesService.getMyBeamtimesProtectedBeamtimesMyBeamtimesGet(); + const formattedEvents: CustomEvent[] = beamtimes.map((beamtime: any) => ({ + id: `${beamtime.beamline}-${beamtime.start_date}`, + title: `${beamtime.beamline}: ${beamtime.shift}`, + start: beamtime.start_date, + end: beamtime.end_date, + beamtime_id: beamtime.id, + beamline: beamtime.beamline || 'Unknown', + beamtime_shift: beamtime.shift || 'Unknown', + backgroundColor: beamlineColors[beamtime.beamline] || beamlineColors.Unknown, + borderColor: '#000', + textColor: '#fff', + })); + setEvents(formattedEvents); + + // Fetch associations for all + const assoc: { [id: string]: { dewars: string[]; pucks: string[] } } = {}; + await Promise.all( + beamtimes.map(async (bm: any) => { + const [dewars, pucks] = await Promise.all([ + DewarsService.getDewarsByBeamtime(bm.id), + PucksService.getPucksByBeamtime(bm.id), + ]); + const eventId = `${bm.beamline}-${bm.start_date}`; + assoc[eventId] = { + dewars: dewars.map((d: any) => d.id), + pucks: pucks.map((p: any) => p.id), + }; + }) + ); + setEventAssociations(assoc); + + } catch (error) { + setFetchError('Failed to load beamtime data. Please try again later.'); + setEvents([]); + setEventAssociations({}); + } finally { + setIsLoading(false); + } + }; + fetchAll(); }, []); + // When an event is selected, fetch up-to-date dewar list useEffect(() => { - // Only fetch if you want to update dewars when eventDetails change, - // or run only on initial mount by passing [] as deps if (eventDetails) { const fetchDewars = async () => { try { - // This name depends on your codegen's operationId / function name! - const dewars = await DewarsService.getRecentDewarsWithPucks(); - setShipments(dewars); + const dewarsWithPucks = await DewarsService.getRecentDewarsWithPucks(); + setShipments(dewarsWithPucks); } catch (err) { - // Optionally handle error - console.error("Failed to fetch dewars with pucks", err); + setShipments([]); } }; fetchDewars(); + } else { + setShipments([]); } - }, [eventDetails]); // Fetch dewars when an event is selected + }, [eventDetails]); - - //const fetchShipments = async () => { - // try { - // const response = await fetch('/shipmentdb.json'); - // - // // Check for HTTP errors - // if (!response.ok) { - // throw new Error(`HTTP error! status: ${response.status}`); - // } - // - // // Parse the JSON response - // const data = await response.json(); - // - // const availableDewars: any[] = []; - // - // data.shipments.forEach(shipment => { - // if (shipment.shipment_status === "In Transit") { - // shipment.dewars.forEach(dewar => { - // if (dewar.shippingStatus === "shipped" && dewar.returned === "") { - // availableDewars.push(dewar); - // } - // }); - // } - // }); - // - // console.log('Available Dewars:', availableDewars); - // setShipments(availableDewars); - // } catch (error) { - // console.error('Error fetching shipments:', error); - // // Optionally display the error to the user in the UI - // } - // }; - // - // fetchEvents(); - // fetchShipments(); - //}, []); + // Refresh associations after (un)assign action + const refetchEventAssociations = async (beamtimeId: number, eventId: string) => { + const [dewars, pucks] = await Promise.all([ + DewarsService.getDewarsByBeamtime(beamtimeId), + PucksService.getPucksByBeamtime(beamtimeId), + ]); + setEventAssociations(prev => ({ + ...prev, + [eventId]: { + dewars: dewars.map((d: any) => d.id), + pucks: pucks.map((p: any) => p.id), + } + })); + }; const handleEventClick = (eventInfo: any) => { const clickedEventId = eventInfo.event.id; setSelectedEventId(clickedEventId); - - const selectedEvent = events.find(event => event.id === clickedEventId) || null; - setEventDetails(selectedEvent); + const selected = events.find(event => event.id === clickedEventId) || null; + setEventDetails(selected); }; const handleInputChange = (e: React.ChangeEvent) => { @@ -159,23 +148,15 @@ const Calendar: React.FC = () => { })); }; - const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - if (eventDetails) { - const updatedEvents = events.map(event => - event.id === eventDetails.id - ? { ...event, isSubmitted: true, selectedDewars } // Associate selected dewars - : event + setEvents(prev => + prev.map(event => + event.id === eventDetails.id ? { ...event, isSubmitted: true } : event + ) ); - setEvents(updatedEvents); } - - console.log('User Details:', userDetails); - console.log('Selected Dewars:', selectedDewars); - - // Reset user details and selected dewars after submission setUserDetails({ name: '', firstName: '', @@ -184,143 +165,48 @@ const Calendar: React.FC = () => { extAccount: '', experimentMode: experimentModes[0], }); - setSelectedDewars([]); // Reset selected dewars - }; - const assignDewarToBeamtime = async (dewarId: number, beamtimeId: number) => { - return DewarsService.assignDewarToBeamtime(dewarId, beamtimeId); }; - const unassignDewarFromBeamtime = async (dewarId: number) => { - // Pass "0" as the special value (if that's how you unassign) - return DewarsService.assignDewarToBeamtime(dewarId, 0); // or whatever your backend supports - }; - - const assignPuckToBeamtime = async (puckId: number, beamtimeId: number) => { - return PucksService.assignPuckToBeamtime(puckId, beamtimeId); - }; - -// For unassignment (beamtime_id = 0) - const unassignPuckFromBeamtime = async (puckId: number) => { - return PucksService.assignPuckToBeamtime(puckId, 0); - }; - - - - function handleDewarAssignment(dewarId: string) { - const event = events.find(e => e.id === selectedEventId); - if (!event) return; + // Unified assign/unassign for Dewars + const handleDewarAssignment = async (dewarId: string) => { + if (!selectedEventId) return; + const event = events.find(e => e.id === selectedEventId)!; const beamtimeId = event.beamtime_id; - if (beamtimeId == null) { - console.error("No numeric beamtime_id found for selected event:", event); - return; + if (!beamtimeId) return; + // Is this dewar already assigned here? + const assigned = eventAssociations[selectedEventId]?.dewars.includes(dewarId); + try { + if (assigned) { + await DewarsService.assignDewarToBeamtime(Number(dewarId), 0); + } else { + await DewarsService.assignDewarToBeamtime(Number(dewarId), Number(beamtimeId)); + } + await refetchEventAssociations(Number(beamtimeId), selectedEventId); + } catch (e) { + // Optionally report error } + }; - // Find out if it's already assigned to this shift! - const prev = eventAssociations[selectedEventId] || { dewars: [], pucks: [] }; - const isAssigned = prev.dewars.includes(dewarId); - - if (!isAssigned) { - // Assign it immediately - assignDewarToBeamtime(Number(dewarId), Number(beamtimeId)) - .then(() => { - setEventAssociations(prevAssoc => { - const updated = prevAssoc[selectedEventId] || { dewars: [], pucks: [] }; - return { - ...prevAssoc, - [selectedEventId]: { - ...updated, - dewars: [...updated.dewars, dewarId] - } - }; - }); - }) - .catch(e => { - console.error("Failed to assign dewar to beamtime", e); - }); - } else { - // Unassign (patch to None) immediately - unassignDewarFromBeamtime(Number(dewarId)) - .then(() => { - setEventAssociations(prevAssoc => { - const updated = prevAssoc[selectedEventId] || { dewars: [], pucks: [] }; - return { - ...prevAssoc, - [selectedEventId]: { - ...updated, - dewars: updated.dewars.filter(id => id !== dewarId) - } - }; - }); - }) - .catch(e => { - console.error("Failed to unassign dewar from beamtime", e); - }); - } - } - - - function handlePuckAssignment(puckId: string) { - const event = events.find(e => e.id === selectedEventId); - if (!event) return; + // Unified assign/unassign for Pucks + const handlePuckAssignment = async (puckId: string) => { + if (!selectedEventId) return; + const event = events.find(e => e.id === selectedEventId)!; const beamtimeId = event.beamtime_id; - if (beamtimeId == null) { - console.error("No numeric beamtime_id found for selected event:", event); - return; + if (!beamtimeId) return; + const assigned = eventAssociations[selectedEventId]?.pucks.includes(puckId); + try { + if (assigned) { + await PucksService.assignPuckToBeamtime(Number(puckId), 0); + } else { + await PucksService.assignPuckToBeamtime(Number(puckId), Number(beamtimeId)); + } + await refetchEventAssociations(Number(beamtimeId), selectedEventId); + } catch (e) { + // Optionally report error } + }; - // Get current association state - const prev = eventAssociations[selectedEventId] || { dewars: [], pucks: [] }; - const isAssigned = prev.pucks.includes(puckId); - - if (!isAssigned) { - assignPuckToBeamtime(Number(puckId), Number(beamtimeId)) - .then(() => { - setEventAssociations(prevAssoc => { - const updated = prevAssoc[selectedEventId] || { dewars: [], pucks: [] }; - return { - ...prevAssoc, - [selectedEventId]: { - ...updated, - pucks: [...updated.pucks, puckId] - } - }; - }); - }) - .catch(e => { - console.error("Failed to assign puck to beamtime", e); - }); - } else { - unassignPuckFromBeamtime(Number(puckId)) - .then(() => { - setEventAssociations(prevAssoc => { - const updated = prevAssoc[selectedEventId] || { dewars: [], pucks: [] }; - return { - ...prevAssoc, - [selectedEventId]: { - ...updated, - pucks: updated.pucks.filter(id => id !== puckId) - } - }; - }); - }) - .catch(e => { - console.error("Failed to unassign puck from beamtime", e); - }); - } - } - - function findAssociatedEventForDewar(dewarId: string): string | undefined { - for (const [eventId, assoc] of Object.entries(eventAssociations)) { - if (assoc.dewars.includes(dewarId)) return eventId; - } - } - function findAssociatedEventForPuck(puckId: string): string | undefined { - for (const [eventId, assoc] of Object.entries(eventAssociations)) { - if (assoc.pucks.includes(puckId)) return eventId; - } - } - - + // For displaying badge in calendar and UI const eventContent = (eventInfo: any) => { const beamline = eventInfo.event.extendedProps.beamline || 'Unknown'; const isSelected = selectedEventId === eventInfo.event.id; @@ -328,8 +214,6 @@ const Calendar: React.FC = () => { const hasAssociations = eventAssociations[eventInfo.event.id]?.dewars.length > 0 || eventAssociations[eventInfo.event.id]?.pucks.length > 0; - - const backgroundColor = isSubmitted ? darkenColor(beamlineColors[beamline] || beamlineColors.Unknown, -20) : isSelected @@ -359,6 +243,18 @@ const Calendar: React.FC = () => { ); }; + // Used in Dewar/Puck assign status reporting + function getAssignedEventForDewar(dewarId: string) { + return Object.entries(eventAssociations).find(([eid, assoc]) => + assoc.dewars.includes(dewarId) + ); + } + function getAssignedEventForPuck(puckId: string) { + return Object.entries(eventAssociations).find(([eid, assoc]) => + assoc.pucks.includes(puckId) + ); + } + return (

Beamline Calendar

@@ -385,9 +281,20 @@ const Calendar: React.FC = () => {

Select Dewars

    {shipments.map(dewar => { - const thisEvent = eventAssociations[selectedEventId] || { dewars: [], pucks: [] }; + const thisEvent = eventAssociations[selectedEventId!] || { dewars: [], pucks: [] }; const dewarAssigned = thisEvent.dewars.includes(dewar.id); + const [assocEventId, assoc] = getAssignedEventForDewar(dewar.id) || []; + const assocEvent = assocEventId + ? events.find(ev => ev.id === assocEventId) + : null; + const assocShift = assocEvent?.beamtime_shift; + const assocDate = assocEvent?.start; + const assocBeamline = assocEvent?.beamline; + + const currentShift = eventDetails?.beamtime_shift; + const isAssignedToThis = assocShift && currentShift && assocShift === currentShift; + return (
  • - {!dewarAssigned && dewar.pucks && dewar.pucks.length > 0 && ( + {/* List all pucks in this Dewar, each with assign button */} + {Array.isArray(dewar.pucks) && dewar.pucks.length > 0 && (
      - {(dewar.pucks || []).map(puck => { - // Find eventId for this puck, if assigned - const associatedEventId = Object.keys(eventAssociations).find(eid => - eventAssociations[eid]?.pucks.includes(puck.id) - ); - const associatedEvent = associatedEventId - ? events.find(ev => ev.id === associatedEventId) + {dewar.pucks.map(puck => { + const [pAssocEventId] = getAssignedEventForPuck(puck.id) || []; + const pAssocEvent = pAssocEventId + ? events.find(ev => ev.id === pAssocEventId) : null; - - const associatedShift = associatedEvent?.beamtime_shift; - const associatedDate = associatedEvent?.start; - const associatedBeamline = associatedEvent?.beamline; - - const currentShift = eventDetails?.beamtime_shift; - const isAssignedToCurrentShift = associatedShift && currentShift && associatedShift === currentShift; - + const pAssocShift = pAssocEvent?.beamtime_shift; + const pAssocDate = pAssocEvent?.start; + const pAssocBeamline = pAssocEvent?.beamline; + const isAssignedHere = pAssocShift && currentShift && pAssocShift === currentShift; return ( -
      - {puck.name} +
    • - {associatedEvent && ( - - ← Assigned to shift: {associatedShift} - {associatedDate && ( - <> on {new Date(associatedDate).toLocaleDateString()} - )} - {associatedBeamline && ( - <> ({associatedBeamline}) - )} - + {pAssocEvent && ( + + ← Assigned to: {pAssocShift} {pAssocDate && <>on {new Date(pAssocDate).toLocaleDateString()}} {pAssocBeamline && <>({pAssocBeamline})} + )} -
    • + ); })}
    )} - {/* Show associated shift, date, and beamline for Dewar, if any */} - {(() => { - const associatedEventId = Object.keys(eventAssociations).find(eid => - eventAssociations[eid]?.dewars.includes(dewar.id) - ); - const associatedEvent = associatedEventId - ? events.find(ev => ev.id === associatedEventId) - : null; - const associatedShift = associatedEvent?.beamtime_shift; - const associatedDate = associatedEvent?.start; - const associatedBeamline = associatedEvent?.beamline; - - const currentShift = eventDetails?.beamtime_shift; - const isAssignedToCurrentShift = associatedShift && currentShift && associatedShift === currentShift; - - return associatedEvent && ( - - ← Assigned to shift: {associatedShift} - {associatedDate && ( - <> on {new Date(associatedDate).toLocaleDateString()} - )} - {associatedBeamline && ( - <> ({associatedBeamline}) - )} - - ); - })()} + {/* Show dewar assignment info if not to this shift */} + {assocEvent && ( + + ← Assigned to: {assocShift} + {assocDate && <> on {new Date(assocDate).toLocaleDateString()}} + {assocBeamline && <> ({assocBeamline})} + + )}
  • ); })} @@ -562,4 +434,4 @@ const Calendar: React.FC = () => { ); }; -export default Calendar; +export default Calendar; \ No newline at end of file