mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 15:51:15 +00:00
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:
1 parent
d86cba61e3
commit
4674203502
12 files changed
+583
-5
No files matched your search
@@ -0,0 +1,38 @@
|
||||
import { createSignal, Accessor } from "solid-js";
|
||||
import { apiBaseUrl } from "../../lib/api-client";
|
||||
import { useI18n } from "../../providers/i18n-provider";
|
||||
import { DownloadIcon } from "./icons";
|
||||
|
||||
// Downloads an authenticated CSV endpoint — fetch + blob because the
|
||||
// generated api-client is typed for JSON, not file responses.
|
||||
export function ExportCsvButton(props: { path: string; filename: string; token: Accessor<string | null | undefined> }) {
|
||||
const i18n = useI18n();
|
||||
const [busy, setBusy] = createSignal(false);
|
||||
|
||||
const handleClick = async () => {
|
||||
const bearer = props.token?.();
|
||||
if (!bearer || busy()) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const response = await fetch(`${apiBaseUrl}${props.path}`, {
|
||||
headers: { Authorization: `Bearer ${bearer}` },
|
||||
});
|
||||
if (!response.ok) return;
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = props.filename;
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button onClick={handleClick} disabled={busy()} class="btn-secondary text-sm shrink-0">
|
||||
<DownloadIcon /> {i18n.t("dashboard.exportCsv")}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -234,3 +234,7 @@ export const CheckIcon = () => (
|
||||
export const GlobeIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>
|
||||
);
|
||||
|
||||
export const DownloadIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>
|
||||
);
|
||||
@@ -367,6 +367,7 @@ const dictionaries = {
|
||||
"dashboard.activity.booking.rescheduled": "Rezervace přesunuta",
|
||||
"dashboard.activity.booking.moved": "Přesun na jinou lekci",
|
||||
"dashboard.activity.booking.promoted": "Povýšení z čekací listiny",
|
||||
"dashboard.exportCsv": "Export CSV",
|
||||
"dashboard.demoBanner": "Demo režim",
|
||||
"dashboard.demoBannerDesc": "Prozkoumáte Bookra s ukázkovými daty. Pro plnou funkčnost se zaregistrujte.",
|
||||
"dashboard.demoBannerCTA": "Vytvořit účet",
|
||||
@@ -967,6 +968,7 @@ const dictionaries = {
|
||||
"dashboard.activity.booking.rescheduled": "Booking rescheduled",
|
||||
"dashboard.activity.booking.moved": "Moved to another session",
|
||||
"dashboard.activity.booking.promoted": "Promoted from waitlist",
|
||||
"dashboard.exportCsv": "Export CSV",
|
||||
"dashboard.demoBanner": "Demo mode",
|
||||
"dashboard.demoBannerDesc": "You're exploring Bookra with sample data. Register for full functionality.",
|
||||
"dashboard.demoBannerCTA": "Create account",
|
||||
|
||||
@@ -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)} />
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"root":["./src/App.tsx","./src/main.tsx","./src/components/animated-list.tsx","./src/components/bookra-character.tsx","./src/components/calendar-feed.tsx","./src/components/dashboard-mockup.tsx","./src/components/floating-dock.tsx","./src/components/hover-feature-cards.tsx","./src/components/index.ts","./src/components/integration-modal.tsx","./src/components/location-map.tsx","./src/components/pinned-list.tsx","./src/components/shell.tsx","./src/components/sms-settings.tsx","./src/components/video-player.tsx","./src/components/widget-builder.tsx","./src/components/dashboard/activity-timeline.tsx","./src/components/dashboard/calendar-view.tsx","./src/components/dashboard/icons.tsx","./src/components/dashboard/kpi-card.tsx","./src/components/dashboard/notification-dropdown.tsx","./src/components/dashboard/revenue-chart.tsx","./src/components/dashboard/types.ts","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/dialog.tsx","./src/components/ui/index.ts","./src/components/ui/input.tsx","./src/components/ui/select.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/tooltip.tsx","./src/lib/api-client.ts","./src/lib/map.ts","./src/lib/sentry.ts","./src/lib/types.ts","./src/providers/auth-provider.tsx","./src/providers/i18n-provider.tsx","./src/providers/theme-provider.tsx","./src/routes/about-route.tsx","./src/routes/auth-callback-route.tsx","./src/routes/booking-manage-route.tsx","./src/routes/contact-route.tsx","./src/routes/home-route.tsx","./src/routes/legal-route.tsx","./src/routes/not-found-route.tsx","./src/routes/pricing-route.tsx","./src/routes/public-booking-route.tsx","./src/routes/dashboard/billing-page.tsx","./src/routes/dashboard/bookings-page.tsx","./src/routes/dashboard/customers-page.tsx","./src/routes/dashboard/dashboard-data.ts","./src/routes/dashboard/layout.tsx","./src/routes/dashboard/overview-page.tsx","./src/routes/dashboard/services-page.tsx","./src/routes/dashboard/settings-page.tsx","./src/routes/dashboard/staff-page.tsx","./src/routes/dashboard/zones-page.tsx","./vite.config.ts"],"version":"5.9.3"}
|
||||
{"root":["./src/App.tsx","./src/main.tsx","./src/components/animated-list.tsx","./src/components/bookra-character.tsx","./src/components/calendar-feed.tsx","./src/components/dashboard-mockup.tsx","./src/components/floating-dock.tsx","./src/components/hover-feature-cards.tsx","./src/components/index.ts","./src/components/integration-modal.tsx","./src/components/location-map.tsx","./src/components/pinned-list.tsx","./src/components/shell.tsx","./src/components/sms-settings.tsx","./src/components/video-player.tsx","./src/components/widget-builder.tsx","./src/components/dashboard/activity-timeline.tsx","./src/components/dashboard/calendar-view.tsx","./src/components/dashboard/export-csv-button.tsx","./src/components/dashboard/icons.tsx","./src/components/dashboard/kpi-card.tsx","./src/components/dashboard/notification-dropdown.tsx","./src/components/dashboard/revenue-chart.tsx","./src/components/dashboard/types.ts","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/dialog.tsx","./src/components/ui/index.ts","./src/components/ui/input.tsx","./src/components/ui/select.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/tooltip.tsx","./src/lib/api-client.ts","./src/lib/map.ts","./src/lib/sentry.ts","./src/lib/types.ts","./src/providers/auth-provider.tsx","./src/providers/i18n-provider.tsx","./src/providers/theme-provider.tsx","./src/routes/about-route.tsx","./src/routes/auth-callback-route.tsx","./src/routes/booking-manage-route.tsx","./src/routes/contact-route.tsx","./src/routes/home-route.tsx","./src/routes/legal-route.tsx","./src/routes/not-found-route.tsx","./src/routes/pricing-route.tsx","./src/routes/public-booking-route.tsx","./src/routes/dashboard/billing-page.tsx","./src/routes/dashboard/bookings-page.tsx","./src/routes/dashboard/customers-page.tsx","./src/routes/dashboard/dashboard-data.ts","./src/routes/dashboard/layout.tsx","./src/routes/dashboard/overview-page.tsx","./src/routes/dashboard/services-page.tsx","./src/routes/dashboard/settings-page.tsx","./src/routes/dashboard/staff-page.tsx","./src/routes/dashboard/zones-page.tsx","./vite.config.ts"],"version":"5.9.3"}
|
||||
Reference in new issue
Block a user