desker/apps/web/src/routes/dashboard/my-bookings/+page.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}