"use server"; import { prisma } from "@/lib/db/prisma"; import { getActingStaff } from "@/lib/staff/get-acting-staff"; import { cancelBooking, rescheduleBooking } from "@/services/scheduling/booking.service"; import { checkIn, complete, confirm, getAppointmentById, markNoShow, start, } from "@/services/scheduling/appointment.service"; import { awardStampForCompletedAppointment } from "@/services/loyalty/loyalty.service"; import { assertOwnAppointment, StaffPermissionDeniedError } from "../authorization"; /** * Every status transition here does exactly three things: load the * appointment, assert it's this staff member's own (in their own * business), and delegate to AppointmentService/BookingService for the * actual transition — the transition rules themselves (which statuses can * move to which) live entirely in AppointmentService's own state machine, * never re-implemented here. Completing an appointment additionally calls * LoyaltyService, since "complete this visit" and "award a stamp for it" * are two different domain responsibilities this layer is allowed to * orchestrate together — see docs/phase-5-summary.md. */ export interface StaffAppointmentActionResult { success: boolean; error?: string; loyaltyStampAwarded?: boolean; } async function loadOwnAppointment(appointmentId: string) { const staff = await getActingStaff(); if (!staff) return { staff: null, appointment: null, error: "You must be signed in as staff." } as const; const appointment = await getAppointmentById(prisma, appointmentId); if (!appointment) return { staff, appointment: null, error: "Appointment not found." } as const; try { assertOwnAppointment(appointment, staff); } catch (error) { if (error instanceof StaffPermissionDeniedError) return { staff, appointment: null, error: error.message } as const; throw error; } return { staff, appointment, error: null } as const; } export async function confirmAppointmentAction( appointmentId: string, ): Promise { const { appointment, error } = await loadOwnAppointment(appointmentId); if (!appointment) return { success: false, error: error ?? "Not found." }; const result = await confirm(prisma, appointmentId); return result.success ? { success: true } : { success: false, error: friendlyTransitionMessage(result.validation.issues[0]?.code) }; } export async function checkInAppointmentAction( appointmentId: string, ): Promise { const { appointment, error } = await loadOwnAppointment(appointmentId); if (!appointment) return { success: false, error: error ?? "Not found." }; const result = await checkIn(prisma, appointmentId); return result.success ? { success: true } : { success: false, error: friendlyTransitionMessage(result.validation.issues[0]?.code) }; } export async function startAppointmentAction( appointmentId: string, ): Promise { const { appointment, error } = await loadOwnAppointment(appointmentId); if (!appointment) return { success: false, error: error ?? "Not found." }; const result = await start(prisma, appointmentId); return result.success ? { success: true } : { success: false, error: friendlyTransitionMessage(result.validation.issues[0]?.code) }; } export async function completeAppointmentAction( appointmentId: string, ): Promise { const { staff, appointment, error } = await loadOwnAppointment(appointmentId); if (!appointment || !staff) return { success: false, error: error ?? "Not found." }; const result = await complete(prisma, appointmentId); if (!result.success) { return { success: false, error: friendlyTransitionMessage(result.validation.issues[0]?.code) }; } const loyaltyResult = await awardStampForCompletedAppointment(prisma, { appointmentId, staffId: staff.id, }); return { success: true, loyaltyStampAwarded: loyaltyResult.success && !loyaltyResult.alreadyAwarded, }; } export async function markNoShowAction( appointmentId: string, ): Promise { const { appointment, error } = await loadOwnAppointment(appointmentId); if (!appointment) return { success: false, error: error ?? "Not found." }; const result = await markNoShow(prisma, appointmentId); return result.success ? { success: true } : { success: false, error: friendlyTransitionMessage(result.validation.issues[0]?.code) }; } export async function cancelStaffAppointmentAction(params: { appointmentId: string; reason?: string; }): Promise { const { staff, appointment, error } = await loadOwnAppointment(params.appointmentId); if (!appointment || !staff) return { success: false, error: error ?? "Not found." }; const result = await cancelBooking(prisma, { appointmentId: params.appointmentId, cancelledByUserId: staff.user.id, reason: params.reason, }); return result.success ? { success: true } : { success: false, error: result.validation.issues[0]?.message ?? "Couldn't cancel this appointment.", }; } export async function rescheduleStaffAppointmentAction(params: { appointmentId: string; staffId?: string; startAt: string; }): Promise { const { appointment, error } = await loadOwnAppointment(params.appointmentId); if (!appointment) return { success: false, error: error ?? "Not found." }; const result = await rescheduleBooking(prisma, { appointmentId: params.appointmentId, staffId: params.staffId, startAt: new Date(params.startAt), }); return result.success ? { success: true } : { success: false, error: result.validation.issues[0]?.message ?? "Couldn't reschedule this appointment.", }; } function friendlyTransitionMessage(code?: string): string { if (code === "INVALID_STATUS_TRANSITION") return "This appointment can't move to that status right now — someone may have already updated it."; if (code === "APPOINTMENT_NOT_FOUND") return "We couldn't find that appointment."; return "Something went wrong. Please try again."; }