84 lines
2.8 KiB
Svelte
84 lines
2.8 KiB
Svelte
<script lang="ts">
|
|
import { onMount, onDestroy } from 'svelte';
|
|
import { io, Socket } from 'socket.io-client';
|
|
import { Laptop, Bath, Coffee, Users } from 'lucide-svelte';
|
|
|
|
export let floorId: string;
|
|
export let spaces: any[] = [];
|
|
export let planImageUrl: string | null = null;
|
|
|
|
let container: HTMLDivElement;
|
|
let socket: Socket;
|
|
let spaceStatus: Record<string, boolean> = {};
|
|
|
|
$: {
|
|
// Initialize space status from the current state
|
|
spaces.forEach(s => {
|
|
spaceStatus[s.id] = s.reservations && s.reservations.length > 0;
|
|
});
|
|
}
|
|
|
|
onMount(() => {
|
|
const socketUrl = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000';
|
|
socket = io(socketUrl);
|
|
|
|
socket.on('connect', () => {
|
|
socket.emit('join_floor', floorId);
|
|
});
|
|
|
|
socket.on('space_status_changed', (data: { spaceId: string, isOccupied: boolean }) => {
|
|
spaceStatus[data.spaceId] = data.isOccupied;
|
|
spaceStatus = { ...spaceStatus }; // Trigger reactivity
|
|
});
|
|
});
|
|
|
|
onDestroy(() => {
|
|
if (socket) {
|
|
socket.emit('leave_floor', floorId);
|
|
socket.disconnect();
|
|
}
|
|
});
|
|
|
|
function getSpaceTypeIcon(type: string, name: string) {
|
|
if (type === 'DESK') return Laptop;
|
|
if (type === 'MEETING_ROOM') return Users;
|
|
const lowerName = name.toLowerCase();
|
|
if (lowerName.includes('toilet') || lowerName.includes('bath')) return Bath;
|
|
if (lowerName.includes('coffee')) return Coffee;
|
|
return null;
|
|
}
|
|
</script>
|
|
|
|
<div
|
|
bind:this={container}
|
|
class="relative bg-white shadow-xl rounded-lg overflow-hidden border mx-auto floorplan-container"
|
|
style="width: 800px; height: 600px; background-image: url({planImageUrl}); background-size: contain; background-repeat: no-repeat; background-position: center;"
|
|
>
|
|
{#each spaces as space (space.id)}
|
|
<div
|
|
class="absolute p-2 border-2 rounded-lg shadow-md transition-colors {spaceStatus[space.id] ? 'bg-red-50 border-red-500' : 'bg-green-50 border-green-500'}"
|
|
style="left: 0; top: 0; transform: translate({(space.x / 100) * (container?.clientWidth || 800)}px, {(space.y / 100) * (container?.clientHeight || 600)}px)"
|
|
title="{space.name} ({spaceStatus[space.id] ? 'Occupied' : 'Available'})"
|
|
>
|
|
<div class="flex items-center gap-1">
|
|
<div class={spaceStatus[space.id] ? 'text-red-600' : 'text-green-600'}>
|
|
<svelte:component this={getSpaceTypeIcon(space.type, space.name)} size={16} />
|
|
</div>
|
|
<span class="text-[9px] font-bold text-gray-800 whitespace-nowrap">
|
|
{space.name}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
|
|
<style>
|
|
.floorplan-container {
|
|
background-color: #f8fafc;
|
|
background-image:
|
|
linear-gradient(to right, #e2e8f0 1px, transparent 1px),
|
|
linear-gradient(to bottom, #e2e8f0 1px, transparent 1px);
|
|
background-size: 40px 40px;
|
|
}
|
|
</style>
|