Multi-tenant financial management platform for homeowner associations featuring: - NestJS backend with 16 modules (auth, accounts, transactions, budgets, units, invoices, payments, vendors, reserves, investments, capital projects, reports) - React + Mantine frontend with dashboard, CRUD pages, and financial reports - Schema-per-tenant PostgreSQL isolation with JWT-based tenant resolution - Docker Compose infrastructure (nginx, backend, frontend, postgres, redis) - Comprehensive seed data for Sunrise Valley HOA demo - 39 API endpoints with Swagger documentation - Double-entry bookkeeping with journal entries - Budget vs actual reporting and Sankey cash flow visualization Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
94 lines
2.6 KiB
TypeScript
94 lines
2.6 KiB
TypeScript
import { useState } from 'react';
|
|
import {
|
|
Container,
|
|
Paper,
|
|
Title,
|
|
Text,
|
|
TextInput,
|
|
PasswordInput,
|
|
Button,
|
|
Anchor,
|
|
Stack,
|
|
Alert,
|
|
} from '@mantine/core';
|
|
import { useForm } from '@mantine/form';
|
|
import { IconAlertCircle } from '@tabler/icons-react';
|
|
import { useNavigate, Link } from 'react-router-dom';
|
|
import api from '../../services/api';
|
|
import { useAuthStore } from '../../stores/authStore';
|
|
|
|
export function LoginPage() {
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const navigate = useNavigate();
|
|
const setAuth = useAuthStore((s) => s.setAuth);
|
|
|
|
const form = useForm({
|
|
initialValues: { email: '', password: '' },
|
|
validate: {
|
|
email: (v) => (/^\S+@\S+$/.test(v) ? null : 'Invalid email'),
|
|
password: (v) => (v.length >= 1 ? null : 'Password required'),
|
|
},
|
|
});
|
|
|
|
const handleSubmit = async (values: typeof form.values) => {
|
|
setLoading(true);
|
|
setError('');
|
|
try {
|
|
const { data } = await api.post('/auth/login', values);
|
|
setAuth(data.accessToken, data.user, data.organizations);
|
|
// Always go through org selection to ensure correct JWT with orgSchema
|
|
if (data.organizations.length >= 1) {
|
|
navigate('/select-org');
|
|
} else {
|
|
navigate('/');
|
|
}
|
|
} catch (err: any) {
|
|
setError(err.response?.data?.message || 'Login failed');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Container size={420} my={80}>
|
|
<Title ta="center" order={2}>
|
|
HOA Financial Platform
|
|
</Title>
|
|
<Text c="dimmed" size="sm" ta="center" mt={5}>
|
|
Don't have an account?{' '}
|
|
<Anchor component={Link} to="/register" size="sm">
|
|
Register
|
|
</Anchor>
|
|
</Text>
|
|
|
|
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
|
|
<form onSubmit={form.onSubmit(handleSubmit)}>
|
|
<Stack>
|
|
{error && (
|
|
<Alert icon={<IconAlertCircle size={16} />} color="red" variant="light">
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
<TextInput
|
|
label="Email"
|
|
placeholder="your@email.com"
|
|
required
|
|
{...form.getInputProps('email')}
|
|
/>
|
|
<PasswordInput
|
|
label="Password"
|
|
placeholder="Your password"
|
|
required
|
|
{...form.getInputProps('password')}
|
|
/>
|
|
<Button type="submit" fullWidth loading={loading}>
|
|
Sign in
|
|
</Button>
|
|
</Stack>
|
|
</form>
|
|
</Paper>
|
|
</Container>
|
|
);
|
|
}
|