fix(realtime): remove duplicate state updates from handlers
SSE broadcasts the authoritative state before the HTTP response returns, so local setState calls in handlers caused every entry to appear twice. Handlers now only call the API and show notifications; SSE drives all state. Added a useEffect to keep selectedBookingForDetails in sync with the SSE-updated bookings list.
This commit is contained in:
101
src/App.tsx
101
src/App.tsx
@ -195,6 +195,13 @@ export default function App() {
|
||||
return () => evtSource.close();
|
||||
}, [currentUser]);
|
||||
|
||||
// Keep the booking details modal in sync when SSE updates the bookings list.
|
||||
useEffect(() => {
|
||||
if (!selectedBookingForDetails) return;
|
||||
const fresh = bookings.find(b => b.id === selectedBookingForDetails.id);
|
||||
setSelectedBookingForDetails(fresh ?? null);
|
||||
}, [bookings]);
|
||||
|
||||
// Upcoming-booking reminder - checks every 60s, fires once per booking
|
||||
useEffect(() => {
|
||||
if (!currentUser || bookings.length === 0) return;
|
||||
@ -242,10 +249,7 @@ export default function App() {
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setBookings(prev => [data.booking, ...prev]);
|
||||
if (data.alertGenerated) setNotifications(prev => [data.alertGenerated, ...prev]);
|
||||
const logsRes = await authFetch('/api/logs');
|
||||
if (logsRes.ok) setLogs(await logsRes.json());
|
||||
}
|
||||
} catch (err) { console.error('[App] Error adding booking:', err); }
|
||||
};
|
||||
@ -258,111 +262,62 @@ export default function App() {
|
||||
body: JSON.stringify({ status: 'cancelled', operatorName: currentUser!.name }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const updated = await res.json();
|
||||
setBookings(prev => prev.map(b => b.id === bookingId ? updated : b));
|
||||
if (selectedBookingForDetails?.id === bookingId) setSelectedBookingForDetails(updated);
|
||||
const labName = booking ? (labs.find(l => l.id === booking.labId)?.name ?? 'Lab') : 'Lab';
|
||||
setNotifications(prev => [`Booking cancelled: "${labName}" - slot has been released.`, ...prev]);
|
||||
const logsRes = await authFetch('/api/logs');
|
||||
if (logsRes.ok) setLogs(await logsRes.json());
|
||||
}
|
||||
} catch (err) { console.error('[App] Error cancelling booking:', err); }
|
||||
};
|
||||
|
||||
const handleDeleteBooking = async (bookingId: string) => {
|
||||
try {
|
||||
const res = await authFetch(`/api/bookings/${bookingId}`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
setBookings(prev => prev.filter(b => b.id !== bookingId));
|
||||
if (selectedBookingForDetails?.id === bookingId) setSelectedBookingForDetails(null);
|
||||
const logsRes = await authFetch('/api/logs');
|
||||
if (logsRes.ok) setLogs(await logsRes.json());
|
||||
}
|
||||
await authFetch(`/api/bookings/${bookingId}`, { method: 'DELETE' });
|
||||
} catch (err) { console.error('[App] Error deleting booking:', err); }
|
||||
};
|
||||
|
||||
// Device handlers
|
||||
const handleAddDevice = async (newDev: Omit<Device, 'id'>) => {
|
||||
try {
|
||||
const res = await authFetch('/api/devices', { method: 'POST', body: JSON.stringify(newDev) });
|
||||
if (res.ok) {
|
||||
const created = await res.json();
|
||||
setDevices(prev => [...prev, created]);
|
||||
const logsRes = await authFetch('/api/logs');
|
||||
if (logsRes.ok) setLogs(await logsRes.json());
|
||||
}
|
||||
await authFetch('/api/devices', { method: 'POST', body: JSON.stringify(newDev) });
|
||||
} catch (err) { console.error('[App] Error adding device:', err); }
|
||||
};
|
||||
|
||||
const handleUpdateDevice = async (updatedDev: Device) => {
|
||||
try {
|
||||
const res = await authFetch(`/api/devices/${updatedDev.id}`, {
|
||||
await authFetch(`/api/devices/${updatedDev.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ ...updatedDev, operatorName: currentUser!.name }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const updated = await res.json();
|
||||
setDevices(prev => prev.map(d => d.id === updatedDev.id ? updated : d));
|
||||
const logsRes = await authFetch('/api/logs');
|
||||
if (logsRes.ok) setLogs(await logsRes.json());
|
||||
}
|
||||
} catch (err) { console.error('[App] Error updating device:', err); }
|
||||
};
|
||||
|
||||
const handleDeleteDevice = async (id: string) => {
|
||||
try {
|
||||
const res = await authFetch(`/api/devices/${id}`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
setDevices(prev => prev.filter(d => d.id !== id));
|
||||
const labsRes = await authFetch('/api/labs');
|
||||
if (labsRes.ok) setLabs(await labsRes.json());
|
||||
const logsRes = await authFetch('/api/logs');
|
||||
if (logsRes.ok) setLogs(await logsRes.json());
|
||||
}
|
||||
await authFetch(`/api/devices/${id}`, { method: 'DELETE' });
|
||||
} catch (err) { console.error('[App] Error deleting device:', err); }
|
||||
};
|
||||
|
||||
// Lab handlers
|
||||
const handleAddLab = async (newLab: Omit<LabTemplate, 'id' | 'ownerId'>) => {
|
||||
try {
|
||||
const res = await authFetch('/api/labs', { method: 'POST', body: JSON.stringify(newLab) });
|
||||
if (res.ok) {
|
||||
const created = await res.json();
|
||||
setLabs(prev => [...prev, created]);
|
||||
const logsRes = await authFetch('/api/logs');
|
||||
if (logsRes.ok) setLogs(await logsRes.json());
|
||||
}
|
||||
await authFetch('/api/labs', { method: 'POST', body: JSON.stringify(newLab) });
|
||||
} catch (err) { console.error('[App] Error adding lab:', err); }
|
||||
};
|
||||
|
||||
const handleUpdateLab = async (updatedLab: LabTemplate) => {
|
||||
try {
|
||||
const res = await authFetch(`/api/labs/${updatedLab.id}`, { method: 'PUT', body: JSON.stringify(updatedLab) });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setLabs(prev => prev.map(l => l.id === updatedLab.id ? data : l));
|
||||
const logsRes = await authFetch('/api/logs');
|
||||
if (logsRes.ok) setLogs(await logsRes.json());
|
||||
}
|
||||
await authFetch(`/api/labs/${updatedLab.id}`, { method: 'PUT', body: JSON.stringify(updatedLab) });
|
||||
} catch (err) { console.error('[App] Error updating lab:', err); }
|
||||
};
|
||||
|
||||
const handleDeleteLab = async (id: string) => {
|
||||
try {
|
||||
const res = await authFetch(`/api/labs/${id}`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
setLabs(prev => prev.filter(l => l.id !== id));
|
||||
setBookings(prev => prev.map(b => b.labId === id && b.status === 'upcoming' ? { ...b, status: 'cancelled' as const } : b));
|
||||
const logsRes = await authFetch('/api/logs');
|
||||
if (logsRes.ok) setLogs(await logsRes.json());
|
||||
}
|
||||
await authFetch(`/api/labs/${id}`, { method: 'DELETE' });
|
||||
} catch (err) { console.error('[App] Error deleting lab:', err); }
|
||||
};
|
||||
|
||||
const handleAddLogManually = async (newLogEntry: Omit<LogEntry, 'id' | 'timestamp'>) => {
|
||||
try {
|
||||
const res = await authFetch('/api/logs', { method: 'POST', body: JSON.stringify(newLogEntry) });
|
||||
if (res.ok) { const log = await res.json(); setLogs(prev => [log, ...prev]); }
|
||||
await authFetch('/api/logs', { method: 'POST', body: JSON.stringify(newLogEntry) });
|
||||
} catch (err) { console.error('[App] Error adding log:', err); }
|
||||
};
|
||||
|
||||
@ -370,18 +325,14 @@ export default function App() {
|
||||
const handleDeleteUser = async (id: string) => {
|
||||
try {
|
||||
const res = await authFetch(`/api/users/${id}`, { method: 'DELETE' });
|
||||
if (res.ok) setUsers(prev => prev.filter(u => u.id !== id));
|
||||
else { const d = await res.json(); throw new Error(d.error); }
|
||||
if (!res.ok) { const d = await res.json(); throw new Error(d.error); }
|
||||
} catch (err: any) { throw err; }
|
||||
};
|
||||
|
||||
const handleUpdateUser = async (id: string, name: string, email: string) => {
|
||||
try {
|
||||
const res = await authFetch(`/api/users/${id}`, { method: 'PUT', body: JSON.stringify({ name, email }) });
|
||||
if (res.ok) {
|
||||
const updated = await res.json();
|
||||
setUsers(prev => prev.map(u => u.id === id ? updated : u));
|
||||
} else { const d = await res.json(); throw new Error(d.error); }
|
||||
if (!res.ok) { const d = await res.json(); throw new Error(d.error); }
|
||||
} catch (err: any) { throw err; }
|
||||
};
|
||||
|
||||
@ -390,7 +341,8 @@ export default function App() {
|
||||
const res = await authFetch(`/api/users/${id}/role`, { method: 'PATCH', body: JSON.stringify({ role }) });
|
||||
if (res.ok) {
|
||||
const updated: User = await res.json();
|
||||
setUsers(prev => prev.map(u => u.id === id ? updated : u));
|
||||
// Update currentUser immediately if the acting user changed their own role,
|
||||
// since currentUser is not driven by the users SSE event.
|
||||
if (updated.id === currentUser?.id) setCurrentUser(updated);
|
||||
} else { const d = await res.json(); throw new Error(d.error); }
|
||||
} catch (err: any) { throw err; }
|
||||
@ -399,28 +351,19 @@ export default function App() {
|
||||
// Quick-link handlers (shared link dashboard)
|
||||
const handleAddLink = async (newLink: Omit<QuickLink, 'id' | 'createdAt' | 'createdBy'>) => {
|
||||
try {
|
||||
const res = await authFetch('/api/links', { method: 'POST', body: JSON.stringify(newLink) });
|
||||
if (res.ok) {
|
||||
const created = await res.json();
|
||||
setLinks(prev => [...prev, created]);
|
||||
}
|
||||
await authFetch('/api/links', { method: 'POST', body: JSON.stringify(newLink) });
|
||||
} catch (err) { console.error('[App] Error adding link:', err); }
|
||||
};
|
||||
|
||||
const handleUpdateLink = async (updated: QuickLink) => {
|
||||
try {
|
||||
const res = await authFetch(`/api/links/${updated.id}`, { method: 'PUT', body: JSON.stringify(updated) });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setLinks(prev => prev.map(l => l.id === updated.id ? data : l));
|
||||
}
|
||||
await authFetch(`/api/links/${updated.id}`, { method: 'PUT', body: JSON.stringify(updated) });
|
||||
} catch (err) { console.error('[App] Error updating link:', err); }
|
||||
};
|
||||
|
||||
const handleDeleteLink = async (id: string) => {
|
||||
try {
|
||||
const res = await authFetch(`/api/links/${id}`, { method: 'DELETE' });
|
||||
if (res.ok) setLinks(prev => prev.filter(l => l.id !== id));
|
||||
await authFetch(`/api/links/${id}`, { method: 'DELETE' });
|
||||
} catch (err) { console.error('[App] Error deleting link:', err); }
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user