= ({ onMasquerade, effectiveUser }) =>
| {customer.lastVisit ? customer.lastVisit.toLocaleDateString() : {t('customers.never')}} |
+
)}
+
+ {/* Verify Email Confirmation Modal */}
+ {verifyEmailTarget && (
+
+
+
+
+
+ {verifyEmailTarget.email_verified ? t('customers.unverifyEmailTitle') : t('customers.verifyEmailTitle')}
+
+
+
+
+ {verifyEmailTarget.email_verified
+ ? t('customers.unverifyEmailConfirm', { email: verifyEmailTarget.email })
+ : t('customers.verifyEmailConfirm', { email: verifyEmailTarget.email })}
+
+
+
+
+
+
+
+
+
+ )}
);
};
diff --git a/frontend/src/pages/Messages.tsx b/frontend/src/pages/Messages.tsx
index 18781ae4..ac34e00d 100644
--- a/frontend/src/pages/Messages.tsx
+++ b/frontend/src/pages/Messages.tsx
@@ -251,7 +251,6 @@ const Messages: React.FC = () => {
// Computed
const roleOptions = [
{ value: 'owner', label: 'Owners', icon: Users, description: 'Business owners' },
- { value: 'manager', label: 'Managers', icon: Users, description: 'Team leads' },
{ value: 'staff', label: 'Staff', icon: Users, description: 'Employees' },
{ value: 'customer', label: 'Customers', icon: Users, description: 'Clients' },
];
diff --git a/frontend/src/pages/Payments.tsx b/frontend/src/pages/Payments.tsx
index 5e17f0dc..2f458fba 100644
--- a/frontend/src/pages/Payments.tsx
+++ b/frontend/src/pages/Payments.tsx
@@ -50,7 +50,7 @@ const Payments: React.FC = () => {
const { t } = useTranslation();
const { user: effectiveUser, business } = useOutletContext<{ user: User, business: Business }>();
- const isBusiness = effectiveUser.role === 'owner' || effectiveUser.role === 'manager';
+ const isBusiness = effectiveUser.role === 'owner' || effectiveUser.role === 'staff';
const isCustomer = effectiveUser.role === 'customer';
// Tab state
diff --git a/frontend/src/pages/Staff.tsx b/frontend/src/pages/Staff.tsx
index 8028036b..92ffa3d3 100644
--- a/frontend/src/pages/Staff.tsx
+++ b/frontend/src/pages/Staff.tsx
@@ -1,8 +1,8 @@
-import React, { useState } from 'react';
+import React, { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { User } from '../types';
import { useCreateResource, useResources } from '../hooks/useBusiness';
-import { useStaff, useToggleStaffActive, useUpdateStaff, StaffMember } from '../hooks/useStaff';
+import { useStaff, useToggleStaffActive, useUpdateStaff, useVerifyStaffEmail, useSendStaffPasswordReset, StaffMember } from '../hooks/useStaff';
import {
useInvitations,
useCreateInvitation,
@@ -30,9 +30,13 @@ import {
ChevronRight,
UserX,
Power,
+ BadgeCheck,
+ Key,
+ Phone,
+ Eye,
+ ArrowUpDown,
} from 'lucide-react';
import Portal from '../components/Portal';
-import StaffPermissions from '../components/StaffPermissions';
interface StaffProps {
onMasquerade: (user: User) => void;
@@ -51,10 +55,13 @@ const Staff: React.FC = ({ onMasquerade, effectiveUser }) => {
const resendInvitationMutation = useResendInvitation();
const toggleActiveMutation = useToggleStaffActive();
const updateStaffMutation = useUpdateStaff();
+ const verifyEmailMutation = useVerifyStaffEmail();
+ const passwordResetMutation = useSendStaffPasswordReset();
const [isInviteModalOpen, setIsInviteModalOpen] = useState(false);
const [inviteEmail, setInviteEmail] = useState('');
- const [inviteRole, setInviteRole] = useState<'TENANT_MANAGER' | 'TENANT_STAFF'>('TENANT_STAFF');
+ // All invitations are for TENANT_STAFF - manager role removed
+ const inviteRole = 'TENANT_STAFF';
const [inviteStaffRoleId, setInviteStaffRoleId] = useState(null);
const [createBookableResource, setCreateBookableResource] = useState(false);
const [resourceName, setResourceName] = useState('');
@@ -66,16 +73,55 @@ const Staff: React.FC = ({ onMasquerade, effectiveUser }) => {
// Edit modal state
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [editingStaff, setEditingStaff] = useState(null);
- const [editPermissions, setEditPermissions] = useState>({});
const [editStaffRoleId, setEditStaffRoleId] = useState(null);
+ const [editFirstName, setEditFirstName] = useState('');
+ const [editLastName, setEditLastName] = useState('');
+ const [editPhone, setEditPhone] = useState('');
const [editError, setEditError] = useState('');
const [editSuccess, setEditSuccess] = useState('');
- // Check if user can invite managers (only owners can)
- const canInviteManagers = effectiveUser.role === 'owner';
+ // Verify email confirmation modal state
+ const [verifyEmailTarget, setVerifyEmailTarget] = useState(null);
+
+ // Sorting state
+ const [sortConfig, setSortConfig] = useState<{ key: 'name' | 'role'; direction: 'asc' | 'desc' }>({
+ key: 'name',
+ direction: 'asc'
+ });
+
+ const handleSort = (key: 'name' | 'role') => {
+ setSortConfig(current => ({
+ key,
+ direction: current.key === key && current.direction === 'asc' ? 'desc' : 'asc',
+ }));
+ };
+
+ // Separate active and inactive staff, then sort
+ const activeStaff = useMemo(() => {
+ const active = staffMembers.filter((s) => s.is_active);
+ return [...active].sort((a, b) => {
+ let aValue: string;
+ let bValue: string;
+
+ if (sortConfig.key === 'name') {
+ aValue = (a.name || a.email || '').toLowerCase();
+ bValue = (b.name || b.email || '').toLowerCase();
+ } else {
+ // Sort by role: owners first, then by staff_role_name
+ const aIsOwner = a.role === 'owner';
+ const bIsOwner = b.role === 'owner';
+ if (aIsOwner && !bIsOwner) return sortConfig.direction === 'asc' ? -1 : 1;
+ if (!aIsOwner && bIsOwner) return sortConfig.direction === 'asc' ? 1 : -1;
+ aValue = (a.staff_role_name || '').toLowerCase();
+ bValue = (b.staff_role_name || '').toLowerCase();
+ }
+
+ if (aValue < bValue) return sortConfig.direction === 'asc' ? -1 : 1;
+ if (aValue > bValue) return sortConfig.direction === 'asc' ? 1 : -1;
+ return 0;
+ });
+ }, [staffMembers, sortConfig]);
- // Separate active and inactive staff
- const activeStaff = staffMembers.filter((s) => s.is_active);
const inactiveStaff = staffMembers.filter((s) => !s.is_active);
// Helper to check if a user is already linked to a resource
@@ -151,7 +197,6 @@ const Staff: React.FC = ({ onMasquerade, effectiveUser }) => {
const openInviteModal = () => {
setInviteEmail('');
- setInviteRole('TENANT_STAFF');
setInviteStaffRoleId(null);
setCreateBookableResource(false);
setResourceName('');
@@ -196,8 +241,10 @@ const Staff: React.FC = ({ onMasquerade, effectiveUser }) => {
const openEditModal = (staff: StaffMember) => {
setEditingStaff(staff);
- setEditPermissions(staff.permissions || {});
setEditStaffRoleId(staff.staff_role_id);
+ setEditFirstName(staff.first_name);
+ setEditLastName(staff.last_name);
+ setEditPhone(staff.phone || '');
setEditError('');
setEditSuccess('');
setIsEditModalOpen(true);
@@ -206,8 +253,10 @@ const Staff: React.FC = ({ onMasquerade, effectiveUser }) => {
const closeEditModal = () => {
setIsEditModalOpen(false);
setEditingStaff(null);
- setEditPermissions({});
setEditStaffRoleId(null);
+ setEditFirstName('');
+ setEditLastName('');
+ setEditPhone('');
setEditError('');
setEditSuccess('');
};
@@ -217,10 +266,17 @@ const Staff: React.FC = ({ onMasquerade, effectiveUser }) => {
setEditError('');
try {
- const updates: { permissions: Record; staff_role_id?: number | null } = {
- permissions: editPermissions,
+ const updates: {
+ staff_role_id?: number | null;
+ first_name?: string;
+ last_name?: string;
+ phone?: string;
+ } = {
+ first_name: editFirstName,
+ last_name: editLastName,
+ phone: editPhone,
};
- // Only include staff_role_id for staff users (not owners/managers)
+ // Only include staff_role_id for staff users (not owners)
if (editingStaff.role === 'staff') {
updates.staff_role_id = editStaffRoleId;
}
@@ -237,6 +293,21 @@ const Staff: React.FC = ({ onMasquerade, effectiveUser }) => {
}
};
+ const handleSendPasswordReset = async () => {
+ if (!editingStaff) return;
+
+ if (!confirm(t('staff.confirmPasswordReset', { email: editingStaff.email }))) {
+ return;
+ }
+
+ try {
+ await passwordResetMutation.mutateAsync(editingStaff.id);
+ setEditSuccess(t('staff.passwordResetSent'));
+ } catch (err: any) {
+ setEditError(err.response?.data?.error || t('staff.passwordResetFailed'));
+ }
+ };
+
const handleDeactivateFromModal = async () => {
if (!editingStaff) return;
@@ -251,6 +322,20 @@ const Staff: React.FC = ({ onMasquerade, effectiveUser }) => {
}
};
+ const handleVerifyEmailClick = (user: StaffMember) => {
+ setVerifyEmailTarget(user);
+ };
+
+ const handleVerifyEmailConfirm = async () => {
+ if (!verifyEmailTarget) return;
+ try {
+ await verifyEmailMutation.mutateAsync(verifyEmailTarget.id);
+ setVerifyEmailTarget(null);
+ } catch (err: any) {
+ console.error('Failed to toggle email verification:', err);
+ }
+ };
+
return (
{/* Header */}
@@ -325,9 +410,12 @@ const Staff: React.FC = ({ onMasquerade, effectiveUser }) => {
- | {t('staff.name')} |
- {t('staff.role')} |
- {t('staff.staffRole')} |
+ handleSort('name')}>
+
+ |
+ handleSort('role')}>
+
+ |
{t('staff.bookableResource')} |
{t('common.actions')} |
@@ -356,34 +444,19 @@ const Staff: React.FC = ({ onMasquerade, effectiveUser }) => {
{user.role === 'owner' && }
- {user.role === 'manager' && }
- {user.role}
+ {user.staff_role_name === 'Manager' && }
+ {user.role === 'owner' ? t('staff.roleOwner') : (user.staff_role_name || t('staff.noRoleAssigned'))}
|
-
- {user.role === 'staff' ? (
- user.staff_role_name ? (
-
- {user.staff_role_name}
-
- ) : (
-
- {t('staff.noRoleAssigned')}
-
- )
- ) : (
- —
- )}
- |
{linkedResource ? (
@@ -401,21 +474,38 @@ const Staff: React.FC = ({ onMasquerade, effectiveUser }) => {
|
+
+
{canMasquerade && (
)}
-
|
@@ -473,10 +563,10 @@ const Staff: React.FC = ({ onMasquerade, effectiveUser }) => {
-
+
{user.role === 'owner' && }
- {user.role === 'manager' && }
- {user.role}
+ {user.staff_role_name === 'Manager' && }
+ {user.role === 'owner' ? t('staff.roleOwner') : (user.staff_role_name || t('staff.noRoleAssigned'))}
|
@@ -545,33 +635,11 @@ const Staff: React.FC = ({ onMasquerade, effectiveUser }) => {
/>
- {/* Role Selector */}
-
-
-
-
- {inviteRole === 'TENANT_MANAGER'
- ? t('staff.managerRoleHint')
- : t('staff.staffRoleHint')}
-
-
-
- {/* Staff Role Selector (only for staff invitations) */}
- {inviteRole === 'TENANT_STAFF' && staffRoles.length > 0 && (
+ {/* Staff Role Selector */}
+ {staffRoles.length > 0 && (
| |