From b1fce9c0029fae80997323d567d33aecbc3d540a Mon Sep 17 00:00:00 2001 From: Stefan Lazic Date: Sat, 12 Sep 2026 10:21:29 +0100 Subject: [PATCH] Appointment details layout 2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Add to Calendar" removed from the staff appointment details page — it never showed for owner/customer views anyway, only here. Buttons now stack in one column, in exactly the order you asked: Check In → Reschedule → Mark No-Show → Cancel. Reschedule moved from a separate top row into appointment-status-actions.tsx itself (new rescheduleUrl prop) so it can sit in the right spot relative to the status-specific buttons — this also means it correctly disappears for statuses that were never reschedulable (e.g. once In Progress), same as before. Add Loyalty Stamp is now an icon-only button (add-stamp-button.tsx gained an iconOnly prop) sitting directly opposite "X/Y stamps" inside the card — same click-to-open-modal behavior, just compact. --- .../(staff)/staff/appointments/[id]/page.tsx | 79 +++++++++---------- .../staff/components/add-stamp-button.tsx | 8 +- .../components/appointment-status-actions.tsx | 11 ++- 3 files changed, 52 insertions(+), 46 deletions(-) diff --git a/src/app/(staff)/staff/appointments/[id]/page.tsx b/src/app/(staff)/staff/appointments/[id]/page.tsx index ec725ba..812e2b2 100644 --- a/src/app/(staff)/staff/appointments/[id]/page.tsx +++ b/src/app/(staff)/staff/appointments/[id]/page.tsx @@ -1,5 +1,5 @@ import type { Metadata } from "next"; -import { CalendarPlus, Mail, Phone } from "lucide-react"; +import { Mail, Phone } from "lucide-react"; import Link from "next/link"; import { notFound, redirect } from "next/navigation"; import { Card } from "@/components/ui/card"; @@ -213,15 +213,20 @@ export default async function StaffAppointmentDetailsPage({ {loyalty?.isEnabled && (
- - Loyalty - -

- {loyalty.currentStamps}/{resolveRewardThreshold(loyalty)} stamps - {loyalty.availableReward && ( - · Reward available - )} -

+
+
+ + Loyalty + +

+ {loyalty.currentStamps}/{resolveRewardThreshold(loyalty)} stamps + {loyalty.availableReward && ( + · Reward available + )} +

+
+ +
{loyaltyTransactions.length > 0 && (
); diff --git a/src/features/staff/components/add-stamp-button.tsx b/src/features/staff/components/add-stamp-button.tsx index dd2b88c..8c87b53 100644 --- a/src/features/staff/components/add-stamp-button.tsx +++ b/src/features/staff/components/add-stamp-button.tsx @@ -14,11 +14,14 @@ export function AddStampButton({ appointmentId, className, size = "sm", + iconOnly = false, }: { customerId: string; appointmentId?: string; className?: string; size?: "sm" | "md" | "lg"; + /** Renders just the stamp icon (e.g. opposite a "X/Y stamps" line) instead of icon + label — same action, just compact enough to sit inline with other text. */ + iconOnly?: boolean; }) { const router = useRouter(); const [open, setOpen] = useState(false); @@ -64,12 +67,13 @@ export function AddStampButton({ <> {result ? ( diff --git a/src/features/staff/components/appointment-status-actions.tsx b/src/features/staff/components/appointment-status-actions.tsx index 2c06c2e..0f59bf1 100644 --- a/src/features/staff/components/appointment-status-actions.tsx +++ b/src/features/staff/components/appointment-status-actions.tsx @@ -1,5 +1,6 @@ "use client"; +import Link from "next/link"; import { useRouter } from "next/navigation"; import { useState, useTransition } from "react"; import { Button } from "@/components/ui/button"; @@ -55,12 +56,15 @@ export function AppointmentStatusActions({ status, actions = DEFAULT_ACTIONS, layout = "wrap", + rescheduleUrl, }: { appointmentId: string; status: RawStatus; actions?: AppointmentActionSet; - /** "stack" renders one full-width button per row (Current Appointment); "wrap" (default, used by Appointment Details) keeps the original inline small buttons. */ + /** "stack" renders one full-width button per row (used by both Current Appointment and Appointment Details); "wrap" (default) keeps the original inline small buttons. */ layout?: "wrap" | "stack"; + /** When given, renders a "Reschedule" link right after the primary status button and before Mark No-Show/Cancel — the caller (Appointment Details) decides whether the status is actually reschedulable, this component just renders what it's given. */ + rescheduleUrl?: string; }) { const router = useRouter(); const [isPending, startTransition] = useTransition(); @@ -169,6 +173,11 @@ export function AppointmentStatusActions({ Complete Appointment )} + {rescheduleUrl && ( + + )} {status === "CONFIRMED" && (