Initial commit: Helpdesk application setup
- Backend: Node.js/TypeScript with Prisma ORM - Frontend: Vite + TypeScript - Project configuration and documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
204
frontend/src/pages/tasks/TaskForm.tsx
Normal file
204
frontend/src/pages/tasks/TaskForm.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
|
||||
import { tasksApi, type CreateTaskData } from '@/services/tasks.api';
|
||||
import { projectsApi } from '@/services/projects.api';
|
||||
import { settingsApi } from '@/services/settings.api'; // Pre statusy a priority
|
||||
import type { Task } from '@/types';
|
||||
import { Button, Input, Textarea, Select, ModalFooter, UserSelect } from '@/components/ui';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const taskSchema = z.object({
|
||||
title: z.string().min(1, 'Názov je povinný'),
|
||||
description: z.string().optional(),
|
||||
projectId: z.string().optional(),
|
||||
statusId: z.string().optional(),
|
||||
priorityId: z.string().optional(),
|
||||
deadline: z.string().optional(),
|
||||
});
|
||||
|
||||
type TaskFormData = z.infer<typeof taskSchema>;
|
||||
|
||||
interface TaskFormProps {
|
||||
task: Task | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function TaskForm({ task, onClose }: TaskFormProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const isEditing = !!task;
|
||||
|
||||
// State pre vybraných používateľov
|
||||
const [selectedAssignees, setSelectedAssignees] = useState<string[]>(
|
||||
task?.assignees?.map((a) => a.userId) || []
|
||||
);
|
||||
|
||||
const { data: projectsData } = useQuery({
|
||||
queryKey: ['projects-select'],
|
||||
queryFn: () => projectsApi.getAll({ limit: 1000 }),
|
||||
});
|
||||
|
||||
const { data: statusesData } = useQuery({
|
||||
queryKey: ['task-statuses'],
|
||||
queryFn: () => settingsApi.getTaskStatuses(),
|
||||
});
|
||||
|
||||
const { data: prioritiesData } = useQuery({
|
||||
queryKey: ['priorities'],
|
||||
queryFn: () => settingsApi.getPriorities(),
|
||||
});
|
||||
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<TaskFormData>({
|
||||
resolver: zodResolver(taskSchema),
|
||||
defaultValues: task
|
||||
? {
|
||||
title: task.title,
|
||||
description: task.description || '',
|
||||
projectId: task.projectId || '',
|
||||
statusId: task.statusId,
|
||||
priorityId: task.priorityId,
|
||||
deadline: task.deadline?.split('T')[0] || '',
|
||||
}
|
||||
: {
|
||||
title: '',
|
||||
description: '',
|
||||
projectId: '',
|
||||
statusId: '',
|
||||
priorityId: '',
|
||||
deadline: '',
|
||||
},
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: CreateTaskData) => tasksApi.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||
toast.success('Úloha bola vytvorená');
|
||||
onClose();
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
console.error('Create task error:', error);
|
||||
const axiosError = error as { response?: { data?: { message?: string } } };
|
||||
const message = axiosError.response?.data?.message || 'Chyba pri vytváraní úlohy';
|
||||
toast.error(message);
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: (data: CreateTaskData) => tasksApi.update(task!.id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||
toast.success('Úloha bola aktualizovaná');
|
||||
onClose();
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
console.error('Update task error:', error);
|
||||
const axiosError = error as { response?: { data?: { message?: string } } };
|
||||
const message = axiosError.response?.data?.message || 'Chyba pri aktualizácii úlohy';
|
||||
toast.error(message);
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (data: TaskFormData) => {
|
||||
const cleanData = {
|
||||
...data,
|
||||
projectId: data.projectId || undefined,
|
||||
statusId: data.statusId || undefined,
|
||||
priorityId: data.priorityId || undefined,
|
||||
deadline: data.deadline || undefined,
|
||||
// Pre create: undefined ak prázdne (backend priradí default)
|
||||
// Pre update: vždy poslať pole (aj prázdne) aby sa aktualizovali assignees
|
||||
assigneeIds: isEditing ? selectedAssignees : (selectedAssignees.length > 0 ? selectedAssignees : undefined),
|
||||
};
|
||||
|
||||
console.log('Submitting task data:', cleanData);
|
||||
|
||||
if (isEditing) {
|
||||
updateMutation.mutate(cleanData);
|
||||
} else {
|
||||
createMutation.mutate(cleanData);
|
||||
}
|
||||
};
|
||||
|
||||
const isPending = createMutation.isPending || updateMutation.isPending;
|
||||
|
||||
const projectOptions = projectsData?.data.map((p) => ({ value: p.id, label: p.name })) || [];
|
||||
const statusOptions = statusesData?.data.map((s) => ({ value: s.id, label: s.name })) || [];
|
||||
const priorityOptions = prioritiesData?.data.map((p) => ({ value: p.id, label: p.name })) || [];
|
||||
|
||||
// Pripraviť počiatočných používateľov pre editáciu (už priradení)
|
||||
const initialAssignees = task?.assignees?.map((a) => ({
|
||||
id: a.userId,
|
||||
name: a.user?.name || '',
|
||||
email: a.user?.email || '',
|
||||
})) || [];
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<Input
|
||||
id="title"
|
||||
label="Názov *"
|
||||
error={errors.title?.message}
|
||||
{...register('title')}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
id="description"
|
||||
label="Popis"
|
||||
rows={3}
|
||||
{...register('description')}
|
||||
/>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Select
|
||||
id="projectId"
|
||||
label="Projekt"
|
||||
options={[{ value: '', label: '-- Bez projektu --' }, ...projectOptions]}
|
||||
{...register('projectId')}
|
||||
/>
|
||||
<Select
|
||||
id="statusId"
|
||||
label="Stav"
|
||||
options={[{ value: '', label: '-- Predvolený --' }, ...statusOptions]}
|
||||
{...register('statusId')}
|
||||
/>
|
||||
<Select
|
||||
id="priorityId"
|
||||
label="Priorita"
|
||||
options={[{ value: '', label: '-- Predvolená --' }, ...priorityOptions]}
|
||||
{...register('priorityId')}
|
||||
/>
|
||||
<Input
|
||||
id="deadline"
|
||||
type="date"
|
||||
label="Termín"
|
||||
{...register('deadline')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UserSelect
|
||||
label="Priradiť na"
|
||||
selectedIds={selectedAssignees}
|
||||
onChange={setSelectedAssignees}
|
||||
initialUsers={initialAssignees}
|
||||
placeholder="Vyhľadať používateľa..."
|
||||
/>
|
||||
|
||||
<ModalFooter>
|
||||
<Button type="button" variant="outline" onClick={onClose}>
|
||||
Zrušiť
|
||||
</Button>
|
||||
<Button type="submit" isLoading={isPending}>
|
||||
{isEditing ? 'Uložiť' : 'Vytvoriť'}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user