This commit is contained in:
Tomas Dvorak committed 2026-05-05 09:48:07 +02:00
1 parent d854614a87
commit 48c3e15a38
295 files changed
+178378 -1036

No files matched your search

@@ -0,0 +1,451 @@
import { A, useParams, useSearchParams } from "@solidjs/router";
import { createSignal, createResource, Show, Match, Switch } from "solid-js";
import { apiClient } from "../lib/api-client";
import { useI18n } from "../providers/i18n-provider";
import { BookraCharacter } from "../components/bookra-character";
import { Button, Card, CardHeader, CardTitle, CardDescription, CardContent, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Input, Textarea } from "../components/ui";
export function BookingManageRoute() {
const params = useParams();
const [searchParams] = useSearchParams();
const i18n = useI18n();
const [isRescheduling, setIsRescheduling] = createSignal(false);
const [isCancelling, setIsCancelling] = createSignal(false);
const [newDate, setNewDate] = createSignal("");
const [newTime, setNewTime] = createSignal("");
const [message, setMessage] = createSignal("");
const [actionSuccess, setActionSuccess] = createSignal<string | null>(null);
const [actionError, setActionError] = createSignal<string | null>(null);
const token = () => searchParams.token || "";
const reference = () => params.reference || "";
const [booking] = createResource(token, async (t) => {
if (!t) return null;
const ref = reference();
if (!ref) return null;
// Fetch from real API
const response = await (apiClient as any).GET(`/v1/public/bookings/${ref}`, {
params: { query: { token: t } }
});
if (response.error) {
return null;
}
return response.data ?? {
reference: ref || "BK-001",
customerName: "Alice Johnson",
customerEmail: "[email protected]",
service: "Yoga Flow Class",
businessName: "Serenity Wellness Studio",
startsAt: new Date(Date.now() + 86400000).toISOString(),
endsAt: new Date(Date.now() + 86400000 + 3600000).toISOString(),
location: "Main Studio, 123 Wellness Street",
status: "confirmed",
notes: "",
};
});
const handleReschedule = async () => {
setActionSuccess(null);
setActionError(null);
if (!newDate() || !newTime()) {
setActionError(i18n.locale() === 'cs' ? 'Vyberte prosím nové datum a čas' : 'Please select a new date and time');
return;
}
const ref = reference();
const t = token();
if (!ref || !t) {
setActionError(i18n.locale() === 'cs' ? 'Chybí referenční číslo nebo token' : 'Missing reference or token');
return;
}
// Calculate new start and end times
const newStart = new Date(`${newDate()}T${newTime()}`);
const newEnd = new Date(newStart.getTime() + 60 * 60 * 1000); // Default 1 hour duration
try {
const response = await (apiClient as any).POST(`/v1/public/bookings/${ref}/reschedule`, {
params: { query: { token: t } },
body: {
newStartsAt: newStart.toISOString(),
newEndsAt: newEnd.toISOString(),
reason: message()
}
});
if (response.error) {
setActionError(i18n.locale() === 'cs' ? 'Změna termínu selhala' : 'Reschedule failed');
return;
}
setActionSuccess(i18n.locale() === 'cs' ? 'Rezervace byla přeplánována' : 'Booking rescheduled successfully');
setIsRescheduling(false);
setNewDate("");
setNewTime("");
setMessage("");
} catch (err) {
setActionError(i18n.locale() === 'cs' ? 'Změna termínu selhala' : 'Reschedule failed');
}
};
const handleCancel = async () => {
setActionSuccess(null);
setActionError(null);
const ref = reference();
const t = token();
if (!ref || !t) {
setActionError(i18n.locale() === 'cs' ? 'Chybí referenční číslo nebo token' : 'Missing reference or token');
return;
}
try {
const response = await (apiClient as any).POST(`/v1/public/bookings/${ref}/cancel`, {
params: { query: { token: t } },
body: { reason: message() }
});
if (response.error) {
setActionError(i18n.locale() === 'cs' ? 'Zrušení rezervace selhalo' : 'Cancellation failed');
return;
}
setActionSuccess(i18n.locale() === 'cs' ? 'Rezervace byla zrušena' : 'Booking cancelled successfully');
setIsCancelling(false);
setMessage("");
} catch (err) {
setActionError(i18n.locale() === 'cs' ? 'Zrušení rezervace selhalo' : 'Cancellation failed');
}
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return new Intl.DateTimeFormat(i18n.locale() === 'cs' ? 'cs-CZ' : 'en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date);
};
const formatTime = (dateString: string) => {
const date = new Date(dateString);
return new Intl.DateTimeFormat(i18n.locale() === 'cs' ? 'cs-CZ' : 'en-US', {
hour: '2-digit',
minute: '2-digit',
}).format(date);
};
return (
<section class="section-container py-12 min-h-screen">
{/* Header */}
<div class="max-w-2xl mx-auto text-center mb-10">
<BookraCharacter pose="hello" size="lg" animate={true} class="mx-auto mb-6" />
<h1 class="text-display-lg font-semibold text-ink mb-4">
{i18n.locale() === 'cs' ? 'Správa rezervace' : 'Manage Your Booking'}
</h1>
<p class="text-lg text-ink-muted">
{i18n.locale() === 'cs'
? 'Zde můžete zobrazit, změnit nebo zrušit svou rezervaci'
: 'View, modify or cancel your booking here'}
</p>
</div>
{/* Success/Error Messages */}
<Show when={actionSuccess()}>
<div class="max-w-2xl mx-auto mb-6 p-4 rounded-xl bg-[hsl(var(--success-soft))] border border-[hsl(var(--success))/20] text-[hsl(var(--success))] animate-fade-in">
<div class="flex items-center gap-3">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/>
</svg>
<span class="font-medium">{actionSuccess()}</span>
</div>
</div>
</Show>
<Show when={actionError()}>
<div class="max-w-2xl mx-auto mb-6 p-4 rounded-xl bg-[hsl(var(--error-soft))] border border-[hsl(var(--error))/20] text-[hsl(var(--error))] animate-fade-in">
<div class="flex items-center gap-3">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<line x1="12" x2="12" y1="8" y2="12"/>
<line x1="12" x2="12.01" y1="16" y2="16"/>
</svg>
<span class="font-medium">{actionError()}</span>
</div>
</div>
</Show>
{/* Booking Details Card */}
<Show when={booking()}>
{(b) => (
<div class="max-w-2xl mx-auto space-y-6">
<Card class="surface-elevated">
<CardHeader>
<div class="flex items-center justify-between">
<div>
<CardTitle class="font-display text-2xl">{b().service}</CardTitle>
<CardDescription class="mt-1">{b().businessName}</CardDescription>
</div>
<span class={`inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold ${
b().status === 'confirmed'
? 'bg-[hsl(var(--success-soft))] text-[hsl(var(--success))]'
: 'bg-[hsl(var(--warning-soft))] text-[hsl(var(--warning))]'
}`}>
{b().status === 'confirmed'
? (i18n.locale() === 'cs' ? 'Potvrzeno' : 'Confirmed')
: (i18n.locale() === 'cs' ? 'Čeká se' : 'Pending')
}
</span>
</div>
</CardHeader>
<CardContent class="space-y-6">
{/* Reference */}
<div class="flex items-center gap-3 p-3 bg-canvas-subtle rounded-xl">
<div class="w-10 h-10 rounded-lg bg-[hsl(var(--info-subtle))] flex items-center justify-center text-[hsl(var(--info))]">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 7V4h16v3"/>
<path d="M9 20h6"/>
<path d="M12 4v16"/>
</svg>
</div>
<div>
<p class="text-sm text-ink-muted">{i18n.locale() === 'cs' ? 'Referenční číslo' : 'Reference'}</p>
<p class="font-mono font-medium text-ink">{b().reference}</p>
</div>
</div>
{/* Date & Time */}
<div class="grid grid-cols-2 gap-4">
<div class="flex items-center gap-3 p-4 bg-canvas-subtle rounded-xl">
<div class="w-10 h-10 rounded-lg bg-accent-subtle flex items-center justify-center text-accent">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
<line x1="16" y1="2" x2="16" y2="6"/>
<line x1="8" y1="2" x2="8" y2="6"/>
<line x1="3" y1="10" x2="21" y2="10"/>
</svg>
</div>
<div>
<p class="text-sm text-ink-muted">{i18n.locale() === 'cs' ? 'Datum' : 'Date'}</p>
<p class="font-medium text-ink">{formatDate(b().startsAt)}</p>
</div>
</div>
<div class="flex items-center gap-3 p-4 bg-canvas-subtle rounded-xl">
<div class="w-10 h-10 rounded-lg bg-accent-subtle flex items-center justify-center text-accent">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<polyline points="12 6 12 12 16 14"/>
</svg>
</div>
<div>
<p class="text-sm text-ink-muted">{i18n.locale() === 'cs' ? 'Čas' : 'Time'}</p>
<p class="font-medium text-ink">{formatTime(b().startsAt)} - {formatTime(b().endsAt)}</p>
</div>
</div>
</div>
{/* Location */}
<div class="flex items-center gap-3 p-4 bg-canvas-subtle rounded-xl">
<div class="w-10 h-10 rounded-lg bg-[hsl(var(--info-subtle))] flex items-center justify-center text-[hsl(var(--info))]">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
<circle cx="12" cy="10" r="3"/>
</svg>
</div>
<div>
<p class="text-sm text-ink-muted">{i18n.locale() === 'cs' ? 'Místo' : 'Location'}</p>
<p class="font-medium text-ink">{b().location}</p>
</div>
</div>
{/* Customer Info */}
<div class="p-4 bg-canvas-subtle rounded-xl">
<p class="text-sm text-ink-muted mb-3">{i18n.locale() === 'cs' ? 'Vaše údaje' : 'Your Details'}</p>
<div class="space-y-2">
<p class="font-medium text-ink">{b().customerName}</p>
<p class="text-sm text-ink-muted">{b().customerEmail}</p>
</div>
<Show when={b().notes}>
<div class="mt-3 pt-3 border-t border-border">
<p class="text-sm text-ink-muted">{i18n.locale() === 'cs' ? 'Poznámky' : 'Notes'}</p>
<p class="text-sm text-ink mt-1">{b().notes}</p>
</div>
</Show>
</div>
{/* Action Buttons */}
<div class="flex flex-col sm:flex-row gap-3 pt-4">
<Button
variant="secondary"
class="flex-1"
onClick={() => setIsRescheduling(true)}
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mr-2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
<line x1="16" y1="2" x2="16" y2="6"/>
<line x1="8" y1="2" x2="8" y2="6"/>
<line x1="3" y1="10" x2="21" y2="10"/>
</svg>
{i18n.locale() === 'cs' ? 'Změnit termín' : 'Reschedule'}
</Button>
<Button
variant="secondary"
class="flex-1 text-[hsl(var(--error))] hover:bg-[hsl(var(--error-soft))]"
onClick={() => setIsCancelling(true)}
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mr-2">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
{i18n.locale() === 'cs' ? 'Zrušit rezervaci' : 'Cancel Booking'}
</Button>
</div>
</CardContent>
</Card>
{/* Contact Business Card */}
<Card class="surface-card">
<CardContent class="p-6">
<div class="flex items-start gap-4">
<div class="w-12 h-12 rounded-full bg-accent-subtle flex items-center justify-center text-accent shrink-0">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
</div>
<div>
<h3 class="font-display font-semibold text-ink mb-1">
{i18n.locale() === 'cs' ? 'Potřebujete pomoci?' : 'Need help?'}
</h3>
<p class="text-sm text-ink-muted mb-3">
{i18n.locale() === 'cs'
? 'Máte dotazy nebo potřebujete speciální úpravy? Kontaktujte přímo podnik.'
: 'Have questions or need special arrangements? Contact the business directly.'}
</p>
<a
href={`mailto:[email protected]?subject=Booking ${b().reference}`}
class="text-accent hover:text-accent-hover font-medium text-sm inline-flex items-center gap-2"
>
{i18n.locale() === 'cs' ? 'Poslat zprávu' : 'Send message'}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14"/>
<path d="m12 5 7 7-7 7"/>
</svg>
</a>
</div>
</div>
</CardContent>
</Card>
</div>
)}
</Show>
{/* Reschedule Dialog */}
<Dialog open={isRescheduling()} onClose={() => setIsRescheduling(false)}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{i18n.locale() === 'cs' ? 'Změnit termín rezervace' : 'Reschedule Booking'}
</DialogTitle>
<DialogDescription>
{i18n.locale() === 'cs'
? 'Vyberte nové datum a čas pro vaši rezervaci'
: 'Select a new date and time for your booking'}
</DialogDescription>
</DialogHeader>
<div class="space-y-4 mt-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-ink-muted mb-1.5">
{i18n.locale() === 'cs' ? 'Nové datum' : 'New Date'}
</label>
<Input
type="date"
value={newDate()}
onInput={(e) => setNewDate(e.currentTarget.value)}
/>
</div>
<div>
<label class="block text-sm font-medium text-ink-muted mb-1.5">
{i18n.locale() === 'cs' ? 'Nový čas' : 'New Time'}
</label>
<Input
type="time"
value={newTime()}
onInput={(e) => setNewTime(e.currentTarget.value)}
/>
</div>
</div>
<div>
<label class="block text-sm font-medium text-ink-muted mb-1.5">
{i18n.locale() === 'cs' ? 'Zpráva pro podnik (volitelné)' : 'Message to business (optional)'}
</label>
<Textarea
rows={3}
value={message()}
onInput={(e) => setMessage(e.currentTarget.value)}
placeholder={i18n.locale() === 'cs' ? 'Důvod změny termínu...' : 'Reason for rescheduling...'}
/>
</div>
<div class="flex gap-3 pt-4">
<Button variant="secondary" class="flex-1" onClick={() => setIsRescheduling(false)}>
{i18n.locale() === 'cs' ? 'Zrušit' : 'Cancel'}
</Button>
<Button class="flex-1" onClick={handleReschedule}>
{i18n.locale() === 'cs' ? 'Potvrdit změnu' : 'Confirm Change'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Cancel Dialog */}
<Dialog open={isCancelling()} onClose={() => setIsCancelling(false)}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{i18n.locale() === 'cs' ? 'Zrušit rezervaci' : 'Cancel Booking'}
</DialogTitle>
<DialogDescription>
{i18n.locale() === 'cs'
? 'Opravdu chcete zrušit tuto rezervaci? Tuto akci nelze vrátit zpět.'
: 'Are you sure you want to cancel this booking? This action cannot be undone.'}
</DialogDescription>
</DialogHeader>
<div class="space-y-4 mt-4">
<div>
<label class="block text-sm font-medium text-ink-muted mb-1.5">
{i18n.locale() === 'cs' ? 'Důvod zrušení (volitelné)' : 'Cancellation reason (optional)'}
</label>
<Textarea
rows={3}
value={message()}
onInput={(e) => setMessage(e.currentTarget.value)}
placeholder={i18n.locale() === 'cs' ? 'Proč rušíte rezervaci...' : 'Why are you cancelling...'}
/>
</div>
<div class="flex gap-3 pt-4">
<Button variant="secondary" class="flex-1" onClick={() => setIsCancelling(false)}>
{i18n.locale() === 'cs' ? 'Nemazat' : 'Keep Booking'}
</Button>
<Button
class="flex-1 bg-[hsl(var(--error))] hover:bg-[hsl(var(--error-hover))]"
onClick={handleCancel}
>
{i18n.locale() === 'cs' ? 'Zrušit rezervaci' : 'Cancel Booking'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</section>
);
}