Files
smoothschedule/CLAUDE.md
poduck 8c52d6a275 refactor: Extract reusable UI components and add TDD documentation
- Add comprehensive TDD documentation to CLAUDE.md with coverage requirements and examples
- Extract reusable UI components to frontend/src/components/ui/ (Modal, FormInput, Button, Alert, etc.)
- Add shared constants (schedulePresets) and utility hooks (useCrudMutation, useFormValidation)
- Update frontend/CLAUDE.md with component documentation and usage examples
- Refactor CreateTaskModal to use shared components and constants
- Fix test assertions to be more robust and accurate across all test files

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-10 15:27:27 -05:00

18 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: Test-Driven Development (TDD) Required

All code changes MUST follow TDD. This is non-negotiable.

TDD Workflow

  1. Write tests FIRST before writing any implementation code
  2. Run tests to verify they fail (red)
  3. Write minimal code to make tests pass (green)
  4. Refactor while keeping tests green
  5. Repeat for each new feature or bug fix

Coverage Requirements

Target Minimum Goal
Backend (Django) 80% 100%
Frontend (React) 80% 100%

Running Tests with Coverage

Backend (Django):

cd /home/poduck/Desktop/smoothschedule2/smoothschedule

# Run all tests with coverage
docker compose -f docker-compose.local.yml exec django pytest --cov --cov-report=term-missing

# Run tests for a specific app
docker compose -f docker-compose.local.yml exec django pytest smoothschedule/scheduling/schedule/tests/ --cov=smoothschedule/scheduling/schedule

# Run a single test file
docker compose -f docker-compose.local.yml exec django pytest smoothschedule/path/to/test_file.py -v

# Run tests matching a pattern
docker compose -f docker-compose.local.yml exec django pytest -k "test_create_resource" -v

Frontend (React):

cd /home/poduck/Desktop/smoothschedule2/frontend

# Run all tests with coverage
npm test -- --coverage

# Run tests in watch mode during development
npm test

# Run a single test file
npm test -- src/hooks/__tests__/useResources.test.ts

# Run tests matching a pattern
npm test -- -t "should create resource"

Test File Organization

Backend:

smoothschedule/smoothschedule/{domain}/{app}/
├── models.py
├── views.py
├── serializers.py
└── tests/
    ├── __init__.py
    ├── test_models.py      # Model unit tests
    ├── test_serializers.py # Serializer tests
    ├── test_views.py       # API endpoint tests
    └── factories.py        # Test factories (optional)

Frontend:

frontend/src/
├── hooks/
│   ├── useResources.ts
│   └── __tests__/
│       └── useResources.test.ts
├── components/
│   ├── MyComponent.tsx
│   └── __tests__/
│       └── MyComponent.test.tsx
└── pages/
    ├── MyPage.tsx
    └── __tests__/
        └── MyPage.test.tsx

What to Test

Backend:

  • Model methods and properties
  • Model validation (clean methods)
  • Serializer validation
  • API endpoints (all HTTP methods)
  • Permission classes
  • Custom querysets and managers
  • Signals
  • Celery tasks
  • Utility functions

Frontend:

  • Custom hooks (state changes, API calls)
  • Component rendering
  • User interactions (clicks, form submissions)
  • Conditional rendering
  • Error states
  • Loading states
  • API client functions

TDD Example - Adding a New Feature

Step 1: Write the test first

# Backend: test_views.py
def test_create_resource_with_schedule(self, api_client, tenant):
    """New feature: resources can have a default schedule."""
    data = {
        "name": "Test Resource",
        "type": "STAFF",
        "default_schedule": {
            "monday": {"start": "09:00", "end": "17:00"},
            "tuesday": {"start": "09:00", "end": "17:00"},
        }
    }
    response = api_client.post("/api/resources/", data, format="json")
    assert response.status_code == 201
    assert response.data["default_schedule"]["monday"]["start"] == "09:00"
// Frontend: useResources.test.ts
it('should create resource with schedule', async () => {
  const { result } = renderHook(() => useCreateResource());

  await act(async () => {
    await result.current.mutateAsync({
      name: 'Test Resource',
      type: 'STAFF',
      defaultSchedule: { monday: { start: '09:00', end: '17:00' } }
    });
  });

  expect(mockApiClient.post).toHaveBeenCalledWith('/resources/', expect.objectContaining({
    default_schedule: expect.any(Object)
  }));
});

Step 2: Run tests - they should FAIL

Step 3: Write minimal implementation to make tests pass

Step 4: Refactor if needed while keeping tests green

Pre-Commit Checklist

Before committing ANY code:

  1. Tests written BEFORE implementation
  2. All tests pass
  3. Coverage meets minimum threshold (80%)
  4. No skipped or disabled tests without justification

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

Django App Organization (Domain-Based)

Apps are organized into domain packages under smoothschedule/smoothschedule/:

Identity Domain

App Location Purpose
core identity/core/ Tenant, Domain, PermissionGrant, middleware, mixins
users identity/users/ User model, authentication, MFA

Scheduling Domain

App Location Purpose
schedule scheduling/schedule/ Resources, Events, Services, Participants
contracts scheduling/contracts/ Contract/e-signature system
analytics scheduling/analytics/ Business analytics and reporting

Communication Domain

App Location Purpose
notifications communication/notifications/ Notification system
credits communication/credits/ SMS/calling credits
mobile communication/mobile/ Field employee mobile app
messaging communication/messaging/ Email templates and messaging

Commerce Domain

App Location Purpose
payments commerce/payments/ Stripe Connect payments bridge
tickets commerce/tickets/ Support ticket system

Platform Domain

App Location Purpose
admin platform/admin/ Platform administration, subscriptions
api platform/api/ Public API v1 for third-party integrations

Core Mixins & Base Classes

Located in smoothschedule/smoothschedule/identity/core/mixins.py. Use these to avoid code duplication.

Permission Classes

from smoothschedule.identity.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.identity.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 smoothschedule.identity.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 smoothschedule.identity.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 smoothschedule.identity.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.