feat(export): CSV export for bookings and customers

GET /v1/export/bookings.csv (optional from/to bounds) and
/v1/export/customers.csv — UTF-8 with BOM for Excel, labels resolved
at export time, buffered responses so auth failures stay JSON.
Export buttons on the bookings and customers pages.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-18 16:15:36 +02:00
1 parent d86cba61e3
commit 4674203502
12 files changed
+583 -5

No files matched your search

@@ -5,6 +5,7 @@ import { Textarea } from "../../components/ui/textarea";
import { getInitials } from "../../components/dashboard/types";
import { CalendarDaysIcon, PlusIcon, XIcon, EyeIcon } from "../../components/dashboard/icons";
import { apiClient } from "../../lib/api-client";
import { ExportCsvButton } from "../../components/dashboard/export-csv-button";
import { DashboardLayout, useDashboardData } from "./layout";
function BookingsPage() {
@@ -123,9 +124,12 @@ function BookingsPage() {
<h1 class="text-2xl sm:text-3xl font-bold text-ink font-display">{data.i18n.t("dashboard.bookingManagement")}</h1>
<p class="text-ink-muted mt-1">{bookingStats().total} {data.i18n.t("dashboard.totalBookings")}</p>
</div>
<button onClick={() => setShowNewBooking(true)} class="btn-primary text-sm shrink-0">
<PlusIcon /> {data.i18n.t("dashboard.newBooking")}
</button>
<div class="flex items-center gap-2 shrink-0">
<ExportCsvButton path="/v1/export/bookings.csv" filename="bookra-bookings.csv" token={data.token} />
<button onClick={() => setShowNewBooking(true)} class="btn-primary text-sm">
<PlusIcon /> {data.i18n.t("dashboard.newBooking")}
</button>
</div>
</div>
{/* Stats */}
@@ -1,5 +1,6 @@
import { Show, createSignal, createMemo, createResource } from "solid-js";
import { apiClient } from "../../lib/api-client";
import { ExportCsvButton } from "../../components/dashboard/export-csv-button";
import { Input } from "../../components/ui/input";
import { getInitials } from "../../components/dashboard/types";
import { UsersIcon, XIcon } from "../../components/dashboard/icons";
@@ -73,6 +74,7 @@ function CustomersPage() {
<h1 class="text-2xl sm:text-3xl font-bold text-ink font-display">{data.i18n.t("dashboard.customers")}</h1>
<p class="text-ink-muted mt-1">{filteredCustomers().length} {data.i18n.locale() === "cs" ? "zákazníků" : "customers"}</p>
</div>
<ExportCsvButton path="/v1/export/customers.csv" filename="bookra-customers.csv" token={data.token} />
</div>
<div class="surface-card p-4">
<Input type="text" placeholder={data.i18n.t("dashboard.search.customers")} value={searchQuery()} onInput={(e) => setSearchQuery(e.currentTarget.value)} />