mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-29 16:04:45 +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
@@ -1,31 +1,146 @@
|
||||
import { Show, createSignal } from "solid-js";
|
||||
import { For, Show, createResource, createSignal } from "solid-js";
|
||||
import { apiClient } from "../../lib/api-client";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { Select } from "../../components/ui/select";
|
||||
import { LocationMap } from "../../components/location-map";
|
||||
import { MapPinIcon, PlusIcon, XIcon, BanIcon, ClockIcon, SparklesIcon } from "../../components/dashboard/icons";
|
||||
import { MapPinIcon, PlusIcon, BanIcon, ClockIcon, SparklesIcon, Trash2Icon } from "../../components/dashboard/icons";
|
||||
import { DashboardLayout, useDashboardData } from "./layout";
|
||||
|
||||
function ZonesPage() {
|
||||
const data = useDashboardData();
|
||||
const cs = () => data.i18n.locale() === "cs";
|
||||
const [activeTab, setActiveTab] = createSignal<"zones" | "blocked" | "hours">("zones");
|
||||
const [showAddZone, setShowAddZone] = createSignal(false);
|
||||
const [newZoneName, setNewZoneName] = createSignal("");
|
||||
const [newZoneType, setNewZoneType] = createSignal("room");
|
||||
const [newZoneCapacity, setNewZoneCapacity] = createSignal(10);
|
||||
const [showUpgradePrompt, setShowUpgradePrompt] = createSignal(false);
|
||||
const [newBlockedDate, setNewBlockedDate] = createSignal("");
|
||||
const [newBlockedReason, setNewBlockedReason] = createSignal("");
|
||||
const [blockedSaving, setBlockedSaving] = createSignal(false);
|
||||
const [hoursSaving, setHoursSaving] = createSignal<number | null>(null);
|
||||
|
||||
const resolvedLocations = () => [
|
||||
{ id: "z1", name: "Main Hall", type: "hall", capacity: 20, bookingsToday: 5 },
|
||||
{ id: "z2", name: "Private Room A", type: "private", capacity: 4, bookingsToday: 2 },
|
||||
{ id: "z3", name: "Treatment Room", type: "room", capacity: 2, bookingsToday: 8 },
|
||||
];
|
||||
const authHeaders = () => ({ headers: { Authorization: `Bearer ${data.token()}` } });
|
||||
|
||||
const [locations, { refetch: refetchLocations }] = createResource(
|
||||
() => data.token(),
|
||||
async (bearer) => {
|
||||
if (!bearer) return [];
|
||||
if (bearer.startsWith("demo.")) {
|
||||
return [
|
||||
{ id: "z1", name: cs() ? "Hlavní sál" : "Main Hall", type: "hall", capacity: 20 },
|
||||
{ id: "z2", name: cs() ? "Soukromá místnost A" : "Private Room A", type: "private", capacity: 4 },
|
||||
{ id: "z3", name: cs() ? "Ošetřovna" : "Treatment Room", type: "room", capacity: 2 },
|
||||
];
|
||||
}
|
||||
const response = await (apiClient as any).GET("/v1/catalog/locations", authHeaders());
|
||||
return (response.data as any[]) ?? [];
|
||||
},
|
||||
);
|
||||
|
||||
const [blockedDays, { refetch: refetchBlocked }] = createResource(
|
||||
() => [data.token(), activeTab()] as const,
|
||||
async ([bearer, tab]) => {
|
||||
if (!bearer || tab !== "blocked") return [];
|
||||
if (bearer.startsWith("demo.")) return [];
|
||||
const response = await (apiClient as any).GET("/v1/catalog/blocked-days", authHeaders());
|
||||
return (response.data as any[]) ?? [];
|
||||
},
|
||||
);
|
||||
|
||||
const [workingHours, { refetch: refetchHours, mutate: mutateHours }] = createResource(
|
||||
() => [data.token(), activeTab()] as const,
|
||||
async ([bearer, tab]) => {
|
||||
if (!bearer || tab !== "hours") return [];
|
||||
if (bearer.startsWith("demo.")) {
|
||||
return [0, 1, 2, 3, 4, 5, 6].map((d) => ({ dayOfWeek: d, open: d === 0 ? "" : "09:00", close: d === 0 ? "" : "18:00", isOpen: d !== 0 }));
|
||||
}
|
||||
const response = await (apiClient as any).GET("/v1/catalog/working-hours", authHeaders());
|
||||
const rows = (response.data as any[]) ?? [];
|
||||
return [0, 1, 2, 3, 4, 5, 6].map((d) => {
|
||||
const row = rows.find((r) => r.dayOfWeek === d);
|
||||
return { dayOfWeek: d, open: (row?.open ?? "").slice(0, 5), close: (row?.close ?? "").slice(0, 5), isOpen: row?.isOpen ?? false };
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const resolvedLocations = () => locations() ?? [];
|
||||
const locationLimit = () => data.resolvedBilling()?.entitlements?.maxLocations ?? 3;
|
||||
const canAddLocation = () => resolvedLocations().length < locationLimit();
|
||||
const canAddLocation = () => locationLimit() < 0 || resolvedLocations().length < locationLimit();
|
||||
|
||||
const handleAddZone = () => {
|
||||
const handleAddZone = async () => {
|
||||
const bearer = data.token();
|
||||
if (!newZoneName().trim()) return;
|
||||
if (!canAddLocation()) { setShowUpgradePrompt(true); setShowAddZone(false); return; }
|
||||
setNewZoneName(""); setNewZoneType("room"); setNewZoneCapacity(10); setShowAddZone(false);
|
||||
if (!bearer || bearer.startsWith("demo.")) {
|
||||
data.setDemoNotice(cs() ? "V demo režimu nelze ukládat." : "Cannot save in demo mode.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await (apiClient as any).POST("/v1/catalog/locations", {
|
||||
...authHeaders(),
|
||||
body: { name: newZoneName().trim(), type: newZoneType(), capacity: newZoneCapacity() },
|
||||
});
|
||||
setNewZoneName(""); setNewZoneType("room"); setNewZoneCapacity(10); setShowAddZone(false);
|
||||
void refetchLocations();
|
||||
} catch {
|
||||
data.setDemoNotice(cs() ? "Uložení zóny selhalo." : "Failed to save location.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteZone = async (id: string) => {
|
||||
const bearer = data.token();
|
||||
if (!bearer || bearer.startsWith("demo.")) { data.setDemoNotice(cs() ? "V demo režimu nelze mazat." : "Cannot delete in demo mode."); return; }
|
||||
try {
|
||||
await (apiClient as any).DELETE(`/v1/catalog/locations/${id}`, authHeaders());
|
||||
void refetchLocations();
|
||||
} catch {
|
||||
data.setDemoNotice(cs() ? "Smazání selhalo." : "Delete failed.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddBlockedDay = async () => {
|
||||
const bearer = data.token();
|
||||
if (!newBlockedDate()) return;
|
||||
if (!bearer || bearer.startsWith("demo.")) { data.setDemoNotice(cs() ? "V demo režimu nelze ukládat." : "Cannot save in demo mode."); return; }
|
||||
setBlockedSaving(true);
|
||||
try {
|
||||
await (apiClient as any).POST("/v1/catalog/blocked-days", {
|
||||
...authHeaders(),
|
||||
body: { date: new Date(`${newBlockedDate()}T00:00:00`).toISOString(), type: "closure", reason: newBlockedReason().trim() || (cs() ? "Zavřeno" : "Closed") },
|
||||
});
|
||||
setNewBlockedDate(""); setNewBlockedReason("");
|
||||
void refetchBlocked();
|
||||
} catch {
|
||||
data.setDemoNotice(cs() ? "Uložení selhalo." : "Save failed.");
|
||||
} finally {
|
||||
setBlockedSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteBlockedDay = async (id: string) => {
|
||||
const bearer = data.token();
|
||||
if (!bearer || bearer.startsWith("demo.")) return;
|
||||
try {
|
||||
await (apiClient as any).DELETE(`/v1/catalog/blocked-days/${id}`, authHeaders());
|
||||
void refetchBlocked();
|
||||
} catch { /* noop */ }
|
||||
};
|
||||
|
||||
const handleSaveHours = async (day: { dayOfWeek: number; open: string; close: string; isOpen: boolean }) => {
|
||||
const bearer = data.token();
|
||||
if (!bearer || bearer.startsWith("demo.")) { data.setDemoNotice(cs() ? "V demo režimu nelze ukládat." : "Cannot save in demo mode."); return; }
|
||||
setHoursSaving(day.dayOfWeek);
|
||||
try {
|
||||
await (apiClient as any).PUT(`/v1/catalog/working-hours/${day.dayOfWeek}`, {
|
||||
...authHeaders(),
|
||||
body: { open: day.open, close: day.close, isOpen: day.isOpen },
|
||||
});
|
||||
void refetchHours();
|
||||
} catch {
|
||||
data.setDemoNotice(cs() ? "Uložení selhalo." : "Save failed.");
|
||||
} finally {
|
||||
setHoursSaving(null);
|
||||
}
|
||||
};
|
||||
|
||||
const typeLabel = (type: string) => {
|
||||
@@ -33,12 +148,19 @@ function ZonesPage() {
|
||||
return map[type] || type;
|
||||
};
|
||||
|
||||
const dayLabel = (d: number) => {
|
||||
const labels = cs()
|
||||
? ["Neděle", "Pondělí", "Úterý", "Středa", "Čtvrtek", "Pátek", "Sobota"]
|
||||
: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||
return labels[d];
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl sm:text-3xl font-bold text-ink font-display">{data.i18n.t("dashboard.zones")}</h1>
|
||||
<p class="text-ink-muted mt-1">{resolvedLocations().length} {data.i18n.locale() === "cs" ? "zon" : "zones"}</p>
|
||||
<p class="text-ink-muted mt-1">{resolvedLocations().length} {cs() ? "zón" : "locations"}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,39 +177,38 @@ function ZonesPage() {
|
||||
</div>
|
||||
|
||||
{activeTab() === "zones" && (
|
||||
<div class="space-y-6">
|
||||
<LocationMap
|
||||
coordinates={{ latitude: 50.0755, longitude: 14.4378, zoom: 14, address: data.i18n.locale() === "cs" ? "Praha, Česko" : "Prague, Czechia", source: "coordinates" }}
|
||||
markerColor={data.resolvedBootstrap()?.brand?.primaryColor}
|
||||
height={320}
|
||||
class="shadow-sm"
|
||||
/>
|
||||
<div class="surface-card overflow-hidden">
|
||||
<div class="p-6 border-b border-border flex items-center justify-between">
|
||||
<h3 class="text-lg font-display font-semibold text-ink">{data.i18n.t("dashboard.zones")}</h3>
|
||||
<Show when={canAddLocation()} fallback={
|
||||
<button onClick={() => setShowUpgradePrompt(true)} class="flex items-center gap-2 px-4 py-2 bg-canvas-subtle text-ink-muted border border-border rounded-lg text-sm opacity-60 cursor-not-allowed">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="18" height="11" x="3" y="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
{data.i18n.t("dashboard.zone.limitReached")}
|
||||
</button>
|
||||
}>
|
||||
<button onClick={() => setShowAddZone(true)} class="btn-primary text-sm"><PlusIcon /> {data.i18n.t("dashboard.zone.add")}</button>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={showAddZone()}>
|
||||
<div class="p-6 border-b border-border bg-canvas-subtle/50">
|
||||
<h4 class="font-medium text-ink mb-4">{data.i18n.t("dashboard.zone.add")}</h4>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<Input type="text" label={data.i18n.t("dashboard.zone.name")} value={newZoneName()} onInput={(e) => setNewZoneName(e.currentTarget.value)} placeholder={data.i18n.locale() === "cs" ? "např. Hlavní sál" : "e.g. Main Hall"} />
|
||||
<Select label={data.i18n.t("dashboard.zone.type")} value={newZoneType()} onChange={(v) => setNewZoneType(v)} options={[{ value: "room", label: data.i18n.t("dashboard.zone.rooms") }, { value: "private", label: data.i18n.t("dashboard.zone.private") }, { value: "hall", label: data.i18n.t("dashboard.zone.hall") }]} />
|
||||
<Input type="number" label={data.i18n.t("dashboard.zone.capacity")} value={newZoneCapacity()} onInput={(e) => setNewZoneCapacity(parseInt(e.currentTarget.value) || 10)} />
|
||||
</div>
|
||||
<div class="flex gap-3 mt-4">
|
||||
<button onClick={handleAddZone} class="btn-primary text-sm">{data.i18n.t("dashboard.settings.save")}</button>
|
||||
<button onClick={() => setShowAddZone(false)} class="btn-secondary text-sm">{data.i18n.t("common.cancel")}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="surface-card overflow-hidden">
|
||||
<div class="p-6 border-b border-border flex items-center justify-between">
|
||||
<h3 class="text-lg font-display font-semibold text-ink">{data.i18n.t("dashboard.zones")}</h3>
|
||||
<Show when={canAddLocation()} fallback={
|
||||
<button onClick={() => setShowUpgradePrompt(true)} class="flex items-center gap-2 px-4 py-2 bg-canvas-subtle text-ink-muted border border-border rounded-lg text-sm opacity-60 cursor-not-allowed">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="18" height="11" x="3" y="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
{data.i18n.t("dashboard.zone.limitReached")}
|
||||
</button>
|
||||
}>
|
||||
<button onClick={() => setShowAddZone(true)} class="btn-primary text-sm"><PlusIcon /> {data.i18n.t("dashboard.zone.add")}</button>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={showAddZone()}>
|
||||
<div class="p-6 border-b border-border bg-canvas-subtle/50">
|
||||
<h4 class="font-medium text-ink mb-4">{data.i18n.t("dashboard.zone.add")}</h4>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<Input type="text" label={data.i18n.t("dashboard.zone.name")} value={newZoneName()} onInput={(e) => setNewZoneName(e.currentTarget.value)} placeholder={cs() ? "např. Hlavní sál" : "e.g. Main Hall"} />
|
||||
<Select label={data.i18n.t("dashboard.zone.type")} value={newZoneType()} onChange={(v) => setNewZoneType(v)} options={[{ value: "room", label: data.i18n.t("dashboard.zone.rooms") }, { value: "private", label: data.i18n.t("dashboard.zone.private") }, { value: "hall", label: data.i18n.t("dashboard.zone.hall") }]} />
|
||||
<Input type="number" label={data.i18n.t("dashboard.zone.capacity")} value={newZoneCapacity()} onInput={(e) => setNewZoneCapacity(parseInt(e.currentTarget.value) || 10)} />
|
||||
</div>
|
||||
<div class="flex gap-3 mt-4">
|
||||
<button onClick={() => void handleAddZone()} class="btn-primary text-sm">{data.i18n.t("dashboard.settings.save")}</button>
|
||||
<button onClick={() => setShowAddZone(false)} class="btn-secondary text-sm">{data.i18n.t("common.cancel")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={resolvedLocations().length > 0} fallback={
|
||||
<div class="p-12 text-center">
|
||||
<p class="text-ink-muted font-medium">{data.i18n.t("dashboard.empty.title")}</p>
|
||||
<p class="text-sm text-ink-subtle mt-1">{cs() ? "Přidejte první zónu nebo místnost." : "Add your first location or room."}</p>
|
||||
</div>
|
||||
}>
|
||||
<div class="divide-y divide-border/60">
|
||||
{resolvedLocations().map((zone: any) => (
|
||||
<div class="flex items-center justify-between p-6 hover:bg-canvas-subtle/30 transition-colors">
|
||||
@@ -98,30 +219,98 @@ function ZonesPage() {
|
||||
<p class="text-sm text-ink-muted">{typeLabel(zone.type)} • {data.i18n.t("dashboard.zone.capacity")}: {zone.capacity}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-sm font-medium text-ink">{zone.bookingsToday}</p>
|
||||
<p class="text-sm text-ink-muted">{zone.bookingsToday} {data.i18n.locale() === "cs" ? "rezervací dnes" : "bookings today"}</p>
|
||||
</div>
|
||||
<button onClick={() => void handleDeleteZone(zone.id)} class="p-2 text-ink-subtle hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors" aria-label={cs() ? "Smazat" : "Delete"}>
|
||||
<Trash2Icon />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab() === "blocked" && (
|
||||
<div class="surface-card p-12 text-center">
|
||||
<div class="w-16 h-16 rounded-full bg-canvas-subtle flex items-center justify-center mx-auto mb-4"><BanIcon /></div>
|
||||
<p class="text-ink-muted font-medium">{data.i18n.t("dashboard.empty.title")}</p>
|
||||
<p class="text-sm text-ink-muted">{data.i18n.locale() === "cs" ? "Spravujte zóny, blokované dny a pracovní dobu." : "Manage locations, blocked days and working hours."}</p>
|
||||
<div class="surface-card overflow-hidden">
|
||||
<div class="p-6 border-b border-border">
|
||||
<h3 class="text-lg font-display font-semibold text-ink">{data.i18n.t("dashboard.zone.blockedDays")}</h3>
|
||||
<p class="text-sm text-ink-muted mt-1">{cs() ? "Dny, kdy nejsou dostupné rezervace." : "Days when bookings are unavailable."}</p>
|
||||
</div>
|
||||
<div class="p-6 border-b border-border bg-canvas-subtle/50">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<Input type="date" label={cs() ? "Datum" : "Date"} value={newBlockedDate()} onInput={(e) => setNewBlockedDate(e.currentTarget.value)} />
|
||||
<Input type="text" label={cs() ? "Důvod" : "Reason"} value={newBlockedReason()} onInput={(e) => setNewBlockedReason(e.currentTarget.value)} placeholder={cs() ? "např. Státní svátek" : "e.g. Public holiday"} />
|
||||
<div class="flex items-end">
|
||||
<button onClick={() => void handleAddBlockedDay()} disabled={blockedSaving() || !newBlockedDate()} class="btn-primary text-sm w-full sm:w-auto disabled:opacity-60">
|
||||
{blockedSaving() ? data.i18n.t("dashboard.saving") : data.i18n.t("dashboard.settings.save")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={(blockedDays() ?? []).length > 0} fallback={
|
||||
<div class="p-12 text-center">
|
||||
<div class="w-16 h-16 rounded-full bg-canvas-subtle flex items-center justify-center mx-auto mb-4"><BanIcon /></div>
|
||||
<p class="text-ink-muted font-medium">{data.i18n.t("dashboard.empty.title")}</p>
|
||||
<p class="text-sm text-ink-muted">{cs() ? "Zatím žádné blokované dny." : "No blocked days yet."}</p>
|
||||
</div>
|
||||
}>
|
||||
<div class="divide-y divide-border/60">
|
||||
<For each={blockedDays() ?? []}>
|
||||
{(day: any) => (
|
||||
<div class="flex items-center justify-between p-4 px-6">
|
||||
<div>
|
||||
<p class="font-medium text-ink">{new Date(day.date).toLocaleDateString(cs() ? "cs-CZ" : "en-US", { weekday: "long", day: "numeric", month: "long", year: "numeric" })}</p>
|
||||
<p class="text-sm text-ink-muted">{day.reason || "—"}</p>
|
||||
</div>
|
||||
<button onClick={() => void handleDeleteBlockedDay(day.id)} class="p-2 text-ink-subtle hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"><Trash2Icon /></button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab() === "hours" && (
|
||||
<div class="surface-card p-12 text-center">
|
||||
<div class="w-16 h-16 rounded-full bg-canvas-subtle flex items-center justify-center mx-auto mb-4"><ClockIcon /></div>
|
||||
<p class="text-ink-muted font-medium">{data.i18n.t("dashboard.empty.title")}</p>
|
||||
<p class="text-ink-muted text-sm">{data.i18n.locale() === "cs" ? "Nastavení pracovní doby bude dostupné brzy." : "Working hours settings coming soon."}</p>
|
||||
<div class="surface-card overflow-hidden">
|
||||
<div class="p-6 border-b border-border">
|
||||
<h3 class="text-lg font-display font-semibold text-ink">{data.i18n.t("dashboard.zone.workingHours")}</h3>
|
||||
<p class="text-sm text-ink-muted mt-1">{cs() ? "Nastavte dny a časy, kdy přijímáte rezervace." : "Set the days and times you accept bookings."}</p>
|
||||
</div>
|
||||
<div class="divide-y divide-border/60">
|
||||
<For each={workingHours() ?? []}>
|
||||
{(day) => {
|
||||
const isOpen = () => day.isOpen;
|
||||
const setField = (patch: Partial<typeof day>) => {
|
||||
const updated = { ...day, ...patch };
|
||||
mutateHours((workingHours() ?? []).map((d) => (d.dayOfWeek === day.dayOfWeek ? updated : d)));
|
||||
};
|
||||
return (
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-3 p-4 px-6">
|
||||
<div class="w-32 shrink-0 font-medium text-ink">{dayLabel(day.dayOfWeek)}</div>
|
||||
<label class="flex items-center gap-2 text-sm text-ink-muted shrink-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isOpen()}
|
||||
onChange={(e) => setField({ isOpen: e.currentTarget.checked, open: e.currentTarget.checked ? (day.open || "09:00") : day.open, close: e.currentTarget.checked ? (day.close || "18:00") : day.close })}
|
||||
class="rounded border-border accent-[#c25e3a]"
|
||||
/>
|
||||
{cs() ? "Otevřeno" : "Open"}
|
||||
</label>
|
||||
<Show when={isOpen()}>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="time" value={day.open} onInput={(e) => setField({ open: e.currentTarget.value })} class="px-3 py-2 bg-canvas-subtle border border-border rounded-lg text-sm text-ink" />
|
||||
<span class="text-ink-muted">–</span>
|
||||
<input type="time" value={day.close} onInput={(e) => setField({ close: e.currentTarget.value })} class="px-3 py-2 bg-canvas-subtle border border-border rounded-lg text-sm text-ink" />
|
||||
</div>
|
||||
</Show>
|
||||
<button onClick={() => void handleSaveHours(workingHours()?.find((d) => d.dayOfWeek === day.dayOfWeek) ?? day)} disabled={hoursSaving() === day.dayOfWeek} class="sm:ml-auto px-4 py-2 bg-accent text-canvas text-sm font-medium rounded-lg hover:bg-accent-hover transition-colors disabled:opacity-60">
|
||||
{hoursSaving() === day.dayOfWeek ? "…" : data.i18n.t("dashboard.settings.save")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -133,8 +322,8 @@ function ZonesPage() {
|
||||
<div class="w-10 h-10 rounded-full bg-accent-subtle flex items-center justify-center shrink-0"><SparklesIcon /></div>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-medium text-ink mb-1">{data.i18n.t("dashboard.locationLimitReached")}</h4>
|
||||
<p class="text-sm text-ink-muted">{data.i18n.locale() === "cs" ? "Váš plán umožňuje pouze omezený počet lokací. Pro více lokací upgradujte." : "Your plan only allows a limited number of locations. Upgrade for more."}</p>
|
||||
<div class="flex gap-3">
|
||||
<p class="text-sm text-ink-muted">{cs() ? "Váš plán umožňuje pouze omezený počet lokací. Pro více lokací upgradujte." : "Your plan only allows a limited number of locations. Upgrade for more."}</p>
|
||||
<div class="flex gap-3 mt-3">
|
||||
<button onClick={() => { setShowUpgradePrompt(false); void data.openCheckout(); }} class="px-4 py-2 bg-accent text-canvas text-sm font-medium rounded-lg hover:bg-accent-hover transition-colors">{data.i18n.t("dashboard.upgrade")}</button>
|
||||
<button onClick={() => setShowUpgradePrompt(false)} class="px-4 py-2 bg-canvas-subtle text-ink-muted text-sm font-medium rounded-lg hover:bg-canvas transition-colors">{data.i18n.t("dashboard.close")}</button>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user