Files
smoothschedule/frontend/src/hooks/useLocations.ts
poduck b384d9912a Add TenantCustomTier system and fix BusinessEditModal feature loading
Backend:
- Add TenantCustomTier model for per-tenant feature overrides
- Update EntitlementService to check custom tier before plan features
- Add custom_tier action on TenantViewSet (GET/PUT/DELETE)
- Add Celery task for grace period management (30-day expiry)

Frontend:
- Add DynamicFeaturesEditor component for dynamic feature management
- Fix BusinessEditModal to load features from plan defaults when no custom tier
- Update limits (max_users, max_resources, etc.) to use featureValues
- Remove outdated canonical feature check from FeaturePicker (removes warning icons)
- Add useBillingPlans hook for accessing billing system data
- Add custom tier API functions to platform.ts

Features now follow consistent rules:
- Load from plan defaults when no custom tier exists
- Load from custom tier when one exists
- Reset to plan defaults when plan changes
- Save to custom tier on edit

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 21:00:54 -05:00

154 lines
3.6 KiB
TypeScript

/**
* Location Management Hooks
*
* Provides hooks for managing business locations in a multi-location setup.
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import apiClient from '../api/client';
import { Location } from '../types';
interface LocationFilters {
includeInactive?: boolean;
}
/**
* Hook to fetch locations with optional inactive filter
*/
export const useLocations = (filters?: LocationFilters) => {
return useQuery<Location[]>({
queryKey: ['locations', filters],
queryFn: async () => {
let url = '/locations/';
if (filters?.includeInactive) {
url += '?include_inactive=true';
}
const { data } = await apiClient.get(url);
return data;
},
});
};
/**
* Hook to get a single location by ID
*/
export const useLocation = (id: number | undefined) => {
return useQuery<Location>({
queryKey: ['locations', id],
queryFn: async () => {
const { data } = await apiClient.get(`/locations/${id}/`);
return data;
},
enabled: id !== undefined,
});
};
/**
* Hook to create a new location
*/
export const useCreateLocation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (locationData: Partial<Location>) => {
const { data } = await apiClient.post('/locations/', locationData);
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['locations'] });
},
});
};
/**
* Hook to update a location
*/
export const useUpdateLocation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, updates }: { id: number; updates: Partial<Location> }) => {
const { data } = await apiClient.patch(`/locations/${id}/`, updates);
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['locations'] });
},
});
};
/**
* Hook to delete a location
*/
export const useDeleteLocation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: number) => {
await apiClient.delete(`/locations/${id}/`);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['locations'] });
},
});
};
/**
* Hook to set a location as primary
*/
export const useSetPrimaryLocation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: number) => {
const { data } = await apiClient.post(`/locations/${id}/set_primary/`);
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['locations'] });
},
});
};
/**
* Hook to activate or deactivate a location
*/
export const useSetLocationActive = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, isActive }: { id: number; isActive: boolean }) => {
const { data } = await apiClient.post(`/locations/${id}/set_active/`, {
is_active: isActive,
});
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['locations'] });
},
});
};
/**
* Hook to get only active locations (convenience wrapper)
*/
export const useActiveLocations = () => {
return useLocations();
};
/**
* Hook to get all locations including inactive
*/
export const useAllLocations = () => {
return useLocations({ includeInactive: true });
};
/**
* Hook to get the primary location
*/
export const usePrimaryLocation = () => {
const { data: locations, ...rest } = useLocations();
const primaryLocation = locations?.find(loc => loc.is_primary);
return { data: primaryLocation, locations, ...rest };
};