feat: add download functionality for upcoming projects report in Excel format

This commit is contained in:
2025-10-22 11:38:19 +02:00
parent af28be8112
commit 42668862fd
2 changed files with 237 additions and 0 deletions

View File

@@ -0,0 +1,192 @@
import { NextResponse } from 'next/server';
import ExcelJS from 'exceljs';
import { getAllProjects } from '@/lib/queries/projects';
import { parseISO, isAfter, isBefore, startOfDay, addWeeks, differenceInDays } from 'date-fns';
export const dynamic = 'force-dynamic';
export async function GET(request) {
try {
const today = startOfDay(new Date());
const nextMonth = addWeeks(today, 5); // Next 5 weeks
// Get all projects
const allProjects = getAllProjects();
// Filter for upcoming projects (not fulfilled, not cancelled, have finish dates)
const upcomingProjects = allProjects
.filter(project => {
if (!project.finish_date) return false;
if (project.project_status === 'fulfilled' || project.project_status === 'cancelled') return false;
try {
const projectDate = parseISO(project.finish_date);
return isAfter(projectDate, today) && isBefore(projectDate, nextMonth);
} catch (error) {
return false;
}
})
.sort((a, b) => {
const dateA = parseISO(a.finish_date);
const dateB = parseISO(b.finish_date);
return dateA - dateB;
});
// Filter for overdue projects
const overdueProjects = allProjects
.filter(project => {
if (!project.finish_date) return false;
if (project.project_status === 'fulfilled' || project.project_status === 'cancelled') return false;
try {
const projectDate = parseISO(project.finish_date);
return isBefore(projectDate, today);
} catch (error) {
return false;
}
})
.sort((a, b) => {
const dateA = parseISO(a.finish_date);
const dateB = parseISO(b.finish_date);
return dateB - dateA; // Most recently overdue first
});
// Create workbook
const workbook = new ExcelJS.Workbook();
workbook.creator = 'Panel Zarządzania Projektami';
workbook.created = new Date();
// Status translations
const statusTranslations = {
registered: 'Zarejestrowany',
approved: 'Zatwierdzony',
pending: 'Oczekujący',
in_progress: 'W trakcie',
in_progress_design: 'W realizacji (projektowanie)',
in_progress_construction: 'W realizacji (realizacja)',
fulfilled: 'Zakończony',
cancelled: 'Wycofany',
};
// Create Upcoming Projects sheet
const upcomingSheet = workbook.addWorksheet('Nadchodzące terminy');
upcomingSheet.columns = [
{ header: 'Nazwa projektu', key: 'name', width: 35 },
{ header: 'Klient', key: 'customer', width: 25 },
{ header: 'Adres', key: 'address', width: 30 },
{ header: 'Działka', key: 'plot', width: 15 },
{ header: 'Data zakończenia', key: 'finish_date', width: 18 },
{ header: 'Dni do terminu', key: 'days_until', width: 15 },
{ header: 'Status', key: 'status', width: 25 },
{ header: 'Odpowiedzialny', key: 'assigned_to', width: 20 }
];
// Style header row
upcomingSheet.getRow(1).font = { bold: true };
upcomingSheet.getRow(1).fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FF4472C4' }
};
upcomingSheet.getRow(1).font = { bold: true, color: { argb: 'FFFFFFFF' } };
// Add upcoming projects data
upcomingProjects.forEach(project => {
const daysUntil = differenceInDays(parseISO(project.finish_date), today);
const row = upcomingSheet.addRow({
name: project.project_name,
customer: project.customer || '',
address: project.address || '',
plot: project.plot || '',
finish_date: project.finish_date,
days_until: daysUntil,
status: statusTranslations[project.project_status] || project.project_status,
assigned_to: project.assigned_to || ''
});
// Color code based on urgency
if (daysUntil <= 7) {
row.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFFFE0E0' } // Light red
};
} else if (daysUntil <= 14) {
row.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFFFF4E0' } // Light orange
};
}
});
// Create Overdue Projects sheet
if (overdueProjects.length > 0) {
const overdueSheet = workbook.addWorksheet('Przeterminowane');
overdueSheet.columns = [
{ header: 'Nazwa projektu', key: 'name', width: 35 },
{ header: 'Klient', key: 'customer', width: 25 },
{ header: 'Adres', key: 'address', width: 30 },
{ header: 'Działka', key: 'plot', width: 15 },
{ header: 'Data zakończenia', key: 'finish_date', width: 18 },
{ header: 'Dni po terminie', key: 'days_overdue', width: 15 },
{ header: 'Status', key: 'status', width: 25 },
{ header: 'Odpowiedzialny', key: 'assigned_to', width: 20 }
];
// Style header row
overdueSheet.getRow(1).font = { bold: true };
overdueSheet.getRow(1).fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFE74C3C' }
};
overdueSheet.getRow(1).font = { bold: true, color: { argb: 'FFFFFFFF' } };
// Add overdue projects data
overdueProjects.forEach(project => {
const daysOverdue = Math.abs(differenceInDays(parseISO(project.finish_date), today));
const row = overdueSheet.addRow({
name: project.project_name,
customer: project.customer || '',
address: project.address || '',
plot: project.plot || '',
finish_date: project.finish_date,
days_overdue: daysOverdue,
status: statusTranslations[project.project_status] || project.project_status,
assigned_to: project.assigned_to || ''
});
// Color code based on severity
row.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFFFE0E0' } // Light red
};
});
}
// Generate buffer
const buffer = await workbook.xlsx.writeBuffer();
// Generate filename with current date
const filename = `nadchodzace_projekty_${new Date().toISOString().split('T')[0]}.xlsx`;
// Return response with Excel file
return new NextResponse(buffer, {
headers: {
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'Content-Disposition': `attachment; filename="${filename}"`,
},
});
} catch (error) {
console.error('Error generating upcoming projects report:', error);
return NextResponse.json(
{ error: 'Failed to generate report', details: error.message },
{ status: 500 }
);
}
}