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:
Binary file not shown.
|
Before Width: | Height: | Size: 224 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 231 KiB |
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"status": "failed",
|
||||
"failedTests": [
|
||||
"5f1889fdd7b10a4db9e9-2b1e81c51a733cc89956",
|
||||
"5f1889fdd7b10a4db9e9-66724cc37c12aaf9dc66",
|
||||
"5f1889fdd7b10a4db9e9-b93c630b7987c0eb4adc"
|
||||
"7662eeffef95b745c0c7-05f7d22eaed6ca80a04d"
|
||||
]
|
||||
}
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
97
frontend/tests/e2e/business-owner-login.spec.ts
Normal file
97
frontend/tests/e2e/business-owner-login.spec.ts
Normal 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 });
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
0
smoothschedule/schedule/management/__init__.py
Normal file
0
smoothschedule/schedule/management/__init__.py
Normal 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')
|
||||
)
|
||||
34
smoothschedule/schedule/migrations/0002_add_service_model.py
Normal file
34
smoothschedule/schedule/migrations/0002_add_service_model.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
22
smoothschedule/schedule/migrations/0003_add_resource_type.py
Normal file
22
smoothschedule/schedule/migrations/0003_add_resource_type.py
Normal 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
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user