Add scheduler improvements, API endpoints, and month calendar view

Backend:
- Add /api/customers/ endpoint (CustomerViewSet, CustomerSerializer)
- Add /api/services/ endpoint with Service model and migrations
- Add Resource.type field (STAFF, ROOM, EQUIPMENT) with migration
- Fix EventSerializer to return resource_id, customer_id, service_id
- Add date range filtering to EventViewSet (start_date, end_date params)
- Add create_demo_appointments management command
- Set default brand colors in business API

Frontend:
- Add calendar grid view for month mode in OwnerScheduler
- Fix sidebar navigation active link contrast (bg-white/10)
- Add default primaryColor/secondaryColor fallbacks in useBusiness
- Disable WebSocket (backend not implemented) to stop reconnect loop
- Fix Resource.type.toLowerCase() error by adding type to backend

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
poduck
2025-11-27 20:09:04 -05:00
parent 38c43d3f27
commit 373257469b
38 changed files with 977 additions and 2111 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 KiB

View File

@@ -1,343 +0,0 @@
# Page snapshot
```yaml
- generic [ref=e2]:
- generic [ref=e3]:
- navigation [ref=e4]:
- generic [ref=e6]:
- link "Smooth Schedule" [ref=e7] [cursor=pointer]:
- /url: "#/"
- img [ref=e8]
- generic [ref=e14]: Smooth Schedule
- generic [ref=e15]:
- link "Features" [ref=e16] [cursor=pointer]:
- /url: "#/features"
- link "Pricing" [ref=e17] [cursor=pointer]:
- /url: "#/pricing"
- link "About" [ref=e18] [cursor=pointer]:
- /url: "#/about"
- link "Contact" [ref=e19] [cursor=pointer]:
- /url: "#/contact"
- generic [ref=e20]:
- button "🇺🇸 English" [ref=e23]:
- img [ref=e24]
- generic [ref=e27]: 🇺🇸
- generic [ref=e28]: English
- img [ref=e29]
- button "Switch to dark mode" [ref=e31]:
- img [ref=e32]
- link "Login" [ref=e34] [cursor=pointer]:
- /url: "#/login"
- link "Get Started" [ref=e35] [cursor=pointer]:
- /url: "#/signup"
- main [ref=e36]:
- generic [ref=e37]:
- generic [ref=e42]:
- generic [ref=e43]:
- generic [ref=e44]:
- generic [ref=e47]: Get started today
- heading "Scheduling Made Simple" [level=1] [ref=e48]
- paragraph [ref=e49]: The all-in-one platform for managing appointments, resources, and customers. Start free, scale as you grow.
- generic [ref=e50]:
- link "Get Started Free" [ref=e51] [cursor=pointer]:
- /url: "#/signup"
- text: Get Started Free
- img [ref=e52]
- button "Watch Demo" [ref=e54]:
- img [ref=e55]
- text: Watch Demo
- generic [ref=e57]:
- generic [ref=e58]:
- img [ref=e59]
- generic [ref=e62]: No credit card required
- generic [ref=e64]:
- img [ref=e65]
- generic [ref=e68]: Get started today
- generic [ref=e69]:
- generic [ref=e71]:
- generic [ref=e78]: dashboard.smoothschedule.com
- generic [ref=e79]:
- generic [ref=e80]:
- generic [ref=e81]:
- generic [ref=e82]: Today
- generic [ref=e83]: "12"
- generic [ref=e84]:
- generic [ref=e85]: This Week
- generic [ref=e86]: "48"
- generic [ref=e87]:
- generic [ref=e88]: Revenue
- generic [ref=e89]: $2.4k
- generic [ref=e90]:
- generic [ref=e91]: Today's Schedule
- generic [ref=e92]:
- generic [ref=e95]:
- generic [ref=e96]: 9:00 AM
- generic [ref=e97]: Sarah J. - Haircut
- generic [ref=e100]:
- generic [ref=e101]: 10:30 AM
- generic [ref=e102]: Mike T. - Consultation
- generic [ref=e105]:
- generic [ref=e106]: 2:00 PM
- generic [ref=e107]: Emma W. - Color
- generic [ref=e109]:
- img [ref=e111]
- generic [ref=e114]:
- generic [ref=e115]: New Booking!
- generic [ref=e116]: Just now
- generic [ref=e117]:
- paragraph [ref=e118]: Trusted by 1,000+ businesses worldwide
- generic [ref=e119]:
- generic [ref=e120]: TechCorp
- generic [ref=e121]: Innovate
- generic [ref=e122]: StartupX
- generic [ref=e123]: GrowthCo
- generic [ref=e124]: ScaleUp
- generic [ref=e126]:
- generic [ref=e127]:
- heading "Everything You Need" [level=2] [ref=e128]
- paragraph [ref=e129]: Powerful features to run your service business
- generic [ref=e130]:
- generic [ref=e131]:
- img [ref=e133]
- heading "Smart Scheduling" [level=3] [ref=e135]
- paragraph [ref=e136]: Drag-and-drop calendar with real-time availability, automated reminders, and conflict detection.
- generic [ref=e137]:
- img [ref=e139]
- heading "Resource Management" [level=3] [ref=e144]
- paragraph [ref=e145]: Manage staff, rooms, and equipment. Set availability, skills, and booking rules.
- generic [ref=e146]:
- img [ref=e148]
- heading "Customer Portal" [level=3] [ref=e152]
- paragraph [ref=e153]: Self-service booking portal for customers. View history, manage appointments, and save payment methods.
- generic [ref=e154]:
- img [ref=e156]
- heading "Integrated Payments" [level=3] [ref=e158]
- paragraph [ref=e159]: Accept payments online with Stripe. Deposits, full payments, and automatic invoicing.
- generic [ref=e160]:
- img [ref=e162]
- heading "Multi-Location Support" [level=3] [ref=e166]
- paragraph [ref=e167]: Manage multiple locations or brands from a single dashboard with isolated data.
- generic [ref=e168]:
- img [ref=e170]
- heading "White-Label Ready" [level=3] [ref=e176]
- paragraph [ref=e177]: Custom domain, branding, and remove SmoothSchedule branding for a seamless experience.
- link "View All features" [ref=e179] [cursor=pointer]:
- /url: "#/features"
- text: View All features
- img [ref=e180]
- generic [ref=e183]:
- generic [ref=e184]:
- heading "Get Started in Minutes" [level=2] [ref=e185]
- paragraph [ref=e186]: Three simple steps to transform your scheduling
- generic [ref=e187]:
- generic [ref=e190]:
- generic [ref=e191]: "01"
- img [ref=e193]
- heading "Create Your Account" [level=3] [ref=e196]
- paragraph [ref=e197]: Sign up for free and set up your business profile in minutes.
- generic [ref=e200]:
- generic [ref=e201]: "02"
- img [ref=e203]
- heading "Add Your Services" [level=3] [ref=e206]
- paragraph [ref=e207]: Configure your services, pricing, and available resources.
- generic [ref=e209]:
- generic [ref=e210]: "03"
- img [ref=e212]
- heading "Start Booking" [level=3] [ref=e217]
- paragraph [ref=e218]: Share your booking link and let customers schedule instantly.
- generic [ref=e221]:
- generic [ref=e222]:
- img [ref=e224]
- generic [ref=e226]: 1M+
- generic [ref=e227]: Appointments Scheduled
- generic [ref=e228]:
- img [ref=e230]
- generic [ref=e234]: 5,000+
- generic [ref=e235]: Businesses
- generic [ref=e236]:
- img [ref=e238]
- generic [ref=e241]: 50+
- generic [ref=e242]: Countries
- generic [ref=e243]:
- img [ref=e245]
- generic [ref=e248]: 99.9%
- generic [ref=e249]: Uptime
- generic [ref=e251]:
- generic [ref=e252]:
- heading "Loved by Businesses Everywhere" [level=2] [ref=e253]
- paragraph [ref=e254]: See what our customers have to say
- generic [ref=e255]:
- generic [ref=e256]:
- generic [ref=e257]:
- img [ref=e258]
- img [ref=e260]
- img [ref=e262]
- img [ref=e264]
- img [ref=e266]
- blockquote [ref=e268]: "\"SmoothSchedule transformed how we manage appointments. Our no-show rate dropped by 40% with automated reminders.\""
- generic [ref=e269]:
- generic [ref=e271]: S
- generic [ref=e272]:
- generic [ref=e273]: Sarah Johnson
- generic [ref=e274]: Owner at Luxe Salon
- generic [ref=e275]:
- generic [ref=e276]:
- img [ref=e277]
- img [ref=e279]
- img [ref=e281]
- img [ref=e283]
- img [ref=e285]
- blockquote [ref=e287]: "\"The white-label feature is perfect for our multi-location business. Each location has its own branded booking experience.\""
- generic [ref=e288]:
- generic [ref=e290]: M
- generic [ref=e291]:
- generic [ref=e292]: Michael Chen
- generic [ref=e293]: CEO at FitLife Studios
- generic [ref=e294]:
- generic [ref=e295]:
- img [ref=e296]
- img [ref=e298]
- img [ref=e300]
- img [ref=e302]
- img [ref=e304]
- blockquote [ref=e306]: "\"Setup was incredibly easy. We were up and running in under an hour, and our clients love the self-service booking.\""
- generic [ref=e307]:
- generic [ref=e309]: E
- generic [ref=e310]:
- generic [ref=e311]: Emily Rodriguez
- generic [ref=e312]: Manager at Peak Performance Therapy
- generic [ref=e314]:
- generic [ref=e315]:
- heading "Simple, Transparent Pricing" [level=2] [ref=e316]
- paragraph [ref=e317]: Start free, upgrade as you grow. No hidden fees.
- generic [ref=e318]:
- generic [ref=e319]:
- heading "Free" [level=3] [ref=e320]
- paragraph [ref=e321]: Perfect for getting started
- generic [ref=e322]: $0/month
- link "Get Started" [ref=e323] [cursor=pointer]:
- /url: "#/signup"
- generic [ref=e324]:
- generic [ref=e325]: Most Popular
- heading "Professional" [level=3] [ref=e326]
- paragraph [ref=e327]: For growing businesses
- generic [ref=e328]: $29/month
- link "Get Started" [ref=e329] [cursor=pointer]:
- /url: "#/signup"
- generic [ref=e330]:
- heading "Business" [level=3] [ref=e331]
- paragraph [ref=e332]: For established teams
- generic [ref=e333]: $79/month
- link "Get Started" [ref=e334] [cursor=pointer]:
- /url: "#/signup"
- link "View full pricing details" [ref=e336] [cursor=pointer]:
- /url: "#/pricing"
- text: View full pricing details
- img [ref=e337]
- generic [ref=e343]:
- heading "Ready to get started?" [level=2] [ref=e344]
- paragraph [ref=e345]: Join thousands of businesses already using SmoothSchedule.
- generic [ref=e346]:
- link "Get Started Free" [ref=e347] [cursor=pointer]:
- /url: "#/signup"
- text: Get Started Free
- img [ref=e348]
- link "Talk to Sales" [ref=e350] [cursor=pointer]:
- /url: "#/contact"
- paragraph [ref=e351]: No credit card required
- contentinfo [ref=e352]:
- generic [ref=e353]:
- generic [ref=e354]:
- generic [ref=e355]:
- link "Smooth Schedule" [ref=e356] [cursor=pointer]:
- /url: "#/"
- img [ref=e357]
- generic [ref=e363]: Smooth Schedule
- paragraph [ref=e364]: The all-in-one scheduling platform for businesses of all sizes. Manage resources, staff, and bookings effortlessly.
- generic [ref=e365]:
- link "Twitter" [ref=e366] [cursor=pointer]:
- /url: https://twitter.com/smoothschedule
- img [ref=e367]
- link "LinkedIn" [ref=e369] [cursor=pointer]:
- /url: https://linkedin.com/company/smoothschedule
- img [ref=e370]
- link "GitHub" [ref=e374] [cursor=pointer]:
- /url: https://github.com/smoothschedule
- img [ref=e375]
- link "YouTube" [ref=e378] [cursor=pointer]:
- /url: https://youtube.com/@smoothschedule
- img [ref=e379]
- generic [ref=e382]:
- heading "Product" [level=3] [ref=e383]
- list [ref=e384]:
- listitem [ref=e385]:
- link "Features" [ref=e386] [cursor=pointer]:
- /url: "#/features"
- listitem [ref=e387]:
- link "Pricing" [ref=e388] [cursor=pointer]:
- /url: "#/pricing"
- listitem [ref=e389]:
- link "Get Started" [ref=e390] [cursor=pointer]:
- /url: "#/signup"
- generic [ref=e391]:
- heading "Company" [level=3] [ref=e392]
- list [ref=e393]:
- listitem [ref=e394]:
- link "About" [ref=e395] [cursor=pointer]:
- /url: "#/about"
- listitem [ref=e396]:
- link "Contact" [ref=e397] [cursor=pointer]:
- /url: "#/contact"
- generic [ref=e398]:
- heading "Legal" [level=3] [ref=e399]
- list [ref=e400]:
- listitem [ref=e401]:
- link "Privacy Policy" [ref=e402] [cursor=pointer]:
- /url: "#/privacy"
- listitem [ref=e403]:
- link "Terms of Service" [ref=e404] [cursor=pointer]:
- /url: "#/terms"
- paragraph [ref=e406]: © 2025 Smooth Schedule Inc. All rights reserved.
- generic [ref=e407]:
- generic [ref=e408]:
- heading "🔓 Quick Login (Dev Only)" [level=3] [ref=e409]:
- generic [ref=e410]: 🔓
- generic [ref=e411]: Quick Login (Dev Only)
- button "×" [ref=e412]
- generic [ref=e413]:
- button "Logging in..." [disabled] [ref=e414]:
- generic [ref=e415]:
- img [ref=e416]
- text: Logging in...
- button "Platform Manager PLATFORM_MANAGER" [disabled] [ref=e419]:
- generic [ref=e420]:
- generic [ref=e421]: Platform Manager
- generic [ref=e422]: PLATFORM_MANAGER
- button "Platform Sales PLATFORM_SALES" [disabled] [ref=e423]:
- generic [ref=e424]:
- generic [ref=e425]: Platform Sales
- generic [ref=e426]: PLATFORM_SALES
- button "Platform Support PLATFORM_SUPPORT" [disabled] [ref=e427]:
- generic [ref=e428]:
- generic [ref=e429]: Platform Support
- generic [ref=e430]: PLATFORM_SUPPORT
- button "Business Owner TENANT_OWNER" [disabled] [ref=e431]:
- generic [ref=e432]:
- generic [ref=e433]: Business Owner
- generic [ref=e434]: TENANT_OWNER
- button "Business Manager TENANT_MANAGER" [disabled] [ref=e435]:
- generic [ref=e436]:
- generic [ref=e437]: Business Manager
- generic [ref=e438]: TENANT_MANAGER
- button "Staff Member TENANT_STAFF" [disabled] [ref=e439]:
- generic [ref=e440]:
- generic [ref=e441]: Staff Member
- generic [ref=e442]: TENANT_STAFF
- button "Customer CUSTOMER" [disabled] [ref=e443]:
- generic [ref=e444]:
- generic [ref=e445]: Customer
- generic [ref=e446]: CUSTOMER
- generic [ref=e447]:
- text: "Password for all:"
- code [ref=e448]: test123
```

View File

