Improve real-time space status handling and date-based resets

- Add logic to clear real-time updates when the selected date changes.
- Enhance space status updates to consider reservation time ranges for relevance.
- Update relevant Svelte components (`SpaceCard`, `FloorplanViewer`) to handle date-sensitive real-time updates.
- Update API to include reservation details in `space_status_changed` events for improved update precision.
main
Pau Costa Ferrer 2026-03-28 18:12:17 +01:00
parent 93bd2808ac
commit fa93fc0ac6
4 changed files with 90 additions and 31 deletions

View File

@ -4,10 +4,8 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="6c8b3c54-eabb-40e5-967f-c7b594c750bc" name="Changes" comment="Add i18n integration and translations for improved localization&#10;&#10;- Extend English (`en.json`) and Spanish (`es-ES.json`) locale files with additional keys for buildings, bookings, reports, and user management. &#10;- Update Svelte components and pages to use `svelte-i18n` for dynamic translations across the app. &#10;- Refactor labels, titles, and error messages for multilingual support. &#10;- Improve date formatting based on locale preferences."> <list default="true" id="6c8b3c54-eabb-40e5-967f-c7b594c750bc" name="Changes" comment="Add seed service to initialize default admin and organization&#10;&#10;- Implement `SeedService` to create a default admin account and organization on app startup.&#10;- Update `PrismaModule` to include `SeedService` as a provider.&#10;- Remove seeded credentials display from login page.&#10;- Refactor existing seed script for consistency with the new service.">
<change beforePath="$PROJECT_DIR$/apps/api/prisma/seed.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/prisma/seed.ts" afterDir="false" /> <change beforePath="$PROJECT_DIR$/apps/web/src/lib/components/FloorplanViewer.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/lib/components/FloorplanViewer.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/api/src/prisma/prisma.module.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/prisma/prisma.module.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/web/src/routes/login/+page.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/routes/login/+page.svelte" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -129,7 +127,7 @@
<updated>1774643312599</updated> <updated>1774643312599</updated>
<workItem from="1774643313611" duration="74000" /> <workItem from="1774643313611" duration="74000" />
<workItem from="1774643422240" duration="5278000" /> <workItem from="1774643422240" duration="5278000" />
<workItem from="1774706361502" duration="11023000" /> <workItem from="1774706361502" duration="11346000" />
</task> </task>
<task id="LOCAL-00001" summary="Initialize Hot Desking app with web and API services &#10;&#10;- Set up SvelteKit for the web interface with Tailwind CSS. &#10;- Build the API using NestJS with Prisma ORM for database interaction. &#10;- Add environment variable management and docker-compose for PostgreSQL and PgAdmin instances. &#10;- Create shared data models for consistent typing across services. &#10;- Establish basic routing and layouts for web and API services."> <task id="LOCAL-00001" summary="Initialize Hot Desking app with web and API services &#10;&#10;- Set up SvelteKit for the web interface with Tailwind CSS. &#10;- Build the API using NestJS with Prisma ORM for database interaction. &#10;- Add environment variable management and docker-compose for PostgreSQL and PgAdmin instances. &#10;- Create shared data models for consistent typing across services. &#10;- Establish basic routing and layouts for web and API services.">
<option name="closed" value="true" /> <option name="closed" value="true" />
@ -299,7 +297,15 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1774717104130</updated> <updated>1774717104130</updated>
</task> </task>
<option name="localTasksCounter" value="22" /> <task id="LOCAL-00022" summary="Add seed service to initialize default admin and organization&#10;&#10;- Implement `SeedService` to create a default admin account and organization on app startup.&#10;- Update `PrismaModule` to include `SeedService` as a provider.&#10;- Remove seeded credentials display from login page.&#10;- Refactor existing seed script for consistency with the new service.">
<option name="closed" value="true" />
<created>1774717642991</created>
<option name="number" value="00022" />
<option name="presentableId" value="LOCAL-00022" />
<option name="project" value="LOCAL" />
<updated>1774717642991</updated>
</task>
<option name="localTasksCounter" value="23" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@ -329,6 +335,7 @@
<MESSAGE value="Add Docker deployment support for API and Web services&#10;&#10;- Add Dockerfiles for API and Web apps with multi-stage builds.&#10;- Configure `docker-compose.yml` to manage services (Postgres, API, Web, pgAdmin).&#10;- Add `.dockerignore` for optimized builds.&#10;- Include `DOCKER_SETUP.md` for deployment steps and guidance." /> <MESSAGE value="Add Docker deployment support for API and Web services&#10;&#10;- Add Dockerfiles for API and Web apps with multi-stage builds.&#10;- Configure `docker-compose.yml` to manage services (Postgres, API, Web, pgAdmin).&#10;- Add `.dockerignore` for optimized builds.&#10;- Include `DOCKER_SETUP.md` for deployment steps and guidance." />
<MESSAGE value="Add rotation support for spaces in floorplan editor &#10;&#10;- Extend `Space` model and Prisma schema to include `rotation` property. &#10;- Update API services and DTOs to handle rotation data. &#10;- Add UI controls in `FloorplanEditor.svelte` for rotating spaces. &#10;- Refactor space rendering logic in both editor and viewer to support rotation." /> <MESSAGE value="Add rotation support for spaces in floorplan editor &#10;&#10;- Extend `Space` model and Prisma schema to include `rotation` property. &#10;- Update API services and DTOs to handle rotation data. &#10;- Add UI controls in `FloorplanEditor.svelte` for rotating spaces. &#10;- Refactor space rendering logic in both editor and viewer to support rotation." />
<MESSAGE value="Add i18n integration and translations for improved localization&#10;&#10;- Extend English (`en.json`) and Spanish (`es-ES.json`) locale files with additional keys for buildings, bookings, reports, and user management. &#10;- Update Svelte components and pages to use `svelte-i18n` for dynamic translations across the app. &#10;- Refactor labels, titles, and error messages for multilingual support. &#10;- Improve date formatting based on locale preferences." /> <MESSAGE value="Add i18n integration and translations for improved localization&#10;&#10;- Extend English (`en.json`) and Spanish (`es-ES.json`) locale files with additional keys for buildings, bookings, reports, and user management. &#10;- Update Svelte components and pages to use `svelte-i18n` for dynamic translations across the app. &#10;- Refactor labels, titles, and error messages for multilingual support. &#10;- Improve date formatting based on locale preferences." />
<option name="LAST_COMMIT_MESSAGE" value="Add i18n integration and translations for improved localization&#10;&#10;- Extend English (`en.json`) and Spanish (`es-ES.json`) locale files with additional keys for buildings, bookings, reports, and user management. &#10;- Update Svelte components and pages to use `svelte-i18n` for dynamic translations across the app. &#10;- Refactor labels, titles, and error messages for multilingual support. &#10;- Improve date formatting based on locale preferences." /> <MESSAGE value="Add seed service to initialize default admin and organization&#10;&#10;- Implement `SeedService` to create a default admin account and organization on app startup.&#10;- Update `PrismaModule` to include `SeedService` as a provider.&#10;- Remove seeded credentials display from login page.&#10;- Refactor existing seed script for consistency with the new service." />
<option name="LAST_COMMIT_MESSAGE" value="Add seed service to initialize default admin and organization&#10;&#10;- Implement `SeedService` to create a default admin account and organization on app startup.&#10;- Update `PrismaModule` to include `SeedService` as a provider.&#10;- Remove seeded credentials display from login page.&#10;- Refactor existing seed script for consistency with the new service." />
</component> </component>
</project> </project>

