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>
129 lines
3.5 KiB
TypeScript
129 lines
3.5 KiB
TypeScript
/**
|
|
* Resource Management Hooks
|
|
*/
|
|
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import apiClient from '../api/client';
|
|
import { Resource, ResourceType } from '../types';
|
|
|
|
interface ResourceFilters {
|
|
type?: ResourceType;
|
|
}
|
|
|
|
/**
|
|
* Hook to fetch resources with optional type filter
|
|
*/
|
|
export const useResources = (filters?: ResourceFilters) => {
|
|
return useQuery<Resource[]>({
|
|
queryKey: ['resources', filters],
|
|
queryFn: async () => {
|
|
const params = new URLSearchParams();
|
|
if (filters?.type) params.append('type', filters.type);
|
|
|
|
const { data } = await apiClient.get(`/api/resources/?${params}`);
|
|
|
|
// Transform backend format to frontend format
|
|
return data.map((r: any) => ({
|
|
id: String(r.id),
|
|
name: r.name,
|
|
type: r.type as ResourceType,
|
|
userId: r.user_id ? String(r.user_id) : undefined,
|
|
maxConcurrentEvents: r.max_concurrent_events ?? 1,
|
|
savedLaneCount: r.saved_lane_count,
|
|
}));
|
|
},
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Hook to get a single resource
|
|
*/
|
|
export const useResource = (id: string) => {
|
|
return useQuery<Resource>({
|
|
queryKey: ['resources', id],
|
|
queryFn: async () => {
|
|
const { data } = await apiClient.get(`/api/resources/${id}/`);
|
|
|
|
return {
|
|
id: String(data.id),
|
|
name: data.name,
|
|
type: data.type as ResourceType,
|
|
userId: data.user_id ? String(data.user_id) : undefined,
|
|
maxConcurrentEvents: data.max_concurrent_events ?? 1,
|
|
savedLaneCount: data.saved_lane_count,
|
|
};
|
|
},
|
|
enabled: !!id,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Hook to create a resource
|
|
*/
|
|
export const useCreateResource = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async (resourceData: Omit<Resource, 'id'>) => {
|
|
const backendData = {
|
|
name: resourceData.name,
|
|
type: resourceData.type,
|
|
user: resourceData.userId ? parseInt(resourceData.userId) : null,
|
|
timezone: 'UTC', // Default timezone
|
|
};
|
|
|
|
const { data } = await apiClient.post('/api/resources/', backendData);
|
|
return data;
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['resources'] });
|
|
},
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Hook to update a resource
|
|
*/
|
|
export const useUpdateResource = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async ({ id, updates }: { id: string; updates: Partial<Resource> }) => {
|
|
const backendData: any = {};
|
|
if (updates.name) backendData.name = updates.name;
|
|
if (updates.type) backendData.type = updates.type;
|
|
if (updates.userId !== undefined) {
|
|
backendData.user = updates.userId ? parseInt(updates.userId) : null;
|
|
}
|
|
if (updates.maxConcurrentEvents !== undefined) {
|
|
backendData.max_concurrent_events = updates.maxConcurrentEvents;
|
|
}
|
|
if (updates.savedLaneCount !== undefined) {
|
|
backendData.saved_lane_count = updates.savedLaneCount;
|
|
}
|
|
|
|
const { data } = await apiClient.patch(`/api/resources/${id}/`, backendData);
|
|
return data;
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['resources'] });
|
|
},
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Hook to delete a resource
|
|
*/
|
|
export const useDeleteResource = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async (id: string) => {
|
|
await apiClient.delete(`/api/resources/${id}/`);
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['resources'] });
|
|
},
|
|
});
|
|
};
|