"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.
This commit is contained in:
3 files changed
+33
-27
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 { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
@@ -213,6 +213,8 @@ export default async function StaffAppointmentDetailsPage({
|
|||||||
|
|
||||||
{loyalty?.isEnabled && (
|
{loyalty?.isEnabled && (
|
||||||
<div className="border-line border-t pt-4">
|
<div className="border-line border-t pt-4">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
<span className="text-muted block text-[10px] font-bold tracking-widest uppercase">
|
<span className="text-muted block text-[10px] font-bold tracking-widest uppercase">
|
||||||
Loyalty
|
Loyalty
|
||||||
</span>
|
</span>
|
||||||
@@ -222,6 +224,9 @@ export default async function StaffAppointmentDetailsPage({
|
|||||||
<span className="text-accent-loyalty"> · Reward available</span>
|
<span className="text-accent-loyalty"> · Reward available</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</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) => (
|
||||||
@@ -255,32 +260,21 @@ export default async function StaffAppointmentDetailsPage({
|
|||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-3">
|
|
||||||
<a
|
|
||||||
href={`/api/appointments/${appointment.id}/calendar.ics`}
|
|
||||||
className="rounded-md border-line flex min-w-[140px] flex-1 items-center justify-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={`/staff/appointments/${appointment.id}/reschedule`}
|
|
||||||
className="rounded-md border-line min-w-[110px] flex-1 border bg-white px-5 py-3 text-center text-sm font-bold"
|
|
||||||
>
|
|
||||||
Reschedule
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
{appointment.status === "COMPLETED" && (
|
{appointment.status === "COMPLETED" && (
|
||||||
<BookFollowUpButton appointmentId={appointment.id} className="min-w-[110px] flex-1 text-center" />
|
<BookFollowUpButton appointmentId={appointment.id} className="w-full text-center" />
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
<AppointmentStatusActions appointmentId={appointment.id} status={appointment.status} />
|
<AppointmentStatusActions
|
||||||
|
appointmentId={appointment.id}
|
||||||
|
status={appointment.status}
|
||||||
|
layout="stack"
|
||||||
|
rescheduleUrl={
|
||||||
|
RESCHEDULABLE.has(appointment.status)
|
||||||
|
? `/staff/appointments/${appointment.id}/reschedule`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-3">
|
|
||||||
{loyalty?.isEnabled && (
|
|
||||||
<AddStampButton customerId={appointment.customerId} appointmentId={appointment.id} />
|
|
||||||
)}
|
|
||||||
{canRedeemReward && loyalty?.availableReward && (
|
{canRedeemReward && loyalty?.availableReward && (
|
||||||
<RedeemRewardButton
|
<RedeemRewardButton
|
||||||
appointmentId={appointment.id}
|
appointmentId={appointment.id}
|
||||||
@@ -289,7 +283,6 @@ export default async function StaffAppointmentDetailsPage({
|
|||||||
subtotal={totalPrice}
|
subtotal={totalPrice}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,11 +14,14 @@ export function AddStampButton({
|
|||||||
appointmentId,
|
appointmentId,
|
||||||
className,
|
className,
|
||||||
size = "sm",
|
size = "sm",
|
||||||
|
iconOnly = false,
|
||||||
}: {
|
}: {
|
||||||
customerId: string;
|
customerId: string;
|
||||||
appointmentId?: string;
|
appointmentId?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
size?: "sm" | "md" | "lg";
|
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 router = useRouter();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -64,12 +67,13 @@ export function AddStampButton({
|
|||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size={size}
|
size={iconOnly ? "icon" : size}
|
||||||
className={cn("shrink-0", className)}
|
className={cn("shrink-0", className)}
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
|
aria-label={iconOnly ? "Add loyalty stamp" : undefined}
|
||||||
>
|
>
|
||||||
<Stamp className="h-4 w-4" aria-hidden="true" />
|
<Stamp className="h-4 w-4" aria-hidden="true" />
|
||||||
Add Loyalty Stamp
|
{!iconOnly && "Add Loyalty Stamp"}
|
||||||
</Button>
|
</Button>
|
||||||
<Modal open={open} onClose={close} title={result ? "Stamp Added" : "Add Loyalty Stamp"}>
|
<Modal open={open} onClose={close} title={result ? "Stamp Added" : "Add Loyalty Stamp"}>
|
||||||
{result ? (
|
{result ? (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState, useTransition } from "react";
|
import { useState, useTransition } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -55,12 +56,15 @@ export function AppointmentStatusActions({
|
|||||||
status,
|
status,
|
||||||
actions = DEFAULT_ACTIONS,
|
actions = DEFAULT_ACTIONS,
|
||||||
layout = "wrap",
|
layout = "wrap",
|
||||||
|
rescheduleUrl,
|
||||||
}: {
|
}: {
|
||||||
appointmentId: string;
|
appointmentId: string;
|
||||||
status: RawStatus;
|
status: RawStatus;
|
||||||
actions?: AppointmentActionSet;
|
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";
|
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 router = useRouter();
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
@@ -169,6 +173,11 @@ export function AppointmentStatusActions({
|
|||||||
Complete Appointment
|
Complete Appointment
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{rescheduleUrl && (
|
||||||
|
<Button asChild size={buttonSize} className={buttonClassName} variant="secondary">
|
||||||
|
<Link href={rescheduleUrl}>Reschedule</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{status === "CONFIRMED" && (
|
{status === "CONFIRMED" && (
|
||||||
<Button size={buttonSize} className={buttonClassName} variant="secondary" onClick={() => setConfirmKind("no_show")}>
|
<Button size={buttonSize} className={buttonClassName} variant="secondary" onClick={() => setConfirmKind("no_show")}>
|
||||||
Mark No-Show
|
Mark No-Show
|
||||||
|
|||||||
Reference in new issue
Block a user