Owner Administration is complete. Summary of this final stretch of work: Seed data extended with an inactive staff member, a pending invitation, a redeemed-reward fixture, a service with no eligible staff, and a future-appointment fixture for conflict testing — all idempotent, verified against the dev DB. Testing brought to 179 unit / 86 integration / 27 E2E tests, all passing together on a freshly seeded, disposable Postgres: Unit tests for every new pure validator (business hours, categories/services, booking rules, loyalty config, staff creation). Integration tests covering opening-hours conflicts, last-owner protection, staff deactivation/eligibility conflicts, loyalty redemption's atomic race-guard, and owner-vs-barber/business-isolation authorization — all against a real Postgres. 12 new Playwright owner journeys, which surfaced and fixed two genuine issues along the way: a cookie-clearing race condition in the sign-out-mid-test flow (a real, reproducible timing bug, not just a test artifact), and confirmation that seed fixtures upserted by deterministic ID go stale once "today" has moved on — now handled defensively. Desktop responsiveness: converted the single-column Business Details, Booking Rules, and Loyalty Configuration forms to a genuine two-column layout at the lg: breakpoint (verified via measured element positions, not just screenshots), confirmed mobile stacking is unaffected. Final verification: clean tsc, eslint, and next build, plus the full three-suite run against one fresh database as the authoritative pass.
78 lines
3.2 KiB
TypeScript
78 lines
3.2 KiB
TypeScript
import type { Metadata } from "next";
|
|
import { redirect } from "next/navigation";
|
|
import { auth } from "@/auth";
|
|
import { TopNav } from "@/components/navigation/top-nav";
|
|
import { prisma } from "@/lib/db/prisma";
|
|
import { hasPermission } from "@/lib/auth/permissions";
|
|
import { Permission } from "@/generated/prisma/enums";
|
|
import { getProgrammeConfig } from "@/services/loyalty/loyalty.service";
|
|
import { LoyaltyConfigForm } from "@/features/owner/components/loyalty-config-form";
|
|
|
|
export const metadata: Metadata = { title: "Loyalty Programme" };
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export default async function OwnerLoyaltyConfigPage() {
|
|
const session = await auth();
|
|
if (!session?.user) redirect("/sign-in");
|
|
|
|
const canManage = await hasPermission(session.user.id, Permission.MANAGE_LOYALTY_PROGRAMME);
|
|
if (!canManage) redirect("/staff");
|
|
|
|
const owner = await prisma.staffProfile.findUnique({ where: { userId: session.user.id } });
|
|
if (!owner) redirect("/staff");
|
|
|
|
const [programme, services, staff] = await Promise.all([
|
|
getProgrammeConfig(prisma, owner.businessId),
|
|
prisma.service.findMany({ where: { businessId: owner.businessId, isActive: true }, orderBy: { displayOrder: "asc" } }),
|
|
prisma.staffProfile.findMany({
|
|
where: { businessId: owner.businessId, status: "ACTIVE" },
|
|
include: { user: { select: { name: true } } },
|
|
}),
|
|
]);
|
|
|
|
const initialConfig = programme
|
|
? {
|
|
isEnabled: programme.isEnabled,
|
|
stampsRequired: programme.stampsRequired,
|
|
rewardType: programme.rewardType,
|
|
rewardValue: programme.rewardValue?.toString(),
|
|
rewardLabel: programme.rewardLabel ?? undefined,
|
|
maxRewardValue: programme.maxRewardValue?.toString(),
|
|
freeServiceId: programme.freeServiceId ?? undefined,
|
|
autoApplyReward: programme.autoApplyReward,
|
|
requiresStaffApproval: programme.requiresStaffApproval,
|
|
stampsExpireAfterDays: programme.stampsExpireAfterDays ?? undefined,
|
|
rewardExpiresAfterDays: programme.rewardExpiresAfterDays ?? undefined,
|
|
earnOnCancelled: programme.earnOnCancelled,
|
|
earnOnNoShow: programme.earnOnNoShow,
|
|
cardBackgroundColor: programme.cardBackgroundColor ?? undefined,
|
|
cardBackgroundImageUrl: programme.cardBackgroundImageUrl ?? undefined,
|
|
stampIcon: programme.stampIcon ?? undefined,
|
|
termsLine: programme.termsLine ?? undefined,
|
|
}
|
|
: {
|
|
isEnabled: false,
|
|
stampsRequired: 10,
|
|
rewardType: "PERCENTAGE" as const,
|
|
autoApplyReward: true,
|
|
requiresStaffApproval: false,
|
|
earnOnCancelled: false,
|
|
earnOnNoShow: false,
|
|
};
|
|
|
|
return (
|
|
<div className="pb-12">
|
|
<TopNav title="Loyalty Programme" onBack sticky className="bg-stone" />
|
|
<main className="mx-auto max-w-2xl px-6 py-6 lg:max-w-4xl">
|
|
<LoyaltyConfigForm
|
|
initialConfig={initialConfig}
|
|
initialEligibleServiceIds={programme?.eligibleServices.map((s) => s.serviceId) ?? []}
|
|
initialEligibleStaffIds={programme?.eligibleStaff.map((s) => s.staffId) ?? []}
|
|
services={services.map((s) => ({ id: s.id, name: s.name }))}
|
|
staff={staff.map((s) => ({ id: s.id, name: s.user.name ?? "Staff" }))}
|
|
/>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|