Files
hrm-medpark/apps/web/src/pages/employees/drawers/FamilyMemberDrawer.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

109 lines
5.7 KiB
TypeScript

import { useEffect } from 'react';
import { Drawer, Select, TextInput, Button, Group, Stack, Text, LoadingOverlay, Box } from '@mantine/core';
import { DateInput } from '@mantine/dates';
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { notifications } from '@mantine/notifications';
import dayjs from 'dayjs';
import { apiClient } from '../../../api/client';
import type { FamilyMember, TaxExemption } from '../../../api/types';
const font = "'Montserrat', Arial, sans-serif";
const teal = '#008286';
const schema = z.object({
tip: z.enum(['contact_principal', 'sot', 'sotie', 'mama', 'tata', 'copil']),
numePrenume: z.string().min(1, 'Câmp obligatoriu'),
dataNasterii: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().or(z.literal('')),
idnp: z.string().length(13).optional().or(z.literal('')),
telefon: z.string().optional(),
tipScutireId: z.string().uuid().optional().or(z.literal('')),
});
type FormValues = z.infer<typeof schema>;
interface Props { employeeId: string; record?: FamilyMember; opened: boolean; onClose: () => void }
export function FamilyMemberDrawer({ employeeId, record, opened, onClose }: Props) {
const isEdit = !!record;
const qc = useQueryClient();
const { data: exemptions } = useQuery({
queryKey: ['ref', 'tax-exemptions'],
queryFn: () => apiClient.get<TaxExemption[]>('/reference/tax-exemptions').then((r) => r.data),
staleTime: 300_000,
});
const { register, handleSubmit, control, reset, watch, formState: { errors, isSubmitting } } =
useForm<FormValues>({ resolver: zodResolver(schema) });
useEffect(() => {
reset(record
? {
tip: record.tip,
numePrenume: record.numePrenume,
dataNasterii: record.dataNasterii?.slice(0, 10) ?? '',
idnp: record.idnp ?? '',
telefon: record.telefon ?? undefined,
tipScutireId: record.tipScutireId ?? '',
}
: {});
}, [record, reset, opened]);
const tipValue = watch('tip');
const mutation = useMutation({
mutationFn: (data: FormValues) => {
const payload = { ...data, dataNasterii: data.dataNasterii || undefined, idnp: data.idnp || undefined, tipScutireId: data.tipScutireId || undefined };
return isEdit
? apiClient.patch(`/employees/${employeeId}/family-members/${record.id}`, payload)
: apiClient.post(`/employees/${employeeId}/family-members`, payload);
},
onSuccess: () => { void qc.invalidateQueries({ queryKey: ['employee', employeeId] }); notifications.show({ color: 'medpark', title: 'Salvat', message: 'Modificat.' }); onClose(); },
onError: () => notifications.show({ color: 'red', title: 'Eroare', message: 'Nu s-a putut salva.' }),
});
return (
<Drawer opened={opened} onClose={onClose} position="right" size="md"
title={<Text style={{ fontFamily: font, fontWeight: 700, color: '#58595b' }}>{isEdit ? 'Editează membru' : 'Membru nou'}</Text>}
styles={{ header: { borderBottom: `2px solid ${teal}` }, body: { padding: '16px 24px 24px' } }}
>
<Box style={{ position: 'relative' }}><LoadingOverlay visible={isSubmitting} />
<form onSubmit={handleSubmit((d) => mutation.mutate(d))}>
<Stack gap={12}>
<Controller name="tip" control={control} render={({ field }) => (
<Select label="Tip *" data={[
{ value: 'contact_principal', label: 'Contact principal' }, { value: 'sot', label: 'Soț' },
{ value: 'sotie', label: 'Soție' }, { value: 'mama', label: 'Mamă' },
{ value: 'tata', label: 'Tată' }, { value: 'copil', label: 'Copil' },
]} value={field.value} onChange={field.onChange} error={errors.tip?.message}
styles={{ label: { fontFamily: font, fontWeight: 500, fontSize: '0.8rem' } }} />
)} />
<TextInput label="Nume prenume *" error={errors.numePrenume?.message} styles={{ label: { fontFamily: font, fontWeight: 500, fontSize: '0.8rem' } }} {...register('numePrenume')} />
<TextInput label="IDNP" maxLength={13} styles={{ label: { fontFamily: font, fontWeight: 500, fontSize: '0.8rem' } }} {...register('idnp')} />
<Controller name="dataNasterii" control={control} render={({ field }) => (
<DateInput label="Data nașterii" valueFormat="YYYY-MM-DD" value={field.value ? new Date(field.value) : null}
onChange={(d) => field.onChange(d ? dayjs(d).format('YYYY-MM-DD') : '')}
styles={{ label: { fontFamily: font, fontWeight: 500, fontSize: '0.8rem' } }} />
)} />
<TextInput label="Telefon" styles={{ label: { fontFamily: font, fontWeight: 500, fontSize: '0.8rem' } }} {...register('telefon')} />
{tipValue === 'copil' && (
<Controller name="tipScutireId" control={control} render={({ field }) => (
<Select label="Scutire fiscală (IRS)" clearable
data={(exemptions ?? []).map((e) => ({ value: e.id, label: `${e.code}${e.description}` }))}
value={field.value || null} onChange={(v) => field.onChange(v ?? '')}
styles={{ label: { fontFamily: font, fontWeight: 500, fontSize: '0.8rem' } }} />
)} />
)}
</Stack>
<Group justify="flex-end" mt={24} pt={16} style={{ borderTop: '1px solid #e9ecef' }}>
<Button variant="subtle" onClick={onClose} style={{ fontFamily: font, color: '#58595b' }}>Anulează</Button>
<Button type="submit" loading={isSubmitting} style={{ background: teal, fontFamily: font, fontWeight: 500 }}>Salvează</Button>
</Group>
</form>
</Box>
</Drawer>
);
}