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>
This commit is contained in:
@@ -0,0 +1,378 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Title,
|
||||
TextInput,
|
||||
Button,
|
||||
Group,
|
||||
Text,
|
||||
Loader,
|
||||
Center,
|
||||
Select,
|
||||
Pagination,
|
||||
Stack,
|
||||
ActionIcon,
|
||||
Tooltip,
|
||||
Box,
|
||||
Paper,
|
||||
} from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { IconSearch } from '@tabler/icons-react';
|
||||
import { apiClient } from '../../api/client';
|
||||
import { EmployeeDrawer } from './components/EmployeeDrawer';
|
||||
|
||||
type EmployeeStatus = 'activ' | 'concediat' | 'suspendat';
|
||||
|
||||
interface EmployeeRow {
|
||||
id: string;
|
||||
idnp: string;
|
||||
nume: string;
|
||||
prenume: string;
|
||||
sex: 'F' | 'M';
|
||||
status: EmployeeStatus;
|
||||
telefonPersonal: string;
|
||||
contracts: {
|
||||
functiaOrganigrama: string | null;
|
||||
department: { name: string };
|
||||
}[];
|
||||
}
|
||||
|
||||
const STATUS_CONFIG: Record<EmployeeStatus, { color: string; bg: string; label_ro: string }> = {
|
||||
activ: { color: '#008286', bg: '#e6f4f4', label_ro: 'Activ' },
|
||||
concediat: { color: '#b11116', bg: '#ffeaea', label_ro: 'Concediat' },
|
||||
suspendat: { color: '#f15a31', bg: '#fff3ee', label_ro: 'Suspendat' },
|
||||
};
|
||||
|
||||
const font = "'Montserrat', Arial, sans-serif";
|
||||
const charcoal = '#58595b';
|
||||
const teal = '#008286';
|
||||
const border = '#e9ecef';
|
||||
|
||||
export function EmployeesPage() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [search, setSearch] = useState('');
|
||||
const [status, setStatus] = useState<string | null>(null);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['employees', search, status, page],
|
||||
queryFn: () =>
|
||||
apiClient
|
||||
.get<{ total: number; items: EmployeeRow[] }>('/employees', {
|
||||
params: { search: search || undefined, status: status || undefined, page },
|
||||
})
|
||||
.then((r) => r.data),
|
||||
});
|
||||
|
||||
const totalPages = Math.ceil((data?.total ?? 0) / 20);
|
||||
|
||||
return (
|
||||
<Stack gap={24}>
|
||||
{/* ── Page header ── */}
|
||||
<Group justify="space-between" align="flex-end">
|
||||
<Box>
|
||||
<Title
|
||||
order={2}
|
||||
style={{
|
||||
fontFamily: font,
|
||||
fontWeight: 700,
|
||||
color: charcoal,
|
||||
fontSize: '1.5rem',
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
{t('employees.title')}
|
||||
</Title>
|
||||
{/* Teal accent underline */}
|
||||
<Box style={{ width: 40, height: 3, background: teal, borderRadius: 2 }} />
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
onClick={() => setDrawerOpen(true)}
|
||||
style={{
|
||||
background: teal,
|
||||
fontFamily: font,
|
||||
fontWeight: 500,
|
||||
fontSize: '0.875rem',
|
||||
height: 40,
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
}}
|
||||
>
|
||||
+ {t('employees.add')}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* ── Filters ── */}
|
||||
<Paper
|
||||
shadow="none"
|
||||
p={20}
|
||||
style={{ border: `1px solid ${border}`, borderRadius: 8, background: '#ffffff' }}
|
||||
>
|
||||
<Group gap={12}>
|
||||
<TextInput
|
||||
placeholder={t('employees.search')}
|
||||
value={search}
|
||||
onChange={(e) => { setSearch(e.currentTarget.value); setPage(1); }}
|
||||
style={{ flex: 1 }}
|
||||
styles={{
|
||||
input: {
|
||||
fontFamily: font,
|
||||
fontWeight: 300,
|
||||
fontSize: '0.875rem',
|
||||
color: charcoal,
|
||||
borderColor: border,
|
||||
'&:focus': { borderColor: teal },
|
||||
},
|
||||
}}
|
||||
leftSection={<IconSearch size={16} color="#adb5bd" />}
|
||||
/>
|
||||
<Select
|
||||
placeholder={t('employees.status.all') ?? 'Statut'}
|
||||
clearable
|
||||
data={[
|
||||
{ value: 'activ', label: 'Activ' },
|
||||
{ value: 'concediat', label: 'Concediat' },
|
||||
{ value: 'suspendat', label: 'Suspendat' },
|
||||
]}
|
||||
value={status}
|
||||
onChange={(v) => { setStatus(v); setPage(1); }}
|
||||
style={{ width: 160 }}
|
||||
styles={{
|
||||
input: { fontFamily: font, fontWeight: 300, fontSize: '0.875rem', color: charcoal },
|
||||
}}
|
||||
/>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
{/* ── Table ── */}
|
||||
<Paper
|
||||
shadow="none"
|
||||
style={{ border: `1px solid ${border}`, borderRadius: 8, overflow: 'hidden', background: '#ffffff' }}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Center h={200}><Loader color="medpark" size="sm" /></Center>
|
||||
) : (
|
||||
<>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<table className="brand-table" style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ background: '#fafafa' }}>
|
||||
{[
|
||||
t('employees.columns.idnp'),
|
||||
t('employees.columns.name'),
|
||||
t('employees.columns.position'),
|
||||
t('employees.columns.department'),
|
||||
t('employees.columns.phone'),
|
||||
t('employees.columns.status'),
|
||||
'',
|
||||
].map((col, i) => (
|
||||
<th
|
||||
key={i}
|
||||
style={{
|
||||
fontFamily: font,
|
||||
fontWeight: 600,
|
||||
fontSize: '0.7rem',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.07em',
|
||||
color: charcoal,
|
||||
padding: '14px 16px',
|
||||
textAlign: 'left',
|
||||
borderBottom: `2px solid ${teal}`,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{col}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="hrm-stagger">
|
||||
{!data?.items.length ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={7}
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
padding: 48,
|
||||
fontFamily: font,
|
||||
fontWeight: 300,
|
||||
color: '#adb5bd',
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
>
|
||||
Niciun angajat găsit
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
data.items.map((emp, idx) => {
|
||||
const st = STATUS_CONFIG[emp.status];
|
||||
return (
|
||||
<tr
|
||||
key={emp.id}
|
||||
onClick={() => navigate(`/employees/${emp.id}`)}
|
||||
style={{
|
||||
background: idx % 2 === 0 ? '#ffffff' : '#fafafa',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.1s',
|
||||
}}
|
||||
onMouseEnter={(e) =>
|
||||
((e.currentTarget as HTMLElement).style.background = '#e6f4f4')
|
||||
}
|
||||
onMouseLeave={(e) =>
|
||||
((e.currentTarget as HTMLElement).style.background =
|
||||
idx % 2 === 0 ? '#ffffff' : '#fafafa')
|
||||
}
|
||||
>
|
||||
<td
|
||||
style={{
|
||||
padding: '13px 16px',
|
||||
borderBottom: `1px solid ${border}`,
|
||||
fontFamily: "'Courier New', monospace",
|
||||
fontSize: '0.8rem',
|
||||
color: '#6c757d',
|
||||
letterSpacing: '0.04em',
|
||||
}}
|
||||
>
|
||||
{emp.idnp}
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: '13px 16px',
|
||||
borderBottom: `1px solid ${border}`,
|
||||
fontFamily: font,
|
||||
fontWeight: 500,
|
||||
fontSize: '0.875rem',
|
||||
color: charcoal,
|
||||
}}
|
||||
>
|
||||
{emp.nume} {emp.prenume}
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: '13px 16px',
|
||||
borderBottom: `1px solid ${border}`,
|
||||
fontFamily: font,
|
||||
fontWeight: 300,
|
||||
fontSize: '0.875rem',
|
||||
color: charcoal,
|
||||
}}
|
||||
>
|
||||
{emp.contracts[0]?.functiaOrganigrama ?? (
|
||||
<Text c="#ced4da" size="sm" style={{ fontFamily: font }}>—</Text>
|
||||
)}
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: '13px 16px',
|
||||
borderBottom: `1px solid ${border}`,
|
||||
fontFamily: font,
|
||||
fontWeight: 300,
|
||||
fontSize: '0.875rem',
|
||||
color: charcoal,
|
||||
}}
|
||||
>
|
||||
{emp.contracts[0]?.department.name ?? (
|
||||
<Text c="#ced4da" size="sm" style={{ fontFamily: font }}>—</Text>
|
||||
)}
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: '13px 16px',
|
||||
borderBottom: `1px solid ${border}`,
|
||||
fontFamily: font,
|
||||
fontWeight: 300,
|
||||
fontSize: '0.875rem',
|
||||
color: charcoal,
|
||||
}}
|
||||
>
|
||||
{emp.telefonPersonal}
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: '13px 16px',
|
||||
borderBottom: `1px solid ${border}`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
padding: '3px 10px',
|
||||
borderRadius: 20,
|
||||
background: st.bg,
|
||||
color: st.color,
|
||||
fontFamily: font,
|
||||
fontWeight: 600,
|
||||
fontSize: '0.72rem',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
}}
|
||||
>
|
||||
{st.label_ro}
|
||||
</Box>
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: '13px 12px',
|
||||
borderBottom: `1px solid ${border}`,
|
||||
textAlign: 'right',
|
||||
}}
|
||||
>
|
||||
<Tooltip label="Vizualizează" position="left" withArrow>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="medpark"
|
||||
size="sm"
|
||||
style={{ fontFamily: font }}
|
||||
>
|
||||
→
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</Box>
|
||||
|
||||
{/* Footer: count + pagination */}
|
||||
{(data?.total ?? 0) > 0 && (
|
||||
<Group justify="space-between" px={20} py={14} style={{ borderTop: `1px solid ${border}` }}>
|
||||
<Text
|
||||
size="xs"
|
||||
c="#adb5bd"
|
||||
style={{ fontFamily: font, fontWeight: 300 }}
|
||||
>
|
||||
{data?.total} angajați total
|
||||
</Text>
|
||||
{totalPages > 1 && (
|
||||
<Pagination
|
||||
total={totalPages}
|
||||
value={page}
|
||||
onChange={setPage}
|
||||
size="sm"
|
||||
color="medpark"
|
||||
styles={{
|
||||
control: { fontFamily: font, fontWeight: 500 },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
<EmployeeDrawer
|
||||
opened={drawerOpen}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user