@@ -1,343 +0,0 @@
# Page snapshot
```yaml
- generic [ref=e2]:
- generic [ref=e3]:
- navigation [ref=e4]:
- generic [ref=e6]:
- link "Smooth Schedule" [ref=e7]:
- /url: "#/"
- img [ref=e8]
- generic [ref=e14]: Smooth Schedule
- generic [ref=e15]:
- link "Features" [ref=e16]:
- /url: "#/features"
- link "Pricing" [ref=e17]:
- /url: "#/pricing"
- link "About" [ref=e18]:
- /url: "#/about"
- link "Contact" [ref=e19]:
- /url: "#/contact"
- generic [ref=e20]:
- button "🇺🇸 English" [ref=e23]:
- img [ref=e24]
- generic [ref=e27]: 🇺🇸
- generic [ref=e28]: English
- img [ref=e29]
- button "Switch to dark mode" [ref=e31]:
- img [ref=e32]
- link "Login" [ref=e34]:
- /url: "#/login"
- link "Get Started" [ref=e35]:
- /url: "#/signup"
- main [ref=e36]:
- generic [ref=e37]:
- generic [ref=e42]:
- generic [ref=e43]:
- generic [ref=e44]:
- generic [ref=e47]: Get started today
- heading "Scheduling Made Simple" [level=1] [ref=e48]
- paragraph [ref=e49]: The all-in-one platform for managing appointments, resources, and customers. Start free, scale as you grow.
- generic [ref=e50]:
- link "Get Started Free" [ref=e51]:
- /url: "#/signup"
- text: Get Started Free
- img [ref=e52]
- button "Watch Demo" [ref=e54]:
- img [ref=e55]
- text: Watch Demo
- generic [ref=e57]:
- generic [ref=e58]:
- img [ref=e59]
- generic [ref=e62]: No credit card required
- generic [ref=e64]:
- img [ref=e65]
- generic [ref=e68]: Get started today
- generic [ref=e69]:
- generic [ref=e71]:
- generic [ref=e78]: dashboard.smoothschedule.com
- generic [ref=e79]:
- generic [ref=e80]:
- generic [ref=e81]:
- generic [ref=e82]: Today
- generic [ref=e83]: "12"
- generic [ref=e84]:
- generic [ref=e85]: This Week
- generic [ref=e86]: "48"
- generic [ref=e87]:
- generic [ref=e88]: Revenue
- generic [ref=e89]: $2.4k
- generic [ref=e90]:
- generic [ref=e91]: Today's Schedule
- generic [ref=e92]:
- generic [ref=e95]:
- generic [ref=e96]: 9:00 AM
- generic [ref=e97]: Sarah J. - Haircut
- generic [ref=e100]:
- generic [ref=e101]: 10:30 AM
- generic [ref=e102]: Mike T. - Consultation
- generic [ref=e105]:
- generic [ref=e106]: 2:00 PM
- generic [ref=e107]: Emma W. - Color
- generic [ref=e109]:
- img [ref=e111]
- generic [ref=e114]:
- generic [ref=e115]: New Booking!
- generic [ref=e116]: Just now
- generic [ref=e117]:
- paragraph [ref=e118]: Trusted by 1,000+ businesses worldwide
- generic [ref=e119]:
- generic [ref=e120]: TechCorp
- generic [ref=e121]: Innovate
- generic [ref=e122]: StartupX
- generic [ref=e123]: GrowthCo
- generic [ref=e124]: ScaleUp
- generic [ref=e126]:
- generic [ref=e127]:
- heading "Everything You Need" [level=2] [ref=e128]
- paragraph [ref=e129]: Powerful features to run your service business
- generic [ref=e130]:
- generic [ref=e131]:
- img [ref=e133]
- heading "Smart Scheduling" [level=3] [ref=e135]
- paragraph [ref=e136]: Drag-and-drop calendar with real-time availability, automated reminders, and conflict detection.
- generic [ref=e137]:
- img [ref=e139]
- heading "Resource Management" [level=3] [ref=e144]
- paragraph [ref=e145]: Manage staff, rooms, and equipment. Set availability, skills, and booking rules.
- generic [ref=e146]:
- img [ref=e148]
- heading "Customer Portal" [level=3] [ref=e152]
- paragraph [ref=e153]: Self-service booking portal for customers. View history, manage appointments, and save payment methods.
- generic [ref=e154]:
- img [ref=e156]
- heading "Integrated Payments" [level=3] [ref=e158]
- paragraph [ref=e159]: Accept payments online with Stripe. Deposits, full payments, and automatic invoicing.
- generic [ref=e160]:
- img [ref=e162]
- heading "Multi-Location Support" [level=3] [ref=e166]
- paragraph [ref=e167]: Manage multiple locations or brands from a single dashboard with isolated data.
- generic [ref=e168]:
- img [ref=e170]
- heading "White-Label Ready" [level=3] [ref=e176]
- paragraph [ref=e177]: Custom domain, branding, and remove SmoothSchedule branding for a seamless experience.
- link "View All features" [ref=e179]:
- /url: "#/features"
- text: View All features
- img [ref=e180]
- generic [ref=e183]:
- generic [ref=e184]:
- heading "Get Started in Minutes" [level=2] [ref=e185]
- paragraph [ref=e186]: Three simple steps to transform your scheduling
- generic [ref=e187]:
- generic [ref=e190]:
- generic [ref=e191]: "01"
- img [ref=e193]
- heading "Create Your Account" [level=3] [ref=e196]
- paragraph [ref=e197]: Sign up for free and set up your business profile in minutes.
- generic [ref=e200]:
- generic [ref=e201]: "02"
- img [ref=e203]
- heading "Add Your Services" [level=3] [ref=e206]
- paragraph [ref=e207]: Configure your services, pricing, and available resources.
- generic [ref=e209]:
- generic [ref=e210]: "03"
- img [ref=e212]
- heading "Start Booking" [level=3] [ref=e217]
- paragraph [ref=e218]: Share your booking link and let customers schedule instantly.
- generic [ref=e221]:
- generic [ref=e222]:
- img [ref=e224]
- generic [ref=e226]: 1M+
- generic [ref=e227]: Appointments Scheduled
- generic [ref=e228]:
- img [ref=e230]
- generic [ref=e234]: 5,000+
- generic [ref=e235]: Businesses
- generic [ref=e236]:
- img [ref=e238]
- generic [ref=e241]: 50+
- generic [ref=e242]: Countries
- generic [ref=e243]:
- img [ref=e245]
- generic [ref=e248]: 99.9%
- generic [ref=e249]: Uptime
- generic [ref=e251]:
- generic [ref=e252]:
- heading "Loved by Businesses Everywhere" [level=2] [ref=e253]
- paragraph [ref=e254]: See what our customers have to say
- generic [ref=e255]:
- generic [ref=e256]:
- generic [ref=e257]:
- img [ref=e258]
- img [ref=e260]
- img [ref=e262]
- img [ref=e264]
- img [ref=e266]
- blockquote [ref=e268]: "\"SmoothSchedule transformed how we manage appointments. Our no-show rate dropped by 40% with automated reminders.\""
- generic [ref=e269]:
- generic [ref=e271]: S
- generic [ref=e272]:
- generic [ref=e273]: Sarah Johnson
- generic [ref=e274]: Owner at Luxe Salon
- generic [ref=e275]:
- generic [ref=e276]:
- img [ref=e277]
- img [ref=e279]
- img [ref=e281]
- img [ref=e283]
- img [ref=e285]
- blockquote [ref=e287]: "\"The white-label feature is perfect for our multi-location business. Each location has its own branded booking experience.\""
- generic [ref=e288]:
- generic [ref=e290]: M
- generic [ref=e291]:
- generic [ref=e292]: Michael Chen
- generic [ref=e293]: CEO at FitLife Studios
- generic [ref=e294]:
- generic [ref=e295]:
- img [ref=e296]
- img [ref=e298]
- img [ref=e300]
- img [ref=e302]
- img [ref=e304]
- blockquote [ref=e306]: "\"Setup was incredibly easy. We were up and running in under an hour, and our clients love the self-service booking.\""
- generic [ref=e307]:
- generic [ref=e309]: E
- generic [ref=e310]:
- generic [ref=e311]: Emily Rodriguez
- generic [ref=e312]: Manager at Peak Performance Therapy
- generic [ref=e314]:
- generic [ref=e315]:
- heading "Simple, Transparent Pricing" [level=2] [ref=e316]
- paragraph [ref=e317]: Start free, upgrade as you grow. No hidden fees.
- generic [ref=e318]:
- generic [ref=e319]:
- heading "Free" [level=3] [ref=e320]
- paragraph [ref=e321]: Perfect for getting started
- generic [ref=e322]: $0/month
- link "Get Started" [ref=e323]:
- /url: "#/signup"
- generic [ref=e324]:
- generic [ref=e325]: Most Popular
- heading "Professional" [level=3] [ref=e326]
- paragraph [ref=e327]: For growing businesses
- generic [ref=e328]: $29/month
- link "Get Started" [ref=e329]:
- /url: "#/signup"
- generic [ref=e330]:
- heading "Business" [level=3] [ref=e331]
- paragraph [ref=e332]: For established teams
- generic [ref=e333]: $79/month
- link "Get Started" [ref=e334]:
- /url: "#/signup"
- link "View full pricing details" [ref=e336]:
- /url: "#/pricing"
- text: View full pricing details
- img [ref=e337]
- generic [ref=e343]:
- heading "Ready to get started?" [level=2] [ref=e344]
- paragraph [ref=e345]: Join thousands of businesses already using SmoothSchedule.
- generic [ref=e346]:
- link "Get Started Free" [ref=e347]:
- /url: "#/signup"
- text: Get Started Free
- img [ref=e348]
- link "Talk to Sales" [ref=e350]:
- /url: "#/contact"
- paragraph [ref=e351]: No credit card required
- contentinfo [ref=e352]:
- generic [ref=e353]:
- generic [ref=e354]:
- generic [ref=e355]:
- link "Smooth Schedule" [ref=e356]:
- /url: "#/"
- img [ref=e357]
- generic [ref=e363]: Smooth Schedule
- paragraph [ref=e364]: The all-in-one scheduling platform for businesses of all sizes. Manage resources, staff, and bookings effortlessly.
- generic [ref=e365]:
- link "Twitter" [ref=e366]:
- /url: https://twitter.com/smoothschedule
- img [ref=e367]
- link "LinkedIn" [ref=e369]:
- /url: https://linkedin.com/company/smoothschedule
- img [ref=e370]
- link "GitHub" [ref=e374]:
- /url: https://github.com/smoothschedule
- img [ref=e375]
- link "YouTube" [ref=e378]:
- /url: https://youtube.com/@smoothschedule
- img [ref=e379]
- generic [ref=e382]:
- heading "Product" [level=3] [ref=e383]
- list [ref=e384]:
- listitem [ref=e385]:
- link "Features" [ref=e386]:
- /url: "#/features"
- listitem [ref=e387]:
- link "Pricing" [ref=e388]:
- /url: "#/pricing"
- listitem [ref=e389]:
- link "Get Started" [ref=e390]:
- /url: "#/signup"
- generic [ref=e391]:
- heading "Company" [level=3] [ref=e392]
- list [ref=e393]:
- listitem [ref=e394]:
- link "About" [ref=e395]:
- /url: "#/about"
- listitem [ref=e396]:
- link "Contact" [ref=e397]:
- /url: "#/contact"
- generic [ref=e398]:
- heading "Legal" [level=3] [ref=e399]
- list [ref=e400]:
- listitem [ref=e401]:
- link "Privacy Policy" [ref=e402]:
- /url: "#/privacy"
- listitem [ref=e403]:
- link "Terms of Service" [ref=e404]:
- /url: "#/terms"
- paragraph [ref=e406]: © 2025 Smooth Schedule Inc. All rights reserved.
- generic [ref=e407]:
- generic [ref=e408]:
- heading "🔓 Quick Login (Dev Only)" [level=3] [ref=e409]:
- generic [ref=e410]: 🔓
- generic [ref=e411]: Quick Login (Dev Only)
- button "×" [ref=e412]
- generic [ref=e413]:
- button "Logging in..." [disabled] [ref=e414]:
- generic [ref=e415]:
- img [ref=e416]
- text: Logging in...
- button "Platform Manager PLATFORM_MANAGER" [disabled] [ref=e419]:
- generic [ref=e420]:
- generic [ref=e421]: Platform Manager
- generic [ref=e422]: PLATFORM_MANAGER
- button "Platform Sales PLATFORM_SALES" [disabled] [ref=e423]:
- generic [ref=e424]:
- generic [ref=e425]: Platform Sales
- generic [ref=e426]: PLATFORM_SALES
- button "Platform Support PLATFORM_SUPPORT" [disabled] [ref=e427]:
- generic [ref=e428]:
- generic [ref=e429]: Platform Support
- generic [ref=e430]: PLATFORM_SUPPORT
- button "Business Owner TENANT_OWNER" [disabled] [ref=e431]:
- generic [ref=e432]:
- generic [ref=e433]: Business Owner
- generic [ref=e434]: TENANT_OWNER
- button "Business Manager TENANT_MANAGER" [disabled] [ref=e435]:
- generic [ref=e436]:
- generic [ref=e437]: Business Manager
- generic [ref=e438]: TENANT_MANAGER
- button "Staff Member TENANT_STAFF" [disabled] [ref=e439]:
- generic [ref=e440]:
- generic [ref=e441]: Staff Member
- generic [ref=e442]: TENANT_STAFF
- button "Customer CUSTOMER" [disabled] [ref=e443]:
- generic [ref=e444]:
- generic [ref=e445]: Customer
- generic [ref=e446]: CUSTOMER
- generic [ref=e447]:
- text: "Password for all:"
- code [ref=e448]: test123
```

View File

