# 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: ```bash 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 ``` ## 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`: ```typescript 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: ```python # 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: ```python 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 ```typescript // 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 ```python 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:** ```bash # Open Django shell docker compose -f docker-compose.local.yml exec django python manage.py shell ``` ```python 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):** ```python 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 ```python 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 ```python 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) ```python 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: ```bash 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: ```bash cd /home/poduck/Desktop/smoothschedule2/frontend npm run dev ``` ### Debugging 500 errors: ```bash cd /home/poduck/Desktop/smoothschedule2/smoothschedule docker compose -f docker-compose.local.yml logs django --tail=100 ``` ### Testing API directly: ```bash curl -s "http://lvh.me:8000/api/resources/" | jq ``` ## Git Branch Currently on: `feature/platform-superuser-ui` Main branch: `main` ## Production Deployment ### Quick Deploy ```bash # From your local machine cd /home/poduck/Desktop/smoothschedule2 ./deploy.sh poduck@smoothschedule.com ``` ### Initial Server Setup (one-time) ```bash # 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 ```bash # 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](DEPLOYMENT.md) for detailed deployment guide.