From b10426fbdb0435a2a7a3dc0aa3947717c88554a7 Mon Sep 17 00:00:00 2001 From: poduck Date: Fri, 28 Nov 2025 01:11:53 -0500 Subject: [PATCH] feat: Add photo galleries to services, resource types management, and UI improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major features: - Add drag-and-drop photo gallery to Service create/edit modals - Add Resource Types management section to Settings (CRUD for custom types) - Add edit icon consistency to Resources table (pencil icon in actions) - Improve Services page with drag-to-reorder and customer preview mockup Backend changes: - Add photos JSONField to Service model with migration - Add ResourceType model with category (STAFF/OTHER), description fields - Add ResourceTypeViewSet with CRUD operations - Add service reorder endpoint for display order Frontend changes: - Services page: two-column layout, drag-reorder, photo upload - Settings page: Resource Types tab with full CRUD modal - Resources page: Edit icon in actions column instead of row click - Sidebar: Payments link visibility based on role and paymentsEnabled - Update types.ts with Service.photos and ResourceTypeDefinition Note: Removed photos from ResourceType (kept only for Service) πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CLAUDE.md | 102 +++ IMPLEMENTATION_SUMMARY.md | 354 +++++++ RESOURCE_TYPES_PLAN.md | 281 ++++++ frontend/CLAUDE.md | 31 + ...a18c07f8cb8b46b200c1a5f81ddde2ae1b0547c.md | 268 ++++++ ...c066d04c391fc33d77982244596792cc0cf7ab.png | Bin 0 -> 105560 bytes frontend/playwright-report/index.html | 2 +- frontend/src/App.tsx | 141 ++- frontend/src/api/auth.ts | 12 +- frontend/src/api/platform.ts | 11 +- frontend/src/components/Sidebar.tsx | 80 +- frontend/src/components/marketing/Navbar.tsx | 61 +- frontend/src/hooks/useAuth.ts | 4 +- frontend/src/hooks/useBusiness.ts | 6 +- frontend/src/hooks/useResourceTypes.ts | 91 ++ frontend/src/hooks/useResources.ts | 10 + frontend/src/hooks/useServices.ts | 25 + frontend/src/hooks/useStaff.ts | 42 + frontend/src/layouts/MarketingLayout.tsx | 9 +- frontend/src/pages/Customers.tsx | 3 +- frontend/src/pages/LoginPage.tsx | 58 +- frontend/src/pages/Resources.tsx | 186 +++- frontend/src/pages/Services.tsx | 521 +++++++++-- frontend/src/pages/Settings.tsx | 866 +++++++++++++++++- frontend/src/pages/Staff.tsx | 4 +- .../src/pages/platform/PlatformBusinesses.tsx | 22 +- frontend/src/pages/platform/PlatformUsers.tsx | 19 +- frontend/src/types.ts | 21 +- frontend/test-results/.last-run.json | 2 +- .../test-results/before-scheduler-click.png | Bin 0 -> 105539 bytes .../error-context.md | 268 ++++++ .../test-failed-1.png | Bin 0 -> 105560 bytes smoothschedule/CLAUDE.md | 175 ++++ smoothschedule/config/urls.py | 13 +- ..._logo_tenant_logo_display_mode_and_more.py | 33 + ...004_tenant_email_logo_alter_tenant_logo.py | 23 + smoothschedule/core/models.py | 40 +- smoothschedule/core/permissions.py | 17 +- smoothschedule/schedule/api_views.py | 120 ++- .../0005_resource_saved_lane_count.py | 22 + .../migrations/0006_add_user_to_resource.py | 21 + ...ter_resource_type_resourcetype_and_more.py | 40 + .../0008_create_default_resource_types.py | 51 ++ .../0009_add_service_display_order.py | 30 + ...010_add_resourcetype_description_photos.py | 23 + .../migrations/0011_add_photos_to_service.py | 18 + .../0012_remove_photos_from_resourcetype.py | 17 + smoothschedule/schedule/models.py | 91 +- smoothschedule/schedule/serializers.py | 82 +- smoothschedule/schedule/urls.py | 7 +- smoothschedule/schedule/views.py | 123 ++- .../smoothschedule/users/api_views.py | 169 ++++ 52 files changed, 4259 insertions(+), 356 deletions(-) create mode 100644 CLAUDE.md create mode 100644 IMPLEMENTATION_SUMMARY.md create mode 100644 RESOURCE_TYPES_PLAN.md create mode 100644 frontend/playwright-report/data/2a18c07f8cb8b46b200c1a5f81ddde2ae1b0547c.md create mode 100644 frontend/playwright-report/data/54c066d04c391fc33d77982244596792cc0cf7ab.png create mode 100644 frontend/src/hooks/useResourceTypes.ts create mode 100644 frontend/src/hooks/useStaff.ts create mode 100644 frontend/test-results/before-scheduler-click.png create mode 100644 frontend/test-results/month-overlay-scroll-Month-36f33-verlay-scrolls-horizontally-chromium/error-context.md create mode 100644 frontend/test-results/month-overlay-scroll-Month-36f33-verlay-scrolls-horizontally-chromium/test-failed-1.png create mode 100644 smoothschedule/CLAUDE.md create mode 100644 smoothschedule/core/migrations/0003_tenant_logo_tenant_logo_display_mode_and_more.py create mode 100644 smoothschedule/core/migrations/0004_tenant_email_logo_alter_tenant_logo.py create mode 100644 smoothschedule/schedule/migrations/0005_resource_saved_lane_count.py create mode 100644 smoothschedule/schedule/migrations/0006_add_user_to_resource.py create mode 100644 smoothschedule/schedule/migrations/0007_alter_resource_type_resourcetype_and_more.py create mode 100644 smoothschedule/schedule/migrations/0008_create_default_resource_types.py create mode 100644 smoothschedule/schedule/migrations/0009_add_service_display_order.py create mode 100644 smoothschedule/schedule/migrations/0010_add_resourcetype_description_photos.py create mode 100644 smoothschedule/schedule/migrations/0011_add_photos_to_service.py create mode 100644 smoothschedule/schedule/migrations/0012_remove_photos_from_resourcetype.py diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0d89960 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,102 @@ +# 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 | + +## 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) | + +## 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` diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..67febae --- /dev/null +++ b/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,354 @@ +# Resource Types & Logo Upload - Implementation Summary + +## βœ… Completed + +### Backend Models + +1. **ResourceType Model** (`smoothschedule/schedule/models.py`) + - Custom resource type definitions (e.g., "Hair Stylist", "Massage Room") + - Category field: STAFF (requires staff assignment) or OTHER + - `is_default` flag to prevent deletion of core types + - Validation to prevent deletion when in use + - Database migration created and applied βœ… + +2. **Resource Model Updates** (`smoothschedule/schedule/models.py`) + - Added `resource_type` ForeignKey to ResourceType model + - Kept legacy `type` field for backwards compatibility + - Migration created and applied βœ… + +3. **Tenant Model Updates** (`smoothschedule/core/models.py`) + - Added `logo` ImageField for business logo upload + - Added `logo_display_mode` field with choices: + - `logo-only`: Show only logo in sidebar + - `text-only`: Show only business name (default) + - `logo-and-text`: Show both logo and name + - Added `primary_color` and `secondary_color` fields + - Database migration created and applied βœ… + +### Frontend Updates + +1. **TypeScript Types** (`frontend/src/types.ts`) + - Added `ResourceTypeDefinition` interface + - Added `ResourceTypeCategory` type + - Added `logoDisplayMode` to Business interface + - Updated Resource interface with `typeId` field + +2. **React Hooks** (`frontend/src/hooks/useResourceTypes.ts`) + - `useResourceTypes()` - Fetch resource types + - `useCreateResourceType()` - Create new type + - `useUpdateResourceType()` - Update existing type + - `useDeleteResourceType()` - Delete type (with validation) + - Includes placeholder data for default types + +3. **Sidebar Component** (`frontend/src/components/Sidebar.tsx`) + - Updated to display logo based on `logoDisplayMode` + - Shows logo image when mode is `logo-only` or `logo-and-text` + - Hides business name text when mode is `logo-only` + - Maintains fallback to initials when no logo + +4. **Resource Modal** (`frontend/src/pages/Resources.tsx`) + - Resource type dropdown now works (no longer disabled) + - Staff autocomplete fully functional with keyboard navigation + - Debounced search to reduce API calls + - All three default types available (Staff, Room, Equipment) + +## ⏳ Next Steps + +### 1. Data Migration for Default Types +Create a data migration to populate default ResourceType records for existing tenants: + +```bash +cd /home/poduck/Desktop/smoothschedule2/smoothschedule +docker compose -f docker-compose.local.yml exec django python manage.py makemigrations schedule --empty --name create_default_resource_types +``` + +Then edit the migration to add: +```python +def create_default_types(apps, schema_editor): + ResourceType = apps.get_model('schedule', 'ResourceType') + + # Create default types + ResourceType.objects.get_or_create( + name='Staff', + defaults={ + 'category': 'STAFF', + 'is_default': True, + 'icon_name': 'user' + } + ) + ResourceType.objects.get_or_create( + name='Room', + defaults={ + 'category': 'OTHER', + 'is_default': True, + 'icon_name': 'home' + } + ) + ResourceType.objects.get_or_create( + name='Equipment', + defaults={ + 'category': 'OTHER', + 'is_default': True, + 'icon_name': 'wrench' + } + ) +``` + +### 2. API Serializers & Views +Create Django REST Framework serializers and viewsets: + +**File**: `smoothschedule/schedule/serializers.py` +```python +class ResourceTypeSerializer(serializers.ModelSerializer): + class Meta: + model = ResourceType + fields = ['id', 'name', 'category', 'is_default', 'icon_name', 'created_at'] + read_only_fields = ['id', 'created_at'] + + def validate_delete(self, instance): + if instance.is_default: + raise serializers.ValidationError("Cannot delete default resource types.") + if instance.resources.exists(): + raise serializers.ValidationError(f"Cannot delete - in use by {instance.resources.count()} resources.") +``` + +**File**: `smoothschedule/schedule/views.py` +```python +class ResourceTypeViewSet(viewsets.ModelViewSet): + queryset = ResourceType.objects.all() + serializer_class = ResourceTypeSerializer + permission_classes = [IsAuthenticated] + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + self.serializer_class().validate_delete(instance) + return super().destroy(request, *args, **kwargs) +``` + +**File**: `smoothschedule/schedule/urls.py` +```python +router.register(r'resource-types', ResourceTypeViewSet, basename='resourcetype') +``` + +### 3. Logo Upload API +Add logo upload endpoint to tenant/business API: + +**File**: `smoothschedule/core/api_views.py` (or similar) +```python +class TenantLogoUploadView(APIView): + permission_classes = [IsAuthenticated] + parser_classes = [MultiPartParser, FormParser] + + def post(self, request): + tenant = request.tenant # From django-tenants middleware + logo_file = request.FILES.get('logo') + + if not logo_file: + return Response({'error': 'No logo file provided'}, status=400) + + # Validate file size (5MB max) + if logo_file.size > 5 * 1024 * 1024: + return Response({'error': 'Logo file too large (max 5MB)'}, status=400) + + # Validate file type + if not logo_file.content_type.startswith('image/'): + return Response({'error': 'File must be an image'}, status=400) + + tenant.logo = logo_file + tenant.save() + + return Response({ + 'logoUrl': tenant.logo.url, + 'message': 'Logo uploaded successfully' + }) + + def delete(self, request): + tenant = request.tenant + tenant.logo.delete() + tenant.logo = None + tenant.save() + return Response({'message': 'Logo removed successfully'}) +``` + +### 4. Settings Page UI Components +Add to `frontend/src/pages/Settings.tsx`: + +#### A. Resource Types Tab +```tsx +{activeTab === 'resources' && ( +
+
+

