Skip to main content

Appointment Management

Complete guide to managing appointments in the Salonnz Admin Panel - from creation to checkout.

Overview

The Appointment module is the central hub of Salonnz, connecting customers, staff, services, payments, and locations into a cohesive booking experience. Every appointment flows through a well-defined lifecycle with multiple stages and comprehensive tracking.

Appointment Lifecycle


Appointment Statuses

The system uses 7 distinct statuses to track appointment state:

StatusIDDescriptionAdmin VisibleCustomer VisibleActions Available
Confirmed1Future appointment✅ Yes✅ YesReschedule, Cancel, Edit
Completed2Service rendered, awaiting checkout✅ Yes✅ Yes (as "Past")Checkout, Refund
In Progress3Currently being serviced✅ Yes✅ YesMark Complete
Cancelled4Cancelled by customer/admin✅ Yes❌ NoView Only
No-Show5Customer didn't arrive✅ Yes❌ NoView Only
Deleted6Soft-deleted (hidden)❌ No❌ NoPermanent removal

Status Filtering Logic

Admin Panel Lists:

// Excludes Cancelled (4), No-Show (5), and Deleted (6)
$appointments = Appointment::where('status', '!=', '4')
->where('status', '!=', '5')
->where('status', '!=', '6')
->get();

Customer View:

// Only shows Confirmed (1), Completed (2), In Progress (3)
$appointments = Appointment::where('customer_id', $customerId)
->whereIn('status', [1, 2, 3])
->get();

Creating Appointments

Admin Manual Booking

Navigation: Admin Panel → Appointments → Create New

Required Fields:

  • Location: Which branch.
  • Customer: Select existing or create walk-in.
  • Booking Date: Date of service.
  • Services: At least one service with:
    • Service name
    • Pricing option
    • Staff member
    • Resource (if applicable)
    • Start time
    • End time
  • Amount: Total cost (auto-calculated).

Optional Fields:

  • Internal Note: Staff-only notes (not visible to customer).
  • Client Note: Customer-facing notes.
  • Payments: If collecting payment upfront.

Auto-Status Determination:

const bookingDateTime = moment(`${booking_date} ${start_time}`);
const now = moment();

if (bookingDateTime.isAfter(now)) {
appointment.status = 1; // Confirmed (future)
} else {
appointment.status = 2; // Completed (past, e.g., walk-in)
}

Example Scenario:

Current time: 2024-12-06 3:00 PM

Scenario 1 - Future booking:
Booking date: 2024-12-08 10:00 AM
Result: Status = 1 (Confirmed)

Scenario 2 - Walk-in (same day, earlier time):
Booking date: 2024-12-06 1:00 PM
Result: Status = 2 (Completed)

Multi-Service Booking

A single appointment can contain multiple services, each with different:

  • Staff members (e.g., Haircut by Sarah, Manicure by Lisa).
  • Resources (e.g., Station 1, Massage Room 3).
  • Time slots (sequential or overlapping).

Database Structure:

appointments (id: 123)
├─→ appointment_services (id: 1)
│ ├─→ service_id: 5 (Haircut)
│ ├─→ staff_id: 3 (Sarah)
│ ├─→ start_time: 10:00 AM
│ └─→ end_time: 10:45 AM
├─→ appointment_services (id: 2)
│ ├─→ service_id: 12 (Manicure)
│ ├─→ staff_id: 7 (Lisa)
│ ├─→ start_time: 10:45 AM
│ └─→ end_time: 11:30 AM
└─→ Total appointment: 10:00 AM - 11:30 AM

Key Point: All services are treated as atomic - updating/cancelling the appointment affects all services together.


Walk-In Quick Add

Use Case: Customer walks in without prior booking.

Workflow:

  1. Create appointment with past date/time (triggers status = 2).
  2. Optionally create new customer profile (or use "Walk-In" generic).
  3. Skip payment (can checkout later).
  4. Appointment immediately appears in checkout queue.

Appointment List Views

