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 (
{getLabel()}
{open && (
{options.map((option) => (
{ e.preventDefault(); e.stopPropagation(); handleOptionClick(option.value); }} > {option.label} {value === option.value && }
))}
)}
); } // 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 (
{value && value !== "unassigned" ? ( {getInitials(value)} ) : ( )} {getEmployeeName(value)}
{!disabled && } {disabled && }
{open && (
{/* Search Input */}
setSearchTerm(e.target.value)} className="h-8 text-sm" autoFocus onClick={(e) => e.stopPropagation()} />
{/* Options Container */}
{/* Unassigned Option */} {(!searchTerm || "unassigned".includes(searchTerm.toLowerCase())) && (
{ e.preventDefault(); e.stopPropagation(); handleOptionClick("unassigned"); }} > Unassigned {isUnassigned() && ( )}
)} {/* Employee Options */} {filteredEmployees.length > 0 ? ( filteredEmployees.map((employee) => (
{ e.preventDefault(); e.stopPropagation(); handleOptionClick(employee.id.toString()); }} > {(employee.firstName.charAt(0) + employee.lastName.charAt(0)).toUpperCase()} {employee.firstName} {employee.lastName} {isEmployeeSelected(employee.id.toString()) && ( )}
)) ) : ( searchTerm && (
No employees found matching "{searchTerm}"
) )}
)}
); } // 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 (
{value !== null ? getProjectName(value) : placeholder}
{open && (
{ e.preventDefault(); e.stopPropagation(); handleOptionClick("all"); }} > All Projects {isAllProjectsSelected() && }
{projects.map((project) => (
{ e.preventDefault(); e.stopPropagation(); handleOptionClick(project.id.toString()); }} >
{project.projectName} Code: {project.projectCode}
{isProjectSelected(project.id) && }
))}
)}
); } // 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 (
{value !== null ? getIssueName(value) : placeholder} {showError && *}
{showError && !open && (

This field is required

)} {open && (
setSearchTerm(e.target.value)} onClick={(e) => e.stopPropagation()} />
{ e.preventDefault(); e.stopPropagation(); handleOptionClick("null"); }} > No linked issue {isNoIssueSelected() && }
{filteredIssues.length === 0 ? (
No issues found in your assigned projects
) : ( filteredIssues.map((issue) => (
{ e.preventDefault(); e.stopPropagation(); handleOptionClick(issue.id.toString()); }} >
{issue.title} #{issue.id} • {issue.status}
{isIssueSelected(issue.id) && }
)) )}
)}
); } type BugFormProps = { isEditMode?: boolean; defaultType?: string; }; const BugForm: React.FC = ({ 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([]); const [issues, setIssues] = useState([]); const [allIssues, setAllIssues] = useState([]); const [projects, setProjects] = useState([]); const [selectedProject, setSelectedProject] = useState(null); const [activeTab, setActiveTab] = useState("details"); const [validationErrors, setValidationErrors] = useState([]); const [bug, setBug] = useState({ 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(new Date()); const [endDate, setEndDate] = useState(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) => { 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 (
{/* Breadcrumb Navigation */}

{isEditMode ? `Edit ${bug.type === "Production Bug" ? "Production Bug" : "Bug"}` : `New ${bug.type === "Production Bug" ? "Production Bug" : "Bug"}`}

{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`}

{isLoading ? (

Loading bug data...

) : (
Bug Details Steps & Behavior {userData?.roleName?.toLowerCase() !== "client" && ( Resolution )}
Fields marked with * are required
handleSelectChange("project", value)} projects={projects} placeholder="All Projects" />

Filter issues by project

handleSelectChange("issuesId", value)} issues={issues} placeholder="Link to a feature" required isError={validationErrors.includes("issuesId")} />

Connect this bug to a related feature