Owner appointment details
CI / ci (push) Canceled after 0s

same changes applied to this route
This commit is contained in:
Stefan Lazic committed 2026-09-12 10:52:48 +01:00
1 parent b1fce9c002
commit 0e39ca7cdc
2 files changed
+34 -36

No files matched your search

@@ -1,5 +1,5 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { CalendarPlus, Mail, Phone } from "lucide-react"; import { Mail, Phone } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import { auth } from "@/auth"; import { auth } from "@/auth";
@@ -196,11 +196,16 @@ export default async function OwnerAppointmentDetailsPage({
{loyalty?.isEnabled && ( {loyalty?.isEnabled && (
<div className="border-line border-t pt-4"> <div className="border-line border-t pt-4">
<span className="text-muted block text-[10px] font-bold tracking-widest uppercase">Loyalty</span> <div className="flex items-center justify-between gap-3">
<p className="text-sm font-bold"> <div>
{loyalty.currentStamps}/{resolveRewardThreshold(loyalty)} stamps <span className="text-muted block text-[10px] font-bold tracking-widest uppercase">Loyalty</span>
{loyalty.availableReward && <span className="text-accent-loyalty"> · Reward available</span>} <p className="text-sm font-bold">
</p> {loyalty.currentStamps}/{resolveRewardThreshold(loyalty)} stamps
{loyalty.availableReward && <span className="text-accent-loyalty"> · Reward available</span>}
</p>
</div>
<AddStampButton customerId={appointment.customerId} appointmentId={appointment.id} iconOnly />
</div>
{loyaltyTransactions.length > 0 && ( {loyaltyTransactions.length > 0 && (
<ul className="mt-2 space-y-1"> <ul className="mt-2 space-y-1">
{loyaltyTransactions.map((t) => ( {loyaltyTransactions.map((t) => (
@@ -229,41 +234,31 @@ export default async function OwnerAppointmentDetailsPage({
)} )}
</Card> </Card>
<div className="flex flex-wrap gap-3"> {appointment.status === "COMPLETED" && (
<a <BookFollowUpButton appointmentId={appointment.id} className="w-full text-center" />
href={`/api/appointments/${appointment.id}/calendar.ics`} )}
className="rounded-md border-line flex items-center gap-2 border bg-white px-5 py-3 text-sm font-bold"
>
<CalendarPlus className="text-accent h-4 w-4" aria-hidden="true" /> Add to Calendar
</a>
{RESCHEDULABLE.has(appointment.status) && (
<Link
href={`/owner/appointments/${appointment.id}/reschedule`}
className="rounded-md border-line border bg-white px-5 py-3 text-sm font-bold"
>
Reschedule / Reassign
</Link>
)}
{appointment.status === "COMPLETED" && <BookFollowUpButton appointmentId={appointment.id} />}
</div>
<AppointmentStatusActions <AppointmentStatusActions
appointmentId={appointment.id} appointmentId={appointment.id}
status={appointment.status} status={appointment.status}
actions={ownerAppointmentActions} actions={ownerAppointmentActions}
layout="stack"
rescheduleUrl={
RESCHEDULABLE.has(appointment.status)
? `/owner/appointments/${appointment.id}/reschedule`
: undefined
}
rescheduleLabel="Reschedule / Reassign"
/> />
<div className="flex flex-wrap gap-3"> {canRedeemReward && loyalty.availableReward && (
{loyalty?.isEnabled && <AddStampButton customerId={appointment.customerId} appointmentId={appointment.id} />} <RedeemRewardButton
{canRedeemReward && loyalty.availableReward && ( appointmentId={appointment.id}
<RedeemRewardButton rewardId={loyalty.availableReward.id}
appointmentId={appointment.id} rewardLabel={loyalty.availableReward.label}
rewardId={loyalty.availableReward.id} subtotal={subtotal}
rewardLabel={loyalty.availableReward.label} />
subtotal={subtotal} )}
/>
)}
</div>
</main> </main>
</div> </div>
); );
@@ -57,14 +57,17 @@ export function AppointmentStatusActions({
actions = DEFAULT_ACTIONS, actions = DEFAULT_ACTIONS,
layout = "wrap", layout = "wrap",
rescheduleUrl, rescheduleUrl,
rescheduleLabel = "Reschedule",
}: { }: {
appointmentId: string; appointmentId: string;
status: RawStatus; status: RawStatus;
actions?: AppointmentActionSet; actions?: AppointmentActionSet;
/** "stack" renders one full-width button per row (used by both Current Appointment and Appointment Details); "wrap" (default) 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"; 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. */ /** 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; rescheduleUrl?: string;
/** Owner's variant reads "Reschedule / Reassign" (they can also hand the appointment to a different staff member); staff's own reschedule can't reassign anyone, so it stays plain "Reschedule". */
rescheduleLabel?: string;
}) { }) {
const router = useRouter(); const router = useRouter();
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
@@ -175,7 +178,7 @@ export function AppointmentStatusActions({
)} )}
{rescheduleUrl && ( {rescheduleUrl && (
<Button asChild size={buttonSize} className={buttonClassName} variant="secondary"> <Button asChild size={buttonSize} className={buttonClassName} variant="secondary">
<Link href={rescheduleUrl}>Reschedule</Link> <Link href={rescheduleUrl}>{rescheduleLabel}</Link>
</Button> </Button>
)} )}
{status === "CONFIRMED" && ( {status === "CONFIRMED" && (