Default List View

Filters Available:

  • Location: Filter by branch.
  • Staff: Show appointments for specific staff member.
  • Date Range: From date → To date.
  • Status: Filter by status (Confirmed/Completed/etc.).

Caching:

// Cached for performance
$appointments = getOrSetCache('appointment_list', function() {
return Appointment::with(['appointment_services', 'location', 'customer'])
->where('status', '!=', '4')
->where('status', '!=', '5')
->where('status', '!=', '6')
->orderBy('date', 'desc')
->get();
});

Cache Invalidation: Cleared automatically on any create/update/delete operation.


Checkout Queue

Purpose: Lists appointments that are completed but not yet paid (status = 2 and isCheckout = 0).

Navigation: Admin Panel → Checkout / POS

Query:

$checkoutQueue = Appointment::where('status', '2')
->where('isCheckout', '0')
->where('location_id', $currentLocation)
->orderBy('date', 'desc')
->get();

Use Case: After services are rendered, appointments sit in this queue until final payment is processed at the POS.


Managing Appointments

Viewing Appointment Details

Information Displayed:

  • Customer Info: Name, email, phone, profile image.
  • Location & Date: Which branch, appointment date/time.
  • Services Breakdown:
    • Each service name.
    • Assigned staff member.
    • Price per service.
    • Time slot (start/end).
  • Payment History: All payments made (deposits, final payment, tips).
  • Add-ons: Products or extra services added during checkout.
  • Status History: Full audit log of changes.

Editing Appointments

What Can Be Edited:

  • ✅ Services (add/remove services).
  • ✅ Staff assignments.
  • ✅ Date and time (see Rescheduling below).
  • ✅ Internal/Client notes.
  • ❌ Customer (must create new appointment).
  • ❌ Status (use dedicated status change flow).

Warning: Editing services triggers cache invalidation and may send updated notifications to staff.


Rescheduling Appointments

Admin Flow:

  1. Navigate to Appointment Details.
  2. Click "Reschedule".
  3. Select new date and time slot.
  4. System validates:
    • Staff availability on new date.
    • No overlapping appointments.
    • Slot can accommodate all services.
  5. Confirm reschedule.

Backend Logic:

// BookingController.rescheduleTime()
$appointment->update(['date' => $newDate]);

foreach ($appointment->appointment_services as $service) {
$service->update([
'date' => $newDate,
'start_time' => calculateNewStartTime($service, $newDate),
'end_time' => calculateNewEndTime($service, $newDate)
]);
}

AppointmentHistory::create([
'appointment_id' => $appointment->id,
'action' => 'RESCHEDULED',
'message' => "Rescheduled from {$oldDate} to {$newDate}"
]);

// Clear slot caches
clearCacheByPattern('appointment_list');
clearCacheByPattern('staff_get_staff_list_by_availability');

Notifications Sent:

  • ✅ Email to customer: "Appointment Rescheduled".
  • ✅ SMS to customer (if enabled).
  • ✅ Push notification to customer.
  • ✅ Email to ALL assigned staff members.
  • ✅ SMS to ALL assigned staff members.
  • ✅ Push notifications to staff app.

Cancelling Appointments

Admin Cancellation

Workflow:

  1. Open Appointment Details.
  2. Click "Cancel Appointment".
  3. Optional: Add cancellation reason/note.
  4. Confirm cancellation.

Backend Changes:

$appointment->update(['status' => 4]);  // Status: Cancelled

AppointmentHistory::create([
'appointment_id' => $appointment->id,
'action' => 'CANCELED',
'message' => $cancellationNote
]);

Result:

  • Appointment remains visible in Admin Panel (for record-keeping).
  • Appointment hidden from customer app/booking site.
  • Slot becomes available for new bookings.

Customer Cancellation

Same flow, but triggered by customer in their app.

Cancellation Policy (if configured):

  • May enforce time restrictions (e.g., "Cannot cancel within 24 hours").
  • May charge cancellation fees.
  • Policy displayed before customer confirms cancellation.

