Enhance real-time space status updates and handling
- Improve WebSocket connection reliability with updated transport options. - Introduce logic to prioritize real-time updates over initial data for live views. - Add logging for better debugging of WebSocket events and data propagation. - Update UI components (`SpaceCard`, `FloorplanViewer`) to avoid overwrites from stale data. - Refactor date handling for consistent behavior across components.main
parent
1b3c3044ca
commit
453fe68e42
|
|
@ -4,11 +4,13 @@
|
||||||
<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 "My Bookings" page and enable reservation updates and cancellations - Implement `+page.svelte` for managing user reservations with real-time updates. - Add API endpoints for updating and canceling reservations with enhanced validation and authorization. - Introduce `UpdateReservationDto` for refined reservation update handling. - Update `reservations.service.ts` to handle overlapping checks, time adjustments, and admin actions. - Extend layout to include a link to the "My Bookings" page. - Add `date-fns` library for date formatting.">
|
<list default="true" id="6c8b3c54-eabb-40e5-967f-c7b594c750bc" name="Changes" comment="Add building and floor management with admin editor modal - Implement `BuildingEditorModal.svelte` for creating, editing, and deleting buildings and floors. - Add `CreateBuildingDto`, `UpdateBuildingDto`, `CreateFloorDto`, and `UpdateFloorDto` for validation. - Extend `SpacesService` and `SpacesController` with endpoints for building and floor CRUD operations. - Update Prisma schema to include building descriptions and image URLs, and floor names. - Enhance dashboard with building management functionality for admin users.">
|
||||||
<change beforePath="$PROJECT_DIR$/apps/api/prisma/schema.prisma" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/prisma/schema.prisma" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/apps/api/src/events/events.gateway.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/events/events.gateway.ts" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/apps/api/src/spaces/spaces.controller.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/spaces/spaces.controller.ts" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/apps/api/src/reservations/reservations.service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/reservations/reservations.service.ts" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/apps/api/src/spaces/spaces.service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/spaces/spaces.service.ts" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/apps/web/src/lib/components/BookingModal.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/lib/components/BookingModal.svelte" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/apps/web/src/routes/dashboard/+page.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/routes/dashboard/+page.svelte" 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/web/src/lib/components/SpaceCard.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/lib/components/SpaceCard.svelte" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/apps/web/src/routes/dashboard/floor/[id]/+page.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/routes/dashboard/floor/[id]/+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" />
|
||||||
|
|
@ -130,7 +132,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="4842000" />
|
<workItem from="1774706361502" duration="5157000" />
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00001" summary="Initialize Hot Desking app with web and API services - Set up SvelteKit for the web interface with Tailwind CSS. - Build the API using NestJS with Prisma ORM for database interaction. - Add environment variable management and docker-compose for PostgreSQL and PgAdmin instances. - Create shared data models for consistent typing across services. - Establish basic routing and layouts for web and API services.">
|
<task id="LOCAL-00001" summary="Initialize Hot Desking app with web and API services - Set up SvelteKit for the web interface with Tailwind CSS. - Build the API using NestJS with Prisma ORM for database interaction. - Add environment variable management and docker-compose for PostgreSQL and PgAdmin instances. - Create shared data models for consistent typing across services. - Establish basic routing and layouts for web and API services.">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
|
|
@ -212,7 +214,15 @@
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1774710987605</updated>
|
<updated>1774710987605</updated>
|
||||||
</task>
|
</task>
|
||||||
<option name="localTasksCounter" value="11" />
|
<task id="LOCAL-00011" summary="Add building and floor management with admin editor modal - Implement `BuildingEditorModal.svelte` for creating, editing, and deleting buildings and floors. - Add `CreateBuildingDto`, `UpdateBuildingDto`, `CreateFloorDto`, and `UpdateFloorDto` for validation. - Extend `SpacesService` and `SpacesController` with endpoints for building and floor CRUD operations. - Update Prisma schema to include building descriptions and image URLs, and floor names. - Enhance dashboard with building management functionality for admin users.">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1774711356754</created>
|
||||||
|
<option name="number" value="00011" />
|
||||||
|
<option name="presentableId" value="LOCAL-00011" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1774711356754</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="12" />
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
|
|
@ -230,6 +240,7 @@
|
||||||
<MESSAGE value="Add user management and registration features with invite-based onboarding - Implement `+page.svelte` for admin user management, allowing role updates, password resets, and invite link generation." />
|
<MESSAGE value="Add user management and registration features with invite-based onboarding - Implement `+page.svelte` for admin user management, allowing role updates, password resets, and invite link generation." />
|
||||||
<MESSAGE value="Add user management and registration features with invite-based onboarding - Implement `+page.svelte` for admin user management, allowing role updates, password resets, and invite link generation. Update `UsersController` and `UsersService` to support user role updates, password changes, and invite token generation. - Create DTOs (`RegisterDto`, `UpdateUserDto`) for input validation in user management endpoints. - Extend `AuthService` and `AuthController` with invite-based registration logic. - Update admin layout to include "Users" link for easy access to user management." />
|
<MESSAGE value="Add user management and registration features with invite-based onboarding - Implement `+page.svelte` for admin user management, allowing role updates, password resets, and invite link generation. Update `UsersController` and `UsersService` to support user role updates, password changes, and invite token generation. - Create DTOs (`RegisterDto`, `UpdateUserDto`) for input validation in user management endpoints. - Extend `AuthService` and `AuthController` with invite-based registration logic. - Update admin layout to include "Users" link for easy access to user management." />
|
||||||
<MESSAGE value="Add "My Bookings" page and enable reservation updates and cancellations - Implement `+page.svelte` for managing user reservations with real-time updates. - Add API endpoints for updating and canceling reservations with enhanced validation and authorization. - Introduce `UpdateReservationDto` for refined reservation update handling. - Update `reservations.service.ts` to handle overlapping checks, time adjustments, and admin actions. - Extend layout to include a link to the "My Bookings" page. - Add `date-fns` library for date formatting." />
|
<MESSAGE value="Add "My Bookings" page and enable reservation updates and cancellations - Implement `+page.svelte` for managing user reservations with real-time updates. - Add API endpoints for updating and canceling reservations with enhanced validation and authorization. - Introduce `UpdateReservationDto` for refined reservation update handling. - Update `reservations.service.ts` to handle overlapping checks, time adjustments, and admin actions. - Extend layout to include a link to the "My Bookings" page. - Add `date-fns` library for date formatting." />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="Add "My Bookings" page and enable reservation updates and cancellations - Implement `+page.svelte` for managing user reservations with real-time updates. - Add API endpoints for updating and canceling reservations with enhanced validation and authorization. - Introduce `UpdateReservationDto` for refined reservation update handling. - Update `reservations.service.ts` to handle overlapping checks, time adjustments, and admin actions. - Extend layout to include a link to the "My Bookings" page. - Add `date-fns` library for date formatting." />
|
<MESSAGE value="Add building and floor management with admin editor modal - Implement `BuildingEditorModal.svelte` for creating, editing, and deleting buildings and floors. - Add `CreateBuildingDto`, `UpdateBuildingDto`, `CreateFloorDto`, and `UpdateFloorDto` for validation. - Extend `SpacesService` and `SpacesController` with endpoints for building and floor CRUD operations. - Update Prisma schema to include building descriptions and image URLs, and floor names. - Enhance dashboard with building management functionality for admin users." />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="Add building and floor management with admin editor modal - Implement `BuildingEditorModal.svelte` for creating, editing, and deleting buildings and floors. - Add `CreateBuildingDto`, `UpdateBuildingDto`, `CreateFloorDto`, and `UpdateFloorDto` for validation. - Extend `SpacesService` and `SpacesController` with endpoints for building and floor CRUD operations. - Update Prisma schema to include building descriptions and image URLs, and floor names. - Enhance dashboard with building management functionality for admin users." />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
|
|
@ -27,7 +27,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||||
@SubscribeMessage('join_floor')
|
@SubscribeMessage('join_floor')
|
||||||
handleJoinFloor(client: Socket, floorId: string) {
|
handleJoinFloor(client: Socket, floorId: string) {
|
||||||
client.join(floorId);
|
client.join(floorId);
|
||||||
console.log(`Client ${client.id} joined floor: ${floorId}`);
|
console.log(`Client ${client.id} joined floor: ${floorId}. Rooms:`, Array.from(client.rooms));
|
||||||
}
|
}
|
||||||
|
|
||||||
@SubscribeMessage('leave_floor')
|
@SubscribeMessage('leave_floor')
|
||||||
|
|
@ -37,6 +37,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||||
}
|
}
|
||||||
|
|
||||||
emitSpaceStatusChanged(floorId: string, spaceId: string, isOccupied: boolean, reservationData?: any) {
|
emitSpaceStatusChanged(floorId: string, spaceId: string, isOccupied: boolean, reservationData?: any) {
|
||||||
|
console.log(`Emitting space_status_changed to floor ${floorId}:`, { spaceId, isOccupied });
|
||||||
this.server.to(floorId).emit('space_status_changed', {
|
this.server.to(floorId).emit('space_status_changed', {
|
||||||
spaceId,
|
spaceId,
|
||||||
isOccupied,
|
isOccupied,
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,8 @@ export class ReservationsService {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('Emitted space_status_changed for new reservation', space.floorId, spaceId);
|
||||||
|
|
||||||
return reservation;
|
return reservation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,6 +136,8 @@ export class ReservationsService {
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log('Emitted space_status_changed for cancelled reservation', reservation.space.floorId, reservation.spaceId);
|
||||||
|
|
||||||
return { message: 'Reservation cancelled' };
|
return { message: 'Reservation cancelled' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('Booking created successfully');
|
||||||
dispatch('close');
|
dispatch('close');
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error = e.message || "Failed to create booking";
|
error = e.message || "Failed to create booking";
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,18 @@
|
||||||
let containerWidth = 800;
|
let containerWidth = 800;
|
||||||
let containerHeight = 600;
|
let containerHeight = 600;
|
||||||
|
|
||||||
|
// Track if we have received any real-time updates to avoid overwriting them with stale initial status
|
||||||
|
let realtimeUpdates: Record<string, any> = {};
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
// Initialize space status from the current state
|
// Initialize space status from the current state (e.g. from API)
|
||||||
spaces.forEach(s => {
|
spaces.forEach(s => {
|
||||||
// For desks, we check if there's any reservation for the selected date
|
// Don't overwrite if we already have a real-time update for today
|
||||||
// For meeting rooms, we check if there's a reservation overlapping with the current/selected time
|
if (isToday && realtimeUpdates[s.id]) {
|
||||||
|
spaceStatus[s.id] = realtimeUpdates[s.id];
|
||||||
|
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] = {
|
spaceStatus[s.id] = {
|
||||||
isOccupied: !!activeReservation,
|
isOccupied: !!activeReservation,
|
||||||
|
|
@ -74,23 +81,37 @@
|
||||||
containerHeight += 40;
|
containerHeight += 40;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable real-time updates if viewing a different date than today
|
$: isToday = new Date(selectedDate.getTime()).setHours(0,0,0,0) === new Date().setHours(0,0,0,0);
|
||||||
$: isToday = new Date().toDateString() === selectedDate.toDateString();
|
|
||||||
|
|
||||||
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, {
|
||||||
|
transports: ['websocket'], // Prefer WebSockets
|
||||||
|
});
|
||||||
|
|
||||||
socket.on('connect', () => {
|
socket.on('connect', () => {
|
||||||
|
console.log('Connected to WebSocket for floor:', floorId);
|
||||||
socket.emit('join_floor', floorId);
|
socket.emit('join_floor', floorId);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('space_status_changed', (data: { spaceId: string, isOccupied: boolean, reservation?: any }) => {
|
socket.on('space_status_changed', (data: { spaceId: string, isOccupied: boolean, reservation?: any }) => {
|
||||||
if (!isToday) return; // Ignore real-time updates for historical/future views
|
console.log('Space status changed received:', data);
|
||||||
spaceStatus[data.spaceId] = {
|
if (!isToday) {
|
||||||
|
console.log('Ignoring real-time update: not today', {
|
||||||
|
selectedDate: selectedDate.toDateString(),
|
||||||
|
now: new Date().toDateString()
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = {
|
||||||
isOccupied: data.isOccupied,
|
isOccupied: data.isOccupied,
|
||||||
reservation: data.reservation
|
reservation: data.reservation
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Save to realtimeUpdates to prevent overwriting by API re-fetches
|
||||||
|
realtimeUpdates[data.spaceId] = update;
|
||||||
|
spaceStatus[data.spaceId] = update;
|
||||||
spaceStatus = { ...spaceStatus }; // Trigger reactivity
|
spaceStatus = { ...spaceStatus }; // Trigger reactivity
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -12,24 +12,35 @@
|
||||||
let isOccupied = initialStatus;
|
let isOccupied = initialStatus;
|
||||||
let socket: Socket;
|
let socket: Socket;
|
||||||
|
|
||||||
$: isToday = new Date().toDateString() === selectedDate.toDateString();
|
// Track if we have received any real-time updates for this space
|
||||||
|
let realtimeOccupied: boolean | null = null;
|
||||||
|
|
||||||
|
$: isToday = new Date(selectedDate.getTime()).setHours(0,0,0,0) === new Date().setHours(0,0,0,0);
|
||||||
|
|
||||||
$: if (initialStatus !== undefined) {
|
$: if (initialStatus !== undefined) {
|
||||||
isOccupied = initialStatus;
|
if (isToday && realtimeOccupied !== null) {
|
||||||
|
isOccupied = realtimeOccupied;
|
||||||
|
} else {
|
||||||
|
isOccupied = initialStatus;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Connect to WebSocket using the environment variable for the backend URL.
|
// Connect to WebSocket using the environment variable for the backend URL.
|
||||||
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, {
|
||||||
|
transports: ['websocket'],
|
||||||
|
});
|
||||||
|
|
||||||
socket.on('connect', () => {
|
socket.on('connect', () => {
|
||||||
console.log('Connected to WebSocket');
|
console.log('Connected to WebSocket (SpaceCard)');
|
||||||
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 }) => {
|
||||||
if (data.spaceId === spaceId && isToday) {
|
if (data.spaceId === spaceId && isToday) {
|
||||||
|
console.log('Space status updated for:', spaceId, data.isOccupied);
|
||||||
|
realtimeOccupied = data.isOccupied;
|
||||||
isOccupied = data.isOccupied;
|
isOccupied = data.isOccupied;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -10,31 +10,33 @@
|
||||||
|
|
||||||
let floor: any = null;
|
let floor: any = null;
|
||||||
let loading = true;
|
let loading = true;
|
||||||
let selectedDateString = new Date().toISOString().split('T')[0];
|
let selectedDateString = new Date().toLocaleDateString('en-CA'); // YYYY-MM-DD in local time
|
||||||
let selectedDate = new Date();
|
let selectedDate = new Date();
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
selectedDate = new Date(selectedDateString);
|
selectedDate = new Date(selectedDateString + 'T00:00:00'); // Initialize to start of day in local time
|
||||||
if (selectedDateString === new Date().toISOString().split('T')[0]) {
|
if (selectedDateString === new Date().toLocaleDateString('en-CA')) {
|
||||||
selectedDate = new Date();
|
selectedDate = new Date();
|
||||||
} else {
|
} else {
|
||||||
selectedDate.setHours(7, 0, 0, 0);
|
selectedDate.setHours(7, 0, 0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let floorIdFromUrl = $page.params.id;
|
||||||
|
$: floorIdFromUrl = $page.params.id;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const floorId = $page.params.id;
|
|
||||||
if (!$token) {
|
if (!$token) {
|
||||||
goto(`/login?redirect=/dashboard/floor/${floorId}`);
|
goto(`/login?redirect=/dashboard/floor/${floorIdFromUrl}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await loadFloor(floorId);
|
await loadFloor(floorIdFromUrl, selectedDate);
|
||||||
loading = false;
|
loading = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadFloor(floorId: string) {
|
async function loadFloor(id: string, date: Date) {
|
||||||
try {
|
try {
|
||||||
floor = await apiFetch(`/spaces/floor/${floorId}?date=${selectedDate.toISOString()}`);
|
floor = await apiFetch(`/spaces/floor/${id}?date=${date.toISOString()}`);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
if (e.message?.includes('401')) {
|
if (e.message?.includes('401')) {
|
||||||
|
|
@ -43,8 +45,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (floor && floor.id && selectedDate) {
|
$: if (floorIdFromUrl && selectedDate) {
|
||||||
loadFloor(floor.id);
|
loadFloor(floorIdFromUrl, selectedDate);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue