Files
smoothschedule/CLAUDE.md
poduck f4332153f4 feat: Add timezone architecture for consistent date/time handling
- 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>
2025-12-07 19:39:36 -05:00

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

  1. Database: All times stored in UTC
  2. API Communication: Always use UTC (both directions)
  3. API Responses: Include business_timezone field
  4. Frontend Display: Convert UTC based on business_timezone
    • If business_timezone is set → display in that timezone
    • If business_timezone is null/blank → display in user's local timezone

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 null if 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.