Updates 1
CI / ci (push) Canceled after 0s

The session built:

Appointments page: a single list, newest first.
Block Time: edit and delete.
Customer CSV export: with barber notes excluded.
Guest-link regression test: a test that an appointment id alone is rejected, plus fixes to the stale docs.

It built the scheduler, job runner with live progress, backup and restore, and the health alerts. Its own report says a real delete-and-restore on throwaway Docker passed, but I haven’t repeated that. It also says real SMTP and large dumps were not exercised. The manager image needs rebuilding for the backups to work, because it now installs 7zip.
This commit is contained in:
Stefan Lazic committed 2026-10-17 22:29:00 +01:00
1 parent 9f981173f4
commit 17eee5b932
95 files changed
+7955 -150

No files matched your search

+22 -1
View File
@@ -1,7 +1,7 @@
import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import { CalendarX2, Mail, Phone } from "lucide-react";
import { CalendarX2, Download, Mail, Phone } from "lucide-react";
import { WhatsAppButton } from "@/features/staff/components/whatsapp-button";
import { notFound, redirect } from "next/navigation";
import { AppointmentCard } from "@/components/ui/appointment-card";
@@ -12,6 +12,8 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { TopNav } from "@/components/navigation/top-nav";
import { prisma } from "@/lib/db/prisma";
import { clientDirectoryWhere } from "@/services/customer/client-directory";
import { hasPermission } from "@/lib/auth/permissions";
import { Permission } from "@/generated/prisma/enums";
import { getActingBusinessContext } from "@/lib/business/get-acting-business-context";
import { getLoyaltyHistory, getLoyaltySummary } from "@/services/loyalty/loyalty.service";
import { resolveRewardThreshold } from "@/services/loyalty/reward-threshold";
@@ -60,6 +62,9 @@ export default async function StaffCustomerDetailsPage({
(await prisma.customerProfile.count({ where: { AND: [{ id }, clientDirectoryWhere(context.businessId)] } })) > 0;
if (!inDirectory) notFound();
// Only offered to those who may use it; the export route re-checks the same permission itself.
const canExport = await hasPermission(context.userId, Permission.MANAGE_BUSINESS_PROFILE);
const [upcoming, past, cancelled, notesRaw, loyalty] = await Promise.all([
listAppointments(prisma, {
customerId: id,
@@ -271,6 +276,22 @@ export default async function StaffCustomerDetailsPage({
</p>
</div>
</div>
{canExport && (
<div className="border-line border-t pt-4">
{/* A plain link, not next/link: the response is a file download, not a page. */}
<a
href={`/api/customers/${customer.id}/export`}
download
className="border-line flex h-12 w-full items-center justify-center gap-2 rounded-md border bg-white px-4 text-xs font-bold"
>
<Download className="text-accent h-3.5 w-3.5" aria-hidden="true" /> Export client data (CSV)
</a>
<p className="text-muted mt-2 text-xs">
Profile, appointments, loyalty and messages sent. Private staff notes are not included.
</p>
</div>
)}
</Card>
<div className="mt-4 space-y-4 lg:mt-0">