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>
12 KiB
Resource Types & Logo Upload - Implementation Summary
✅ Completed
Backend Models
-
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_defaultflag to prevent deletion of core types- Validation to prevent deletion when in use
- Database migration created and applied ✅
-
Resource Model Updates (
smoothschedule/schedule/models.py)- Added
resource_typeForeignKey to ResourceType model - Kept legacy
typefield for backwards compatibility - Migration created and applied ✅
- Added
-
Tenant Model Updates (
smoothschedule/core/models.py)- Added
logoImageField for business logo upload - Added
logo_display_modefield with choices:logo-only: Show only logo in sidebartext-only: Show only business name (default)logo-and-text: Show both logo and name
- Added
primary_colorandsecondary_colorfields - Database migration created and applied ✅
- Added
Frontend Updates
-
TypeScript Types (
frontend/src/types.ts)- Added
ResourceTypeDefinitioninterface - Added
ResourceTypeCategorytype - Added
logoDisplayModeto Business interface - Updated Resource interface with
typeIdfield
- Added
-
React Hooks (
frontend/src/hooks/useResourceTypes.ts)useResourceTypes()- Fetch resource typesuseCreateResourceType()- Create new typeuseUpdateResourceType()- Update existing typeuseDeleteResourceType()- Delete type (with validation)- Includes placeholder data for default types
-
Sidebar Component (
frontend/src/components/Sidebar.tsx)- Updated to display logo based on
logoDisplayMode - Shows logo image when mode is
logo-onlyorlogo-and-text - Hides business name text when mode is
logo-only - Maintains fallback to initials when no logo
- Updated to display logo based on
-
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:
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:
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
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
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
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)
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
{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
<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:
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
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
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
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.typefield kept for backwards compatibility