Add demo tenant reseed, staff roles, and fix masquerade redirect
Demo Tenant: - Add block_emails field to Tenant model for demo accounts - Add is_email_blocked() and wrapper functions in email_service - Create reseed_demo management command with salon/spa theme - Add Celery beat task for daily reseed at midnight UTC - Create 100 appointments, 20 customers, 13 services, 12 resources Staff Roles: - Add StaffRole model with permission toggles - Create default roles: Full Access, Front Desk, Limited Staff - Add StaffRolesSettings page and hooks - Integrate role assignment in Staff management Bug Fixes: - Fix masquerade redirect using wrong role names (tenant_owner vs owner) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -122,9 +122,11 @@ export const useIsAuthenticated = (): boolean => {
|
||||
/**
|
||||
* Get the redirect path based on user role
|
||||
* Tenant users go to /dashboard/, platform users go to /
|
||||
* Note: Backend maps tenant_owner -> owner, tenant_manager -> manager, etc.
|
||||
*/
|
||||
const getRedirectPathForRole = (role: string): string => {
|
||||
const tenantRoles = ['tenant_owner', 'tenant_manager', 'tenant_staff'];
|
||||
// Tenant roles (as returned by backend after role mapping)
|
||||
const tenantRoles = ['owner', 'manager', 'staff', 'customer'];
|
||||
if (tenantRoles.includes(role)) {
|
||||
return '/dashboard/';
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ export interface StaffInvitation {
|
||||
create_bookable_resource: boolean;
|
||||
resource_name: string;
|
||||
permissions: Record<string, boolean>;
|
||||
staff_role_id: number | null;
|
||||
staff_role_name: string | null;
|
||||
}
|
||||
|
||||
export interface InvitationDetails {
|
||||
@@ -52,6 +54,7 @@ export interface CreateInvitationData {
|
||||
create_bookable_resource?: boolean;
|
||||
resource_name?: string;
|
||||
permissions?: StaffPermissions;
|
||||
staff_role_id?: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,7 @@ import apiClient from '../api/client';
|
||||
|
||||
export interface StaffPermissions {
|
||||
can_invite_staff?: boolean;
|
||||
[key: string]: boolean | undefined;
|
||||
}
|
||||
|
||||
export interface StaffMember {
|
||||
@@ -18,6 +19,9 @@ export interface StaffMember {
|
||||
is_active: boolean;
|
||||
permissions: StaffPermissions;
|
||||
can_invite_staff: boolean;
|
||||
staff_role_id: number | null;
|
||||
staff_role_name: string | null;
|
||||
effective_permissions: Record<string, boolean>;
|
||||
}
|
||||
|
||||
interface StaffFilters {
|
||||
@@ -48,6 +52,9 @@ export const useStaff = (filters?: StaffFilters) => {
|
||||
is_active: s.is_active ?? true,
|
||||
permissions: s.permissions || {},
|
||||
can_invite_staff: s.can_invite_staff ?? false,
|
||||
staff_role_id: s.staff_role_id ?? null,
|
||||
staff_role_name: s.staff_role_name ?? null,
|
||||
effective_permissions: s.effective_permissions || {},
|
||||
}));
|
||||
},
|
||||
retry: false,
|
||||
@@ -66,7 +73,7 @@ export const useUpdateStaff = () => {
|
||||
updates,
|
||||
}: {
|
||||
id: string;
|
||||
updates: { is_active?: boolean; permissions?: StaffPermissions };
|
||||
updates: { is_active?: boolean; permissions?: StaffPermissions; staff_role_id?: number | null };
|
||||
}) => {
|
||||
const { data } = await apiClient.patch(`/staff/${id}/`, updates);
|
||||
return data;
|
||||
|
||||
95
frontend/src/hooks/useStaffRoles.ts
Normal file
95
frontend/src/hooks/useStaffRoles.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import apiClient from '../api/client';
|
||||
import { StaffRole, AvailablePermissions } from '../types';
|
||||
|
||||
/**
|
||||
* Hook to fetch all staff roles for the current tenant
|
||||
*/
|
||||
export const useStaffRoles = () => {
|
||||
return useQuery<StaffRole[]>({
|
||||
queryKey: ['staffRoles'],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get('/staff-roles/');
|
||||
return data;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to fetch a single staff role by ID
|
||||
*/
|
||||
export const useStaffRole = (id: number | null) => {
|
||||
return useQuery<StaffRole>({
|
||||
queryKey: ['staffRoles', id],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get(`/staff-roles/${id}/`);
|
||||
return data;
|
||||
},
|
||||
enabled: id !== null,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to fetch available permission keys and their metadata
|
||||
*/
|
||||
export const useAvailablePermissions = () => {
|
||||
return useQuery<AvailablePermissions>({
|
||||
queryKey: ['staffRoles', 'availablePermissions'],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get('/staff-roles/available_permissions/');
|
||||
return data;
|
||||
},
|
||||
staleTime: 1000 * 60 * 60, // Cache for 1 hour - permissions don't change often
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to create a new staff role
|
||||
*/
|
||||
export const useCreateStaffRole = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (data: Partial<StaffRole>) => {
|
||||
const response = await apiClient.post('/staff-roles/', data);
|
||||
return response.data as StaffRole;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['staffRoles'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to update an existing staff role
|
||||
*/
|
||||
export const useUpdateStaffRole = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ id, ...data }: { id: number } & Partial<StaffRole>) => {
|
||||
const response = await apiClient.patch(`/staff-roles/${id}/`, data);
|
||||
return response.data as StaffRole;
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['staffRoles'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['staffRoles', variables.id] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to delete a staff role
|
||||
*/
|
||||
export const useDeleteStaffRole = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (id: number) => {
|
||||
await apiClient.delete(`/staff-roles/${id}/`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['staffRoles'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user