View File

@ -137,6 +137,13 @@ export class ReservationsService {
reservation.space.floorId, reservation.space.floorId,
reservation.spaceId, reservation.spaceId,
false, false,
{
reservation: {
id: reservation.id,
startTime: reservation.startTime,
endTime: reservation.endTime,
}
}
); );
console.log('Emitted space_status_changed for cancelled reservation', reservation.space.floorId, reservation.spaceId); console.log('Emitted space_status_changed for cancelled reservation', reservation.space.floorId, reservation.spaceId);

View File

@ -21,23 +21,35 @@
// Track if we have received any real-time updates to avoid overwriting them with stale initial status // Track if we have received any real-time updates to avoid overwriting them with stale initial status
let realtimeUpdates: Record<string, any> = {}; let realtimeUpdates: Record<string, any> = {};
let lastSelectedDateString = selectedDate.toDateString();
$: isToday = new Date(selectedDate.getTime()).setHours(0,0,0,0) === new Date().setHours(0,0,0,0);
$: { $: {
// Initialize space status from the current state (e.g. from API) // Detect when the actual day changes to clear real-time updates
const currentDateString = selectedDate.toDateString();
if (currentDateString !== lastSelectedDateString) {
realtimeUpdates = {};
lastSelectedDateString = currentDateString;
}
// Reset spaceStatus and ensure we re-calculate from new spaces prop
let newStatus: Record<string, any> = {};
spaces.forEach(s => { spaces.forEach(s => {
// Don't overwrite if we already have a real-time update for today // Don't overwrite if we already have a real-time update for this view
if (isToday && realtimeUpdates[s.id]) { if (realtimeUpdates[s.id]) {
spaceStatus[s.id] = realtimeUpdates[s.id]; newStatus[s.id] = realtimeUpdates[s.id];
return; return;
} }
const activeReservation = s.reservations && s.reservations.length > 0 ? s.reservations[0] : null; const activeReservation = s.reservations && s.reservations.length > 0 ? s.reservations[0] : null;
spaceStatus[s.id] = { newStatus[s.id] = {
isOccupied: !!activeReservation, isOccupied: !!activeReservation,
reservation: activeReservation reservation: activeReservation
}; };
}); });
spaceStatus = { ...spaceStatus }; spaceStatus = newStatus;
updateContainerSizeToFitAll(); updateContainerSizeToFitAll();
} }
@ -58,15 +70,6 @@
if (b > maxB) maxB = b; if (b > maxB) maxB = b;
}); });
// To be consistent with editor, we should probably use the same base.
// However, in the viewer, we just want to show everything.
// If spaces were saved with percentages relative to a larger container,
// we need to know that container's aspect ratio.
// Actually, the percentages ARE the aspect ratio.
// If a space is at x=110%, it means it's outside the "original" 800px.
// So we should just find the max percentage and scale the container.
let maxXP = 100; let maxXP = 100;
let maxYP = 100; let maxYP = 100;
spaces.forEach(s => { spaces.forEach(s => {
@ -84,8 +87,6 @@
containerHeight += 40; containerHeight += 40;
} }
$: isToday = new Date(selectedDate.getTime()).setHours(0,0,0,0) === new Date().setHours(0,0,0,0);
onMount(() => { onMount(() => {
const socketUrl = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000'; const socketUrl = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000';
socket = io(socketUrl, { socket = io(socketUrl, {
@ -99,10 +100,30 @@
socket.on('space_status_changed', (data: { spaceId: string, isOccupied: boolean, reservation?: any }) => { socket.on('space_status_changed', (data: { spaceId: string, isOccupied: boolean, reservation?: any }) => {
console.log('Space status changed received:', data); console.log('Space status changed received:', data);
if (!isToday) {
console.log('Ignoring real-time update: not today', { let isRelevant = false;
selectedDate: selectedDate.toDateString(), if (data.reservation) {
now: new Date().toDateString() const start = new Date(data.reservation.startTime);
const end = new Date(data.reservation.endTime);
const viewStart = new Date(selectedDate.getTime());
viewStart.setHours(0, 0, 0, 0);
const viewEnd = new Date(selectedDate.getTime());
viewEnd.setHours(23, 59, 59, 999);
isRelevant = (start <= viewEnd && end >= viewStart);
} else {
// If we don't have reservation data (should not happen with updated API),
// fallback to isToday check.
isRelevant = isToday;
}
if (!isRelevant) {
console.log('Ignoring real-time update: not relevant to current view', {
selectedDate: selectedDate.toDateString(),
reservation: data.reservation ? {
start: new Date(data.reservation.startTime).toDateString(),
end: new Date(data.reservation.endTime).toDateString()
} : 'none'
}); });
return; return;
} }

View File

@ -14,11 +14,19 @@
// Track if we have received any real-time updates for this space // Track if we have received any real-time updates for this space
let realtimeOccupied: boolean | null = null; let realtimeOccupied: boolean | null = null;
let lastSelectedDateString = selectedDate.toDateString();
$: isToday = new Date(selectedDate.getTime()).setHours(0,0,0,0) === new Date().setHours(0,0,0,0); $: isToday = new Date(selectedDate.getTime()).setHours(0,0,0,0) === new Date().setHours(0,0,0,0);
$: if (initialStatus !== undefined) { $: if (initialStatus !== undefined) {
if (isToday && realtimeOccupied !== null) { // Detect when the actual day changes to clear real-time updates
const currentDateString = selectedDate.toDateString();
if (currentDateString !== lastSelectedDateString) {
realtimeOccupied = null;
lastSelectedDateString = currentDateString;
}
if (realtimeOccupied !== null) {
isOccupied = realtimeOccupied; isOccupied = realtimeOccupied;
} else { } else {
isOccupied = initialStatus; isOccupied = initialStatus;
@ -37,8 +45,24 @@
socket.emit('join_floor', floorId); socket.emit('join_floor', floorId);
}); });
socket.on('space_status_changed', (data: { spaceId: string, isOccupied: boolean }) => { socket.on('space_status_changed', (data: { spaceId: string, isOccupied: boolean, reservation?: any }) => {
if (data.spaceId === spaceId && isToday) { if (data.spaceId !== spaceId) return;
let isRelevant = false;
if (data.reservation) {
const start = new Date(data.reservation.startTime);
const end = new Date(data.reservation.endTime);
const viewStart = new Date(selectedDate.getTime());
viewStart.setHours(0, 0, 0, 0);
const viewEnd = new Date(selectedDate.getTime());
viewEnd.setHours(23, 59, 59, 999);
isRelevant = (start <= viewEnd && end >= viewStart);
} else {
isRelevant = isToday;
}
if (isRelevant) {
console.log('Space status updated for:', spaceId, data.isOccupied); console.log('Space status updated for:', spaceId, data.isOccupied);
realtimeOccupied = data.isOccupied; realtimeOccupied = data.isOccupied;
isOccupied = data.isOccupied; isOccupied = data.isOccupied;