Add endpoints and logic for fetching associations by beamtime

Introduced endpoints to fetch pucks, dewars, and samples by beamtime ID. Updated backend logic to ensure consistency between dewars, pucks, and samples assignments. Enhanced frontend to display and handle beamline-specific associations dynamically.
This commit is contained in:
GotthardG 2025-05-07 11:19:00 +02:00
parent e341459590
commit 0fa038be94
5 changed files with 217 additions and 308 deletions

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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<string | null>(null);
const [selectedEventId, setSelectedEventId] = useState<string | null>(null);
const [eventDetails, setEventDetails] = useState<CustomEvent | null>(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<any[]>([]); // State for shipments
const [selectedDewars, setSelectedDewars] = useState<string[]>([]); // Track selected dewars for the experiment
const [shipments, setShipments] = useState<any[]>([]);
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<HTMLInputElement | HTMLSelectElement>) => {
@ -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 (
<div className="calendar-container">
<h2>Beamline Calendar</h2>
@ -385,9 +281,20 @@ const Calendar: React.FC = () => {
<h4>Select Dewars</h4>
<ul>
{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 (
<li key={dewar.id}>
<label>
@ -398,92 +305,57 @@ const Calendar: React.FC = () => {
/>
<b>{dewar.dewar_name}</b>
</label>
{!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 && (
<ul>
{(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 (
<div key={puck.id} style={{ display: 'flex', alignItems: 'center' }}>
<span>{puck.name}</span>
<li key={puck.id} style={{marginLeft:8}}>
<button
type="button"
style={{
marginLeft: 8,
background: isAssignedToCurrentShift ? '#4CAF50' : (associatedShift ? '#B3E5B3' : '#e0e0e0'),
color: isAssignedToCurrentShift ? 'white' : 'black',
border: isAssignedToCurrentShift ? '1px solid #388e3c' : '1px solid #bdbdbd',
background: isAssignedHere ? '#4CAF50' : (pAssocShift ? '#B3E5B3' : '#e0e0e0'),
color: isAssignedHere ? 'white' : 'black',
border: isAssignedHere ? '1px solid #388e3c' : '1px solid #bdbdbd',
borderRadius: 4,
padding: '4px 10px',
padding: '2px 10px',
cursor: 'pointer',
transition: 'background 0.2s',
}}
onClick={() => handlePuckAssignment(puck.id)}
>
{puck.puck_name}
{puck.puck_name || puck.name}
</button>
{associatedEvent && (
<span
style={{
marginLeft: 8,
color: isAssignedToCurrentShift ? 'green' : '#388e3c',
fontWeight: isAssignedToCurrentShift ? 700 : 400
}}
>
Assigned to shift: {associatedShift}
{associatedDate && (
<> on {new Date(associatedDate).toLocaleDateString()}</>
)}
{associatedBeamline && (
<> ({associatedBeamline})</>
)}
</span>
{pAssocEvent && (
<span style={{
marginLeft: 8,
color: isAssignedHere ? 'green' : '#388e3c',
fontWeight: isAssignedHere ? 700 : 400
}}>
Assigned to: {pAssocShift} {pAssocDate && <>on {new Date(pAssocDate).toLocaleDateString()}</>} {pAssocBeamline && <>({pAssocBeamline})</>}
</span>
)}
</div>
</li>
);
})}
</ul>
)}
{/* 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 && (
<span style={{ marginLeft: 8, color: isAssignedToCurrentShift ? 'green' : '#388e3c', fontWeight: isAssignedToCurrentShift ? 700 : 400 }}>
Assigned to shift: {associatedShift}
{associatedDate && (
<> on {new Date(associatedDate).toLocaleDateString()}</>
)}
{associatedBeamline && (
<> ({associatedBeamline})</>
)}
</span>
);
})()}
{/* Show dewar assignment info if not to this shift */}
{assocEvent && (
<span style={{marginLeft:8, color:isAssignedToThis?'green':'#388e3c', fontWeight:isAssignedToThis?700:400}}>
Assigned to: {assocShift}
{assocDate && <> on {new Date(assocDate).toLocaleDateString()}</>}
{assocBeamline && <> ({assocBeamline})</>}
</span>
)}
</li>
);
})}
@ -562,4 +434,4 @@ const Calendar: React.FC = () => {
);
};
export default Calendar;
export default Calendar;