Phase 6
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.
This commit is contained in:
73 files changed
+8400
-53
No files matched your search
@@ -13,6 +13,7 @@ import { assertOwnAppointment, StaffPermissionDeniedError } from "@/features/sta
|
||||
import { AppointmentStatusActions } from "@/features/staff/components/appointment-status-actions";
|
||||
import { AddStampButton } from "@/features/staff/components/add-stamp-button";
|
||||
import { BookFollowUpButton } from "@/features/staff/components/book-follow-up-button";
|
||||
import { RedeemRewardButton } from "@/features/owner/components/redeem-reward-button";
|
||||
import type { AppointmentStatus } from "@/types/domain";
|
||||
|
||||
export const metadata: Metadata = { title: "Appointment Details" };
|
||||
@@ -74,6 +75,9 @@ export default async function StaffAppointmentDetailsPage({
|
||||
0,
|
||||
);
|
||||
const totalPrice = appointment.services.reduce((sum, s) => sum + Number(s.priceAtBooking), 0);
|
||||
const discount = appointment.discountAmount ? Number(appointment.discountAmount) : 0;
|
||||
const canRedeemReward =
|
||||
loyalty?.availableReward && appointment.discountAmount === null && appointment.status !== "CANCELLED";
|
||||
|
||||
return (
|
||||
<div className="pb-12">
|
||||
@@ -163,6 +167,9 @@ export default async function StaffAppointmentDetailsPage({
|
||||
</span>
|
||||
<p className="font-bold">
|
||||
{totalDuration} min · £{totalPrice.toFixed(2)}
|
||||
{discount > 0 && (
|
||||
<span className="text-accent-loyalty"> (-£{discount.toFixed(2)} reward)</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -257,9 +264,19 @@ export default async function StaffAppointmentDetailsPage({
|
||||
|
||||
<AppointmentStatusActions appointmentId={appointment.id} status={appointment.status} />
|
||||
|
||||
{loyalty?.isEnabled && (
|
||||
<AddStampButton customerId={appointment.customerId} appointmentId={appointment.id} />
|
||||
)}
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{loyalty?.isEnabled && (
|
||||
<AddStampButton customerId={appointment.customerId} appointmentId={appointment.id} />
|
||||
)}
|
||||
{canRedeemReward && loyalty?.availableReward && (
|
||||
<RedeemRewardButton
|
||||
appointmentId={appointment.id}
|
||||
rewardId={loyalty.availableReward.id}
|
||||
rewardLabel={loyalty.availableReward.label}
|
||||
subtotal={totalPrice}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user