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();
|
return () => evtSource.close();
|
||||||
}, [currentUser]);
|
}, [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
|
// Upcoming-booking reminder - checks every 60s, fires once per booking
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentUser || bookings.length === 0) return;
|
if (!currentUser || bookings.length === 0) return;
|
||||||
@ -242,10 +249,7 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setBookings(prev => [data.booking, ...prev]);
|
|
||||||
if (data.alertGenerated) setNotifications(prev => [data.alertGenerated, ...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); }
|
} 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 }),
|
body: JSON.stringify({ status: 'cancelled', operatorName: currentUser!.name }),
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
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';
|
const labName = booking ? (labs.find(l => l.id === booking.labId)?.name ?? 'Lab') : 'Lab';
|
||||||
setNotifications(prev => [`Booking cancelled: "${labName}" - slot has been released.`, ...prev]);
|
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); }
|
} catch (err) { console.error('[App] Error cancelling booking:', err); }
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteBooking = async (bookingId: string) => {
|
const handleDeleteBooking = async (bookingId: string) => {
|
||||||
try {
|
try {
|
||||||
const res = await authFetch(`/api/bookings/${bookingId}`, { method: 'DELETE' });
|
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());
|
|
||||||
}
|
|
||||||
} catch (err) { console.error('[App] Error deleting booking:', err); }
|
} catch (err) { console.error('[App] Error deleting booking:', err); }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Device handlers
|
// Device handlers
|
||||||
const handleAddDevice = async (newDev: Omit<Device, 'id'>) => {
|
const handleAddDevice = async (newDev: Omit<Device, 'id'>) => {
|
||||||
try {
|
try {
|
||||||
const res = await authFetch('/api/devices', { method: 'POST', body: JSON.stringify(newDev) });
|
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());
|
|
||||||
}
|
|
||||||
} catch (err) { console.error('[App] Error adding device:', err); }
|
} catch (err) { console.error('[App] Error adding device:', err); }
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateDevice = async (updatedDev: Device) => {
|
const handleUpdateDevice = async (updatedDev: Device) => {
|
||||||
try {
|
try {
|
||||||
const res = await authFetch(`/api/devices/${updatedDev.id}`, {
|
await authFetch(`/api/devices/${updatedDev.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({ ...updatedDev, operatorName: currentUser!.name }),
|
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); }
|
} catch (err) { console.error('[App] Error updating device:', err); }
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteDevice = async (id: string) => {
|
const handleDeleteDevice = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
const res = await authFetch(`/api/devices/${id}`, { method: 'DELETE' });
|
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());
|
|
||||||
}
|
|
||||||
} catch (err) { console.error('[App] Error deleting device:', err); }
|
} catch (err) { console.error('[App] Error deleting device:', err); }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Lab handlers
|
// Lab handlers
|
||||||
const handleAddLab = async (newLab: Omit<LabTemplate, 'id' | 'ownerId'>) => {
|
const handleAddLab = async (newLab: Omit<LabTemplate, 'id' | 'ownerId'>) => {
|
||||||
try {
|
try {
|
||||||
const res = await authFetch('/api/labs', { method: 'POST', body: JSON.stringify(newLab) });
|
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());
|
|
||||||
}
|
|
||||||
} catch (err) { console.error('[App] Error adding lab:', err); }
|
} catch (err) { console.error('[App] Error adding lab:', err); }
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateLab = async (updatedLab: LabTemplate) => {
|
const handleUpdateLab = async (updatedLab: LabTemplate) => {
|
||||||
try {
|
try {
|
||||||
const res = await authFetch(`/api/labs/${updatedLab.id}`, { method: 'PUT', body: JSON.stringify(updatedLab) });
|
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());
|
|
||||||
}
|
|
||||||
} catch (err) { console.error('[App] Error updating lab:', err); }
|
} catch (err) { console.error('[App] Error updating lab:', err); }
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteLab = async (id: string) => {
|
const handleDeleteLab = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
const res = await authFetch(`/api/labs/${id}`, { method: 'DELETE' });
|
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());
|
|
||||||
}
|
|
||||||
} catch (err) { console.error('[App] Error deleting lab:', err); }
|
} catch (err) { console.error('[App] Error deleting lab:', err); }
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddLogManually = async (newLogEntry: Omit<LogEntry, 'id' | 'timestamp'>) => {
|
const handleAddLogManually = async (newLogEntry: Omit<LogEntry, 'id' | 'timestamp'>) => {
|
||||||
try {
|
try {
|
||||||
const res = await authFetch('/api/logs', { method: 'POST', body: JSON.stringify(newLogEntry) });
|
await authFetch('/api/logs', { method: 'POST', body: JSON.stringify(newLogEntry) });
|
||||||
if (res.ok) { const log = await res.json(); setLogs(prev => [log, ...prev]); }
|
|
||||||
} catch (err) { console.error('[App] Error adding log:', err); }
|
} catch (err) { console.error('[App] Error adding log:', err); }
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -370,18 +325,14 @@ export default function App() {
|
|||||||
const handleDeleteUser = async (id: string) => {
|
const handleDeleteUser = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
const res = await authFetch(`/api/users/${id}`, { method: 'DELETE' });
|
const res = await authFetch(`/api/users/${id}`, { method: 'DELETE' });
|
||||||
if (res.ok) setUsers(prev => prev.filter(u => u.id !== id));
|
if (!res.ok) { const d = await res.json(); throw new Error(d.error); }
|
||||||
else { const d = await res.json(); throw new Error(d.error); }
|
|
||||||
} catch (err: any) { throw err; }
|
} catch (err: any) { throw err; }
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateUser = async (id: string, name: string, email: string) => {
|
const handleUpdateUser = async (id: string, name: string, email: string) => {
|
||||||
try {
|
try {
|
||||||
const res = await authFetch(`/api/users/${id}`, { method: 'PUT', body: JSON.stringify({ name, email }) });
|
const res = await authFetch(`/api/users/${id}`, { method: 'PUT', body: JSON.stringify({ name, email }) });
|
||||||
if (res.ok) {
|
if (!res.ok) { const d = await res.json(); throw new Error(d.error); }
|
||||||
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); }
|
|
||||||
} catch (err: any) { throw err; }
|
} 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 }) });
|
const res = await authFetch(`/api/users/${id}/role`, { method: 'PATCH', body: JSON.stringify({ role }) });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const updated: User = await res.json();
|
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);
|
if (updated.id === currentUser?.id) setCurrentUser(updated);
|
||||||
} else { const d = await res.json(); throw new Error(d.error); }
|
} else { const d = await res.json(); throw new Error(d.error); }
|
||||||
} catch (err: any) { throw err; }
|
} catch (err: any) { throw err; }
|
||||||
@ -399,28 +351,19 @@ export default function App() {
|
|||||||
// Quick-link handlers (shared link dashboard)
|
// Quick-link handlers (shared link dashboard)
|
||||||
const handleAddLink = async (newLink: Omit<QuickLink, 'id' | 'createdAt' | 'createdBy'>) => {
|
const handleAddLink = async (newLink: Omit<QuickLink, 'id' | 'createdAt' | 'createdBy'>) => {
|
||||||
try {
|
try {
|
||||||
const res = await authFetch('/api/links', { method: 'POST', body: JSON.stringify(newLink) });
|
await authFetch('/api/links', { method: 'POST', body: JSON.stringify(newLink) });
|
||||||
if (res.ok) {
|
|
||||||
const created = await res.json();
|
|
||||||
setLinks(prev => [...prev, created]);
|
|
||||||
}
|
|
||||||
} catch (err) { console.error('[App] Error adding link:', err); }
|
} catch (err) { console.error('[App] Error adding link:', err); }
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateLink = async (updated: QuickLink) => {
|
const handleUpdateLink = async (updated: QuickLink) => {
|
||||||
try {
|
try {
|
||||||
const res = await authFetch(`/api/links/${updated.id}`, { method: 'PUT', body: JSON.stringify(updated) });
|
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));
|
|
||||||
}
|
|
||||||
} catch (err) { console.error('[App] Error updating link:', err); }
|
} catch (err) { console.error('[App] Error updating link:', err); }
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteLink = async (id: string) => {
|
const handleDeleteLink = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
const res = await authFetch(`/api/links/${id}`, { method: 'DELETE' });
|
await authFetch(`/api/links/${id}`, { method: 'DELETE' });
|
||||||
if (res.ok) setLinks(prev => prev.filter(l => l.id !== id));
|
|
||||||
} catch (err) { console.error('[App] Error deleting link:', err); }
|
} catch (err) { console.error('[App] Error deleting link:', err); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user