Spaces:
Sleeping
Sleeping
| 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> | |
| ); | |
| } |