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:
Vendored
+10
-11
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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, user‑by‑user, 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 30‑day 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 address‑book ID is currently hard‑coded 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` | Front‑end 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 user’s account is closed. The code also contains a `export_all_user_data()` helper to support subject‑access 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
|
||||
Reference in New Issue
Block a user