33800292aa
- apps/api/Dockerfile: build NestJS, run prisma migrate deploy on start - apps/web/Dockerfile + nginx.conf: build Vite, serve static, proxy /api -> api - docker-compose.coolify.yml: full prod stack (postgres, redis, minio, keycloak, api, web) - .dockerignore / .gitignore / .gitattributes Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
155 lines
4.4 KiB
TypeScript
155 lines
4.4 KiB
TypeScript
import { useState } from 'react';
|
|
import { Box, Button, Select, Text, TextInput, Alert, Group, Stack } from '@mantine/core';
|
|
import { apiClient } from '../../api/client';
|
|
|
|
const font = "'Montserrat', Arial, sans-serif";
|
|
const teal = '#008286';
|
|
|
|
const ROLES = [
|
|
{ value: 'hr_admin', label: 'HR Admin' },
|
|
{ value: 'hr_specialist', label: 'HR Specialist' },
|
|
{ value: 'nursing_director', label: 'Nursing Director' },
|
|
{ value: 'quality_auditor', label: 'Quality Auditor' },
|
|
{ value: 'manager', label: 'Manager' },
|
|
{ value: 'medic_familie', label: 'Medic Familie' },
|
|
{ value: 'employee', label: 'Angajat' },
|
|
];
|
|
|
|
interface Props {
|
|
onLogin: () => void;
|
|
}
|
|
|
|
export function LoginPage({ onLogin }: Props) {
|
|
const [username, setUsername] = useState('admin');
|
|
const [role, setRole] = useState<string>('hr_admin');
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const handleLogin = async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const res = await apiClient.post<{ token: string; username: string; role: string }>(
|
|
'/auth/dev-login',
|
|
{ username, role },
|
|
);
|
|
localStorage.setItem('kc_token', res.data.token);
|
|
localStorage.setItem('kc_username', res.data.username);
|
|
localStorage.setItem('kc_role', res.data.role);
|
|
onLogin();
|
|
} catch {
|
|
setError('Nu s-a putut autentifica. Verificați că API-ul rulează.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Box
|
|
style={{
|
|
minHeight: '100vh',
|
|
background: '#f8f9fa',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
}}
|
|
>
|
|
<Box
|
|
style={{
|
|
background: '#fff',
|
|
border: '1px solid #e9ecef',
|
|
borderTop: `4px solid ${teal}`,
|
|
borderRadius: 10,
|
|
padding: '40px 48px',
|
|
width: 400,
|
|
boxShadow: '0 4px 24px rgba(0,0,0,0.07)',
|
|
}}
|
|
>
|
|
{/* Logo */}
|
|
<Box style={{ textAlign: 'center', marginBottom: 32 }}>
|
|
<img
|
|
src="/logo-medpark.png"
|
|
alt="Medpark International Hospital"
|
|
style={{ height: 44, objectFit: 'contain' }}
|
|
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}
|
|
/>
|
|
<Text
|
|
style={{
|
|
fontFamily: font,
|
|
fontWeight: 700,
|
|
fontSize: '1.1rem',
|
|
color: teal,
|
|
marginTop: 12,
|
|
}}
|
|
>
|
|
HRM — Sistem de management HR
|
|
</Text>
|
|
<Text
|
|
style={{
|
|
fontFamily: font,
|
|
fontWeight: 300,
|
|
fontSize: '0.75rem',
|
|
color: '#adb5bd',
|
|
marginTop: 4,
|
|
}}
|
|
>
|
|
Mod dezvoltare — autentificare locală
|
|
</Text>
|
|
</Box>
|
|
|
|
{error && (
|
|
<Alert color="red" mb={20} style={{ fontFamily: font, fontWeight: 300, fontSize: '0.85rem' }}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
<Stack gap={16}>
|
|
<TextInput
|
|
label="Utilizator"
|
|
value={username}
|
|
onChange={(e) => setUsername(e.currentTarget.value)}
|
|
placeholder="admin"
|
|
styles={{
|
|
label: { fontFamily: font, fontWeight: 500, fontSize: '0.8rem' },
|
|
input: { fontFamily: font },
|
|
}}
|
|
/>
|
|
|
|
<Select
|
|
label="Rol"
|
|
value={role}
|
|
onChange={(v) => setRole(v ?? 'hr_admin')}
|
|
data={ROLES}
|
|
styles={{
|
|
label: { fontFamily: font, fontWeight: 500, fontSize: '0.8rem' },
|
|
input: { fontFamily: font },
|
|
}}
|
|
/>
|
|
|
|
<Button
|
|
fullWidth
|
|
mt={8}
|
|
loading={loading}
|
|
onClick={() => void handleLogin()}
|
|
style={{
|
|
background: teal,
|
|
fontFamily: font,
|
|
fontWeight: 600,
|
|
fontSize: '0.9rem',
|
|
height: 42,
|
|
}}
|
|
>
|
|
Autentifică-te
|
|
</Button>
|
|
</Stack>
|
|
|
|
<Group justify="center" mt={24}>
|
|
<Text style={{ fontFamily: font, fontWeight: 300, fontSize: '0.7rem', color: '#ced4da' }}>
|
|
În producție autentificarea se face prin Keycloak SSO
|
|
</Text>
|
|
</Group>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|