@@ -1,343 +0,0 @@
# Page snapshot
```yaml
- generic [ref=e2]:
- generic [ref=e3]:
- navigation [ref=e4]:
- generic [ref=e6]:
- link "Smooth Schedule" [ref=e7] [cursor=pointer]:
- /url: "#/"
- img [ref=e8]
- generic [ref=e14]: Smooth Schedule
- generic [ref=e15]:
- link "Features" [ref=e16] [cursor=pointer]:
- /url: "#/features"
- link "Pricing" [ref=e17] [cursor=pointer]:
- /url: "#/pricing"
- link "About" [ref=e18] [cursor=pointer]:
- /url: "#/about"
- link "Contact" [ref=e19] [cursor=pointer]:
- /url: "#/contact"
- generic [ref=e20]:
- button "🇺🇸 English" [ref=e23]:
- img [ref=e24]
- generic [ref=e28]: 🇺🇸
- generic [ref=e29]: English
- img [ref=e30]
- button "Switch to dark mode" [ref=e32]:
- img [ref=e33]
- link "Login" [ref=e35] [cursor=pointer]:
- /url: "#/login"
- link "Get Started" [ref=e36] [cursor=pointer]:
- /url: "#/signup"
- main [ref=e37]:
- generic [ref=e38]:
- generic [ref=e43]:
- generic [ref=e44]:
- generic [ref=e45]:
- generic [ref=e48]: Get started today
- heading "Scheduling Made Simple" [level=1] [ref=e49]
- paragraph [ref=e50]: The all-in-one platform for managing appointments, resources, and customers. Start free, scale as you grow.
- generic [ref=e51]:
- link "Get Started Free" [ref=e52] [cursor=pointer]:
- /url: "#/signup"
- text: Get Started Free
- img [ref=e53]
- button "Watch Demo" [ref=e56]:
- img [ref=e57]
- text: Watch Demo
- generic [ref=e59]:
- generic [ref=e60]:
- img [ref=e61]
- generic [ref=e64]: No credit card required
- generic [ref=e66]:
- img [ref=e67]
- generic [ref=e70]: Get started today
- generic [ref=e71]:
- generic [ref=e73]:
- generic [ref=e80]: dashboard.smoothschedule.com
- generic [ref=e81]:
- generic [ref=e82]:
- generic [ref=e83]:
- generic [ref=e84]: Today
- generic [ref=e85]: "12"
- generic [ref=e86]:
- generic [ref=e87]: This Week
- generic [ref=e88]: "48"
- generic [ref=e89]:
- generic [ref=e90]: Revenue
- generic [ref=e91]: $2.4k
- generic [ref=e92]:
- generic [ref=e93]: Today's Schedule
- generic [ref=e94]:
- generic [ref=e97]:
- generic [ref=e98]: 9:00 AM
- generic [ref=e99]: Sarah J. - Haircut
- generic [ref=e102]:
- generic [ref=e103]: 10:30 AM
- generic [ref=e104]: Mike T. - Consultation
- generic [ref=e107]:
- generic [ref=e108]: 2:00 PM
- generic [ref=e109]: Emma W. - Color
- generic [ref=e111]:
- img [ref=e113]
- generic [ref=e116]:
- generic [ref=e117]: New Booking!
- generic [ref=e118]: Just now
- generic [ref=e119]:
- paragraph [ref=e120]: Trusted by 1,000+ businesses worldwide
- generic [ref=e121]:
- generic [ref=e122]: TechCorp
- generic [ref=e123]: Innovate
- generic [ref=e124]: StartupX
- generic [ref=e125]: GrowthCo
- generic [ref=e126]: ScaleUp
- generic [ref=e128]:
- generic [ref=e129]:
- heading "Everything You Need" [level=2] [ref=e130]
- paragraph [ref=e131]: Powerful features to run your service business
- generic [ref=e132]:
- generic [ref=e133]:
- img [ref=e135]
- heading "Smart Scheduling" [level=3] [ref=e140]
- paragraph [ref=e141]: Drag-and-drop calendar with real-time availability, automated reminders, and conflict detection.
- generic [ref=e142]:
- img [ref=e144]
- heading "Resource Management" [level=3] [ref=e149]
- paragraph [ref=e150]: Manage staff, rooms, and equipment. Set availability, skills, and booking rules.
- generic [ref=e151]:
- img [ref=e153]
- heading "Customer Portal" [level=3] [ref=e157]
- paragraph [ref=e158]: Self-service booking portal for customers. View history, manage appointments, and save payment methods.
- generic [ref=e159]:
- img [ref=e161]
- heading "Integrated Payments" [level=3] [ref=e164]
- paragraph [ref=e165]: Accept payments online with Stripe. Deposits, full payments, and automatic invoicing.
- generic [ref=e166]:
- img [ref=e168]
- heading "Multi-Location Support" [level=3] [ref=e174]
- paragraph [ref=e175]: Manage multiple locations or brands from a single dashboard with isolated data.
- generic [ref=e176]:
- img [ref=e178]
- heading "White-Label Ready" [level=3] [ref=e184]
- paragraph [ref=e185]: Custom domain, branding, and remove SmoothSchedule branding for a seamless experience.
- link "View All features" [ref=e187] [cursor=pointer]:
- /url: "#/features"
- text: View All features
- img [ref=e188]
- generic [ref=e192]:
- generic [ref=e193]:
- heading "Get Started in Minutes" [level=2] [ref=e194]
- paragraph [ref=e195]: Three simple steps to transform your scheduling
- generic [ref=e196]:
- generic [ref=e199]:
- generic [ref=e200]: "01"
- img [ref=e202]
- heading "Create Your Account" [level=3] [ref=e207]
- paragraph [ref=e208]: Sign up for free and set up your business profile in minutes.
- generic [ref=e211]:
- generic [ref=e212]: "02"
- img [ref=e214]
- heading "Add Your Services" [level=3] [ref=e217]
- paragraph [ref=e218]: Configure your services, pricing, and available resources.
- generic [ref=e220]:
- generic [ref=e221]: "03"
- img [ref=e223]
- heading "Start Booking" [level=3] [ref=e228]
- paragraph [ref=e229]: Share your booking link and let customers schedule instantly.
- generic [ref=e232]:
- generic [ref=e233]:
- img [ref=e235]
- generic [ref=e240]: 1M+
- generic [ref=e241]: Appointments Scheduled
- generic [ref=e242]:
- img [ref=e244]
- generic [ref=e250]: 5,000+
- generic [ref=e251]: Businesses
- generic [ref=e252]:
- img [ref=e254]
- generic [ref=e258]: 50+
- generic [ref=e259]: Countries
- generic [ref=e260]:
- img [ref=e262]
- generic [ref=e265]: 99.9%
- generic [ref=e266]: Uptime
- generic [ref=e268]:
- generic [ref=e269]:
- heading "Loved by Businesses Everywhere" [level=2] [ref=e270]
- paragraph [ref=e271]: See what our customers have to say
- generic [ref=e272]:
- generic [ref=e273]:
- generic [ref=e274]:
- img [ref=e275]
- img [ref=e277]
- img [ref=e279]
- img [ref=e281]
- img [ref=e283]
- blockquote [ref=e285]: "\"SmoothSchedule transformed how we manage appointments. Our no-show rate dropped by 40% with automated reminders.\""
- generic [ref=e286]:
- generic [ref=e288]: S
- generic [ref=e289]:
- generic [ref=e290]: Sarah Johnson
- generic [ref=e291]: Owner at Luxe Salon
- generic [ref=e292]:
- generic [ref=e293]:
- img [ref=e294]
- img [ref=e296]
- img [ref=e298]
- img [ref=e300]
- img [ref=e302]
- blockquote [ref=e304]: "\"The white-label feature is perfect for our multi-location business. Each location has its own branded booking experience.\""
- generic [ref=e305]:
- generic [ref=e307]: M
- generic [ref=e308]:
- generic [ref=e309]: Michael Chen
- generic [ref=e310]: CEO at FitLife Studios
- generic [ref=e311]:
- generic [ref=e312]:
- img [ref=e313]
- img [ref=e315]
- img [ref=e317]
- img [ref=e319]
- img [ref=e321]
- blockquote [ref=e323]: "\"Setup was incredibly easy. We were up and running in under an hour, and our clients love the self-service booking.\""
- generic [ref=e324]:
- generic [ref=e326]: E
- generic [ref=e327]:
- generic [ref=e328]: Emily Rodriguez
- generic [ref=e329]: Manager at Peak Performance Therapy
- generic [ref=e331]:
- generic [ref=e332]:
- heading "Simple, Transparent Pricing" [level=2] [ref=e333]
- paragraph [ref=e334]: Start free, upgrade as you grow. No hidden fees.
- generic [ref=e335]:
- generic [ref=e336]:
- heading "Free" [level=3] [ref=e337]
- paragraph [ref=e338]: Perfect for getting started
- generic [ref=e339]: $0/month
- link "Get Started" [ref=e340] [cursor=pointer]:
- /url: "#/signup"
- generic [ref=e341]:
- generic [ref=e342]: Most Popular
- heading "Professional" [level=3] [ref=e343]
- paragraph [ref=e344]: For growing businesses
- generic [ref=e345]: $29/month
- link "Get Started" [ref=e346] [cursor=pointer]:
- /url: "#/signup"
- generic [ref=e347]:
- heading "Business" [level=3] [ref=e348]
- paragraph [ref=e349]: For established teams
- generic [ref=e350]: $79/month
- link "Get Started" [ref=e351] [cursor=pointer]:
- /url: "#/signup"
- link "View full pricing details" [ref=e353] [cursor=pointer]:
- /url: "#/pricing"
- text: View full pricing details
- img [ref=e354]
- generic [ref=e361]:
- heading "Ready to get started?" [level=2] [ref=e362]
- paragraph [ref=e363]: Join thousands of businesses already using SmoothSchedule.
- generic [ref=e364]:
- link "Get Started Free" [ref=e365] [cursor=pointer]:
- /url: "#/signup"
- text: Get Started Free
- img [ref=e366]
- link "Talk to Sales" [ref=e369] [cursor=pointer]:
- /url: "#/contact"
- paragraph [ref=e370]: No credit card required
- contentinfo [ref=e371]:
- generic [ref=e372]:
- generic [ref=e373]:
- generic [ref=e374]:
- link "Smooth Schedule" [ref=e375] [cursor=pointer]:
- /url: "#/"
- img [ref=e376]
- generic [ref=e382]: Smooth Schedule
- paragraph [ref=e383]: The all-in-one scheduling platform for businesses of all sizes. Manage resources, staff, and bookings effortlessly.
- generic [ref=e384]:
- link "Twitter" [ref=e385] [cursor=pointer]:
- /url: https://twitter.com/smoothschedule
- img [ref=e386]
- link "LinkedIn" [ref=e388] [cursor=pointer]:
- /url: https://linkedin.com/company/smoothschedule
- img [ref=e389]
- link "GitHub" [ref=e393] [cursor=pointer]:
- /url: https://github.com/smoothschedule
- img [ref=e394]
- link "YouTube" [ref=e397] [cursor=pointer]:
- /url: https://youtube.com/@smoothschedule
- img [ref=e398]
- generic [ref=e401]:
- heading "Product" [level=3] [ref=e402]
- list [ref=e403]:
- listitem [ref=e404]:
- link "Features" [ref=e405] [cursor=pointer]:
- /url: "#/features"
- listitem [ref=e406]:
- link "Pricing" [ref=e407] [cursor=pointer]:
- /url: "#/pricing"
- listitem [ref=e408]:
- link "Get Started" [ref=e409] [cursor=pointer]:
- /url: "#/signup"
- generic [ref=e410]:
- heading "Company" [level=3] [ref=e411]
- list [ref=e412]:
- listitem [ref=e413]:
- link "About" [ref=e414] [cursor=pointer]:
- /url: "#/about"
- listitem [ref=e415]:
- link "Contact" [ref=e416] [cursor=pointer]:
- /url: "#/contact"
- generic [ref=e417]:
- heading "Legal" [level=3] [ref=e418]
- list [ref=e419]:
- listitem [ref=e420]:
- link "Privacy Policy" [ref=e421] [cursor=pointer]:
- /url: "#/privacy"
- listitem [ref=e422]:
- link "Terms of Service" [ref=e423] [cursor=pointer]:
- /url: "#/terms"
- paragraph [ref=e425]: © 2025 Smooth Schedule Inc. All rights reserved.
- generic [ref=e426]:
- generic [ref=e427]:
- heading "🔓 Quick Login (Dev Only)" [level=3] [ref=e428]:
- generic [ref=e429]: 🔓
- generic [ref=e430]: Quick Login (Dev Only)
- button "×" [ref=e431]
- generic [ref=e432]:
- button "Logging in..." [disabled] [ref=e433]:
- generic [ref=e434]:
- img [ref=e435]
- text: Logging in...
- button "Platform Manager PLATFORM_MANAGER" [disabled] [ref=e438]:
- generic [ref=e439]:
- generic [ref=e440]: Platform Manager
- generic [ref=e441]: PLATFORM_MANAGER
- button "Platform Sales PLATFORM_SALES" [disabled] [ref=e442]:
- generic [ref=e443]:
- generic [ref=e444]: Platform Sales
- generic [ref=e445]: PLATFORM_SALES
- button "Platform Support PLATFORM_SUPPORT" [disabled] [ref=e446]:
- generic [ref=e447]:
- generic [ref=e448]: Platform Support
- generic [ref=e449]: PLATFORM_SUPPORT
- button "Business Owner TENANT_OWNER" [disabled] [ref=e450]:
- generic [ref=e451]:
- generic [ref=e452]: Business Owner
- generic [ref=e453]: TENANT_OWNER
- button "Business Manager TENANT_MANAGER" [disabled] [ref=e454]:
- generic [ref=e455]:
- generic [ref=e456]: Business Manager
- generic [ref=e457]: TENANT_MANAGER
- button "Staff Member TENANT_STAFF" [disabled] [ref=e458]:
- generic [ref=e459]:
- generic [ref=e460]: Staff Member
- generic [ref=e461]: TENANT_STAFF
- button "Customer CUSTOMER" [disabled] [ref=e462]:
- generic [ref=e463]:
- generic [ref=e464]: Customer
- generic [ref=e465]: CUSTOMER
- generic [ref=e466]:
- text: "Password for all:"
- code [ref=e467]: test123
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 410 KiB

File diff suppressed because one or more lines are too long

View File

@@ -96,11 +96,36 @@ export function DevQuickLogin({ embedded = false }: DevQuickLoginProps) {
// Store token in cookie (use 'access_token' to match what client.ts expects)
setCookie('access_token', response.data.token, 7);
// Invalidate queries to refetch user data
await queryClient.invalidateQueries({ queryKey: ['currentUser'] });
await queryClient.invalidateQueries({ queryKey: ['currentBusiness'] });
// Fetch user data to determine redirect
const userResponse = await apiClient.get('/api/auth/me/');
const userData = userResponse.data;
// Reload page to trigger auth flow
// Determine the correct subdomain based on user role
const currentHostname = window.location.hostname;
const currentPort = window.location.port;
let targetSubdomain: string | null = null;
// Platform users (superuser, platform_manager, platform_support)
if (['superuser', 'platform_manager', 'platform_support'].includes(userData.role)) {
targetSubdomain = 'platform';
}
// Business users - redirect to their business subdomain
else if (userData.business_subdomain) {
targetSubdomain = userData.business_subdomain;
}
// Check if we need to redirect to a different subdomain
const isOnTargetSubdomain = currentHostname === `${targetSubdomain}.lvh.me`;
const needsRedirect = targetSubdomain && !isOnTargetSubdomain;
if (needsRedirect) {
// Redirect to the correct subdomain
const portStr = currentPort ? `:${currentPort}` : '';
window.location.href = `http://${targetSubdomain}.lvh.me${portStr}/`;
return;
}
// Already on correct subdomain - just reload to update auth state
window.location.reload();
} catch (error: any) {
console.error('Quick login failed:', error);

View File

@@ -36,7 +36,7 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
const baseClasses = `flex items-center gap-3 py-3 text-sm font-medium rounded-lg transition-colors`;
const collapsedClasses = isCollapsed ? 'px-3 justify-center' : 'px-4';
const activeClasses = 'bg-opacity-10 text-white bg-white';
const activeClasses = 'bg-white/10 text-white';
const inactiveClasses = 'text-white/70 hover:text-white hover:bg-white/5';
const disabledClasses = 'text-white/30 cursor-not-allowed';

View File

@@ -35,6 +35,9 @@ interface UseAppointmentWebSocketOptions {
onError?: (error: Event) => void;
}
// WebSocket is not yet implemented in the backend - disable for now
const WEBSOCKET_ENABLED = false;
/**
* Transform backend appointment format to frontend format
*/
@@ -60,6 +63,9 @@ function transformAppointment(data: WebSocketMessage['appointment']): Appointmen
*/
export function useAppointmentWebSocket(options: UseAppointmentWebSocketOptions = {}) {
const { enabled = true, onConnected, onDisconnected, onError } = options;
// Early return if WebSocket is globally disabled
const effectivelyEnabled = enabled && WEBSOCKET_ENABLED;
const queryClient = useQueryClient();
const wsRef = useRef<WebSocket | null>(null);
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@@ -138,7 +144,7 @@ export function useAppointmentWebSocket(options: UseAppointmentWebSocketOptions
// Main effect to manage WebSocket connection
// Only depends on `enabled` - other values are read from refs or called as functions
useEffect(() => {
if (!enabled) {
if (!effectivelyEnabled) {
return;
}
@@ -285,7 +291,7 @@ export function useAppointmentWebSocket(options: UseAppointmentWebSocketOptions
setIsConnected(false);
};
}, [enabled]); // Only re-run when enabled changes
}, [effectivelyEnabled]); // Only re-run when enabled changes
const reconnect = useCallback(() => {
isCleaningUpRef.current = false;

View File

@@ -30,8 +30,8 @@ export const useCurrentBusiness = () => {
id: String(data.id),
name: data.name,
subdomain: data.subdomain,
primaryColor: data.primary_color,
secondaryColor: data.secondary_color,
primaryColor: data.primary_color || '#3B82F6', // Blue-500 default
secondaryColor: data.secondary_color || '#1E40AF', // Blue-800 default
logoUrl: data.logo_url,
whitelabelEnabled: data.whitelabel_enabled,
plan: data.tier, // Map tier to plan

View File

@@ -43,6 +43,7 @@ export const useCustomers = (filters?: CustomerFilters) => {
user_data: c.user_data, // Include user_data for masquerading
}));
},
retry: false, // Don't retry on 404 - endpoint may not exist yet
});
};

