feat(bookings): improve admin booking wizard and user dashboard

Backend:
- Enriched GetAllUserBookings response with calculated total_amount,
  amount_paid, and duration_minutes.
- Refactored GetBookingHandler to return a flat booking object matching
  frontend expectations.
- Added account_role to admin user list response and sorted users by
  booking activity.
- Corrected function name oo to AdminCreateBookingForUserHandler.

Frontend:
- Rebuilt BookingCreateModal into a 4-step wizard supporting guest
  bookings, service overrides, and real-time availability checks.
- Fixed account dashboard logic to correctly identify upcoming vs past
  bookings and sort unpaid items to the top.
- Extracted booking flow into a shared BookingFlow component.
- Redirected admin users from home page to /today.
This commit is contained in:
2026-02-12 22:15:10 +00:00
parent 2ace6d4d87
commit 50746595e7
34 changed files with 7688 additions and 5413 deletions
+10 -11
View File
@@ -4,21 +4,21 @@
"type": "split",
"children": [
{
"id": "f918f140cb277962",
"id": "4f4ac1241ce3420f",
"type": "tabs",
"children": [
{
"id": "7fe99991d0e457c6",
"id": "0c84336298f72379",
"type": "leaf",
"state": {
"type": "markdown",
"state": {
"file": "Express.js Cheat Sheet.md",
"mode": "preview",
"file": "Crussell/Crussell Nails.md",
"mode": "source",
"source": false
},
"icon": "lucide-file",
"title": "Express.js Cheat Sheet"
"title": "Crussell Nails"
}
}
]
@@ -78,8 +78,7 @@
}
],
"direction": "horizontal",
"width": 300,
"collapsed": true
"width": 300
},
"right": {
"id": "2750d7726f904ef3",
@@ -170,12 +169,12 @@
"bases:Create new base": false
}
},
"active": "7fe99991d0e457c6",
"active": "0c84336298f72379",
"lastOpenFiles": [
"Crussell/Backend/bookings.md",
"Crussell/Crussell Nails.md",
"Untitled.base",
"Untitled.canvas",
"Express.js Cheat Sheet.md",
"Crussell/Crussell Nails.md",
"Crussell/Backend/bookings.md"
"Express.js Cheat Sheet.md"
]
}
-522
View File
@@ -1,522 +0,0 @@
# Booking API - cURL Examples (Revised)
## User Endpoints (Requires Authentication)
-----
### 1\. Create Booking
Creates a new booking with the specified services and preferred time.
```bash
curl -X POST http://localhost:8080/api/bookings \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-d '{
"start_time": "2025-10-25T14:00:00Z",
"service_ids": ["service-uuid-1", "service-uuid-2"],
"notes": "Please use the side entrance"
}'
```
**Response (201 Created):**
```json
{
"id": "booking-uuid",
"user_id": "user-uuid",
"start_time": "2025-10-25T14:00:00Z",
"status": "pending",
"notes": "Please use the side entrance",
"created_at": "2025-10-21T10:30:00Z",
"updated_at": "2025-10-21T10:30:00Z",
"created_by": "user-uuid"
}
```
-----
### 2\. Get User Booking by ID
Retrieves a specific booking for the authenticated user.
```bash
curl -X GET http://localhost:8080/api/bookings/booking-uuid \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
```
**Response (200 OK):**
*The response now consistently includes all joined data: `services` and `payments`.*
```json
{
"id": "booking-uuid",
"user_id": "user-uuid",
"start_time": "2025-10-25T14:00:00Z",
"status": "pending",
"notes": "Please use the side entrance",
"created_at": "2025-10-21T10:30:00Z",
"updated_at": "2025-10-21T10:30:00Z",
"created_by": "user-uuid",
"services": [
{
"booking_id": "booking-uuid",
"service_id": "service-uuid-1",
"override_price": null,
"override_duration_minutes": null,
"service_name": "Haircut",
"base_price": 25.00
}
],
"payments": [
{
"id": "payment-uuid",
"booking_id": "booking-uuid",
"payment_type": "deposit",
"payment_method": "card",
"status": "completed",
"amount": 10.00,
"is_vat_applicable": true,
"vat_rate": 20.0,
"vat_amount": 2.00,
"net_amount": 8.00
}
]
}
```
-----
### 3\. Get All User Bookings
Retrieves a list of all bookings associated with the authenticated user.
```bash
curl -X GET http://localhost:8080/api/bookings \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
```
**Response (200 OK):**
```json
[
{
"id": "booking-uuid-1",
"user_id": "user-uuid",
"start_time": "2025-10-25T14:00:00Z",
"status": "pending",
"updated_at": "2025-10-21T10:30:00Z"
},
{
"id": "booking-uuid-2",
"user_id": "user-uuid",
"start_time": "2025-10-26T10:00:00Z",
"status": "confirmed",
"updated_at": "2025-10-22T09:00:00Z"
}
]
```
-----
### 4\. Edit Booking (Change Start Time and/or Notes)
Updates the `start_time` and/or `notes` for a booking.
```bash
curl -X PUT http://localhost:8080/api/bookings/booking-uuid \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-d '{
"start_time": "2025-10-25T15:00:00Z",
"notes": "Please use the front entrance this time"
}'
```
**Response (200 OK):**
```json
{
"id": "booking-uuid",
"user_id": "user-uuid",
"start_time": "2025-10-25T15:00:00Z",
"status": "pending",
"notes": "Please use the front entrance this time",
"created_at": "2025-10-21T10:30:00Z",
"updated_at": "2025-10-21T10:40:00Z",
"created_by": "user-uuid"
}
```
-----
### 5\. Delete/Cancel Booking
This endpoint performs a **hard delete** if no payments exist, or a **soft delete (cancel)** if payments exist, requiring a `reason`.
#### A. Hard Delete (No Payments)
```bash
# Hard delete when no payments exist
curl -X DELETE http://localhost:8080/api/bookings/booking-uuid \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
```
**Response (200 OK):**
```json
{
"message": "Booking deleted successfully",
"id": "booking-uuid"
}
```
#### B. Cancel Booking (With Payments)
```bash
# Soft delete/cancel when payments exist - requires reason
curl -X DELETE http://localhost:8080/api/bookings/booking-uuid \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-d '{
"reason": "client_cancelled"
}'
```
**Valid reasons:** `client_cancelled`, `we_cancelled`, `re-schedule`, `no_show`
**Response (200 OK):**
```json
{
"message": "Booking cancelled successfully",
"id": "booking-uuid",
"status": "client_cancelled"
}
```
-----
-----
## Admin Endpoints (Requires Authentication + Admin Role)
-----
### 6\. Get All Admin Bookings
Retrieves a paginated list of all bookings in the system.
```bash
curl -X GET 'http://localhost:8080/api/admin/bookings?limit=50&offset=0' \
-H "Authorization: Bearer YOUR_ADMIN_JWT_TOKEN"
```
**Response (200 OK):**
```json
[
{
"id": "booking-uuid-a",
"user_id": "user-uuid-1",
"start_time": "2025-10-25T14:00:00Z",
"status": "confirmed",
"updated_at": "2025-10-21T10:45:00Z"
},
{
"id": "booking-uuid-b",
"user_id": "user-uuid-2",
"start_time": "2025-10-26T10:00:00Z",
"status": "pending",
"updated_at": "2025-10-22T09:00:00Z"
}
]
```
-----
### 7\. Search Admin Bookings
Retrieves a filtered list of bookings based on query parameters.
```bash
# Search for all 'pending' bookings for a specific user ID
curl -X GET 'http://localhost:8080/api/admin/bookings/search?status=pending&user_id=user-uuid-1&start_date=2025-10-01' \
-H "Authorization: Bearer YOUR_ADMIN_JWT_TOKEN"
```
**Response (200 OK):**
*Same list format as 'Get All Admin Bookings'*
-----
### 8\. Get Admin Bookings by User ID
Retrieves all bookings for a single, specified user.
```bash
curl -X GET http://localhost:8080/api/admin/bookings/user/user-uuid \
-H "Authorization: Bearer YOUR_ADMIN_JWT_TOKEN"
```
**Response (200 OK):**
*Same list format as 'Get All Admin Bookings'*
-----
### 9\. Get Admin Booking by ID
Retrieves a specific booking including all service and payment details.
```bash
curl -X GET http://localhost:8080/api/admin/bookings/booking-uuid \
-H "Authorization: Bearer YOUR_ADMIN_JWT_TOKEN"
```
**Response (200 OK):**
*Full booking object, including `services` and `payments` arrays.*
```json
{
"id": "booking-uuid",
"user_id": "user-uuid",
"start_time": "2025-10-25T14:00:00Z",
"status": "confirmed",
"notes": "Please use the side entrance",
"created_at": "2025-10-21T10:30:00Z",
"updated_at": "2025-10-21T10:45:00Z",
"created_by": "admin-uuid",
"services": [ ... ],
"payments": [ ... ]
}
```
-----
### 10\. Get Admin Booking Summary
Retrieves the booking, user details, services, payments, and financial totals in a single, comprehensive response.
```bash
curl -X GET http://localhost:8080/api/admin/bookings/booking-uuid/summary \
-H "Authorization: Bearer YOUR_ADMIN_JWT_TOKEN"
```
**Response (200 OK):**
```json
{
"booking": {
"id": "booking-uuid",
"user_id": "user-uuid",
"start_time": "2025-10-25T14:00:00Z",
"status": "confirmed",
"notes": "Please use the side entrance",
"created_at": "2025-10-21T10:30:00Z",
"updated_at": "2025-10-21T10:45:00Z",
"created_by": "admin-uuid"
},
"user": {
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"account_role": "client",
"loyalty_stamps": 5
// ... other user fields
},
"services": [
{
"service_name": "Haircut",
"base_price": 25.00,
"override_price": 20.00
// ... other service fields
}
],
"payments": [
{
"id": "payment-uuid",
"payment_type": "deposit",
"status": "completed",
"amount": 10.00,
"vat_rate": 20.0
// ... other payment fields
}
],
"total_amount": 20.00,
"amount_paid": 10.00,
"amount_due": 10.00,
"duration_minutes": 25
}
```
-----
### 11\. Progress Booking Status
Moves the booking to the next status in its lifecycle (e.g., from `confirmed` to `in_progress`).
```bash
curl -X PUT http://localhost:8080/api/admin/bookings/booking-uuid/progress \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_ADMIN_JWT_TOKEN" \
-d '{
"status": "in_progress"
}'
```
**Valid statuses:** `pending`, `confirmed`, `in_progress`, `completed`, `client_cancelled`, `we_cancelled`, `re-schedule`, `no_show`
**Response (200 OK):**
```json
{
"id": "booking-uuid",
"user_id": "user-uuid",
"start_time": "2025-10-25T14:00:00Z",
"status": "in_progress",
"notes": "Please use the side entrance",
"created_at": "2025-10-21T10:30:00Z",
"updated_at": "2025-10-25T14:05:00Z",
"created_by": "admin-uuid"
}
```
-----
### 12\. Confirm Booking (With/Without Overrides)
Confirms a `pending` booking, optionally applying price/duration overrides and updating notes.
#### A. Confirm with Overrides
```bash
curl -X POST http://localhost:8080/api/admin/bookings/booking-uuid/confirm \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_ADMIN_JWT_TOKEN" \
-d '{
"service_overrides": [
{
"service_id": "service-uuid-1",
"override_price": 20.00,
"override_duration_minutes": 25
}
],
"notes": "Confirmed by phone. Applied special discount."
}'
```
#### B. Confirm with Notes Update Only
```bash
curl -X POST http://localhost:8080/api/admin/bookings/booking-uuid/confirm \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_ADMIN_JWT_TOKEN" \
-d '{
"notes": "Confirmed via email"
}'
```
**Response (200 OK):**
```json
{
"id": "booking-uuid",
"user_id": "user-uuid",
"start_time": "2025-10-25T14:00:00Z",
"status": "confirmed",
"notes": "Confirmed by phone. Applied special discount.",
"created_at": "2025-10-21T10:30:00Z",
"updated_at": "2025-10-21T10:50:00Z",
"created_by": "admin-uuid"
}
```
-----
### 13\. Add Payment to Booking (New Endpoint)
Adds a new payment record to the specified booking.
```bash
curl -X POST http://localhost:8080/api/admin/bookings/booking-uuid/payments \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_ADMIN_JWT_TOKEN" \
-d '{
"payment_type": "final",
"payment_method": "cash",
"status": "completed",
"amount": 15.00,
"is_vat_applicable": false,
"invoice_number": 1042
}'
```
**Valid Payment Types:** `deposit`, `final`
**Valid Payment Methods:** `card`, `cash`, `bank_transfer`
**Valid Payment Statuses:** `pending`, `completed`, `failed`
**Response (201 Created):**
*Returns the newly created payment object.*
```json
{
"id": "new-payment-uuid",
"booking_id": "booking-uuid",
"payment_type": "final",
"payment_method": "cash",
"status": "completed",
"amount": 15.00,
"is_vat_applicable": false,
"vat_rate": null,
"vat_amount": null,
"net_amount": 15.00,
"invoice_number": 1042,
"created_at": "2025-10-22T12:00:00Z",
"updated_at": "2025-10-22T12:00:00Z",
"created_by": "admin-uuid"
}
```
-----
### 14\. Remove Payment from Booking (New Endpoint)
Deletes a specific payment record associated with a booking.
```bash
curl -X DELETE http://localhost:8080/api/admin/bookings/booking-uuid/payments/payment-uuid-to-delete \
-H "Authorization: Bearer YOUR_ADMIN_JWT_TOKEN"
```
**Response (200 OK):**
```json
{
"message": "Payment removed successfully",
"id": "payment-uuid-to-delete"
}
```
-----
## Error Responses
| Status Code | Example Response | Description |
| :--- | :--- | :--- |
| **400 Bad Request** | `{"error": "Start time is required"}` | Missing or invalid required fields. |
| **401 Unauthorized** | `{"error": "Authentication required"}` | Missing or expired JWT token. |
| **403 Forbidden** | `{"error": "Admin access required"}` | Attempting to access an admin endpoint without the necessary role. |
| **404 Not Found** | `{"error": "Booking not found or access denied"}` | Resource does not exist or user/admin does not have permission to view it. |
| **500 Internal Server Error** | `{"error": "Internal server error"}` | Unhandled server error. |
-----
## Notes
1. Replace `YOUR_JWT_TOKEN` with a standard user's JWT token.
2. Replace `YOUR_ADMIN_JWT_TOKEN` with an admin user's JWT token.
3. Replace UUIDs (`booking-uuid`, `service-uuid-1`, etc.) with actual IDs from your database.
4. All timestamps should be in **RFC3339 format (ISO 8601)**, e.g., `"2025-10-25T14:00:00Z"`.
5. Start times must be in the future when creating or editing bookings.
6. Bookings can only be **hard-deleted** (Endpoint 5A) if they have no associated payments. Otherwise, a `reason` must be supplied to cancel the booking (Endpoint 5B).
7. The **Confirm Booking** endpoint (12) only works on bookings with status `pending`.
+487 -234
View File
@@ -1,299 +1,552 @@
## ✅ / ⏳ Project Checklist
### Backend (Go / Chi / Postgres)
- [x] JWT authentication (login, refresh, role verification)
- [x] User registration with input validation
- [x] Password hashing with bcrypt
- [x] Middleware for auth/roles
- [x] DB connection pooling
- [ ] Booking endpoints
-`/api/bookings` (CRUD for users)
-`/api/admin/bookings` (list, search, userbyuser, progress, confirm)
- [ ] Admin endpoints
-`/api/admin/services` (create, delete, list, toggle)
-`/api/admin/bookings` (list, search, user bookings, progress, confirm)
-`/api/admin/users` (list, view) *handlers still to be implemented*
- [ ] Unit tests
- [ ] CI/CD
### Frontend (SvelteKit / Tailwind / shadcn)
- [x] Core pages (Home, Prices, Contact, Book)
- [x] Booking wizard prototype
- [x] Calendar/time slot selector
- [ ] API integration for booking flow
*The wizard currently only shows a prototype alert. Wire it to the backend `/api/bookings` POST endpoint in the next iteration.*
- [ ] Payments (Stripe/Square) *pending integration*
- [ ] Auth screens
- [ ] Admin dashboard
### Infrastructure
- [x] `.env` config
- [x] Docker Compose full stack
- [ ] nginx reverse proxy *still under review*
- [ ] Monitoring/logging *no stack configured*
- [ ] CI/CD pipeline *not yet defined*
### Integrations
- [x] CardDAV sync for contacts
- [ ] Email/SMS reminders *not yet implemented*
- [ ] Payment provider *placeholder only*
- [ ] Loyalty tracking frontend *no component yet*
> **Last Updated:** January 2025
> **Status:** Work in Progress
---
## 📐 Current Architecture
## Project Checklist
### Backend (Go / Chi / Postgres)
#### Authentication & Authorization
- [x] JWT authentication (login, refresh, role verification)
- [x] User registration with input validation
- Names: 1-50 chars, unicode letters/spaces/hyphen/apostrophe/dot
- Phone: UK format → E.164 (+44...)
- Email: standard format
- Age: Must be 16+ years
- [x] Password hashing with bcrypt
- [x] Middleware for auth/roles (`mw.RequireAuth`, `mw.RequireAdmin`)
- [x] DB connection pooling
- [ ] **Refresh token endpoint** - `RefreshTokenHandler` exists at `local.go:322`, NOT wired in router
- [x] Login rate limiting (1 attempt per 5 seconds)
#### Booking System
- [x] `/api/bookings` - Full CRUD for authenticated users
- [x] `/api/admin/bookings` - List, search, create for user, progress, confirm, cancel
- [x] `/api/admin/bookings/search` - Search functionality
- [x] `/api/admin/bookings/user/{user_id}` - User-specific bookings
- [x] `/api/admin/bookings/{id}/progress` - Progress booking status
- [x] `/api/admin/bookings/{id}/confirm` - Confirm booking
- [x] `/api/admin/bookings/{id}/cancel` - Cancel booking
- [ ] **In-progress auto-infer** - Status should auto-set based on time
- [ ] **Begin button on Today** - Manual start for early arrivals (gray out if >3hrs away)
#### Admin Endpoints
- [x] `/api/admin/services` - Create, delete, list, toggle
- [x] `/api/admin/users` - List, view with booking history
- [x] `/api/admin/today` - Current/next appointment, today's appointments, pending approvals
- [x] `/api/admin/notifications` - GET/acknowledge endpoint wired, but:
- [ ] Frontend UI to display notifications
- [ ] Push mechanism (currently only pull-based)
- [ ] User notifications (only admin notifications exist)
#### Scheduling System
- [x] `/api/scheduling/default-hours` - GET public, PUT admin
- [x] `/api/scheduling/exceptional-groups` - CRUD for holiday/special hours
- [x] `/api/scheduling/working-hours` - Merged default + exceptional hours
- [x] `/api/scheduling/available-hours` - Available slots accounting for bookings
#### User Endpoints
- [x] `/api/user/profile` - GET, PUT
- [x] `/api/user/account` - DELETE (GDPR compliant)
- [x] `/api/user/loyalty` - GET loyalty stamps
- [ ] **GDPR data export** - `export_all_user_data()` exists but not wired to endpoint
- [ ] **Tax data export** - Admin endpoint for tax-software-compatible format
#### Not Yet Wired
- [ ] Social auth (`handlers/auth/social.go` exists, not imported)
- [ ] Analytics (`handlers/admin/analytics.go` exists, not imported)
- [ ] Portfolio/images (`handlers/portfolio/images.go` exists, not imported)
- [ ] Guest user endpoint (`/api/users/guest` - needed for walk-in bookings)
#### Unit Tests & CI/CD
- [ ] Unit tests
- [ ] CI/CD pipeline
---
### Frontend (SvelteKit / Tailwind / shadcn)
#### Core Pages
- [x] Home (`/`)
- [x] Prices (`/prices`)
- [x] Contact (`/contact`)
- [x] Book (`/book`) - Full wizard with service selection, date/time, customer details
- [ ] Portfolio (`/portfolio`) - Stubbed, needs S3/R2 integration for images
- [x] Today (`/today`) - Admin only, real-time schedule view
- [x] Account (`/account`)
- [x] Login (`/login`)
- [x] Manage (`/manage`)
#### Admin Dashboard (`/admin`)
- [x] Auth guard with role check
- [x] ImageUpload component
- [x] UsersCard + UserModal
- [x] BookingsCard + BookingModal
- [x] HolidayHours (exceptional hours management)
- [x] WeeklySchedule (default hours management)
- [x] ServicesManagement
- [x] BookingCreateModal (call-in/admin booking creation)
- [x] WalkInBooking + WalkInCreateModal
- [x] CallInBooking
- [x] ApprovalModal
#### Booking Flow
- [x] Service selection with pricing/duration
- [x] Calendar with availability detection
- [x] Time slot generation with gap logic
- [x] Customer details form (guest or authenticated)
- [x] Auth store with token refresh logic
- [ ] **Customer booking submit** - `submitBooking()` only logs, needs `POST /api/bookings`
- [ ] Payment integration (Square placeholder)
#### API Integration
- [x] Services fetch from `/api/services`
- [x] Working hours fetch from `/api/scheduling/working-hours`
- [x] Available hours fetch from `/api/scheduling/available-hours`
- [x] Admin bookings use `/api/admin/bookings`
- [ ] Guest user creation (`/api/users/guest` not implemented)
---
### Infrastructure
- [x] `.env` config
- [x] Docker Compose (postgres, backend, sabredav, nginx)
- [x] Static frontend build served via nginx
- [ ] nginx reverse proxy - config under review
- [ ] Monitoring/logging - no stack configured
- [ ] CI/CD pipeline (Gitea) - not yet defined
- [ ] Prometheus metrics integration
---
### Integrations
- [x] CardDAV sync for contacts (SabreDAV)
- [x] CalDAV ready
- [ ] Email/SMS reminders - not yet implemented
- [ ] Square payment - placeholder only
- [ ] S3/R2 image hosting - not configured
---
## Current Architecture
### Overview Diagram
```mermaid
flowchart TD
User([Customer])
Admin([Admin])
CardReader[Square Card Reader<br/>Physical Terminal]
subgraph CF[Cloudflare Protection]
CDN[CDN / DDoS Protection]
subgraph Docker[Docker Compose Stack]
subgraph NGINX[Nginx :80/:443]
Static[Static Frontend Build]
Proxy[API Proxy → Backend:8080]
DAVProxy[DAV Proxy → SabreDAV]
end
subgraph Backend[Go + Chi :8080]
Router[chi Router]
Auth[JWT Middleware]
Handlers[API Handlers]
end
subgraph Database[PostgreSQL :5432]
DB[(Users / Bookings<br/>Payments / Services<br/>Scheduling)]
end
subgraph DAV[SabreDAV :9000]
CardDAV[(vCard Contacts)]
CalDAV[(Calendar Events)]
end
end
subgraph Lightsail[AWS Lightsail Instance]
subgraph NGINX[Nginx Reverse Proxy]
Proxy[Port 443/80]
end
subgraph Frontend[SvelteKit Frontend]
UI[Booking UI / Prices / Contact]
AdminUI[Admin Dashboard]
APIProxy[API Proxy Routes]
end
subgraph Backend[Go + Chi Backend]
Router[chi Router]
Auth[JWT Middleware]
Handlers[API Handlers]
end
subgraph Database[PostgreSQL]
DB[(Users / Bookings<br/>Transactions)]
end
subgraph DAV[SabreDAV Server]
CardDAV[(vCard Contacts)]
CalDAV[(Calendar Events)]
end
subgraph External[External Services - TODO]
Gmail[Gmail SMTP]
SquareAPI[Square API]
S3[S3/R2 Storage]
end
subgraph External[External Services]
Gmail[Gmail SMTP<br/>smtp.gmail.com:587]
SquareAPI[Square API<br/>Online Payments]
end
%% Connections
User -->|HTTPS| CF
Admin -->|HTTPS| CF
CF --> Proxy
Proxy --> UI
Proxy --> AdminUI
Proxy --> APIProxy
UI --> APIProxy
AdminUI --> APIProxy
APIProxy --> Router
User -->|HTTPS| NGINX
Admin -->|HTTPS| NGINX
Static --> User
Proxy --> Router
Router --> Auth
Auth --> Handlers
Handlers --> DB
Handlers --> CardDAV
Handlers --> CalDAV
Handlers -->|Send Emails| Gmail
Handlers -->|Process Payments| SquareAPI
Admin -.->|Sync Contacts/Calendar| DAV
CardReader -->|Transaction Data| SquareAPI
Handlers -.->|Poll Transactions| SquareAPI
%% Force External Services to appear below
Lightsail --> External
Handlers --> DAV
Handlers -.->|TODO| Gmail
Handlers -.->|TODO| SquareAPI
Handlers -.->|TODO| S3
```
---
## 🔧 Backend Implementation
## API Reference
### JWT Auth
JWT uses **HS256** algorithm with 30day expiry. Implemented via `go-chi/jwtauth`.
### Public Endpoints
### Middleware
Validates JWT and attaches user context. Supports role restrictions.
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/services` | List active services |
| POST | `/api/register` | Create new user account |
| POST | `/api/login` | Authenticate and receive JWT |
| GET | `/api/scheduling/default-hours` | Get weekly default hours |
| GET | `/api/scheduling/exceptional-groups` | List holiday/special hour groups |
| GET | `/api/scheduling/working-hours` | Get merged working hours for date range |
| GET | `/api/scheduling/available-hours` | Get available booking slots |
### Registration Flow
The registration process:
- Validates names, email, phone, DOB
- Rejects users under 16 years old
- Hashes password with bcrypt
- Saves user profile and creates vCard in CardDAV
### Authenticated User Endpoints
```go
hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
tx, _ := db.DB.Begin(r.Context())
defer tx.Rollback(r.Context())
_, err := tx.Exec(r.Context(),
`INSERT INTO users (first_name, last_name, email, phone, dob, password_hash)
VALUES ($1,$2,$3,$4,$5,$6)`,
req.FirstName, req.LastName, req.Email, req.Phone, dob, string(hash))
```
### CardDAV Integration
Each registered user generates a `.vcf` file in SabreDAV, ensuring external calendar and contact apps stay in sync. The code now uses the internal `dav.BaseService.CreateContact` helper.
```go
func CreateCardDAVContact(service *dav.BaseService, addressBookID int, userID, firstName, lastName, email, phone, dob string) error {
input := dav.ContactInput{
UserID: userID,
FirstName: firstName,
LastName: lastName,
Email: email,
Phone: phone,
DOB: dob,
}
return service.CreateContact(addressBookID, userID, input)
}
```
The addressbook ID is currently hardcoded in the database (`dav_cards` table). A migration will expose the ID via a dedicated endpoint in the next sprint.
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/user/profile` | Get current user profile |
| PUT | `/api/user/profile` | Update profile |
| DELETE | `/api/user/account` | Delete account (GDPR) |
| GET | `/api/user/loyalty` | Get loyalty stamp count |
| GET | `/api/bookings` | List user's bookings |
| POST | `/api/bookings` | Create booking |
| GET | `/api/bookings/{id}` | Get specific booking |
| PUT | `/api/bookings/{id}` | Update booking |
| DELETE | `/api/bookings/{id}` | Cancel booking |
### Admin Endpoints
*Handlers for `/api/admin/services`, `/api/admin/bookings`, and `/api/admin/users` are defined in the router, but the concrete implementation files are still empty. These will be fleshed out in the next sprint.*
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/admin/services` | List all services |
| POST | `/api/admin/services` | Create service |
| DELETE | `/api/admin/services/{id}` | Delete service |
| PUT | `/api/admin/services/{id}/toggle` | Toggle active status |
| GET | `/api/admin/bookings` | List all bookings |
| POST | `/api/admin/bookings` | Create booking for user |
| GET | `/api/admin/bookings/search` | Search bookings |
| GET | `/api/admin/bookings/user/{user_id}` | User's bookings |
| PUT | `/api/admin/bookings/{id}/progress` | Progress status |
| POST | `/api/admin/bookings/{id}/confirm` | Confirm booking |
| POST | `/api/admin/bookings/{id}/cancel` | Cancel booking |
| GET | `/api/admin/users` | List users |
| GET | `/api/admin/users/{id}` | Get user details |
| GET | `/api/admin/today/current-next` | Current and next appointment |
| GET | `/api/admin/today/appointments` | Today's appointments |
| GET | `/api/admin/today/pending-approvals` | Pending approval queue |
| GET | `/api/admin/notifications` | List notifications |
| POST | `/api/admin/notifications/{id}/acknowledge` | Acknowledge |
| PUT | `/api/scheduling/default-hours` | Update weekly hours |
| POST | `/api/scheduling/exceptional-groups` | Create exception group |
| DELETE | `/api/scheduling/exceptional-groups` | Delete exception group |
| PUT | `/api/scheduling/exceptional-applications` | Apply exceptions to dates |
---
## 🎨 Frontend Implementation
## Database Schema
### API Proxy
Prevents CORS issues by proxying all API calls through SvelteKit.
### Enums
```ts
// routes/api/[...path]/+server.ts
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8080';
async function proxyRequest(request: Request, path: string) {
const url = `${BACKEND_URL}/api/${path}`;
const backendRes = await fetch(url, {
method: request.method,
headers: request.headers
});
// Forward everything transparently
return new Response(backendRes.body, {
status: backendRes.status,
headers: new Headers(backendRes.headers)
});
}
```sql
account_role: unverified_email | verified_email | admin | guest | affiliate
account_type: email | google | microsoft | facebook | guest
booking_status: pending | confirmed | in_progress | completed | client_cancelled | we_cancelled | re-schedule | no_show
payment_type: deposit | full | tip | balance | partial
payment_method: online_square | in_person_card | cash | giftcard | discount
payment_status: pending | completed | failed | refunded
admin_notification_reason: pending_booking | cancelled_booking | rescheduled_booking | 1_week_no_pay | 1_month_no_pay | affiliate_claim
```
### Booking Flow
**Step 1**: Select services
**Step 2**: Choose date/time
**Step 3**: Enter details
**Step 4**: Payment (TODO)
**Suggested additional `admin_notification_reason` values:**
**Calendar Component:**
| Reason | Purpose |
| --------------------- | ------------------------------------------------------------------------ |
| `payment_failed` | Payment processing failed |
| `patch_test_due` | Customer needs patch test before appointment |
| `first_time_customer` | New customer's first booking |
| `inactive_customer` | Regular hasn't booked in X months - 5% discount (non stacking) |
| `birthday_this_week` | Customer birthday - 5% discount (stacking) |
| `schedule_conflict` | Potential double-booking detected - admin alert, 'second customer' alert |
```svelte
<Calendar
type="single"
bind:value={selectedDate}
isDateUnavailable={(date) => bookedDates.some(d => d.compare(date) === 0)}
/>
```
### Core Tables
**Time Slot Generator:**
| Table | Purpose |
|-------|---------|
| `users` | User accounts with profile data |
| `user_social_logins` | Social auth provider links |
| `services` | Service offerings |
| `user_service_patch_tests` | Patch test tracking |
| `bookings` | Appointment records |
| `booking_services` | Services per booking |
| `user_referrals` | Referral tracking |
| `working_hours` | Default weekly schedule |
| `exceptional_working_hours_groups` | Holiday/special hour groups |
| `exceptional_working_hours` | Hours for exception groups |
| `exceptional_group_applications` | Apply exceptions to date ranges |
| `payments` | Payment transactions |
| `business_settings` | Business configuration |
| `admin_notifications` | Admin notification queue |
```ts
function generateTimeSlots(duration: number) {
const slots = []
for (let hour = 9; hour < 17; hour++) {
for (let minute = 0; minute < 60; minute += 15) {
slots.push(`${hour.toString().padStart(2,'0')}:${minute.toString().padStart(2,'0')}`);
}
}
return slots
}
```
### Key Functions
| Function | Purpose |
|----------|---------|
| `generate_short_id()` | Generate 12-char IDs |
| `anonymize_user()` | GDPR data removal |
| `delete_guest_user()` | Clean up guest accounts |
| `export_all_user_data()` | GDPR subject access |
| `get_monthly_business_summary()` | Analytics |
| `get_vat_return_data()` | VAT reporting |
| `calculate_vat()` | VAT calculation |
| `get_receipt_data()` | Receipt generation |
---
## 🎯 Next Steps
- **Booking Integration** wire the wizard to the backend `/api/bookings` POST endpoint.
- **Payments** integrate Stripe or Square SDK; add a payment screen.
- **Admin Dashboard** implement admin routes and UI.
- **Monitoring/Logging** add a lightweight Prometheus/Grafana stack or use CloudWatch.
- **CI/CD Pipeline** create GitHub Actions workflow for linting, testing, and container publishing.
- **Admin Handlers** flesh out `/api/admin/services`, `/api/admin/bookings`, and `/api/admin/users` endpoints.
- **Email/SMS Reminders** add scheduled job to send reminders.
- **Loyalty Tracking** add a frontend component to display loyalty stamps.
## Remaining Work
### High Priority
| Task | Description | Files Affected |
| ------------------------------ | ------------------------------------------------------------------------------ | -------------------------------------------------------- |
| **Customer booking submit** | `submitBooking()` at line 600 only logs, needs `POST /api/bookings` | `frontend/src/lib/components/booking/BookingFlow.svelte` |
| **Remove console.logs** | Debug logs left in: `BookingFlow.svelte:600`, `BookingCreateModal.svelte:224` | Frontend components |
| **Guest user endpoint** | Create `/api/users/guest` for walk-in bookings | `backend/handlers/user/` (new file) |
| **In-progress auto-infer** | Auto-set `in_progress` status based on time | Backend booking logic |
| **Begin button (Today)** | Manual start for early arrivals, gray out if >3hrs away | `CurrentAppointment.svelte` + backend |
| **One-off custom services** | Admin creates custom service for single booking without adding to main list | Backend + frontend booking modals |
| **One-off exceptional hours** | Single-day exceptions (dentist, afternoon off) - not yearly/weekly | Backend scheduling + frontend HolidayHours |
| **Auto lunch protection** | Block bookings that remove lunch break (1h customer, 30min admin with warning) | Backend `available-hours` logic |
| **Walk-in slot blocking** | Properly block next available slot during walk-in intake | `WalkInCreateModal.svelte` |
| **Square payment integration** | Full Square SDK integration | Backend payment handlers + frontend payment step |
| **GDPR data export** | User button for "give me my data" using `export_all_user_data()` | Backend endpoint + account page |
| **Tax data export** | Admin button for tax-software-compatible format | Backend endpoint + admin page |
### Medium Priority
| Task | Description |
|------|-------------|
| **Notifications UI** | Frontend panel to display admin notifications |
| **Notifications push** | Real-time notification mechanism (WebSocket/polling) |
| **User notifications** | Notification system for regular users (booking confirmations, reminders) |
| **Remove debug logs** | `console.log` in BookingFlow.svelte:600 and BookingCreateModal.svelte:224 |
| **S3/R2 image hosting** | Portfolio image storage with admin upload |
| **Refresh token endpoint** | Wire existing `RefreshTokenHandler` to router |
| **Loyalty display component** | Show stamps in account/bookings |
| **Email/SMS reminders** | Scheduled notification jobs |
| **Prometheus metrics** | Monitoring integration |
### Low Priority
| Task | Description |
| ----------------------- | ----------------------------------- |
| **Social auth** | Wire `handlers/auth/social.go` |
| **Analytics** | Wire `handlers/admin/analytics.go` |
| **Portfolio API** | Wire `handlers/portfolio/images.go` |
| **nginx config review** | Finalize production config |
| **CI/CD pipeline** | Gitea Actions workflow |
| And many more | |
---
## 📦 Environment & Runtime
## Environment Variables
| Variable | Purpose | Default / Example |
|----------|---------|-------------------|
| `JWT_SECRET_KEY` | Secret used to sign JWTs | **REQUIRED** set in `.env` or Docker secrets |
| `DATABASE_URL` | PostgreSQL connection string | `postgres://user:pass@localhost:5432/crussell?sslmode=disable` |
| `VITE_BACKEND_URL` | Frontend proxy target | `http://localhost:8080` (dev) |
| `VITE_SQUARE_ENV` | Square environment | `sandbox` or `production` |
| `VITE_STRIPE_KEY` | Stripe publishable key | `pk_test_...` |
> **Tip**: The `.env.example` file contains all required keys copy it to `.env` and edit.
| Variable | Purpose | Required |
|----------|---------|----------|
| `JWT_SECRET_KEY` | Secret for JWT signing | **Yes** |
| `DATABASE_URL` | PostgreSQL connection string | **Yes** |
| `POSTGRES_USER` | Database username | Docker |
| `POSTGRES_PASSWORD` | Database password | Docker |
| `POSTGRES_DB` | Database name | Docker |
---
## 🐳 Docker Compose
## Docker Compose
```yaml
# docker-compose.yml
services:
postgres:
image: postgres:17
volumes:
- pgdata:/var/lib/postgresql/data
- ./init-scripts/init-script.sql:/docker-entrypoint-initdb.d/init-script.sql:ro
backend:
build: ./backend
environment:
- JWT_SECRET_KEY=supersecret
- DATABASE_URL=postgres://crussell:crussell@db:5432/crussell
depends_on: [db, dav]
dav:
image: sabredav/sabredav
environment:
- DAV_URL=http://dav:8000
depends_on: [postgres]
sabredav:
image: php:8.2-fpm
volumes:
- dav-data:/data
db:
image: postgres:15
environment:
- POSTGRES_USER=crussell
- POSTGRES_PASSWORD=crussell
- POSTGRES_DB=crussell
- ./sabredav:/var/www/dav
depends_on: [postgres]
nginx:
image: nginx:alpine
depends_on: [backend, dav]
image: nginx:stable
ports: ["80:80", "443:443"]
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
frontend:
build: ./frontend
environment:
- VITE_BACKEND_URL=http://backend:8080
- ./nginx/conf.d:/etc/nginx/conf.d
- ./frontend/build:/usr/share/nginx/html
depends_on: [backend, sabredav]
```
> Run `docker compose up -d` to bring the stack up.
---
## Frontend Component Structure
```
src/lib/components/
├── admin/
│ ├── ApprovalModal.svelte
│ ├── BookingCreateModal.svelte
│ ├── BookingModal.svelte
│ ├── BookingsCard.svelte
│ ├── CallInBooking.svelte
│ ├── HolidayHours.svelte
│ ├── ImageUpload.svelte
│ ├── ServicesManagement.svelte
│ ├── UserModal.svelte
│ ├── UsersCard.svelte
│ ├── WalkInBooking.svelte
│ └── WalkInCreateModal.svelte
├── booking/
│ ├── BookingActions.svelte
│ ├── BookingFlow.svelte
│ ├── BookingSummary.svelte
│ ├── DatePicker.svelte
│ ├── ServiceCard.svelte
│ ├── ServiceSelector.svelte
│ ├── StepIndicator.svelte
│ └── TimeSlotPicker.svelte
└── today/
├── CurrentAppointment.svelte
├── PendingApprovals.svelte
└── TodayCalendar.svelte
```
---
## 📜 GDPR & Data Retention
## Notes
The database schema includes a `anonymize_user()` function (see `init-script.sql`) that removes PII after a users account is closed. The code also contains a `export_all_user_data()` helper to support subjectaccess requests.
- Holiday hours ARE integrated into all 3 booking flows (customer, call-in, walk-in) via `/api/scheduling/working-hours` and `/api/scheduling/available-hours`
- The backend merges default hours with applied exceptional hours automatically
- Static frontend is built and served by nginx; API calls go directly to Go backend in production
- Local dev uses SvelteKit's API proxy for CORS avoidance
- **GDPR functions exist in SQL** (`anonymize_user`, `export_all_user_data`, `delete_guest_user`) but only `DELETE /api/user/account` is wired - need user data export and admin tax export endpoints
- **Debug console.logs** in `BookingFlow.svelte:600` and `BookingCreateModal.svelte:224` should be removed before production
- **Notifications** are pull-based only (no push/WebSocket). Admin endpoint exists but no frontend UI. No user-facing notification system yet.
---
## Development Workflow
### Quick Start
```bash
./local-dev-2.sh # Creates tmux session 'crussell-dev'
```
Creates 3 panes:
- **Pane 0**: psql interactive shell
- **Pane 1**: Backend (`go run -tags dev ./main.go`)
- **Pane 2**: Frontend (`npm run dev -- --host`)
### Dev Build Tag
Backend uses `-tags dev` - check for dev-specific behavior in code with build constraints.
### Admin User Setup
No API endpoint exists for role promotion. Admin users are created via direct SQL:
```sql
UPDATE users SET account_role = 'admin' WHERE email = 'admin@example.com';
```
### Seed Data
Running `local-dev-2.sh` creates:
| Resource | Count | Details |
|----------|-------|---------|
| Users | 18 | 1 admin, 17 regular users |
| Services | 6 | Classic Manicure, Gel Manicure (BIAB), Luxury Pedicure, Express Mani & Pedi, Gel Removal, Nail Art Add-on |
| Bookings | 45 | 8 past, 3 today, 4 tomorrow, 30 future (spread over 15 days) |
| Confirmed | ~50% | Random selection of upcoming bookings auto-confirmed |
| Exceptional | 2 | November Break (closed), Christmas Holiday (reduced hours) |
### API JSON Examples
**Create Booking:**
```json
{
"start_time": "2025-01-15T10:00:00+00:00",
"service_ids": ["abc123def456"],
"notes": "Optional notes"
}
```
**Create Service:**
```json
{
"name": "Classic Manicure",
"description": "Nail shaping, cuticle care, hand massage, and polish.",
"price": 25.00,
"duration_minutes": 45,
"patch_test_duration_hours": 0,
"minimum_age_required": 0
}
```
**Confirm Booking:**
```json
POST /api/admin/bookings/{id}/confirm
Body: {"serviceOverrides": []}
```
**Create Exceptional Group:**
```json
{
"name": "Christmas Holiday Period",
"description": "Reduced hours for Christmas and New Year",
"hours": [
{"weekday": 0, "startTime": "00:00:00", "endTime": "00:00:00", "isOpen": false},
{"weekday": 1, "startTime": "10:00:00", "endTime": "15:00:00", "isOpen": true}
],
"weekStarts": ["2025-12-22", "2025-12-29"]
}
```
### Time Format
All timestamps use ISO 8601 with timezone: `YYYY-MM-DDTHH:MM:SS±HH:MM` (e.g., `2025-01-15T10:00:00+00:00`)
Timezone is always `Europe/London` (handles BST automatically).
---
## Code Patterns
### Transaction Pattern
Used throughout for atomic operations:
```go
tx, err := db.DB.Begin(r.Context())
if err != nil { /* handle error */ }
defer tx.Rollback(r.Context())
// Use tx instead of db.DB for queries
err = tx.QueryRow(r.Context(), `INSERT INTO...`)
if err := tx.Commit(r.Context()); err != nil { /* handle error */ }
```
### CardDAV Synchronization
- **On registration**: Creates vCard in SabreDAV via `dav.Service.CreateContact()`
- **On profile update**: Updates existing vCard via `updateCardDAV()` helper
- Uses internal HTTP calls to DAV server
### Role Change Detection
`RefreshTokenHandler` verifies user's current role hasn't changed since token was issued. If role changed, forces re-login with `401 Unauthorized`.
### Build Tags
- `db_dev.go` - Used with `-tags dev` for local development (localhost connection)
- `db.go` - Production build (uses env var for host)
- `internal/dav/service_dev.go` / `service_prod.go` - Same pattern for DAV service