- Add deposit_percent field back to Service model for percentage-based deposits - Reorganize service form: variable pricing toggle at top, deposit toggle with amount/percent options (percent only available for fixed pricing) - Disable price field when variable pricing is enabled - Add backend validation: variable pricing cannot use percentage deposits - Update frontend types and hooks to handle deposit_percent field 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
174 lines
5.3 KiB
TypeScript
174 lines
5.3 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('/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 || [],
|
|
// Pricing fields
|
|
variable_pricing: s.variable_pricing ?? false,
|
|
deposit_amount: s.deposit_amount ? parseFloat(s.deposit_amount) : null,
|
|
deposit_percent: s.deposit_percent ? parseFloat(s.deposit_percent) : null,
|
|
requires_deposit: s.requires_deposit ?? false,
|
|
requires_saved_payment_method: s.requires_saved_payment_method ?? false,
|
|
deposit_display: s.deposit_display || null,
|
|
}));
|
|
},
|
|
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(`/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,
|
|
});
|
|
};
|
|
|
|
// Input type for creating/updating services (not all Service fields required)
|
|
interface ServiceInput {
|
|
name: string;
|
|
durationMinutes: number;
|
|
price: number;
|
|
description?: string;
|
|
photos?: string[];
|
|
variable_pricing?: boolean;
|
|
deposit_amount?: number | null;
|
|
deposit_percent?: number | null;
|
|
}
|
|
|
|
/**
|
|
* Hook to create a service
|
|
*/
|
|
export const useCreateService = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async (serviceData: ServiceInput) => {
|
|
const backendData: Record<string, any> = {
|
|
name: serviceData.name,
|
|
duration: serviceData.durationMinutes,
|
|
price: serviceData.price.toString(),
|
|
description: serviceData.description || '',
|
|
photos: serviceData.photos || [],
|
|
};
|
|
|
|
// Add pricing fields
|
|
if (serviceData.variable_pricing !== undefined) {
|
|
backendData.variable_pricing = serviceData.variable_pricing;
|
|
}
|
|
if (serviceData.deposit_amount !== undefined) {
|
|
backendData.deposit_amount = serviceData.deposit_amount;
|
|
}
|
|
if (serviceData.deposit_percent !== undefined) {
|
|
backendData.deposit_percent = serviceData.deposit_percent;
|
|
}
|
|
|
|
const { data } = await apiClient.post('/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<ServiceInput> }) => {
|
|
const backendData: Record<string, any> = {};
|
|
if (updates.name) backendData.name = updates.name;
|
|
if (updates.durationMinutes) backendData.duration = updates.durationMinutes;
|
|
if (updates.price !== undefined) backendData.price = updates.price.toString();
|
|
if (updates.description !== undefined) backendData.description = updates.description;
|
|
if (updates.photos !== undefined) backendData.photos = updates.photos;
|
|
// Pricing fields
|
|
if (updates.variable_pricing !== undefined) backendData.variable_pricing = updates.variable_pricing;
|
|
if (updates.deposit_amount !== undefined) backendData.deposit_amount = updates.deposit_amount;
|
|
if (updates.deposit_percent !== undefined) backendData.deposit_percent = updates.deposit_percent;
|
|
|
|
const { data } = await apiClient.patch(`/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(`/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('/services/reorder/', { order });
|
|
return data;
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['services'] });
|
|
},
|
|
});
|
|
};
|