Resource Types

+

Customize how you categorize your bookable resources.

+
+ + +
+)} +``` + +#### B. Logo Upload in Branding Section +```tsx +
+

Business Logo

+ + {/* Preview */} + {business.logoUrl ? ( +
+ Logo + +
+ ) : ( +
+ +
+ )} + + {/* Upload */} + + + + {/* Display Mode */} +
+ + +
+ + {/* Preview with menu background */} +
+ +
+
+ {business.logoUrl && (business.logoDisplayMode === 'logo-only' || business.logoDisplayMode === 'logo-and-text') ? ( + Logo + ) : ( +
+ {business.name.substring(0, 2).toUpperCase()} +
+ )} + {business.logoDisplayMode !== 'logo-only' && ( +
+

{business.name}

+

{business.subdomain}.smoothschedule.com

+
+ )} +
+
+
+
+``` + +### 5. Update Resources Page +Modify `frontend/src/pages/Resources.tsx` to use custom resource types: + +```tsx +const { data: resourceTypes = [] } = useResourceTypes(); + +// In the modal: + + +{/* Show staff selector if selected type is STAFF category */} +{selectedType?.category === 'STAFF' && ( + +)} +``` + +## Testing Checklist + +- [ ] Create a custom resource type (e.g., "Massage Therapist") +- [ ] Delete a custom type (should work if not in use) +- [ ] Try to delete a default type (should fail) +- [ ] Try to delete a type in use (should fail) +- [ ] Upload a business logo +- [ ] Change logo display mode and verify sidebar updates +- [ ] Remove logo +- [ ] Create a resource using custom type +- [ ] Verify staff assignment required for STAFF category types + +## Files Modified/Created + +### Backend +- βœ… `smoothschedule/schedule/models.py` - Added ResourceType model, updated Resource +- βœ… `smoothschedule/core/models.py` - Added logo fields to Tenant +- βœ… `smoothschedule/schedule/migrations/0007_*.py` - ResourceType migration +- βœ… `smoothschedule/core/migrations/0003_*.py` - Tenant logo migration +- ⏳ `smoothschedule/schedule/serializers.py` - Need ResourceTypeSerializer +- ⏳ `smoothschedule/schedule/views.py` - Need ResourceTypeViewSet +- ⏳ `smoothschedule/schedule/urls.py` - Need router registration +- ⏳ `smoothschedule/core/api_views.py` - Need logo upload endpoint + +### Frontend +- βœ… `frontend/src/types.ts` - Added ResourceType interfaces +- βœ… `frontend/src/hooks/useResourceTypes.ts` - CRUD hooks +- βœ… `frontend/src/components/Sidebar.tsx` - Logo display logic +- βœ… `frontend/src/pages/Resources.tsx` - Fixed dropdown, autocomplete +- ⏳ `frontend/src/pages/Settings.tsx` - Need resource types tab & logo upload UI +- ⏳ `frontend/src/components/ResourceTypesManager.tsx` - Need new component + +## Database Schema + +### ResourceType Table +```sql +CREATE TABLE schedule_resourcetype ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + category VARCHAR(10) NOT NULL DEFAULT 'OTHER', + is_default BOOLEAN NOT NULL DEFAULT FALSE, + icon_name VARCHAR(50), + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL +); +``` + +### Tenant Table Updates +```sql +ALTER TABLE core_tenant ADD COLUMN logo VARCHAR(100); +ALTER TABLE core_tenant ADD COLUMN logo_display_mode VARCHAR(20) DEFAULT 'text-only'; +ALTER TABLE core_tenant ADD COLUMN primary_color VARCHAR(7) DEFAULT '#2563eb'; +ALTER TABLE core_tenant ADD COLUMN secondary_color VARCHAR(7) DEFAULT '#0ea5e9'; +``` + +### Resource Table Updates +```sql +ALTER TABLE schedule_resource ADD COLUMN resource_type_id INTEGER REFERENCES schedule_resourcetype(id) ON DELETE PROTECT; +``` + +## Notes + +- Logo files stored in `MEDIA_ROOT/tenant_logos/` +- Max file size: 5MB +- Supported formats: PNG, JPG, SVG +- Recommended dimensions: 500x500px (square) or 500x200px (wide) +- Default resource types are created via data migration +- Legacy `Resource.type` field kept for backwards compatibility diff --git a/RESOURCE_TYPES_PLAN.md b/RESOURCE_TYPES_PLAN.md new file mode 100644 index 0000000..05b5706 --- /dev/null +++ b/RESOURCE_TYPES_PLAN.md @@ -0,0 +1,281 @@ +# Custom Resource Types & Logo Upload - Implementation Plan + +## Overview +Allow businesses to create custom resource types (e.g., "Stylist", "Massage Therapist", "Treatment Room") instead of hardcoded types. Also add logo upload to business branding. + +## Features + +### 1. Custom Resource Types +- **Default Types**: Always include one "Staff" type (cannot be deleted) +- **Custom Names**: Users can name types whatever they want (e.g., "Hair Stylist", "Nail Technician", "Massage Room") +- **Categories**: Each type has a category: + - `STAFF`: Requires staff member assignment + - `OTHER`: No staff assignment needed +- **Management**: Add, edit, delete custom types in Business Settings + +### 2. Logo Upload +- Upload business logo in branding section +- Support PNG, JPG, SVG +- Preview before saving +- Used in customer-facing pages + +## Database Changes + +### Backend Models + +```python +# smoothschedule/schedule/models.py + +class ResourceType(models.Model): + """Custom resource type definitions per business""" + business = models.ForeignKey('tenants.Business', on_delete=models.CASCADE, related_name='resource_types') + name = models.CharField(max_length=100) # "Stylist", "Treatment Room", etc. + category = models.CharField(max_length=10, choices=[ + ('STAFF', 'Staff'), + ('OTHER', 'Other'), + ]) + is_default = models.BooleanField(default=False) # Cannot be deleted + icon_name = models.CharField(max_length=50, null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ['business', 'name'] + ordering = ['name'] + + def __str__(self): + return f"{self.business.name} - {self.name}" + + +class Resource(models.Model): + """Update existing Resource model""" + # ... existing fields ... + + # NEW FIELD - references custom resource type + resource_type = models.ForeignKey( + ResourceType, + on_delete=models.PROTECT, # Cannot delete type if resources use it + related_name='resources', + null=True, # For migration + blank=True + ) + + # DEPRECATED - keep for backwards compatibility during migration + type = models.CharField( + max_length=20, + choices=[('STAFF', 'Staff'), ('ROOM', 'Room'), ('EQUIPMENT', 'Equipment')], + default='STAFF' + ) + + +# smoothschedule/tenants/models.py + +class Business(models.Model): + # ... existing fields ... + + # NEW FIELD - logo upload + logo = models.ImageField(upload_to='business_logos/', null=True, blank=True) +``` + +### Migration Strategy + +1. Create `ResourceType` model +2. For each business, create default resource types: + - "Staff" (category=STAFF, is_default=True) + - "Room" (category=OTHER, is_default=True) + - "Equipment" (category=OTHER, is_default=True) +3. Migrate existing resources to use default types based on their `type` field +4. Eventually deprecate `Resource.type` field + +## API Endpoints + +### Resource Types + +``` +GET /api/resource-types/ # List business resource types +POST /api/resource-types/ # Create new type +PATCH /api/resource-types/{id}/ # Update type +DELETE /api/resource-types/{id}/ # Delete type (if not in use & not default) +``` + +### Logo Upload + +``` +POST /api/business/logo/ # Upload logo +DELETE /api/business/logo/ # Remove logo +``` + +## Frontend Implementation + +### 1. Business Settings - Resource Types Tab + +```tsx +// Add new tab to Settings.tsx + + + + +``` + +### ResourceTypesManager Component + +```tsx +interface ResourceTypesManagerProps {} + +const ResourceTypesManager: React.FC = () => { + const { data: types = [] } = useResourceTypes(); + const createMutation = useCreateResourceType(); + const updateMutation = useUpdateResourceType(); + const deleteMutation = useDeleteResourceType(); + + return ( +
+