Refund Processing: If payment was collected, refund must be processed manually via Stripe dashboard (system does not auto-refund).


No-Show Marking

Use Case: Customer doesn't arrive for confirmed appointment.

Workflow:

  1. Open Appointment Details.
  2. Change status to No-Show (5).
  3. Appointment hidden from customer view.
  4. Staff can still see it for tracking purposes.

Difference vs Cancellation:

ActionStatusCustomer NotifiedRefund Expected
Cancel4Yes (email/SMS)Maybe
No-Show5NoNo

Checkout Process

Overview

After services are completed (status = 2), the appointment moves to the Checkout Queue for final payment processing.

POS Workflow

Step 1: Select Appointment

  • Navigate to Checkout / POS.
  • Select appointment from the queue.

Step 2: Review Services

  • Verify all services rendered.
  • Check total amount due.

Step 3: Add Extras (Optional)

  • Tips: Add tip amount for staff.
  • Add-ons: Sell retail products or additional services.
// Add-ons stored separately
AppointmentAddon::create([
'appointment_id' => $appointment->id,
'addon_type' => 'product', // or 'service'
'addon_id' => $productId,
'quantity' => 2,
'price' => 50.00
]);

Step 4: Process Payment

Payment Methods Supported:

  • Cash
  • Credit/Debit Card (Stripe)
  • Gift Card (redemption)
  • Package (use package credit)
  • Membership (apply discount)

Payment Recording:

AppointmentPayment::create([
'appointment_id' => $appointment->id,
'payment_type_id' => 1, // Cash, Card, etc.
'amount' => $finalAmount
]);

$appointment->update([
'amount_paid' => $appointment->payments->sum('amount'),
'isCheckout' => 1 // Mark as fully settled
]);

Step 5: Finalize

  • Print receipt (optional).
  • Send receipt email to customer.
  • Appointment removed from checkout queue.

Checkout Status vs Completion Status

Important Distinction:

FieldPurposeValues
statusAppointment lifecycle stage1-6 (Confirmed/Completed/etc.)
isCheckoutPayment settlement status0 = unpaid, 1 = fully paid

Scenarios:

Scenario 1: Service done, not paid yet
status = 2 (Completed)
isCheckout = 0
→ Appears in Checkout Queue

Scenario 2: Service done, fully paid
status = 2 (Completed)
isCheckout = 1
→ Removed from Checkout Queue

Scenario 3: Future appointment
status = 1 (Confirmed)
isCheckout = 0
→ Not in Checkout Queue

Payment Integration

Payment Tracking

Fields:

  • amount: Total cost of all services.
  • amount_paid: Sum of all appointment_payments.

Balance Calculation:

$balance = $appointment->amount - $appointment->payments->sum('amount');

if ($balance > 0) {
// Outstanding balance
} elseif ($balance < 0) {
// Overpaid (tip or credit)
}

Deposit Payments

If deposit system is enabled (deposit_enable = 1), customers can pay a percentage upfront:

// At booking time
$deposit = ($appointment->amount * $depositPercentage) / 100;

AppointmentPayment::create([
'appointment_id' => $appointment->id,
'payment_type_id' => 2, // Card
'amount' => $deposit,
'payment_source' => 'deposit'
]);

$appointment->update([
'amount_paid' => $deposit
]);

// Remaining balance due at checkout
$balanceDue = $appointment->amount - $deposit;

Payment History

Stored in: appointment_payments table.

Each payment record contains:

  • Payment type (Cash/Card/Gift Card).
  • Amount.
  • Timestamp.
  • Associated Stripe transaction ID (if card payment).

Viewing History: Admin can see full payment timeline in Appointment Details → Payments tab.


Notification System

Every significant action triggers notifications across 3 channels: Email, SMS, and Push.

Notification Matrix

EventCustomer EmailCustomer SMSCustomer PushStaff EmailStaff SMSStaff Push
Booking Created
Rescheduled
Cancelled
Reminder (24hr)
Reminder (2hr)

