324 lines
13 KiB
Svelte
324 lines
13 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from "svelte";
|
|
import { token } from "$lib/stores/auth";
|
|
import { apiFetch } from "$lib/api/client";
|
|
import { Calendar, Clock, MapPin, Trash2, Edit2, ChevronRight, X, ChevronDown, ChevronUp } from "lucide-svelte";
|
|
import { format, isPast } from "date-fns";
|
|
import { es, enUS } from 'date-fns/locale';
|
|
import { t, locale } from "svelte-i18n";
|
|
|
|
let reservations = [];
|
|
let loading = true;
|
|
let error = null;
|
|
|
|
let editingReservation = null;
|
|
let editStartTimeDate = "";
|
|
let editStartTimeTime = "";
|
|
let editEndTimeDate = "";
|
|
let editEndTimeTime = "";
|
|
let saving = false;
|
|
let showPast = false;
|
|
|
|
$: dateLocale = $locale === 'es-ES' ? es : enUS;
|
|
$: dateFormat = $locale === 'es-ES' ? 'dd/MM/yyyy' : 'PPP';
|
|
|
|
$: upcomingReservations = reservations.filter(r => !isPast(new Date(r.endTime)));
|
|
$: pastReservations = reservations.filter(r => isPast(new Date(r.endTime)));
|
|
|
|
onMount(async () => {
|
|
await fetchReservations();
|
|
});
|
|
|
|
async function fetchReservations() {
|
|
loading = true;
|
|
try {
|
|
reservations = await apiFetch("/reservations/my");
|
|
} catch (e) {
|
|
error = $t('myBookings.loadFailed');
|
|
console.error(e);
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
|
|
async function deleteReservation(id) {
|
|
if (!confirm($t('myBookings.cancelConfirm'))) return;
|
|
|
|
try {
|
|
await apiFetch(`/reservations/${id}`, { method: "DELETE" });
|
|
reservations = reservations.filter(r => r.id !== id);
|
|
} catch (e) {
|
|
alert($t('myBookings.cancelFailed'));
|
|
}
|
|
}
|
|
|
|
function openEditModal(res) {
|
|
editingReservation = res;
|
|
// Separate date and time for inputs
|
|
const start = new Date(res.startTime);
|
|
const end = new Date(res.endTime);
|
|
|
|
editStartTimeDate = format(start, "yyyy-MM-dd");
|
|
editStartTimeTime = format(start, "HH:mm");
|
|
editEndTimeDate = format(end, "yyyy-MM-dd");
|
|
editEndTimeTime = format(end, "HH:mm");
|
|
}
|
|
|
|
function closeEditModal() {
|
|
editingReservation = null;
|
|
}
|
|
|
|
async function handleUpdate() {
|
|
saving = true;
|
|
try {
|
|
const start = new Date(`${editStartTimeDate}T${editStartTimeTime}`);
|
|
const end = new Date(`${editEndTimeDate}T${editEndTimeTime}`);
|
|
|
|
await apiFetch(`/reservations/${editingReservation.id}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({
|
|
startTime: start.toISOString(),
|
|
endTime: end.toISOString(),
|
|
})
|
|
});
|
|
await fetchReservations();
|
|
closeEditModal();
|
|
} catch (e: any) {
|
|
alert(e.message || $t('myBookings.updateFailed'));
|
|
} finally {
|
|
saving = false;
|
|
}
|
|
}
|
|
|
|
function getSpaceTypeLabel(type) {
|
|
return $t(`booking.spaceType.${type}`);
|
|
}
|
|
</script>
|
|
|
|
<div class="max-w-4xl mx-auto">
|
|
<div class="flex justify-between items-center mb-8">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-gray-900">{$t('myBookings.title')}</h1>
|
|
<p class="text-gray-500 mt-1">{$t('myBookings.description')}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{#if loading && !reservations.length}
|
|
<div class="flex justify-center items-center h-64">
|
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div>
|
|
</div>
|
|
{:else if error}
|
|
<div class="bg-red-50 text-red-700 p-4 rounded-lg border border-red-200">
|
|
{error}
|
|
</div>
|
|
{:else if reservations.length === 0}
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
|
|
<Calendar class="mx-auto text-gray-300 mb-4" size={48} />
|
|
<h3 class="text-lg font-medium text-gray-900">{$t('myBookings.noBookings')}</h3>
|
|
<p class="text-gray-500 mt-1">{$t('myBookings.noBookingsDesc')}</p>
|
|
<a href="/dashboard" class="mt-6 inline-flex items-center text-indigo-600 font-medium hover:underline">
|
|
{$t('myBookings.goToDashboard')} <ChevronRight size={16} />
|
|
</a>
|
|
</div>
|
|
{:else}
|
|
<div class="space-y-8">
|
|
{#if upcomingReservations.length > 0}
|
|
<section>
|
|
<h2 class="text-xl font-semibold text-gray-900 mb-4">{$t('myBookings.upcoming')}</h2>
|
|
<div class="space-y-4">
|
|
{#each upcomingReservations as res}
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden hover:border-indigo-300 transition-colors">
|
|
<div class="p-6 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div class="flex items-start gap-4">
|
|
<div class="w-12 h-12 {res.space.type === 'DESK' ? 'bg-blue-50 text-blue-600' : 'bg-purple-50 text-purple-600'} rounded-lg flex items-center justify-center flex-shrink-0">
|
|
{#if res.space.type === 'DESK'}
|
|
<MapPin size={24} />
|
|
{:else}
|
|
<Clock size={24} />
|
|
{/if}
|
|
</div>
|
|
<div>
|
|
<h3 class="text-lg font-bold text-gray-900">{res.space.name}</h3>
|
|
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 mt-1 text-sm text-gray-500">
|
|
<span class="flex items-center gap-1">
|
|
<MapPin size={14} />
|
|
{res.space.floor.building.name}, {$t('myBookings.floor', { values: { number: res.space.floor.number } })}
|
|
</span>
|
|
<span class="flex items-center gap-1">
|
|
<Calendar size={14} />
|
|
{format(new Date(res.startTime), dateFormat, { locale: dateLocale })}
|
|
</span>
|
|
<span class="flex items-center gap-1">
|
|
<Clock size={14} />
|
|
{format(new Date(res.startTime), "HH:mm")} - {format(new Date(res.endTime), "HH:mm")}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2 border-t md:border-t-0 pt-4 md:pt-0">
|
|
<button
|
|
on:click={() => openEditModal(res)}
|
|
class="p-2 text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 rounded-lg transition-colors"
|
|
title={$t('common.edit')}
|
|
>
|
|
<Edit2 size={20} />
|
|
</button>
|
|
<button
|
|
on:click={() => deleteReservation(res.id)}
|
|
class="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
|
title={$t('common.delete')}
|
|
>
|
|
<Trash2 size={20} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</section>
|
|
{/if}
|
|
|
|
{#if pastReservations.length > 0}
|
|
<section>
|
|
<button
|
|
on:click={() => showPast = !showPast}
|
|
class="flex items-center justify-between w-full text-left mb-4 group"
|
|
>
|
|
<h2 class="text-xl font-semibold text-gray-900">
|
|
{$t('myBookings.past')}
|
|
<span class="text-sm font-normal text-gray-500 ml-2">({pastReservations.length})</span>
|
|
</h2>
|
|
<div class="flex items-center text-indigo-600 text-sm font-medium">
|
|
{showPast ? $t('myBookings.hidePast') : $t('myBookings.showPast')}
|
|
{#if showPast}
|
|
<ChevronUp size={20} class="ml-1" />
|
|
{:else}
|
|
<ChevronDown size={20} class="ml-1" />
|
|
{/if}
|
|
</div>
|
|
</button>
|
|
|
|
{#if showPast}
|
|
<div class="space-y-4">
|
|
{#each pastReservations as res}
|
|
<div class="bg-gray-50 rounded-xl border border-gray-200 overflow-hidden opacity-75">
|
|
<div class="p-6 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div class="flex items-start gap-4">
|
|
<div class="w-12 h-12 {res.space.type === 'DESK' ? 'bg-gray-200 text-gray-500' : 'bg-gray-200 text-gray-500'} rounded-lg flex items-center justify-center flex-shrink-0">
|
|
{#if res.space.type === 'DESK'}
|
|
<MapPin size={24} />
|
|
{:else}
|
|
<Clock size={24} />
|
|
{/if}
|
|
</div>
|
|
<div>
|
|
<h3 class="text-lg font-bold text-gray-500">{res.space.name}</h3>
|
|
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 mt-1 text-sm text-gray-400">
|
|
<span class="flex items-center gap-1">
|
|
<MapPin size={14} />
|
|
{res.space.floor.building.name}, {$t('myBookings.floor', { values: { number: res.space.floor.number } })}
|
|
</span>
|
|
<span class="flex items-center gap-1">
|
|
<Calendar size={14} />
|
|
{format(new Date(res.startTime), dateFormat, { locale: dateLocale })}
|
|
</span>
|
|
<span class="flex items-center gap-1">
|
|
<Clock size={14} />
|
|
{format(new Date(res.startTime), "HH:mm")} - {format(new Date(res.endTime), "HH:mm")}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</section>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if editingReservation}
|
|
<div class="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
|
|
<div class="bg-white rounded-2xl shadow-xl max-w-md w-full overflow-hidden">
|
|
<div class="p-6 border-b border-gray-100 flex justify-between items-center">
|
|
<h3 class="text-xl font-bold text-gray-900">{$t('myBookings.editBookingTitle')}</h3>
|
|
<button on:click={closeEditModal} class="text-gray-400 hover:text-gray-600">
|
|
<X size={24} />
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-6 space-y-4">
|
|
<div class="bg-gray-50 p-4 rounded-xl mb-4">
|
|
<p class="text-sm font-medium text-gray-500 uppercase tracking-wider">{$t('myBookings.space')}</p>
|
|
<p class="text-lg font-bold text-gray-900">{editingReservation.space.name}</p>
|
|
<div class="flex justify-between items-end">
|
|
<p class="text-sm text-gray-500">{editingReservation.space.floor.building.name}, {$t('myBookings.floor', { values: { number: editingReservation.space.floor.number } })}</p>
|
|
<p class="text-sm font-bold text-indigo-600">{format(new Date(editingReservation.startTime), dateFormat, { locale: dateLocale })}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="col-span-2">
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">{$t('booking.date')}</label>
|
|
<div class="relative">
|
|
<div class="w-full px-4 py-2 bg-gray-50 border border-gray-300 rounded-lg text-gray-700 flex justify-between items-center">
|
|
<span>{format(new Date(editStartTimeDate + 'T00:00:00'), dateFormat, { locale: dateLocale })}</span>
|
|
<Calendar size={16} class="text-gray-400" />
|
|
</div>
|
|
<input
|
|
type="date"
|
|
bind:value={editStartTimeDate}
|
|
class="absolute inset-0 opacity-0 cursor-pointer w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">{$t('booking.startTime')}</label>
|
|
<input
|
|
type="time"
|
|
bind:value={editStartTimeTime}
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">{$t('booking.endTime')}</label>
|
|
<input
|
|
type="time"
|
|
bind:value={editEndTimeTime}
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{#if editingReservation.space.type === 'DESK'}
|
|
<p class="text-xs text-amber-600 bg-amber-50 p-2 rounded">
|
|
{$t('myBookings.deskNote')}
|
|
</p>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="p-6 bg-gray-50 flex gap-3">
|
|
<button
|
|
on:click={closeEditModal}
|
|
class="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg font-medium hover:bg-gray-100 transition-colors"
|
|
>
|
|
{$t('common.cancel')}
|
|
</button>
|
|
<button
|
|
on:click={handleUpdate}
|
|
disabled={saving}
|
|
class="flex-1 px-4 py-2 bg-indigo-600 text-white rounded-lg font-medium hover:bg-indigo-700 transition-colors disabled:opacity-50"
|
|
>
|
|
{saving ? $t('myBookings.saving') : $t('myBookings.saveChanges')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|