Files
smoothschedule/IMPLEMENTATION_SUMMARY.md
poduck b10426fbdb feat: Add photo galleries to services, resource types management, and UI improvements
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 <noreply@anthropic.com>
2025-11-28 01:11:53 -05:00

355 lines
12 KiB
Markdown

# 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' && (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold">Resource Types</h3>
<p className="text-gray-600">Customize how you categorize your bookable resources.</p>
</div>
<ResourceTypesManager />
</div>
)}
```
#### B. Logo Upload in Branding Section
```tsx
<div className="space-y-4">
<h4 className="font-semibold">Business Logo</h4>
{/* Preview */}
{business.logoUrl ? (
<div className="relative inline-block">
<img src={business.logoUrl} alt="Logo" className="w-32 h-32 object-contain border rounded" />
<button
onClick={handleRemoveLogo}
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1"
>
<X size={16} />
</button>
</div>
) : (
<div className="w-32 h-32 border-2 border-dashed rounded flex items-center justify-center text-gray-400">
<Image size={32} />
</div>
)}
{/* Upload */}
<input
type="file"
id="logo-upload"
className="hidden"
accept="image/*"
onChange={handleLogoUpload}
/>
<label htmlFor="logo-upload" className="btn-secondary">
<Upload size={16} />
Upload Logo
</label>
{/* Display Mode */}
<div>
<label className="block font-medium mb-2">Logo Display Mode</label>
<select
value={business.logoDisplayMode}
onChange={(e) => handleUpdateBusiness({ logoDisplayMode: e.target.value })}
className="form-select"
>
<option value="text-only">Text Only</option>
<option value="logo-only">Logo Only</option>
<option value="logo-and-text">Logo and Text</option>
</select>
</div>
{/* Preview with menu background */}
<div>
<label className="block font-medium mb-2">Preview in Sidebar</label>
<div
className="w-64 p-4 rounded-lg"
style={{ backgroundColor: business.primaryColor }}
>
<div className="flex items-center gap-3">
{business.logoUrl && (business.logoDisplayMode === 'logo-only' || business.logoDisplayMode === 'logo-and-text') ? (
<img src={business.logoUrl} alt="Logo" className="w-10 h-10 object-contain" />
) : (
<div className="w-10 h-10 bg-white rounded-lg flex items-center justify-center text-brand-600 font-bold">
{business.name.substring(0, 2).toUpperCase()}
</div>
)}
{business.logoDisplayMode !== 'logo-only' && (
<div>
<h1 className="font-bold text-white truncate">{business.name}</h1>
<p className="text-xs text-white/60 truncate">{business.subdomain}.smoothschedule.com</p>
</div>
)}
</div>
</div>
</div>
</div>
```
### 5. Update Resources Page
Modify `frontend/src/pages/Resources.tsx` to use custom resource types:
```tsx
const { data: resourceTypes = [] } = useResourceTypes();
// In the modal:
<select value={selectedTypeId} onChange={handleTypeChange}>
{resourceTypes.map(type => (
<option key={type.id} value={type.id}>
{type.name}
</option>
))}
</select>
{/* Show staff selector if selected type is STAFF category */}
{selectedType?.category === 'STAFF' && (
<StaffAutocomplete />
)}
```
## 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