Email Templates

Customer Templates:

  • customer_email_appt_booked: Booking confirmation with appointment details, location, staff info, add-to-calendar link.
  • customer_email_appt_reschedule: Reschedule notification with new date/time.
  • customer_email_appt_cancel: Cancellation confirmation with possible refund info.

Staff Templates:

  • staff_email_appt_booked: New appointment assigned with customer details.
  • staff_email_appt_reschedule: Updated appointment time.
  • staff_email_appt_cancel: Appointment cancelled, time slot freed.

Template Variables

Templates use placeholders replaced with actual data:

$templateVars = [
'{customer_name}' => 'John Doe',
'{appointment_id}' => '1701234567890',
'{html_date}' => 'Mon, Dec 15',
'{html_all_service}' => 'Haircut, Color Treatment',
'{location_name}' => 'Downtown Branch',
'{location_address}' => '123 Main St, New York, NY 10001',
'{appointment_link}' => 'https://booking.salonnz.com/...',
// ... 20+ more variables
];

SMS & Push Notifications

SMS: Sent via configured SMS gateway (Twilio/etc.).

Push Notifications:

  • Sent to customers.fcm_token (Firebase Cloud Messaging).
  • Stored in notifications table for in-app notification center.

Notification Preferences: Customers can disable notifications in their profile:

  • email_notification (0/1)
  • text_notification (0/1)
  • push_notification (0/1)

Appointment History

Every action is logged to appointment_history table.

Action Types

ActionWhen TriggeredMessage Example
CREATEDAppointment booked"Appointment Created"
STATUS_UPDATEStatus changed"Status changed from Confirmed to Completed"
CANCELEDCancelled"Cancelled by admin: Customer requested"
RESCHEDULEDDate/time changed"Rescheduled from Dec 15 to Dec 18"
PAYMENT_ADDEDPayment recorded"Payment of $150 received (Cash)"
EDITEDServices/staff updated"Added service: Manicure"

Viewing History

Admin Panel:

  • Appointment Details → History Tab.
  • Chronological list with timestamps.

Use Cases:

  • Audit trail for disputes.
  • Tracking who made changes.
  • Understanding appointment lifecycle.

Database Schema

appointments Table

CREATE TABLE appointments (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
appointment_id BIGINT UNIQUE, -- Unix timestamp for user-facing ID
location_id BIGINT,
customer_id BIGINT,
customer_type INT, -- 0=Registered, 1=Walk-in, 2=Guest
date DATE,
internalNote TEXT,
clientNote TEXT,
amount DECIMAL(10,2),
amount_paid DECIMAL(10,2) DEFAULT 0,
booked_by INT, -- User ID who created appointment
isCheckout TINYINT DEFAULT 0, -- 0=Pending, 1=Paid
status INT, -- 1-6
is_reminder_email_sent TINYINT DEFAULT 0,
is_reminder_sms_sent TINYINT DEFAULT 0,
created_at TIMESTAMP,
updated_at TIMESTAMP
);

appointment_services Table

CREATE TABLE appointment_services (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
appointment_id BIGINT,
service_id BIGINT,
pricing_option_id BIGINT,
staff_id BIGINT,
resource_id BIGINT,
price DECIMAL(10,2),
has_processing_time TINYINT,
durtion INT, -- Duration in minutes (typo in schema)
processing_time INT,
setup_time TIME,
start_time TIME,
finish_time TIME,
end_time TIME,
buffer_time INT,
buffer_time_type VARCHAR(20),
date DATE,
FOREIGN KEY (appointment_id) REFERENCES appointments(id) ON DELETE CASCADE
);

appointment_payments Table

CREATE TABLE appointment_payments (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
appointment_id BIGINT,
payment_type_id INT, -- 1=Cash, 2=Card, 3=Gift Card, etc.
amount DECIMAL(10,2),
created_at TIMESTAMP,
FOREIGN KEY (appointment_id) REFERENCES appointments(id) ON DELETE CASCADE
);

