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:
| Status | ID | Description | Admin Visible | Customer Visible | Actions Available |
|---|---|---|---|---|---|
| Confirmed | 1 | Future appointment | ✅ Yes | ✅ Yes | Reschedule, Cancel, Edit |
| Completed | 2 | Service rendered, awaiting checkout | ✅ Yes | ✅ Yes (as "Past") | Checkout, Refund |
| In Progress | 3 | Currently being serviced | ✅ Yes | ✅ Yes | Mark Complete |
| Cancelled | 4 | Cancelled by customer/admin | ✅ Yes | ❌ No | View Only |
| No-Show | 5 | Customer didn't arrive | ✅ Yes | ❌ No | View Only |
| Deleted | 6 | Soft-deleted (hidden) | ❌ No | ❌ No | Permanent 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:
- Create appointment with past date/time (triggers
status = 2). - Optionally create new customer profile (or use "Walk-In" generic).
- Skip payment (can checkout later).
- 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:
- Navigate to Appointment Details.
- Click "Reschedule".
- Select new date and time slot.
- System validates:
- Staff availability on new date.
- No overlapping appointments.
- Slot can accommodate all services.
- 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:
- Open Appointment Details.
- Click "Cancel Appointment".
- Optional: Add cancellation reason/note.
- 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:
- Open Appointment Details.
- Change status to No-Show (5).
- Appointment hidden from customer view.
- Staff can still see it for tracking purposes.
Difference vs Cancellation:
| Action | Status | Customer Notified | Refund Expected |
|---|---|---|---|
| Cancel | 4 | Yes (email/SMS) | Maybe |
| No-Show | 5 | No | No |
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:
| Field | Purpose | Values |
|---|---|---|
status | Appointment lifecycle stage | 1-6 (Confirmed/Completed/etc.) |
isCheckout | Payment settlement status | 0 = 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 allappointment_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
| Event | Customer Email | Customer SMS | Customer Push | Staff Email | Staff SMS | Staff 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
notificationstable 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
| Action | When Triggered | Message Example |
|---|---|---|
CREATED | Appointment booked | "Appointment Created" |
STATUS_UPDATE | Status changed | "Status changed from Confirmed to Completed" |
CANCELED | Cancelled | "Cancelled by admin: Customer requested" |
RESCHEDULED | Date/time changed | "Rescheduled from Dec 15 to Dec 18" |
PAYMENT_ADDED | Payment recorded | "Payment of $150 received (Cash)" |
EDITED | Services/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:
- ✅ Status is not 4, 5, or 6 (these are filtered out).
- ✅ Location filter matches appointment location.
- ✅ Date range includes appointment date.
- ✅ Cache is up-to-date (try clearing:
php artisan cache:clear).
Issue: Notifications not sending
Check:
- ✅ Customer has valid email/phone in profile.
- ✅ Customer notification preferences are enabled (
email_notification = 1). - ✅ Email/SMS templates exist for the event (
customer_email_appt_booked, etc.). - ✅ Email service (SendGrid/etc.) is configured correctly.
- ✅ SMS gateway (Twilio) has credits.
Issue: Checkout queue shows old appointments
Likely Cause: Appointments with isCheckout = 0 are still pending.
Solution:
- Find appointments with
status = 2andisCheckout = 0. - Process checkout or manually set
isCheckout = 1. - Clear checkout cache:
clearCacheByPattern('appointment_checkout_appointment_list').
Issue: Cannot reschedule appointment
Check:
- ✅ New time slot is available (staff not booked).
- ✅ Slot can accommodate all services (total duration fits).
- ✅ Resource (if assigned) is available at new time.
- ✅ Appointment status is 1 or 3 (cannot reschedule cancelled/completed).
Best Practices
-
Always Add Internal Notes: Useful for tracking special requests or customer preferences.
-
Use Status Transitions Properly:
- Confirmed → In Progress → Completed → Checkout ✅ Correct flow.
- Confirmed → Completed (skipping In Progress) ✅ Also valid.
- Completed → Confirmed ❌ Avoid going backwards.
-
Handle Walk-Ins Efficiently: Create appointment with past date to auto-set
status = 2, skipping confirmation stage. -
Monitor Checkout Queue: Appointments should not sit in queue for days - process payments promptly.
-
Review History for Disputes: Use appointment history to track who made changes and when.
-
Clear Caches After Bulk Updates: If manually updating database, clear appointment caches to ensure UI reflects changes.
-
Test Notifications: After setting up email/SMS, create a test appointment to verify templates render correctly.
Related Documentation
- Booking Flow - Customer journey from browsing to booking
- Booking Slots - How slots are generated and filtered
- Payment Integration - Stripe setup and payment methods
- Staff Management - Managing staff schedules and assignments