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

@@ -2,11 +2,10 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Search, Filter, MoreHorizontal, Eye, ShieldCheck, Ban } from 'lucide-react';
import { User } from '../../types';
import { useBusinesses } from '../../hooks/usePlatform';
interface PlatformBusinessesProps {
onMasquerade: (targetUser: User) => void;
onMasquerade: (targetUser: { id: number; username?: string; name?: string; email?: string; role?: string }) => void;
}
const PlatformBusinesses: React.FC<PlatformBusinessesProps> = ({ onMasquerade }) => {
@@ -22,19 +21,14 @@ const PlatformBusinesses: React.FC<PlatformBusinessesProps> = ({ onMasquerade })
const handleLoginAs = (business: any) => {
// Use the owner data from the API response
if (business.owner) {
const targetOwner: User = {
id: business.owner.id.toString(),
// Pass owner info to masquerade - we only need the id
onMasquerade({
id: business.owner.id,
username: business.owner.username,
name: business.owner.name,
name: business.owner.full_name,
email: business.owner.email,
role: business.owner.role,
business_id: business.id.toString(),
business_subdomain: business.subdomain,
is_active: true,
is_staff: false,
is_superuser: false,
};
onMasquerade(targetOwner);
});
}
};
@@ -130,14 +124,14 @@ const PlatformBusinesses: React.FC<PlatformBusinessesProps> = ({ onMasquerade })
</div>
</td>
<td className="px-6 py-4 text-gray-500 dark:text-gray-400">
{new Date(biz.created_at).toLocaleDateString()}
{new Date(biz.created_on).toLocaleDateString()}
</td>
<td className="px-6 py-4 text-right">
<button
onClick={() => handleLoginAs(biz)}
className="text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300 font-medium text-xs inline-flex items-center gap-1 px-3 py-1 border border-indigo-200 dark:border-indigo-800 rounded-lg hover:bg-indigo-50 dark:hover:bg-indigo-900/30 transition-colors mr-2"
disabled={!biz.owner}
title={!biz.owner ? 'No owner assigned' : `Masquerade as ${biz.owner.name}`}
title={!biz.owner ? 'No owner assigned' : `Masquerade as ${biz.owner.full_name}`}
>
<Eye size={14} /> {t('platform.masquerade')}
</button>

View File

@@ -2,11 +2,10 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Search, Filter, Eye, Shield, User as UserIcon } from 'lucide-react';
import { User } from '../../types';
import { usePlatformUsers } from '../../hooks/usePlatform';
interface PlatformUsersProps {
onMasquerade: (targetUser: User) => void;
onMasquerade: (targetUser: { id: number; username?: string; name?: string; email?: string; role?: string }) => void;
}
const PlatformUsers: React.FC<PlatformUsersProps> = ({ onMasquerade }) => {
@@ -36,20 +35,14 @@ const PlatformUsers: React.FC<PlatformUsersProps> = ({ onMasquerade }) => {
};
const handleMasquerade = (platformUser: any) => {
// Convert platform user to User type for masquerade
const targetUser: User = {
id: platformUser.id.toString(),
// Pass user info to masquerade - we only need the id
onMasquerade({
id: platformUser.id,
username: platformUser.username,
name: platformUser.name || platformUser.username,
name: platformUser.full_name || platformUser.username,
email: platformUser.email,
role: platformUser.role || 'customer',
business_id: platformUser.business?.toString() || null,
business_subdomain: platformUser.business_subdomain || null,
is_active: platformUser.is_active,
is_staff: platformUser.is_staff,
is_superuser: platformUser.is_superuser,
};
onMasquerade(targetUser);
});
};
if (isLoading) {