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:
95 files changed
+7955
-150
No files matched your search
@@ -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">
|
||||
|
||||
Reference in new issue
Block a user