feat: add email verification, profile pictures, deposits, and calendar

export
Backend:
- Add email verification code generation and verification endpoints
- Add profile picture upload with S3 storage and image processing
- Add deposit_required field to users with 48h advance booking
  requirement
- Add loyalty stamps that accumulate on completed bookings
- Auto-transition bookings: confirmed → in_progress → completed
- Add booking cancellation handler with no-show detection
- Add ICS calendar file download endpoint for bookings
- Sync bookings to CalDAV on confirmation
  Frontend:
- Add schedule page route
- Add avatar and image-cropper UI components
- Update shadcn-svelte components (button, dialog)
- Add "Add to Calendar" button in booking modal
  Database:
- Add verification_codes table
- Add profile_pic_url, loyalty_stamps, deposits_required to users
- Various schema updates
This commit is contained in:
2026-02-21 18:48:29 +00:00
parent 88d8469180
commit 970cc5554d
48 changed files with 1984 additions and 175 deletions
+45 -14
View File
@@ -46,8 +46,8 @@
- [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)
- [x] **In-progress auto-infer** - Status auto-sets based on time (confirmed → in_progress → completed)
- [x] **Auto-complete** - Bookings auto-complete when duration elapses
#### Admin Endpoints
- [x] `/api/admin/services` - Create, delete, list, toggle
@@ -69,11 +69,24 @@
#### User Endpoints
- [x] `/api/user/profile` - GET, PUT
- [x] `/api/user/profile-picture` - POST upload profile picture (separate bucket)
- [x] `/api/user/account` - DELETE (GDPR compliant)
- [x] `/api/user/loyalty` - GET loyalty stamps
- [x] `/api/contact` - Public endpoint returning first admin's contact info (name, phone, email, profilePicUrl)
- [ ] **GDPR data export** - `export_all_user_data()` exists but not wired to endpoint
- [ ] **Tax data export** - Admin endpoint for tax-software-compatible format
#### Deposits System (Simplified)
- [x] `users.deposits_required` INT DEFAULT 3
- [x] 48h notice required when `deposits_required > 0`
- [x] Reduces by 1 when booking completes with payment
- [x] Increases by 3 on <12h cancellation (bad behavior)
- [ ] Frontend display of deposits_required
#### CalDAV Contact Sync
- [x] Profile photos synced to CardDAV contacts (PHOTO field in vCard)
- [x] Auto-updates when profile is changed
#### Not Yet Wired
- [ ] Social auth (`handlers/auth/social.go` exists, not imported)
- [ ] Analytics (`handlers/admin/analytics.go` exists, not imported)
@@ -91,13 +104,15 @@
#### Core Pages
- [x] Home (`/`)
- [x] Prices (`/prices`)
- [x] Contact (`/contact`)
- [x] Contact (`/contact`) - Dynamic, fetches from `/api/contact`
- [x] Book (`/book`) - Full wizard with service selection, date/time, customer details
- [x] Portfolio (`/portfolio`) - S3/R2 storage with tag filtering, category filters, pagination, ?img= featured image, admin upload
- [x] Today (`/today`) - Admin only, real-time schedule view
- [x] Account (`/account`)
- [x] Today (`/today`) - Admin only, real-time schedule view with auto-status transitions
- [x] Schedule (`/schedule`) - User's upcoming bookings with .ics export
- [x] Account (`/account`) - Profile management, profile picture upload with cropper
- [x] Login (`/login`)
- [x] Manage (`/manage`)
- [x] Manage (`/manage`)
#### Admin Dashboard (`/admin`)
- [x] Auth guard with role check
@@ -149,6 +164,7 @@
- [x] CardDAV sync for contacts (SabreDAV)
- [x] CalDAV ready
- [x] Profile pics bucket - separate bucket `crussell-profile-pics` for user profile pictures
- [ ] Email/SMS reminders - not yet implemented
- [ ] Square payment - placeholder only
- [x] S3/R2 image hosting - Rustfs for dev, Cloudflare R2 for prod via build tags
@@ -356,17 +372,21 @@ admin_notification_reason: pending_booking | cancelled_booking | rescheduled_boo
| 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 |
| **Remove console.logs** | Debug logs left in: `BookingFlow.svelte:600` | 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 |
| ~~In-progress auto-infer~~ | ~~Auto-set `in_progress` status based on time~~ DONE | Backend booking logic |
| ~~Auto-complete~~ | ~~Auto-complete bookings when duration elapses~~ DONE | Backend today handlers |
| ~~Profile picture upload~~ | ~~Upload with cropper to separate bucket, sync to CalDAV~~ DONE | Backend + Account page |
| ~~Contact page dynamic~~ | ~~Fetch from `/api/contact` using first admin~~ DONE | Backend + Contact page |
| ~~Simplified deposits~~ | ~~`deposits_required` INT on users, 48h check, reduce on payment~~ DONE | 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 |
| **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
@@ -402,6 +422,17 @@ admin_notification_reason: pending_booking | cancelled_booking | rescheduled_boo
| `POSTGRES_USER` | Database username | Docker |
| `POSTGRES_PASSWORD` | Database password | Docker |
| `POSTGRES_DB` | Database name | Docker |
| `S3_BUCKET` | Main image bucket (portfolio) | No (default: crussell) |
| `S3_PROFILE_PICS_BUCKET` | Profile pictures bucket | No (default: crussell-profile-pics) |
| `S3_ENDPOINT` | S3/Rustfs endpoint | Dev |
| `S3_PUBLIC_URL` | Public URL for S3 bucket | Dev |
| `S3_ACCESS_KEY` | S3 access key | Dev |
| `S3_SECRET_KEY` | S3 secret key | Dev |
| `R2_ENDPOINT` | Cloudflare R2 endpoint | Prod |
| `R2_BUCKET` | R2 bucket name | Prod |
| `R2_PUBLIC_URL` | R2 public URL | Prod |
| `R2_ACCESS_KEY` | R2 access key | Prod |
| `R2_SECRET_KEY` | R2 secret key | Prod |
---