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>
This commit is contained in:
153
frontend/src/hooks/useLocations.ts
Normal file
153
frontend/src/hooks/useLocations.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* 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 };
|
||||
};
|
||||
Reference in New Issue
Block a user