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>
140 lines
3.8 KiB
TypeScript
140 lines
3.8 KiB
TypeScript
/**
|
|
* Service Management Hooks
|
|
*/
|
|
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import apiClient from '../api/client';
|
|
import { Service } from '../types';
|
|
|
|
/**
|
|
* Hook to fetch all services for current business
|
|
*/
|
|
export const useServices = () => {
|
|
return useQuery<Service[]>({
|
|
queryKey: ['services'],
|
|
queryFn: async () => {
|
|
const { data } = await apiClient.get('/api/services/');
|
|
|
|
// Transform backend format to frontend format
|
|
return data.map((s: any) => ({
|
|
id: String(s.id),
|
|
name: s.name,
|
|
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
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Hook to get a single service
|
|
*/
|
|
export const useService = (id: string) => {
|
|
return useQuery<Service>({
|
|
queryKey: ['services', id],
|
|
queryFn: async () => {
|
|
const { data } = await apiClient.get(`/api/services/${id}/`);
|
|
|
|
return {
|
|
id: String(data.id),
|
|
name: data.name,
|
|
durationMinutes: data.duration || data.duration_minutes,
|
|
price: parseFloat(data.price),
|
|
description: data.description || '',
|
|
displayOrder: data.display_order ?? 0,
|
|
photos: data.photos || [],
|
|
};
|
|
},
|
|
enabled: !!id,
|
|
retry: false,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Hook to create a service
|
|
*/
|
|
export const useCreateService = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async (serviceData: Omit<Service, 'id'>) => {
|
|
const backendData = {
|
|
name: serviceData.name,
|
|
duration: serviceData.durationMinutes,
|
|
price: serviceData.price.toString(),
|
|
description: serviceData.description,
|
|
photos: serviceData.photos || [],
|
|
};
|
|
|
|
const { data } = await apiClient.post('/api/services/', backendData);
|
|
return data;
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['services'] });
|
|
},
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Hook to update a service
|
|
*/
|
|
export const useUpdateService = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async ({ id, updates }: { id: string; updates: Partial<Service> }) => {
|
|
const backendData: any = {};
|
|
if (updates.name) backendData.name = updates.name;
|
|
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;
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['services'] });
|
|
},
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Hook to delete a service
|
|
*/
|
|
export const useDeleteService = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async (id: string) => {
|
|
await apiClient.delete(`/api/services/${id}/`);
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['services'] });
|
|
},
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 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'] });
|
|
},
|
|
});
|
|
};
|