appointment_history Table

CREATE TABLE appointment_history (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
appointment_id BIGINT,
action VARCHAR(50), -- CREATED, CANCELED, RESCHEDULED, etc.
message TEXT,
created_at TIMESTAMP,
FOREIGN KEY (appointment_id) REFERENCES appointments(id) ON DELETE CASCADE
);

appointment_addons Table

CREATE TABLE appointment_addons (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
appointment_id BIGINT,
addon_type VARCHAR(20), -- 'product' or 'service'
addon_id BIGINT, -- ID of product or service
quantity INT DEFAULT 1,
price DECIMAL(10,2),
FOREIGN KEY (appointment_id) REFERENCES appointments(id) ON DELETE CASCADE
);

Caching Strategy

The appointment system uses 5 cache layers for performance:

1. Appointment List Cache (appointment_list)

Stores: Filtered appointment lists.
Invalidated: On create/update/delete.

2. Paginated List Cache (appointment_appointment_by_pages)

Stores: Paginated appointment results.
Invalidated: On create/update/delete.

3. Checkout Queue Cache (appointment_checkout_appointment_list)

Stores: Pending checkout appointments.
Invalidated: On status change or checkout completion.

4. Detail Cache (appointment_get_appointment_detail)

Stores: Full appointment details with relations.
Invalidated: On any appointment update.

5. Database-Specific Cache (cache_appointment_detail_{id})

Stores: Individual appointment details.
Invalidated: On specific appointment update.

Manual Cache Clear:

clearCacheByPattern('appointment_list');
clearCacheByPattern('appointment_checkout_appointment_list');
clearCacheByPattern('staff_get_staff_list_by_availability');

Troubleshooting

Issue: Appointment not appearing in list

Check:

  1. ✅ Status is not 4, 5, or 6 (these are filtered out).
  2. ✅ Location filter matches appointment location.
  3. ✅ Date range includes appointment date.
  4. ✅ Cache is up-to-date (try clearing: php artisan cache:clear).

Issue: Notifications not sending

Check:

  1. ✅ Customer has valid email/phone in profile.
  2. ✅ Customer notification preferences are enabled (email_notification = 1).
  3. ✅ Email/SMS templates exist for the event (customer_email_appt_booked, etc.).
  4. ✅ Email service (SendGrid/etc.) is configured correctly.
  5. ✅ SMS gateway (Twilio) has credits.

Issue: Checkout queue shows old appointments

Likely Cause: Appointments with isCheckout = 0 are still pending.

Solution:

  1. Find appointments with status = 2 and isCheckout = 0.
  2. Process checkout or manually set isCheckout = 1.
  3. Clear checkout cache: clearCacheByPattern('appointment_checkout_appointment_list').

Issue: Cannot reschedule appointment

Check:

  1. ✅ New time slot is available (staff not booked).
  2. ✅ Slot can accommodate all services (total duration fits).
  3. ✅ Resource (if assigned) is available at new time.
  4. ✅ Appointment status is 1 or 3 (cannot reschedule cancelled/completed).

Best Practices

  1. Always Add Internal Notes: Useful for tracking special requests or customer preferences.

  2. Use Status Transitions Properly:

    • Confirmed → In Progress → Completed → Checkout ✅ Correct flow.
    • Confirmed → Completed (skipping In Progress) ✅ Also valid.
    • Completed → Confirmed ❌ Avoid going backwards.
  3. Handle Walk-Ins Efficiently: Create appointment with past date to auto-set status = 2, skipping confirmation stage.

  4. Monitor Checkout Queue: Appointments should not sit in queue for days - process payments promptly.

  5. Review History for Disputes: Use appointment history to track who made changes and when.

  6. Clear Caches After Bulk Updates: If manually updating database, clear appointment caches to ensure UI reflects changes.

  7. Test Notifications: After setting up email/SMS, create a test appointment to verify templates render correctly.