feat: add download functionality for upcoming projects report in Excel format
This commit is contained in:
192
src/app/api/reports/upcoming-projects/route.js
Normal file
192
src/app/api/reports/upcoming-projects/route.js
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user