281 lines
7.1 KiB
TypeScript
281 lines
7.1 KiB
TypeScript
|
|
import { jsPDF } from "jspdf";
|
||
|
|
import autoTable from "jspdf-autotable";
|
||
|
|
|
||
|
|
import { Project, STATUS_CONFIG } from "@/types/project";
|
||
|
|
import { Task, TASK_STATUS_CONFIG, TASK_PRIORITY_CONFIG, TaskStatus, TaskPriority } from "@/types/task";
|
||
|
|
|
||
|
|
// Formateadores
|
||
|
|
function formatCurrency(amount: number): string {
|
||
|
|
return new Intl.NumberFormat("es-ES", {
|
||
|
|
style: "currency",
|
||
|
|
currency: "EUR",
|
||
|
|
minimumFractionDigits: 0,
|
||
|
|
maximumFractionDigits: 0,
|
||
|
|
}).format(amount);
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatDate(dateString: string | null | undefined): string {
|
||
|
|
if (!dateString) return "-";
|
||
|
|
try {
|
||
|
|
return new Date(dateString).toLocaleDateString("es-ES", {
|
||
|
|
day: "2-digit",
|
||
|
|
month: "short",
|
||
|
|
year: "numeric",
|
||
|
|
});
|
||
|
|
} catch {
|
||
|
|
return "-";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function getStatusLabel(status: string): string {
|
||
|
|
return STATUS_CONFIG[status as keyof typeof STATUS_CONFIG]?.label || status;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getTaskStatusLabel(status: TaskStatus): string {
|
||
|
|
return TASK_STATUS_CONFIG[status]?.label || status;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getTaskPriorityLabel(priority: TaskPriority): string {
|
||
|
|
return TASK_PRIORITY_CONFIG[priority]?.label || priority;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Exporta una lista de proyectos a PDF (vista resumen)
|
||
|
|
*/
|
||
|
|
export function exportProjectsToPDF(projects: Project[]): void {
|
||
|
|
const doc = new jsPDF();
|
||
|
|
const pageWidth = doc.internal.pageSize.getWidth();
|
||
|
|
|
||
|
|
// Título
|
||
|
|
doc.setFontSize(20);
|
||
|
|
doc.setTextColor(59, 130, 246); // blue-500
|
||
|
|
doc.text("Listado de Proyectos", pageWidth / 2, 20, { align: "center" });
|
||
|
|
|
||
|
|
// Fecha de generación
|
||
|
|
doc.setFontSize(10);
|
||
|
|
doc.setTextColor(100);
|
||
|
|
doc.text(
|
||
|
|
`Generado el ${new Date().toLocaleDateString("es-ES", {
|
||
|
|
day: "numeric",
|
||
|
|
month: "long",
|
||
|
|
year: "numeric",
|
||
|
|
hour: "2-digit",
|
||
|
|
minute: "2-digit",
|
||
|
|
})}`,
|
||
|
|
pageWidth / 2,
|
||
|
|
28,
|
||
|
|
{ align: "center" }
|
||
|
|
);
|
||
|
|
|
||
|
|
// Resumen
|
||
|
|
doc.setFontSize(11);
|
||
|
|
doc.setTextColor(60);
|
||
|
|
doc.text(`Total de proyectos: ${projects.length}`, 14, 40);
|
||
|
|
|
||
|
|
// Tabla de proyectos
|
||
|
|
const tableData = projects.map((project) => [
|
||
|
|
project.ref,
|
||
|
|
project.name,
|
||
|
|
project.client,
|
||
|
|
getStatusLabel(project.status),
|
||
|
|
`${project.progress.toFixed(0)}%`,
|
||
|
|
formatCurrency(project.budget),
|
||
|
|
formatDate(project.startDate),
|
||
|
|
formatDate(project.endDate),
|
||
|
|
]);
|
||
|
|
|
||
|
|
autoTable(doc, {
|
||
|
|
startY: 48,
|
||
|
|
head: [
|
||
|
|
["Ref", "Nombre", "Cliente", "Estado", "Progreso", "Presupuesto", "Inicio", "Fin"],
|
||
|
|
],
|
||
|
|
body: tableData,
|
||
|
|
styles: {
|
||
|
|
fontSize: 9,
|
||
|
|
cellPadding: 3,
|
||
|
|
},
|
||
|
|
headStyles: {
|
||
|
|
fillColor: [59, 130, 246], // blue-500
|
||
|
|
textColor: 255,
|
||
|
|
fontStyle: "bold",
|
||
|
|
},
|
||
|
|
alternateRowStyles: {
|
||
|
|
fillColor: [248, 250, 252], // slate-50
|
||
|
|
},
|
||
|
|
columnStyles: {
|
||
|
|
0: { cellWidth: 20 }, // Ref
|
||
|
|
1: { cellWidth: 40 }, // Nombre
|
||
|
|
2: { cellWidth: 30 }, // Cliente
|
||
|
|
3: { cellWidth: 22 }, // Estado
|
||
|
|
4: { cellWidth: 18 }, // Progreso
|
||
|
|
5: { cellWidth: 25 }, // Presupuesto
|
||
|
|
6: { cellWidth: 22 }, // Inicio
|
||
|
|
7: { cellWidth: 22 }, // Fin
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
// Guardar
|
||
|
|
const fileName = `proyectos_${new Date().toISOString().split("T")[0]}.pdf`;
|
||
|
|
doc.save(fileName);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Exporta un proyecto con sus tareas a PDF (vista detallada)
|
||
|
|
*/
|
||
|
|
export function exportProjectDetailToPDF(project: Project, tasks: Task[]): void {
|
||
|
|
const doc = new jsPDF();
|
||
|
|
const pageWidth = doc.internal.pageSize.getWidth();
|
||
|
|
|
||
|
|
// === HEADER ===
|
||
|
|
doc.setFontSize(22);
|
||
|
|
doc.setTextColor(59, 130, 246); // blue-500
|
||
|
|
doc.text(project.name, pageWidth / 2, 20, { align: "center" });
|
||
|
|
|
||
|
|
doc.setFontSize(12);
|
||
|
|
doc.setTextColor(100);
|
||
|
|
doc.text(`Referencia: ${project.ref}`, pageWidth / 2, 28, { align: "center" });
|
||
|
|
|
||
|
|
// Fecha de generación
|
||
|
|
doc.setFontSize(9);
|
||
|
|
doc.text(
|
||
|
|
`Generado el ${new Date().toLocaleDateString("es-ES", {
|
||
|
|
day: "numeric",
|
||
|
|
month: "long",
|
||
|
|
year: "numeric",
|
||
|
|
hour: "2-digit",
|
||
|
|
minute: "2-digit",
|
||
|
|
})}`,
|
||
|
|
pageWidth / 2,
|
||
|
|
35,
|
||
|
|
{ align: "center" }
|
||
|
|
);
|
||
|
|
|
||
|
|
// === INFORMACIÓN DEL PROYECTO ===
|
||
|
|
let yPos = 48;
|
||
|
|
|
||
|
|
doc.setFontSize(14);
|
||
|
|
doc.setTextColor(30);
|
||
|
|
doc.text("Información del Proyecto", 14, yPos);
|
||
|
|
yPos += 8;
|
||
|
|
|
||
|
|
// Tabla de información
|
||
|
|
const projectInfo = [
|
||
|
|
["Cliente", project.client || "-"],
|
||
|
|
["Estado", getStatusLabel(project.status)],
|
||
|
|
["Progreso", `${project.progress.toFixed(0)}%`],
|
||
|
|
["Presupuesto", formatCurrency(project.budget)],
|
||
|
|
["Fecha de inicio", formatDate(project.startDate)],
|
||
|
|
["Fecha de fin", formatDate(project.endDate)],
|
||
|
|
];
|
||
|
|
|
||
|
|
autoTable(doc, {
|
||
|
|
startY: yPos,
|
||
|
|
body: projectInfo,
|
||
|
|
styles: {
|
||
|
|
fontSize: 10,
|
||
|
|
cellPadding: 4,
|
||
|
|
},
|
||
|
|
columnStyles: {
|
||
|
|
0: { fontStyle: "bold", cellWidth: 45, textColor: [100, 100, 100] },
|
||
|
|
1: { cellWidth: 60 },
|
||
|
|
},
|
||
|
|
theme: "plain",
|
||
|
|
margin: { left: 14 },
|
||
|
|
});
|
||
|
|
|
||
|
|
// Obtener posición después de la tabla
|
||
|
|
yPos = (doc as jsPDF & { lastAutoTable?: { finalY: number } }).lastAutoTable?.finalY || yPos + 50;
|
||
|
|
|
||
|
|
// Descripción si existe
|
||
|
|
if (project.description) {
|
||
|
|
yPos += 10;
|
||
|
|
doc.setFontSize(14);
|
||
|
|
doc.setTextColor(30);
|
||
|
|
doc.text("Descripción", 14, yPos);
|
||
|
|
yPos += 6;
|
||
|
|
|
||
|
|
doc.setFontSize(10);
|
||
|
|
doc.setTextColor(60);
|
||
|
|
const descriptionLines = doc.splitTextToSize(project.description, pageWidth - 28);
|
||
|
|
doc.text(descriptionLines, 14, yPos);
|
||
|
|
yPos += descriptionLines.length * 5 + 5;
|
||
|
|
}
|
||
|
|
|
||
|
|
// === TAREAS ===
|
||
|
|
if (tasks.length > 0) {
|
||
|
|
yPos += 10;
|
||
|
|
|
||
|
|
// Verificar si necesitamos nueva página
|
||
|
|
if (yPos > 250) {
|
||
|
|
doc.addPage();
|
||
|
|
yPos = 20;
|
||
|
|
}
|
||
|
|
|
||
|
|
doc.setFontSize(14);
|
||
|
|
doc.setTextColor(30);
|
||
|
|
doc.text(`Tareas (${tasks.length})`, 14, yPos);
|
||
|
|
yPos += 8;
|
||
|
|
|
||
|
|
// Tabla de tareas
|
||
|
|
const taskData = tasks.map((task) => [
|
||
|
|
task.ref,
|
||
|
|
task.title,
|
||
|
|
getTaskStatusLabel(task.status),
|
||
|
|
getTaskPriorityLabel(task.priority),
|
||
|
|
`${task.progress}%`,
|
||
|
|
formatDate(task.startDate),
|
||
|
|
formatDate(task.endDate),
|
||
|
|
]);
|
||
|
|
|
||
|
|
autoTable(doc, {
|
||
|
|
startY: yPos,
|
||
|
|
head: [["Ref", "Tarea", "Estado", "Prioridad", "Progreso", "Inicio", "Fin"]],
|
||
|
|
body: taskData,
|
||
|
|
styles: {
|
||
|
|
fontSize: 8,
|
||
|
|
cellPadding: 3,
|
||
|
|
},
|
||
|
|
headStyles: {
|
||
|
|
fillColor: [147, 51, 234], // purple-600
|
||
|
|
textColor: 255,
|
||
|
|
fontStyle: "bold",
|
||
|
|
},
|
||
|
|
alternateRowStyles: {
|
||
|
|
fillColor: [248, 250, 252], // slate-50
|
||
|
|
},
|
||
|
|
columnStyles: {
|
||
|
|
0: { cellWidth: 18 }, // Ref
|
||
|
|
1: { cellWidth: 50 }, // Tarea
|
||
|
|
2: { cellWidth: 25 }, // Estado
|
||
|
|
3: { cellWidth: 22 }, // Prioridad
|
||
|
|
4: { cellWidth: 18 }, // Progreso
|
||
|
|
5: { cellWidth: 22 }, // Inicio
|
||
|
|
6: { cellWidth: 22 }, // Fin
|
||
|
|
},
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
yPos += 15;
|
||
|
|
doc.setFontSize(11);
|
||
|
|
doc.setTextColor(100);
|
||
|
|
doc.text("Este proyecto no tiene tareas asignadas.", 14, yPos);
|
||
|
|
}
|
||
|
|
|
||
|
|
// === FOOTER ===
|
||
|
|
const pageCount = doc.getNumberOfPages();
|
||
|
|
for (let i = 1; i <= pageCount; i++) {
|
||
|
|
doc.setPage(i);
|
||
|
|
doc.setFontSize(8);
|
||
|
|
doc.setTextColor(150);
|
||
|
|
doc.text(
|
||
|
|
`Página ${i} de ${pageCount}`,
|
||
|
|
pageWidth / 2,
|
||
|
|
doc.internal.pageSize.getHeight() - 10,
|
||
|
|
{ align: "center" }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Guardar
|
||
|
|
const fileName = `proyecto_${project.ref}_${new Date().toISOString().split("T")[0]}.pdf`;
|
||
|
|
doc.save(fileName);
|
||
|
|
}
|