import React, { useState, useCallback, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Upload, FileText, AlertCircle, Download } from "lucide-react"; import { toast } from "sonner"; import { tasksApi, TaskCreateRequest } from "@/services/tasksApi"; import { employeeService, projectService, issuesApi } from "@/lib/api"; import * as XLSX from 'xlsx'; import { Employee, ProjectApi, IssueApi } from "@/types"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Progress } from "@/components/ui/progress"; import CustomDialog from "@/components/CustomDialog"; import { Dropdown } from "@/components/ui/dropdown"; interface ImportTasksDialogProps { taskType: "Bug" | "Test Case"; projectId?: number; onImportComplete: () => void; } interface PreviewData { preview: any[]; total: number; all: any[]; } const ImportTasksDialog: React.FC = ({ taskType, projectId, onImportComplete }) => { const [isOpen, setIsOpen] = useState(false); const [isUploading, setIsUploading] = useState(false); const [error, setError] = useState(null); const [uploadProgress, setUploadProgress] = useState(0); const [previewData, setPreviewData] = useState(null); const [fileName, setFileName] = useState(""); // State for data from APIs const [loading, setLoading] = useState(false); const [employees, setEmployees] = useState([]); const [projects, setProjects] = useState([]); const [issues, setIssues] = useState([]); const [filteredIssues, setFilteredIssues] = useState([]); // Selected values const [selectedAssignedTo, setSelectedAssignedTo] = useState(""); const [selectedProjectId, setSelectedProjectId] = useState(projectId || ""); const [selectedIssueId, setSelectedIssueId] = useState(""); // Get default status based on task type const getDefaultStatus = () => { if (taskType === "Bug") { return "Open"; } else { return "Draft"; } }; // Fetch initial data useEffect(() => { const fetchInitialData = async () => { if (!isOpen) return; setLoading(true); try { const [employeeData, projectData, issuesData] = await Promise.all([ employeeService.getAll(), projectService.getAll(), issuesApi.getAll() ]); setEmployees(employeeData); setProjects(projectData); setIssues(issuesData); if (projectId) { setSelectedProjectId(projectId); } } catch (error) { console.error("Error fetching data:", error); toast.error("Failed to load data"); } finally { setLoading(false); } }; fetchInitialData(); }, [isOpen, projectId]); // Filter issues based on selected project useEffect(() => { if (selectedProjectId && typeof selectedProjectId === 'number') { const filtered = issues.filter(issue => issue.projectId === selectedProjectId); setFilteredIssues(filtered); // Reset selected issue if it's not in the filtered list if (selectedIssueId && !filtered.find(issue => issue.id === selectedIssueId)) { setSelectedIssueId(""); } } else { setFilteredIssues(issues); } }, [selectedProjectId, issues, selectedIssueId]); // Template Excel download handler const handleDownloadTemplate = () => { const headers = taskType === "Bug" ? [ "Title", "Description", "Issue/Feature ID (Required)", "Priority", "Severity", "Assigned To (Employee ID - Optional)", "Project ID (Optional)", "Version (Optional)", "Environment (Optional)", "Steps To Reproduce (Optional)", "Expected Behavior (Optional)", "Actual Behavior (Optional)" ] : [ "Title", "Description", "Issue/Feature ID (Required)", "Priority", "Severity", "Assigned To (Employee ID - Optional)", "Project ID (Optional)", "Version (Optional)", "Test Steps (Optional)", "Expected Result (Optional)", "Actual Result (Optional)" ]; const sampleData = taskType === "Bug" ? [ [ "Sample Bug Title", "Description of the bug", "1", // Issue/Feature ID - Required (use actual ID from your issues list) "High", // Priority: Low, Medium, High, Critical "Major", // Severity: Critical, Major, Minor, Trivial "", // Assigned To (Employee ID) - Optional projectId || "", // Project ID - Optional "1.0.0", // Version "Production", // Environment "1. Step one\n2. Step two", // Steps to reproduce "Expected behavior description", // Expected Behavior "Actual behavior description" // Actual Behavior ] ] : [ [ "Sample Test Case Title", "Description of the test case", "1", // Issue/Feature ID - Required (use actual ID from your issues list) "High", // Priority: Low, Medium, High, Critical "High", // Severity: High, Medium, Low "", // Assigned To (Employee ID) - Optional projectId || "", // Project ID - Optional "1.0.0", // Version "1. Test step one\n2. Test step two", // Test Steps "Expected result description", // Expected Result "Actual result description" // Actual Result ] ]; const workbook = XLSX.utils.book_new(); const worksheet = XLSX.utils.aoa_to_sheet([headers, ...sampleData]); // Set column widths worksheet['!cols'] = headers.map(() => ({ wch: 20 })); XLSX.utils.book_append_sheet(workbook, worksheet, taskType === "Bug" ? "Bugs" : "Test Cases"); const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' }); const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.setAttribute('href', url); link.setAttribute('download', `${taskType.toLowerCase().replace(' ', '_')}_import_template.xlsx`); document.body.appendChild(link); link.click(); document.body.removeChild(link); toast.success("Template downloaded successfully"); }; const handleFileUpload = useCallback(async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) { return; } const fileExtension = file.name.split('.').pop()?.toLowerCase(); if (fileExtension !== 'xlsx' && fileExtension !== 'xls') { const errorMsg = "Invalid file type. Please upload an Excel file (.xlsx or .xls)."; setError(errorMsg); toast.error(errorMsg); return; } if (file.size > 10 * 1024 * 1024) { const errorMsg = "File size too large. Maximum size is 10MB"; setError(errorMsg); toast.error(errorMsg); return; } setFileName(file.name); setError(null); setPreviewData(null); setIsUploading(true); try { const reader = new FileReader(); reader.onload = (e) => { try { const data = new Uint8Array(e.target?.result as ArrayBuffer); const workbook = XLSX.read(data, { type: 'array' }); const firstSheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[firstSheetName]; const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); if (jsonData.length <= 1) { setError("The Excel file contains no data rows."); setIsUploading(false); return; } const headers = jsonData[0] as string[]; const rows = []; for (let i = 1; i < jsonData.length; i++) { const row = jsonData[i] as any[]; const obj: any = {}; for (let j = 0; j < headers.length; j++) { obj[headers[j]] = row[j] || ""; } rows.push(obj); } const requiredFields = ["Title", "Description", "Issue/Feature ID (Required)"]; const missingFields = requiredFields.filter(field => !headers.includes(field) ); if (missingFields.length > 0) { setError(`Missing required columns: ${missingFields.join(', ')}`); setIsUploading(false); return; } const allRows = [...rows]; const previewRows = rows.slice(0, 10); setPreviewData({ preview: previewRows, total: allRows.length, all: allRows }); toast.success(`Excel file loaded with ${allRows.length} ${taskType.toLowerCase()}s ready to import`); setIsUploading(false); } catch (error) { console.error('Excel parsing error:', error); setError("Failed to parse Excel file. Please check the file format."); toast.error("Failed to parse Excel file"); setIsUploading(false); } }; reader.readAsArrayBuffer(file); } catch (error) { console.error('File reading error:', error); setError("Failed to read the file. Please try again."); toast.error("Failed to read the file"); setIsUploading(false); } }, [taskType]); const handleImport = useCallback(async () => { if (!previewData?.all?.length) { toast.error("No data to import"); return; } setIsUploading(true); setError(null); setUploadProgress(10); try { const allTasks = previewData.all; const progressStep = 80 / allTasks.length; let successCount = 0; let failureCount = 0; for (let i = 0; i < allTasks.length; i++) { const taskData = allTasks[i] as Record; try { const issueId = selectedIssueId || parseInt(String(taskData["Issue/Feature ID (Required)"])) || null; // Skip if missing required issue/feature ID if (!issueId) { console.error(`Row ${i + 1}: Missing required Issue/Feature ID`); failureCount++; continue; } const taskRequest: TaskCreateRequest = { type: taskType, title: taskData.Title || "", description: taskData.Description || "", priority: taskData.Priority || "Medium", severity: taskData.Severity || (taskType === "Bug" ? "Minor" : "Medium"), status: getDefaultStatus(), assignedTo: selectedAssignedTo || parseInt(String(taskData["Assigned To (Employee ID - Optional)"])) || null, issuesId: issueId, version: taskData["Version (Optional)"] || "", estimates: "0", esUnit: 1, remainingHr: 0, stateDate: new Date().toISOString(), endDate: null, sprintName: null, }; // Add bug-specific fields if (taskType === "Bug") { taskRequest.environment = taskData["Environment (Optional)"] || ""; taskRequest.stepsToReproduce = taskData["Steps To Reproduce (Optional)"] || ""; taskRequest.expectedBehavior = taskData["Expected Behavior (Optional)"] || ""; taskRequest.actualBehavior = taskData["Actual Behavior (Optional)"] || ""; } else { // Test case specific fields taskRequest.stepsToReproduce = taskData["Test Steps (Optional)"] || ""; taskRequest.expectedBehavior = taskData["Expected Result (Optional)"] || ""; taskRequest.actualBehavior = taskData["Actual Result (Optional)"] || ""; } if (!taskRequest.title) { failureCount++; continue; } await tasksApi.create(taskRequest); successCount++; } catch (error) { console.error(`Error importing ${taskType.toLowerCase()}:`, error); failureCount++; } setUploadProgress(10 + (i + 1) * progressStep); } setUploadProgress(100); if (successCount > 0) { toast.success(`Successfully imported ${successCount} ${taskType.toLowerCase()}${successCount > 1 ? 's' : ''}`); if (failureCount > 0) { toast.error(`Failed to import ${failureCount} ${taskType.toLowerCase()}${failureCount > 1 ? 's' : ''}`); } setTimeout(() => { setIsOpen(false); onImportComplete(); setPreviewData(null); setFileName(""); setUploadProgress(0); setSelectedAssignedTo(""); setSelectedProjectId(""); setSelectedIssueId(""); }, 1500); } else { throw new Error(`Failed to import any ${taskType.toLowerCase()}s`); } } catch (error: any) { console.error('Import processing error:', error); setError(error.message || "Failed to process Excel data"); toast.error(error.message || "Failed to process Excel data"); setUploadProgress(0); setIsUploading(false); } }, [previewData, projectId, onImportComplete, selectedAssignedTo, selectedProjectId, selectedIssueId, taskType]); const renderContent = () => (
{fileName && previewData?.preview?.length > 0 && (
Showing {previewData.preview.length} of {previewData.total} {taskType.toLowerCase()}s
)}
{!fileName && (
)} {isUploading && !previewData && (
)} {fileName && previewData?.preview?.length > 0 && (
{fileName} {!isUploading && ( )}

Set values for all {taskType.toLowerCase()}s:

({ label: `${emp.firstName} ${emp.lastName}`, value: emp.id.toString() })) ]} value={selectedAssignedTo ? selectedAssignedTo.toString() : ""} onValueChange={(value) => setSelectedAssignedTo(value ? parseInt(value) : "")} placeholder="Select Assignee" disabled={isUploading || loading} className="w-full" />
({ label: proj.projectName, value: proj.id.toString() })) ]} value={selectedProjectId ? selectedProjectId.toString() : ""} onValueChange={(value) => setSelectedProjectId(value ? parseInt(value) : "")} placeholder="Select Project" disabled={isUploading || loading} className="w-full" />
({ label: `${issue.biCode} - ${issue.title}`, value: issue.id.toString() })) ]} value={selectedIssueId ? selectedIssueId.toString() : ""} onValueChange={(value) => setSelectedIssueId(value ? parseInt(value) : "")} placeholder="Select Issue/Feature" disabled={isUploading || loading} className="w-full" />

Note: Issue/Feature is mandatory for all {taskType.toLowerCase()}s. If set here, it will override values in the imported file. Otherwise, each row must have a valid Issue/Feature ID.

Preview rows:

{previewData?.preview?.length > 0 && Object.keys(previewData.preview[0]).map((header) => ( ))} {previewData?.preview?.map((row, rowIndex) => ( {Object.values(row).map((cell: any, cellIndex) => ( ))} ))}
{header}
{String(cell || '').substring(0, 50)} {String(cell || '').length > 50 ? '...' : ''}
)} {isUploading && previewData && (
Importing {taskType.toLowerCase()}s... {Math.round(uploadProgress)}%
)}
{error && ( Error {error} )}
); const renderFooter = () => (
{previewData?.preview?.length > 0 && !isUploading && ( )}
); return ( <> !isUploading && setIsOpen(false)} title={`Import ${taskType}s`} description={`Upload an Excel file containing ${taskType.toLowerCase()} data. Download the template for the required format.`} allowClose={!isUploading} isPending={isUploading} >
{renderContent()}
{renderFooter()}
); }; export default ImportTasksDialog;