pmtool / src /components /employee-projects /EmployeeProjectList.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
27 kB
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;