282 lines
7.6 KiB
Markdown
282 lines
7.6 KiB
Markdown
|
|
# 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
|
||
|
|
|
||
|
|
<Tab name="Resource Types">
|
||
|
|
<ResourceTypesManager />
|
||
|
|
</Tab>
|
||
|
|
```
|
||
|
|
|
||
|
|
### ResourceTypesManager Component
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
interface ResourceTypesManagerProps {}
|
||
|
|
|
||
|
|
const ResourceTypesManager: React.FC = () => {
|
||
|
|
const { data: types = [] } = useResourceTypes();
|
||
|
|
const createMutation = useCreateResourceType();
|
||
|
|
const updateMutation = useUpdateResourceType();
|
||
|
|
const deleteMutation = useDeleteResourceType();
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div>
|
||
|
|
<h3>Resource Types</h3>
|
||
|
|
<p>Customize how you categorize your bookable resources.</p>
|
||
|
|
|
||
|
|
{/* List of types */}
|
||
|
|
{types.map(type => (
|
||
|
|
<TypeCard
|
||
|
|
key={type.id}
|
||
|
|
type={type}
|
||
|
|
onEdit={handleEdit}
|
||
|
|
onDelete={handleDelete}
|
||
|
|
canDelete={!type.isDefault && !type.inUse}
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
|
||
|
|
{/* Add new type button */}
|
||
|
|
<button onClick={openCreateModal}>
|
||
|
|
+ Add Resource Type
|
||
|
|
</button>
|
||
|
|
|
||
|
|
{/* Create/Edit Modal */}
|
||
|
|
<ResourceTypeModal
|
||
|
|
type={editingType}
|
||
|
|
onSave={handleSave}
|
||
|
|
onClose={closeModal}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Logo Upload in Branding Section
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// In Settings.tsx, Branding section
|
||
|
|
|
||
|
|
<div className="space-y-4">
|
||
|
|
<h4>Business Logo</h4>
|
||
|
|
|
||
|
|
{/* Logo preview */}
|
||
|
|
{business.logoUrl && (
|
||
|
|
<div className="relative w-32 h-32">
|
||
|
|
<img src={business.logoUrl} alt="Business logo" />
|
||
|
|
<button onClick={removeLogo}>
|
||
|
|
<X size={16} />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Upload button */}
|
||
|
|
<input
|
||
|
|
type="file"
|
||
|
|
accept="image/png,image/jpeg,image/svg+xml"
|
||
|
|
onChange={handleLogoUpload}
|
||
|
|
className="hidden"
|
||
|
|
id="logo-upload"
|
||
|
|
/>
|
||
|
|
<label htmlFor="logo-upload">
|
||
|
|
<Upload size={16} />
|
||
|
|
Upload Logo
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Update Resources.tsx to use custom types
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// Resources.tsx
|
||
|
|
|
||
|
|
const Resources: React.FC = () => {
|
||
|
|
const { data: resourceTypes = [] } = useResourceTypes();
|
||
|
|
|
||
|
|
return (
|
||
|
|
<ResourceModal>
|
||
|
|
<select name="resourceType">
|
||
|
|
{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' && (
|
||
|
|
<StaffSelector required />
|
||
|
|
)}
|
||
|
|
</ResourceModal>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
## 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)
|