Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect } from "react"; | |
| import { useNavigate, useParams } from "react-router-dom"; | |
| import { ArrowLeft, Loader2 } from "lucide-react"; | |
| import { useForm } from "react-hook-form"; | |
| import Layout from "@/components/layout/layout"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Input } from "@/components/ui/input"; | |
| import { Textarea } from "@/components/ui/textarea"; | |
| import { | |
| Card, | |
| CardContent, | |
| CardDescription, | |
| CardHeader, | |
| CardTitle, | |
| } from "@/components/ui/card"; | |
| import { | |
| Form, | |
| FormControl, | |
| FormField, | |
| FormItem, | |
| FormLabel, | |
| FormMessage, | |
| } from "@/components/ui/form"; | |
| import { toast } from "@/lib/custom-toast"; | |
| import { issuesApi, projectService, deliverablesApi, employeeService } from "@/lib/api"; | |
| import { IssueUpdateRequest, IssueApi, ProjectApi, Deliverable, Employee } from "@/types"; | |
| import { Dropdown, DropdownOption } from "@/components/ui/dropdown"; | |
| const IssueEdit = () => { | |
| const { id } = useParams<{ id: string }>(); | |
| const navigate = useNavigate(); | |
| const [isSubmitting, setIsSubmitting] = useState(false); | |
| const [isLoading, setIsLoading] = useState(true); | |
| const [issue, setIssue] = useState<IssueApi | null>(null); | |
| const [projects, setProjects] = useState<ProjectApi[]>([]); | |
| const [deliverables, setDeliverables] = useState<Deliverable[]>([]); | |
| const [employees, setEmployees] = useState<Employee[]>([]); | |
| const [filteredDeliverables, setFilteredDeliverables] = useState<Deliverable[]>([]); | |
| const [selectedProjectId, setSelectedProjectId] = useState<number>(0); | |
| const form = useForm<IssueUpdateRequest>({ | |
| defaultValues: { | |
| id: 0, | |
| biCode: "", | |
| type: "User Story", | |
| description: "", | |
| estimates: "", | |
| esUnit: 0, | |
| priority: "Medium", | |
| severity: "Medium", | |
| reportedBy: 0, | |
| assignedTo: 0, | |
| remainingHr: 0, | |
| title: "", | |
| projectId: 0, | |
| deliverablesId: 0 | |
| } | |
| }); | |
| // Step 1: Fetch the issue data first | |
| useEffect(() => { | |
| const fetchIssue = async () => { | |
| if (!id) { | |
| navigate("/issues"); | |
| return; | |
| } | |
| const issueId = parseInt(id); | |
| if (isNaN(issueId)) { | |
| navigate("/issues"); | |
| return; | |
| } | |
| try { | |
| setIsLoading(true); | |
| const issueData = await issuesApi.getById(issueId); | |
| setIssue(issueData); | |
| setSelectedProjectId(issueData.projectId); | |
| // Set form values | |
| form.reset({ | |
| ...issueData | |
| }); | |
| } catch (error) { | |
| console.error("Error fetching issue:", error); | |
| toast.error("Failed to load issue data. Please try again."); | |
| navigate("/issues"); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| fetchIssue(); | |
| }, [id, navigate, form]); | |
| // Step 2: Fetch projects separately | |
| useEffect(() => { | |
| const fetchProjects = async () => { | |
| try { | |
| const projectsData = await projectService.getAll(); | |
| setProjects(projectsData); | |
| } catch (error) { | |
| console.error("Error fetching projects:", error); | |
| toast.error("Failed to load projects. Some features may be limited."); | |
| } | |
| }; | |
| fetchProjects(); | |
| }, []); | |
| // Step 3: Fetch deliverables, but only after issue and projects are loaded | |
| useEffect(() => { | |
| if (!selectedProjectId) return; | |
| const fetchDeliverables = async () => { | |
| try { | |
| const deliverablesData = await deliverablesApi.getAll(); | |
| setDeliverables(deliverablesData); | |
| // Filter deliverables based on the current project | |
| const filtered = deliverablesData.filter(d => d.projectId === selectedProjectId); | |
| setFilteredDeliverables(filtered); | |
| } catch (error) { | |
| console.error("Error fetching deliverables:", error); | |
| toast.error("Failed to load deliverables. Some features may be limited."); | |
| } | |
| }; | |
| fetchDeliverables(); | |
| }, [selectedProjectId]); | |
| // Step 4: Fetch employees separately | |
| useEffect(() => { | |
| const fetchEmployees = async () => { | |
| try { | |
| const employeesData = await employeeService.getAll(); | |
| setEmployees(employeesData); | |
| } catch (error) { | |
| console.error("Error fetching employees:", error); | |
| toast.error("Failed to load employees. Some features may be limited."); | |
| } | |
| }; | |
| fetchEmployees(); | |
| }, []); | |
| // Handle project change to filter deliverables | |
| const handleProjectChange = (projectId: string) => { | |
| const numProjectId = parseInt(projectId); | |
| setSelectedProjectId(numProjectId); | |
| form.setValue("projectId", numProjectId); | |
| // Reset deliverable selection when project changes | |
| form.setValue("deliverablesId", 0); | |
| }; | |
| const handleSubmit = async (data: IssueUpdateRequest) => { | |
| if (!validateForm(data)) { | |
| return; | |
| } | |
| setIsSubmitting(true); | |
| try { | |
| await issuesApi.update(data); | |
| toast.success("Issue updated successfully"); | |
| navigate(`/issues/${data.id}`); | |
| } catch (error) { | |
| console.error("Error updating issue:", error); | |
| toast.error("Failed to update issue. Please try again."); | |
| } finally { | |
| setIsSubmitting(false); | |
| } | |
| }; | |
| const validateForm = (data: IssueUpdateRequest): boolean => { | |
| if (!data.title.trim()) { | |
| toast.error("Title is required"); | |
| return false; | |
| } | |
| if (!data.description.trim()) { | |
| toast.error("Description is required"); | |
| return false; | |
| } | |
| if (data.projectId <= 0) { | |
| toast.error("Project is required"); | |
| return false; | |
| } | |
| if (data.reportedBy <= 0) { | |
| toast.error("Reported By is required"); | |
| return false; | |
| } | |
| if (data.assignedTo <= 0) { | |
| toast.error("Assigned To is required"); | |
| return false; | |
| } | |
| if (data.deliverablesId <= 0) { | |
| toast.error("Deliverable is required"); | |
| return false; | |
| } | |
| return true; | |
| }; | |
| // Convert data to Dropdown options format | |
| const issueTypeOptions: DropdownOption[] = ["User Story", "Bug", "Task", "Documentation", "Enhancement"] | |
| .map(type => ({ label: type, value: type })); | |
| const priorityOptions: DropdownOption[] = ["Low", "Medium", "High", "Critical"] | |
| .map(priority => ({ label: priority, value: priority })); | |
| const severityOptions: DropdownOption[] = ["Low", "Medium", "High", "Critical"] | |
| .map(severity => ({ label: severity, value: severity })); | |
| const projectOptions: DropdownOption[] = projects.map(project => ({ | |
| label: project.projectName, | |
| value: project.id.toString() | |
| })); | |
| const deliverableOptions: DropdownOption[] = filteredDeliverables.map(deliverable => ({ | |
| label: deliverable.name, | |
| value: deliverable.id.toString() | |
| })); | |
| const employeeOptions: DropdownOption[] = employees.map(employee => ({ | |
| label: `${employee.firstName} ${employee.lastName}`, | |
| value: employee.id.toString() | |
| })); | |
| const estimateUnitOptions: DropdownOption[] = [ | |
| { label: "Hours", value: "Hours" }, | |
| { label: "Days", value: "Days" } | |
| ]; | |
| if (isLoading) { | |
| return ( | |
| <Layout> | |
| <div className="flex justify-center items-center h-full"> | |
| <Loader2 className="h-8 w-8 animate-spin text-primary" /> | |
| </div> | |
| </Layout> | |
| ); | |
| } | |
| return ( | |
| <Layout> | |
| <div className="space-y-6 animate-fadeIn"> | |
| <div className="flex items-center gap-4"> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| onClick={() => navigate(`/issues/${id}`)} | |
| > | |
| <ArrowLeft className="h-4 w-4" /> | |
| </Button> | |
| <div> | |
| <h1 className="text-3xl font-bold tracking-tight">Edit Feature</h1> | |
| <p className="text-muted-foreground"> | |
| Update issue #{id} details | |
| </p> | |
| </div> | |
| </div> | |
| <Form {...form}> | |
| <form onSubmit={form.handleSubmit(handleSubmit)}> | |
| <div className="grid grid-cols-1 gap-6"> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Basic Information</CardTitle> | |
| <CardDescription> | |
| Edit the basic details of the Feature | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent className="space-y-4"> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <FormField | |
| control={form.control} | |
| name="title" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Title <span className="text-destructive">*</span></FormLabel> | |
| <FormControl> | |
| <Input | |
| placeholder="Enter issue title" | |
| {...field} | |
| /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| <FormField | |
| control={form.control} | |
| name="biCode" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>BI Code</FormLabel> | |
| <FormControl> | |
| <Input | |
| placeholder="Enter BI code (optional)" | |
| {...field} | |
| /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| </div> | |
| <FormField | |
| control={form.control} | |
| name="description" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Description <span className="text-destructive">*</span></FormLabel> | |
| <FormControl> | |
| <Textarea | |
| placeholder="Describe the issue in detail" | |
| rows={4} | |
| {...field} | |
| /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| </CardContent> | |
| </Card> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Classification</CardTitle> | |
| <CardDescription> | |
| Categorize and prioritize the issue | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent className="space-y-4"> | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <FormField | |
| control={form.control} | |
| name="type" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Type <span className="text-destructive">*</span></FormLabel> | |
| <FormControl> | |
| <Dropdown | |
| options={issueTypeOptions} | |
| value={field.value} | |
| onValueChange={field.onChange} | |
| placeholder="Select type" | |
| /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| <FormField | |
| control={form.control} | |
| name="priority" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Priority <span className="text-destructive">*</span></FormLabel> | |
| <FormControl> | |
| <Dropdown | |
| options={priorityOptions} | |
| value={field.value} | |
| onValueChange={field.onChange} | |
| placeholder="Select priority" | |
| /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| <FormField | |
| control={form.control} | |
| name="severity" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Severity <span className="text-destructive">*</span></FormLabel> | |
| <FormControl> | |
| <Dropdown | |
| options={severityOptions} | |
| value={field.value} | |
| onValueChange={field.onChange} | |
| placeholder="Select severity" | |
| /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Project & Assignment</CardTitle> | |
| <CardDescription> | |
| Link the issue to a project and assign it to a team member | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent className="space-y-4"> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <FormField | |
| control={form.control} | |
| name="projectId" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Project <span className="text-destructive">*</span></FormLabel> | |
| <FormControl> | |
| <Dropdown | |
| options={projectOptions} | |
| value={field.value > 0 ? field.value.toString() : ""} | |
| onValueChange={handleProjectChange} | |
| placeholder="Select project" | |
| /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| <FormField | |
| control={form.control} | |
| name="deliverablesId" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Deliverable <span className="text-destructive">*</span></FormLabel> | |
| <FormControl> | |
| <Dropdown | |
| options={deliverableOptions} | |
| value={field.value > 0 ? field.value.toString() : ""} | |
| onValueChange={(value) => form.setValue("deliverablesId", parseInt(value))} | |
| placeholder={ | |
| selectedProjectId <= 0 | |
| ? "Select a project first" | |
| : filteredDeliverables.length === 0 | |
| ? "No deliverables found" | |
| : "Select deliverable" | |
| } | |
| disabled={selectedProjectId <= 0 || filteredDeliverables.length === 0} | |
| /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| </div> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <FormField | |
| control={form.control} | |
| name="reportedBy" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Reported By <span className="text-destructive">*</span></FormLabel> | |
| <FormControl> | |
| <Dropdown | |
| options={employeeOptions} | |
| value={field.value > 0 ? field.value.toString() : ""} | |
| onValueChange={(value) => form.setValue("reportedBy", parseInt(value))} | |
| placeholder="Select reporter" | |
| /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| <FormField | |
| control={form.control} | |
| name="assignedTo" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Assigned To <span className="text-destructive">*</span></FormLabel> | |
| <FormControl> | |
| <Dropdown | |
| options={employeeOptions} | |
| value={field.value > 0 ? field.value.toString() : ""} | |
| onValueChange={(value) => form.setValue("assignedTo", parseInt(value))} | |
| placeholder="Select assignee" | |
| /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Effort Estimation</CardTitle> | |
| <CardDescription> | |
| Estimate the work effort required | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent className="space-y-4"> | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <FormField | |
| control={form.control} | |
| name="estimates" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Estimates Description</FormLabel> | |
| <FormControl> | |
| <Input | |
| placeholder="e.g., 2 days, 4 hours" | |
| {...field} | |
| /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| <FormField | |
| control={form.control} | |
| name="esUnit" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Estimate Units</FormLabel> | |
| <FormControl> | |
| <select | |
| className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" | |
| value={field.value >= 8 ? "Days" : "Hours"} | |
| onChange={(e) => { | |
| const value = e.target.value; | |
| const numValue = value === "Hours" ? 1 : 8; | |
| form.setValue("esUnit", numValue); | |
| }} | |
| > | |
| <option value="Hours">Hours</option> | |
| <option value="Days">Days</option> | |
| </select> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| <FormField | |
| control={form.control} | |
| name="remainingHr" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Remaining Hours</FormLabel> | |
| <FormControl> | |
| <Input | |
| type="number" | |
| placeholder="Enter hours" | |
| {...field} | |
| onChange={(e) => form.setValue("remainingHr", parseInt(e.target.value) || 0)} | |
| /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| <div className="flex justify-end gap-4"> | |
| <Button | |
| type="button" | |
| variant="outline" | |
| onClick={() => navigate(`/issues/${id}`)} | |
| > | |
| Cancel | |
| </Button> | |
| <Button type="submit" disabled={isSubmitting}> | |
| {isSubmitting ? ( | |
| <> | |
| <Loader2 className="mr-2 h-4 w-4 animate-spin" /> | |
| Updating... | |
| </> | |
| ) : ( | |
| "Update Feature" | |
| )} | |
| </Button> | |
| </div> | |
| </div> | |
| </form> | |
| </Form> | |
| </div> | |
| </Layout> | |
| ); | |
| }; | |
| export default IssueEdit; |