View File

@@ -24,6 +24,7 @@ export const useServices = () => {
description: s.description || '',
}));
},
retry: false, // Don't retry on 404 - endpoint may not exist yet
});
};
@@ -45,6 +46,7 @@ export const useService = (id: string) => {
};
},
enabled: !!id,
retry: false,
});
};

View File

@@ -224,6 +224,56 @@ const OwnerScheduler: React.FC<OwnerSchedulerProps> = ({ user, business }) => {
return new Date(date.getFullYear(), date.getMonth() + 1, 0);
};
// Generate calendar grid data for month view
const getMonthCalendarData = () => {
const firstDay = getStartOfMonth(viewDate);
const lastDay = getEndOfMonth(viewDate);
const startDayOfWeek = firstDay.getDay(); // 0 = Sunday
const daysInMonth = lastDay.getDate();
// Create array of week rows
const weeks: (Date | null)[][] = [];
let currentWeek: (Date | null)[] = [];
// Add empty cells for days before the first of the month
for (let i = 0; i < startDayOfWeek; i++) {
currentWeek.push(null);
}
// Add all days of the month
for (let day = 1; day <= daysInMonth; day++) {
currentWeek.push(new Date(viewDate.getFullYear(), viewDate.getMonth(), day));
if (currentWeek.length === 7) {
weeks.push(currentWeek);
currentWeek = [];
}
}
// Add empty cells for remaining days after the last of the month
if (currentWeek.length > 0) {
while (currentWeek.length < 7) {
currentWeek.push(null);
}
weeks.push(currentWeek);
}
return weeks;
};
// Get appointments for a specific day (for month view)
const getAppointmentsForDay = (date: Date) => {
const dayStart = new Date(date);
dayStart.setHours(0, 0, 0, 0);
const dayEnd = new Date(date);
dayEnd.setHours(23, 59, 59, 999);
return filteredAppointments.filter(apt => {
if (!apt.resourceId) return false; // Exclude pending
const aptDate = new Date(apt.startTime);
return aptDate >= dayStart && aptDate <= dayEnd;
}).sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime());
};
const navigateDate = (direction: 'prev' | 'next') => {
const newDate = new Date(viewDate);
@@ -685,11 +735,13 @@ const OwnerScheduler: React.FC<OwnerSchedulerProps> = ({ user, business }) => {
Month
</button>
</div>
{viewMode !== 'month' && (
<div className="flex items-center gap-2">
<button className="p-1.5 text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors" onClick={() => setZoomLevel(Math.max(0.5, zoomLevel - 0.25))}>-</button>
<span className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Zoom</span>
<button className="p-1.5 text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors" onClick={() => setZoomLevel(Math.min(2, zoomLevel + 0.25))}>+</button>
</div>
)}
<div className="flex items-center gap-1 border-l border-gray-300 dark:border-gray-600 pl-4">
<button
onClick={undo}
@@ -727,6 +779,108 @@ const OwnerScheduler: React.FC<OwnerSchedulerProps> = ({ user, business }) => {
</div>
</div>
{/* Month View - Calendar Grid */}
{viewMode === 'month' && (
<div className="flex flex-1 overflow-hidden">
{/* Pending Sidebar for Month View */}
<div className="flex flex-col bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 shrink-0 shadow-lg z-20 transition-colors duration-200" style={{ width: SIDEBAR_WIDTH }}>
<div className={`flex-1 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 p-4 flex flex-col transition-colors duration-200 ${draggedAppointmentId ? 'bg-blue-50/50 dark:bg-blue-900/20' : ''}`}>
<h3 className="text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3 flex items-center gap-2 shrink-0"><Clock size={12} /> Pending Requests ({pendingAppointments.length})</h3>
<div className="space-y-2 overflow-y-auto flex-1 mb-2">
{pendingAppointments.length === 0 && (<div className="text-xs text-gray-400 italic text-center py-4">No pending requests</div>)}
{pendingAppointments.map(apt => {
const service = services.find(s => s.id === apt.serviceId);
return (
<div
key={apt.id}
className="p-3 bg-white dark:bg-gray-700 border border-l-4 border-gray-200 dark:border-gray-600 border-l-orange-400 dark:border-l-orange-500 rounded shadow-sm cursor-pointer hover:shadow-md transition-all"
onClick={() => handleAppointmentClick(apt)}
>
<p className="font-semibold text-sm text-gray-900 dark:text-white">{apt.customerName}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{service?.name}</p>
<div className="mt-2 flex items-center gap-1 text-xs text-gray-400 dark:text-gray-500">
<Clock size={10} /> {formatDuration(apt.durationMinutes)}
</div>
</div>
);
})}
</div>
</div>
</div>
{/* Calendar Grid */}
<div className="flex-1 flex flex-col overflow-hidden bg-white dark:bg-gray-900 transition-colors duration-200">
<div className="flex-1 overflow-auto p-4">
{/* Day headers */}
<div className="grid grid-cols-7 gap-px bg-gray-200 dark:bg-gray-700 rounded-t-lg overflow-hidden">
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => (
<div key={day} className="bg-gray-50 dark:bg-gray-800 px-2 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{day}
</div>
))}
</div>
{/* Calendar weeks */}
<div className="grid grid-cols-7 gap-px bg-gray-200 dark:bg-gray-700 rounded-b-lg overflow-hidden">
{getMonthCalendarData().flat().map((date, index) => {
const isToday = date && new Date().toDateString() === date.toDateString();
const dayAppointments = date ? getAppointmentsForDay(date) : [];
const displayedAppointments = dayAppointments.slice(0, 3);
const remainingCount = dayAppointments.length - 3;
return (
<div
key={index}
className={`bg-white dark:bg-gray-900 min-h-[120px] p-2 transition-colors ${
date ? 'hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer' : 'bg-gray-50 dark:bg-gray-800/50'
}`}
onClick={() => { if (date) { setViewDate(date); setViewMode('day'); } }}
>
{date && (
<>
<div className={`text-sm font-medium mb-1 ${
isToday
? 'w-7 h-7 flex items-center justify-center rounded-full bg-brand-500 text-white'
: 'text-gray-700 dark:text-gray-300'
}`}>
{date.getDate()}
</div>
<div className="space-y-1">
{displayedAppointments.map(apt => {
const service = services.find(s => s.id === apt.serviceId);
const resource = resources.find(r => r.id === apt.resourceId);
const startTime = new Date(apt.startTime);
return (
<div
key={apt.id}
className="text-xs p-1.5 rounded bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200 truncate cursor-pointer hover:bg-blue-200 dark:hover:bg-blue-800/50 transition-colors"
onClick={(e) => { e.stopPropagation(); handleAppointmentClick(apt); }}
title={`${apt.customerName} - ${service?.name} with ${resource?.name}`}
>
<span className="font-medium">{startTime.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}</span>
{' '}{apt.customerName}
</div>
);
})}
{remainingCount > 0 && (
<div className="text-xs text-gray-500 dark:text-gray-400 font-medium pl-1">
+{remainingCount} more
</div>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
</div>
</div>
)}
{/* Day/Week View - Timeline */}
{viewMode !== 'month' && (
<div className="flex flex-1 overflow-hidden">
<div className="flex flex-col bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 shrink-0 shadow-lg z-20 transition-colors duration-200" style={{ width: SIDEBAR_WIDTH }}>
<div className="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 flex items-center px-4 font-semibold text-gray-500 dark:text-gray-400 text-xs uppercase tracking-wider shrink-0 transition-colors duration-200" style={{ height: HEADER_HEIGHT }}>Resources</div>
@@ -849,6 +1003,7 @@ const OwnerScheduler: React.FC<OwnerSchedulerProps> = ({ user, business }) => {
</div>
</div>
</div>
)}
{/* Appointment Detail/Edit Modal */}
{selectedAppointment && (

View File

@@ -24,20 +24,20 @@ const VerifyEmail: React.FC = () => {
setStatus('loading');
try {
const response = await apiClient.get(`/api/auth/emails/verify/${token}/`);
const response = await apiClient.post('/api/auth/email/verify/', { token });
// Immediately clear auth cookies to log out
deleteCookie('access_token');
deleteCookie('refresh_token');
if (response.data.message === 'Email is already verified') {
if (response.data.detail === 'Email already verified.') {
setStatus('already_verified');
} else {
setStatus('success');
}
} catch (err: any) {
setStatus('error');
setErrorMessage(err.response?.data?.detail || 'Failed to verify email');
setErrorMessage(err.response?.data?.error || 'Failed to verify email');
}
};

View File

@@ -1,8 +1,6 @@
{
"status": "failed",
"failedTests": [
"5f1889fdd7b10a4db9e9-2b1e81c51a733cc89956",
"5f1889fdd7b10a4db9e9-66724cc37c12aaf9dc66",
"5f1889fdd7b10a4db9e9-b93c630b7987c0eb4adc"
"7662eeffef95b745c0c7-05f7d22eaed6ca80a04d"
]
}

View File

@@ -1,343 +0,0 @@
# Page snapshot
```yaml
- generic [ref=e2]:
- generic [ref=e3]:
- navigation [ref=e4]:
- generic [ref=e6]:
- link "Smooth Schedule" [ref=e7] [cursor=pointer]:
- /url: "#/"
- img [ref=e8]
- generic [ref=e14]: Smooth Schedule
- generic [ref=e15]:
- link "Features" [ref=e16] [cursor=pointer]:
- /url: "#/features"
- link "Pricing" [ref=e17] [cursor=pointer]:
- /url: "#/pricing"
- link "About" [ref=e18] [cursor=pointer]:
- /url: "#/about"
- link "Contact" [ref=e19] [cursor=pointer]:
- /url: "#/contact"
- generic [ref=e20]:
- button "🇺🇸 English" [ref=e23]:
- img [ref=e24]
- generic [ref=e27]: 🇺🇸
- generic [ref=e28]: English
- img [ref=e29]
- button "Switch to dark mode" [ref=e31]:
- img [ref=e32]
- link "Login" [ref=e34] [cursor=pointer]:
- /url: "#/login"
- link "Get Started" [ref=e35] [cursor=pointer]:
- /url: "#/signup"
- main [ref=e36]:
- generic [ref=e37]:
- generic [ref=e42]:
- generic [ref=e43]:
- generic [ref=e44]:
- generic [ref=e47]: Get started today
- heading "Scheduling Made Simple" [level=1] [ref=e48]
- paragraph [ref=e49]: The all-in-one platform for managing appointments, resources, and customers. Start free, scale as you grow.
- generic [ref=e50]:
- link "Get Started Free" [ref=e51] [cursor=pointer]:
- /url: "#/signup"
- text: Get Started Free
- img [ref=e52]
- button "Watch Demo" [ref=e54]:
- img [ref=e55]
- text: Watch Demo
- generic [ref=e57]:
- generic [ref=e58]:
- img [ref=e59]
- generic [ref=e62]: No credit card required
- generic [ref=e64]:
- img [ref=e65]
- generic [ref=e68]: Get started today
- generic [ref=e69]:
- generic [ref=e71]:
- generic [ref=e78]: dashboard.smoothschedule.com
- generic [ref=e79]:
- generic [ref=e80]:
- generic [ref=e81]:
- generic [ref=e82]: Today
- generic [ref=e83]: "12"
- generic [ref=e84]:
- generic [ref=e85]: This Week
- generic [ref=e86]: "48"
- generic [ref=e87]:
- generic [ref=e88]: Revenue
- generic [ref=e89]: $2.4k
- generic [ref=e90]:
- generic [ref=e91]: Today's Schedule
- generic [ref=e92]:
- generic [ref=e95]:
- generic [ref=e96]: 9:00 AM
- generic [ref=e97]: Sarah J. - Haircut
- generic [ref=e100]:
- generic [ref=e101]: 10:30 AM
- generic [ref=e102]: Mike T. - Consultation
- generic [ref=e105]:
- generic [ref=e106]: 2:00 PM
- generic [ref=e107]: Emma W. - Color
- generic [ref=e109]:
- img [ref=e111]
- generic [ref=e114]:
- generic [ref=e115]: New Booking!
- generic [ref=e116]: Just now
- generic [ref=e117]:
- paragraph [ref=e118]: Trusted by 1,000+ businesses worldwide
- generic [ref=e119]:
- generic [ref=e120]: TechCorp
- generic [ref=e121]: Innovate
- generic [ref=e122]: StartupX
- generic [ref=e123]: GrowthCo
- generic [ref=e124]: ScaleUp
- generic [ref=e126]:
- generic [ref=e127]:
- heading "Everything You Need" [level=2] [ref=e128]
- paragraph [ref=e129]: Powerful features to run your service business
- generic [ref=e130]:
- generic [ref=e131]:
- img [ref=e133]
- heading "Smart Scheduling" [level=3] [ref=e135]
- paragraph [ref=e136]: Drag-and-drop calendar with real-time availability, automated reminders, and conflict detection.
- generic [ref=e137]:
- img [ref=e139]
- heading "Resource Management" [level=3] [ref=e144]
- paragraph [ref=e145]: Manage staff, rooms, and equipment. Set availability, skills, and booking rules.
- generic [ref=e146]:
- img [ref=e148]
- heading "Customer Portal" [level=3] [ref=e152]
- paragraph [ref=e153]: Self-service booking portal for customers. View history, manage appointments, and save payment methods.
- generic [ref=e154]:
- img [ref=e156]
- heading "Integrated Payments" [level=3] [ref=e158]
- paragraph [ref=e159]: Accept payments online with Stripe. Deposits, full payments, and automatic invoicing.
- generic [ref=e160]:
- img [ref=e162]
- heading "Multi-Location Support" [level=3] [ref=e166]
- paragraph [ref=e167]: Manage multiple locations or brands from a single dashboard with isolated data.
- generic [ref=e168]:
- img [ref=e170]
- heading "White-Label Ready" [level=3] [ref=e176]
- paragraph [ref=e177]: Custom domain, branding, and remove SmoothSchedule branding for a seamless experience.
- link "View All features" [ref=e179] [cursor=pointer]:
- /url: "#/features"
- text: View All features
- img [ref=e180]
- generic [ref=e183]:
- generic [ref=e184]:
- heading "Get Started in Minutes" [level=2] [ref=e185]
- paragraph [ref=e186]: Three simple steps to transform your scheduling
- generic [ref=e187]:
- generic [ref=e190]:
- generic [ref=e191]: "01"
- img [ref=e193]
- heading "Create Your Account" [level=3] [ref=e196]
- paragraph [ref=e197]: Sign up for free and set up your business profile in minutes.
- generic [ref=e200]:
- generic [ref=e201]: "02"
- img [ref=e203]
- heading "Add Your Services" [level=3] [ref=e206]
- paragraph [ref=e207]: Configure your services, pricing, and available resources.
- generic [ref=e209]:
- generic [ref=e210]: "03"
- img [ref=e212]
- heading "Start Booking" [level=3] [ref=e217]
- paragraph [ref=e218]: Share your booking link and let customers schedule instantly.
- generic [ref=e221]:
- generic [ref=e222]:
- img [ref=e224]
- generic [ref=e226]: 1M+
- generic [ref=e227]: Appointments Scheduled
- generic [ref=e228]:
- img [ref=e230]
- generic [ref=e234]: 5,000+
- generic [ref=e235]: Businesses
- generic [ref=e236]:
- img [ref=e238]
- generic [ref=e241]: 50+
- generic [ref=e242]: Countries
- generic [ref=e243]:
- img [ref=e245]
- generic [ref=e248]: 99.9%
- generic [ref=e249]: Uptime
- generic [ref=e251]:
- generic [ref=e252]:
- heading "Loved by Businesses Everywhere" [level=2] [ref=e253]
- paragraph [ref=e254]: See what our customers have to say
- generic [ref=e255]:
- generic [ref=e256]:
- generic [ref=e257]:
- img [ref=e258]
- img [ref=e260]
- img [ref=e262]
- img [ref=e264]
- img [ref=e266]
- blockquote [ref=e268]: "\"SmoothSchedule transformed how we manage appointments. Our no-show rate dropped by 40% with automated reminders.\""
- generic [ref=e269]:
- generic [ref=e271]: S
- generic [ref=e272]:
- generic [ref=e273]: Sarah Johnson
- generic [ref=e274]: Owner at Luxe Salon
- generic [ref=e275]:
- generic [ref=e276]:
- img [ref=e277]
- img [ref=e279]
- img [ref=e281]
- img [ref=e283]
- img [ref=e285]
- blockquote [ref=e287]: "\"The white-label feature is perfect for our multi-location business. Each location has its own branded booking experience.\""
- generic [ref=e288]:
- generic [ref=e290]: M
- generic [ref=e291]:
- generic [ref=e292]: Michael Chen
- generic [ref=e293]: CEO at FitLife Studios
- generic [ref=e294]:
- generic [ref=e295]:
- img [ref=e296]
- img [ref=e298]
- img [ref=e300]
- img [ref=e302]
- img [ref=e304]
- blockquote [ref=e306]: "\"Setup was incredibly easy. We were up and running in under an hour, and our clients love the self-service booking.\""
- generic [ref=e307]:
- generic [ref=e309]: E
- generic [ref=e310]:
- generic [ref=e311]: Emily Rodriguez
- generic [ref=e312]: Manager at Peak Performance Therapy
- generic [ref=e314]:
- generic [ref=e315]:
- heading "Simple, Transparent Pricing" [level=2] [ref=e316]
- paragraph [ref=e317]: Start free, upgrade as you grow. No hidden fees.
- generic [ref=e318]:
- generic [ref=e319]:
- heading "Free" [level=3] [ref=e320]
- paragraph [ref=e321]: Perfect for getting started
- generic [ref=e322]: $0/month
- link "Get Started" [ref=e323] [cursor=pointer]:
- /url: "#/signup"
- generic [ref=e324]:
- generic [ref=e325]: Most Popular
- heading "Professional" [level=3] [ref=e326]
- paragraph [ref=e327]: For growing businesses
- generic [ref=e328]: $29/month
- link "Get Started" [ref=e329] [cursor=pointer]:
- /url: "#/signup"
- generic [ref=e330]:
- heading "Business" [level=3] [ref=e331]
- paragraph [ref=e332]: For established teams
- generic [ref=e333]: $79/month
- link "Get Started" [ref=e334] [cursor=pointer]:
- /url: "#/signup"
- link "View full pricing details" [ref=e336] [cursor=pointer]:
- /url: "#/pricing"
- text: View full pricing details
- img [ref=e337]
- generic [ref=e343]:
- heading "Ready to get started?" [level=2] [ref=e344]
- paragraph [ref=e345]: Join thousands of businesses already using SmoothSchedule.
- generic [ref=e346]:
- link "Get Started Free" [ref=e347] [cursor=pointer]:
- /url: "#/signup"
- text: Get Started Free
- img [ref=e348]
- link "Talk to Sales" [ref=e350] [cursor=pointer]:
- /url: "#/contact"
- paragraph [ref=e351]: No credit card required
- contentinfo [ref=e352]:
- generic [ref=e353]:
- generic [ref=e354]:
- generic [ref=e355]:
- link "Smooth Schedule" [ref=e356] [cursor=pointer]:
- /url: "#/"
- img [ref=e357]
- generic [ref=e363]: Smooth Schedule
- paragraph [ref=e364]: The all-in-one scheduling platform for businesses of all sizes. Manage resources, staff, and bookings effortlessly.
- generic [ref=e365]:
- link "Twitter" [ref=e366] [cursor=pointer]:
- /url: https://twitter.com/smoothschedule
- img [ref=e367]
- link "LinkedIn" [ref=e369] [cursor=pointer]:
- /url: https://linkedin.com/company/smoothschedule
- img [ref=e370]
- link "GitHub" [ref=e374] [cursor=pointer]:
- /url: https://github.com/smoothschedule
- img [ref=e375]
- link "YouTube" [ref=e378] [cursor=pointer]:
- /url: https://youtube.com/@smoothschedule
- img [ref=e379]
- generic [ref=e382]:
- heading "Product" [level=3] [ref=e383]
- list [ref=e384]:
- listitem [ref=e385]:
- link "Features" [ref=e386] [cursor=pointer]:
- /url: "#/features"
- listitem [ref=e387]:
- link "Pricing" [ref=e388] [cursor=pointer]:
- /url: "#/pricing"
- listitem [ref=e389]:
- link "Get Started" [ref=e390] [cursor=pointer]:
- /url: "#/signup"
- generic [ref=e391]:
- heading "Company" [level=3] [ref=e392]
- list [ref=e393]:
- listitem [ref=e394]:
- link "About" [ref=e395] [cursor=pointer]:
- /url: "#/about"
- listitem [ref=e396]:
- link "Contact" [ref=e397] [cursor=pointer]:
- /url: "#/contact"
- generic [ref=e398]:
- heading "Legal" [level=3] [ref=e399]
- list [ref=e400]:
- listitem [ref=e401]:
- link "Privacy Policy" [ref=e402] [cursor=pointer]:
- /url: "#/privacy"
- listitem [ref=e403]:
- link "Terms of Service" [ref=e404] [cursor=pointer]:
- /url: "#/terms"
- paragraph [ref=e406]: © 2025 Smooth Schedule Inc. All rights reserved.
- generic [ref=e407]:
- generic [ref=e408]:
- heading "🔓 Quick Login (Dev Only)" [level=3] [ref=e409]:
- generic [ref=e410]: 🔓
- generic [ref=e411]: Quick Login (Dev Only)
- button "×" [ref=e412]
- generic [ref=e413]:
- button "Logging in..." [disabled] [ref=e414]:
- generic [ref=e415]:
- img [ref=e416]
- text: Logging in...
- button "Platform Manager PLATFORM_MANAGER" [disabled] [ref=e419]:
- generic [ref=e420]:
- generic [ref=e421]: Platform Manager
- generic [ref=e422]: PLATFORM_MANAGER
- button "Platform Sales PLATFORM_SALES" [disabled] [ref=e423]:
- generic [ref=e424]:
- generic [ref=e425]: Platform Sales
- generic [ref=e426]: PLATFORM_SALES
- button "Platform Support PLATFORM_SUPPORT" [disabled] [ref=e427]:
- generic [ref=e428]:
- generic [ref=e429]: Platform Support
- generic [ref=e430]: PLATFORM_SUPPORT
- button "Business Owner TENANT_OWNER" [disabled] [ref=e431]:
- generic [ref=e432]:
- generic [ref=e433]: Business Owner
- generic [ref=e434]: TENANT_OWNER
- button "Business Manager TENANT_MANAGER" [disabled] [ref=e435]:
- generic [ref=e436]:
- generic [ref=e437]: Business Manager
- generic [ref=e438]: TENANT_MANAGER
- button "Staff Member TENANT_STAFF" [disabled] [ref=e439]:
- generic [ref=e440]:
- generic [ref=e441]: Staff Member
- generic [ref=e442]: TENANT_STAFF
- button "Customer CUSTOMER" [disabled] [ref=e443]:
- generic [ref=e444]:
- generic [ref=e445]: Customer
- generic [ref=e446]: CUSTOMER
- generic [ref=e447]:
- text: "Password for all:"
- code [ref=e448]: test123
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 KiB

View File

@@ -1,343 +0,0 @@
# Page snapshot
```yaml
- generic [ref=e2]:
- generic [ref=e3]:
- navigation [ref=e4]:
- generic [ref=e6]:
- link "Smooth Schedule" [ref=e7] [cursor=pointer]:
- /url: "#/"
- img [ref=e8]
- generic [ref=e14]: Smooth Schedule
- generic [ref=e15]:
- link "Features" [ref=e16] [cursor=pointer]:
- /url: "#/features"
- link "Pricing" [ref=e17] [cursor=pointer]:
- /url: "#/pricing"
- link "About" [ref=e18] [cursor=pointer]:
- /url: "#/about"
- link "Contact" [ref=e19] [cursor=pointer]:
- /url: "#/contact"
- generic [ref=e20]:
- button "🇺🇸 English" [ref=e23]:
- img [ref=e24]
- generic [ref=e28]: 🇺🇸
- generic [ref=e29]: English
- img [ref=e30]
- button "Switch to dark mode" [ref=e32]:
- img [ref=e33]
- link "Login" [ref=e35] [cursor=pointer]:
- /url: "#/login"
- link "Get Started" [ref=e36] [cursor=pointer]:
- /url: "#/signup"
- main [ref=e37]:
- generic [ref=e38]:
- generic [ref=e43]:
- generic [ref=e44]:
- generic [ref=e45]:
- generic [ref=e48]: Get started today
- heading "Scheduling Made Simple" [level=1] [ref=e49]
- paragraph [ref=e50]: The all-in-one platform for managing appointments, resources, and customers. Start free, scale as you grow.
- generic [ref=e51]:
- link "Get Started Free" [ref=e52] [cursor=pointer]:
- /url: "#/signup"
- text: Get Started Free
- img [ref=e53]
- button "Watch Demo" [ref=e56]:
- img [ref=e57]
- text: Watch Demo
- generic [ref=e59]:
- generic [ref=e60]:
- img [ref=e61]
- generic [ref=e64]: No credit card required
- generic [ref=e66]:
- img [ref=e67]
- generic [ref=e70]: Get started today
- generic [ref=e71]:
- generic [ref=e73]:
- generic [ref=e80]: dashboard.smoothschedule.com
- generic [ref=e81]:
- generic [ref=e82]:
- generic [ref=e83]:
- generic [ref=e84]: Today
- generic [ref=e85]: "12"
- generic [ref=e86]:
- generic [ref=e87]: This Week
- generic [ref=e88]: "48"
- generic [ref=e89]:
- generic [ref=e90]: Revenue
- generic [ref=e91]: $2.4k
- generic [ref=e92]:
- generic [ref=e93]: Today's Schedule
- generic [ref=e94]:
- generic [ref=e97]:
- generic [ref=e98]: 9:00 AM
- generic [ref=e99]: Sarah J. - Haircut
- generic [ref=e102]:
- generic [ref=e103]: 10:30 AM
- generic [ref=e104]: Mike T. - Consultation
- generic [ref=e107]:
- generic [ref=e108]: 2:00 PM
- generic [ref=e109]: Emma W. - Color
- generic [ref=e111]:
- img [ref=e113]
- generic [ref=e116]:
- generic [ref=e117]: New Booking!
- generic [ref=e118]: Just now
- generic [ref=e119]:
- paragraph [ref=e120]: Trusted by 1,000+ businesses worldwide
- generic [ref=e121]:
- generic [ref=e122]: TechCorp
- generic [ref=e123]: Innovate
- generic [ref=e124]: StartupX
- generic [ref=e125]: GrowthCo
- generic [ref=e126]: ScaleUp
- generic [ref=e128]:
- generic [ref=e129]:
- heading "Everything You Need" [level=2] [ref=e130]
- paragraph [ref=e131]: Powerful features to run your service business
- generic [ref=e132]:
- generic [ref=e133]:
- img [ref=e135]
- heading "Smart Scheduling" [level=3] [ref=e140]
- paragraph [ref=e141]: Drag-and-drop calendar with real-time availability, automated reminders, and conflict detection.
- generic [ref=e142]:
- img [ref=e144]
- heading "Resource Management" [level=3] [ref=e149]
- paragraph [ref=e150]: Manage staff, rooms, and equipment. Set availability, skills, and booking rules.
- generic [ref=e151]:
- img [ref=e153]
- heading "Customer Portal" [level=3] [ref=e157]
- paragraph [ref=e158]: Self-service booking portal for customers. View history, manage appointments, and save payment methods.
- generic [ref=e159]:
- img [ref=e161]
- heading "Integrated Payments" [level=3] [ref=e164]
- paragraph [ref=e165]: Accept payments online with Stripe. Deposits, full payments, and automatic invoicing.
- generic [ref=e166]:
- img [ref=e168]
- heading "Multi-Location Support" [level=3] [ref=e174]
- paragraph [ref=e175]: Manage multiple locations or brands from a single dashboard with isolated data.
- generic [ref=e176]:
- img [ref=e178]
- heading "White-Label Ready" [level=3] [ref=e184]
- paragraph [ref=e185]: Custom domain, branding, and remove SmoothSchedule branding for a seamless experience.
- link "View All features" [ref=e187] [cursor=pointer]:
- /url: "#/features"
- text: View All features
- img [ref=e188]
- generic [ref=e192]:
- generic [ref=e193]:
- heading "Get Started in Minutes" [level=2] [ref=e194]
- paragraph [ref=e195]: Three simple steps to transform your scheduling
- generic [ref=e196]:
- generic [ref=e199]:
- generic [ref=e200]: "01"
- img [ref=e202]
- heading "Create Your Account" [level=3] [ref=e207]
- paragraph [ref=e208]: Sign up for free and set up your business profile in minutes.
- generic [ref=e211]:
- generic [ref=e212]: "02"
- img [ref=e214]
- heading "Add Your Services" [level=3] [ref=e217]
- paragraph [ref=e218]: Configure your services, pricing, and available resources.
- generic [ref=e220]:
- generic [ref=e221]: "03"
- img [ref=e223]
- heading "Start Booking" [level=3] [ref=e228]
- paragraph [ref=e229]: Share your booking link and let customers schedule instantly.
- generic [ref=e232]:
- generic [ref=e233]:
- img [ref=e235]
- generic [ref=e240]: 1M+
- generic [ref=e241]: Appointments Scheduled
- generic [ref=e242]:
- img [ref=e244]
- generic [ref=e250]: 5,000+
- generic [ref=e251]: Businesses
- generic [ref=e252]:
- img [ref=e254]
- generic [ref=e258]: 50+
- generic [ref=e259]: Countries
- generic [ref=e260]:
- img [ref=e262]
- generic [ref=e265]: 99.9%
- generic [ref=e266]: Uptime
- generic [ref=e268]:
- generic [ref=e269]:
- heading "Loved by Businesses Everywhere" [level=2] [ref=e270]
- paragraph [ref=e271]: See what our customers have to say
- generic [ref=e272]:
- generic [ref=e273]:
- generic [ref=e274]:
- img [ref=e275]
- img [ref=e277]
- img [ref=e279]
- img [ref=e281]
- img [ref=e283]
- blockquote [ref=e285]: "\"SmoothSchedule transformed how we manage appointments. Our no-show rate dropped by 40% with automated reminders.\""
- generic [ref=e286]:
- generic [ref=e288]: S
- generic [ref=e289]:
- generic [ref=e290]: Sarah Johnson
- generic [ref=e291]: Owner at Luxe Salon
- generic [ref=e292]:
- generic [ref=e293]:
- img [ref=e294]
- img [ref=e296]
- img [ref=e298]
- img [ref=e300]
- img [ref=e302]
- blockquote [ref=e304]: "\"The white-label feature is perfect for our multi-location business. Each location has its own branded booking experience.\""
- generic [ref=e305]:
- generic [ref=e307]: M
- generic [ref=e308]:
- generic [ref=e309]: Michael Chen
- generic [ref=e310]: CEO at FitLife Studios
- generic [ref=e311]:
- generic [ref=e312]:
- img [ref=e313]
- img [ref=e315]
- img [ref=e317]
- img [ref=e319]
- img [ref=e321]
- blockquote [ref=e323]: "\"Setup was incredibly easy. We were up and running in under an hour, and our clients love the self-service booking.\""
- generic [ref=e324]:
- generic [ref=e326]: E
- generic [ref=e327]:
- generic [ref=e328]: Emily Rodriguez
- generic [ref=e329]: Manager at Peak Performance Therapy
- generic [ref=e331]:
- generic [ref=e332]:
- heading "Simple, Transparent Pricing" [level=2] [ref=e333]
- paragraph [ref=e334]: Start free, upgrade as you grow. No hidden fees.
- generic [ref=e335]:
- generic [ref=e336]:
- heading "Free" [level=3] [ref=e337]
- paragraph [ref=e338]: Perfect for getting started
- generic [ref=e339]: $0/month
- link "Get Started" [ref=e340] [cursor=pointer]:
- /url: "#/signup"
- generic [ref=e341]:
- generic [ref=e342]: Most Popular
- heading "Professional" [level=3] [ref=e343]
- paragraph [ref=e344]: For growing businesses
- generic [ref=e345]: $29/month
- link "Get Started" [ref=e346] [cursor=pointer]:
- /url: "#/signup"
- generic [ref=e347]:
- heading "Business" [level=3] [ref=e348]
- paragraph [ref=e349]: For established teams
- generic [ref=e350]: $79/month
- link "Get Started" [ref=e351] [cursor=pointer]:
- /url: "#/signup"
- link "View full pricing details" [ref=e353] [cursor=pointer]:
- /url: "#/pricing"
- text: View full pricing details
- img [ref=e354]
- generic [ref=e361]:
- heading "Ready to get started?" [level=2] [ref=e362]
- paragraph [ref=e363]: Join thousands of businesses already using SmoothSchedule.
- generic [ref=e364]:
- link "Get Started Free" [ref=e365] [cursor=pointer]:
- /url: "#/signup"
- text: Get Started Free
- img [ref=e366]
- link "Talk to Sales" [ref=e369] [cursor=pointer]:
- /url: "#/contact"
- paragraph [ref=e370]: No credit card required
- contentinfo [ref=e371]:
- generic [ref=e372]:
- generic [ref=e373]:
- generic [ref=e374]:
- link "Smooth Schedule" [ref=e375] [cursor=pointer]:
- /url: "#/"
- img [ref=e376]
- generic [ref=e382]: Smooth Schedule
- paragraph [ref=e383]: The all-in-one scheduling platform for businesses of all sizes. Manage resources, staff, and bookings effortlessly.
- generic [ref=e384]:
- link "Twitter" [ref=e385] [cursor=pointer]:
- /url: https://twitter.com/smoothschedule
- img [ref=e386]
- link "LinkedIn" [ref=e388] [cursor=pointer]:
- /url: https://linkedin.com/company/smoothschedule
- img [ref=e389]
- link "GitHub" [ref=e393] [cursor=pointer]:
- /url: https://github.com/smoothschedule
- img [ref=e394]
- link "YouTube" [ref=e397] [cursor=pointer]:
- /url: https://youtube.com/@smoothschedule
- img [ref=e398]
- generic [ref=e401]:
- heading "Product" [level=3] [ref=e402]
- list [ref=e403]:
- listitem [ref=e404]:
- link "Features" [ref=e405] [cursor=pointer]:
- /url: "#/features"
- listitem [ref=e406]:
- link "Pricing" [ref=e407] [cursor=pointer]:
- /url: "#/pricing"
- listitem [ref=e408]:
- link "Get Started" [ref=e409] [cursor=pointer]:
- /url: "#/signup"
- generic [ref=e410]:
- heading "Company" [level=3] [ref=e411]
- list [ref=e412]:
- listitem [ref=e413]:
- link "About" [ref=e414] [cursor=pointer]:
- /url: "#/about"
- listitem [ref=e415]:
- link "Contact" [ref=e416] [cursor=pointer]:
- /url: "#/contact"
- generic [ref=e417]:
- heading "Legal" [level=3] [ref=e418]
- list [ref=e419]:
- listitem [ref=e420]:
- link "Privacy Policy" [ref=e421] [cursor=pointer]:
- /url: "#/privacy"
- listitem [ref=e422]:
- link "Terms of Service" [ref=e423] [cursor=pointer]:
- /url: "#/terms"
- paragraph [ref=e425]: © 2025 Smooth Schedule Inc. All rights reserved.
- generic [ref=e426]:
- generic [ref=e427]:
- heading "🔓 Quick Login (Dev Only)" [level=3] [ref=e428]:
- generic [ref=e429]: 🔓
- generic [ref=e430]: Quick Login (Dev Only)
- button "×" [ref=e431]
- generic [ref=e432]:
- button "Logging in..." [disabled] [ref=e433]:
- generic [ref=e434]:
- img [ref=e435]
- text: Logging in...
- button "Platform Manager PLATFORM_MANAGER" [disabled] [ref=e438]:
- generic [ref=e439]:
- generic [ref=e440]: Platform Manager
- generic [ref=e441]: PLATFORM_MANAGER
- button "Platform Sales PLATFORM_SALES" [disabled] [ref=e442]:
- generic [ref=e443]:
- generic [ref=e444]: Platform Sales
- generic [ref=e445]: PLATFORM_SALES
- button "Platform Support PLATFORM_SUPPORT" [disabled] [ref=e446]:
- generic [ref=e447]:
- generic [ref=e448]: Platform Support
- generic [ref=e449]: PLATFORM_SUPPORT
- button "Business Owner TENANT_OWNER" [disabled] [ref=e450]:
- generic [ref=e451]:
- generic [ref=e452]: Business Owner
- generic [ref=e453]: TENANT_OWNER
- button "Business Manager TENANT_MANAGER" [disabled] [ref=e454]:
- generic [ref=e455]:
- generic [ref=e456]: Business Manager
- generic [ref=e457]: TENANT_MANAGER
- button "Staff Member TENANT_STAFF" [disabled] [ref=e458]:
- generic [ref=e459]:
- generic [ref=e460]: Staff Member
- generic [ref=e461]: TENANT_STAFF
- button "Customer CUSTOMER" [disabled] [ref=e462]:
- generic [ref=e463]:
- generic [ref=e464]: Customer
- generic [ref=e465]: CUSTOMER
- generic [ref=e466]:
- text: "Password for all:"
- code [ref=e467]: test123
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

View File

@@ -1,343 +0,0 @@
# Page snapshot
```yaml
- generic [ref=e2]:
- generic [ref=e3]:
- navigation [ref=e4]:
- generic [ref=e6]:
- link "Smooth Schedule" [ref=e7]:
- /url: "#/"
- img [ref=e8]
- generic [ref=e14]: Smooth Schedule
- generic [ref=e15]:
- link "Features" [ref=e16]:
- /url: "#/features"
- link "Pricing" [ref=e17]:
- /url: "#/pricing"
- link "About" [ref=e18]:
- /url: "#/about"
- link "Contact" [ref=e19]:
- /url: "#/contact"
- generic [ref=e20]:
- button "🇺🇸 English" [ref=e23]:
- img [ref=e24]
- generic [ref=e27]: 🇺🇸
- generic [ref=e28]: English
- img [ref=e29]
- button "Switch to dark mode" [ref=e31]:
- img [ref=e32]
- link "Login" [ref=e34]:
- /url: "#/login"
- link "Get Started" [ref=e35]:
- /url: "#/signup"
- main [ref=e36]:
- generic [ref=e37]:
- generic [ref=e42]:
- generic [ref=e43]:
- generic [ref=e44]:
- generic [ref=e47]: Get started today
- heading "Scheduling Made Simple" [level=1] [ref=e48]
- paragraph [ref=e49]: The all-in-one platform for managing appointments, resources, and customers. Start free, scale as you grow.
- generic [ref=e50]:
- link "Get Started Free" [ref=e51]:
- /url: "#/signup"
- text: Get Started Free
- img [ref=e52]
- button "Watch Demo" [ref=e54]:
- img [ref=e55]
- text: Watch Demo
- generic [ref=e57]:
- generic [ref=e58]:
- img [ref=e59]
- generic [ref=e62]: No credit card required
- generic [ref=e64]:
- img [ref=e65]
- generic [ref=e68]: Get started today
- generic [ref=e69]:
- generic [ref=e71]:
- generic [ref=e78]: dashboard.smoothschedule.com
- generic [ref=e79]:
- generic [ref=e80]:
- generic [ref=e81]:
- generic [ref=e82]: Today
- generic [ref=e83]: "12"
- generic [ref=e84]:
- generic [ref=e85]: This Week
- generic [ref=e86]: "48"
- generic [ref=e87]:
- generic [ref=e88]: Revenue
- generic [ref=e89]: $2.4k
- generic [ref=e90]:
- generic [ref=e91]: Today's Schedule
- generic [ref=e92]:
- generic [ref=e95]:
- generic [ref=e96]: 9:00 AM
- generic [ref=e97]: Sarah J. - Haircut
- generic [ref=e100]:
- generic [ref=e101]: 10:30 AM
- generic [ref=e102]: Mike T. - Consultation
- generic [ref=e105]:
- generic [ref=e106]: 2:00 PM
- generic [ref=e107]: Emma W. - Color
- generic [ref=e109]:
- img [ref=e111]
- generic [ref=e114]:
- generic [ref=e115]: New Booking!
- generic [ref=e116]: Just now
- generic [ref=e117]:
- paragraph [ref=e118]: Trusted by 1,000+ businesses worldwide
- generic [ref=e119]:
- generic [ref=e120]: TechCorp
- generic [ref=e121]: Innovate
- generic [ref=e122]: StartupX
- generic [ref=e123]: GrowthCo
- generic [ref=e124]: ScaleUp
- generic [ref=e126]:
- generic [ref=e127]:
- heading "Everything You Need" [level=2] [ref=e128]
- paragraph [ref=e129]: Powerful features to run your service business
- generic [ref=e130]:
- generic [ref=e131]:
- img [ref=e133]
- heading "Smart Scheduling" [level=3] [ref=e135]
- paragraph [ref=e136]: Drag-and-drop calendar with real-time availability, automated reminders, and conflict detection.
- generic [ref=e137]:
- img [ref=e139]
- heading "Resource Management" [level=3] [ref=e144]
- paragraph [ref=e145]: Manage staff, rooms, and equipment. Set availability, skills, and booking rules.
- generic [ref=e146]:
- img [ref=e148]
- heading "Customer Portal" [level=3] [ref=e152]
- paragraph [ref=e153]: Self-service booking portal for customers. View history, manage appointments, and save payment methods.
- generic [ref=e154]:
- img [ref=e156]
- heading "Integrated Payments" [level=3] [ref=e158]
- paragraph [ref=e159]: Accept payments online with Stripe. Deposits, full payments, and automatic invoicing.
- generic [ref=e160]:
- img [ref=e162]
- heading "Multi-Location Support" [level=3] [ref=e166]
- paragraph [ref=e167]: Manage multiple locations or brands from a single dashboard with isolated data.
- generic [ref=e168]:
- img [ref=e170]
- heading "White-Label Ready" [level=3] [ref=e176]
- paragraph [ref=e177]: Custom domain, branding, and remove SmoothSchedule branding for a seamless experience.
- link "View All features" [ref=e179]:
- /url: "#/features"
- text: View All features
- img [ref=e180]
- generic [ref=e183]:
- generic [ref=e184]:
- heading "Get Started in Minutes" [level=2] [ref=e185]
- paragraph [ref=e186]: Three simple steps to transform your scheduling
- generic [ref=e187]:
- generic [ref=e190]:
- generic [ref=e191]: "01"
- img [ref=e193]
- heading "Create Your Account" [level=3] [ref=e196]
- paragraph [ref=e197]: Sign up for free and set up your business profile in minutes.
- generic [ref=e200]:
- generic [ref=e201]: "02"
- img [ref=e203]
- heading "Add Your Services" [level=3] [ref=e206]
- paragraph [ref=e207]: Configure your services, pricing, and available resources.
- generic [ref=e209]:
- generic [ref=e210]: "03"
- img [ref=e212]
- heading "Start Booking" [level=3] [ref=e217]
- paragraph [ref=e218]: Share your booking link and let customers schedule instantly.
- generic [ref=e221]:
- generic [ref=e222]:
- img [ref=e224]
- generic [ref=e226]: 1M+
- generic [ref=e227]: Appointments Scheduled
- generic [ref=e228]:
- img [ref=e230]
- generic [ref=e234]: 5,000+
- generic [ref=e235]: Businesses
- generic [ref=e236]:
- img [ref=e238]
- generic [ref=e241]: 50+
- generic [ref=e242]: Countries
- generic [ref=e243]:
- img [ref=e245]
- generic [ref=e248]: 99.9%
- generic [ref=e249]: Uptime
- generic [ref=e251]:
- generic [ref=e252]:
- heading "Loved by Businesses Everywhere" [level=2] [ref=e253]
- paragraph [ref=e254]: See what our customers have to say
- generic [ref=e255]:
- generic [ref=e256]:
- generic [ref=e257]:
- img [ref=e258]
- img [ref=e260]
- img [ref=e262]
- img [ref=e264]
- img [ref=e266]
- blockquote [ref=e268]: "\"SmoothSchedule transformed how we manage appointments. Our no-show rate dropped by 40% with automated reminders.\""
- generic [ref=e269]:
- generic [ref=e271]: S
- generic [ref=e272]:
- generic [ref=e273]: Sarah Johnson
- generic [ref=e274]: Owner at Luxe Salon
- generic [ref=e275]:
- generic [ref=e276]:
- img [ref=e277]
- img [ref=e279]
- img [ref=e281]
- img [ref=e283]
- img [ref=e285]
- blockquote [ref=e287]: "\"The white-label feature is perfect for our multi-location business. Each location has its own branded booking experience.\""
- generic [ref=e288]:
- generic [ref=e290]: M
- generic [ref=e291]:
- generic [ref=e292]: Michael Chen
- generic [ref=e293]: CEO at FitLife Studios
- generic [ref=e294]:
- generic [ref=e295]:
- img [ref=e296]
- img [ref=e298]
- img [ref=e300]
- img [ref=e302]
- img [ref=e304]
- blockquote [ref=e306]: "\"Setup was incredibly easy. We were up and running in under an hour, and our clients love the self-service booking.\""
- generic [ref=e307]:
- generic [ref=e309]: E
- generic [ref=e310]:
- generic [ref=e311]: Emily Rodriguez
- generic [ref=e312]: Manager at Peak Performance Therapy
- generic [ref=e314]:
- generic [ref=e315]:
- heading "Simple, Transparent Pricing" [level=2] [ref=e316]
- paragraph [ref=e317]: Start free, upgrade as you grow. No hidden fees.
- generic [ref=e318]:
- generic [ref=e319]:
- heading "Free" [level=3] [ref=e320]
- paragraph [ref=e321]: Perfect for getting started
- generic [ref=e322]: $0/month
- link "Get Started" [ref=e323]:
- /url: "#/signup"
- generic [ref=e324]:
- generic [ref=e325]: Most Popular
- heading "Professional" [level=3] [ref=e326]
- paragraph [ref=e327]: For growing businesses
- generic [ref=e328]: $29/month
- link "Get Started" [ref=e329]:
- /url: "#/signup"
- generic [ref=e330]:
- heading "Business" [level=3] [ref=e331]
- paragraph [ref=e332]: For established teams
- generic [ref=e333]: $79/month
- link "Get Started" [ref=e334]:
- /url: "#/signup"
- link "View full pricing details" [ref=e336]:
- /url: "#/pricing"
- text: View full pricing details
- img [ref=e337]
- generic [ref=e343]:
- heading "Ready to get started?" [level=2] [ref=e344]
- paragraph [ref=e345]: Join thousands of businesses already using SmoothSchedule.
- generic [ref=e346]:
- link "Get Started Free" [ref=e347]:
- /url: "#/signup"
- text: Get Started Free
- img [ref=e348]
- link "Talk to Sales" [ref=e350]:
- /url: "#/contact"
- paragraph [ref=e351]: No credit card required
- contentinfo [ref=e352]:
- generic [ref=e353]:
- generic [ref=e354]:
- generic [ref=e355]:
- link "Smooth Schedule" [ref=e356]:
- /url: "#/"
- img [ref=e357]
- generic [ref=e363]: Smooth Schedule
- paragraph [ref=e364]: The all-in-one scheduling platform for businesses of all sizes. Manage resources, staff, and bookings effortlessly.
- generic [ref=e365]:
- link "Twitter" [ref=e366]:
- /url: https://twitter.com/smoothschedule
- img [ref=e367]
- link "LinkedIn" [ref=e369]:
- /url: https://linkedin.com/company/smoothschedule
- img [ref=e370]
- link "GitHub" [ref=e374]:
- /url: https://github.com/smoothschedule
- img [ref=e375]
- link "YouTube" [ref=e378]:
- /url: https://youtube.com/@smoothschedule
- img [ref=e379]
- generic [ref=e382]:
- heading "Product" [level=3] [ref=e383]
- list [ref=e384]:
- listitem [ref=e385]:
- link "Features" [ref=e386]:
- /url: "#/features"
- listitem [ref=e387]:
- link "Pricing" [ref=e388]:
- /url: "#/pricing"
- listitem [ref=e389]:
- link "Get Started" [ref=e390]:
- /url: "#/signup"
- generic [ref=e391]:
- heading "Company" [level=3] [ref=e392]
- list [ref=e393]:
- listitem [ref=e394]:
- link "About" [ref=e395]:
- /url: "#/about"
- listitem [ref=e396]:
- link "Contact" [ref=e397]:
- /url: "#/contact"
- generic [ref=e398]:
- heading "Legal" [level=3] [ref=e399]
- list [ref=e400]:
- listitem [ref=e401]:
- link "Privacy Policy" [ref=e402]:
- /url: "#/privacy"
- listitem [ref=e403]:
- link "Terms of Service" [ref=e404]:
- /url: "#/terms"
- paragraph [ref=e406]: © 2025 Smooth Schedule Inc. All rights reserved.
- generic [ref=e407]:
- generic [ref=e408]:
- heading "🔓 Quick Login (Dev Only)" [level=3] [ref=e409]:
- generic [ref=e410]: 🔓
- generic [ref=e411]: Quick Login (Dev Only)
- button "×" [ref=e412]
- generic [ref=e413]:
- button "Logging in..." [disabled] [ref=e414]:
- generic [ref=e415]:
- img [ref=e416]
- text: Logging in...
- button "Platform Manager PLATFORM_MANAGER" [disabled] [ref=e419]:
- generic [ref=e420]:
- generic [ref=e421]: Platform Manager
- generic [ref=e422]: PLATFORM_MANAGER
- button "Platform Sales PLATFORM_SALES" [disabled] [ref=e423]:
- generic [ref=e424]:
- generic [ref=e425]: Platform Sales
- generic [ref=e426]: PLATFORM_SALES
- button "Platform Support PLATFORM_SUPPORT" [disabled] [ref=e427]:
- generic [ref=e428]:
- generic [ref=e429]: Platform Support
- generic [ref=e430]: PLATFORM_SUPPORT
- button "Business Owner TENANT_OWNER" [disabled] [ref=e431]:
- generic [ref=e432]:
- generic [ref=e433]: Business Owner
- generic [ref=e434]: TENANT_OWNER
- button "Business Manager TENANT_MANAGER" [disabled] [ref=e435]:
- generic [ref=e436]:
- generic [ref=e437]: Business Manager
- generic [ref=e438]: TENANT_MANAGER
- button "Staff Member TENANT_STAFF" [disabled] [ref=e439]:
- generic [ref=e440]:
- generic [ref=e441]: Staff Member
- generic [ref=e442]: TENANT_STAFF
- button "Customer CUSTOMER" [disabled] [ref=e443]:
- generic [ref=e444]:
- generic [ref=e445]: Customer
- generic [ref=e446]: CUSTOMER
- generic [ref=e447]:
- text: "Password for all:"
- code [ref=e448]: test123
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 410 KiB

View File

@@ -0,0 +1,97 @@
import { test, expect } from '@playwright/test';
test('business owner login flow', async ({ page }) => {
// Enable console logging
page.on('console', msg => {
const type = msg.type();
const text = msg.text();
// Only log errors, warnings, and our custom debug logs
if (type === 'error' || type === 'warning' || text.includes('Failed to')) {
console.log(`BROWSER ${type.toUpperCase()}:`, text);
}
});
// Enable error logging
page.on('pageerror', error => {
console.error('PAGE ERROR:', error.message);
console.error('STACK:', error.stack);
});
// Track network errors
page.on('requestfailed', request => {
console.error('REQUEST FAILED:', request.url(), request.failure()?.errorText);
});
// Go to the login page
console.log('Navigating to login page...');
await page.goto('http://lvh.me:5173/#/login');
// Wait for the page to load
await page.waitForLoadState('networkidle');
// Take screenshot of initial state
await page.screenshot({ path: 'test-results/01-initial-page.png', fullPage: true });
// Check if DevQuickLogin component loaded
const quickLoginVisible = await page.locator('text=Quick Login (Dev Only)').isVisible().catch(() => false);
console.log('Quick Login visible:', quickLoginVisible);
if (quickLoginVisible) {
// Click the Business Owner button
console.log('Clicking Business Owner button...');
await page.click('button:has-text("Business Owner")');
// Wait for navigation or changes
await page.waitForTimeout(2000);
// Take screenshot after clicking
await page.screenshot({ path: 'test-results/02-after-click.png', fullPage: true });
// Check the current URL
const currentUrl = page.url();
console.log('Current URL:', currentUrl);
// Check if #root has content
const rootContent = await page.locator('#root').innerHTML();
console.log('Root content length:', rootContent.length);
console.log('Root content:', rootContent.substring(0, 500));
// Check full page HTML if root is empty
if (rootContent.length === 0) {
const bodyContent = await page.locator('body').innerHTML();
console.log('\nFull body HTML (first 1000 chars):');
console.log(bodyContent.substring(0, 1000));
// Check for script tags
const scripts = await page.locator('script').count();
console.log('\nNumber of script tags:', scripts);
// Evaluate JavaScript in the page context to check for errors
const jsErrors = await page.evaluate(() => {
// Check if React root exists
const root = document.getElementById('root');
return {
rootExists: !!root,
rootHasChildren: root ? root.childNodes.length : 0,
documentReady: document.readyState,
};
});
console.log('\nJS Context:', JSON.stringify(jsErrors, null, 2));
}
// Check for React errors
const hasReactError = await page.locator('text=/error|failed/i').count();
console.log('\nError count on page:', hasReactError);
} else {
console.log('Quick Login component not found. Page content:');
const bodyText = await page.locator('body').textContent();
console.log(bodyText?.substring(0, 1000));
}
// Wait a bit more to see what happens
await page.waitForTimeout(3000);
// Final screenshot
await page.screenshot({ path: 'test-results/03-final-state.png', fullPage: true });
});

View File

@@ -10,7 +10,7 @@ from drf_spectacular.views import SpectacularAPIView
from drf_spectacular.views import SpectacularSwaggerView
from rest_framework.authtoken.views import obtain_auth_token
from smoothschedule.users.api_views import current_user_view, logout_view
from smoothschedule.users.api_views import current_user_view, logout_view, send_verification_email, verify_email
from schedule.api_views import current_business_view
urlpatterns = [
@@ -37,6 +37,8 @@ urlpatterns += [
path("api/auth-token/", csrf_exempt(obtain_auth_token), name="obtain_auth_token"),
path("api/auth/me/", current_user_view, name="current_user"),
path("api/auth/logout/", logout_view, name="logout"),
path("api/auth/email/verify/send/", send_verification_email, name="send_verification_email"),
path("api/auth/email/verify/", verify_email, name="verify_email"),
# Business API
path("api/business/current/", current_business_view, name="current_business"),
# API Docs

View File

@@ -40,8 +40,8 @@ def current_business_view(request):
'status': 'active' if tenant.is_active else 'inactive',
'created_at': tenant.created_on.isoformat() if tenant.created_on else None,
# Optional fields with defaults
'primary_color': None,
'secondary_color': None,
'primary_color': '#3B82F6', # Blue-500 default
'secondary_color': '#1E40AF', # Blue-800 default
'logo_url': None,
'whitelabel_enabled': False,
'resources_can_reschedule': False,

View File

@@ -0,0 +1,167 @@
"""
Management command to create demo appointments for the current month.
"""
import random
from datetime import datetime, timedelta
from decimal import Decimal
from django.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand
from django.utils import timezone
from schedule.models import Event, Resource, Service, Participant
from smoothschedule.users.models import User
class Command(BaseCommand):
help = 'Create demo appointments spanning the current month'
def add_arguments(self, parser):
parser.add_argument(
'--clear',
action='store_true',
help='Clear existing events before creating new ones',
)
parser.add_argument(
'--count',
type=int,
default=50,
help='Number of appointments to create (default: 50)',
)
def handle(self, *args, **options):
if options['clear']:
deleted_count = Event.objects.all().delete()[0]
self.stdout.write(f'Deleted {deleted_count} existing events')
# Ensure we have some resources
resources = list(Resource.objects.filter(is_active=True))
if not resources:
self.stdout.write('Creating demo resources...')
resources = [
Resource.objects.create(name='Sarah Johnson', type='STAFF', description='Senior Stylist'),
Resource.objects.create(name='Mike Chen', type='STAFF', description='Barber'),
Resource.objects.create(name='Room A', type='ROOM', description='Main treatment room'),
Resource.objects.create(name='Room B', type='ROOM', description='Private consultation'),
]
self.stdout.write(f'Created {len(resources)} resources')
# Ensure we have some services
services = list(Service.objects.filter(is_active=True))
if not services:
self.stdout.write('Creating demo services...')
services = [
Service.objects.create(name='Haircut', duration=30, price=Decimal('35.00')),
Service.objects.create(name='Hair Coloring', duration=90, price=Decimal('120.00')),
Service.objects.create(name='Beard Trim', duration=15, price=Decimal('15.00')),
Service.objects.create(name='Full Styling', duration=60, price=Decimal('75.00')),
Service.objects.create(name='Consultation', duration=30, price=Decimal('0.00')),
]
self.stdout.write(f'Created {len(services)} services')
# Ensure we have customer users
customers = list(User.objects.filter(role=User.Role.CUSTOMER))
if not customers:
self.stdout.write('Creating demo customers...')
customer_data = [
('alice', 'Alice Williams', 'alice@example.com'),
('bob', 'Bob Martinez', 'bob@example.com'),
('carol', 'Carol Davis', 'carol@example.com'),
('david', 'David Lee', 'david@example.com'),
('emma', 'Emma Thompson', 'emma@example.com'),
('frank', 'Frank Wilson', 'frank@example.com'),
('grace', 'Grace Kim', 'grace@example.com'),
('henry', 'Henry Brown', 'henry@example.com'),
('ivy', 'Ivy Chen', 'ivy@example.com'),
('jack', 'Jack Taylor', 'jack@example.com'),
]
for username, full_name, email in customer_data:
first_name, last_name = full_name.split(' ', 1)
user = User.objects.create_user(
username=username,
email=email,
password='test123',
first_name=first_name,
last_name=last_name,
role=User.Role.CUSTOMER,
)
customers.append(user)
self.stdout.write(f'Created {len(customer_data)} customer users')
statuses = ['SCHEDULED', 'SCHEDULED', 'SCHEDULED', 'CONFIRMED', 'CONFIRMED', 'COMPLETED']
# Get the current month range
now = timezone.now()
start_of_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
if now.month == 12:
end_of_month = start_of_month.replace(year=now.year + 1, month=1)
else:
end_of_month = start_of_month.replace(month=now.month + 1)
count = options['count']
created = 0
# Get content types for participants
resource_ct = ContentType.objects.get_for_model(Resource)
user_ct = ContentType.objects.get_for_model(User)
self.stdout.write(f'Creating {count} appointments for {start_of_month.strftime("%B %Y")}...')
for _ in range(count):
# Random day in the month
days_in_month = (end_of_month - start_of_month).days
random_day = random.randint(0, days_in_month - 1)
appointment_date = start_of_month + timedelta(days=random_day)
# Random time between 8am and 6pm
hour = random.randint(8, 17)
minute = random.choice([0, 15, 30, 45])
start_time = appointment_date.replace(hour=hour, minute=minute)
# Skip if in the past and marked as scheduled
status = random.choice(statuses)
if start_time < now and status == 'SCHEDULED':
status = 'COMPLETED'
# Random service and duration
service = random.choice(services)
duration = service.duration
# Random resource
resource = random.choice(resources)
# Random customer
customer = random.choice(customers)
# Create the event
end_time = start_time + timedelta(minutes=duration)
event = Event.objects.create(
title=f'{customer.full_name} - {service.name}',
start_time=start_time,
end_time=end_time,
status=status,
notes=f'Service: {service.name}',
)
# Create participant for the resource
Participant.objects.create(
event=event,
role=Participant.Role.RESOURCE,
content_type=resource_ct,
object_id=resource.id,
)
# Create participant for the customer
Participant.objects.create(
event=event,
role=Participant.Role.CUSTOMER,
content_type=user_ct,
object_id=customer.id,
)
created += 1
self.stdout.write(
self.style.SUCCESS(f'Successfully created {created} demo appointments with resource and customer links')
)

View File

@@ -0,0 +1,34 @@
# Generated manually
from decimal import Decimal
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('schedule', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Service',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('description', models.TextField(blank=True)),
('duration', models.PositiveIntegerField(default=60, help_text='Duration in minutes')),
('price', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10)),
('is_active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'ordering': ['name'],
},
),
migrations.AddIndex(
model_name='service',
index=models.Index(fields=['is_active', 'name'], name='schedule_se_is_acti_idx'),
),
]

View File

@@ -0,0 +1,22 @@
# Generated manually
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('schedule', '0002_add_service_model'),
]
operations = [
migrations.AddField(
model_name='resource',
name='type',
field=models.CharField(
choices=[('STAFF', 'Staff Member'), ('ROOM', 'Room'), ('EQUIPMENT', 'Equipment')],
default='STAFF',
max_length=20
),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.8 on 2025-11-28 01:04
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('schedule', '0003_add_resource_type'),
]
operations = [
migrations.RenameIndex(
model_name='service',
new_name='schedule_se_is_acti_8c055e_idx',
old_name='schedule_se_is_acti_idx',
),
]

View File

@@ -3,6 +3,34 @@ from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.validators import MinValueValidator
from django.utils import timezone
from decimal import Decimal
class Service(models.Model):
"""
A service offered by the business (e.g., Haircut, Massage, Consultation).
"""
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
duration = models.PositiveIntegerField(
help_text="Duration in minutes",
default=60
)
price = models.DecimalField(
max_digits=10,
decimal_places=2,
default=Decimal('0.00')
)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['name']
indexes = [models.Index(fields=['is_active', 'name'])]
def __str__(self):
return f"{self.name} ({self.duration} min - ${self.price})"
class Resource(models.Model):
@@ -14,7 +42,17 @@ class Resource(models.Model):
- max_concurrent_events > 1: Limited overlap (Waiting Room with N seats)
- max_concurrent_events = 0: Infinite capacity (Virtual Resource/Category)
"""
class Type(models.TextChoices):
STAFF = 'STAFF', 'Staff Member'
ROOM = 'ROOM', 'Room'
EQUIPMENT = 'EQUIPMENT', 'Equipment'
name = models.CharField(max_length=200)
type = models.CharField(
max_length=20,
choices=Type.choices,
default=Type.STAFF
)
description = models.TextField(blank=True)
max_concurrent_events = models.PositiveIntegerField(
default=1,

View File

@@ -3,8 +3,74 @@ DRF Serializers for Schedule App with Availability Validation
"""
from rest_framework import serializers
from django.contrib.contenttypes.models import ContentType
from .models import Resource, Event, Participant
from .models import Resource, Event, Participant, Service
from .services import AvailabilityService
from smoothschedule.users.models import User
class CustomerSerializer(serializers.ModelSerializer):
"""Serializer for Customer (User with role=CUSTOMER)"""
name = serializers.SerializerMethodField()
total_spend = serializers.SerializerMethodField()
last_visit = serializers.SerializerMethodField()
status = serializers.SerializerMethodField()
avatar_url = serializers.SerializerMethodField()
tags = serializers.SerializerMethodField()
user_id = serializers.IntegerField(source='id', read_only=True)
city = serializers.SerializerMethodField()
state = serializers.SerializerMethodField()
zip = serializers.SerializerMethodField()
class Meta:
model = User
fields = [
'id', 'name', 'email', 'phone', 'city', 'state', 'zip',
'total_spend', 'last_visit', 'status', 'avatar_url', 'tags',
'user_id',
]
read_only_fields = ['id', 'email']
def get_name(self, obj):
return obj.full_name
def get_total_spend(self, obj):
# TODO: Calculate from payments when implemented
return 0
def get_last_visit(self, obj):
# TODO: Get from last appointment when implemented
return None
def get_status(self, obj):
return 'Active' if obj.is_active else 'Inactive'
def get_avatar_url(self, obj):
return None # TODO: Implement avatar
def get_tags(self, obj):
return [] # TODO: Implement customer tags
def get_city(self, obj):
return '' # TODO: Add address fields to User model
def get_state(self, obj):
return ''
def get_zip(self, obj):
return ''
class ServiceSerializer(serializers.ModelSerializer):
"""Serializer for Service model"""
duration_minutes = serializers.IntegerField(source='duration', read_only=True)
class Meta:
model = Service
fields = [
'id', 'name', 'description', 'duration', 'duration_minutes',
'price', 'is_active', 'created_at', 'updated_at',
]
read_only_fields = ['created_at', 'updated_at']
class ResourceSerializer(serializers.ModelSerializer):
@@ -14,7 +80,7 @@ class ResourceSerializer(serializers.ModelSerializer):
class Meta:
model = Resource
fields = [
'id', 'name', 'description', 'max_concurrent_events',
'id', 'name', 'type', 'description', 'max_concurrent_events',
'buffer_duration', 'is_active', 'capacity_description',
'created_at', 'updated_at',
]
@@ -58,9 +124,9 @@ class EventSerializer(serializers.ModelSerializer):
duration_minutes = serializers.SerializerMethodField()
# Simplified fields for frontend compatibility
resource = serializers.SerializerMethodField()
customer = serializers.SerializerMethodField()
service = serializers.SerializerMethodField()
resource_id = serializers.SerializerMethodField()
customer_id = serializers.SerializerMethodField()
service_id = serializers.SerializerMethodField()
customer_name = serializers.SerializerMethodField()
service_name = serializers.SerializerMethodField()
is_paid = serializers.SerializerMethodField()
@@ -84,7 +150,7 @@ class EventSerializer(serializers.ModelSerializer):
fields = [
'id', 'title', 'start_time', 'end_time', 'status', 'notes',
'duration_minutes', 'participants', 'resource_ids', 'staff_ids',
'resource', 'customer', 'service', 'customer_name', 'service_name', 'is_paid',
'resource_id', 'customer_id', 'service_id', 'customer_name', 'service_name', 'is_paid',
'created_at', 'updated_at', 'created_by',
]
read_only_fields = ['created_at', 'updated_at', 'created_by']
@@ -92,25 +158,35 @@ class EventSerializer(serializers.ModelSerializer):
def get_duration_minutes(self, obj):
return int(obj.duration.total_seconds() / 60)
def get_resource(self, obj):
def get_resource_id(self, obj):
"""Get first resource ID from participants"""
resource_participant = obj.participants.filter(role='RESOURCE').first()
return resource_participant.object_id if resource_participant else None
def get_customer(self, obj):
"""Get customer ID - placeholder for now"""
return 1 # TODO: Implement actual customer logic
def get_customer_id(self, obj):
"""Get customer ID from participants"""
customer_participant = obj.participants.filter(role='CUSTOMER').first()
return customer_participant.object_id if customer_participant else None
def get_service(self, obj):
def get_service_id(self, obj):
"""Get service ID - placeholder for now"""
return 1 # TODO: Implement actual service logic
# TODO: Add service link to Event model or participants
return 1
def get_customer_name(self, obj):
"""Get customer name from title for now"""
return obj.title
"""Get customer name from participant"""
customer_participant = obj.participants.filter(role='CUSTOMER').first()
if customer_participant and customer_participant.content_object:
user = customer_participant.content_object
return user.full_name if hasattr(user, 'full_name') else str(user)
# Fallback to title
return obj.title.split(' - ')[0] if ' - ' in obj.title else obj.title
def get_service_name(self, obj):
"""Get service name - placeholder"""
"""Get service name from title"""
# Extract from title format "Customer Name - Service Name"
if ' - ' in obj.title:
return obj.title.split(' - ')[-1]
return "Service"
def get_is_paid(self, obj):

View File

@@ -3,7 +3,7 @@ Schedule App URLs
"""
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import ResourceViewSet, EventViewSet, ParticipantViewSet
from .views import ResourceViewSet, EventViewSet, ParticipantViewSet, CustomerViewSet, ServiceViewSet
# Create router and register viewsets
router = DefaultRouter()
@@ -11,6 +11,8 @@ router.register(r'resources', ResourceViewSet, basename='resource')
router.register(r'appointments', EventViewSet, basename='appointment') # Alias for frontend
router.register(r'events', EventViewSet, basename='event')
router.register(r'participants', ParticipantViewSet, basename='participant')
router.register(r'customers', CustomerViewSet, basename='customer')
router.register(r'services', ServiceViewSet, basename='service')
# URL patterns
urlpatterns = [

View File

@@ -7,8 +7,10 @@ from rest_framework import viewsets, status
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.response import Response
from .models import Resource, Event, Participant
from .serializers import ResourceSerializer, EventSerializer, ParticipantSerializer
from .serializers import ResourceSerializer, EventSerializer, ParticipantSerializer, CustomerSerializer, ServiceSerializer
from .models import Service
from core.permissions import HasQuota
from smoothschedule.users.models import User
class ResourceViewSet(viewsets.ModelViewSet):
@@ -52,17 +54,46 @@ class EventViewSet(viewsets.ModelViewSet):
- EventSerializer.validate() automatically checks resource availability
- If resource capacity exceeded, returns 400 Bad Request
- See schedule/services.py AvailabilityService for logic
Query Parameters:
- start_date: Filter events starting on or after this date (ISO format)
- end_date: Filter events starting before this date (ISO format)
- status: Filter by event status
"""
queryset = Event.objects.all()
serializer_class = EventSerializer
# TODO: Re-enable authentication for production
permission_classes = [AllowAny] # Temporarily allow unauthenticated access for development
filterset_fields = ['status', 'start_time', 'end_time']
filterset_fields = ['status']
search_fields = ['title', 'notes']
ordering_fields = ['start_time', 'end_time', 'created_at']
ordering = ['start_time']
def get_queryset(self):
"""
Filter events by date range if start_date and end_date are provided.
"""
queryset = Event.objects.all()
# Filter by date range
start_date = self.request.query_params.get('start_date')
end_date = self.request.query_params.get('end_date')
if start_date:
from django.utils.dateparse import parse_datetime
start_dt = parse_datetime(start_date)
if start_dt:
queryset = queryset.filter(start_time__gte=start_dt)
if end_date:
from django.utils.dateparse import parse_datetime
end_dt = parse_datetime(end_date)
if end_dt:
queryset = queryset.filter(start_time__lt=end_dt)
return queryset
def perform_create(self, serializer):
"""
Create event with automatic availability validation.
@@ -101,3 +132,82 @@ class ParticipantViewSet(viewsets.ModelViewSet):
filterset_fields = ['event', 'role', 'content_type']
ordering_fields = ['created_at']
ordering = ['-created_at']
class CustomerViewSet(viewsets.ModelViewSet):
"""
API endpoint for managing Customers.
Customers are Users with role=CUSTOMER belonging to the current tenant.
"""
serializer_class = CustomerSerializer
# TODO: Re-enable authentication for production
permission_classes = [AllowAny] # Temporarily allow unauthenticated access for development
filterset_fields = ['is_active']
search_fields = ['email', 'first_name', 'last_name']
ordering_fields = ['email', 'created_at']
ordering = ['email']
def get_queryset(self):
"""
Return customers for the current tenant.
Customers are Users with role=CUSTOMER.
For now, return all customers. When authentication is enabled,
filter by the user's tenant.
"""
queryset = User.objects.filter(role=User.Role.CUSTOMER)
# Filter by tenant if user is authenticated and has a tenant
# TODO: Re-enable this when authentication is enabled
# if self.request.user.is_authenticated and self.request.user.tenant:
# queryset = queryset.filter(tenant=self.request.user.tenant)
# Apply status filter if provided
status_filter = self.request.query_params.get('status')
if status_filter:
if status_filter == 'Active':
queryset = queryset.filter(is_active=True)
elif status_filter == 'Inactive':
queryset = queryset.filter(is_active=False)
# Apply search filter if provided
search = self.request.query_params.get('search')
if search:
from django.db.models import Q
queryset = queryset.filter(
Q(email__icontains=search) |
Q(first_name__icontains=search) |
Q(last_name__icontains=search)
)
return queryset
class ServiceViewSet(viewsets.ModelViewSet):
"""
API endpoint for managing Services.
Services are the offerings a business provides (e.g., Haircut, Massage).
"""
queryset = Service.objects.filter(is_active=True)
serializer_class = ServiceSerializer
# TODO: Re-enable authentication for production
permission_classes = [AllowAny] # Temporarily allow unauthenticated access for development
filterset_fields = ['is_active']
search_fields = ['name', 'description']
ordering_fields = ['name', 'price', 'duration', 'created_at']
ordering = ['name']
def get_queryset(self):
"""Return services, optionally including inactive ones."""
queryset = Service.objects.all()
# By default only show active services
show_inactive = self.request.query_params.get('show_inactive', 'false')
if show_inactive.lower() != 'true':
queryset = queryset.filter(is_active=True)
return queryset

View File

@@ -1,12 +1,16 @@
"""
API views for user authentication
"""
import secrets
from django.core.mail import send_mail
from django.conf import settings
from django.utils import timezone
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.response import Response
from .models import User
from .models import User, EmailVerificationToken
@api_view(['GET'])
@@ -31,14 +35,27 @@ def current_user_view(request):
else:
business_subdomain = user.tenant.schema_name
# Map database roles to frontend roles
role_mapping = {
'superuser': 'superuser',
'platform_manager': 'platform_manager',
'platform_sales': 'platform_sales',
'platform_support': 'platform_support',
'tenant_owner': 'owner',
'tenant_manager': 'manager',
'tenant_staff': 'staff',
'customer': 'customer',
}
frontend_role = role_mapping.get(user.role.lower(), user.role.lower())
user_data = {
'id': user.id,
'username': user.username,
'email': user.email,
'name': user.full_name,
'role': user.role.lower(),
'role': frontend_role,
'avatar_url': None, # TODO: Implement avatar
'email_verified': False, # TODO: Implement email verification
'email_verified': user.email_verified,
'is_staff': user.is_staff,
'is_superuser': user.is_superuser,
'business': user.tenant_id,
@@ -59,3 +76,87 @@ def logout_view(request):
from django.contrib.auth import logout
logout(request)
return Response({"detail": "Successfully logged out."}, status=status.HTTP_200_OK)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def send_verification_email(request):
"""
Send email verification link
POST /api/auth/email/verify/send/
"""
user = request.user
if user.email_verified:
return Response({"detail": "Email already verified."}, status=status.HTTP_400_BAD_REQUEST)
# Create token
token = EmailVerificationToken.create_for_user(user)
# Build verification URL
# Use the frontend URL for verification
port = ':5173' if settings.DEBUG else ''
subdomain = ''
if user.tenant:
primary_domain = user.tenant.domains.filter(is_primary=True).first()
if primary_domain:
subdomain = primary_domain.domain.split('.')[0] + '.'
verify_url = f"http://{subdomain}lvh.me{port}/#/verify-email?token={token.token}"
# Send email (goes to console in development)
subject = "Verify your email - Smooth Schedule"
message = f"""Hi {user.full_name},
Please click the link below to verify your email address:
{verify_url}
This link will expire in 24 hours.
If you did not request this, please ignore this email.
Thanks,
The Smooth Schedule Team
"""
send_mail(
subject,
message,
settings.DEFAULT_FROM_EMAIL if hasattr(settings, 'DEFAULT_FROM_EMAIL') else 'noreply@smoothschedule.com',
[user.email],
fail_silently=False,
)
return Response({"detail": "Verification email sent."}, status=status.HTTP_200_OK)
@api_view(['POST'])
@permission_classes([AllowAny])
def verify_email(request):
"""
Verify email with token
POST /api/auth/email/verify/
"""
token_str = request.data.get('token')
if not token_str:
return Response({"error": "Token is required."}, status=status.HTTP_400_BAD_REQUEST)
try:
token = EmailVerificationToken.objects.get(token=token_str)
except EmailVerificationToken.DoesNotExist:
return Response({"error": "Invalid token."}, status=status.HTTP_400_BAD_REQUEST)
if not token.is_valid():
return Response({"error": "Token has expired or already been used."}, status=status.HTTP_400_BAD_REQUEST)
# Mark token as used
token.used = True
token.save()
# Mark user email as verified
token.user.email_verified = True
token.user.save(update_fields=['email_verified'])
return Response({"detail": "Email verified successfully."}, status=status.HTTP_200_OK)

View File

@@ -0,0 +1,34 @@
# Generated by Django 5.2.8 on 2025-11-28 00:18
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0002_alter_user_options_remove_user_name_user_created_at_and_more'),
]
operations = [
migrations.AddField(
model_name='user',
name='email_verified',
field=models.BooleanField(default=False, help_text='Whether user has verified their email address'),
),
migrations.CreateModel(
name='EmailVerificationToken',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('token', models.CharField(max_length=64, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('expires_at', models.DateTimeField()),
('used', models.BooleanField(default=False)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='email_tokens', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created_at'],
},
),
]

View File

@@ -2,8 +2,11 @@
Smooth Schedule Custom User Model
Implements strict role hierarchy and multi-tenant user management
"""
import secrets
from datetime import timedelta
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
@@ -53,6 +56,10 @@ class User(AbstractUser):
default=False,
help_text="True for sales demo accounts - can be masqueraded by Platform Sales"
)
email_verified = models.BooleanField(
default=False,
help_text="Whether user has verified their email address"
)
# Additional profile fields
phone = models.CharField(max_length=20, blank=True)
@@ -159,3 +166,32 @@ class User(AbstractUser):
raise ValueError(f"Users with role {self.role} must be assigned to a tenant")
super().save(*args, **kwargs)
class EmailVerificationToken(models.Model):
"""Token for email verification"""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='email_tokens')
token = models.CharField(max_length=64, unique=True)
created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField()
used = models.BooleanField(default=False)
class Meta:
ordering = ['-created_at']
def save(self, *args, **kwargs):
if not self.token:
self.token = secrets.token_urlsafe(32)
if not self.expires_at:
self.expires_at = timezone.now() + timedelta(hours=24)
super().save(*args, **kwargs)
def is_valid(self):
return not self.used and timezone.now() < self.expires_at
@classmethod
def create_for_user(cls, user):
# Invalidate old tokens
cls.objects.filter(user=user, used=False).update(used=True)
# Create new token
return cls.objects.create(user=user)