mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-29 07:54:44 +00:00
refactor(db): replace sqlc with handwritten pgx repos
- Finish Paddle-to-Stripe billing migration; drop paddle/stripe libs - Renumber migrations; add 00012 memberships+manage_token, 00013 location_type_capacity - Add tests: admin, auth, bookings customer service - Remove graphify-out artifacts and stale env files Authored By: TDvorak <[email protected]> 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
2039669e2c
commit
f2f7439050
93 files changed
+6987
-69060
No files matched your search
@@ -83,17 +83,29 @@ export function CalendarView(props: { bookings: any[]; locale?: string; onBookin
|
||||
return calendarDays().find((d) => d.day === day)?.bookings ?? [];
|
||||
});
|
||||
|
||||
// Booking visual density for bars
|
||||
const eventDot = (status: string) =>
|
||||
status === "confirmed" ? "bg-accent" : status === "pending" ? "bg-ink-subtle" : "bg-border-strong";
|
||||
|
||||
// Up to two readable event chips, then a "+N" overflow marker.
|
||||
const getBookingBars = (bookings: any[]) => {
|
||||
if (bookings.length === 0) return null;
|
||||
const count = bookings.length;
|
||||
const hasMultiple = count >= 2;
|
||||
const hasMany = count >= 3;
|
||||
const visible = bookings.slice(0, 2);
|
||||
const overflow = bookings.length - visible.length;
|
||||
return (
|
||||
<div class="mt-2 space-y-1.5">
|
||||
<div class="h-2 bg-accent/30 rounded-full w-full group-hover:bg-accent/40 transition-colors" />
|
||||
{hasMultiple && <div class="h-2 bg-accent/20 rounded-full w-3/4 group-hover:bg-accent/30 transition-colors" />}
|
||||
{hasMany && <div class="h-2 bg-accent/10 rounded-full w-1/2 group-hover:bg-accent/20 transition-colors" />}
|
||||
<div class="mt-1.5 space-y-1">
|
||||
<For each={visible}>
|
||||
{(b) => (
|
||||
<div class="flex items-center gap-1 rounded-md bg-accent-subtle/60 px-1.5 py-0.5 group-hover:bg-accent-subtle transition-colors">
|
||||
<span class={`w-1.5 h-1.5 rounded-full shrink-0 ${eventDot(b.status)}`} />
|
||||
<span class="text-[10px] font-medium text-ink leading-tight whitespace-nowrap">
|
||||
{new Date(b.startsAt).toLocaleTimeString(isCs() ? "cs-CZ" : "en-US", { hour: "2-digit", minute: "2-digit" })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
<Show when={overflow > 0}>
|
||||
<p class="text-[10px] font-medium text-ink-subtle pl-1">+{overflow} {isCs() ? "další" : "more"}</p>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -145,29 +157,25 @@ export function CalendarView(props: { bookings: any[]; locale?: string; onBookin
|
||||
<div class={`grid grid-cols-7 gap-px bg-border/50 transition-opacity duration-200 ${isAnimating() ? "opacity-0" : "opacity-100"}`}>
|
||||
{calendarDays().map(({ day, bookings, isToday }) => {
|
||||
if (!day) {
|
||||
return <div class="p-3 min-h-[80px] lg:min-h-[100px] bg-canvas/50" />;
|
||||
return <div class="p-2 min-h-[80px] lg:min-h-[96px] bg-canvas/50" />;
|
||||
}
|
||||
return (
|
||||
<button
|
||||
onClick={() => handleDayClick(day)}
|
||||
class={`
|
||||
p-3 min-h-[80px] lg:min-h-[100px] bg-canvas transition-all duration-200 hover:bg-canvas-subtle group relative text-left
|
||||
${isToday ? "ring-2 ring-inset ring-accent bg-accent-soft" : ""}
|
||||
${selectedDay() === day ? "ring-2 ring-inset ring-accent bg-accent-subtle/30" : ""}
|
||||
${bookings.length > 0 && !isToday ? "bg-accent-subtle/10" : ""}
|
||||
p-2 min-h-[80px] lg:min-h-[96px] bg-canvas transition-all duration-200 hover:bg-canvas-subtle group relative text-left flex flex-col
|
||||
${isToday ? "bg-accent-soft" : ""}
|
||||
${selectedDay() === day ? "bg-accent-subtle/40" : ""}
|
||||
`}
|
||||
>
|
||||
<span class={`text-sm transition-colors ${isToday ? "font-semibold text-accent" : "text-ink-muted group-hover:text-ink"}`}>
|
||||
<span class={`inline-flex items-center justify-center w-6 h-6 rounded-full text-sm transition-colors ${
|
||||
isToday
|
||||
? "bg-accent text-canvas font-semibold"
|
||||
: "text-ink-muted group-hover:text-ink"
|
||||
}`}>
|
||||
{day}
|
||||
</span>
|
||||
{getBookingBars(bookings)}
|
||||
{bookings.length > 0 && (
|
||||
<div class="absolute top-2 right-2">
|
||||
<span class="text-[10px] font-semibold text-accent bg-accent/10 px-1.5 py-0.5 rounded-full">
|
||||
{bookings.length}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -26,8 +26,8 @@ export function NotificationDropdown(props: { bookings?: any[]; billing?: any })
|
||||
id: `booking-${i}`,
|
||||
type: "booking",
|
||||
title: `${cs ? "Nová rezervace" : "New booking"} ${b.customerName}`,
|
||||
message: `${b.service} — ${new Date(b.startsAt).toLocaleDateString()}`,
|
||||
time: new Date(b.startsAt).toLocaleDateString(undefined, { month: "short", day: "numeric" }),
|
||||
message: `${b.service} — ${new Date(b.startsAt).toLocaleDateString(cs ? "cs-CZ" : "en-US")}`,
|
||||
time: new Date(b.startsAt).toLocaleDateString(cs ? "cs-CZ" : "en-US", { month: "short", day: "numeric" }),
|
||||
read: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,32 +1,43 @@
|
||||
import { createMemo } from "solid-js";
|
||||
import { createMemo, Show } from "solid-js";
|
||||
import { useI18n } from "../../providers/i18n-provider";
|
||||
|
||||
interface DataPoint {
|
||||
label: string;
|
||||
dateLabel: string;
|
||||
value: number;
|
||||
isToday: boolean;
|
||||
isFuture: boolean;
|
||||
}
|
||||
|
||||
export function RevenueChart(props: { data?: DataPoint[]; bookings?: any[] }) {
|
||||
const i18n = useI18n();
|
||||
const isCs = () => i18n.locale() === "cs";
|
||||
|
||||
const chartData = createMemo(() => {
|
||||
const chartData = createMemo<DataPoint[]>(() => {
|
||||
if (props.data && props.data.length > 0) return props.data;
|
||||
|
||||
// Build last 7 days from bookings
|
||||
const days: DataPoint[] = [];
|
||||
// Bucket bookings into the current calendar week (Mon-Sun) so both
|
||||
// completed and upcoming bookings are visible.
|
||||
const bookings = props.bookings ?? [];
|
||||
const today = new Date();
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const d = new Date(today);
|
||||
d.setDate(d.getDate() - i);
|
||||
const dateStr = d.toISOString().split("T")[0];
|
||||
const monday = new Date(today);
|
||||
monday.setDate(today.getDate() - ((today.getDay() + 6) % 7));
|
||||
|
||||
const days: DataPoint[] = [];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const d = new Date(monday);
|
||||
d.setDate(monday.getDate() + i);
|
||||
const isToday = d.toDateString() === today.toDateString();
|
||||
const count = bookings.filter((b: any) => {
|
||||
const bd = new Date(b.startsAt).toISOString().split("T")[0];
|
||||
return bd === dateStr;
|
||||
const bd = new Date(b.startsAt);
|
||||
return bd.getFullYear() === d.getFullYear() && bd.getMonth() === d.getMonth() && bd.getDate() === d.getDate();
|
||||
}).length;
|
||||
days.push({
|
||||
label: d.toLocaleDateString(i18n.locale() === "cs" ? "cs-CZ" : "en-US", { weekday: "short" }),
|
||||
label: d.toLocaleDateString(isCs() ? "cs-CZ" : "en-US", { weekday: "short" }),
|
||||
dateLabel: d.toLocaleDateString(isCs() ? "cs-CZ" : "en-US", { day: "numeric", month: "numeric" }),
|
||||
value: count,
|
||||
isToday,
|
||||
isFuture: d > today,
|
||||
});
|
||||
}
|
||||
return days;
|
||||
@@ -36,34 +47,46 @@ export function RevenueChart(props: { data?: DataPoint[]; bookings?: any[] }) {
|
||||
const total = createMemo(() => chartData().reduce((s, d) => s + d.value, 0));
|
||||
|
||||
return (
|
||||
<div class="surface-card p-6">
|
||||
<div class="flex items-start justify-between mb-6">
|
||||
<div class="surface-card p-6 h-full flex flex-col">
|
||||
<div class="flex items-start justify-between mb-8">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-ink">{i18n.t("dashboard.revenueTitle")}</h3>
|
||||
<h3 class="text-lg font-semibold text-ink font-display">{i18n.t("dashboard.revenueTitle")}</h3>
|
||||
<p class="text-sm text-ink-muted mt-0.5">{i18n.t("dashboard.revenueSubtitle")}</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-2xl font-bold text-ink tracking-tight">{total()}</p>
|
||||
<div class="text-right shrink-0">
|
||||
<p class="text-3xl font-bold text-ink tracking-tight font-display">{total()}</p>
|
||||
<p class="text-xs text-ink-muted">{i18n.t("dashboard.totalBookings")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-end gap-2 sm:gap-3 h-40">
|
||||
{chartData().map((point, i) => {
|
||||
<div class="flex items-end gap-2 sm:gap-3 flex-1 min-h-40">
|
||||
{chartData().map((point) => {
|
||||
const heightPct = Math.round((point.value / maxValue()) * 100);
|
||||
return (
|
||||
<div class="flex-1 flex flex-col items-center gap-2">
|
||||
<div class="w-full flex-1 flex items-end">
|
||||
<div class="flex-1 flex flex-col items-center gap-2 h-full" title={`${point.dateLabel}: ${point.value}`}>
|
||||
<div class="w-full flex-1 flex items-end relative group">
|
||||
<div
|
||||
class="w-full rounded-t-md bg-accent/80 hover:bg-accent transition-all duration-300 relative group"
|
||||
style={{ height: `${Math.max(heightPct, 4)}%` }}
|
||||
>
|
||||
<div class="absolute -top-7 left-1/2 -translate-x-1/2 px-2 py-0.5 rounded-md bg-ink text-canvas text-xs font-medium opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none">
|
||||
class={`w-full rounded-t-lg transition-all duration-300 ${
|
||||
point.isToday
|
||||
? "bg-accent"
|
||||
: point.isFuture
|
||||
? "bg-accent/40 group-hover:bg-accent/60"
|
||||
: "bg-accent/70 group-hover:bg-accent"
|
||||
}`}
|
||||
style={{ height: `${Math.max(heightPct, point.value > 0 ? 8 : 3)}%`, "min-height": point.value > 0 ? "6px" : "3px", opacity: point.value === 0 ? 0.15 : 1 }}
|
||||
/>
|
||||
<Show when={point.value > 0}>
|
||||
<span class={`absolute -top-6 left-1/2 -translate-x-1/2 text-xs font-semibold ${point.isToday ? "text-accent" : "text-ink-muted"} opacity-0 group-hover:opacity-100 transition-opacity`}>
|
||||
{point.value}
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<span class={`text-[11px] font-medium uppercase tracking-wide ${point.isToday ? "text-accent font-semibold" : "text-ink-subtle"}`}>
|
||||
{point.label}
|
||||
</span>
|
||||
<p class={`text-[10px] leading-tight ${point.isToday ? "text-accent" : "text-ink-subtle/70"}`}>{point.dateLabel}</p>
|
||||
</div>
|
||||
<span class="text-[10px] sm:text-xs text-ink-muted font-medium uppercase">{point.label}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -82,21 +82,21 @@ export function PinnedList(props: PinnedListProps) {
|
||||
const ItemCard = (p: { item: PinnedListItem; pinned: boolean }) => {
|
||||
const content = (
|
||||
<div
|
||||
class={`flex items-center gap-3 rounded-xl px-3 py-3 border transition-all duration-200 ${
|
||||
class={`flex items-center gap-2.5 rounded-lg px-2.5 py-2.5 border transition-all duration-200 ${
|
||||
p.pinned
|
||||
? "bg-accent-soft border-accent/20"
|
||||
: "bg-canvas-subtle/40 border-border/50 hover:border-border"
|
||||
}`}
|
||||
>
|
||||
<Show when={p.item.icon}>
|
||||
<div class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-canvas border border-border text-ink-muted">
|
||||
<div class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-canvas border border-border text-ink-muted">
|
||||
{p.item.icon}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-medium text-ink">{p.item.name}</p>
|
||||
<p class="truncate text-xs text-ink-muted">{p.item.subtitle}</p>
|
||||
<p class="truncate text-[13px] font-medium text-ink">{p.item.name}</p>
|
||||
<p class="truncate text-[11px] text-ink-muted">{p.item.subtitle}</p>
|
||||
</div>
|
||||
|
||||
<DotToggle id={p.item.id} />
|
||||
|
||||
Reference in new issue
Block a user