Spaces:
Sleeping
Sleeping
| import React, { useState, useRef, useEffect } from 'react'; | |
| import { EmployeeProjectDetail } from '../../types'; | |
| import { Button } from '../ui/button'; | |
| import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; | |
| import { | |
| Table, | |
| TableBody, | |
| TableCell, | |
| TableHead, | |
| TableHeader, | |
| TableRow | |
| } from '../ui/table'; | |
| import { format } from 'date-fns'; | |
| import { toast } from 'sonner'; | |
| import { | |
| Pencil, | |
| Trash2, | |
| Plus, | |
| CalendarRange, | |
| Briefcase, | |
| Building, | |
| Code, | |
| AlertCircle, | |
| Printer, | |
| User, | |
| FileDown | |
| } from 'lucide-react'; | |
| import CustomDialog from '../CustomDialog'; | |
| import EmployeeProjectForm from './EmployeeProjectForm'; | |
| import { deleteEmployeeProjectDetail, employeeProjectDetailApi } from '../../services/employeeProjectDetailApi'; | |
| interface EmployeeProjectListProps { | |
| projects: EmployeeProjectDetail[]; | |
| employeeId: number; | |
| employeeName?: string; | |
| onEdit: (projectId: number, data: Partial<EmployeeProjectDetail>) => Promise<void>; | |
| onAdd: (data: Partial<EmployeeProjectDetail>) => Promise<void>; | |
| onRefresh: () => void; | |
| } | |
| // Add a utility component to safely render HTML content | |
| const HtmlContent: React.FC<{ html: string }> = ({ html }) => { | |
| return ( | |
| <div dangerouslySetInnerHTML={{ __html: html }} className="prose prose-sm max-w-none" /> | |
| ); | |
| }; | |
| const EmployeeProjectList: React.FC<EmployeeProjectListProps> = ({ | |
| projects = [], | |
| employeeId, | |
| employeeName = '', | |
| onEdit, | |
| onAdd, | |
| onRefresh | |
| }) => { | |
| const projectsArray = Array.isArray(projects) ? projects : []; | |
| // Debug logs | |
| console.log('EmployeeProjectList received props:'); | |
| console.log('- employeeId:', employeeId); | |
| console.log('- employeeName:', employeeName); | |
| console.log('- projects count:', projectsArray.length); | |
| const [showAddDialog, setShowAddDialog] = useState(false); | |
| const [showEditDialog, setShowEditDialog] = useState(false); | |
| const [showDeleteDialog, setShowDeleteDialog] = useState(false); | |
| const [selectedProject, setSelectedProject] = useState<EmployeeProjectDetail | null>(null); | |
| const [deletingId, setDeletingId] = useState<number | null>(null); | |
| const [isProcessing, setIsProcessing] = useState(false); | |
| const [printMode, setPrintMode] = useState(false); | |
| const printRef = useRef<HTMLDivElement>(null); | |
| // Store employee name in local state to ensure it's available for printing | |
| const [localEmployeeName, setLocalEmployeeName] = useState(employeeName); | |
| // Update local state when prop changes | |
| useEffect(() => { | |
| console.log('Employee name prop changed to:', employeeName); | |
| if (employeeName) { | |
| setLocalEmployeeName(employeeName); | |
| } | |
| }, [employeeName]); | |
| const handleAdd = async (data: Partial<EmployeeProjectDetail>) => { | |
| setIsProcessing(true); | |
| try { | |
| await onAdd(data); | |
| setShowAddDialog(false); | |
| onRefresh(); | |
| toast.success('Project added successfully'); | |
| } catch (error) { | |
| console.error('Error adding project:', error); | |
| toast.error('Failed to add project'); | |
| } finally { | |
| setIsProcessing(false); | |
| } | |
| }; | |
| const handleEdit = async (data: Partial<EmployeeProjectDetail>) => { | |
| if (selectedProject?.id) { | |
| setIsProcessing(true); | |
| try { | |
| await onEdit(selectedProject.id, data); | |
| setShowEditDialog(false); | |
| setSelectedProject(null); | |
| onRefresh(); | |
| toast.success('Project updated successfully'); | |
| } catch (error) { | |
| console.error('Error updating project:', error); | |
| toast.error('Failed to update project'); | |
| } finally { | |
| setIsProcessing(false); | |
| } | |
| } | |
| }; | |
| const handleDeleteClick = (project: EmployeeProjectDetail) => { | |
| setSelectedProject(project); | |
| setShowDeleteDialog(true); | |
| }; | |
| const handleDelete = async () => { | |
| if (!selectedProject) return; | |
| setIsProcessing(true); | |
| setDeletingId(selectedProject.id); | |
| try { | |
| const result = await employeeProjectDetailApi.delete(selectedProject.id); | |
| if (result.success) { | |
| toast.success(result.message || 'Project deleted successfully'); | |
| setShowDeleteDialog(false); | |
| setSelectedProject(null); | |
| onRefresh(); | |
| } else { | |
| toast.error(result.message || 'Failed to delete project'); | |
| } | |
| } catch (error) { | |
| console.error('Error deleting project:', error); | |
| toast.error('Failed to delete project'); | |
| } finally { | |
| setIsProcessing(false); | |
| setDeletingId(null); | |
| } | |
| }; | |
| const formatDate = (dateString: string | undefined) => { | |
| if (!dateString) return '-'; | |
| try { | |
| return format(new Date(dateString), 'MMM dd, yyyy'); | |
| } catch (error) { | |
| return dateString; | |
| } | |
| }; | |
| const handlePrint = () => { | |
| setPrintMode(true); | |
| // Debug log before printing | |
| console.log('Print function called with employeeName:', employeeName); | |
| console.log('Print function using localEmployeeName:', localEmployeeName); | |
| // Create a new window for printing | |
| const printWindow = window.open('', '_blank'); | |
| if (printWindow && printRef.current) { | |
| // Get the HTML content directly | |
| const printContents = printRef.current.innerHTML; | |
| const totalProjects = projectsArray.length; | |
| printWindow.document.write(` | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>Project History${localEmployeeName ? ' - ' + localEmployeeName : ''}</title> | |
| <style> | |
| @page { margin: 1cm; } | |
| body { | |
| font-family: Arial, sans-serif; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| .print-container { | |
| width: 100%; | |
| padding: 20px; | |
| } | |
| .header { | |
| display: flex; | |
| flex-direction: column; | |
| margin-bottom: 20px; | |
| border-bottom: 1px solid #ccc; | |
| padding-bottom: 10px; | |
| } | |
| .header h1 { | |
| margin: 0 0 10px 0; | |
| color: #333; | |
| } | |
| .document-type { | |
| font-size: 16px; | |
| color: #666; | |
| margin-bottom: 5px; | |
| font-weight: 500; | |
| } | |
| .content-info { | |
| margin-bottom: 20px; | |
| font-style: italic; | |
| color: #666; | |
| } | |
| .project-item { | |
| margin-bottom: 25px; | |
| page-break-inside: avoid; | |
| border-bottom: 1px solid #eee; | |
| padding-bottom: 20px; | |
| } | |
| .project-item h3 { | |
| margin-bottom: 10px; | |
| color: #333; | |
| } | |
| .client-company { | |
| display: inline-block; | |
| padding: 3px 8px; | |
| background-color: #f3f4f6; | |
| border: 1px solid #e5e7eb; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| margin-bottom: 10px; | |
| } | |
| .details-section { | |
| margin-bottom: 15px; | |
| } | |
| .meta-info { | |
| color: #666; | |
| font-size: 12px; | |
| margin-bottom: 5px; | |
| } | |
| .meta-info-label { | |
| font-weight: bold; | |
| display: inline-block; | |
| width: 120px; | |
| } | |
| @media print { | |
| .no-print { display: none; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="print-container"> | |
| <div class="header"> | |
| ${localEmployeeName ? `<h1>${localEmployeeName}</h1>` : `<h1>Employee Project History</h1>`} | |
| <div class="document-type">Project History</div> | |
| </div> | |
| <div class="content-info"> | |
| Total projects: ${totalProjects} | |
| </div> | |
| <div class="print-content"> | |
| ${printContents} | |
| </div> | |
| <div class="no-print" style="margin-top: 30px; text-align: center;"> | |
| <button onclick="window.print()" style="padding: 8px 16px; margin-right: 10px;">Print</button> | |
| <button onclick="window.close()" style="padding: 8px 16px;">Close</button> | |
| </div> | |
| </div> | |
| </body> | |
| </html> | |
| `); | |
| printWindow.document.close(); | |
| // Add event listeners | |
| printWindow.onafterprint = () => { | |
| setPrintMode(false); | |
| }; | |
| printWindow.onunload = () => { | |
| setPrintMode(false); | |
| }; | |
| // Use a longer delay to ensure content is properly rendered | |
| setTimeout(() => { | |
| // Log to debug | |
| console.log(`Preparing to print ${totalProjects} project details`); | |
| try { | |
| printWindow.focus(); | |
| printWindow.print(); | |
| } catch (error) { | |
| console.error("Print error:", error); | |
| setPrintMode(false); | |
| } | |
| }, 1200); | |
| } else { | |
| // If window couldn't be opened, reset the state | |
| setPrintMode(false); | |
| toast.error("Could not open print window. Please check your popup blocker settings."); | |
| } | |
| }; | |
| const handleDownloadAsWord = () => { | |
| // Create a Blob with HTML content | |
| const htmlContent = ` | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>Project History${localEmployeeName ? ' - ' + localEmployeeName : ''}</title> | |
| <style> | |
| body { | |
| font-family: Arial, sans-serif; | |
| margin: 0; | |
| padding: 20px; | |
| } | |
| h1 { | |
| color: #333; | |
| margin-bottom: 10px; | |
| } | |
| .document-type { | |
| font-size: 16px; | |
| color: #666; | |
| margin-bottom: 10px; | |
| } | |
| .project-item { | |
| margin-bottom: 25px; | |
| border-bottom: 1px solid #eee; | |
| padding-bottom: 20px; | |
| } | |
| .project-item h3 { | |
| margin-bottom: 10px; | |
| color: #333; | |
| } | |
| .client-company { | |
| display: inline-block; | |
| padding: 3px 8px; | |
| background-color: #f3f4f6; | |
| border: 1px solid #e5e7eb; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| margin-bottom: 10px; | |
| } | |
| .meta-info { | |
| color: #666; | |
| font-size: 12px; | |
| margin-bottom: 5px; | |
| } | |
| .meta-info-label { | |
| font-weight: bold; | |
| display: inline-block; | |
| width: 120px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>${localEmployeeName ? localEmployeeName : 'Employee Project History'}</h1> | |
| <div class="document-type">Project History</div> | |
| <div>Total projects: ${projectsArray.length}</div> | |
| <br> | |
| ${projectsArray.map(project => ` | |
| <div class="project-item"> | |
| <h3>${project.projectName}</h3> | |
| <div class="client-company">${project.clientCompany}</div> | |
| <div class="details-section"> | |
| <div class="meta-info"> | |
| <span class="meta-info-label">Duration:</span> | |
| ${formatDate(project.startDate)} - ${formatDate(project.endDate)} | |
| </div> | |
| <div class="meta-info"> | |
| <span class="meta-info-label">Role:</span> | |
| ${project.role} | |
| </div> | |
| ${project.technologiesUsed ? ` | |
| <div class="meta-info"> | |
| <span class="meta-info-label">Technologies:</span> | |
| ${project.technologiesUsed} | |
| </div> | |
| ` : ''} | |
| ${project.projectDetails ? ` | |
| <div class="meta-info"> | |
| <span class="meta-info-label">Project Details:</span> | |
| ${project.projectDetails} | |
| </div> | |
| ` : ''} | |
| ${project.responsibilities ? ` | |
| <div class="meta-info"> | |
| <span class="meta-info-label">Responsibilities:</span> | |
| ${project.responsibilities} | |
| </div> | |
| ` : ''} | |
| ${project.sector ? ` | |
| <div class="meta-info"> | |
| <span class="meta-info-label">Sector:</span> | |
| ${project.sector} | |
| </div> | |
| ` : ''} | |
| </div> | |
| </div> | |
| `).join('')} | |
| </body> | |
| </html> | |
| `; | |
| const blob = new Blob([htmlContent], { type: 'application/msword' }); | |
| const url = URL.createObjectURL(blob); | |
| // Create a temporary link to trigger download | |
| const link = document.createElement('a'); | |
| link.href = url; | |
| link.download = `${localEmployeeName ? localEmployeeName.replace(/\s+/g, '_') : 'Employee'}_Project_History.doc`; | |
| document.body.appendChild(link); | |
| link.click(); | |
| // Clean up | |
| setTimeout(() => { | |
| document.body.removeChild(link); | |
| URL.revokeObjectURL(url); | |
| }, 100); | |
| toast.success("Downloaded as Word document"); | |
| }; | |
| return ( | |
| <div className="space-y-4"> | |
| <Card> | |
| <CardHeader className="flex flex-row items-center justify-between"> | |
| <CardTitle>Project History</CardTitle> | |
| <div className="flex items-center gap-2"> | |
| {projectsArray.length > 0 && ( | |
| <> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={handlePrint} | |
| className="flex items-center gap-1" | |
| > | |
| <Printer className="h-4 w-4 mr-1" /> | |
| Print All | |
| </Button> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={handleDownloadAsWord} | |
| className="flex items-center gap-1" | |
| > | |
| <FileDown className="h-4 w-4 mr-1" /> | |
| Download as Word | |
| </Button> | |
| </> | |
| )} | |
| <Button onClick={() => setShowAddDialog(true)} size="sm"> | |
| <Plus className="mr-2 h-4 w-4" /> Add Project | |
| </Button> | |
| </div> | |
| </CardHeader> | |
| <CardContent> | |
| {projectsArray.length === 0 ? ( | |
| <div className="text-center py-8 text-gray-500"> | |
| <Briefcase className="mx-auto h-12 w-12 opacity-20 mb-2" /> | |
| <p>No project history found</p> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={() => setShowAddDialog(true)} | |
| className="mt-2" | |
| > | |
| Add your first project | |
| </Button> | |
| </div> | |
| ) : ( | |
| <div className="rounded-md border"> | |
| <Table> | |
| <TableHeader> | |
| <TableRow> | |
| <TableHead>Project Name</TableHead> | |
| <TableHead>Client Company</TableHead> | |
| <TableHead>Duration</TableHead> | |
| <TableHead>Role</TableHead> | |
| <TableHead className="w-[100px]">Actions</TableHead> | |
| </TableRow> | |
| </TableHeader> | |
| <TableBody> | |
| {projectsArray.map((project) => ( | |
| <TableRow key={project.id}> | |
| <TableCell className="font-medium">{project.projectName}</TableCell> | |
| <TableCell>{project.clientCompany}</TableCell> | |
| <TableCell> | |
| <div className="flex items-center"> | |
| <CalendarRange className="mr-1 h-3.5 w-3.5 text-muted-foreground" /> | |
| <span> | |
| {formatDate(project.startDate)} - {formatDate(project.endDate)} | |
| </span> | |
| </div> | |
| </TableCell> | |
| <TableCell>{project.role}</TableCell> | |
| <TableCell> | |
| <div className="flex space-x-2"> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| onClick={() => { | |
| setSelectedProject(project); | |
| setShowEditDialog(true); | |
| }} | |
| > | |
| <Pencil className="h-4 w-4" /> | |
| </Button> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| onClick={() => handleDeleteClick(project)} | |
| disabled={deletingId === project.id} | |
| > | |
| <Trash2 className="h-4 w-4 text-destructive" /> | |
| </Button> | |
| </div> | |
| </TableCell> | |
| </TableRow> | |
| ))} | |
| </TableBody> | |
| </Table> | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| {projectsArray.length > 0 && ( | |
| <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6"> | |
| {projectsArray.map((project) => ( | |
| <Card key={project.id} className="overflow-hidden h-full"> | |
| <div className="bg-primary/10 p-4"> | |
| <div className="flex justify-between items-start"> | |
| <div> | |
| <h3 className="text-lg font-semibold line-clamp-1">{project.projectName}</h3> | |
| <div className="flex items-center text-sm text-muted-foreground mt-1"> | |
| <Building className="mr-1 h-3.5 w-3.5 shrink-0" /> | |
| <span className="line-clamp-1">{project.clientCompany}</span> | |
| </div> | |
| </div> | |
| <div className="flex shrink-0 ml-2"> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| onClick={() => { | |
| setSelectedProject(project); | |
| setShowEditDialog(true); | |
| }} | |
| > | |
| <Pencil className="h-4 w-4" /> | |
| </Button> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| onClick={() => handleDeleteClick(project)} | |
| disabled={deletingId === project.id} | |
| > | |
| <Trash2 className="h-4 w-4 text-destructive" /> | |
| </Button> | |
| </div> | |
| </div> | |
| </div> | |
| <CardContent className="p-4"> | |
| <div className="grid grid-cols-2 gap-3 mb-3"> | |
| <div> | |
| <p className="text-sm font-medium mb-1">Duration</p> | |
| <div className="flex items-center text-sm"> | |
| <CalendarRange className="mr-1 h-3.5 w-3.5 shrink-0 text-muted-foreground" /> | |
| <span> | |
| {formatDate(project.startDate)} - {formatDate(project.endDate)} | |
| </span> | |
| </div> | |
| </div> | |
| <div> | |
| <p className="text-sm font-medium mb-1">Role</p> | |
| <div className="flex items-center text-sm"> | |
| <Briefcase className="mr-1 h-3.5 w-3.5 shrink-0 text-muted-foreground" /> | |
| <span className="line-clamp-1">{project.role}</span> | |
| </div> | |
| </div> | |
| </div> | |
| {project.technologiesUsed && ( | |
| <div className="mb-3"> | |
| <p className="text-sm font-medium mb-1">Technologies</p> | |
| <div className="flex items-start text-sm"> | |
| <Code className="mr-1 h-3.5 w-3.5 shrink-0 mt-0.5 text-muted-foreground" /> | |
| <span className="line-clamp-2">{project.technologiesUsed}</span> | |
| </div> | |
| </div> | |
| )} | |
| {project.projectDetails && ( | |
| <div className="mb-3"> | |
| <p className="text-sm font-medium mb-1">Project Details</p> | |
| <div className="text-sm text-muted-foreground max-h-24 overflow-y-auto"> | |
| <HtmlContent html={project.projectDetails} /> | |
| </div> | |
| </div> | |
| )} | |
| {project.responsibilities && ( | |
| <div> | |
| <p className="text-sm font-medium mb-1">Responsibilities</p> | |
| <div className="text-sm text-muted-foreground max-h-24 overflow-y-auto"> | |
| <HtmlContent html={project.responsibilities} /> | |
| </div> | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| ))} | |
| </div> | |
| )} | |
| {/* Add Project Dialog */} | |
| <CustomDialog | |
| isOpen={showAddDialog} | |
| onClose={() => setShowAddDialog(false)} | |
| title="Add New Project" | |
| allowClose={!isProcessing} | |
| isPending={isProcessing} | |
| > | |
| <EmployeeProjectForm | |
| employeeId={employeeId} | |
| onSubmit={handleAdd} | |
| onCancel={() => setShowAddDialog(false)} | |
| /> | |
| </CustomDialog> | |
| {/* Edit Project Dialog */} | |
| <CustomDialog | |
| isOpen={showEditDialog} | |
| onClose={() => { | |
| setShowEditDialog(false); | |
| setSelectedProject(null); | |
| }} | |
| title="Edit Project" | |
| allowClose={!isProcessing} | |
| isPending={isProcessing} | |
| > | |
| {selectedProject && ( | |
| <EmployeeProjectForm | |
| employeeId={employeeId} | |
| project={selectedProject} | |
| onSubmit={handleEdit} | |
| onCancel={() => { | |
| setShowEditDialog(false); | |
| setSelectedProject(null); | |
| }} | |
| /> | |
| )} | |
| </CustomDialog> | |
| {/* Delete Confirmation Dialog */} | |
| <CustomDialog | |
| isOpen={showDeleteDialog} | |
| onClose={() => { | |
| setShowDeleteDialog(false); | |
| setSelectedProject(null); | |
| }} | |
| title="Delete Project" | |
| allowClose={!isProcessing} | |
| isPending={isProcessing} | |
| > | |
| <div className="space-y-4"> | |
| <div className="flex items-start gap-3 p-4 bg-destructive/10 rounded-md text-destructive"> | |
| <AlertCircle className="h-5 w-5 shrink-0 mt-0.5" /> | |
| <div> | |
| <h3 className="font-medium">Are you sure you want to delete this project?</h3> | |
| <p className="text-sm text-destructive/80"> | |
| This action cannot be undone. This will permanently delete the project | |
| "{selectedProject?.projectName}" from the employee's history. | |
| </p> | |
| </div> | |
| </div> | |
| <div className="flex justify-end gap-2 mt-4"> | |
| <Button | |
| variant="outline" | |
| onClick={() => { | |
| setShowDeleteDialog(false); | |
| setSelectedProject(null); | |
| }} | |
| disabled={isProcessing} | |
| > | |
| Cancel | |
| </Button> | |
| <Button | |
| variant="destructive" | |
| onClick={handleDelete} | |
| disabled={isProcessing} | |
| > | |
| {isProcessing ? 'Deleting...' : 'Delete Project'} | |
| </Button> | |
| </div> | |
| </div> | |
| </CustomDialog> | |
| {/* Hidden printable content */} | |
| <div className="hidden"> | |
| <div ref={printRef}> | |
| {projectsArray.map((project) => ( | |
| <div key={project.id} className="project-item"> | |
| <h3>{project.projectName}</h3> | |
| <div className="client-company">{project.clientCompany}</div> | |
| <div className="details-section"> | |
| <div className="meta-info"> | |
| <span className="meta-info-label">Duration:</span> | |
| {formatDate(project.startDate)} - {formatDate(project.endDate)} | |
| </div> | |
| <div className="meta-info"> | |
| <span className="meta-info-label">Role:</span> | |
| {project.role} | |
| </div> | |
| {project.technologiesUsed && ( | |
| <div className="meta-info"> | |
| <span className="meta-info-label">Technologies:</span> | |
| {project.technologiesUsed} | |
| </div> | |
| )} | |
| {project.projectDetails && ( | |
| <div className="meta-info"> | |
| <span className="meta-info-label">Project Details:</span> | |
| <div dangerouslySetInnerHTML={{ __html: project.projectDetails }} /> | |
| </div> | |
| )} | |
| {project.responsibilities && ( | |
| <div className="meta-info"> | |
| <span className="meta-info-label">Responsibilities:</span> | |
| <div dangerouslySetInnerHTML={{ __html: project.responsibilities }} /> | |
| </div> | |
| )} | |
| {project.sector && ( | |
| <div className="meta-info"> | |
| <span className="meta-info-label">Sector:</span> | |
| {project.sector} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default EmployeeProjectList; |