pmtool / src /components /tasks /TaskForm.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
17.1 kB
import React, { useState, useEffect } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { format } from "date-fns";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Task, TaskCreateRequest } from "@/services/tasksApi";
import { issuesApi, employeeService, projectService } from "@/lib/api"; // Import the project service
import { IssueApi, Employee, ProjectApi } from "@/types"; // Import ProjectApi
import { Dropdown, DropdownOption } from "@/components/ui/dropdown"; // Import the Dropdown component
import { useAuth } from "@/lib/auth-context"; // Correct import for useAuth
import SlateRichTextEditor from "@/components/custom/SlateRichTextEditor"; // Import SlateRichTextEditor
// Helper function to ensure content is HTML formatted
const ensureHtmlFormat = (content: string): string => {
if (!content) return '';
// Check if the content already looks like HTML (contains HTML tags)
const hasHtmlTags = /<[a-z][\s\S]*>/i.test(content);
// If it's already HTML, return as is
if (hasHtmlTags) {
return content;
}
// Otherwise, wrap it in a paragraph tag
return `<p>${content}</p>`;
};
// Extend TaskCreateRequest to include projectId
interface ExtendedTaskCreateRequest extends TaskCreateRequest {
projectId?: number | null;
}
const taskSchema = z.object({
id: z.number().optional(),
type: z.string().min(1, "Type is required"),
description: z.string().min(1, "Description is required"),
sprintName: z.string().nullable().optional(),
estimates: z.string().min(1, "Estimates is required"),
esUnit: z.number().nullable().optional(),
stateDate: z.string().min(1, "Start date is required"),
endDate: z.string().nullable().optional(),
assignedTo: z.number().nullable().optional(),
remainingHr: z.number().nullable().optional(),
status: z.string().min(1, "Status is required"),
issuesId: z.number().min(1, "Feature is required"),
title: z.string().min(1, "Title is required"),
projectId: z.number().nullable().optional(), // Add projectId field
});
interface TaskFormProps {
task?: Task & { projectId?: number | null };
onSubmit: (data: ExtendedTaskCreateRequest) => Promise<void>;
onCancel: () => void;
isLoading?: boolean;
}
const statusOptions = ["New", "In Progress", "In Review", "Completed", "Closed"];
const typeOptions = ["Task"];
const estimateUnitOptions: DropdownOption[] = [
{ label: "Days", value: "1" },
{ label: "Hours", value: "2" }
];
export function TaskForm({ task, onSubmit, onCancel, isLoading }: TaskFormProps) {
const [issues, setIssues] = useState<IssueApi[]>([]);
const [filteredIssues, setFilteredIssues] = useState<IssueApi[]>([]);
const [isLoadingIssues, setIsLoadingIssues] = useState(false);
const [employees, setEmployees] = useState<Employee[]>([]);
const [isLoadingEmployees, setIsLoadingEmployees] = useState(false);
const [projects, setProjects] = useState<ProjectApi[]>([]);
const [isLoadingProjects, setIsLoadingProjects] = useState(false);
const isNewTask = !task?.id;
const { userData } = useAuth(); // Add useAuth hook to get current user
// Helper function to format date strings for input fields
const formatDateStringForInput = (dateString: string): string => {
// If date includes time part (T), parse and format it
if (dateString.includes('T')) {
const date = new Date(dateString);
return format(date, "yyyy-MM-dd");
}
// Return as is if it's already in yyyy-MM-dd format
return dateString;
};
const defaultValues: ExtendedTaskCreateRequest = {
id: task?.id || 0,
type: task?.type || "Task",
description: task?.description ? ensureHtmlFormat(task.description) : "",
sprintName: task?.sprintName || null,
estimates: task?.estimates || (isNewTask ? "8" : ""),
esUnit: task?.esUnit || 1, // Default to Days (1)
stateDate: task?.stateDate ? formatDateStringForInput(task.stateDate) : format(new Date(), "yyyy-MM-dd"),
endDate: task?.endDate ? formatDateStringForInput(task.endDate) : null,
assignedTo: task?.assignedTo || null,
remainingHr: task?.remainingHr || null,
status: task?.status || "New",
issuesId: task?.issuesId || null,
title: task?.title || "",
projectId: task?.projectId || null, // Add projectId field
};
const form = useForm<ExtendedTaskCreateRequest>({
resolver: zodResolver(taskSchema),
defaultValues,
});
// Get the current projectId value from the form
const selectedProjectId = form.watch("projectId");
useEffect(() => {
// Reset form when task prop changes
if (task) {
console.log("Initializing form with task:", task);
form.reset({
...defaultValues,
description: task.description || "" // Ensure description is properly set
});
}
}, [task]);
// Additional effect to specifically handle description field initialization
useEffect(() => {
if (task?.description) {
console.log("Setting description value:", task.description);
form.setValue("description", ensureHtmlFormat(task.description));
}
}, [task?.description, form]);
useEffect(() => {
// Fetch issues when the component mounts
const fetchIssues = async () => {
setIsLoadingIssues(true);
try {
const issuesData = await issuesApi.getAll();
setIssues(issuesData);
setFilteredIssues(issuesData); // Initially, all issues are available
} catch (error) {
console.error("Error fetching issues:", error);
} finally {
setIsLoadingIssues(false);
}
};
// Fetch employees when the component mounts
const fetchEmployees = async () => {
setIsLoadingEmployees(true);
try {
const employeesData = await employeeService.getAll();
setEmployees(employeesData);
console.log("Fetched employees:", employeesData);
} catch (error) {
console.error("Error fetching employees:", error);
} finally {
setIsLoadingEmployees(false);
}
};
// Fetch projects when the component mounts
const fetchProjects = async () => {
setIsLoadingProjects(true);
try {
let projectsData;
if (userData?.employeeId) {
// If user has an employee ID, get only their projects
projectsData = await projectService.getByEmployeeId(userData.employeeId);
} else {
// Otherwise, get all projects
projectsData = await projectService.getAll();
}
setProjects(projectsData);
console.log("Fetched projects:", projectsData);
} catch (error) {
console.error("Error fetching projects:", error);
} finally {
setIsLoadingProjects(false);
}
};
fetchIssues();
fetchEmployees();
fetchProjects();
}, [userData?.employeeId]);
// Filter issues when project selection changes
useEffect(() => {
if (selectedProjectId) {
const projectIssues = issues.filter(issue => issue.projectId === selectedProjectId);
setFilteredIssues(projectIssues);
// If we have a current issue selected that doesn't belong to this project, clear it
const currentIssueId = form.getValues("issuesId");
if (currentIssueId) {
const issueExists = projectIssues.some(issue => issue.id === currentIssueId);
if (!issueExists) {
form.setValue("issuesId", null);
}
}
} else {
// If no project selected, show all issues
setFilteredIssues(issues);
}
}, [selectedProjectId, issues, form]);
// Create dropdown options from filtered issues
const issueOptions: DropdownOption[] = filteredIssues.map(issue => ({
label: `${issue.title}`,
value: issue.id.toString()
}));
// Create dropdown options from projects
const projectOptions: DropdownOption[] = [
{ label: "Select Project", value: "" }, // Add an empty option
...projects.map(project => ({
label: project.projectName,
value: project.id.toString()
}))
];
// Create dropdown options for employees
const employeeOptions: DropdownOption[] = employees.map(employee => ({
label: `${employee.firstName} ${employee.lastName}`,
value: employee.id.toString()
}));
// Create dropdown options for status and type
const statusDropdownOptions: DropdownOption[] = statusOptions.map(status => ({
label: status,
value: status
}));
const typeDropdownOptions: DropdownOption[] = typeOptions.map(type => ({
label: type,
value: type
}));
const handleSubmit = async (data: ExtendedTaskCreateRequest) => {
try {
console.log("Raw form data:", data);
// Create a new object with null values for empty fields
const formattedData = Object.entries(data).reduce((acc, [key, value]) => {
// Check if the value is empty string or 0 (except for certain fields like ID)
if (key !== 'id' && (value === '' || value === 0)) {
acc[key] = null;
} else {
acc[key] = value;
}
return acc;
}, {} as any);
console.log("Submitting task with formatted data:", formattedData);
await onSubmit(formattedData);
} catch (error) {
console.error("Error submitting form:", error);
}
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input placeholder="Enter task title" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="status"
render={({ field }) => (
<FormItem>
<FormLabel>Status</FormLabel>
<FormControl>
<Dropdown
options={statusDropdownOptions}
value={field.value}
onValueChange={field.onChange}
placeholder="Select status"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem className="md:col-span-2">
<FormLabel>Description</FormLabel>
<FormControl>
<SlateRichTextEditor
key={task?.id || 'new-task'}
initialValue={ensureHtmlFormat(field.value || '')}
onChange={(value) => {
console.log("Description changed:", value);
field.onChange(value);
}}
placeholder="Enter task description"
minHeight="150px"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Project Selection Field */}
<FormField
control={form.control}
name="projectId"
render={({ field }) => (
<FormItem>
<FormLabel>Project</FormLabel>
<FormControl>
<Dropdown
options={projectOptions}
value={field.value ? field.value.toString() : ""}
onValueChange={(value) => {
field.onChange(value ? parseInt(value) : null);
}}
placeholder="Select project"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="issuesId"
render={({ field }) => (
<FormItem>
<FormLabel>Feature</FormLabel>
<FormControl>
<Dropdown
options={issueOptions}
value={field.value ? field.value.toString() : ""}
onValueChange={(value) => {
field.onChange(value ? parseInt(value) : null);
}}
placeholder={isLoadingIssues ? "Loading Features..." : "Select Feature"}
disabled={isLoadingIssues || issueOptions.length === 0}
/>
</FormControl>
{issueOptions.length === 0 && !isLoadingIssues && selectedProjectId && (
<p className="text-xs text-red-500">No Features available for this project</p>
)}
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="estimates"
render={({ field }) => (
<FormItem>
<FormLabel>Estimates</FormLabel>
<FormControl>
<Input placeholder="Enter estimates" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="stateDate"
render={({ field }) => (
<FormItem>
<FormLabel>Start Date</FormLabel>
<FormControl>
<Input type="date" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="endDate"
render={({ field }) => (
<FormItem>
<FormLabel>End Date</FormLabel>
<FormControl>
<Input type="date" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="assignedTo"
render={({ field }) => (
<FormItem>
<FormLabel>Assigned To</FormLabel>
<FormControl>
<Dropdown
options={employeeOptions}
value={field.value?.toString() || ""}
onValueChange={(value) => field.onChange(value ? Number(value) : null)}
placeholder="Select employee"
className="w-full"
disabled={isLoadingEmployees}
emptyMessage={isLoadingEmployees ? "Loading employees..." : "No employees found"}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{!isNewTask && (
<FormField
control={form.control}
name="remainingHr"
render={({ field }) => (
<FormItem>
<FormLabel>Remaining Hours</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Enter remaining hours"
{...field}
onChange={(e) => field.onChange(e.target.value ? Number(e.target.value) : null)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
{/* Hidden field for esUnit to always be 1 (Days) */}
<input type="hidden" name="esUnit" value="1" />
{/* Hidden field for type to always be Task */}
<input type="hidden" name="type" value="Task" />
</div>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={onCancel}>
Cancel
</Button>
<Button type="submit" disabled={isLoading}>
{isLoading ? "Saving..." : "Save"}
</Button>
</div>
</form>
</Form>
);
}