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>
169 lines
6.4 KiB
Python
169 lines
6.4 KiB
Python
"""
|
|
API views for business/tenant management
|
|
"""
|
|
import base64
|
|
import uuid
|
|
from django.core.files.base import ContentFile
|
|
from rest_framework import status
|
|
from rest_framework.decorators import api_view, permission_classes
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.response import Response
|
|
|
|
|
|
@api_view(['GET'])
|
|
@permission_classes([IsAuthenticated])
|
|
def current_business_view(request):
|
|
"""
|
|
Get current business (tenant) for the authenticated user
|
|
GET /api/business/current/
|
|
|
|
Returns null if user is a platform user without a tenant
|
|
"""
|
|
user = request.user
|
|
tenant = user.tenant
|
|
|
|
# Platform users don't have a tenant
|
|
if not tenant:
|
|
return Response(None, status=status.HTTP_200_OK)
|
|
|
|
# Get subdomain from primary domain
|
|
subdomain = None
|
|
primary_domain = tenant.domains.filter(is_primary=True).first()
|
|
if primary_domain:
|
|
# Extract subdomain from domain (e.g., "mybusiness.lvh.me" -> "mybusiness")
|
|
domain_parts = primary_domain.domain.split('.')
|
|
if len(domain_parts) > 0:
|
|
subdomain = domain_parts[0]
|
|
|
|
business_data = {
|
|
'id': tenant.id,
|
|
'name': tenant.name,
|
|
'subdomain': subdomain or tenant.schema_name,
|
|
'tier': tenant.subscription_tier,
|
|
'status': 'active' if tenant.is_active else 'inactive',
|
|
'created_at': tenant.created_on.isoformat() if tenant.created_on else None,
|
|
# Branding fields from Tenant model
|
|
'primary_color': tenant.primary_color,
|
|
'secondary_color': tenant.secondary_color,
|
|
'logo_url': request.build_absolute_uri(tenant.logo.url) if tenant.logo else None,
|
|
'email_logo_url': request.build_absolute_uri(tenant.email_logo.url) if tenant.email_logo else None,
|
|
'logo_display_mode': tenant.logo_display_mode,
|
|
# Other optional fields with defaults
|
|
'whitelabel_enabled': False,
|
|
'resources_can_reschedule': False,
|
|
'require_payment_method_to_book': False,
|
|
'cancellation_window_hours': 24,
|
|
'late_cancellation_fee_percent': 0,
|
|
'initial_setup_complete': False,
|
|
'website_pages': {},
|
|
'customer_dashboard_content': [],
|
|
}
|
|
|
|
return Response(business_data, status=status.HTTP_200_OK)
|
|
|
|
|
|
@api_view(['PATCH'])
|
|
@permission_classes([IsAuthenticated])
|
|
def update_business_view(request):
|
|
"""
|
|
Update business (tenant) settings for the authenticated user
|
|
PATCH /api/business/current/update/
|
|
|
|
Only business owners can update settings
|
|
"""
|
|
user = request.user
|
|
tenant = user.tenant
|
|
|
|
# Platform users don't have a tenant
|
|
if not tenant:
|
|
return Response({'error': 'No business found'}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
# Only owners can update business settings
|
|
if user.role.lower() != 'tenant_owner':
|
|
return Response({'error': 'Only business owners can update settings'}, status=status.HTTP_403_FORBIDDEN)
|
|
|
|
# Update fields if provided in request
|
|
if 'name' in request.data:
|
|
tenant.name = request.data['name']
|
|
|
|
if 'primary_color' in request.data:
|
|
tenant.primary_color = request.data['primary_color']
|
|
|
|
if 'secondary_color' in request.data:
|
|
tenant.secondary_color = request.data['secondary_color']
|
|
|
|
if 'logo_display_mode' in request.data:
|
|
tenant.logo_display_mode = request.data['logo_display_mode']
|
|
|
|
# Handle logo uploads (base64 data URLs)
|
|
if 'logo_url' in request.data:
|
|
logo_data = request.data['logo_url']
|
|
if logo_data and logo_data.startswith('data:image'):
|
|
# Extract base64 data and file extension
|
|
format_str, imgstr = logo_data.split(';base64,')
|
|
ext = format_str.split('/')[-1]
|
|
# Decode base64 and create Django file
|
|
data = ContentFile(base64.b64decode(imgstr), name=f'logo_{uuid.uuid4()}.{ext}')
|
|
# Delete old logo if exists
|
|
if tenant.logo:
|
|
tenant.logo.delete(save=False)
|
|
tenant.logo = data
|
|
elif logo_data is None or logo_data == '':
|
|
# Remove logo if set to None or empty string
|
|
if tenant.logo:
|
|
tenant.logo.delete(save=False)
|
|
tenant.logo = None
|
|
|
|
if 'email_logo_url' in request.data:
|
|
email_logo_data = request.data['email_logo_url']
|
|
if email_logo_data and email_logo_data.startswith('data:image'):
|
|
# Extract base64 data and file extension
|
|
format_str, imgstr = email_logo_data.split(';base64,')
|
|
ext = format_str.split('/')[-1]
|
|
# Decode base64 and create Django file
|
|
data = ContentFile(base64.b64decode(imgstr), name=f'email_logo_{uuid.uuid4()}.{ext}')
|
|
# Delete old email logo if exists
|
|
if tenant.email_logo:
|
|
tenant.email_logo.delete(save=False)
|
|
tenant.email_logo = data
|
|
elif email_logo_data is None or email_logo_data == '':
|
|
# Remove email logo if set to None or empty string
|
|
if tenant.email_logo:
|
|
tenant.email_logo.delete(save=False)
|
|
tenant.email_logo = None
|
|
|
|
# Save the tenant
|
|
tenant.save()
|
|
|
|
# Return updated business data
|
|
subdomain = None
|
|
primary_domain = tenant.domains.filter(is_primary=True).first()
|
|
if primary_domain:
|
|
domain_parts = primary_domain.domain.split('.')
|
|
if len(domain_parts) > 0:
|
|
subdomain = domain_parts[0]
|
|
|
|
business_data = {
|
|
'id': tenant.id,
|
|
'name': tenant.name,
|
|
'subdomain': subdomain or tenant.schema_name,
|
|
'tier': tenant.subscription_tier,
|
|
'status': 'active' if tenant.is_active else 'inactive',
|
|
'created_at': tenant.created_on.isoformat() if tenant.created_on else None,
|
|
'primary_color': tenant.primary_color,
|
|
'secondary_color': tenant.secondary_color,
|
|
'logo_url': request.build_absolute_uri(tenant.logo.url) if tenant.logo else None,
|
|
'email_logo_url': request.build_absolute_uri(tenant.email_logo.url) if tenant.email_logo else None,
|
|
'logo_display_mode': tenant.logo_display_mode,
|
|
'whitelabel_enabled': False,
|
|
'resources_can_reschedule': False,
|
|
'require_payment_method_to_book': False,
|
|
'cancellation_window_hours': 24,
|
|
'late_cancellation_fee_percent': 0,
|
|
'initial_setup_complete': False,
|
|
'website_pages': {},
|
|
'customer_dashboard_content': [],
|
|
}
|
|
|
|
return Response(business_data, status=status.HTTP_200_OK)
|