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>
This commit is contained in:
poduck
2025-11-28 01:11:53 -05:00
parent a7c756a8ec
commit b10426fbdb
52 changed files with 4259 additions and 356 deletions

View File

@@ -6,13 +6,60 @@ API endpoints for Resources and Events with quota enforcement.
from rest_framework import viewsets, status
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.response import Response
from .models import Resource, Event, Participant
from .serializers import ResourceSerializer, EventSerializer, ParticipantSerializer, CustomerSerializer, ServiceSerializer
from rest_framework.decorators import action
from django.core.exceptions import ValidationError as DjangoValidationError
from .models import Resource, Event, Participant, ResourceType
from .serializers import (
ResourceSerializer, EventSerializer, ParticipantSerializer,
CustomerSerializer, ServiceSerializer, ResourceTypeSerializer, StaffSerializer
)
from .models import Service
from core.permissions import HasQuota
from smoothschedule.users.models import User
class ResourceTypeViewSet(viewsets.ModelViewSet):
"""
API endpoint for managing custom Resource Types.
Permissions:
- Must be authenticated
- Only owners/managers can create/update/delete
Functionality:
- List all resource types
- Create new custom types
- Update existing types (except is_default flag)
- Delete types (only if not default and not in use)
"""
queryset = ResourceType.objects.all()
serializer_class = ResourceTypeSerializer
permission_classes = [AllowAny] # TODO: Change to IsAuthenticated for production
ordering = ['name']
def destroy(self, request, *args, **kwargs):
"""Override destroy to add validation"""
instance = self.get_object()
# Check if default
if instance.is_default:
return Response(
{'error': 'Cannot delete default resource types.'},
status=status.HTTP_400_BAD_REQUEST
)
# Check if in use
if instance.resources.exists():
return Response(
{
'error': f"Cannot delete resource type '{instance.name}' because it is in use by {instance.resources.count()} resource(s)."
},
status=status.HTTP_400_BAD_REQUEST
)
return super().destroy(request, *args, **kwargs)
class ResourceViewSet(viewsets.ModelViewSet):
"""
API endpoint for managing Resources.
@@ -198,8 +245,8 @@ class ServiceViewSet(viewsets.ModelViewSet):
filterset_fields = ['is_active']
search_fields = ['name', 'description']
ordering_fields = ['name', 'price', 'duration', 'created_at']
ordering = ['name']
ordering_fields = ['name', 'price', 'duration', 'display_order', 'created_at']
ordering = ['display_order', 'name']
def get_queryset(self):
"""Return services, optionally including inactive ones."""
@@ -211,3 +258,71 @@ class ServiceViewSet(viewsets.ModelViewSet):
queryset = queryset.filter(is_active=True)
return queryset
@action(detail=False, methods=['post'])
def reorder(self, request):
"""
Bulk update service display order.
Expects: { "order": [1, 3, 2, 5, 4] }
Where the list contains service IDs in the desired display order.
"""
order = request.data.get('order', [])
if not isinstance(order, list):
return Response(
{'error': 'order must be a list of service IDs'},
status=status.HTTP_400_BAD_REQUEST
)
# Update display_order for each service
for index, service_id in enumerate(order):
Service.objects.filter(id=service_id).update(display_order=index)
return Response({'status': 'ok', 'updated': len(order)})
class StaffViewSet(viewsets.ReadOnlyModelViewSet):
"""
API endpoint for listing staff members (Users who can be assigned to resources).
Staff members are Users with roles: TENANT_OWNER, TENANT_MANAGER, TENANT_STAFF.
This endpoint is read-only for assigning staff to resources.
"""
serializer_class = StaffSerializer
# TODO: Re-enable authentication for production
permission_classes = [AllowAny]
search_fields = ['email', 'first_name', 'last_name']
ordering_fields = ['email', 'first_name', 'last_name']
ordering = ['first_name', 'last_name']
def get_queryset(self):
"""
Return staff members for the current tenant.
Staff are Users with roles: TENANT_OWNER, TENANT_MANAGER, TENANT_STAFF.
"""
from django.db.models import Q
queryset = User.objects.filter(
Q(role=User.Role.TENANT_OWNER) |
Q(role=User.Role.TENANT_MANAGER) |
Q(role=User.Role.TENANT_STAFF)
).filter(is_active=True)
# Filter by tenant if user is authenticated and has a tenant
# TODO: Re-enable this when authentication is enabled
# if self.request.user.is_authenticated and self.request.user.tenant:
# queryset = queryset.filter(tenant=self.request.user.tenant)
# Apply search filter if provided
search = self.request.query_params.get('search')
if search:
queryset = queryset.filter(
Q(email__icontains=search) |
Q(first_name__icontains=search) |
Q(last_name__icontains=search)
)
return queryset