Improve staff management UI and add sorting functionality
- Remove WIP badge from staff sidebar navigation - Make action buttons consistent between Customers and Staff pages - Edit button: icon + text with gray border - Masquerade button: icon + text with indigo border - Verify email button: icon-only with colored border (green/amber) - Add sortable columns to Staff list (name and role) - Include migrations for tenant manager role removal 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -51,8 +51,8 @@ describe('useInvitations hooks', () => {
|
||||
{
|
||||
id: 1,
|
||||
email: 'john@example.com',
|
||||
role: 'TENANT_MANAGER',
|
||||
role_display: 'Manager',
|
||||
role: 'TENANT_STAFF',
|
||||
role_display: 'Staff',
|
||||
status: 'PENDING',
|
||||
invited_by: 5,
|
||||
invited_by_name: 'Admin User',
|
||||
@@ -205,10 +205,10 @@ describe('useInvitations hooks', () => {
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/staff/invitations/', invitationData);
|
||||
});
|
||||
|
||||
it('creates manager invitation with permissions', async () => {
|
||||
it('creates staff invitation with permissions', async () => {
|
||||
const invitationData: CreateInvitationData = {
|
||||
email: 'manager@example.com',
|
||||
role: 'TENANT_MANAGER',
|
||||
email: 'staff@example.com',
|
||||
role: 'TENANT_STAFF',
|
||||
permissions: {
|
||||
can_invite_staff: true,
|
||||
can_manage_resources: true,
|
||||
@@ -219,7 +219,7 @@ describe('useInvitations hooks', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const mockResponse = { id: 5, email: 'manager@example.com', role: 'TENANT_MANAGER' };
|
||||
const mockResponse = { id: 5, email: 'staff@example.com', role: 'TENANT_STAFF' };
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const { result } = renderHook(() => useCreateInvitation(), {
|
||||
|
||||
@@ -46,7 +46,7 @@ describe('useStaff hooks', () => {
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
phone: '555-1234',
|
||||
role: 'TENANT_MANAGER',
|
||||
role: 'TENANT_STAFF',
|
||||
is_active: true,
|
||||
permissions: { can_invite_staff: true },
|
||||
can_invite_staff: true,
|
||||
@@ -79,7 +79,7 @@ describe('useStaff hooks', () => {
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
phone: '555-1234',
|
||||
role: 'TENANT_MANAGER',
|
||||
role: 'TENANT_STAFF',
|
||||
is_active: true,
|
||||
permissions: { can_invite_staff: true },
|
||||
can_invite_staff: true,
|
||||
|
||||
@@ -122,11 +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.
|
||||
* Note: Backend maps tenant_owner -> owner, tenant_staff -> staff, etc.
|
||||
*/
|
||||
const getRedirectPathForRole = (role: string): string => {
|
||||
// Tenant roles (as returned by backend after role mapping)
|
||||
const tenantRoles = ['owner', 'manager', 'staff', 'customer'];
|
||||
const tenantRoles = ['owner', 'staff', 'customer'];
|
||||
if (tenantRoles.includes(role)) {
|
||||
return '/dashboard/';
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ const transformCustomer = (c: any): Customer => ({
|
||||
paymentMethods: [],
|
||||
user_data: c.user_data,
|
||||
notes: c.notes || '',
|
||||
email_verified: c.email_verified ?? false,
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -208,3 +209,20 @@ export const useDeleteCustomer = () => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to verify a customer's email address
|
||||
*/
|
||||
export const useVerifyCustomerEmail = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
const { data } = await apiClient.post(`/customers/${id}/verify_email/`);
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['customers'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import apiClient from '../api/client';
|
||||
export interface StaffInvitation {
|
||||
id: number;
|
||||
email: string;
|
||||
role: 'TENANT_MANAGER' | 'TENANT_STAFF';
|
||||
role: 'TENANT_STAFF';
|
||||
role_display: string;
|
||||
status: 'PENDING' | 'ACCEPTED' | 'DECLINED' | 'EXPIRED' | 'CANCELLED';
|
||||
invited_by: number | null;
|
||||
@@ -50,7 +50,7 @@ export interface StaffPermissions {
|
||||
|
||||
export interface CreateInvitationData {
|
||||
email: string;
|
||||
role: 'TENANT_MANAGER' | 'TENANT_STAFF';
|
||||
role: 'TENANT_STAFF';
|
||||
create_bookable_resource?: boolean;
|
||||
resource_name?: string;
|
||||
permissions?: StaffPermissions;
|
||||
|
||||
@@ -13,6 +13,8 @@ export interface StaffPermissions {
|
||||
export interface StaffMember {
|
||||
id: string;
|
||||
name: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
role: string;
|
||||
@@ -22,6 +24,7 @@ export interface StaffMember {
|
||||
staff_role_id: number | null;
|
||||
staff_role_name: string | null;
|
||||
effective_permissions: Record<string, boolean>;
|
||||
email_verified: boolean;
|
||||
}
|
||||
|
||||
interface StaffFilters {
|
||||
@@ -30,7 +33,7 @@ interface StaffFilters {
|
||||
|
||||
/**
|
||||
* Hook to fetch staff members with optional filters
|
||||
* Staff members are Users with roles: TENANT_OWNER, TENANT_MANAGER, TENANT_STAFF
|
||||
* Staff members are Users with roles: TENANT_OWNER, TENANT_STAFF
|
||||
*/
|
||||
export const useStaff = (filters?: StaffFilters) => {
|
||||
return useQuery<StaffMember[]>({
|
||||
@@ -46,6 +49,8 @@ export const useStaff = (filters?: StaffFilters) => {
|
||||
return data.map((s: any) => ({
|
||||
id: String(s.id),
|
||||
name: s.name || `${s.first_name || ''} ${s.last_name || ''}`.trim() || s.email,
|
||||
first_name: s.first_name || '',
|
||||
last_name: s.last_name || '',
|
||||
email: s.email || '',
|
||||
phone: s.phone || '',
|
||||
role: s.role || 'staff',
|
||||
@@ -55,14 +60,27 @@ export const useStaff = (filters?: StaffFilters) => {
|
||||
staff_role_id: s.staff_role_id ?? null,
|
||||
staff_role_name: s.staff_role_name ?? null,
|
||||
effective_permissions: s.effective_permissions || {},
|
||||
email_verified: s.email_verified ?? false,
|
||||
}));
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
};
|
||||
|
||||
export interface StaffProfileUpdate {
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
export interface StaffUpdate extends StaffProfileUpdate {
|
||||
is_active?: boolean;
|
||||
permissions?: StaffPermissions;
|
||||
staff_role_id?: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to update a staff member's settings
|
||||
* Hook to update a staff member's settings and profile
|
||||
*/
|
||||
export const useUpdateStaff = () => {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -73,7 +91,7 @@ export const useUpdateStaff = () => {
|
||||
updates,
|
||||
}: {
|
||||
id: string;
|
||||
updates: { is_active?: boolean; permissions?: StaffPermissions; staff_role_id?: number | null };
|
||||
updates: StaffUpdate;
|
||||
}) => {
|
||||
const { data } = await apiClient.patch(`/staff/${id}/`, updates);
|
||||
return data;
|
||||
@@ -102,3 +120,38 @@ export const useToggleStaffActive = () => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to verify a staff member's email address
|
||||
*/
|
||||
export const useVerifyStaffEmail = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
const { data } = await apiClient.post(`/staff/${id}/verify_email/`);
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['staff'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['businessUsers'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to send a password reset email to a staff member
|
||||
*/
|
||||
export const useSendStaffPasswordReset = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
const { data } = await apiClient.post(`/staff/${id}/send_password_reset/`);
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['staff'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user