- Database: Add login_history, ai_recommendation_log tables; is_platform_owner column on users; subscription fields on organizations (payment_date, confirmation_number, renewal_date) - Backend: New AdminAnalyticsService with platform metrics, tenant detail, and health score calculations (0-100 based on activity, budget, transactions, members, AI usage) - Backend: Login/org-switch now records to login_history; AI recommendations logged to ai_recommendation_log; platform owner protected from superadmin toggle - Frontend: 4-tab admin panel (Dashboard, Organizations, Users, Tenant Health) with tenant detail drawer, subscription management, health scoring visualization - Platform owner account (admin@hoaledgeriq.com) auto-redirects to admin panel - Seed data includes platform owner account and sample login history Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
98 lines
2.8 KiB
TypeScript
98 lines
2.8 KiB
TypeScript
import { useState } from 'react';
|
|
import {
|
|
Center,
|
|
Container,
|
|
Paper,
|
|
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';
|
|
import logoSrc from '../../assets/logo.svg';
|
|
|
|
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);
|
|
// Platform owner / superadmin with no orgs → admin panel
|
|
if (data.user?.isSuperadmin && data.organizations.length === 0) {
|
|
navigate('/admin');
|
|
} else if (data.organizations.length >= 1) {
|
|
// Always go through org selection to ensure correct JWT with orgSchema
|
|
navigate('/select-org');
|
|
} else {
|
|
navigate('/');
|
|
}
|
|
} catch (err: any) {
|
|
setError(err.response?.data?.message || 'Login failed');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Container size={420} my={80}>
|
|
<Center>
|
|
<img src={logoSrc} alt="HOA LedgerIQ" style={{ height: 60 }} />
|
|
</Center>
|
|
<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>
|
|
);
|
|
}
|