Files
hrm-medpark/apps/web/src/pages/auth/LoginPage.tsx
T
Danil Suhomlinov 33800292aa chore: add Coolify deployment scaffolding (Dockerfiles, prod compose, git hygiene)
- 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>
2026-06-08 17:42:45 +03:00

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>
);
}