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>
154 lines
3.6 KiB
TypeScript
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 };
|
|
};
|