Resource Types

+

Customize how you categorize your bookable resources.

+ + {/* List of types */} + {types.map(type => ( + + ))} + + {/* Add new type button */} + + + {/* Create/Edit Modal */} + +
+ ); +}; +``` + +### 2. Logo Upload in Branding Section + +```tsx +// In Settings.tsx, Branding section + +
+

Business Logo

+ + {/* Logo preview */} + {business.logoUrl && ( +
+ Business logo + +
+ )} + + {/* Upload button */} + + +
+``` + +### 3. Update Resources.tsx to use custom types + +```tsx +// Resources.tsx + +const Resources: React.FC = () => { + const { data: resourceTypes = [] } = useResourceTypes(); + + return ( + + + + {/* Show staff selector if selected type is STAFF category */} + {selectedType?.category === 'STAFF' && ( + + )} + + ); +}; +``` + +## Implementation Steps + +### Phase 1: Backend Foundation +1. βœ… Create TypeScript types (DONE) +2. ⏳ Create Django models (`ResourceType`, add `Business.logo`) +3. ⏳ Create migrations with default data +4. ⏳ Create serializers +5. ⏳ Create API views and URLs +6. ⏳ Add file upload handling for logos + +### Phase 2: Frontend Hooks & Types +1. βœ… Create `useResourceTypes` hook (DONE) +2. ⏳ Create logo upload utilities +3. ⏳ Update `Resource` type to include `typeId` + +### Phase 3: UI Components +1. ⏳ Add "Resource Types" tab to Settings +2. ⏳ Create ResourceTypesManager component +3. ⏳ Create ResourceTypeModal component +4. ⏳ Add logo upload to Branding section +5. ⏳ Update Resources page to use custom types + +### Phase 4: Migration & Testing +1. ⏳ Test creating/editing/deleting types +2. ⏳ Test logo upload/removal +3. ⏳ Ensure backwards compatibility +4. ⏳ Test resource creation with custom types + +## User Experience + +### Creating a Custom Resource Type +1. Navigate to Business Settings β†’ Resource Types +2. Click "+ Add Resource Type" +3. Enter name (e.g., "Massage Therapist") +4. Select category: Staff or Other +5. Click Save +6. New type appears in list and is available when creating resources + +### Uploading a Logo +1. Navigate to Business Settings β†’ General β†’ Branding +2. Click "Upload Logo" +3. Select image file (PNG, JPG, or SVG) +4. Preview appears +5. Click "Save Changes" +6. Logo appears in header and customer-facing pages + +## Benefits + +1. **Flexibility**: Businesses can name types to match their industry +2. **Clarity**: "Massage Therapist" is clearer than "Staff" for a spa +3. **Scalability**: Add new types as business grows +4. **Branding**: Logo upload improves professional appearance +5. **User-Friendly**: Simple UI for non-technical users + +## Notes + +- Default "Staff" type cannot be deleted (ensures at least one staff type exists) +- Cannot delete types that are in use by existing resources +- Logo file size limit: 5MB +- Logo recommended dimensions: 500x500px (square) or 500x200px (wide) diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index 7f08de0..1ae582c 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -1,5 +1,36 @@ # SmoothSchedule Frontend Development Guide +## Project Overview + +This is the React frontend for SmoothSchedule, a multi-tenant scheduling platform. + +**See also:** `/home/poduck/Desktop/smoothschedule2/smoothschedule/CLAUDE.md` for backend documentation. + +## Key Paths + +``` +/home/poduck/Desktop/smoothschedule2/ +β”œβ”€β”€ frontend/ # This React frontend +β”‚ β”œβ”€β”€ src/ +β”‚ β”‚ β”œβ”€β”€ api/client.ts # Axios API client +β”‚ β”‚ β”œβ”€β”€ components/ # Reusable components +β”‚ β”‚ β”œβ”€β”€ hooks/ # React Query hooks (useResources, useAuth, etc.) +β”‚ β”‚ β”œβ”€β”€ pages/ # Page components +β”‚ β”‚ β”œβ”€β”€ types.ts # TypeScript interfaces +β”‚ β”‚ β”œβ”€β”€ i18n/locales/en.json # English translations +β”‚ β”‚ └── utils/cookies.ts # Cookie utilities +β”‚ β”œβ”€β”€ .env.development # Frontend env vars +β”‚ └── vite.config.ts # Vite configuration +β”‚ +└── smoothschedule/ # Django backend (runs in Docker!) + β”œβ”€β”€ docker-compose.local.yml # Docker config + β”œβ”€β”€ .envs/.local/ # Backend env vars + β”œβ”€β”€ config/settings/ # Django settings + └── smoothschedule/ + β”œβ”€β”€ schedule/ # Core scheduling app + └── users/ # User management +``` + ## Local Development Domain Setup ### Why lvh.me instead of localhost? diff --git a/frontend/playwright-report/data/2a18c07f8cb8b46b200c1a5f81ddde2ae1b0547c.md b/frontend/playwright-report/data/2a18c07f8cb8b46b200c1a5f81ddde2ae1b0547c.md new file mode 100644 index 0000000..aebc645 --- /dev/null +++ b/frontend/playwright-report/data/2a18c07f8cb8b46b200c1a5f81ddde2ae1b0547c.md @@ -0,0 +1,268 @@ +# Page snapshot + +```yaml +- generic [ref=e1]: + - generic [ref=e3]: + - generic [ref=e5]: + - button "Collapse sidebar" [ref=e6]: + - generic [ref=e7]: DE + - generic [ref=e8]: + - heading "Demo Company" [level=1] [ref=e9] + - paragraph [ref=e10]: demo.smoothschedule.com + - navigation [ref=e11]: + - link "Dashboard" [ref=e12] [cursor=pointer]: + - /url: "#/" + - img [ref=e13] + - generic [ref=e18]: Dashboard + - link "Scheduler" [ref=e19] [cursor=pointer]: + - /url: "#/scheduler" + - img [ref=e20] + - generic [ref=e22]: Scheduler + - link "Customers" [ref=e23] [cursor=pointer]: + - /url: "#/customers" + - img [ref=e24] + - generic [ref=e29]: Customers + - link "Services" [ref=e30] [cursor=pointer]: + - /url: "#/services" + - img [ref=e31] + - generic [ref=e34]: Services + - link "Resources" [ref=e35] [cursor=pointer]: + - /url: "#/resources" + - img [ref=e36] + - generic [ref=e39]: Resources + - generic "Payments are disabled. Enable them in Business Settings to accept payments from customers." [ref=e40]: + - img [ref=e41] + - generic [ref=e43]: Payments + - link "Messages" [ref=e44] [cursor=pointer]: + - /url: "#/messages" + - img [ref=e45] + - generic [ref=e47]: Messages + - link "Staff" [ref=e48] [cursor=pointer]: + - /url: "#/staff" + - img [ref=e49] + - generic [ref=e54]: Staff + - link "Business Settings" [ref=e56] [cursor=pointer]: + - /url: "#/settings" + - img [ref=e57] + - generic [ref=e60]: Business Settings + - generic [ref=e61]: + - generic [ref=e62]: + - img [ref=e63] + - generic [ref=e69]: + - generic [ref=e70]: Powered by + - text: Smooth Schedule + - button "Sign Out" [ref=e71]: + - img [ref=e72] + - generic [ref=e75]: Sign Out + - generic [ref=e76]: + - banner [ref=e77]: + - generic [ref=e79]: + - img [ref=e81] + - textbox "Search" [ref=e84] + - generic [ref=e85]: + - button "πŸ‡ΊπŸ‡Έ English" [ref=e87]: + - img [ref=e88] + - generic [ref=e91]: πŸ‡ΊπŸ‡Έ + - generic [ref=e92]: English + - img [ref=e93] + - button [ref=e95]: + - img [ref=e96] + - button [ref=e98]: + - img [ref=e99] + - button "Business Owner Owner BO" [ref=e104]: + - generic [ref=e105]: + - paragraph [ref=e106]: Business Owner + - paragraph [ref=e107]: Owner + - generic [ref=e108]: BO + - img [ref=e109] + - main [active] [ref=e111]: + - generic [ref=e112]: + - generic [ref=e113]: + - heading "Dashboard" [level=2] [ref=e114] + - paragraph [ref=e115]: Today's Overview + - generic [ref=e116]: + - generic [ref=e117]: + - paragraph [ref=e118]: Total Appointments + - generic [ref=e119]: + - generic [ref=e120]: "50" + - generic [ref=e121]: + - img [ref=e122] + - text: +12% + - generic [ref=e125]: + - paragraph [ref=e126]: Customers + - generic [ref=e127]: + - generic [ref=e128]: "1" + - generic [ref=e129]: + - img [ref=e130] + - text: +8% + - generic [ref=e133]: + - paragraph [ref=e134]: Services + - generic [ref=e135]: + - generic [ref=e136]: "5" + - generic [ref=e137]: + - img [ref=e138] + - text: 0% + - generic [ref=e139]: + - paragraph [ref=e140]: Resources + - generic [ref=e141]: + - generic [ref=e142]: "4" + - generic [ref=e143]: + - img [ref=e144] + - text: +3% + - generic [ref=e147]: + - generic [ref=e149]: + - generic [ref=e150]: + - img [ref=e152] + - heading "Quick Add Appointment" [level=3] [ref=e154] + - generic [ref=e155]: + - generic [ref=e156]: + - generic [ref=e157]: + - img [ref=e158] + - text: Customer + - combobox [ref=e161]: + - option "Walk-in / No customer" [selected] + - option "Customer User (customer@demo.com)" + - generic [ref=e162]: + - generic [ref=e163]: + - img [ref=e164] + - text: Service * + - combobox [ref=e167]: + - option "Select service..." [selected] + - option "Beard Trim (15 min - $15)" + - option "Consultation (30 min - $0)" + - option "Full Styling (60 min - $75)" + - option "Hair Coloring (90 min - $120)" + - option "Haircut (30 min - $35)" + - generic [ref=e168]: + - generic [ref=e169]: + - img [ref=e170] + - text: Resource + - combobox [ref=e173]: + - option "Unassigned" [selected] + - option "Conference Room A" + - option "Dental Chair 1" + - option "Meeting Room B" + - option "Meeting Room B" + - generic [ref=e174]: + - generic [ref=e175]: + - generic [ref=e176]: Date * + - textbox [ref=e177]: 2025-11-27 + - generic [ref=e178]: + - generic [ref=e179]: + - img [ref=e180] + - text: Time * + - combobox [ref=e183]: + - option "06:00" + - option "06:15" + - option "06:30" + - option "06:45" + - option "07:00" + - option "07:15" + - option "07:30" + - option "07:45" + - option "08:00" + - option "08:15" + - option "08:30" + - option "08:45" + - option "09:00" [selected] + - option "09:15" + - option "09:30" + - option "09:45" + - option "10:00" + - option "10:15" + - option "10:30" + - option "10:45" + - option "11:00" + - option "11:15" + - option "11:30" + - option "11:45" + - option "12:00" + - option "12:15" + - option "12:30" + - option "12:45" + - option "13:00" + - option "13:15" + - option "13:30" + - option "13:45" + - option "14:00" + - option "14:15" + - option "14:30" + - option "14:45" + - option "15:00" + - option "15:15" + - option "15:30" + - option "15:45" + - option "16:00" + - option "16:15" + - option "16:30" + - option "16:45" + - option "17:00" + - option "17:15" + - option "17:30" + - option "17:45" + - option "18:00" + - option "18:15" + - option "18:30" + - option "18:45" + - option "19:00" + - option "19:15" + - option "19:30" + - option "19:45" + - option "20:00" + - option "20:15" + - option "20:30" + - option "20:45" + - option "21:00" + - option "21:15" + - option "21:30" + - option "21:45" + - option "22:00" + - option "22:15" + - option "22:30" + - option "22:45" + - generic [ref=e184]: + - generic [ref=e185]: + - img [ref=e186] + - text: Notes + - textbox "Optional notes..." [ref=e189] + - button "Add Appointment" [disabled] [ref=e190]: + - img [ref=e191] + - text: Add Appointment + - generic [ref=e193]: + - heading "Total Revenue" [level=3] [ref=e194] + - application [ref=e198]: + - generic [ref=e202]: + - generic [ref=e203]: + - generic [ref=e205]: Mon + - generic [ref=e207]: Tue + - generic [ref=e209]: Wed + - generic [ref=e211]: Thu + - generic [ref=e213]: Fri + - generic [ref=e215]: Sat + - generic [ref=e217]: Sun + - generic [ref=e218]: + - generic [ref=e220]: $0 + - generic [ref=e222]: $1 + - generic [ref=e224]: $2 + - generic [ref=e226]: $3 + - generic [ref=e228]: $4 + - generic [ref=e229]: + - heading "Upcoming Appointments" [level=3] [ref=e230] + - application [ref=e234]: + - generic [ref=e250]: + - generic [ref=e251]: + - generic [ref=e253]: Mon + - generic [ref=e255]: Tue + - generic [ref=e257]: Wed + - generic [ref=e259]: Thu + - generic [ref=e261]: Fri + - generic [ref=e263]: Sat + - generic [ref=e265]: Sun + - generic [ref=e266]: + - generic [ref=e268]: "0" + - generic [ref=e270]: "3" + - generic [ref=e272]: "6" + - generic [ref=e274]: "9" + - generic [ref=e276]: "12" + - generic [ref=e277]: "0" +``` \ No newline at end of file diff --git a/frontend/playwright-report/data/54c066d04c391fc33d77982244596792cc0cf7ab.png b/frontend/playwright-report/data/54c066d04c391fc33d77982244596792cc0cf7ab.png new file mode 100644 index 0000000000000000000000000000000000000000..79646abf70a907c539bb05a84b3736591810fd19 GIT binary patch literal 105560 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ω(S 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 - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8bb1a53..c63b3af 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -194,29 +194,29 @@ const AppContent: React.FC = () => { return hostname === 'lvh.me' || hostname === 'localhost' || hostname === '127.0.0.1'; }; - // Not authenticated - show public routes - if (!user) { - // On root domain, show marketing site - if (isRootDomain()) { - return ( - - }> - } /> - } /> - } /> - } /> - } /> - } /> - - } /> - } /> - } /> - } /> - - ); - } + // On root domain, ALWAYS show marketing site (even if logged in) + // Logged-in users will see a "Go to Dashboard" link in the navbar + if (isRootDomain()) { + return ( + + }> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + } /> + + ); + } - // On business subdomain, show login + // Not authenticated on subdomain - show login + if (!user) { return ( } /> @@ -232,6 +232,43 @@ const AppContent: React.FC = () => { return ; } + // Subdomain validation for logged-in users + const currentHostname = window.location.hostname; + const isPlatformDomain = currentHostname === 'platform.lvh.me'; + const currentSubdomain = currentHostname.split('.')[0]; + const isBusinessSubdomain = !isRootDomain() && !isPlatformDomain && currentSubdomain !== 'api'; + + const isPlatformUser = ['superuser', 'platform_manager', 'platform_support'].includes(user.role); + const isBusinessUser = ['owner', 'manager', 'staff', 'resource'].includes(user.role); + const isCustomer = user.role === 'customer'; + + // RULE: Platform users must be on platform subdomain (not business subdomains) + if (isPlatformUser && isBusinessSubdomain) { + const port = window.location.port ? `:${window.location.port}` : ''; + window.location.href = `http://platform.lvh.me${port}/`; + return ; + } + + // RULE: Business users must be on their own business subdomain + if (isBusinessUser && isBusinessSubdomain && user.business_subdomain && user.business_subdomain !== currentSubdomain) { + const port = window.location.port ? `:${window.location.port}` : ''; + window.location.href = `http://${user.business_subdomain}.lvh.me${port}/`; + return ; + } + + // RULE: Customers must be on their business subdomain + if (isCustomer && isPlatformDomain && user.business_subdomain) { + const port = window.location.port ? `:${window.location.port}` : ''; + window.location.href = `http://${user.business_subdomain}.lvh.me${port}/`; + return ; + } + + if (isCustomer && isBusinessSubdomain && user.business_subdomain && user.business_subdomain !== currentSubdomain) { + const port = window.location.port ? `:${window.location.port}` : ''; + window.location.href = `http://${user.business_subdomain}.lvh.me${port}/`; + return ; + } + // Handlers const toggleTheme = () => setDarkMode((prev) => !prev); const handleSignOut = () => { @@ -242,22 +279,20 @@ const AppContent: React.FC = () => { }; const handleMasquerade = (targetUser: any) => { - // Call the masquerade API with the target user's username - // Fallback to email prefix if username is not available - const username = targetUser.username || targetUser.email?.split('@')[0]; - if (!username) { - console.error('Cannot masquerade: no username or email available', targetUser); + // Call the masquerade API with the target user's id + const userId = targetUser.id; + if (!userId) { + console.error('Cannot masquerade: no user id available', targetUser); return; } - masqueradeMutation.mutate(username); + // Ensure userId is a number + const userPk = typeof userId === 'string' ? parseInt(userId, 10) : userId; + masqueradeMutation.mutate(userPk); }; // Helper to check access based on roles const hasAccess = (allowedRoles: string[]) => allowedRoles.includes(user.role); - // Platform users (superuser, platform_manager, platform_support) - const isPlatformUser = ['superuser', 'platform_manager', 'platform_support'].includes(user.role); - if (isPlatformUser) { return ( @@ -329,54 +364,16 @@ const AppContent: React.FC = () => { return ; } - // Check if we're on root/platform domain without proper business context - const currentHostname = window.location.hostname; - const isRootOrPlatform = currentHostname === 'lvh.me' || currentHostname === 'localhost' || currentHostname === 'platform.lvh.me'; - // Business error or no business found if (businessError || !business) { - // If user is a business owner on root domain, redirect to their business - if (isRootOrPlatform && user.role === 'owner' && user.business_subdomain) { + // If user has a business subdomain, redirect them there + if (user.business_subdomain) { const port = window.location.port ? `:${window.location.port}` : ''; window.location.href = `http://${user.business_subdomain}.lvh.me${port}/`; return ; } - // If on root/platform and shouldn't be here, show appropriate message - if (isRootOrPlatform) { - return ( -
-
-

