187 lines
7.2 KiB
Svelte
187 lines
7.2 KiB
Svelte
<script lang="ts">
|
|
import { page } from "$app/stores";
|
|
import { onMount } from "svelte";
|
|
import { token, user } from "$lib/stores/auth";
|
|
import { goto } from "$app/navigation";
|
|
import { apiFetch } from "$lib/api/client";
|
|
import FloorplanViewer from '$lib/components/FloorplanViewer.svelte';
|
|
import { Building2, Layers, Map as MapIcon, ChevronLeft, Settings, Calendar } from "lucide-svelte";
|
|
import { t, locale } from "svelte-i18n";
|
|
import { format } from "date-fns";
|
|
import { es, enUS } from "date-fns/locale";
|
|
|
|
let building: any = null;
|
|
let currentFloor: any = null;
|
|
let loading = true;
|
|
let loadingFloor = false;
|
|
let selectedFloorId = "";
|
|
let selectedDateString = new Date().toISOString().split('T')[0];
|
|
let selectedDate = new Date();
|
|
|
|
$: {
|
|
selectedDate = new Date(selectedDateString);
|
|
// Add time component to the selected date if it's today, otherwise use start of day
|
|
if (selectedDateString === new Date().toISOString().split('T')[0]) {
|
|
selectedDate = new Date();
|
|
} else {
|
|
selectedDate.setHours(7, 0, 0, 0); // Default to 7 AM for other days
|
|
}
|
|
}
|
|
|
|
$: dateLocale = $locale?.startsWith('es') ? es : enUS;
|
|
$: dateFormat = $locale === 'es-ES' ? 'dd/MM/yyyy' : 'EEEE, MMMM d';
|
|
|
|
onMount(async () => {
|
|
const buildingId = $page.params.id;
|
|
if (!$token) {
|
|
goto(`/login?redirect=/dashboard/building/${buildingId}`);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
building = await apiFetch(`/spaces/building/${buildingId}`);
|
|
if (building.floors && building.floors.length > 0) {
|
|
selectedFloorId = building.floors[0].id;
|
|
await loadFloor(selectedFloorId);
|
|
}
|
|
} catch (e: any) {
|
|
console.error(e);
|
|
if (e.message?.includes('401')) {
|
|
goto("/login");
|
|
}
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
});
|
|
|
|
async function loadFloor(floorId: string) {
|
|
loadingFloor = true;
|
|
try {
|
|
currentFloor = await apiFetch(`/spaces/floor/${floorId}?date=${selectedDate.toISOString()}`);
|
|
} catch (e) {
|
|
console.error(e);
|
|
} finally {
|
|
loadingFloor = false;
|
|
}
|
|
}
|
|
|
|
$: if (selectedFloorId && building && selectedDate) {
|
|
// Re-fetch if floor or date changes
|
|
loadFloor(selectedFloorId);
|
|
}
|
|
</script>
|
|
|
|
<div class="p-8 max-w-7xl mx-auto">
|
|
<div class="mb-6">
|
|
<a href="/dashboard" class="inline-flex items-center text-indigo-600 hover:text-indigo-700 font-medium gap-1">
|
|
<ChevronLeft size={20} />
|
|
{$t('building.back')}
|
|
</a>
|
|
</div>
|
|
|
|
{#if loading}
|
|
<div class="flex flex-col items-center justify-center h-64">
|
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mb-4"></div>
|
|
<p class="text-gray-500">{$t('building.loading')}</p>
|
|
</div>
|
|
{:else if building}
|
|
<div class="flex flex-col lg:flex-row justify-between items-start lg:items-center mb-8 gap-4">
|
|
<div class="flex items-center gap-4">
|
|
<div class="w-14 h-14 bg-indigo-600 rounded-xl flex items-center justify-center shadow-lg shadow-indigo-100">
|
|
<Building2 class="text-white" size={30} />
|
|
</div>
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-gray-900">{building.name}</h1>
|
|
<div class="flex items-center gap-4 mt-1 text-gray-500">
|
|
<span class="flex items-center gap-1.5 text-sm">
|
|
<Layers size={16} />
|
|
{$t('dashboard.floors', { values: { count: building.floors?.length || 0 } })}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3 w-full lg:w-auto">
|
|
<div class="relative flex-1 lg:flex-none min-w-[200px]">
|
|
<span class="absolute -top-2 left-3 px-1 bg-gray-50 text-[10px] font-bold text-indigo-600 uppercase tracking-wider">{$t('building.selectDate')}</span>
|
|
<input
|
|
id="date-select"
|
|
type="date"
|
|
bind:value={selectedDateString}
|
|
min={new Date().toISOString().split('T')[0]}
|
|
class="absolute inset-0 opacity-0 cursor-pointer z-10 w-full h-full"
|
|
on:click={(e) => e.currentTarget.showPicker()}
|
|
/>
|
|
<div class="w-full pl-3 pr-4 py-3 bg-white border border-gray-300 rounded-xl font-medium text-gray-700 shadow-sm flex items-center justify-between">
|
|
<span class="capitalize">{format(selectedDate, dateFormat, { locale: dateLocale })}</span>
|
|
<Calendar size={18} class="text-indigo-600" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="relative flex-1 lg:flex-none min-w-[200px]">
|
|
<label for="floor-select" class="absolute -top-2 left-3 px-1 bg-gray-50 text-[10px] font-bold text-indigo-600 uppercase tracking-wider">{$t('building.selectFloor')}</label>
|
|
<select
|
|
id="floor-select"
|
|
bind:value={selectedFloorId}
|
|
class="w-full pl-3 pr-10 py-3 bg-white border border-gray-300 rounded-xl appearance-none focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent font-medium text-gray-700 shadow-sm"
|
|
>
|
|
{#each building.floors as floor}
|
|
<option value={floor.id}>{$t('building.floor', { values: { number: floor.number } })}</option>
|
|
{/each}
|
|
</select>
|
|
<div class="absolute inset-y-0 right-3 flex items-center pointer-events-none text-gray-400">
|
|
<Layers size={20} />
|
|
</div>
|
|
</div>
|
|
|
|
{#if $user?.role === 'ADMIN' && selectedFloorId}
|
|
<a
|
|
href="/admin/floorplan/{selectedFloorId}"
|
|
class="p-3 bg-white border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 transition-colors shadow-sm"
|
|
title={$t('building.editFloorplan')}
|
|
>
|
|
<Settings size={20} />
|
|
</a>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-gray-100 rounded-3xl p-8 min-h-[700px] flex items-center justify-center border border-gray-200 shadow-inner relative overflow-hidden">
|
|
{#if loadingFloor}
|
|
<div class="flex flex-col items-center">
|
|
<div class="animate-spin rounded-full h-10 w-10 border-b-2 border-indigo-600 mb-4"></div>
|
|
<p class="text-gray-500 font-medium">{$t('building.switching')}</p>
|
|
</div>
|
|
{:else if currentFloor}
|
|
<div class="flex flex-col items-center">
|
|
<FloorplanViewer
|
|
floorId={currentFloor.id}
|
|
spaces={currentFloor.spaces}
|
|
planImageUrl={currentFloor.planImageUrl}
|
|
selectedDate={selectedDate}
|
|
/>
|
|
{#if currentFloor.planImageUrl}
|
|
<p class="mt-6 text-sm text-gray-500 flex items-center gap-2">
|
|
<MapIcon size={16} />
|
|
{$t('building.interactiveView')}
|
|
</p>
|
|
{:else}
|
|
<p class="mt-6 text-sm text-gray-400 flex items-center gap-2 italic">
|
|
<MapIcon size={16} />
|
|
{$t('building.noFloorplan')}
|
|
</p>
|
|
{/if}
|
|
</div>
|
|
{:else}
|
|
<p class="text-gray-400">{$t('building.selectToSee')}</p>
|
|
{/if}
|
|
</div>
|
|
{:else}
|
|
<div class="text-center py-20">
|
|
<h2 class="text-2xl font-bold text-gray-900">{$t('building.notFound')}</h2>
|
|
<p class="text-gray-500 mt-2">{$t('building.notFoundDesc')}</p>
|
|
<a href="/dashboard" class="mt-8 inline-block text-indigo-600 font-medium">{$t('building.return')}</a>
|
|
</div>
|
|
{/if}
|
|
</div>
|