diff --git a/.kilo/KILO_SPEC.md b/.kilo/KILO_SPEC.md
index c43e4df..fb5db5f 100644
--- a/.kilo/KILO_SPEC.md
+++ b/.kilo/KILO_SPEC.md
@@ -443,7 +443,8 @@ Provider availability depends on configuration. Common providers include:
|---------|-------------|-------|
| `/landing-page` | Create landing page CMS from HTML mockups | ollama-cloud/kimi-k2.5 |
| `/commerce` | Create e-commerce site with products, cart, payments | qwen/qwen3-coder:free |
-| `/blog` | Create blog/CMS with posts, comments, SEO | qwen/qwen3-coder:free |
+| `/blog` | Create blog/CMS with posts, comments, SEO | qwen/qeen3-coder:free |
+| `/booking` | Create booking system for services/appointments | qwen/qwen3-coder:free |
| `/pipeline` | Run full agent pipeline for issue | - |
| `/feature` | Full feature development pipeline | qwen/qwen3-coder:free |
| `/code` | Quick code generation | qwen/qwen3-coder:free |
@@ -510,6 +511,22 @@ Provider availability depends on configuration. Common providers include:
- RSS/Atom feeds and sitemap generation
- Media library management
+### Booking System Domain
+
+**Location**: `.kilo/skills/booking/SKILL.md`
+
+**Purpose**: Domain knowledge for building booking and appointment systems.
+
+**Capabilities**:
+- Service management with categories and pricing
+- Staff scheduling and availability
+- Real-time slot calculation
+- Booking flow (service → staff → date/time → customer)
+- Status management (pending, confirmed, completed, cancelled)
+- Email/SMS notifications
+- Calendar integration (Google, iCal)
+- Revenue and utilization reports
+
---
## File Naming Conventions
diff --git a/.kilo/commands/booking.md b/.kilo/commands/booking.md
new file mode 100644
index 0000000..c223c9f
--- /dev/null
+++ b/.kilo/commands/booking.md
@@ -0,0 +1,1541 @@
+---
+description: Create full-stack booking site with Node.js, Vue, SQLite, admin panel, calendar, and Docker deployment
+mode: booking
+model: qwen/qwen3-coder:free
+color: "#8B5CF6"
+permission:
+ read: allow
+ edit: allow
+ write: allow
+ bash: allow
+ glob: allow
+ grep: allow
+ task:
+ "backend-developer": allow
+ "frontend-developer": allow
+ "system-analyst": allow
+ "lead-developer": allow
+ "sdet-engineer": allow
+ "code-skeptic": allow
+ "the-fixer": allow
+ "release-manager": allow
+ "security-auditor": allow
+ "browser-automation": allow
+---
+
+# Booking System Workflow
+
+Create a full-stack booking and appointment system for service businesses (salons, clinics, massage, etc.) with online booking, staff scheduling, admin management, and Docker deployment.
+
+## Parameters
+
+- `project_name`: Business name (required)
+- `business_type`: Type - 'salon', 'clinic', 'massage', 'fitness', 'consulting' (default: 'salon')
+- `ui_framework`: UI framework - 'vuetify', 'quasar', 'primevue' (default: 'vuetify')
+- `features`: Features - 'payments,sms,calendar_sync,reminders' (default: 'all')
+- `docker`: Create Docker deployment (default: true)
+- `issue`: Gitea issue number for tracking (optional)
+
+## Overview
+
+```
+Requirements → Architecture → Services → Staff → Booking → Admin → Calendar → Tests → Docker → Docs
+```
+
+## Technology Stack
+
+### Frontend
+| Component | Technology |
+|-----------|------------|
+| Framework | Vue.js 3 (Composition API) |
+| UI Library | Vuetify/Quasar/PrimeVue |
+| State | Pinia |
+| Router | Vue Router |
+| Calendar | FullCalendar/Vue Cal |
+| Time Picker | Vuetify Time Picker |
+| HTTP | Axios |
+
+### Backend
+| Component | Technology |
+|-----------|------------|
+| Runtime | Node.js 20.x |
+| Framework | Express.js |
+| Database | SQLite (better-sqlite3) |
+| Auth | JWT + bcrypt |
+| Validation | Joi/Zod |
+| Notifications | Nodemailer + SMS gateway |
+
+## Step 1: Requirements Analysis
+
+**Agent**: `@RequirementRefiner`
+
+### Booking Requirements Checklist
+
+```markdown
+## User Stories
+
+### Customer (Public)
+- [ ] View services with prices and duration
+- [ ] Select service category
+- [ ] Choose preferred staff member
+- [ ] View available time slots
+- [ ] Book appointment
+- [ ] Receive confirmation (email/SMS)
+- [ ] View booking details
+- [ ] Cancel/reschedule booking
+- [ ] View booking history
+- [ ] Register account (optional)
+
+### Business Admin
+- [ ] Manage services (CRUD)
+- [ ] Manage service categories
+- [ ] Manage staff members
+- [ ] Set staff schedules
+- [ ] Manage availability
+- [ ] View all bookings
+- [ ] Confirm/decline bookings
+- [ ] Mark bookings complete/no-show
+- [ ] View calendar (day/week/month)
+- [ ] Generate reports (revenue, utilization)
+- [ ] Manage customers
+- [ ] Send notifications
+
+### Staff Member
+- [ ] View own schedule
+- [ ] View own bookings
+- [ ] Update availability
+- [ ] Mark bookings complete
+- [ ] Add notes to bookings
+
+### Non-Functional
+- [ ] Mobile-responsive booking flow
+- [ ] Real-time availability updates
+- [ ] Email confirmation
+- [ ] SMS reminders
+- [ ] Calendar sync (Google, iCal)
+- [ ] Timezone support
+- [ ] Multi-language support
+```
+
+## Step 2: Architecture Design
+
+**Agent**: `@SystemAnalyst`
+
+### Project Structure
+
+```
+booking/
+├── backend/
+│ ├── src/
+│ │ ├── config/
+│ │ │ ├── database.js
+│ │ │ ├── auth.js
+│ │ │ ├── notifications.js
+│ │ │ └── calendar.js
+│ │ ├── db/
+│ │ │ ├── migrations/
+│ │ │ └── seeds/
+│ │ ├── models/
+│ │ │ ├── Service.js
+│ │ │ ├── Category.js
+│ │ │ ├── Staff.js
+│ │ │ ├── Booking.js
+│ │ │ ├── Customer.js
+│ │ │ └── Schedule.js
+│ │ ├── routes/
+│ │ │ ├── api/
+│ │ │ │ ├── services.js
+│ │ │ │ ├── availability.js
+│ │ │ │ ├── bookings.js
+│ │ │ │ └── calendar.js
+│ │ │ └── admin/
+│ │ │ ├── services.js
+│ │ │ ├── staff.js
+│ │ │ ├── bookings.js
+│ │ │ ├── customers.js
+│ │ │ ├── reports.js
+│ │ │ └── settings.js
+│ │ ├── services/
+│ │ │ ├── availability.js
+│ │ │ ├── booking.js
+│ │ │ ├── notification/
+│ │ │ │ ├── email.js
+│ │ │ │ └── sms.js
+│ │ │ ├── calendar/
+│ │ │ │ ├── google.js
+│ │ │ │ └── ical.js
+│ │ │ └── payment/
+│ │ │ └── stripe.js
+│ │ └── middleware/
+│ │ ├── auth.js
+│ │ ├── staff.js
+│ │ └── validation.js
+│ └── tests/
+├── frontend/
+│ ├── src/
+│ │ ├── views/
+│ │ │ ├── public/
+│ │ │ │ ├── Home.vue
+│ │ │ │ ├── Services.vue
+│ │ │ │ ├── Booking.vue
+│ │ │ │ ├── BookingConfirm.vue
+│ │ │ │ └── BookingDetails.vue
+│ │ │ └── admin/
+│ │ │ ├── Dashboard.vue
+│ │ │ ├── Calendar.vue
+│ │ │ ├── Services.vue
+│ │ │ ├── Staff.vue
+│ │ │ ├── Bookings.vue
+│ │ │ ├── Customers.vue
+│ │ │ ├── Reports.vue
+│ │ │ └── Settings.vue
+│ │ ├── components/
+│ │ │ ├── booking/
+│ │ │ │ ├── ServiceSelect.vue
+│ │ │ │ ├── StaffSelect.vue
+│ │ │ │ ├── DateTimePicker.vue
+│ │ │ │ ├── CustomerForm.vue
+│ │ │ │ └── BookingSummary.vue
+│ │ │ └── admin/
+│ │ │ ├── CalendarView.vue
+│ │ │ ├── ServiceEditor.vue
+│ │ │ ├── StaffEditor.vue
+│ │ │ ├── ScheduleEditor.vue
+│ │ │ └── AvailabilityGrid.vue
+│ │ ├── stores/
+│ │ │ ├── booking.js
+│ │ │ ├── auth.js
+│ │ │ └── calendar.js
+│ │ └── router/
+│ │ └── index.js
+│ └── tests/
+├── database/
+│ └── booking.db
+├── docker/
+│ ├── Dockerfile.backend
+│ ├── Dockerfile.frontend
+│ └── docker-compose.yml
+└── docs/
+ ├── API.md
+ └── DEPLOYMENT.md
+```
+
+Use the database schema from `.kilo/skills/booking/SKILL.md`.
+
+## Step 3: Backend Implementation
+
+**Agent**: `@BackendDeveloper`
+
+### Service API
+
+```javascript
+// backend/src/routes/api/services.js
+const router = require('express').Router();
+
+// GET /api/services - List active services
+router.get('/', async (req, res, next) => {
+ try {
+ const { category_id } = req.query;
+
+ const services = await db.services.findAll({
+ is_active: true,
+ category_id: category_id || undefined
+ });
+
+ res.json(services);
+ } catch (error) {
+ next(error);
+ }
+});
+
+// GET /api/services/:id - Get service details
+router.get('/:id', async (req, res, next) => {
+ try {
+ const service = await db.services.findById(req.params.id);
+
+ if (!service || !service.is_active) {
+ return res.status(404).json({ error: 'Service not found' });
+ }
+
+ // Get staff for this service
+ const staff = await db.staff.findByService(service.id);
+
+ res.json({ ...service, staff });
+ } catch (error) {
+ next(error);
+ }
+});
+
+module.exports = router;
+```
+
+### Availability API
+
+```javascript
+// backend/src/routes/api/availability.js
+const router = require('express').Router();
+
+// GET /api/availability - Get available slots
+router.get('/',
+ [
+ query('service_id').isInt(),
+ query('date').isDate(),
+ query('staff_id').optional().isInt()
+ ],
+ async (req, res, next) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ const { service_id, date, staff_id } = req.query;
+
+ const slots = await availabilityService.getAvailableSlots({
+ serviceId: parseInt(service_id),
+ staffId: staff_id ? parseInt(staff_id) : null,
+ date
+ });
+
+ res.json({ date, slots });
+ } catch (error) {
+ next(error);
+ }
+);
+
+// POST /api/availability/check - Check specific slot
+router.post('/check',
+ [
+ body('service_id').isInt(),
+ body('staff_id').optional().isInt(),
+ body('date').isDate(),
+ body('time').matches(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/)
+ ],
+ async (req, res, next) => {
+ try {
+ const { service_id, staff_id, date, time } = req.body;
+
+ const available = await availabilityService.checkSlot({
+ serviceId: service_id,
+ staffId: staff_id,
+ date,
+ time
+ });
+
+ res.json({ available });
+ } catch (error) {
+ next(error);
+ }
+ }
+);
+
+module.exports = router;
+```
+
+### Booking API
+
+```javascript
+// backend/src/routes/api/bookings.js
+const router = require('express').Router();
+
+// POST /api/bookings - Create booking
+router.post('/',
+ [
+ body('service_id').isInt(),
+ body('date').isDate(),
+ body('time').matches(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/),
+ body('customer_name').notEmpty().isLength({ max: 100 }),
+ body('customer_email').isEmail(),
+ body('customer_phone').optional().isMobilePhone(),
+ body('staff_id').optional().isInt(),
+ body('notes').optional().isLength({ max: 500 })
+ ],
+ async (req, res, next) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ const booking = await bookingService.create(req.body);
+
+ // Send confirmation
+ await notificationService.sendConfirmation(booking);
+
+ res.status(201).json(booking);
+ } catch (error) {
+ next(error);
+ }
+ }
+);
+
+// GET /api/bookings/:id - Get booking details
+router.get('/:id', async (req, res, next) => {
+ try {
+ const booking = await bookingService.findById(req.params.id);
+
+ if (!booking) {
+ return res.status(404).json({ error: 'Booking not found' });
+ }
+
+ res.json(booking);
+ } catch (error) {
+ next(error);
+ }
+});
+
+// POST /api/bookings/:id/cancel - Cancel booking
+router.post('/:id/cancel',
+ [body('reason').optional().isLength({ max: 200 })],
+ async (req, res, next) => {
+ try {
+ const booking = await bookingService.cancel(
+ req.params.id,
+ req.body.reason
+ );
+
+ // Send cancellation notification
+ await notificationService.sendCancellation(booking);
+
+ res.json(booking);
+ } catch (error) {
+ next(error);
+ }
+ }
+);
+
+// POST /api/bookings/:id/reschedule - Reschedule booking
+router.post('/:id/reschedule',
+ [
+ body('date').isDate(),
+ body('time').matches(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/)
+ ],
+ async (req, res, next) => {
+ try {
+ const booking = await bookingService.reschedule(
+ req.params.id,
+ req.body.date,
+ req.body.time
+ );
+
+ // Send reschedule notification
+ await notificationService.sendReschedule(booking);
+
+ res.json(booking);
+ } catch (error) {
+ next(error);
+ }
+ }
+);
+
+module.exports = router;
+```
+
+### Availability Service
+
+```javascript
+// backend/src/services/availability.js
+class AvailabilityService {
+ async getAvailableSlots({ serviceId, staffId, date }) {
+ const service = await db.services.findById(serviceId);
+ if (!service || !service.is_active) {
+ throw new Error('Service not available');
+ }
+
+ // Get all active staff for this service
+ const staffMembers = staffId
+ ? [await db.staff.findById(staffId)]
+ : await db.staff.findByService(serviceId);
+
+ const allSlots = [];
+
+ for (const staff of staffMembers) {
+ if (!staff || !staff.is_active) continue;
+
+ const slots = await this.getStaffSlots(staff, service, date);
+ allSlots.push(...slots.map(s => ({ ...s, staff })));
+ }
+
+ return this.mergeSlots(allSlots);
+ }
+
+ async getStaffSlots(staff, service, date) {
+ const dayOfWeek = new Date(date).getDay();
+
+ // Get staff schedule for this day
+ const schedule = await db.staffSchedules.findOne({
+ staff_id: staff.id,
+ day_of_week: dayOfWeek,
+ is_working: true
+ });
+
+ if (!schedule) return [];
+
+ // Check time off
+ const timeOff = await db.staffTimeOff.findOne({
+ staff_id: staff.id,
+ start_date: { $lte: date },
+ end_date: { $gte: date }
+ });
+
+ if (timeOff) return [];
+
+ // Get existing bookings
+ const bookings = await db.bookings.find({
+ staff_id: staff.id,
+ booking_date: date,
+ status: { $in: ['pending', 'confirmed'] }
+ });
+
+ // Generate time slots
+ const slots = [];
+ let currentTime = this.parseTime(schedule.start_time);
+ const endTime = this.parseTime(schedule.end_time);
+ const bufferTime = service.buffer_time || 0;
+ const interval = service.duration + bufferTime;
+
+ while (this.addMinutes(currentTime, interval) <= endTime) {
+ const slotStart = this.formatTime(currentTime);
+ const slotEnd = this.formatTime(this.addMinutes(currentTime, service.duration));
+
+ // Check if in break
+ const inBreak = schedule.break_start &&
+ slotStart >= schedule.break_start &&
+ slotStart < schedule.break_end;
+
+ // Check if booked
+ const isBooked = bookings.some(b =>
+ b.start_time <= slotStart && b.end_time > slotStart
+ );
+
+ // Check minimum notice
+ const slotDateTime = new Date(`${date}T${slotStart}`);
+ const minNotice = this.getSetting('min_booking_notice');
+ const tooSoon = slotDateTime < this.addMinutes(new Date(), minNotice);
+
+ if (!inBreak && !isBooked && !tooSoon) {
+ slots.push({
+ start_time: slotStart,
+ end_time: slotEnd,
+ duration: service.duration
+ });
+ }
+
+ currentTime = this.addMinutes(currentTime, service.duration);
+ }
+
+ return slots;
+ }
+
+ parseTime(time) {
+ const [hours, minutes] = time.split(':').map(Number);
+ return hours * 60 + minutes;
+ }
+
+ formatTime(minutes) {
+ const h = Math.floor(minutes / 60);
+ const m = minutes % 60;
+ return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
+ }
+
+ addMinutes(time, minutes) {
+ if (time instanceof Date) {
+ return new Date(time.getTime() + minutes * 60000);
+ }
+ return time + minutes;
+ }
+}
+
+module.exports = new AvailabilityService();
+```
+
+### Staff Schedule API
+
+```javascript
+// backend/src/routes/admin/staff.js
+const router = require('express').Router();
+const auth = require('../../middleware/auth');
+
+// GET /api/admin/staff - List all staff
+router.get('/', auth.requireAuth, async (req, res, next) => {
+ try {
+ const staff = await db.staff.findAll();
+ res.json(staff);
+ } catch (error) {
+ next(error);
+ }
+});
+
+// PUT /api/admin/staff/:id/schedule - Update schedule
+router.put('/:id/schedule',
+ auth.requireAuth,
+ [
+ body('schedules').isArray({ min: 7, max: 7 }),
+ body('schedules.*.day_of_week').isInt({ min: 0, max: 6 }),
+ body('schedules.*.start_time').matches(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/),
+ body('schedules.*.end_time').matches(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/)
+ ],
+ async (req, res, next) => {
+ try {
+ const { id } = req.params;
+ const { schedules } = req.body;
+
+ // Validate times
+ for (const schedule of schedules) {
+ if (schedule.start_time >= schedule.end_time) {
+ return res.status(400).json({
+ error: 'Start time must be before end time'
+ });
+ }
+ }
+
+ // Update schedule
+ await db.staffSchedules.deleteByStaffId(id);
+
+ for (const schedule of schedules) {
+ await db.staffSchedules.create({
+ staff_id: id,
+ ...schedule
+ });
+ }
+
+ const updated = await db.staffSchedules.findByStaffId(id);
+ res.json(updated);
+ } catch (error) {
+ next(error);
+ }
+ }
+);
+
+// POST /api/admin/staff/:id/time-off - Add time off
+router.post('/:id/time-off',
+ auth.requireAuth,
+ [
+ body('start_date').isDate(),
+ body('end_date').isDate(),
+ body('reason').optional().isLength({ max: 200 })
+ ],
+ async (req, res, next) => {
+ try {
+ const timeOff = await db.staffTimeOff.create({
+ staff_id: req.params.id,
+ ...req.body
+ });
+
+ res.status(201).json(timeOff);
+ } catch (error) {
+ next(error);
+ }
+ }
+);
+
+module.exports = router;
+```
+
+## Step 4: Frontend Implementation
+
+**Agent**: `@FrontendDeveloper`
+
+### Public Booking Flow
+
+```vue
+
+
+
+
+
+
+ Select Service
+
+
+
+
+
+
+
+ {{ service.name }}
+
+ {{ service.duration }} min · ${{ service.price }}
+
+ {{ service.description }}
+
+
+
+
+
+ Continue
+
+
+
+
+
+ Select Staff (Optional)
+
+
+
+
+
+
+
+
+
+ {{ staff.name }}
+
+
+
+
+
+ Any Available
+
+
+
+
+ Back
+
+ Continue
+
+
+
+
+
+ Select Date & Time
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
event_busy
+
No slots available for this date
+
+
+
+
+ {{ slot.start_time }}
+
+
+
+
+
+ Back
+
+ Continue
+
+
+
+
+
+ Your Details
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back
+
+ Book Appointment
+
+
+
+
+
+ Confirmation
+
+
+
+
+
+ check_circle
+ Booking Confirmed!
+ Confirmation #{{ bookingNumber }}
+
+
+
+
+ Booking Details
+
+
+
+ Service
+ {{ selectedService.name }}
+
+
+ Date
+ {{ formatDate(selectedDate) }}
+
+
+ Time
+ {{ selectedSlot.start_time }}
+
+
+ Staff
+ {{ selectedStaff.name }}
+
+
+ Price
+ ${{ selectedService.price }}
+
+
+
+
+
+
+
+
+
+
+```
+
+### Admin Calendar View
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Day
+ Week
+ Month
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ isNewBooking ? 'New Booking' : 'Booking #' + selectedBooking?.booking_number }}
+
+
+
+
+
+
+
+
+
+
+
+
+ Customer
+
+ {{ selectedBooking?.customer_name }}
+ {{ selectedBooking?.customer_email }}
+
+
+
+ Service
+ {{ selectedBooking?.service.name }}
+
+
+ Time
+
+ {{ selectedBooking?.start_time }} - {{ selectedBooking?.end_time }}
+
+
+
+ Status
+
+
+ {{ selectedBooking?.status }}
+
+
+
+
+
+
+ Close
+
+
+
+ Confirm
+
+
+ Complete
+
+
+ Cancel
+
+
+
+
+
+
+
+
+
+```
+
+## Step 5: E2E Testing
+
+**Agent**: `@SDETEngineer`
+
+```javascript
+// tests/e2e/booking.spec.js
+import { test, expect } from '@playwright/test';
+
+test.describe('Booking Flow', () => {
+ test('complete booking flow', async ({ page }) => {
+ await page.goto('/');
+
+ // 1. Select service
+ await page.click('text=Book Now');
+ await page.click('.service-card:first-child');
+ await page.click('button:has-text("Continue")');
+
+ // 2. Select staff (optional)
+ await page.click('button:has-text("Continue")');
+
+ // 3. Select date and time
+ await page.click('.v-date-picker-body button:not([disabled])');
+ await page.click('.time-slot:first-child');
+ await page.click('button:has-text("Continue")');
+
+ // 4. Enter customer details
+ await page.fill('input[name="name"]', 'Test Customer');
+ await page.fill('input[name="email"]', 'test@example.com');
+ await page.fill('input[name="phone"]', '+1234567890');
+ await page.click('button:has-text("Book")');
+
+ // 5. Verify confirmation
+ await expect(page.locator('.success-card')).toBeVisible();
+ await expect(page.locator('.booking-number')).toBeVisible();
+ });
+
+ test('view booking details', async ({ page }) => {
+ // Create booking first
+ const bookingId = await createTestBooking();
+
+ await page.goto(`/booking/${bookingId}`);
+
+ await expect(page.locator('.service-name')).toBeVisible();
+ await expect(page.locator('.booking-date')).toBeVisible();
+ await expect(page.locator('.booking-time')).toBeVisible();
+ });
+
+ test('cancel booking', async ({ page }) => {
+ const bookingId = await createTestBooking();
+
+ await page.goto(`/booking/${bookingId}`);
+ await page.click('button:has-text("Cancel")');
+ await page.fill('textarea[name="reason"]', 'Test cancellation');
+ await page.click('button:has-text("Confirm")');
+
+ await expect(page.locator('.status-chip')).toContainText('cancelled');
+ });
+});
+
+test.describe('Admin Calendar', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/admin/login');
+ await page.fill('input[name="email"]', 'admin@example.com');
+ await page.fill('input[name="password"]', 'admin123');
+ await page.click('button[type="submit"]');
+ await expect(page).toHaveURL('/admin/dashboard');
+ });
+
+ test('view calendar', async ({ page }) => {
+ await page.goto('/admin/calendar');
+
+ await expect(page.locator('.v-calendar')).toBeVisible();
+ await expect(page.locator('.booking-event')).toHaveCountGreaterThanOrEqual(0);
+ });
+
+ test('create booking from calendar', async ({ page }) => {
+ await page.goto('/admin/calendar');
+
+ // Click on empty time slot
+ await page.click('.v-calendar__slot:first-child');
+
+ // Fill booking form
+ await page.selectOption('select[name="service"]', '1');
+ await page.fill('input[name="customer_name"]', 'Walk-in Customer');
+ await page.fill('input[name="customer_email"]', 'walkin@example.com');
+ await page.click('button:has-text("Create")');
+
+ await expect(page.locator('.notification')).toContainText('created');
+ });
+
+ test('confirm booking', async ({ page }) => {
+ const bookingId = await createTestBooking({ status: 'pending' });
+
+ await page.goto('/admin/calendar');
+ await page.click('.booking-event:first-child');
+ await page.click('button:has-text("Confirm")');
+
+ await expect(page.locator('.status-chip')).toContainText('confirmed');
+ });
+});
+
+test.describe('Staff Management', () => {
+ test('update staff schedule', async ({ page }) => {
+ await page.goto('/admin/staff');
+ await page.click('.staff-row:first-child .edit-btn');
+
+ // Update Monday schedule
+ await page.fill('input[name="monday_start"]', '09:00');
+ await page.fill('input[name="monday_end"]', '18:00');
+
+ await page.click('button:has-text("Save")');
+
+ await expect(page.locator('.notification')).toContainText('saved');
+ });
+
+ test('add time off', async ({ page }) => {
+ await page.goto('/admin/staff/1/time-off');
+
+ await page.fill('input[name="start_date"]', '2024-02-01');
+ await page.fill('input[name="end_date"]', '2024-02-05');
+ await page.fill('input[name="reason"]', 'Vacation');
+
+ await page.click('button:has-text("Add")');
+
+ await expect(page.locator('.time-off-item')).toBeVisible();
+ });
+});
+```
+
+## Step 6: Notifications
+
+### Email Service
+
+```javascript
+// backend/src/services/notification/email.js
+const nodemailer = require('nodemailer');
+const config = require('../../config');
+
+class EmailService {
+ constructor() {
+ this.transporter = nodemailer.createTransport({
+ host: config.email.host,
+ port: config.email.port,
+ secure: config.email.secure,
+ auth: config.email.auth
+ });
+ }
+
+ async sendConfirmation(booking, service, staff) {
+ const mailOptions = {
+ from: config.email.from,
+ to: booking.customer_email,
+ subject: `Booking Confirmed: ${service.name}`,
+ html: this.renderTemplate('confirmation', {
+ booking,
+ service,
+ staff,
+ businessName: config.business.name,
+ businessAddress: config.business.address
+ })
+ };
+
+ return this.transporter.sendMail(mailOptions);
+ }
+
+ async sendReminder(booking, service) {
+ const mailOptions = {
+ from: config.email.from,
+ to: booking.customer_email,
+ subject: `Reminder: ${service.name} tomorrow at ${booking.start_time}`,
+ html: this.renderTemplate('reminder', {
+ booking,
+ service,
+ businessName: config.business.name
+ })
+ };
+
+ return this.transporter.sendMail(mailOptions);
+ }
+
+ async sendCancellation(booking) {
+ const mailOptions = {
+ from: config.email.from,
+ to: booking.customer_email,
+ subject: `Booking Cancelled: ${booking.booking_number}`,
+ html: this.renderTemplate('cancellation', {
+ booking,
+ businessName: config.business.name
+ })
+ };
+
+ return this.transporter.sendMail(mailOptions);
+ }
+}
+
+module.exports = new EmailService();
+```
+
+### SMS Service
+
+```javascript
+// backend/src/services/notification/sms.js
+const config = require('../../config');
+
+class SMSService {
+ constructor() {
+ this.provider = this.getProvider();
+ }
+
+ getProvider() {
+ switch (config.sms.provider) {
+ case 'twilio':
+ return new (require('twilio'))(config.sms.accountSid, config.sms.authToken);
+ case 'smsru':
+ return require('smsru');
+ default:
+ return null;
+ }
+ }
+
+ async send(phone, message) {
+ if (!this.provider) {
+ console.log(`SMS to ${phone}: ${message}`);
+ return;
+ }
+
+ switch (config.sms.provider) {
+ case 'twilio':
+ return this.provider.messages.create({
+ body: message,
+ to: phone,
+ from: config.sms.fromNumber
+ });
+ case 'smsru':
+ return this.provider.send({
+ to: phone,
+ msg: message,
+ from: config.sms.fromName
+ });
+ }
+ }
+
+ async sendConfirmation(booking, service) {
+ const message = `Your ${service.name} appointment is confirmed for ${this.formatDate(booking.booking_date)} at ${booking.start_time}. Booking #${booking.booking_number}`;
+ return this.send(booking.customer_phone, message);
+ }
+
+ async sendReminder(booking, service) {
+ const message = `Reminder: ${service.name} in 2 hours at ${booking.start_time}. Reply C to cancel.`;
+ return this.send(booking.customer_phone, message);
+ }
+
+ formatDate(date) {
+ return new Date(date).toLocaleDateString('en-US', {
+ weekday: 'short',
+ month: 'short',
+ day: 'numeric'
+ });
+ }
+}
+
+module.exports = new SMSService();
+```
+
+## Step 7: Docker & Deployment
+
+Same structure as previous workflows.
+
+## Step 8: Documentation
+
+### API Reference
+
+```markdown
+# Booking API
+
+## Public Endpoints
+
+### Services
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| GET | /api/services | List active services |
+| GET | /api/services/:id | Get service details |
+| GET | /api/categories | List categories |
+
+### Availability
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| GET | /api/availability?service_id=&date=&staff_id= | Get available slots |
+| POST | /api/availability/check | Check specific slot |
+
+### Booking
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| POST | /api/bookings | Create booking |
+| GET | /api/bookings/:id | Get booking details |
+| POST | /api/bookings/:id/cancel | Cancel booking |
+| POST | /api/bookings/:id/reschedule | Reschedule booking |
+
+## Admin Endpoints
+
+### Services
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| GET | /api/admin/services | List all services |
+| POST | /api/admin/services | Create service |
+| PUT | /api/admin/services/:id | Update service |
+| DELETE | /api/admin/services/:id | Delete service |
+
+### Staff
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| GET | /api/admin/staff | List all staff |
+| POST | /api/admin/staff | Add staff |
+| PUT | /api/admin/staff/:id | Update staff |
+| PUT | /api/admin/staff/:id/schedule | Update schedule |
+| POST | /api/admin/staff/:id/time-off | Add time off |
+
+### Bookings
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| GET | /api/admin/bookings | List bookings |
+| PUT | /api/admin/bookings/:id/confirm | Confirm booking |
+| PUT | /api/admin/bookings/:id/complete | Mark complete |
+| PUT | /api/admin/bookings/:id/cancel | Cancel booking |
+
+### Reports
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| GET | /api/admin/reports/revenue | Revenue report |
+| GET | /api/admin/reports/services | Service popularity |
+| GET | /api/admin/reports/staff | Staff utilization |
+```
+
+## Post to Gitea
+
+After each step, post progress to the linked issue.
+
+## Quality Gates
+
+| Gate | Criteria |
+|------|----------|
+| Services | CRUD working, pricing, duration |
+| Staff | Schedules working, availability |
+| Availability | Real-time slots, timezone support |
+| Booking | Complete flow working |
+| Notifications | Email/SMS working |
+| Admin | Calendar, reports working |
+| Tests | E2E tests passing |
+| Docker | Containers building and running |
\ No newline at end of file
diff --git a/.kilo/skills/booking/SKILL.md b/.kilo/skills/booking/SKILL.md
new file mode 100644
index 0000000..305fcf7
--- /dev/null
+++ b/.kilo/skills/booking/SKILL.md
@@ -0,0 +1,639 @@
+---
+name: booking
+description: Booking domain knowledge - appointments, services, staff, schedules, reservations
+---
+
+# Booking Skill
+
+## Purpose
+
+Provides domain knowledge for building booking and appointment systems: services, staff, schedules, availability, reservations, notifications.
+
+## Capabilities
+
+### Service Management
+- Service categories
+- Service duration and pricing
+- Service variants (different durations)
+- Required resources
+
+### Staff Management
+- Staff profiles
+- Role-based permissions
+- Specializations
+- Availability schedules
+
+### Booking Flow
+- Service selection
+- Staff selection (optional)
+- Date/time selection
+- Customer details
+- Confirmation
+- Payment (optional)
+
+### Scheduling
+- Working hours
+- Break times
+- Days off
+- Multi-location support
+- Timezone handling
+
+### Notifications
+- Email confirmations
+- SMS reminders
+- Calendar sync (Google, iCal)
+- Push notifications
+
+### Admin Features
+- Booking management
+- Availability editor
+- Reports and analytics
+- Customer management
+
+## Database Schema
+
+### Services
+
+```sql
+CREATE TABLE services (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ category_id INTEGER,
+ name TEXT NOT NULL,
+ description TEXT,
+ duration INTEGER NOT NULL, -- minutes
+ price DECIMAL(10, 2) NOT NULL,
+ buffer_time INTEGER DEFAULT 0, -- minutes between appointments
+ max_per_slot INTEGER DEFAULT 1,
+ is_active BOOLEAN DEFAULT 1,
+ image_url TEXT,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (category_id) REFERENCES service_categories(id)
+);
+
+CREATE TABLE service_categories (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ slug TEXT UNIQUE NOT NULL,
+ description TEXT,
+ image_url TEXT,
+ sort_order INTEGER DEFAULT 0
+);
+
+CREATE TABLE service_staff (
+ service_id INTEGER NOT NULL,
+ staff_id INTEGER NOT NULL,
+ custom_price DECIMAL(10, 2),
+ custom_duration INTEGER,
+ PRIMARY KEY (service_id, staff_id),
+ FOREIGN KEY (service_id) REFERENCES services(id),
+ FOREIGN KEY (staff_id) REFERENCES staff(id)
+);
+```
+
+### Staff
+
+```sql
+CREATE TABLE staff (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER UNIQUE,
+ name TEXT NOT NULL,
+ email TEXT UNIQUE NOT NULL,
+ phone TEXT,
+ avatar_url TEXT,
+ bio TEXT,
+ role TEXT DEFAULT 'staff', -- 'admin', 'manager', 'staff'
+ is_active BOOLEAN DEFAULT 1,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users(id)
+);
+
+CREATE TABLE staff_schedules (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ staff_id INTEGER NOT NULL,
+ day_of_week INTEGER NOT NULL, -- 0=Sunday, 6=Saturday
+ start_time TEXT NOT NULL, -- '09:00'
+ end_time TEXT NOT NULL, -- '17:00'
+ break_start TEXT, -- '12:00'
+ break_end TEXT, -- '13:00'
+ is_working BOOLEAN DEFAULT 1,
+ FOREIGN KEY (staff_id) REFERENCES staff(id)
+);
+
+CREATE TABLE staff_time_off (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ staff_id INTEGER NOT NULL,
+ start_date DATE NOT NULL,
+ end_date DATE NOT NULL,
+ reason TEXT,
+ FOREIGN KEY (staff_id) REFERENCES staff(id)
+);
+```
+
+### Bookings
+
+```sql
+CREATE TABLE bookings (
+ id TEXT PRIMARY KEY, -- UUID
+ booking_number TEXT UNIQUE NOT NULL,
+ service_id INTEGER NOT NULL,
+ staff_id INTEGER,
+ customer_id INTEGER,
+
+ -- Customer info (guest bookings allowed)
+ customer_name TEXT NOT NULL,
+ customer_email TEXT NOT NULL,
+ customer_phone TEXT,
+ customer_notes TEXT,
+
+ -- Appointment details
+ booking_date DATE NOT NULL,
+ start_time TEXT NOT NULL,
+ end_time TEXT NOT NULL,
+
+ -- Status
+ status TEXT DEFAULT 'pending', -- 'pending', 'confirmed', 'completed', 'cancelled', 'no_show'
+
+ -- Pricing
+ service_price DECIMAL(10, 2) NOT NULL,
+ addons_total DECIMAL(10, 2) DEFAULT 0,
+ discount DECIMAL(10, 2) DEFAULT 0,
+ total DECIMAL(10, 2) NOT NULL,
+ payment_method TEXT, -- 'cash', 'card', 'online'
+ payment_status TEXT DEFAULT 'pending', -- 'pending', 'paid', 'refunded'
+
+ -- Metadata
+ source TEXT DEFAULT 'website', -- 'website', 'phone', 'walk_in'
+ notes TEXT,
+ internal_notes TEXT,
+
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+
+ FOREIGN KEY (service_id) REFERENCES services(id),
+ FOREIGN KEY (staff_id) REFERENCES staff(id),
+ FOREIGN KEY (customer_id) REFERENCES customers(id)
+);
+
+CREATE TABLE booking_addons (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ booking_id TEXT NOT NULL,
+ addon_id INTEGER NOT NULL,
+ price DECIMAL(10, 2) NOT NULL,
+ FOREIGN KEY (booking_id) REFERENCES bookings(id),
+ FOREIGN KEY (addon_id) REFERENCES service_addons(id)
+);
+
+-- Availability cache for fast queries
+CREATE TABLE availability_slots (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ service_id INTEGER NOT NULL,
+ staff_id INTEGER,
+ date DATE NOT NULL,
+ start_time TEXT NOT NULL,
+ end_time TEXT NOT NULL,
+ available BOOLEAN DEFAULT 1,
+ booking_id TEXT,
+ FOREIGN KEY (service_id) REFERENCES services(id),
+ FOREIGN KEY (staff_id) REFERENCES staff(id),
+ FOREIGN KEY (booking_id) REFERENCES bookings(id)
+);
+```
+
+### Customers
+
+```sql
+CREATE TABLE customers (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ email TEXT UNIQUE NOT NULL,
+ phone TEXT,
+ date_of_birth DATE,
+ notes TEXT,
+ total_visits INTEGER DEFAULT 0,
+ total_spent DECIMAL(10, 2) DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE INDEX idx_customers_email ON customers(email);
+CREATE INDEX idx_customers_phone ON customers(phone);
+```
+
+### Settings
+
+```sql
+CREATE TABLE booking_settings (
+ key TEXT PRIMARY KEY,
+ value TEXT NOT NULL,
+ type TEXT DEFAULT 'string' -- 'string', 'number', 'boolean', 'json'
+);
+
+-- Default settings
+INSERT INTO booking_settings (key, value, type) VALUES
+('timezone', 'Europe/Moscow', 'string'),
+('currency', 'RUB', 'string'),
+('booking_interval', '30', 'number'),
+('min_booking_notice', '60', 'number'), -- minutes
+('max_booking_advance', '30', 'number'), -- days
+('require_phone', '1', 'boolean'),
+('require_payment', '0', 'boolean'),
+('deposit_percentage', '20', 'number'),
+('cancellation_hours', '24', 'number'),
+('reminder_hours', '2', 'number'),
+('confirmation_sms', '0', 'boolean'),
+('confirmation_email', '1', 'boolean');
+```
+
+## API Endpoints
+
+### Public API
+
+```yaml
+# Services
+GET /api/services # List all active services
+GET /api/services/:id # Get service details
+GET /api/categories # List service categories
+
+# Staff
+GET /api/staff # List active staff
+GET /api/staff/:id # Get staff details
+GET /api/staff/:id/availability # Get staff availability
+
+# Availability
+GET /api/availability # Get available slots
+POST /api/availability/check # Check specific slot availability
+
+# Booking
+POST /api/bookings # Create booking (guest)
+GET /api/bookings/:id # Get booking details
+POST /api/bookings/:id/cancel # Cancel booking
+POST /api/bookings/:id/reschedule # Reschedule booking
+
+# Customer
+GET /api/customer/bookings # Get customer's bookings (auth)
+POST /api/customer/register # Register customer
+```
+
+### Admin API
+
+```yaml
+# Services
+GET /api/admin/services # List all services
+POST /api/admin/services # Create service
+PUT /api/admin/services/:id # Update service
+DELETE /api/admin/services/:id # Delete service
+
+# Categories
+GET /api/admin/categories # List categories
+POST /api/admin/categories # Create category
+PUT /api/admin/categories/:id # Update category
+DELETE /api/admin/categories/:id # Delete category
+
+# Staff
+GET /api/admin/staff # List all staff
+POST /api/admin/staff # Add staff member
+PUT /api/admin/staff/:id # Update staff
+DELETE /api/admin/staff/:id # Remove staff
+PUT /api/admin/staff/:id/schedule # Update schedule
+POST /api/admin/staff/:id/time-off # Add time off
+
+# Bookings
+GET /api/admin/bookings # List bookings (filters)
+GET /api/admin/bookings/:id # Get booking details
+PUT /api/admin/bookings/:id/confirm # Confirm booking
+PUT /api/admin/bookings/:id/complete # Mark complete
+PUT /api/admin/bookings/:id/cancel # Cancel booking
+PUT /api/admin/bookings/:id/no-show # Mark no-show
+
+# Calendar
+GET /api/admin/calendar # Calendar view
+GET /api/admin/calendar/staff/:id # Staff calendar
+
+# Customers
+GET /api/admin/customers # List customers
+GET /api/admin/customers/:id # Customer details
+GET /api/admin/customers/:id/history # Booking history
+
+# Reports
+GET /api/admin/reports/revenue # Revenue report
+GET /api/admin/reports/services # Service popularity
+GET /api/admin/reports/staff # Staff utilization
+GET /api/admin/reports/trends # Booking trends
+
+# Settings
+GET /api/admin/settings # Get all settings
+PUT /api/admin/settings # Update settings
+```
+
+## Availability Logic
+
+### Get Available Slots
+
+```javascript
+// services/availability.js
+async function getAvailableSlots(serviceId, staffId, date) {
+ const service = await db.services.findById(serviceId);
+ const dayOfWeek = new Date(date).getDay();
+
+ // Get staff schedule
+ const schedule = await db.staffSchedules.findOne({
+ staff_id: staffId,
+ day_of_week: dayOfWeek,
+ is_working: true
+ });
+
+ if (!schedule) return [];
+
+ // Get existing bookings
+ const bookings = await db.bookings.find({
+ staff_id: staffId,
+ booking_date: date,
+ status: { $in: ['pending', 'confirmed'] }
+ });
+
+ // Generate slots
+ const slots = [];
+ let currentTime = parseTime(schedule.start_time);
+ const endTime = parseTime(schedule.end_time);
+ const bufferTime = service.buffer_time || 0;
+
+ while (addMinutes(currentTime, service.duration + bufferTime) <= endTime) {
+ const slotStart = formatTime(currentTime);
+ const slotEnd = formatTime(addMinutes(currentTime, service.duration));
+
+ // Check if slot is available
+ const isBooked = bookings.some(b =>
+ b.start_time <= slotStart && b.end_time >= slotStart ||
+ b.start_time <= slotEnd && b.end_time >= slotEnd ||
+ b.start_time >= slotStart && b.end_time <= slotEnd
+ );
+
+ // Check break time
+ const isBreak = schedule.break_start && (
+ slotStart >= schedule.break_start && slotStart < schedule.break_end ||
+ slotEnd > schedule.break_start && slotEnd <= schedule.break_end
+ );
+
+ // Check advance booking limit
+ const slotDateTime = new Date(`${date}T${slotStart}`);
+ const isTooSoon = slotDateTime < addMinutes(new Date(), settings.min_booking_notice);
+
+ // Check past
+ const isPast = slotDateTime < new Date();
+
+ if (!isBooked && !isBreak && !isTooSoon && !isPast) {
+ slots.push({
+ start_time: slotStart,
+ end_time: slotEnd,
+ available: true
+ });
+ }
+
+ currentTime = addMinutes(currentTime, service.duration);
+ }
+
+ return slots;
+}
+```
+
+### Create Booking
+
+```javascript
+// services/booking.js
+async function createBooking(bookingData) {
+ const { service_id, staff_id, date, time, customer_name, customer_email, customer_phone } = bookingData;
+
+ // Validate service exists
+ const service = await db.services.findById(service_id);
+ if (!service || !service.is_active) {
+ throw new Error('Service not available');
+ }
+
+ // Get staff or auto-assign
+ let staff = staff_id ?
+ await db.staff.findById(staff_id) :
+ await autoAssignStaff(service_id, date, time);
+
+ if (!staff) {
+ throw new Error('No staff available for this slot');
+ }
+
+ // Check availability
+ const available = await checkAvailability(service_id, staff.id, date, time);
+ if (!available) {
+ throw new Error('Slot already booked');
+ }
+
+ // Calculate end time
+ const endTime = addMinutes(parseTime(time), service.duration);
+
+ // Create booking number
+ const bookingNumber = generateBookingNumber();
+
+ // Create booking
+ const booking = await db.bookings.create({
+ id: generateUUID(),
+ booking_number: bookingNumber,
+ service_id,
+ staff_id: staff.id,
+ customer_name,
+ customer_email,
+ customer_phone,
+ booking_date: date,
+ start_time: time,
+ end_time: formatTime(endTime),
+ status: 'pending',
+ service_price: service.price,
+ total: service.price
+ });
+
+ // Create availability slot
+ await db.availabilitySlots.create({
+ service_id,
+ staff_id: staff.id,
+ date,
+ start_time: time,
+ end_time: formatTime(endTime),
+ available: false,
+ booking_id: booking.id
+ });
+
+ // Send confirmation
+ await sendConfirmation(booking);
+
+ return booking;
+}
+```
+
+## Booking Status Flow
+
+```
+pending → confirmed → completed
+ ↓ ↓ ↓
+cancelled cancelled cancelled → refunded
+ ↓
+no_show
+```
+
+### Status Transitions
+
+```javascript
+const STATUS_FLOW = {
+ pending: ['confirmed', 'cancelled'],
+ confirmed: ['completed', 'cancelled', 'no_show'],
+ completed: ['refunded'],
+ cancelled: ['refunded'],
+ no_show: [],
+ refunded: []
+};
+
+function canTransition(currentStatus, newStatus) {
+ return STATUS_FLOW[currentStatus]?.includes(newStatus) || false;
+}
+```
+
+## Notifications
+
+### Email Templates
+
+```javascript
+// services/notifications/email.js
+const bookingConfirmation = (booking, service, staff) => ({
+ subject: `Booking Confirmed - ${service.name}`,
+ html: `
+
Your Booking is Confirmed!
+ Booking #${booking.booking_number}
+
+ Details
+
+ - Service: ${service.name}
+ - Staff: ${staff.name}
+ - Date: ${formatDate(booking.booking_date)}
+ - Time: ${booking.start_time} - ${booking.end_time}
+ - Price: ${formatCurrency(booking.total)}
+
+
+ Location: Your Business Name
+
+ Manage Booking
+
+ Need to cancel? Please give us ${settings.cancellation_hours} hours notice.
+ `
+});
+
+const bookingReminder = (booking, service, staff) => ({
+ subject: `Reminder: ${service.name} in 2 hours`,
+ html: `
+ Upcoming Appointment Reminder
+ Your appointment is in 2 hours!
+
+
+ - Service: ${service.name}
+ - Staff: ${staff.name}
+ - Time: ${booking.start_time}
+
+ `
+});
+```
+
+### SMS Templates
+
+```javascript
+// services/notifications/sms.js
+const templates = {
+ confirmation: (booking, service) =>
+ `Your ${service.name} booking is confirmed for ${formatDate(booking.booking_date)} at ${booking.start_time}. Booking #${booking.booking_number}`,
+
+ reminder: (booking, service) =>
+ `Reminder: ${service.name} appointment in 2 hours at ${booking.start_time}. Reply C to cancel.`,
+
+ cancellation: (booking) =>
+ `Your booking #${booking.booking_number} has been cancelled.`
+};
+```
+
+## Calendar Integration
+
+### iCal Export
+
+```javascript
+function generateICal(booking, service, staff) {
+ return `BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Business Name//Booking System//EN
+BEGIN:VEVENT
+DTSTART:${formatICalDate(booking.booking_date, booking.start_time)}
+DTEND:${formatICalDate(booking.booking_date, booking.end_time)}
+SUMMARY:${service.name} with ${staff.name}
+DESCRIPTION:Booking #${booking.booking_number}
+LOCATION:Business Address
+STATUS:CONFIRMED
+END:VEVENT
+END:VCALENDAR`;
+}
+```
+
+## Reports
+
+### Revenue Report
+
+```sql
+SELECT
+ DATE(booking_date) as date,
+ COUNT(*) as bookings,
+ SUM(total) as revenue,
+ AVG(total) as avg_booking
+FROM bookings
+WHERE status IN ('completed', 'confirmed')
+ AND booking_date BETWEEN ? AND ?
+GROUP BY DATE(booking_date)
+ORDER BY date;
+```
+
+### Staff Utilization
+
+```sql
+SELECT
+ s.id,
+ s.name,
+ COUNT(b.id) as bookings,
+ SUM(TIMESTAMPDIFF(MINUTE,
+ CONCAT(b.booking_date, ' ', b.start_time),
+ CONCAT(b.booking_date, ' ', b.end_time)
+ )) / 60 as hours_booked,
+ COUNT(DISTINCT b.booking_date) as days_worked
+FROM staff s
+LEFT JOIN bookings b ON b.staff_id = s.id
+ AND b.status IN ('completed', 'confirmed')
+ AND b.booking_date BETWEEN ? AND ?
+GROUP BY s.id
+ORDER BY bookings DESC;
+```
+
+### Service Popularity
+
+```sql
+SELECT
+ s.id,
+ s.name,
+ s.category_id,
+ COUNT(b.id) as bookings,
+ SUM(b.total) as revenue
+FROM services s
+LEFT JOIN bookings b ON b.service_id = s.id
+ AND b.status IN ('completed', 'confirmed')
+ AND b.booking_date BETWEEN ? AND ?
+GROUP BY s.id
+ORDER BY bookings DESC;
+```
+
+## Integration Points
+
+- Payment: Stripe, YooKassa
+- Calendar: Google Calendar, iCal
+- SMS: Twilio, SMS.ru
+- Email: SendGrid, Mailgun
+- Analytics: Google Analytics, Yandex Metrika
+- CRM: Integration API for customer data
\ No newline at end of file