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 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: for puck in pucks:
dewar_id = puck.dewar_id # Assuming puck has dewar_id dewar_id = puck.dewar_id # Assuming puck has dewar_id
assigned_beamtime = dewar_to_beamtime[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) positions_with_samples = random.randint(1, 16)
occupied_positions = random.sample(range(1, 17), positions_with_samples) 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}", sample_name=f"Sample{sample_id_counter:03}",
position=pos, position=pos,
puck_id=puck.id, puck_id=puck.id,
beamtime_id=assigned_beamtime, # IMPORTANT: Use the dewar's beamtime beamtime_id=assigned_beamtime,
) )
samples.append(sample) samples.append(sample)
sample_id_counter += 1 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: except SQLAlchemyError as e:
logging.error(f"Database error occurred: {e}") logging.error(f"Database error occurred: {e}")
raise HTTPException(status_code=500, detail="Internal server error") 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 sample.beamtime_id = None if beamtime_id == 0 else beamtime_id
db.commit() db.commit()
return {"status": "success", "puck_id": puck.id, "beamtime_id": beamtime_id} 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 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 '../styles/Calendar.css';
import { BeamtimesService, DewarsService, PucksService } from '../../openapi'; import { BeamtimesService, DewarsService, PucksService } from '../../openapi';
// Define colors for each beamline
const beamlineColors: { [key: string]: string } = { const beamlineColors: { [key: string]: string } = {
X06SA: '#FF5733', X06SA: '#FF5733',
X10SA: '#33FF57', X10SA: '#33FF57',
X06DA: '#3357FF', X06DA: '#3357FF',
Unknown: '#CCCCCC', // Gray color for unknown beamlines Unknown: '#CCCCCC',
}; };
// Custom event interface
interface CustomEvent extends EventInput { interface CustomEvent extends EventInput {
beamline: string; beamline: string;
beamtime_shift: 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']; const experimentModes = ['SDU-Scheduled', 'SDU-queued', 'Remote', 'In-person'];
// Utility function to darken a hex color
const darkenColor = (color: string, percent: number): string => { const darkenColor = (color: string, percent: number): string => {
const num = parseInt(color.slice(1), 16); // Convert hex to number const num = parseInt(color.slice(1), 16);
const amt = Math.round(2.55 * percent); // Calculate amount to darken const amt = Math.round(2.55 * percent);
const r = (num >> 16) + amt; // Red const r = (num >> 16) + amt;
const g = (num >> 8 & 0x00FF) + amt; // Green const g = (num >> 8 & 0x00FF) + amt;
const b = (num & 0x0000FF) + amt; // Blue const b = (num & 0x0000FF) + amt;
const newColor = (0x1000000 + (r < 255 ? (r < 0 ? 0 : r) : 255) * 0x10000
// Ensure values stay within 0-255 range + (g < 255 ? (g < 0 ? 0 : g) : 255) * 0x100
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); + (b < 255 ? (b < 0 ? 0 : b) : 255)).toString(16).slice(1);
return `#${newColor}`; return `#${newColor}`;
}; };
@ -43,7 +40,8 @@ const Calendar: React.FC = () => {
const [fetchError, setFetchError] = useState<string | null>(null); const [fetchError, setFetchError] = useState<string | null>(null);
const [selectedEventId, setSelectedEventId] = useState<string | null>(null); const [selectedEventId, setSelectedEventId] = useState<string | null>(null);
const [eventDetails, setEventDetails] = useState<CustomEvent | 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({ const [userDetails, setUserDetails] = useState({
name: '', name: '',
firstName: '', firstName: '',
@ -52,103 +50,94 @@ const Calendar: React.FC = () => {
extAccount: '', extAccount: '',
experimentMode: experimentModes[0], experimentMode: experimentModes[0],
}); });
const [shipments, setShipments] = useState<any[]>([]); // State for shipments const [shipments, setShipments] = useState<any[]>([]);
const [selectedDewars, setSelectedDewars] = useState<string[]>([]); // Track selected dewars for the experiment
const fetchBeamtimes = async () => { // Load all beamtime events AND their current associations (on mount)
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
useEffect(() => { 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(() => { useEffect(() => {
// Only fetch if you want to update dewars when eventDetails change,
// or run only on initial mount by passing [] as deps
if (eventDetails) { if (eventDetails) {
const fetchDewars = async () => { const fetchDewars = async () => {
try { try {
// This name depends on your codegen's operationId / function name! const dewarsWithPucks = await DewarsService.getRecentDewarsWithPucks();
const dewars = await DewarsService.getRecentDewarsWithPucks(); setShipments(dewarsWithPucks);
setShipments(dewars);
} catch (err) { } catch (err) {
// Optionally handle error setShipments([]);
console.error("Failed to fetch dewars with pucks", err);
} }
}; };
fetchDewars(); fetchDewars();
} else {
setShipments([]);
} }
}, [eventDetails]); // Fetch dewars when an event is selected }, [eventDetails]);
// Refresh associations after (un)assign action
//const fetchShipments = async () => { const refetchEventAssociations = async (beamtimeId: number, eventId: string) => {
// try { const [dewars, pucks] = await Promise.all([
// const response = await fetch('/shipmentdb.json'); DewarsService.getDewarsByBeamtime(beamtimeId),
// PucksService.getPucksByBeamtime(beamtimeId),
// // Check for HTTP errors ]);
// if (!response.ok) { setEventAssociations(prev => ({
// throw new Error(`HTTP error! status: ${response.status}`); ...prev,
// } [eventId]: {
// dewars: dewars.map((d: any) => d.id),
// // Parse the JSON response pucks: pucks.map((p: any) => p.id),
// 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();
//}, []);
const handleEventClick = (eventInfo: any) => { const handleEventClick = (eventInfo: any) => {
const clickedEventId = eventInfo.event.id; const clickedEventId = eventInfo.event.id;
setSelectedEventId(clickedEventId); setSelectedEventId(clickedEventId);
const selected = events.find(event => event.id === clickedEventId) || null;
const selectedEvent = events.find(event => event.id === clickedEventId) || null; setEventDetails(selected);
setEventDetails(selectedEvent);
}; };
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
@ -159,23 +148,15 @@ const Calendar: React.FC = () => {
})); }));
}; };
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (eventDetails) { if (eventDetails) {
const updatedEvents = events.map(event => setEvents(prev =>
event.id === eventDetails.id prev.map(event =>
? { ...event, isSubmitted: true, selectedDewars } // Associate selected dewars event.id === eventDetails.id ? { ...event, isSubmitted: true } : event
: event )
); );
setEvents(updatedEvents);
} }
console.log('User Details:', userDetails);
console.log('Selected Dewars:', selectedDewars);
// Reset user details and selected dewars after submission
setUserDetails({ setUserDetails({
name: '', name: '',
firstName: '', firstName: '',
@ -184,143 +165,48 @@ const Calendar: React.FC = () => {
extAccount: '', extAccount: '',
experimentMode: experimentModes[0], experimentMode: experimentModes[0],
}); });
setSelectedDewars([]); // Reset selected dewars
};
const assignDewarToBeamtime = async (dewarId: number, beamtimeId: number) => {
return DewarsService.assignDewarToBeamtime(dewarId, beamtimeId);
}; };
const unassignDewarFromBeamtime = async (dewarId: number) => { // Unified assign/unassign for Dewars
// Pass "0" as the special value (if that's how you unassign) const handleDewarAssignment = async (dewarId: string) => {
return DewarsService.assignDewarToBeamtime(dewarId, 0); // or whatever your backend supports if (!selectedEventId) return;
}; const event = events.find(e => e.id === selectedEventId)!;
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;
const beamtimeId = event.beamtime_id; const beamtimeId = event.beamtime_id;
if (beamtimeId == null) { if (!beamtimeId) return;
console.error("No numeric beamtime_id found for selected event:", event); // Is this dewar already assigned here?
return; 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! // Unified assign/unassign for Pucks
const prev = eventAssociations[selectedEventId] || { dewars: [], pucks: [] }; const handlePuckAssignment = async (puckId: string) => {
const isAssigned = prev.dewars.includes(dewarId); if (!selectedEventId) return;
const event = events.find(e => e.id === selectedEventId)!;
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;
const beamtimeId = event.beamtime_id; const beamtimeId = event.beamtime_id;
if (beamtimeId == null) { if (!beamtimeId) return;
console.error("No numeric beamtime_id found for selected event:", event); const assigned = eventAssociations[selectedEventId]?.pucks.includes(puckId);
return; 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 // For displaying badge in calendar and UI
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;
}
}
const eventContent = (eventInfo: any) => { const eventContent = (eventInfo: any) => {
const beamline = eventInfo.event.extendedProps.beamline || 'Unknown'; const beamline = eventInfo.event.extendedProps.beamline || 'Unknown';
const isSelected = selectedEventId === eventInfo.event.id; const isSelected = selectedEventId === eventInfo.event.id;
@ -328,8 +214,6 @@ const Calendar: React.FC = () => {
const hasAssociations = const hasAssociations =
eventAssociations[eventInfo.event.id]?.dewars.length > 0 || eventAssociations[eventInfo.event.id]?.dewars.length > 0 ||
eventAssociations[eventInfo.event.id]?.pucks.length > 0; eventAssociations[eventInfo.event.id]?.pucks.length > 0;
const backgroundColor = isSubmitted const backgroundColor = isSubmitted
? darkenColor(beamlineColors[beamline] || beamlineColors.Unknown, -20) ? darkenColor(beamlineColors[beamline] || beamlineColors.Unknown, -20)
: isSelected : 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 ( return (
<div className="calendar-container"> <div className="calendar-container">
<h2>Beamline Calendar</h2> <h2>Beamline Calendar</h2>
@ -385,9 +281,20 @@ const Calendar: React.FC = () => {
<h4>Select Dewars</h4> <h4>Select Dewars</h4>
<ul> <ul>
{shipments.map(dewar => { {shipments.map(dewar => {
const thisEvent = eventAssociations[selectedEventId] || { dewars: [], pucks: [] }; const thisEvent = eventAssociations[selectedEventId!] || { dewars: [], pucks: [] };
const dewarAssigned = thisEvent.dewars.includes(dewar.id); 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 ( return (
<li key={dewar.id}> <li key={dewar.id}>
<label> <label>
@ -398,92 +305,57 @@ const Calendar: React.FC = () => {
/> />
<b>{dewar.dewar_name}</b> <b>{dewar.dewar_name}</b>
</label> </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> <ul>
{(dewar.pucks || []).map(puck => { {dewar.pucks.map(puck => {
// Find eventId for this puck, if assigned const [pAssocEventId] = getAssignedEventForPuck(puck.id) || [];
const associatedEventId = Object.keys(eventAssociations).find(eid => const pAssocEvent = pAssocEventId
eventAssociations[eid]?.pucks.includes(puck.id) ? events.find(ev => ev.id === pAssocEventId)
);
const associatedEvent = associatedEventId
? events.find(ev => ev.id === associatedEventId)
: null; : null;
const pAssocShift = pAssocEvent?.beamtime_shift;
const associatedShift = associatedEvent?.beamtime_shift; const pAssocDate = pAssocEvent?.start;
const associatedDate = associatedEvent?.start; const pAssocBeamline = pAssocEvent?.beamline;
const associatedBeamline = associatedEvent?.beamline; const isAssignedHere = pAssocShift && currentShift && pAssocShift === currentShift;
const currentShift = eventDetails?.beamtime_shift;
const isAssignedToCurrentShift = associatedShift && currentShift && associatedShift === currentShift;
return ( return (
<div key={puck.id} style={{ display: 'flex', alignItems: 'center' }}> <li key={puck.id} style={{marginLeft:8}}>
<span>{puck.name}</span>
<button <button
type="button" type="button"
style={{ style={{
marginLeft: 8, background: isAssignedHere ? '#4CAF50' : (pAssocShift ? '#B3E5B3' : '#e0e0e0'),
background: isAssignedToCurrentShift ? '#4CAF50' : (associatedShift ? '#B3E5B3' : '#e0e0e0'), color: isAssignedHere ? 'white' : 'black',
color: isAssignedToCurrentShift ? 'white' : 'black', border: isAssignedHere ? '1px solid #388e3c' : '1px solid #bdbdbd',
border: isAssignedToCurrentShift ? '1px solid #388e3c' : '1px solid #bdbdbd',
borderRadius: 4, borderRadius: 4,
padding: '4px 10px', padding: '2px 10px',
cursor: 'pointer', cursor: 'pointer',
transition: 'background 0.2s', transition: 'background 0.2s',
}} }}
onClick={() => handlePuckAssignment(puck.id)} onClick={() => handlePuckAssignment(puck.id)}
> >
{puck.puck_name} {puck.puck_name || puck.name}
</button> </button>
{associatedEvent && ( {pAssocEvent && (
<span <span style={{
style={{ marginLeft: 8,
marginLeft: 8, color: isAssignedHere ? 'green' : '#388e3c',
color: isAssignedToCurrentShift ? 'green' : '#388e3c', fontWeight: isAssignedHere ? 700 : 400
fontWeight: isAssignedToCurrentShift ? 700 : 400 }}>
}} Assigned to: {pAssocShift} {pAssocDate && <>on {new Date(pAssocDate).toLocaleDateString()}</>} {pAssocBeamline && <>({pAssocBeamline})</>}
> </span>
Assigned to shift: {associatedShift}
{associatedDate && (
<> on {new Date(associatedDate).toLocaleDateString()}</>
)}
{associatedBeamline && (
<> ({associatedBeamline})</>
)}
</span>
)} )}
</div> </li>
); );
})} })}
</ul> </ul>
)} )}
{/* Show associated shift, date, and beamline for Dewar, if any */} {/* Show dewar assignment info if not to this shift */}
{(() => { {assocEvent && (
const associatedEventId = Object.keys(eventAssociations).find(eid => <span style={{marginLeft:8, color:isAssignedToThis?'green':'#388e3c', fontWeight:isAssignedToThis?700:400}}>
eventAssociations[eid]?.dewars.includes(dewar.id) Assigned to: {assocShift}
); {assocDate && <> on {new Date(assocDate).toLocaleDateString()}</>}
const associatedEvent = associatedEventId {assocBeamline && <> ({assocBeamline})</>}
? events.find(ev => ev.id === associatedEventId) </span>
: 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>
);
})()}
</li> </li>
); );
})} })}