pmtool / src /pages /bug-form.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
62 kB
import React, { useState, useEffect } from "react";
import { useNavigate, useParams, Link } from "react-router-dom";
import { tasksApi, Task, TaskCreateRequest } from "@/services/tasksApi";
import { issuesApi } from "@/lib/api";
import { projectService } from "@/lib/api";
import { IssueApi } from "@/types";
import { ProjectApi } from "@/types";
import { toast } from "sonner";
import {
ChevronRight,
Home,
Save,
Bug,
ArrowLeft,
AlertTriangle,
Check,
ChevronDown,
User,
Lock
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";
import Layout from "../components/layout/layout";
import { employeeService } from "@/lib/api";
import { Employee } from "@/types";
import { Separator } from "@/components/ui/separator";
import { format } from "date-fns";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { useAuth } from "@/lib/auth-context";
// Custom dropdown component
interface CustomDropdownProps {
value: string;
onChange: (value: string) => void;
options: { value: string; label: string }[];
placeholder: string;
}
function CustomDropdown({ value, onChange, options, placeholder }: CustomDropdownProps) {
const [open, setOpen] = useState(false);
console.log("CustomDropdown render with value:", value, typeof value);
const getLabel = () => {
const option = options.find(option => option.value === value);
console.log("Finding option for value:", value, "Found:", option);
return option?.label || placeholder;
};
const handleButtonClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setOpen(!open);
};
const handleOptionClick = (optionValue: string) => {
console.log("Option clicked:", optionValue);
onChange(optionValue);
setOpen(false);
};
// Close dropdown when clicking outside
useEffect(() => {
if (!open) return;
const handleOutsideClick = (e: MouseEvent) => {
// @ts-ignore
if (e.target && !e.target.closest('.dropdown-container')) {
setOpen(false);
}
};
document.addEventListener('mousedown', handleOutsideClick);
return () => {
document.removeEventListener('mousedown', handleOutsideClick);
};
}, [open]);
return (
<div className="relative w-full dropdown-container">
<div
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background cursor-pointer",
open && "ring-2 ring-ring ring-offset-2"
)}
onClick={handleButtonClick}
>
<span className={value ? "" : "text-muted-foreground"}>{getLabel()}</span>
<ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
</div>
{open && (
<div className="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md">
{options.map((option) => (
<div
key={option.value}
className={cn(
"flex cursor-pointer items-center justify-between rounded-sm px-2 py-1.5 hover:bg-accent hover:text-accent-foreground",
value === option.value && "bg-accent text-accent-foreground"
)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleOptionClick(option.value);
}}
>
{option.label}
{value === option.value && <Check className="h-4 w-4" />}
</div>
))}
</div>
)}
</div>
);
}
// Custom Employee Dropdown component
interface CustomEmployeeDropdownProps {
value: string | null;
onChange: (value: string) => void;
employees: Employee[];
placeholder: string;
disabled?: boolean;
}
function CustomEmployeeDropdown({ value, onChange, employees, placeholder, disabled = false }: CustomEmployeeDropdownProps) {
const [open, setOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
console.log("CustomEmployeeDropdown render with value:", value, typeof value);
const getEmployeeName = (id: string | null) => {
console.log("Getting employee name for ID:", id, typeof id);
if (!id || id === "unassigned") return placeholder;
const employee = employees.find(emp => emp.id.toString() === id);
return employee ? `${employee.firstName} ${employee.lastName}` : `Employee ${id}`;
};
const getInitials = (id: string) => {
const employee = employees.find(emp => emp.id.toString() === id);
if (!employee) return "??";
return (employee.firstName.charAt(0) + employee.lastName.charAt(0)).toUpperCase();
};
const handleButtonClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
// Don't open if disabled
if (disabled) return;
setOpen(!open);
if (!open) {
setSearchTerm(""); // Reset search when opening
}
};
const handleOptionClick = (employeeId: string) => {
console.log("Employee option clicked:", employeeId);
onChange(employeeId);
setOpen(false);
setSearchTerm(""); // Reset search when selecting
};
// Helper functions for checking selection state
const isUnassigned = () => {
return value === null || value === "unassigned" || value === undefined;
};
const isEmployeeSelected = (empId: string) => {
return value === empId;
};
// Filter employees based on search term
const filteredEmployees = employees.filter(employee => {
if (!searchTerm) return true;
const fullName = `${employee.firstName} ${employee.lastName}`.toLowerCase();
return fullName.includes(searchTerm.toLowerCase());
});
// Close dropdown when clicking outside
useEffect(() => {
if (!open) return;
const handleOutsideClick = (e: MouseEvent) => {
// @ts-ignore
if (e.target && !e.target.closest('.employee-dropdown-container')) {
setOpen(false);
setSearchTerm(""); // Reset search when closing
}
};
document.addEventListener('mousedown', handleOutsideClick);
return () => {
document.removeEventListener('mousedown', handleOutsideClick);
};
}, [open]);
return (
<div className="relative w-full employee-dropdown-container">
<div
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background",
disabled ? "opacity-70 cursor-not-allowed bg-muted" : "cursor-pointer",
open && "ring-2 ring-ring ring-offset-2"
)}
onClick={handleButtonClick}
>
<div className="flex items-center">
{value && value !== "unassigned" ? (
<Avatar className="h-5 w-5 mr-2">
<AvatarFallback className="text-[10px]">
{getInitials(value)}
</AvatarFallback>
</Avatar>
) : (
<User className="h-4 w-4 mr-2 opacity-50" />
)}
<span className={isUnassigned() ? "text-muted-foreground" : ""}>
{getEmployeeName(value)}
</span>
</div>
{!disabled && <ChevronDown className="h-4 w-4 shrink-0 opacity-50" />}
{disabled && <Lock className="h-4 w-4 shrink-0 opacity-50" />}
</div>
{open && (
<div className="absolute z-50 mt-1 max-h-60 w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md">
{/* Search Input */}
<div className="p-2 border-b">
<Input
placeholder="Search employees..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="h-8 text-sm"
autoFocus
onClick={(e) => e.stopPropagation()}
/>
</div>
{/* Options Container */}
<div className="max-h-48 overflow-auto p-1">
{/* Unassigned Option */}
{(!searchTerm || "unassigned".includes(searchTerm.toLowerCase())) && (
<div
key="unassigned"
className={cn(
"flex cursor-pointer items-center px-2 py-1.5 hover:bg-accent hover:text-accent-foreground bg-muted/30",
isUnassigned() && "bg-accent text-accent-foreground"
)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleOptionClick("unassigned");
}}
>
<User className="h-4 w-4 mr-2 opacity-50" />
<span className="font-semibold text-neutral-500">Unassigned</span>
{isUnassigned() && (
<Check className="ml-auto h-4 w-4" />
)}
</div>
)}
{/* Employee Options */}
{filteredEmployees.length > 0 ? (
filteredEmployees.map((employee) => (
<div
key={employee.id}
className={cn(
"flex cursor-pointer items-center px-2 py-1.5 hover:bg-accent hover:text-accent-foreground",
isEmployeeSelected(employee.id.toString()) && "bg-accent text-accent-foreground"
)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleOptionClick(employee.id.toString());
}}
>
<Avatar className="h-5 w-5 mr-2">
<AvatarFallback className="text-[10px]">
{(employee.firstName.charAt(0) + employee.lastName.charAt(0)).toUpperCase()}
</AvatarFallback>
</Avatar>
<span>{employee.firstName} {employee.lastName}</span>
{isEmployeeSelected(employee.id.toString()) && (
<Check className="ml-auto h-4 w-4" />
)}
</div>
))
) : (
searchTerm && (
<div className="px-2 py-1.5 text-sm text-muted-foreground">
No employees found matching "{searchTerm}"
</div>
)
)}
</div>
</div>
)}
</div>
);
}
// Add ProjectDropdown component
interface CustomProjectDropdownProps {
value: number | null;
onChange: (value: string) => void;
projects: ProjectApi[];
placeholder: string;
}
function CustomProjectDropdown({ value, onChange, projects, placeholder }: CustomProjectDropdownProps) {
const [open, setOpen] = useState(false);
const getProjectName = (id: number | null) => {
if (id === null) return placeholder;
const project = projects.find(project => project.id === id);
return project ? project.projectName : `Project #${id}`;
};
const handleButtonClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setOpen(!open);
};
const handleOptionClick = (projectId: string) => {
if (projectId === "all") {
onChange("all");
} else {
onChange(projectId);
}
setOpen(false);
};
// Close dropdown when clicking outside
useEffect(() => {
if (!open) return;
const handleOutsideClick = (e: MouseEvent) => {
// @ts-ignore
if (e.target && !e.target.closest('.project-dropdown-container')) {
setOpen(false);
}
};
document.addEventListener('mousedown', handleOutsideClick);
return () => {
document.removeEventListener('mousedown', handleOutsideClick);
};
}, [open]);
// Helper functions
const isProjectSelected = (projectId: number) => {
return value === projectId;
};
const isAllProjectsSelected = () => {
return value === null;
};
return (
<div className="relative w-full project-dropdown-container">
<div
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background cursor-pointer",
open && "ring-2 ring-ring ring-offset-2"
)}
onClick={handleButtonClick}
>
<span className={value === null ? "text-muted-foreground" : ""}>
{value !== null ? getProjectName(value) : placeholder}
</span>
<ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
</div>
{open && (
<div className="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md">
<div
key="all"
className={cn(
"flex cursor-pointer items-center px-2 py-1.5 hover:bg-accent hover:text-accent-foreground bg-muted/30",
isAllProjectsSelected() && "bg-accent text-accent-foreground"
)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleOptionClick("all");
}}
>
<span className="font-semibold text-neutral-500">All Projects</span>
{isAllProjectsSelected() && <Check className="ml-auto h-4 w-4" />}
</div>
{projects.map((project) => (
<div
key={project.id}
className={cn(
"flex cursor-pointer items-center px-2 py-1.5 hover:bg-accent hover:text-accent-foreground",
isProjectSelected(project.id) && "bg-accent text-accent-foreground"
)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleOptionClick(project.id.toString());
}}
>
<div className="flex flex-col">
<span className="font-medium">{project.projectName}</span>
<span className="text-xs text-muted-foreground">Code: {project.projectCode}</span>
</div>
{isProjectSelected(project.id) && <Check className="ml-auto h-4 w-4" />}
</div>
))}
</div>
)}
</div>
);
}
// Custom Issue Dropdown component
interface CustomIssueDropdownProps {
value: number | null;
onChange: (value: string) => void;
issues: IssueApi[];
placeholder: string;
required?: boolean;
isError?: boolean;
}
function CustomIssueDropdown({ value, onChange, issues, placeholder, required, isError = false }: CustomIssueDropdownProps) {
const [open, setOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
console.log("CustomIssueDropdown render with value:", value, typeof value);
const getIssueName = (id: number | null) => {
console.log("Getting issue name for ID:", id, typeof id);
if (id === null || id === undefined) {
return placeholder;
}
const issue = issues.find(issue => issue.id === id);
return issue ? issue.title : `Issue #${id}`;
};
const handleButtonClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setOpen(!open);
};
const handleOptionClick = (issueId: string) => {
console.log("Issue option clicked:", issueId, typeof issueId);
if (issueId === "null") {
console.log("Setting issue to null");
onChange("null");
} else {
console.log("Setting issue to:", parseInt(issueId, 10));
onChange(issueId);
}
setOpen(false);
setSearchTerm("");
};
// Filter issues based on search term
const filteredIssues = searchTerm
? issues.filter(issue =>
issue.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
issue.id.toString().includes(searchTerm)
)
: issues;
// Close dropdown when clicking outside
useEffect(() => {
if (!open) return;
const handleOutsideClick = (e: MouseEvent) => {
// @ts-ignore
if (e.target && !e.target.closest('.issue-dropdown-container')) {
setOpen(false);
}
};
document.addEventListener('mousedown', handleOutsideClick);
return () => {
document.removeEventListener('mousedown', handleOutsideClick);
};
}, [open]);
// Helper function to check if an issue is selected
const isIssueSelected = (issueId: number) => {
return value === issueId;
}
// Helper to check if "No linked issue" is selected
const isNoIssueSelected = () => {
return value === null;
}
// Check if the field is invalid (required but not filled)
const isInvalid = required && value === null;
const showError = isInvalid || isError;
return (
<div className="relative w-full issue-dropdown-container">
<div
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background cursor-pointer",
open && "ring-2 ring-ring ring-offset-2",
showError && "border-red-500"
)}
onClick={handleButtonClick}
>
<div className="flex items-center">
<span className={value === null ? "text-muted-foreground" : ""}>
{value !== null ? getIssueName(value) : placeholder}
</span>
{showError && <span className="text-red-500 ml-1">*</span>}
</div>
<ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
</div>
{showError && !open && (
<p className="text-xs text-red-500 mt-1">This field is required</p>
)}
{open && (
<div className="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md">
<div className="sticky top-0 bg-popover p-2 border-b">
<Input
type="search"
placeholder="Search issues..."
className="h-8"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onClick={(e) => e.stopPropagation()}
/>
</div>
<div
key="none"
className={cn(
"flex cursor-pointer items-center px-2 py-1.5 hover:bg-accent hover:text-accent-foreground bg-muted/30",
isNoIssueSelected() && "bg-accent text-accent-foreground"
)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleOptionClick("null");
}}
>
<span className="font-semibold text-neutral-500">No linked issue</span>
{isNoIssueSelected() && <Check className="ml-auto h-4 w-4" />}
</div>
{filteredIssues.length === 0 ? (
<div className="px-2 py-4 text-center text-sm text-muted-foreground">
No issues found in your assigned projects
</div>
) : (
filteredIssues.map((issue) => (
<div
key={issue.id}
className={cn(
"flex cursor-pointer items-center px-2 py-1.5 hover:bg-accent hover:text-accent-foreground",
isIssueSelected(issue.id) && "bg-accent text-accent-foreground"
)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleOptionClick(issue.id.toString());
}}
>
<div className="flex flex-col">
<span className="font-medium">{issue.title}</span>
<span className="text-xs text-muted-foreground">#{issue.id} • {issue.status}</span>
</div>
{isIssueSelected(issue.id) && <Check className="ml-auto h-4 w-4" />}
</div>
))
)}
</div>
)}
</div>
);
}
type BugFormProps = {
isEditMode?: boolean;
defaultType?: string;
};
const BugForm: React.FC<BugFormProps> = ({ isEditMode = false, defaultType }) => {
const navigate = useNavigate();
const { id } = useParams();
const { userData } = useAuth();
const [isLoading, setIsLoading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [employees, setEmployees] = useState<Employee[]>([]);
const [issues, setIssues] = useState<IssueApi[]>([]);
const [allIssues, setAllIssues] = useState<IssueApi[]>([]);
const [projects, setProjects] = useState<ProjectApi[]>([]);
const [selectedProject, setSelectedProject] = useState<number | null>(null);
const [activeTab, setActiveTab] = useState("details");
const [validationErrors, setValidationErrors] = useState<string[]>([]);
const [bug, setBug] = useState<TaskCreateRequest>({
id: 0,
type: defaultType === "Production Bug" ? "Production Bug" : "Bug",
priority: "Medium",
severity: "Minor",
title: "",
description: "",
sprintName: null,
estimates: "",
esUnit: 0,
stateDate: new Date().toISOString(),
endDate: null,
assignedTo: null,
remainingHr: 0,
status: "Open",
issuesId: null,
taskCode: "", // Auto-generated by API, not shown in UI
version: "",
url: "",
stepsToReproduce: "",
inputs: "",
environment: defaultType === "Production Bug" ? "Production" : "Staging",
expectedBehavior: "",
actualBehavior: "",
resolution: "",
rootCause: ""
});
const [startDate, setStartDate] = useState<Date | undefined>(new Date());
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
useEffect(() => {
fetchEmployees();
fetchProjects();
if (isEditMode && id) {
fetchBug(Number(id));
}
}, [isEditMode, id]);
// Add a new effect to fetch issues after projects are loaded
useEffect(() => {
// Only fetch issues when projects change and we have at least one project
if (projects.length > 0 && !isLoading) {
// Check if we already have the issues for these projects to avoid unnecessary refreshes
const projectIds = projects.map(project => project.id);
const currentIssueProjectIds = new Set(issues.map(issue => issue.projectId));
// Only fetch if we have new projects or no issues loaded yet
const needsFetch = issues.length === 0 ||
projectIds.some(id => !currentIssueProjectIds.has(id));
if (needsFetch) {
console.log("Projects changed, refreshing issues");
fetchIssues();
}
}
}, [projects, issues, isLoading]);
const fetchEmployees = async () => {
try {
const data = await employeeService.getAll();
setEmployees(data);
} catch (error) {
console.error("Error fetching employees:", error);
toast.error("Error loading employee data");
}
};
const fetchProjects = async () => {
try {
let projectsData: ProjectApi[] = [];
// Fetch only user's projects if user data is available
if (userData && userData.employeeId) {
projectsData = await projectService.getByEmployeeId(userData.employeeId);
} else {
projectsData = await projectService.getAll();
}
setProjects(projectsData);
} catch (error) {
console.error("Error fetching projects:", error);
toast.error("Error loading projects data");
}
};
const fetchIssues = async () => {
try {
console.log("Fetching issues...");
// Prevent duplicate API calls if already loading
if (isLoading) {
console.log("Already loading, skipping fetchIssues call");
return;
}
// Get all issues first
const allIssuesData = await issuesApi.getAll();
console.log("All issues fetched:", allIssuesData);
setAllIssues(allIssuesData);
// If user has assigned projects, filter the issues to only show those projects
if (projects.length > 0) {
const projectIds = projects.map(project => project.id);
console.log("Filtering issues for user's assigned projects:", projectIds);
// Filter issues to only include those from user's assigned projects
const filteredIssues = allIssuesData.filter(issue =>
projectIds.includes(issue.projectId)
);
console.log("Filtered issues:", filteredIssues.length);
setIssues(filteredIssues);
} else {
// If no projects assigned yet or still loading, show all issues
setIssues(allIssuesData);
}
} catch (error) {
console.error("Error fetching issues:", error);
toast.error("Error loading issues data");
}
};
const fetchBug = async (bugId: number) => {
setIsLoading(true);
try {
const data = await tasksApi.getById(bugId);
if (data) {
// Ensure it's a bug type (regular Bug or Production Bug)
if (data.type !== "Bug" && data.type !== "Production Bug") {
toast.error("The requested item is not a bug");
// Redirect to the appropriate list based on the defaultType
if (defaultType === "Production Bug") {
navigate("/production-bugs");
} else {
navigate("/bugs");
}
return;
}
console.log("Bug data loaded from API:", {
status: data.status,
type: data.type,
priority: data.priority,
severity: data.severity,
environment: data.environment
});
// Convert dates to Date objects if they exist, handling timezone issues
let startDateObj = data.stateDate ? new Date(data.stateDate) : new Date();
let endDateObj = data.endDate ? new Date(data.endDate) : undefined;
// Adjust for timezone differences to show local date
if (startDateObj) {
startDateObj = new Date(startDateObj.getTime() + startDateObj.getTimezoneOffset() * 60000);
}
if (endDateObj) {
endDateObj = new Date(endDateObj.getTime() + endDateObj.getTimezoneOffset() * 60000);
}
setStartDate(startDateObj);
setEndDate(endDateObj);
// Update the form state with the fetched data, preserving the original bug type
setBug({
...data,
type: data.type, // Explicitly keep the original bug type
stateDate: data.stateDate || new Date().toISOString(),
endDate: data.endDate || null
});
} else {
toast.error("Bug not found");
// Check if we're editing a Production Bug and redirect accordingly
if (defaultType === "Production Bug") {
navigate("/production-bugs");
} else {
navigate("/bugs");
}
}
} catch (error) {
console.error("Error fetching bug:", error);
toast.error("Error loading bug data");
// Check if we're editing a Production Bug and redirect accordingly
if (defaultType === "Production Bug") {
navigate("/production-bugs");
} else {
navigate("/bugs");
}
} finally {
setIsLoading(false);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
// Clear validation error for this field when it's changed
if (validationErrors.includes(name)) {
setValidationErrors(prev => prev.filter(err => err !== name));
}
setBug(prev => ({ ...prev, [name]: value }));
};
const handleSelectChange = (field: string, value: string) => {
console.log(`Changing field ${field} to value:`, value, typeof value);
// Clear validation error for this field when it's changed
if (validationErrors.includes(field)) {
setValidationErrors(prev => prev.filter(err => err !== field));
}
if (field === 'assignedTo') {
const newValue = value === "unassigned" ? null : parseInt(value, 10);
console.log(`Setting assignedTo from ${bug.assignedTo} to:`, newValue, typeof newValue);
// Force a re-render by making a completely new object
setBug({
...bug,
assignedTo: newValue
});
} else if (field === 'project') {
// Handle project selection
const newValue = value === "all" ? null : parseInt(value, 10);
setSelectedProject(newValue);
// Filter issues based on selected project
if (newValue === null) {
setIssues(allIssues);
} else {
const filteredIssues = allIssues.filter(issue => issue.projectId === newValue);
setIssues(filteredIssues);
// Clear selected issue if it's not in the filtered list
if (bug.issuesId !== null && !filteredIssues.some(issue => issue.id === bug.issuesId)) {
setBug({
...bug,
issuesId: null
});
}
}
} else if (field === 'issuesId') {
const newValue = value === "null" ? null : parseInt(value, 10);
console.log(`Setting issuesId from ${bug.issuesId} to:`, newValue, typeof newValue);
// Force a re-render by making a completely new object
setBug({
...bug,
issuesId: newValue
});
// If issue is selected, also set the project to match
if (newValue !== null) {
const selectedIssue = allIssues.find(issue => issue.id === newValue);
if (selectedIssue && selectedProject !== selectedIssue.projectId) {
setSelectedProject(selectedIssue.projectId);
}
}
} else if (['status', 'type', 'priority', 'severity', 'environment'].includes(field)) {
// For dropdown fields, ensure we create a new object to trigger re-render
console.log(`Setting ${field} from "${bug[field as keyof typeof bug]}" to "${value}"`);
setBug({
...bug,
[field]: value
});
} else {
// For other fields
setBug(prev => ({ ...prev, [field]: value }));
}
};
const formatDateForApi = (date: Date | undefined) => {
if (!date) return null;
// Ensure we're sending a valid ISO string
try {
return new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString();
} catch (error) {
console.error("Error formatting date:", error);
return null;
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Reset validation errors
const errors: string[] = [];
// Validate required fields
if (bug.issuesId === null) {
errors.push("issuesId");
toast.error("Linked Feature is required");
}
if (!bug.title.trim()) {
errors.push("title");
toast.error("Bug Title is required");
}
if (!bug.description.trim()) {
errors.push("description");
toast.error("Bug Description is required");
}
if (errors.length > 0) {
setValidationErrors(errors);
setActiveTab("details"); // Switch to details tab where most required fields are
return;
}
setIsSaving(true);
try {
// Ensure bug type is preserved - for production bugs this is critical
const isProductionBug = bug.type === "Production Bug" || defaultType === "Production Bug";
// Format dates for API
const formattedBug = {
...bug,
// Ensure type is set correctly for production bugs
type: isProductionBug ? "Production Bug" : bug.type,
// For client users, always use current date for stateDate and clear resolution fields
stateDate: userData?.roleName?.toLowerCase() === "client"
? new Date().toISOString()
: (formatDateForApi(startDate) || new Date().toISOString()),
endDate: formatDateForApi(endDate),
// Clear resolution fields for client users
rootCause: userData?.roleName?.toLowerCase() === "client" ? "" : bug.rootCause,
resolution: userData?.roleName?.toLowerCase() === "client" ? "" : bug.resolution,
// Ensure issuesId is properly set to null or an integer
issuesId: bug.issuesId === null ? null : (typeof bug.issuesId === 'number' ? bug.issuesId : null),
// Ensure assignedTo is properly set to null or an integer
assignedTo: bug.assignedTo === null ? null : (typeof bug.assignedTo === 'number' ? bug.assignedTo : null)
};
console.log("Submitting bug with data:", formattedBug);
console.log("Issue ID value:", formattedBug.issuesId, typeof formattedBug.issuesId);
console.log("Assigned To value:", formattedBug.assignedTo, typeof formattedBug.assignedTo);
let result;
if (isEditMode && id) {
// Ensure id is properly set for update
const updateData = {
...formattedBug,
id: Number(id)
};
console.log("Updating bug with data:", updateData);
result = await tasksApi.update(Number(id), updateData);
toast.success("Bug updated successfully");
} else {
console.log("Creating new bug with data:", formattedBug);
result = await tasksApi.create(formattedBug);
toast.success("Bug created successfully");
}
// Check if the bug is a Production Bug and redirect accordingly
if (isProductionBug) {
navigate(`/production-bugs/${result.id}`);
} else {
navigate(`/bugs/${result.id}`);
}
} catch (error) {
console.error("Error saving bug:", error);
toast.error("Failed to save bug. Please try again.");
} finally {
setIsSaving(false);
}
};
const handleCancel = () => {
// Check if this is a production bug based on either current type or default type
const isProductionBug = bug.type === "Production Bug" || defaultType === "Production Bug";
if (isEditMode && id) {
if (isProductionBug) {
navigate(`/production-bugs/${id}`);
} else {
navigate(`/bugs/${id}`);
}
} else {
if (isProductionBug) {
navigate("/production-bugs");
} else {
navigate("/bugs");
}
}
};
useEffect(() => {
console.log("Render: Issues available:", issues);
}, [issues]);
useEffect(() => {
console.log("Bug issuesId changed:", bug.issuesId, typeof bug.issuesId);
// If issuesId is not null and not a number, fix it
if (bug.issuesId !== null && typeof bug.issuesId !== 'number') {
console.log("Correcting issuesId type");
try {
// Try to convert to a number if possible
const numValue = Number(bug.issuesId);
if (!isNaN(numValue)) {
console.log(`Converting issuesId from ${typeof bug.issuesId} to number: ${numValue}`);
setBug(prev => ({...prev, issuesId: numValue}));
} else {
// If conversion fails, set to null
console.log("Failed to convert issuesId to number, setting to null");
setBug(prev => ({...prev, issuesId: null}));
}
} catch (error) {
console.error("Error converting issuesId:", error);
setBug(prev => ({...prev, issuesId: null}));
}
}
}, [bug.issuesId]);
useEffect(() => {
console.log("Bug assignedTo changed:", bug.assignedTo, typeof bug.assignedTo);
// If assignedTo is not null and not a number, fix it
if (bug.assignedTo !== null && typeof bug.assignedTo !== 'number') {
console.log("Correcting assignedTo type");
try {
// Try to convert to a number if possible
const numValue = Number(bug.assignedTo);
if (!isNaN(numValue)) {
console.log(`Converting assignedTo from ${typeof bug.assignedTo} to number: ${numValue}`);
setBug(prev => ({...prev, assignedTo: numValue}));
} else {
// If conversion fails, set to null
console.log("Failed to convert assignedTo to number, setting to null");
setBug(prev => ({...prev, assignedTo: null}));
}
} catch (error) {
console.error("Error converting assignedTo:", error);
setBug(prev => ({...prev, assignedTo: null}));
}
}
}, [bug.assignedTo]);
useEffect(() => {
console.log("Bug state updated:", {
status: bug.status,
type: bug.type,
priority: bug.priority,
severity: bug.severity,
environment: bug.environment
});
}, [bug.status, bug.type, bug.priority, bug.severity, bug.environment]);
// Set default assignee for client users when component initializes
useEffect(() => {
if (userData?.roleName?.toLowerCase() === "client" && !isEditMode && bug.assignedTo === null) {
// For new bugs, if the user is a client, set assignedTo to null initially
// This will be handled by the backend/admin assignment later
setBug(prev => ({
...prev,
assignedTo: null
}));
}
}, [userData, isEditMode]); // Run once when component mounts and userData is available
return (
<Layout>
<div className="pb-6 w-full animate-fadeIn">
{/* Breadcrumb Navigation */}
<nav className="flex items-center space-x-2 text-sm text-muted-foreground overflow-x-auto pb-2 mb-4 px-4 md:px-6">
<Link to="/" className="flex items-center hover:text-foreground transition-colors flex-shrink-0">
<Home className="h-4 w-4 mr-1" />
Home
</Link>
<ChevronRight className="h-4 w-4 flex-shrink-0" />
{bug.type === "Production Bug" ? (
<Link to="/production-bugs" className="hover:text-foreground transition-colors flex-shrink-0">
Production Bugs
</Link>
) : (
<Link to="/bugs" className="hover:text-foreground transition-colors flex-shrink-0">
Bugs
</Link>
)}
<ChevronRight className="h-4 w-4 flex-shrink-0" />
<span className="text-foreground font-medium flex-shrink-0">
{isEditMode ? "Edit Bug" : "New Bug"}
</span>
</nav>
<div className="px-4 md:px-6 space-y-6 max-w-5xl mx-auto">
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold tracking-tight flex items-center gap-2">
<Bug className="h-6 w-6" />
{isEditMode ?
`Edit ${bug.type === "Production Bug" ? "Production Bug" : "Bug"}` :
`New ${bug.type === "Production Bug" ? "Production Bug" : "Bug"}`}
</h1>
<p className="text-muted-foreground">
{isEditMode
? `Update the details of an existing ${bug.type === "Production Bug" ? "production bug" : "bug"}`
: `Create a new ${bug.type === "Production Bug" ? "production bug" : "bug or defect"} to track`}
</p>
</div>
<Button variant="outline" onClick={handleCancel}>
<ArrowLeft className="h-4 w-4 mr-2" />
Back
</Button>
</div>
{isLoading ? (
<Card>
<CardContent className="pt-6">
<div className="flex justify-center py-8">
<div className="flex flex-col items-center gap-2">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
<p className="text-sm text-muted-foreground">Loading bug data...</p>
</div>
</div>
</CardContent>
</Card>
) : (
<form onSubmit={handleSubmit}>
<Tabs defaultValue="details" className="w-full" onValueChange={setActiveTab}>
<TabsList className={`grid w-full ${userData?.roleName?.toLowerCase() === "client" ? "grid-cols-2" : "grid-cols-3"} mb-4`}>
<TabsTrigger value="details">
Bug Details
</TabsTrigger>
<TabsTrigger value="steps">
Steps & Behavior
</TabsTrigger>
{userData?.roleName?.toLowerCase() !== "client" && (
<TabsTrigger value="resolution">
Resolution
</TabsTrigger>
)}
</TabsList>
<Card>
<CardContent className="pt-6">
<div className="mb-4 text-sm text-muted-foreground">
Fields marked with <span className="text-red-500">*</span> are required
</div>
<TabsContent value="details" className="space-y-4 mt-0">
<div className="grid grid-cols-1 gap-4">
<div className="grid gap-2">
<Label htmlFor="project">Project</Label>
<CustomProjectDropdown
value={selectedProject}
onChange={(value) => handleSelectChange("project", value)}
projects={projects}
placeholder="All Projects"
/>
<p className="text-xs text-muted-foreground">
Filter issues by project
</p>
</div>
<div className="grid gap-2">
<Label htmlFor="issuesId">Linked Feature <span className="text-red-500">*</span></Label>
<CustomIssueDropdown
value={bug.issuesId}
onChange={(value) => handleSelectChange("issuesId", value)}
issues={issues}
placeholder="Link to a feature"
required
isError={validationErrors.includes("issuesId")}
/>
<p className="text-xs text-muted-foreground">
Connect this bug to a related feature
</p>
</div>
<div className="grid gap-2">
<Label htmlFor="title">Bug Title <span className="text-red-500">*</span></Label>
<Input
id="title"
name="title"
value={bug.title}
onChange={handleInputChange}
placeholder="Enter a descriptive title for the bug"
required
className={validationErrors.includes("title") ? "border-red-500" : ""}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="description">Bug Description <span className="text-red-500">*</span></Label>
<Textarea
id="description"
name="description"
value={bug.description}
onChange={handleInputChange}
placeholder="Provide a detailed description of the bug"
rows={4}
required
className={validationErrors.includes("description") ? "border-red-500" : ""}
/>
</div>
<div className="grid gap-4 grid-cols-1 md:grid-cols-2">
<div className="grid gap-2">
<Label htmlFor="status">Status</Label>
<CustomDropdown
value={bug.status || "Open"}
onChange={(value) => handleSelectChange("status", value)}
options={[
{ value: "New", label: "New" },
{ value: "In Progress", label: "In Progress" },
{ value: "In Review", label: "In Review" },
{ value: "Completed", label: "Completed" },
{ value: "Closed", label: "Closed" },
{ value: "Open", label: "Open" },
{ value: "Resolved", label: "Resolved" },
{ value: "Reopened", label: "Reopened" },
{ value: "Ready for QA", label: "Ready for QA" }
]}
placeholder="Select status"
/>
</div>
{defaultType !== "Production Bug" && bug.type !== "Production Bug" ? (
<div className="grid gap-2">
<Label htmlFor="type">Bug Type</Label>
<CustomDropdown
value={bug.type}
onChange={(value) => handleSelectChange("type", value)}
options={[
{ value: "Bug", label: "Bug" },
{ value: "Test Case", label: "Test Case" }
]}
placeholder="Select bug type"
/>
</div>
) : (
<div className="grid gap-2">
<Label htmlFor="type">Bug Type</Label>
<div className="flex h-10 w-full items-center justify-between rounded-md border border-input bg-muted/30 px-3 py-2 text-sm">
<span>Production Bug</span>
<Lock className="h-3.5 w-3.5 text-muted-foreground" />
</div>
</div>
)}
</div>
<div className="grid gap-4 grid-cols-1 md:grid-cols-2">
<div className="grid gap-2">
<Label htmlFor="priority">Priority</Label>
<CustomDropdown
value={bug.priority || "Medium"}
onChange={(value) => handleSelectChange("priority", value)}
options={[
{ value: "Critical", label: "Critical" },
{ value: "High", label: "High" },
{ value: "Medium", label: "Medium" },
{ value: "Low", label: "Low" }
]}
placeholder="Select priority"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="severity">Severity</Label>
<CustomDropdown
value={bug.severity || "Minor"}
onChange={(value) => handleSelectChange("severity", value)}
options={[
{ value: "Blocker", label: "Blocker" },
{ value: "Critical", label: "Critical" },
{ value: "Major", label: "Major" },
{ value: "Minor", label: "Minor" },
{ value: "Trivial", label: "Trivial" }
]}
placeholder="Select severity"
/>
</div>
</div>
<div className="grid gap-4 grid-cols-1 md:grid-cols-2">
<div className="grid gap-2">
<Label htmlFor="version">Affected Version</Label>
<Input
id="version"
name="version"
value={bug.version || ""}
onChange={handleInputChange}
placeholder="e.g., 1.2.3"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="url">Related URL</Label>
<Input
id="url"
name="url"
value={bug.url || ""}
onChange={handleInputChange}
placeholder="e.g., https://example.com/page-with-bug"
/>
</div>
</div>
<div className="grid gap-4 grid-cols-1 md:grid-cols-2">
<div className="grid gap-2">
<Label htmlFor="assignedTo">
Assigned To
{userData?.roleName?.toLowerCase() === "client" && (
<span className="ml-2 text-xs text-muted-foreground inline-flex items-center">
<Lock className="h-3 w-3 mr-1" />
Clients cannot change assignee
</span>
)}
</Label>
<CustomEmployeeDropdown
value={bug.assignedTo?.toString() || "unassigned"}
onChange={(value) => {
// Only allow changes if user is not a client
if (userData?.roleName?.toLowerCase() !== "client") {
handleSelectChange("assignedTo", value);
}
}}
employees={employees}
placeholder="Select assignee"
disabled={userData?.roleName?.toLowerCase() === "client"}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="environment">Environment</Label>
<CustomDropdown
value={bug.environment || "Staging"}
onChange={(value) => handleSelectChange("environment", value)}
options={[
{ value: "Production", label: "Production" },
{ value: "Staging", label: "Staging" },
{ value: "Testing", label: "Testing" },
{ value: "Development", label: "Development" },
{ value: "QA", label: "QA" },
{ value: "UAT", label: "UAT" },
{ value: "Local", label: "Local" }
]}
placeholder="Select environment"
/>
</div>
</div>
<div className="grid gap-4 grid-cols-1 md:grid-cols-2">
{userData?.roleName?.toLowerCase() !== "client" && (
<div className="grid gap-2">
<Label htmlFor="startDate">Start Date</Label>
<div className="relative">
<Input
id="startDate"
type="date"
value={startDate ? format(startDate, "yyyy-MM-dd") : ""}
onChange={(e) => {
const date = e.target.value ? new Date(e.target.value) : undefined;
setStartDate(date);
}}
className="w-full"
/>
</div>
</div>
)}
<div className="grid gap-2">
<Label htmlFor="endDate">Target Fix Date</Label>
<div className="relative">
<Input
id="endDate"
type="date"
value={endDate ? format(endDate, "yyyy-MM-dd") : ""}
onChange={(e) => {
const date = e.target.value ? new Date(e.target.value) : undefined;
setEndDate(date);
}}
className="w-full"
/>
</div>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="steps" className="space-y-4 mt-0">
<div className="grid grid-cols-1 gap-4">
<div className="grid gap-2">
<Label htmlFor="stepsToReproduce">Steps to Reproduce</Label>
<Textarea
id="stepsToReproduce"
name="stepsToReproduce"
value={bug.stepsToReproduce || ""}
onChange={handleInputChange}
placeholder="List the steps to reproduce the bug"
rows={4}
/>
<p className="text-xs text-muted-foreground">
Provide a step-by-step guide to reproduce the bug consistently.
</p>
</div>
<div className="grid gap-2">
<Label htmlFor="inputs">Test Data / Inputs</Label>
<Textarea
id="inputs"
name="inputs"
value={bug.inputs || ""}
onChange={handleInputChange}
placeholder="Test data or inputs needed to reproduce the bug"
rows={3}
/>
</div>
<Separator />
<div className="grid gap-2">
<Label htmlFor="expectedBehavior">Expected Behavior</Label>
<Textarea
id="expectedBehavior"
name="expectedBehavior"
value={bug.expectedBehavior || ""}
onChange={handleInputChange}
placeholder="What should happen when the steps are followed"
rows={3}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="actualBehavior">Actual Behavior</Label>
<Textarea
id="actualBehavior"
name="actualBehavior"
value={bug.actualBehavior || ""}
onChange={handleInputChange}
placeholder="What actually happens when the steps are followed"
rows={3}
/>
</div>
</div>
</TabsContent>
{userData?.roleName?.toLowerCase() !== "client" && (
<TabsContent value="resolution" className="space-y-4 mt-0">
<div className="grid grid-cols-1 gap-4">
<div className="grid gap-2">
<Label htmlFor="rootCause">Root Cause</Label>
<Textarea
id="rootCause"
name="rootCause"
value={bug.rootCause || ""}
onChange={handleInputChange}
placeholder="What caused this bug"
rows={3}
/>
<p className="text-xs text-muted-foreground">
Identify the underlying cause of the bug (to be filled after analysis).
</p>
</div>
<div className="grid gap-2">
<Label htmlFor="resolution">Resolution</Label>
<Textarea
id="resolution"
name="resolution"
value={bug.resolution || ""}
onChange={handleInputChange}
placeholder="How the bug was fixed or will be fixed"
rows={3}
/>
</div>
<div className="mt-2 flex items-center p-3 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800 rounded-md">
<AlertTriangle className="h-5 w-5 text-amber-600 dark:text-amber-500 mr-2 flex-shrink-0" />
<p className="text-sm text-amber-800 dark:text-amber-300">
The resolution details are typically filled after the bug has been analyzed and fixed.
</p>
</div>
</div>
</TabsContent>
)}
</CardContent>
</Card>
<div className="mt-6 flex justify-end">
<Button
variant="outline"
type="button"
onClick={handleCancel}
className="mr-2"
>
Cancel
</Button>
<Button
type="submit"
disabled={isSaving}
className="bg-blue-600 hover:bg-blue-700"
>
{isSaving ? (
<>
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-b-transparent border-white"></div>
Saving...
</>
) : (
<>
<Save className="mr-2 h-4 w-4" />
{isEditMode ? "Update Bug" : "Create Bug"}
</>
)}
</Button>
</div>
</Tabs>
</form>
)}
</div>
</div>
</Layout>
);
};
export default BugForm;