- Create dateUtils.ts with helpers for UTC conversion and timezone display - Add TimezoneSerializerMixin to include business_timezone in API responses - Update GeneralSettings timezone dropdown with IANA identifiers - Apply timezone mixin to Event, TimeBlock, and field mobile serializers - Document timezone architecture in CLAUDE.md All times stored in UTC, converted for display based on business timezone. If business_timezone is null, uses user's local timezone. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
13 KiB
SmoothSchedule - Multi-Tenant Scheduling Platform
Quick Reference
Project Structure
/home/poduck/Desktop/smoothschedule2/
├── frontend/ # React + Vite + TypeScript frontend
│ └── CLAUDE.md # Frontend-specific docs
│
├── smoothschedule/ # Django backend (RUNS IN DOCKER!)
│ └── CLAUDE.md # Backend-specific docs
│
└── legacy_reference/ # Old code for reference (do not modify)
Development URLs
- Frontend:
http://demo.lvh.me:5173(or any business subdomain) - Platform Frontend:
http://platform.lvh.me:5173 - Backend API:
http://lvh.me:8000/api/
Note: lvh.me resolves to 127.0.0.1 - required for subdomain cookies to work.
CRITICAL: Backend Runs in Docker
NEVER run Django commands directly. Always use Docker Compose:
cd /home/poduck/Desktop/smoothschedule2/smoothschedule
# Run migrations
docker compose -f docker-compose.local.yml exec django python manage.py migrate
# Django shell
docker compose -f docker-compose.local.yml exec django python manage.py shell
# View logs
docker compose -f docker-compose.local.yml logs -f django
# Any management command
docker compose -f docker-compose.local.yml exec django python manage.py <command>
Key Configuration Files
Backend (Django)
| File | Purpose |
|---|---|
smoothschedule/docker-compose.local.yml |
Docker services config |
smoothschedule/.envs/.local/.django |
Django env vars (SECRET_KEY, etc.) |
smoothschedule/.envs/.local/.postgres |
Database credentials |
smoothschedule/config/settings/local.py |
Local Django settings |
smoothschedule/config/settings/base.py |
Base Django settings |
smoothschedule/config/urls.py |
URL routing |
Frontend (React)
| File | Purpose |
|---|---|
frontend/.env.development |
Vite env vars |
frontend/vite.config.ts |
Vite configuration |
frontend/src/api/client.ts |
Axios API client |
frontend/src/types.ts |
TypeScript interfaces |
frontend/src/i18n/locales/en.json |
Translations |
frontend/src/utils/dateUtils.ts |
Date formatting utilities |
Timezone Architecture (CRITICAL)
All date/time handling follows this architecture to ensure consistency across timezones.
Core Principles
- Database: All times stored in UTC
- API Communication: Always use UTC (both directions)
- API Responses: Include
business_timezonefield - Frontend Display: Convert UTC based on
business_timezone- If
business_timezoneis set → display in that timezone - If
business_timezoneis null/blank → display in user's local timezone
- If
Data Flow
FRONTEND (User in Eastern Time selects "Dec 8, 2:00 PM")
↓
Convert to UTC: "2024-12-08T19:00:00Z"
↓
Send to API (always UTC)
↓
DATABASE (stores UTC)
↓
API RESPONSE:
{
"start_time": "2024-12-08T19:00:00Z", // Always UTC
"business_timezone": "America/Denver" // IANA timezone (or null for local)
}
↓
FRONTEND CONVERTS:
- If business_timezone set: UTC → Mountain Time → "Dec 8, 12:00 PM MST"
- If business_timezone null: UTC → User local → "Dec 8, 2:00 PM EST"
Frontend Helper Functions
Located in frontend/src/utils/dateUtils.ts:
import {
toUTC,
fromUTC,
formatForDisplay,
formatDateForDisplay,
getDisplayTimezone,
} from '../utils/dateUtils';
// SENDING TO API - Always convert to UTC
const apiPayload = {
start_time: toUTC(selectedDateTime), // "2024-12-08T19:00:00Z"
};
// RECEIVING FROM API - Convert for display
const displayTime = formatForDisplay(
response.start_time, // UTC from API
response.business_timezone // "America/Denver" or null
);
// Result: "Dec 8, 2024 12:00 PM" (in business or local timezone)
// DATE-ONLY fields (time blocks)
const displayDate = formatDateForDisplay(
response.start_date,
response.business_timezone
);
API Response Requirements
All endpoints returning date/time data MUST include:
# In serializers or views
{
"start_time": "2024-12-08T19:00:00Z",
"business_timezone": business.timezone, # "America/Denver" or None
}
Backend Serializer Mixin
Use TimezoneSerializerMixin from core/mixins.py to automatically add the timezone field:
from core.mixins import TimezoneSerializerMixin
class EventSerializer(TimezoneSerializerMixin, serializers.ModelSerializer):
class Meta:
model = Event
fields = [
'id', 'start_time', 'end_time',
# ... other fields ...
'business_timezone', # Provided by mixin
]
read_only_fields = ['business_timezone']
The mixin automatically retrieves the timezone from the tenant context.
- Returns the IANA timezone string if set (e.g., "America/Denver")
- Returns
nullif not set (frontend uses user's local timezone)
Common Mistakes to Avoid
// BAD - Uses browser local time, not UTC
date.toISOString().split('T')[0]
// BAD - Doesn't respect business timezone setting
new Date(utcString).toLocaleString()
// GOOD - Use helper functions
toUTC(date) // For API requests
formatForDisplay(utcString, businessTimezone) // For displaying
Key Django Apps
| App | Location | Purpose |
|---|---|---|
schedule |
smoothschedule/smoothschedule/schedule/ |
Resources, Events, Services |
users |
smoothschedule/smoothschedule/users/ |
Authentication, User model |
tenants |
smoothschedule/smoothschedule/tenants/ |
Multi-tenancy (Business model) |
core |
smoothschedule/core/ |
Shared mixins, permissions, middleware |
payments |
smoothschedule/payments/ |
Stripe integration, subscriptions |
platform_admin |
smoothschedule/platform_admin/ |
Platform administration |
Core Mixins & Base Classes
Located in smoothschedule/core/mixins.py. Use these to avoid code duplication.
Permission Classes
from core.mixins import DenyStaffWritePermission, DenyStaffAllAccessPermission, DenyStaffListPermission
class MyViewSet(ModelViewSet):
# Block write operations for staff (GET allowed)
permission_classes = [IsAuthenticated, DenyStaffWritePermission]
# Block ALL operations for staff
permission_classes = [IsAuthenticated, DenyStaffAllAccessPermission]
# Block list/create/update/delete but allow retrieve
permission_classes = [IsAuthenticated, DenyStaffListPermission]
Per-User Permission Overrides
Staff permissions can be overridden on a per-user basis using the user.permissions JSONField.
Permission keys are auto-derived from the view's basename or model name:
| Permission Class | Auto-derived Key | Example |
|---|---|---|
DenyStaffWritePermission |
can_write_{basename} |
can_write_resources |
DenyStaffAllAccessPermission |
can_access_{basename} |
can_access_services |
DenyStaffListPermission |
can_list_{basename} or can_access_{basename} |
can_list_customers |
Current ViewSet permission keys:
| ViewSet | Permission Class | Override Key |
|---|---|---|
ResourceViewSet |
DenyStaffAllAccessPermission |
can_access_resources |
ServiceViewSet |
DenyStaffAllAccessPermission |
can_access_services |
CustomerViewSet |
DenyStaffListPermission |
can_list_customers or can_access_customers |
ScheduledTaskViewSet |
DenyStaffAllAccessPermission |
can_access_scheduled-tasks |
Granting a specific staff member access:
# Open Django shell
docker compose -f docker-compose.local.yml exec django python manage.py shell
from smoothschedule.users.models import User
# Find the staff member
staff = User.objects.get(email='john@example.com')
# Grant read access to resources
staff.permissions['can_access_resources'] = True
staff.save()
# Or grant list access to customers (but not full CRUD)
staff.permissions['can_list_customers'] = True
staff.save()
Custom permission keys (optional):
class ResourceViewSet(ModelViewSet):
permission_classes = [IsAuthenticated, DenyStaffAllAccessPermission]
# Override the auto-derived key
staff_access_permission_key = 'can_manage_equipment'
Then grant via: staff.permissions['can_manage_equipment'] = True
QuerySet Mixins
from core.mixins import TenantFilteredQuerySetMixin, UserTenantFilteredMixin
# For tenant-scoped models (automatic django-tenants filtering)
class ResourceViewSet(TenantFilteredQuerySetMixin, ModelViewSet):
queryset = Resource.objects.all()
deny_staff_queryset = True # Optional: also filter staff at queryset level
def filter_queryset_for_tenant(self, queryset):
# Override for custom filtering
return queryset.filter(is_active=True)
# For User model (shared schema, needs explicit tenant filter)
class CustomerViewSet(UserTenantFilteredMixin, ModelViewSet):
queryset = User.objects.filter(role=User.Role.CUSTOMER)
Feature Permission Mixins
from core.mixins import PluginFeatureRequiredMixin, TaskFeatureRequiredMixin
# Checks can_use_plugins feature on list/retrieve/create
class PluginViewSet(PluginFeatureRequiredMixin, ModelViewSet):
pass
# Checks both can_use_plugins AND can_use_tasks
class ScheduledTaskViewSet(TaskFeatureRequiredMixin, TenantFilteredQuerySetMixin, ModelViewSet):
pass
Base API Views (for non-ViewSet views)
from rest_framework.views import APIView
from core.mixins import TenantAPIView, TenantRequiredAPIView
# Optional tenant - use self.get_tenant()
class MyView(TenantAPIView, APIView):
def get(self, request):
tenant = self.get_tenant() # May be None
return self.success_response({'data': 'value'})
# or: return self.error_response('Something went wrong', status_code=400)
# Required tenant - self.tenant always available
class MyTenantView(TenantRequiredAPIView, APIView):
def get(self, request):
# self.tenant is guaranteed to exist (returns 400 if missing)
return Response({'name': self.tenant.name})
Helper Methods Available
| Method | Description |
|---|---|
self.get_tenant() |
Get tenant from request (may be None) |
self.get_tenant_or_error() |
Returns (tenant, error_response) tuple |
self.error_response(msg, status_code) |
Standard error response |
self.success_response(data, status_code) |
Standard success response |
self.check_feature(key, name) |
Check feature permission, returns error or None |
Common Tasks
After modifying Django models:
cd /home/poduck/Desktop/smoothschedule2/smoothschedule
docker compose -f docker-compose.local.yml exec django python manage.py makemigrations
docker compose -f docker-compose.local.yml exec django python manage.py migrate
After modifying frontend:
Frontend hot-reloads automatically. If issues, restart:
cd /home/poduck/Desktop/smoothschedule2/frontend
npm run dev
Debugging 500 errors:
cd /home/poduck/Desktop/smoothschedule2/smoothschedule
docker compose -f docker-compose.local.yml logs django --tail=100
Testing API directly:
curl -s "http://lvh.me:8000/api/resources/" | jq
Git Branch
Currently on: feature/platform-superuser-ui
Main branch: main
Production Deployment
Quick Deploy
# From your local machine
cd /home/poduck/Desktop/smoothschedule2
./deploy.sh poduck@smoothschedule.com
Initial Server Setup (one-time)
# Setup server dependencies
ssh poduck@smoothschedule.com 'bash -s' < server-setup.sh
# Setup DigitalOcean Spaces
ssh poduck@smoothschedule.com
./setup-spaces.sh
Production URLs
- Main site:
https://smoothschedule.com - Platform dashboard:
https://platform.smoothschedule.com - Tenant subdomains:
https://*.smoothschedule.com - Flower (Celery):
https://smoothschedule.com:5555
Production Management
# SSH into server
ssh poduck@smoothschedule.com
# Navigate to project
cd ~/smoothschedule
# View logs
docker compose -f docker-compose.production.yml logs -f
# Run migrations
docker compose -f docker-compose.production.yml exec django python manage.py migrate
# Create superuser
docker compose -f docker-compose.production.yml exec django python manage.py createsuperuser
# Restart services
docker compose -f docker-compose.production.yml restart
# View status
docker compose -f docker-compose.production.yml ps
Environment Variables
Production environment configured in:
- Backend:
smoothschedule/.envs/.production/.django - Database:
smoothschedule/.envs/.production/.postgres - Frontend:
frontend/.env.production
DigitalOcean Spaces
- Bucket:
smoothschedule - Region:
nyc3 - Endpoint:
https://nyc3.digitaloceanspaces.com - Public URL:
https://smoothschedule.nyc3.digitaloceanspaces.com
See DEPLOYMENT.md for detailed deployment guide.