import React, { useState, useCallback, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Upload, FileText, AlertCircle, Check } from "lucide-react"; import { toast } from "@/lib/custom-toast"; import { issuesApi, employeeService, projectService, deliverablesApi } from "@/lib/api"; import * as XLSX from 'xlsx'; import { IssueCreateRequest, Employee, ProjectApi, Deliverable } 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 ImportIssuesDialogProps { projectId?: number; deliverableId?: number; onImportComplete: () => void; } interface PreviewData { preview: any[]; total: number; all: any[]; } const ImportIssuesDialog: React.FC = ({ projectId, deliverableId, 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 [deliverables, setDeliverables] = useState([]); // Selected values const [selectedAssignedTo, setSelectedAssignedTo] = useState(""); const [selectedProjectId, setSelectedProjectId] = useState(projectId || ""); const [selectedDeliverableId, setSelectedDeliverableId] = useState(deliverableId || ""); // Fetch initial data useEffect(() => { const fetchInitialData = async () => { if (!isOpen) return; setLoading(true); try { // Fetch employees and projects in parallel const [employeeData, projectData] = await Promise.all([ employeeService.getAll(), projectService.getAll() ]); setEmployees(employeeData); setProjects(projectData); // If projectId is provided, fetch deliverables for that project if (projectId) { try { const deliverablesData = await deliverablesApi.getByProjectId(projectId); setDeliverables(deliverablesData); // Set default selected values from props setSelectedProjectId(projectId); if (deliverableId) { setSelectedDeliverableId(deliverableId); } } catch (error) { console.error("Error fetching deliverables:", error); } } } catch (error) { console.error("Error fetching data:", error); toast.error("Failed to load data"); } finally { setLoading(false); } }; fetchInitialData(); }, [isOpen, projectId, deliverableId]); // Fetch deliverables when project selection changes useEffect(() => { const fetchDeliverables = async () => { if (!selectedProjectId || typeof selectedProjectId !== 'number') { setDeliverables([]); setSelectedDeliverableId(""); return; } setLoading(true); try { const deliverablesData = await deliverablesApi.getByProjectId(selectedProjectId); setDeliverables(deliverablesData); } catch (error) { console.error("Error fetching deliverables:", error); toast.error("Failed to load deliverables"); setDeliverables([]); } finally { setLoading(false); } }; fetchDeliverables(); }, [selectedProjectId]); // Template Excel download handler const handleDownloadTemplate = () => { const headers = [ "Title", "Type", "Priority", "Severity", "Description", "Estimated Hours", "Reported By (Optional)", "Assigned To (Optional)", "Project ID (Optional)", "Deliverable ID (Optional)" ]; const sampleData = [ [ "Sample Issue Title", "Feature", // Type: Bug, User Story, Task, Documentation, Enhancement "Low", // Priority: Low, Medium, High, Critical "Low", // Severity: Critical, Major, Minor, Trivial "Description of the issue", "8", // Estimated Hours "", // Reported By (Employee ID) - Optional "", // Assigned To (Employee ID) - Optional projectId || "", // Project ID - Optional deliverableId || "" // Deliverable ID - Optional ] ]; // Create workbook and worksheet const workbook = XLSX.utils.book_new(); const worksheet = XLSX.utils.aoa_to_sheet([headers, ...sampleData]); // Add worksheet to workbook XLSX.utils.book_append_sheet(workbook, worksheet, "Issues"); // Write to buffer const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' }); // Create blob and download 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', 'issues_import_template.xlsx'); document.body.appendChild(link); link.click(); document.body.removeChild(link); }; const handleFileUpload = useCallback(async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) { return; } // Validate file type 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; } // Validate file size (max 10MB) 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); // Show loading indicator setIsUploading(true); try { // Read the Excel file const reader = new FileReader(); reader.onload = (e) => { try { const data = new Uint8Array(e.target?.result as ArrayBuffer); const workbook = XLSX.read(data, { type: 'array' }); // Get first sheet const firstSheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[firstSheetName]; // Convert to JSON const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); // Check if there's data if (jsonData.length <= 1) { // Just headers or empty setError("The Excel file contains no data rows."); setIsUploading(false); return; } // Get headers from first row const headers = jsonData[0] as string[]; // Convert to array of objects 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); } // Validate column headers const requiredFields = ["Title", "Type", "Priority", "Description"]; const missingFields = requiredFields.filter(field => !headers.includes(field) ); if (missingFields.length > 0) { setError(`Missing required columns: ${missingFields.join(', ')}`); setIsUploading(false); return; } // Store all rows for processing, but only display first 10 in preview const allRows = [...rows]; const previewRows = rows.slice(0, 10); // Set data for preview setPreviewData({ preview: previewRows, total: allRows.length, all: allRows }); // Auto-start import if data is valid toast.success(`Excel file loaded with ${allRows.length} issues 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); } }, []); const handleImport = useCallback(async () => { if (!previewData?.all?.length) { toast.error("No data to import"); return; } setIsUploading(true); setError(null); setUploadProgress(10); try { // Get all rows from the stored data const allIssues = previewData.all; const progressStep = 80 / allIssues.length; let successCount = 0; let failureCount = 0; // Process issues one by one for (let i = 0; i < allIssues.length; i++) { const issueData = allIssues[i] as Record; try { // Map Excel fields to issue create request const issueRequest: IssueCreateRequest = { biCode: `${new Date().getTime()}-${i}`, // Generate a unique code type: issueData.Type || "Task", description: issueData.Description || "", estimates: String(issueData["Estimated Hours"] || "0"), esUnit: 1, // Default unit (hours) priority: issueData.Priority || "Medium", severity: issueData.Severity || "Minor", // Use inline settings if they exist, otherwise use data from excel or default to 0 reportedBy: parseInt(String(issueData["Reported By (Optional)"])) || 0, assignedTo: selectedAssignedTo || parseInt(String(issueData["Assigned To (Optional)"])) || 0, remainingHr: parseFloat(String(issueData["Estimated Hours"])) || 0, title: issueData.Title, projectId: selectedProjectId || parseInt(String(issueData["Project ID (Optional)"])) || projectId || 0, deliverablesId: selectedDeliverableId || parseInt(String(issueData["Deliverable ID (Optional)"])) || deliverableId || 0 }; // Skip if missing essential data if (!issueRequest.title) { failureCount++; continue; } await issuesApi.create(issueRequest); successCount++; } catch (error) { console.error('Error importing issue:', error); failureCount++; } // Update progress setUploadProgress(10 + (i + 1) * progressStep); } setUploadProgress(100); if (successCount > 0) { toast.success(`Successfully imported ${successCount} issues`); if (failureCount > 0) { toast.error(`Failed to import ${failureCount} issues`); } setTimeout(() => { setIsOpen(false); onImportComplete(); setPreviewData(null); setFileName(""); setUploadProgress(0); // Reset inline settings setSelectedAssignedTo(""); setSelectedProjectId(""); setSelectedDeliverableId(""); }, 1500); } else { throw new Error("Failed to import any issues"); } } 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, deliverableId, onImportComplete, selectedAssignedTo, selectedProjectId, selectedDeliverableId]); const renderContent = () => (
{fileName && previewData?.preview?.length > 0 && (
Showing {previewData.preview.length} of {previewData.total} issues
)}
{!fileName && (
)} {isUploading && (
)} {fileName && previewData?.preview?.length > 0 && (
{fileName} {!isUploading && ( )}
{/* Inline settings for all issues */}

Set values for all issues:

{/* Assigned To */}
({ 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" />
{/* Project ID */}
({ label: proj.projectName, value: proj.id.toString() })) ]} value={selectedProjectId ? selectedProjectId.toString() : ""} onValueChange={(value) => { setSelectedProjectId(value ? parseInt(value) : ""); if (!value) { setSelectedDeliverableId(""); } }} placeholder="Select Project" disabled={isUploading || loading} className="w-full" />
{/* Deliverable ID */}
({ label: del.name, value: del.id.toString() })) ]} value={selectedDeliverableId ? selectedDeliverableId.toString() : ""} onValueChange={(value) => setSelectedDeliverableId(value ? parseInt(value) : "")} placeholder="Select Deliverable" disabled={!selectedProjectId || isUploading || loading} className="w-full" />

These values will override any values in the imported file for all issues.

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 && (
Importing issues... {Math.round(uploadProgress)}%
)}
{error && ( Error {error} )}
); const renderFooter = () => (
{previewData?.preview?.length > 0 && !isUploading && ( )}
); return ( <> !isUploading && setIsOpen(false)} title="Import Features" description="Upload an Excel file containing issue data. Download the template for the required format." allowClose={!isUploading} isPending={isUploading} >
{renderContent()}
{renderFooter()}
); }; export default ImportIssuesDialog;