Wrong Location

-

- {user.business_subdomain - ? `Please access the app at your business subdomain: ${user.business_subdomain}.lvh.me` - : 'Your account is not associated with a business. Please contact support.'} -

-
- {user.business_subdomain && ( - - )} - -
-
-
- ); - } - + // No business subdomain - show error return (
diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts index 9c1b27d..ad66a94 100644 --- a/frontend/src/api/auth.ts +++ b/frontend/src/api/auth.ts @@ -86,15 +86,15 @@ export const refreshToken = async (refresh: string): Promise<{ access: string }> }; /** - * Masquerade as another user + * Masquerade as another user (hijack) */ export const masquerade = async ( - username: string, - masquerade_stack?: MasqueradeStackEntry[] + user_pk: number, + hijack_history?: MasqueradeStackEntry[] ): Promise => { const response = await apiClient.post( - `/api/users/${username}/masquerade/`, - { masquerade_stack } + '/api/auth/hijack/acquire/', + { user_pk, hijack_history } ); return response.data; }; @@ -106,7 +106,7 @@ export const stopMasquerade = async ( masquerade_stack: MasqueradeStackEntry[] ): Promise => { const response = await apiClient.post( - '/api/users/stop_masquerade/', + '/api/auth/hijack/release/', { masquerade_stack } ); return response.data; diff --git a/frontend/src/api/platform.ts b/frontend/src/api/platform.ts index 8364881..30e1699 100644 --- a/frontend/src/api/platform.ts +++ b/frontend/src/api/platform.ts @@ -5,14 +5,23 @@ import apiClient from './client'; +export interface PlatformBusinessOwner { + id: number; + username: string; + full_name: string; + email: string; + role: string; +} + export interface PlatformBusiness { id: number; name: string; subdomain: string; tier: string; is_active: boolean; - created_at: string; + created_on: string; user_count: number; + owner: PlatformBusinessOwner | null; } export interface PlatformUser { diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 852645d..c9c8568 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -34,7 +34,7 @@ const Sidebar: React.FC = ({ business, user, isCollapsed, toggleCo ? location.pathname === path : location.pathname.startsWith(path); - const baseClasses = `flex items-center gap-3 py-3 text-sm font-medium rounded-lg transition-colors`; + const baseClasses = `flex items-center gap-3 py-3 text-base font-medium rounded-lg transition-colors`; const collapsedClasses = isCollapsed ? 'px-3 justify-center' : 'px-4'; const activeClasses = 'bg-white/10 text-white'; const inactiveClasses = 'text-white/70 hover:text-white hover:bg-white/5'; @@ -70,14 +70,40 @@ const Sidebar: React.FC = ({ business, user, isCollapsed, toggleCo className={`flex items-center gap-3 w-full text-left px-6 py-8 ${isCollapsed ? 'justify-center' : ''} hover:bg-white/5 transition-colors focus:outline-none`} aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"} > -
- {business.name.substring(0, 2).toUpperCase()} -
- {!isCollapsed && ( -
-

{business.name}

-

{business.subdomain}.smoothschedule.com

+ {/* Logo-only mode: full width */} + {business.logoDisplayMode === 'logo-only' && business.logoUrl ? ( +
+ {business.name}
+ ) : ( + <> + {/* Logo/Icon display */} + {business.logoUrl && business.logoDisplayMode !== 'text-only' ? ( +
+ {business.name} +
+ ) : business.logoDisplayMode !== 'logo-only' && ( +
+ {business.name.substring(0, 2).toUpperCase()} +
+ )} + + {/* Text display */} + {!isCollapsed && business.logoDisplayMode !== 'logo-only' && ( +
+

{business.name}

+

{business.subdomain}.smoothschedule.com

+
+ )} + )} @@ -111,19 +137,22 @@ const Sidebar: React.FC = ({ business, user, isCollapsed, toggleCo {canViewAdminPages && ( <> - {business.paymentsEnabled ? ( - - - {!isCollapsed && {t('nav.payments')}} - - ) : ( -
- - {!isCollapsed && {t('nav.payments')}} -
+ {/* Payments link: always visible for owners, only visible for others if enabled */} + {(role === 'owner' || business.paymentsEnabled) && ( + business.paymentsEnabled ? ( + + + {!isCollapsed && {t('nav.payments')}} + + ) : ( +
+ + {!isCollapsed && {t('nav.payments')}} +
+ ) )} @@ -149,7 +178,12 @@ const Sidebar: React.FC = ({ business, user, isCollapsed, toggleCo
- + {/* Login Button - Hidden on mobile */} - - {t('marketing.nav.login')} - + {user ? ( + + {t('marketing.nav.login')} + + ) : ( + + {t('marketing.nav.login')} + + )} {/* Get Started CTA */} = ({ darkMode, toggleTheme }) => { ))}
- - {t('marketing.nav.login')} - + {user ? ( + + {t('marketing.nav.login')} + + ) : ( + + {t('marketing.nav.login')} + + )} { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (username: string) => { + mutationFn: async (user_pk: number) => { // Get current masquerading stack from localStorage const stackJson = localStorage.getItem('masquerade_stack'); const currentStack: MasqueradeStackEntry[] = stackJson ? JSON.parse(stackJson) : []; // Call masquerade API with current stack - return masquerade(username, currentStack); + return masquerade(user_pk, currentStack); }, onSuccess: async (data) => { // Store the updated masquerading stack diff --git a/frontend/src/hooks/useBusiness.ts b/frontend/src/hooks/useBusiness.ts index bb0e26c..5610631 100644 --- a/frontend/src/hooks/useBusiness.ts +++ b/frontend/src/hooks/useBusiness.ts @@ -33,6 +33,8 @@ export const useCurrentBusiness = () => { primaryColor: data.primary_color || '#3B82F6', // Blue-500 default secondaryColor: data.secondary_color || '#1E40AF', // Blue-800 default logoUrl: data.logo_url, + emailLogoUrl: data.email_logo_url, + logoDisplayMode: data.logo_display_mode || 'text-only', whitelabelEnabled: data.whitelabel_enabled, plan: data.tier, // Map tier to plan status: data.status, @@ -64,6 +66,8 @@ export const useUpdateBusiness = () => { if (updates.primaryColor) backendData.primary_color = updates.primaryColor; if (updates.secondaryColor) backendData.secondary_color = updates.secondaryColor; if (updates.logoUrl !== undefined) backendData.logo_url = updates.logoUrl; + if (updates.emailLogoUrl !== undefined) backendData.email_logo_url = updates.emailLogoUrl; + if (updates.logoDisplayMode !== undefined) backendData.logo_display_mode = updates.logoDisplayMode; if (updates.whitelabelEnabled !== undefined) { backendData.whitelabel_enabled = updates.whitelabelEnabled; } @@ -136,7 +140,7 @@ export const useBusinessUsers = () => { return useQuery({ queryKey: ['businessUsers'], queryFn: async () => { - const { data } = await apiClient.get('/api/business/users/'); + const { data } = await apiClient.get('/api/staff/'); return data; }, staleTime: 5 * 60 * 1000, // 5 minutes diff --git a/frontend/src/hooks/useResourceTypes.ts b/frontend/src/hooks/useResourceTypes.ts new file mode 100644 index 0000000..41735d0 --- /dev/null +++ b/frontend/src/hooks/useResourceTypes.ts @@ -0,0 +1,91 @@ +/** + * Resource Types Management Hooks + */ + +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import apiClient from '../api/client'; +import { ResourceTypeDefinition } from '../types'; + +/** + * Hook to fetch resource types for the current business + */ +export const useResourceTypes = () => { + return useQuery({ + queryKey: ['resourceTypes'], + queryFn: async () => { + const { data } = await apiClient.get('/api/resource-types/'); + return data; + }, + // Provide default types if API doesn't have them yet + placeholderData: [ + { + id: 'default-staff', + name: 'Staff', + category: 'STAFF', + isDefault: true, + }, + { + id: 'default-room', + name: 'Room', + category: 'OTHER', + isDefault: true, + }, + { + id: 'default-equipment', + name: 'Equipment', + category: 'OTHER', + isDefault: true, + }, + ] as ResourceTypeDefinition[], + }); +}; + +/** + * Hook to create a new resource type + */ +export const useCreateResourceType = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (newType: Omit) => { + const { data } = await apiClient.post('/api/resource-types/', newType); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['resourceTypes'] }); + }, + }); +}; + +/** + * Hook to update a resource type + */ +export const useUpdateResourceType = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ id, updates }: { id: string; updates: Partial }) => { + const { data } = await apiClient.patch(`/api/resource-types/${id}/`, updates); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['resourceTypes'] }); + }, + }); +}; + +/** + * Hook to delete a resource type + */ +export const useDeleteResourceType = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (id: string) => { + await apiClient.delete(`/api/resource-types/${id}/`); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['resourceTypes'] }); + }, + }); +}; diff --git a/frontend/src/hooks/useResources.ts b/frontend/src/hooks/useResources.ts index f83164c..afae0f2 100644 --- a/frontend/src/hooks/useResources.ts +++ b/frontend/src/hooks/useResources.ts @@ -28,6 +28,8 @@ export const useResources = (filters?: ResourceFilters) => { name: r.name, type: r.type as ResourceType, userId: r.user_id ? String(r.user_id) : undefined, + maxConcurrentEvents: r.max_concurrent_events ?? 1, + savedLaneCount: r.saved_lane_count, })); }, }); @@ -47,6 +49,8 @@ export const useResource = (id: string) => { name: data.name, type: data.type as ResourceType, userId: data.user_id ? String(data.user_id) : undefined, + maxConcurrentEvents: data.max_concurrent_events ?? 1, + savedLaneCount: data.saved_lane_count, }; }, enabled: !!id, @@ -91,6 +95,12 @@ export const useUpdateResource = () => { if (updates.userId !== undefined) { backendData.user = updates.userId ? parseInt(updates.userId) : null; } + if (updates.maxConcurrentEvents !== undefined) { + backendData.max_concurrent_events = updates.maxConcurrentEvents; + } + if (updates.savedLaneCount !== undefined) { + backendData.saved_lane_count = updates.savedLaneCount; + } const { data } = await apiClient.patch(`/api/resources/${id}/`, backendData); return data; diff --git a/frontend/src/hooks/useServices.ts b/frontend/src/hooks/useServices.ts index dfc1f2a..94c3ebb 100644 --- a/frontend/src/hooks/useServices.ts +++ b/frontend/src/hooks/useServices.ts @@ -22,6 +22,8 @@ export const useServices = () => { durationMinutes: s.duration || s.duration_minutes, price: parseFloat(s.price), description: s.description || '', + displayOrder: s.display_order ?? 0, + photos: s.photos || [], })); }, retry: false, // Don't retry on 404 - endpoint may not exist yet @@ -43,6 +45,8 @@ export const useService = (id: string) => { durationMinutes: data.duration || data.duration_minutes, price: parseFloat(data.price), description: data.description || '', + displayOrder: data.display_order ?? 0, + photos: data.photos || [], }; }, enabled: !!id, @@ -63,6 +67,7 @@ export const useCreateService = () => { duration: serviceData.durationMinutes, price: serviceData.price.toString(), description: serviceData.description, + photos: serviceData.photos || [], }; const { data } = await apiClient.post('/api/services/', backendData); @@ -87,6 +92,7 @@ export const useUpdateService = () => { if (updates.durationMinutes) backendData.duration = updates.durationMinutes; if (updates.price) backendData.price = updates.price.toString(); if (updates.description !== undefined) backendData.description = updates.description; + if (updates.photos !== undefined) backendData.photos = updates.photos; const { data } = await apiClient.patch(`/api/services/${id}/`, backendData); return data; @@ -112,3 +118,22 @@ export const useDeleteService = () => { }, }); }; + +/** + * Hook to reorder services (drag and drop) + */ +export const useReorderServices = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (orderedIds: string[]) => { + // Convert string IDs to numbers for the backend + const order = orderedIds.map(id => parseInt(id, 10)); + const { data } = await apiClient.post('/api/services/reorder/', { order }); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['services'] }); + }, + }); +}; diff --git a/frontend/src/hooks/useStaff.ts b/frontend/src/hooks/useStaff.ts new file mode 100644 index 0000000..c9f580b --- /dev/null +++ b/frontend/src/hooks/useStaff.ts @@ -0,0 +1,42 @@ +/** + * Staff Management Hooks + */ + +import { useQuery } from '@tanstack/react-query'; +import apiClient from '../api/client'; + +export interface StaffMember { + id: string; + name: string; + email: string; + phone?: string; +} + +interface StaffFilters { + search?: string; +} + +/** + * Hook to fetch staff members with optional filters + * Staff members are Users with roles: TENANT_OWNER, TENANT_MANAGER, TENANT_STAFF + */ +export const useStaff = (filters?: StaffFilters) => { + return useQuery({ + queryKey: ['staff', filters], + queryFn: async () => { + const params = new URLSearchParams(); + if (filters?.search) params.append('search', filters.search); + + const { data } = await apiClient.get(`/api/staff/?${params}`); + + // Transform backend format to frontend format + return data.map((s: any) => ({ + id: String(s.id), + name: s.name || `${s.first_name || ''} ${s.last_name || ''}`.trim() || s.email, + email: s.email || '', + phone: s.phone || '', + })); + }, + retry: false, + }); +}; diff --git a/frontend/src/layouts/MarketingLayout.tsx b/frontend/src/layouts/MarketingLayout.tsx index 3a064ba..abc49ae 100644 --- a/frontend/src/layouts/MarketingLayout.tsx +++ b/frontend/src/layouts/MarketingLayout.tsx @@ -3,8 +3,13 @@ import { Outlet } from 'react-router-dom'; import Navbar from '../components/marketing/Navbar'; import Footer from '../components/marketing/Footer'; import { useScrollToTop } from '../hooks/useScrollToTop'; +import { User } from '../api/auth'; -const MarketingLayout: React.FC = () => { +interface MarketingLayoutProps { + user?: User | null; +} + +const MarketingLayout: React.FC = ({ user }) => { useScrollToTop(); const [darkMode, setDarkMode] = useState(() => { @@ -28,7 +33,7 @@ const MarketingLayout: React.FC = () => { return (
- + {/* Main Content - with padding for fixed navbar */}
diff --git a/frontend/src/pages/Customers.tsx b/frontend/src/pages/Customers.tsx index a146fde..72291f5 100644 --- a/frontend/src/pages/Customers.tsx +++ b/frontend/src/pages/Customers.tsx @@ -99,7 +99,8 @@ const Customers: React.FC = ({ onMasquerade, effectiveUser }) => return sorted; }, [customers, searchTerm, sortConfig]); - const canMasquerade = ['owner', 'manager', 'staff'].includes(effectiveUser.role); + // Only owners can masquerade as customers (per backend permissions) + const canMasquerade = effectiveUser.role === 'owner'; if (isLoading) { return ( diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 7375506..cc2a65e 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -33,40 +33,68 @@ const LoginPage: React.FC = () => { const user = data.user; const currentHostname = window.location.hostname; const currentPort = window.location.port; + const portStr = currentPort ? `:${currentPort}` : ''; - // Check if we're on the root domain (no subdomain) + // Check domain type const isRootDomain = currentHostname === 'lvh.me' || currentHostname === 'localhost'; + const isPlatformDomain = currentHostname === 'platform.lvh.me'; + const currentSubdomain = currentHostname.split('.')[0]; + const isBusinessSubdomain = !isRootDomain && !isPlatformDomain && currentSubdomain !== 'api'; - // Roles allowed to login at the root domain - const rootAllowedRoles = ['superuser', 'platform_manager', 'platform_support', 'owner']; + // Platform users (superuser, platform_manager, platform_support) + const isPlatformUser = ['superuser', 'platform_manager', 'platform_support'].includes(user.role); - // If on root domain, only allow specific roles - if (isRootDomain && !rootAllowedRoles.includes(user.role)) { - setError(t('auth.loginAtSubdomain')); + // Business-associated users (owner, manager, staff, resource) + const isBusinessUser = ['owner', 'manager', 'staff', 'resource'].includes(user.role); + + // Customer users + const isCustomer = user.role === 'customer'; + + // RULE 1: Platform users cannot login on business subdomains + if (isPlatformUser && isBusinessSubdomain) { + setError(t('auth.invalidCredentials')); return; } - // Determine the correct subdomain based on user role + // RULE 2: Business users cannot login on other business subdomains + if (isBusinessUser && isBusinessSubdomain && user.business_subdomain !== currentSubdomain) { + setError(t('auth.invalidCredentials')); + return; + } + + // RULE 3: Customers cannot login on root domain (they must use their business subdomain) + if (isCustomer && isRootDomain) { + setError(t('auth.invalidCredentials')); + return; + } + + // RULE 4: Customers cannot login on platform domain + if (isCustomer && isPlatformDomain) { + setError(t('auth.invalidCredentials')); + return; + } + + // RULE 5: Customers cannot login on a different business subdomain + if (isCustomer && isBusinessSubdomain && user.business_subdomain !== currentSubdomain) { + setError(t('auth.invalidCredentials')); + return; + } + + // Determine target subdomain for redirect let targetSubdomain: string | null = null; - // Platform users (superuser, platform_manager, platform_support) - if (['superuser', 'platform_manager', 'platform_support'].includes(user.role)) { + if (isPlatformUser) { targetSubdomain = 'platform'; - } - // Business users - redirect to their business subdomain - else if (user.business_subdomain) { + } else if (user.business_subdomain) { targetSubdomain = user.business_subdomain; } // Check if we need to redirect to a different subdomain - // Need to redirect if we have a target subdomain AND we're not already on it const isOnTargetSubdomain = currentHostname === `${targetSubdomain}.lvh.me`; const needsRedirect = targetSubdomain && !isOnTargetSubdomain; if (needsRedirect) { // Pass tokens in URL to ensure they're available immediately on the new subdomain - // This avoids race conditions where cookies might not be set before the page loads - const portStr = currentPort ? `:${currentPort}` : ''; window.location.href = `http://${targetSubdomain}.lvh.me${portStr}/?access_token=${data.access}&refresh_token=${data.refresh}`; return; } diff --git a/frontend/src/pages/Resources.tsx b/frontend/src/pages/Resources.tsx index b086996..1656f8f 100644 --- a/frontend/src/pages/Resources.tsx +++ b/frontend/src/pages/Resources.tsx @@ -1,5 +1,5 @@ -import React, { useMemo, useEffect, useState, useRef } from 'react'; +import React, { useMemo, useEffect, useState, useRef, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { ResourceType, User, Resource } from '../types'; import { useResources, useCreateResource, useUpdateResource } from '../hooks/useResources'; @@ -15,7 +15,8 @@ import { Eye, Calendar, Settings, - X + X, + Pencil } from 'lucide-react'; const ResourceIcon: React.FC<{ type: ResourceType }> = ({ type }) => { @@ -55,17 +56,29 @@ const Resources: React.FC = ({ onMasquerade, effectiveUser }) => // Staff selection state const [selectedStaffId, setSelectedStaffId] = useState(null); const [staffSearchQuery, setStaffSearchQuery] = useState(''); + const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(''); const [showStaffDropdown, setShowStaffDropdown] = useState(false); + const [highlightedIndex, setHighlightedIndex] = useState(-1); const staffInputRef = useRef(null); const staffDropdownRef = useRef(null); - // Fetch staff members for autocomplete - const { data: staffMembers = [] } = useStaff({ search: staffSearchQuery }); + // Debounce search query for API calls + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearchQuery(staffSearchQuery); + }, 300); + return () => clearTimeout(timer); + }, [staffSearchQuery]); + + // Fetch staff members for autocomplete (using debounced query) + const { data: staffMembers = [] } = useStaff({ search: debouncedSearchQuery }); // Filter staff members based on search query (client-side filtering for immediate feedback) const filteredStaff = useMemo(() => { - if (!staffSearchQuery) return staffMembers; - const query = staffSearchQuery.toLowerCase(); + // Always show all staff when dropdown is open and no search query + if (!staffSearchQuery.trim()) return staffMembers; + + const query = staffSearchQuery.toLowerCase().trim(); return staffMembers.filter( (s) => s.name.toLowerCase().includes(query) || s.email.toLowerCase().includes(query) ); @@ -76,6 +89,59 @@ const Resources: React.FC = ({ onMasquerade, effectiveUser }) => return staffMembers.find((s) => s.id === selectedStaffId) || null; }, [staffMembers, selectedStaffId]); + // Get the list that's currently displayed in the dropdown + const displayedStaff = useMemo(() => { + return staffSearchQuery.trim() === '' ? staffMembers : filteredStaff; + }, [staffSearchQuery, staffMembers, filteredStaff]); + + // Reset highlighted index when filtered staff changes + useEffect(() => { + setHighlightedIndex(-1); + }, [filteredStaff]); + + // Handle keyboard navigation + const handleKeyDown = (e: React.KeyboardEvent) => { + if (!showStaffDropdown || displayedStaff.length === 0) return; + + switch (e.key) { + case 'ArrowDown': + e.preventDefault(); + setHighlightedIndex((prev) => + prev < displayedStaff.length - 1 ? prev + 1 : prev + ); + break; + case 'ArrowUp': + e.preventDefault(); + setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : -1)); + break; + case 'Enter': + e.preventDefault(); + if (highlightedIndex >= 0 && highlightedIndex < displayedStaff.length) { + const staff = displayedStaff[highlightedIndex]; + setSelectedStaffId(staff.id); + setStaffSearchQuery(staff.name); + setShowStaffDropdown(false); + setHighlightedIndex(-1); + } + break; + case 'Escape': + e.preventDefault(); + setShowStaffDropdown(false); + setHighlightedIndex(-1); + break; + } + }; + + // Scroll highlighted item into view + useEffect(() => { + if (highlightedIndex >= 0 && staffDropdownRef.current) { + const highlightedElement = staffDropdownRef.current.children[highlightedIndex] as HTMLElement; + if (highlightedElement) { + highlightedElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + } + } + }, [highlightedIndex]); + const createResourceMutation = useCreateResource(); const updateResourceMutation = useUpdateResource(); @@ -97,6 +163,8 @@ const Resources: React.FC = ({ onMasquerade, effectiveUser }) => }, [allAppointments]); // Reset form when modal opens/closes or editing resource changes + // NOTE: Only depend on editingResource and isModalOpen, NOT staffMembers + // to avoid clearing the form when staff data updates during search useEffect(() => { if (editingResource) { setFormType(editingResource.type); @@ -108,28 +176,40 @@ const Resources: React.FC = ({ onMasquerade, effectiveUser }) => // Pre-fill staff if editing a STAFF resource if (editingResource.type === 'STAFF' && editingResource.userId) { setSelectedStaffId(editingResource.userId); - // Find the staff member to set the initial search query (display name) - const staff = staffMembers.find(s => s.id === editingResource.userId); - setStaffSearchQuery(staff ? staff.name : ''); + // We'll set the staff name in a separate effect } else { setSelectedStaffId(null); setStaffSearchQuery(''); } - } else { + } else if (isModalOpen) { + // Only reset when creating new (modal opened without editing resource) setFormType('STAFF'); setFormName(''); setFormDescription(''); setFormMaxConcurrent(1); setFormMultilaneEnabled(false); setFormSavedLaneCount(undefined); - setSelectedStaffId(null); // Clear selected staff when creating new + setSelectedStaffId(null); setStaffSearchQuery(''); + setDebouncedSearchQuery(''); } - }, [editingResource, staffMembers]); + }, [editingResource, isModalOpen]); + + // Separate effect to populate staff name when editing + // This runs when staffMembers loads and we have a selected staff ID + useEffect(() => { + if (editingResource && editingResource.type === 'STAFF' && editingResource.userId && selectedStaffId === editingResource.userId) { + const staff = staffMembers.find(s => s.id === editingResource.userId); + if (staff && !staffSearchQuery) { + // Only set if not already set to avoid overwriting user input + setStaffSearchQuery(staff.name); + } + } + }, [staffMembers, editingResource, selectedStaffId, staffSearchQuery]); const openCreateModal = () => { - setEditingResource(null); setIsModalOpen(true); + setEditingResource(null); }; const openEditModal = (resource: Resource) => { @@ -138,8 +218,8 @@ const Resources: React.FC = ({ onMasquerade, effectiveUser }) => }; const closeModal = () => { - setIsModalOpen(false); setEditingResource(null); + setIsModalOpen(false); }; const handleMultilaneToggle = (enabled: boolean) => { @@ -251,8 +331,7 @@ const Resources: React.FC = ({ onMasquerade, effectiveUser }) => return ( openEditModal(resource)} + className="hover:bg-gray-50 dark:hover:bg-gray-700/30 transition-colors group" >
@@ -297,7 +376,7 @@ const Resources: React.FC = ({ onMasquerade, effectiveUser }) => -
e.stopPropagation()}> +
+
@@ -324,7 +410,7 @@ const Resources: React.FC = ({ onMasquerade, effectiveUser }) => {isModalOpen && (
-
+

{editingResource ? t('resources.editResource') : t('resources.addNewResource')} @@ -338,7 +424,7 @@ const Resources: React.FC = ({ onMasquerade, effectiveUser }) => {/* Resource Type */}
setFormData({ ...formData, name: e.target.value })} - required - className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-brand-500" - placeholder={t('services.namePlaceholder', 'e.g., Haircut, Massage, Consultation')} - /> -
- -
+
+
setFormData({ ...formData, durationMinutes: parseInt(e.target.value) || 0 })} + type="text" + value={formData.name} + onChange={(e) => setFormData({ ...formData, name: e.target.value })} required - min={5} - step={5} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-brand-500" + placeholder={t('services.namePlaceholder', 'e.g., Haircut, Massage, Consultation')} />
+ +
+
+ + setFormData({ ...formData, durationMinutes: parseInt(e.target.value) || 0 })} + required + min={5} + step={5} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-brand-500" + /> +
+
+ + setFormData({ ...formData, price: parseFloat(e.target.value) || 0 })} + required + min={0} + step={0.01} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-brand-500" + /> +
+
+
- setFormData({ ...formData, price: parseFloat(e.target.value) || 0 })} - required - min={0} - step={0.01} - className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-brand-500" +