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>
355 lines
12 KiB
Markdown
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
|