Sprint 5: User profile menu, preferences, org member management, v0.2.0
- Move Settings from sidebar Admin section to User Profile dropdown menu
- Add User Preferences page (placeholder for future: dark mode, timezone,
notifications, feature visibility)
- Add Manage Members page for tenant admins to invite/manage board members:
- List all org members with roles, status, join date, last login
- Add new members (creates user account + org membership)
- Change member roles (president, treasurer, secretary, board member,
property manager, viewer)
- Remove members (soft-deactivate)
- Role-gated: only president, admin, treasurer can manage members
- Backend: new org member management endpoints on OrganizationsController
- GET /organizations/members
- POST /organizations/members
- PUT /organizations/members/:id/role
- DELETE /organizations/members/:id
- Bump version to 0.2.0 MVP_P2 (package.json + Settings page)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,8 @@ import { CashFlowPage } from './pages/reports/CashFlowPage';
|
||||
import { AgingReportPage } from './pages/reports/AgingReportPage';
|
||||
import { YearEndPage } from './pages/reports/YearEndPage';
|
||||
import { SettingsPage } from './pages/settings/SettingsPage';
|
||||
import { UserPreferencesPage } from './pages/preferences/UserPreferencesPage';
|
||||
import { OrgMembersPage } from './pages/org-members/OrgMembersPage';
|
||||
import { AdminPage } from './pages/admin/AdminPage';
|
||||
import { AssessmentGroupsPage } from './pages/assessment-groups/AssessmentGroupsPage';
|
||||
import { CashFlowForecastPage } from './pages/cash-flow/CashFlowForecastPage';
|
||||
@@ -124,6 +126,8 @@ export function App() {
|
||||
<Route path="reports/sankey" element={<SankeyPage />} />
|
||||
<Route path="reports/year-end" element={<YearEndPage />} />
|
||||
<Route path="settings" element={<SettingsPage />} />
|
||||
<Route path="preferences" element={<UserPreferencesPage />} />
|
||||
<Route path="org-members" element={<OrgMembersPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,9 @@ import {
|
||||
IconLogout,
|
||||
IconSwitchHorizontal,
|
||||
IconChevronDown,
|
||||
IconSettings,
|
||||
IconUserCog,
|
||||
IconUsersGroup,
|
||||
} from '@tabler/icons-react';
|
||||
import { Outlet, useNavigate } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
@@ -20,6 +23,9 @@ export function AppLayout() {
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
// Tenant admins (president role) can manage org members
|
||||
const isTenantAdmin = currentOrg?.role === 'president' || currentOrg?.role === 'admin';
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
header={{ height: 60 }}
|
||||
@@ -36,7 +42,7 @@ export function AppLayout() {
|
||||
{currentOrg && (
|
||||
<Text size="sm" c="dimmed">{currentOrg.name}</Text>
|
||||
)}
|
||||
<Menu shadow="md" width={200}>
|
||||
<Menu shadow="md" width={220}>
|
||||
<Menu.Target>
|
||||
<UnstyledButton>
|
||||
<Group gap="xs">
|
||||
@@ -49,6 +55,28 @@ export function AppLayout() {
|
||||
</UnstyledButton>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Label>Account</Menu.Label>
|
||||
<Menu.Item
|
||||
leftSection={<IconUserCog size={14} />}
|
||||
onClick={() => navigate('/preferences')}
|
||||
>
|
||||
User Preferences
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<IconSettings size={14} />}
|
||||
onClick={() => navigate('/settings')}
|
||||
>
|
||||
Settings
|
||||
</Menu.Item>
|
||||
{isTenantAdmin && (
|
||||
<Menu.Item
|
||||
leftSection={<IconUsersGroup size={14} />}
|
||||
onClick={() => navigate('/org-members')}
|
||||
>
|
||||
Manage Members
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
leftSection={<IconSwitchHorizontal size={14} />}
|
||||
onClick={() => navigate('/select-org')}
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
IconShieldCheck,
|
||||
IconBuildingBank,
|
||||
IconUsers,
|
||||
IconSettings,
|
||||
IconCrown,
|
||||
IconCategory,
|
||||
IconChartAreaLine,
|
||||
@@ -74,12 +73,6 @@ const navSections = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Admin',
|
||||
items: [
|
||||
{ label: 'Settings', icon: IconSettings, path: '/settings' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function Sidebar() {
|
||||
|
||||
376
frontend/src/pages/org-members/OrgMembersPage.tsx
Normal file
376
frontend/src/pages/org-members/OrgMembersPage.tsx
Normal file
@@ -0,0 +1,376 @@
|
||||
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 } 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<string, string> = {
|
||||
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<OrgMember | null>(null);
|
||||
const queryClient = useQueryClient();
|
||||
const { user, currentOrg } = useAuthStore();
|
||||
|
||||
const { data: members = [], isLoading } = useQuery<OrgMember[]>({
|
||||
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 <Center h={300}><Loader /></Center>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Group justify="space-between">
|
||||
<div>
|
||||
<Title order={2}>Organization Members</Title>
|
||||
<Text c="dimmed" size="sm">Manage who has access to {currentOrg?.name}</Text>
|
||||
</div>
|
||||
<Button leftSection={<IconUserPlus size={16} />} onClick={openAdd}>
|
||||
Add Member
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 3 }}>
|
||||
<Card withBorder p="xs">
|
||||
<Group>
|
||||
<ThemeIcon color="blue" variant="light" size={36} radius="md">
|
||||
<IconUsers size={20} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed">Total Members</Text>
|
||||
<Text fw={700}>{activeMembers.length}</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Card>
|
||||
<Card withBorder p="xs">
|
||||
<Group>
|
||||
<ThemeIcon color="green" variant="light" size={36} radius="md">
|
||||
<IconShieldCheck size={20} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed">Board Members</Text>
|
||||
<Text fw={700}>
|
||||
{activeMembers.filter((m) =>
|
||||
['president', 'treasurer', 'secretary', 'board_member'].includes(m.role),
|
||||
).length}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Card>
|
||||
<Card withBorder p="xs">
|
||||
<Group>
|
||||
<ThemeIcon color="violet" variant="light" size={36} radius="md">
|
||||
<IconInfoCircle size={20} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed">Your Role</Text>
|
||||
<Text fw={700} tt="capitalize">{currentOrg?.role || 'N/A'}</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Card>
|
||||
</SimpleGrid>
|
||||
|
||||
<Alert icon={<IconInfoCircle size={16} />} 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.
|
||||
</Alert>
|
||||
|
||||
<Table striped highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Name</Table.Th>
|
||||
<Table.Th>Email</Table.Th>
|
||||
<Table.Th>Role</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>Joined</Table.Th>
|
||||
<Table.Th>Last Login</Table.Th>
|
||||
<Table.Th></Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{activeMembers.length === 0 && (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={7}>
|
||||
<Text ta="center" c="dimmed" py="lg">
|
||||
No members yet. Add your first board member above.
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
{activeMembers.map((member) => (
|
||||
<Table.Tr key={member.id}>
|
||||
<Table.Td fw={500}>
|
||||
{member.firstName} {member.lastName}
|
||||
{member.userId === user?.id && (
|
||||
<Badge size="xs" variant="light" ml={6}>You</Badge>
|
||||
)}
|
||||
</Table.Td>
|
||||
<Table.Td>{member.email}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color={roleColors[member.role] || 'gray'} variant="light" size="sm" tt="capitalize">
|
||||
{member.role.replace(/_/g, ' ')}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color="green" variant="light" size="sm">Active</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{member.joinedAt ? new Date(member.joinedAt).toLocaleDateString() : '-'}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{member.lastLoginAt ? new Date(member.lastLoginAt).toLocaleDateString() : 'Never'}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap={4}>
|
||||
<Tooltip label="Change role">
|
||||
<ActionIcon variant="subtle" onClick={() => handleEditRole(member)}>
|
||||
<IconEdit size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{member.userId !== user?.id && (
|
||||
<Tooltip label="Remove member">
|
||||
<ActionIcon variant="subtle" color="red" onClick={() => handleRemove(member)}>
|
||||
<IconTrash size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
{inactiveMembers.map((member) => (
|
||||
<Table.Tr key={member.id} style={{ opacity: 0.5 }}>
|
||||
<Table.Td fw={500}>{member.firstName} {member.lastName}</Table.Td>
|
||||
<Table.Td>{member.email}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color={roleColors[member.role] || 'gray'} variant="light" size="sm" tt="capitalize">
|
||||
{member.role.replace(/_/g, ' ')}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color="gray" variant="light" size="sm">Inactive</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{member.joinedAt ? new Date(member.joinedAt).toLocaleDateString() : '-'}
|
||||
</Table.Td>
|
||||
<Table.Td>-</Table.Td>
|
||||
<Table.Td></Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
|
||||
{/* Add Member Modal */}
|
||||
<Modal opened={addOpened} onClose={closeAdd} title="Add Organization Member" size="md" closeOnClickOutside={false}>
|
||||
<form onSubmit={addForm.onSubmit((values) => addMemberMutation.mutate(values))}>
|
||||
<Stack>
|
||||
<Alert icon={<IconInfoCircle size={16} />} 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.
|
||||
</Alert>
|
||||
<TextInput
|
||||
label="Email Address"
|
||||
placeholder="boardmember@example.com"
|
||||
required
|
||||
{...addForm.getInputProps('email')}
|
||||
/>
|
||||
<Group grow>
|
||||
<TextInput
|
||||
label="First Name"
|
||||
placeholder="John"
|
||||
required
|
||||
{...addForm.getInputProps('firstName')}
|
||||
/>
|
||||
<TextInput
|
||||
label="Last Name"
|
||||
placeholder="Smith"
|
||||
required
|
||||
{...addForm.getInputProps('lastName')}
|
||||
/>
|
||||
</Group>
|
||||
<TextInput
|
||||
label="Temporary Password"
|
||||
placeholder="Min 6 characters"
|
||||
description="The user can change this after first login"
|
||||
required
|
||||
{...addForm.getInputProps('password')}
|
||||
/>
|
||||
<Select
|
||||
label="Role"
|
||||
required
|
||||
data={ROLE_OPTIONS}
|
||||
{...addForm.getInputProps('role')}
|
||||
/>
|
||||
<Button type="submit" leftSection={<IconPlus size={16} />} loading={addMemberMutation.isPending}>
|
||||
Add Member
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
{/* Edit Role Modal */}
|
||||
<Modal opened={editOpened} onClose={closeEdit} title="Change Member Role" size="sm" closeOnClickOutside={false}>
|
||||
{editingMember && (
|
||||
<form
|
||||
onSubmit={editForm.onSubmit((values) =>
|
||||
updateRoleMutation.mutate({ membershipId: editingMember.id, role: values.role }),
|
||||
)}
|
||||
>
|
||||
<Stack>
|
||||
<Text size="sm" c="dimmed">
|
||||
Changing role for <strong>{editingMember.firstName} {editingMember.lastName}</strong>
|
||||
</Text>
|
||||
<Select
|
||||
label="New Role"
|
||||
required
|
||||
data={ROLE_OPTIONS}
|
||||
{...editForm.getInputProps('role')}
|
||||
/>
|
||||
<Button type="submit" loading={updateRoleMutation.isPending}>
|
||||
Update Role
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
)}
|
||||
</Modal>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
198
frontend/src/pages/preferences/UserPreferencesPage.tsx
Normal file
198
frontend/src/pages/preferences/UserPreferencesPage.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import {
|
||||
Title, Text, Card, Stack, Group, SimpleGrid, ThemeIcon, Switch,
|
||||
Select, Badge, Divider,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconUser, IconPalette, IconClock, IconBell, IconEye,
|
||||
} from '@tabler/icons-react';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
|
||||
export function UserPreferencesPage() {
|
||||
const { user, currentOrg } = useAuthStore();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<div>
|
||||
<Title order={2}>User Preferences</Title>
|
||||
<Text c="dimmed" size="sm">Customize your experience</Text>
|
||||
</div>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }}>
|
||||
{/* Profile */}
|
||||
<Card withBorder padding="lg">
|
||||
<Group mb="md">
|
||||
<ThemeIcon color="blue" variant="light" size={40} radius="md">
|
||||
<IconUser size={24} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Text fw={600} size="lg">Profile</Text>
|
||||
<Text c="dimmed" size="sm">Your account information</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Stack gap="xs">
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c="dimmed">Name</Text>
|
||||
<Text size="sm" fw={500}>{user?.firstName} {user?.lastName}</Text>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c="dimmed">Email</Text>
|
||||
<Text size="sm" fw={500}>{user?.email}</Text>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c="dimmed">Organization</Text>
|
||||
<Text size="sm" fw={500}>{currentOrg?.name || 'N/A'}</Text>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c="dimmed">Role</Text>
|
||||
<Badge variant="light" tt="capitalize">{currentOrg?.role || 'N/A'}</Badge>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{/* Display Preferences */}
|
||||
<Card withBorder padding="lg">
|
||||
<Group mb="md">
|
||||
<ThemeIcon color="violet" variant="light" size={40} radius="md">
|
||||
<IconPalette size={24} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Text fw={600} size="lg">Display</Text>
|
||||
<Text c="dimmed" size="sm">Appearance and layout</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between">
|
||||
<div>
|
||||
<Text size="sm">Dark Mode</Text>
|
||||
<Text size="xs" c="dimmed">Switch to dark color theme</Text>
|
||||
</div>
|
||||
<Switch disabled />
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<div>
|
||||
<Text size="sm">Compact View</Text>
|
||||
<Text size="xs" c="dimmed">Reduce spacing in tables and lists</Text>
|
||||
</div>
|
||||
<Switch disabled />
|
||||
</Group>
|
||||
<Divider />
|
||||
<Text size="xs" c="dimmed" ta="center">Display preferences coming in a future release</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{/* Regional */}
|
||||
<Card withBorder padding="lg">
|
||||
<Group mb="md">
|
||||
<ThemeIcon color="green" variant="light" size={40} radius="md">
|
||||
<IconClock size={24} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Text fw={600} size="lg">Regional</Text>
|
||||
<Text c="dimmed" size="sm">Locale and time settings</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Stack gap="md">
|
||||
<Select
|
||||
label="Time Zone"
|
||||
placeholder="Auto-detect"
|
||||
data={[
|
||||
{ value: 'auto', label: `Auto (${Intl.DateTimeFormat().resolvedOptions().timeZone})` },
|
||||
{ value: 'America/New_York', label: 'Eastern Time (ET)' },
|
||||
{ value: 'America/Chicago', label: 'Central Time (CT)' },
|
||||
{ value: 'America/Denver', label: 'Mountain Time (MT)' },
|
||||
{ value: 'America/Los_Angeles', label: 'Pacific Time (PT)' },
|
||||
{ value: 'America/Anchorage', label: 'Alaska Time (AKT)' },
|
||||
{ value: 'Pacific/Honolulu', label: 'Hawaii Time (HT)' },
|
||||
]}
|
||||
defaultValue="auto"
|
||||
disabled
|
||||
/>
|
||||
<Select
|
||||
label="Date Format"
|
||||
data={[
|
||||
{ value: 'MM/DD/YYYY', label: 'MM/DD/YYYY' },
|
||||
{ value: 'DD/MM/YYYY', label: 'DD/MM/YYYY' },
|
||||
{ value: 'YYYY-MM-DD', label: 'YYYY-MM-DD' },
|
||||
]}
|
||||
defaultValue="MM/DD/YYYY"
|
||||
disabled
|
||||
/>
|
||||
<Divider />
|
||||
<Text size="xs" c="dimmed" ta="center">Regional preferences coming in a future release</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{/* Notifications */}
|
||||
<Card withBorder padding="lg">
|
||||
<Group mb="md">
|
||||
<ThemeIcon color="orange" variant="light" size={40} radius="md">
|
||||
<IconBell size={24} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Text fw={600} size="lg">Notifications</Text>
|
||||
<Text c="dimmed" size="sm">Email and in-app alerts</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between">
|
||||
<div>
|
||||
<Text size="sm">Email Notifications</Text>
|
||||
<Text size="xs" c="dimmed">Receive alerts via email</Text>
|
||||
</div>
|
||||
<Switch disabled />
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<div>
|
||||
<Text size="sm">Payment Alerts</Text>
|
||||
<Text size="xs" c="dimmed">Notify when payments are received</Text>
|
||||
</div>
|
||||
<Switch disabled />
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<div>
|
||||
<Text size="sm">Budget Alerts</Text>
|
||||
<Text size="xs" c="dimmed">Warn when budget thresholds exceeded</Text>
|
||||
</div>
|
||||
<Switch disabled />
|
||||
</Group>
|
||||
<Divider />
|
||||
<Text size="xs" c="dimmed" ta="center">Notification preferences coming in a future release</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{/* Feature Visibility */}
|
||||
<Card withBorder padding="lg">
|
||||
<Group mb="md">
|
||||
<ThemeIcon color="cyan" variant="light" size={40} radius="md">
|
||||
<IconEye size={24} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Text fw={600} size="lg">Feature Visibility</Text>
|
||||
<Text c="dimmed" size="sm">Show or hide sidebar sections</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between">
|
||||
<Text size="sm">Assessments</Text>
|
||||
<Switch disabled defaultChecked />
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text size="sm">Planning</Text>
|
||||
<Switch disabled defaultChecked />
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text size="sm">Invoices & Payments</Text>
|
||||
<Switch disabled defaultChecked />
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text size="sm">Capital Projects</Text>
|
||||
<Switch disabled defaultChecked />
|
||||
</Group>
|
||||
<Divider />
|
||||
<Text size="xs" c="dimmed" ta="center">Feature visibility preferences coming in a future release</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -117,7 +117,7 @@ export function SettingsPage() {
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c="dimmed">Version</Text>
|
||||
<Badge variant="light">0.1.0 MVP</Badge>
|
||||
<Badge variant="light">0.2.0 MVP_P2</Badge>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c="dimmed">API</Text>
|
||||
|
||||
Reference in New Issue
Block a user