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

12 KiB

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:

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.type field kept for backwards compatibility