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

@@ -22,6 +22,8 @@ export const useServices = () => {
durationMinutes: s.duration || s.duration_minutes,
price: parseFloat(s.price),
description: s.description || '',
displayOrder: s.display_order ?? 0,
photos: s.photos || [],
}));
},
retry: false, // Don't retry on 404 - endpoint may not exist yet
@@ -43,6 +45,8 @@ export const useService = (id: string) => {
durationMinutes: data.duration || data.duration_minutes,
price: parseFloat(data.price),
description: data.description || '',
displayOrder: data.display_order ?? 0,
photos: data.photos || [],
};
},
enabled: !!id,
@@ -63,6 +67,7 @@ export const useCreateService = () => {
duration: serviceData.durationMinutes,
price: serviceData.price.toString(),
description: serviceData.description,
photos: serviceData.photos || [],
};
const { data } = await apiClient.post('/api/services/', backendData);
@@ -87,6 +92,7 @@ export const useUpdateService = () => {
if (updates.durationMinutes) backendData.duration = updates.durationMinutes;
if (updates.price) backendData.price = updates.price.toString();
if (updates.description !== undefined) backendData.description = updates.description;
if (updates.photos !== undefined) backendData.photos = updates.photos;
const { data } = await apiClient.patch(`/api/services/${id}/`, backendData);
return data;
@@ -112,3 +118,22 @@ export const useDeleteService = () => {
},
});
};
/**
* Hook to reorder services (drag and drop)
*/
export const useReorderServices = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (orderedIds: string[]) => {
// Convert string IDs to numbers for the backend
const order = orderedIds.map(id => parseInt(id, 10));
const { data } = await apiClient.post('/api/services/reorder/', { order });
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['services'] });
},
});
};