- 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>
608 lines
18 KiB
Markdown
608 lines
18 KiB
Markdown
# 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):**
|
|
```bash
|
|
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):**
|
|
```bash
|
|
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**
|
|
```python
|
|
# 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"
|
|
```
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```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 <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`:
|
|
|
|
```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
|
|
```
|
|
|
|
## 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
|
|
|
|
```python
|
|
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:**
|
|
```bash
|
|
# Open Django shell
|
|
docker compose -f docker-compose.local.yml exec django python manage.py shell
|
|
```
|
|
|
|
```python
|
|
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):**
|
|
```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 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
|
|
|
|
```python
|
|
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)
|
|
|
|
```python
|
|
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:
|
|
```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.
|