pmtool / src /pages /issue-edit.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
22.8 kB
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;