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>
93 lines
5.4 KiB
TypeScript
93 lines
5.4 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, useQueryClient } from '@tanstack/react-query';
|
|
import { notifications } from '@mantine/notifications';
|
|
import dayjs from 'dayjs';
|
|
import { apiClient } from '../../../api/client';
|
|
import type { Qualification } from '../../../api/types';
|
|
|
|
const font = "'Montserrat', Arial, sans-serif";
|
|
const teal = '#008286';
|
|
|
|
const schema = z.object({
|
|
categorie: z.enum(['fara', 'cat_II', 'cat_I', 'superioara']),
|
|
specialitate: z.string().optional(),
|
|
dataObtinerii: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().or(z.literal('')),
|
|
dataUltimeiConfirmari: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().or(z.literal('')),
|
|
dataExpirarii: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().or(z.literal('')),
|
|
});
|
|
type FormValues = z.infer<typeof schema>;
|
|
|
|
interface Props { employeeId: string; record?: Qualification; opened: boolean; onClose: () => void }
|
|
|
|
export function QualificationDrawer({ employeeId, record, opened, onClose }: Props) {
|
|
const isEdit = !!record;
|
|
const qc = useQueryClient();
|
|
|
|
const { register, handleSubmit, control, reset, formState: { errors, isSubmitting } } =
|
|
useForm<FormValues>({ resolver: zodResolver(schema) });
|
|
|
|
useEffect(() => {
|
|
reset(record ? {
|
|
categorie: record.categorie,
|
|
specialitate: record.specialitate ?? '',
|
|
dataObtinerii: record.dataObtinerii?.slice(0, 10) ?? '',
|
|
dataUltimeiConfirmari: record.dataUltimeiConfirmari?.slice(0, 10) ?? '',
|
|
dataExpirarii: record.dataExpirarii?.slice(0, 10) ?? '',
|
|
} : {});
|
|
}, [record, reset, opened]);
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: (data: FormValues) => {
|
|
const payload = { ...data, dataObtinerii: data.dataObtinerii || undefined, dataUltimeiConfirmari: data.dataUltimeiConfirmari || undefined, dataExpirarii: data.dataExpirarii || undefined };
|
|
return isEdit
|
|
? apiClient.patch(`/employees/${employeeId}/qualifications/${record.id}`, payload)
|
|
: apiClient.post(`/employees/${employeeId}/qualifications`, payload);
|
|
},
|
|
onSuccess: () => { void qc.invalidateQueries({ queryKey: ['employee', employeeId] }); notifications.show({ color: 'medpark', title: 'Salvat', message: 'Calificare actualizată.' }); 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ă calificare' : 'Calificare 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="categorie" control={control} render={({ field }) => (
|
|
<Select label="Categorie *" data={[
|
|
{ value: 'fara', label: 'Fără categorie' }, { value: 'cat_II', label: 'Categoria II' },
|
|
{ value: 'cat_I', label: 'Categoria I' }, { value: 'superioara', label: 'Superioară' },
|
|
]} value={field.value} onChange={field.onChange} error={errors.categorie?.message}
|
|
styles={{ label: { fontFamily: font, fontWeight: 500, fontSize: '0.8rem' } }} />
|
|
)} />
|
|
<TextInput label="Specialitate" styles={{ label: { fontFamily: font, fontWeight: 500, fontSize: '0.8rem' } }} {...register('specialitate')} />
|
|
<Controller name="dataObtinerii" control={control} render={({ field }) => (
|
|
<DateInput label="Data obținerii" 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' } }} />
|
|
)} />
|
|
<Controller name="dataUltimeiConfirmari" control={control} render={({ field }) => (
|
|
<DateInput label="Ultima confirmare" 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' } }} />
|
|
)} />
|
|
<Controller name="dataExpirarii" control={control} render={({ field }) => (
|
|
<DateInput label="Data expirării" 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' } }} />
|
|
)} />
|
|
</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>
|
|
);
|
|
}
|