import { useState } from 'react'; import { Title, Text, Card, Stack, Group, Table, Badge, Button, Modal, TextInput, Select, ActionIcon, Tooltip, Alert, SimpleGrid, ThemeIcon, Loader, Center, } from '@mantine/core'; import { useForm } from '@mantine/form'; import { useDisclosure } from '@mantine/hooks'; import { notifications } from '@mantine/notifications'; import { IconPlus, IconEdit, IconTrash, IconUserPlus, IconUsers, IconShieldCheck, IconInfoCircle, } from '@tabler/icons-react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import api from '../../services/api'; import { useAuthStore, useIsReadOnly } from '../../stores/authStore'; interface OrgMember { id: string; userId: string; email: string; firstName: string; lastName: string; role: string; isActive: boolean; joinedAt: string; lastLoginAt: string | null; } const ROLE_OPTIONS = [ { value: 'president', label: 'President' }, { value: 'treasurer', label: 'Treasurer' }, { value: 'secretary', label: 'Secretary' }, { value: 'board_member', label: 'Board Member' }, { value: 'property_manager', label: 'Property Manager' }, { value: 'viewer', label: 'Viewer (Read-Only)' }, ]; const roleColors: Record = { president: 'red', treasurer: 'blue', secretary: 'green', board_member: 'violet', property_manager: 'orange', viewer: 'gray', admin: 'red', }; export function OrgMembersPage() { const [addOpened, { open: openAdd, close: closeAdd }] = useDisclosure(false); const [editOpened, { open: openEdit, close: closeEdit }] = useDisclosure(false); const [editingMember, setEditingMember] = useState(null); const queryClient = useQueryClient(); const { user, currentOrg } = useAuthStore(); const isReadOnly = useIsReadOnly(); const { data: members = [], isLoading } = useQuery({ queryKey: ['org-members'], queryFn: async () => { const { data } = await api.get('/organizations/members'); return data; }, }); const addForm = useForm({ initialValues: { email: '', firstName: '', lastName: '', password: '', role: 'board_member', }, validate: { email: (v) => (/^\S+@\S+\.\S+$/.test(v) ? null : 'Valid email required'), firstName: (v) => (v.length > 0 ? null : 'Required'), lastName: (v) => (v.length > 0 ? null : 'Required'), password: (v) => (v.length >= 6 ? null : 'Min 6 characters'), }, }); const editForm = useForm({ initialValues: { role: 'board_member', }, }); const addMemberMutation = useMutation({ mutationFn: (values: typeof addForm.values) => api.post('/organizations/members', values), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['org-members'] }); notifications.show({ message: 'Member added successfully', color: 'green' }); closeAdd(); addForm.reset(); }, onError: (err: any) => { notifications.show({ message: err.response?.data?.message || 'Failed to add member', color: 'red', }); }, }); const updateRoleMutation = useMutation({ mutationFn: ({ membershipId, role }: { membershipId: string; role: string }) => api.put(`/organizations/members/${membershipId}/role`, { role }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['org-members'] }); notifications.show({ message: 'Role updated', color: 'green' }); closeEdit(); setEditingMember(null); }, onError: (err: any) => { notifications.show({ message: err.response?.data?.message || 'Failed to update role', color: 'red', }); }, }); const removeMemberMutation = useMutation({ mutationFn: (membershipId: string) => api.delete(`/organizations/members/${membershipId}`), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['org-members'] }); notifications.show({ message: 'Member removed', color: 'green' }); }, onError: (err: any) => { notifications.show({ message: err.response?.data?.message || 'Failed to remove member', color: 'red', }); }, }); const handleEditRole = (member: OrgMember) => { setEditingMember(member); editForm.setValues({ role: member.role }); openEdit(); }; const handleRemove = (member: OrgMember) => { if (member.userId === user?.id) { notifications.show({ message: 'You cannot remove yourself', color: 'red' }); return; } if (window.confirm(`Remove ${member.firstName} ${member.lastName} (${member.email}) from this organization?`)) { removeMemberMutation.mutate(member.id); } }; const activeMembers = members.filter((m) => m.isActive); const inactiveMembers = members.filter((m) => !m.isActive); if (isLoading) { return
; } return (
Organization Members Manage who has access to {currentOrg?.name}
{!isReadOnly && ( )}
Total Members {activeMembers.length}
Board Members {activeMembers.filter((m) => ['president', 'treasurer', 'secretary', 'board_member'].includes(m.role), ).length}
Your Role {currentOrg?.role || 'N/A'}
} color="blue" variant="light"> As an organization administrator, you can add board members, property managers, and viewers to give them access to this tenant. Each member can log in with their own credentials and see the same financial data. Name Email Role Status Joined Last Login {activeMembers.length === 0 && ( No members yet. Add your first board member above. )} {activeMembers.map((member) => ( {member.firstName} {member.lastName} {member.userId === user?.id && ( You )} {member.email} {member.role.replace(/_/g, ' ')} Active {member.joinedAt ? new Date(member.joinedAt).toLocaleDateString() : '-'} {member.lastLoginAt ? new Date(member.lastLoginAt).toLocaleDateString() : 'Never'} {!isReadOnly && ( handleEditRole(member)}> {member.userId !== user?.id && ( handleRemove(member)}> )} )} ))} {inactiveMembers.map((member) => ( {member.firstName} {member.lastName} {member.email} {member.role.replace(/_/g, ' ')} Inactive {member.joinedAt ? new Date(member.joinedAt).toLocaleDateString() : '-'} - ))}
{/* Add Member Modal */}
addMemberMutation.mutate(values))}> } color="blue" variant="light" mb="xs"> This will create a new user account (or link an existing one) and add them to your organization. They will be able to log in immediately.
)}
); }