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:
poduck
2025-12-17 19:29:13 -05:00
parent a80b35a806
commit 92019aac7e
68 changed files with 1827 additions and 788 deletions

View File

@@ -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(), {

View File

@@ -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,

View File

@@ -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/';
}

View File

@@ -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'] });
},
});
};

View File

@@ -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;

View File

@@ -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'] });
},
});
};