This commit is contained in:
Kazimierz Ciołek
2026-07-05 11:10:16 +02:00
commit 4828573cae
54 changed files with 11313 additions and 0 deletions

254
src/routes/+page.svelte Normal file
View File

@@ -0,0 +1,254 @@
<script lang="ts">
import { onMount } from "svelte";
import { ScheduleState } from "$lib/schedule.svelte";
import { MONTH_NAMES, todayISO } from "$lib/dates";
import ScheduleGrid from "$lib/components/ScheduleGrid.svelte";
import ReservationDialog from "$lib/components/ReservationDialog.svelte";
import CabinsDialog from "$lib/components/CabinsDialog.svelte";
import InvoiceDialog from "$lib/components/InvoiceDialog.svelte";
import InvoicePreview from "$lib/components/InvoicePreview.svelte";
import { formatMoney } from "$lib/money";
import * as db from "$lib/db";
import type {
Invoice,
InvoiceDialogParams,
InvoiceItem,
Reservation,
ReservationDialogParams
} from "$lib/types";
const schedule = new ScheduleState();
let dialogParams = $state<ReservationDialogParams | null>(null);
let cabinsOpen = $state(false);
let invoiceParams = $state<InvoiceDialogParams | null>(null);
let preview = $state<{ invoice: Invoice; items: InvoiceItem[] } | null>(null);
async function openPreview(invoiceId: number) {
const [invoice, items] = await Promise.all([
db.getInvoice(invoiceId),
db.getInvoiceItems(invoiceId)
]);
if (invoice) {
preview = { invoice, items };
}
}
onMount(() => {
void schedule.reload();
});
function newReservation(cabinId?: number, date?: string) {
const cid = cabinId ?? schedule.cabins[0]?.id;
if (cid == null) {
cabinsOpen = true;
return;
}
dialogParams = { cabin_id: cid, arrival: date ?? todayISO() };
}
function editReservation(r: Reservation) {
dialogParams = { reservation: r };
}
</script>
<svelte:head>
<title>Słoneczko — grafik rezerwacji</title>
</svelte:head>
<main>
<header class="toolbar">
<button type="button" class="icon" onclick={() => schedule.prevMonth()} aria-label="Poprzedni miesiąc"></button>
<span class="month">{MONTH_NAMES[schedule.month]} {schedule.year}</span>
<button type="button" class="icon" onclick={() => schedule.nextMonth()} aria-label="Następny miesiąc"></button>
<button type="button" onclick={() => schedule.goToday()}>Dziś</button>
<span class="spacer"></span>
<button type="button" onclick={() => (cabinsOpen = true)}>Domki</button>
<button type="button" class="primary" onclick={() => newReservation()}>+ Nowa rezerwacja</button>
</header>
{#if schedule.monthStats && schedule.yearStats}
<div class="stats">
<span>
<strong>{MONTH_NAMES[schedule.month]}:</strong>
{Math.round(schedule.monthStats.nights_total)} dób ·
{formatMoney(schedule.monthStats.revenue_total)}
<span class="confirmed">(potwierdzone: {formatMoney(schedule.monthStats.revenue_confirmed)} zł)</span>
</span>
<span>
<strong>Rok {schedule.year}:</strong>
{Math.round(schedule.yearStats.nights_total)} dób ·
{formatMoney(schedule.yearStats.revenue_total)}
<span class="confirmed">(potwierdzone: {formatMoney(schedule.yearStats.revenue_confirmed)} zł)</span>
</span>
</div>
{/if}
{#if schedule.error}
<p class="error">{schedule.error}</p>
{:else if !schedule.loading && schedule.cabins.length === 0}
<div class="empty">
<p>Nie ma jeszcze żadnych domków. Dodaj je, aby zacząć planować rezerwacje.</p>
<button type="button" class="primary" onclick={() => (cabinsOpen = true)}>Dodaj domki</button>
</div>
{:else}
<ScheduleGrid
cabins={schedule.cabins}
days={schedule.days}
reservations={schedule.reservations}
onCellClick={(cabinId, date) => newReservation(cabinId, date)}
onReservationClick={editReservation}
/>
<footer class="legend">
<span><span class="sw confirmed"></span>Potwierdzona</span>
<span><span class="sw option"></span>Opcja (niepotwierdzona)</span>
<span class="hint">✓ = zaliczka wpłacona · kliknij pustą komórkę, aby dodać rezerwację</span>
</footer>
{/if}
</main>
{#if dialogParams}
<ReservationDialog
params={dialogParams}
cabins={schedule.cabins}
onclose={() => (dialogParams = null)}
onsaved={() => {
dialogParams = null;
void schedule.reload();
}}
oninvoice={(reservation) => {
dialogParams = null;
invoiceParams = { reservation };
void schedule.reload();
}}
/>
{/if}
{#if invoiceParams}
<InvoiceDialog
params={invoiceParams}
onclose={() => (invoiceParams = null)}
onsaved={(id) => {
invoiceParams = null;
void openPreview(id);
}}
/>
{/if}
{#if preview}
<InvoicePreview invoice={preview.invoice} items={preview.items} onclose={() => (preview = null)} />
{/if}
{#if cabinsOpen}
<CabinsDialog
cabins={schedule.cabins}
onclose={() => (cabinsOpen = false)}
onchanged={() => void schedule.reload()}
/>
{/if}
<style>
main {
padding: 14px 16px;
}
.toolbar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.toolbar .month {
min-width: 150px;
text-align: center;
font-size: 16px;
font-weight: 600;
}
.toolbar .spacer {
flex: 1;
}
.toolbar button {
font: inherit;
font-size: 14px;
padding: 7px 14px;
border-radius: 7px;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text);
cursor: pointer;
}
.toolbar button.icon {
width: 34px;
padding: 7px 0;
font-size: 16px;
line-height: 1;
}
.toolbar button.primary {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.toolbar button:hover:not(.primary) {
background: var(--bg);
}
.error {
color: var(--danger);
}
.stats {
display: flex;
gap: 24px;
flex-wrap: wrap;
margin-bottom: 10px;
font-size: 12px;
color: var(--muted);
}
.stats strong {
color: var(--text);
}
.stats .confirmed {
color: var(--muted);
}
.empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 60px 20px;
border: 1px dashed var(--border);
border-radius: 10px;
color: var(--muted);
}
.empty button.primary {
font: inherit;
padding: 8px 16px;
border-radius: 7px;
border: 1px solid var(--accent);
background: var(--accent);
color: #fff;
cursor: pointer;
}
.legend {
display: flex;
align-items: center;
gap: 18px;
margin-top: 10px;
font-size: 12px;
color: var(--muted);
}
.legend .sw {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 3px;
vertical-align: -2px;
margin-right: 6px;
}
.legend .sw.confirmed {
background: var(--bar-confirmed-bg);
}
.legend .sw.option {
background: var(--bar-option-bg);
}
.legend .hint {
margin-left: auto;
}
</style>