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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,7 @@ export default function ProjectCalendarPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
const [viewMode, setViewMode] = useState('month'); // 'month' or 'upcoming'
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/projects")
|
||||
@@ -131,6 +132,37 @@ export default function ProjectCalendarPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownloadReport = async () => {
|
||||
setDownloading(true);
|
||||
try {
|
||||
const response = await fetch('/api/reports/upcoming-projects');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to download report');
|
||||
}
|
||||
|
||||
// Get the blob from the response
|
||||
const blob = await response.blob();
|
||||
|
||||
// Create a download link
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `nadchodzace_projekty_${new Date().toISOString().split('T')[0]}.xlsx`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
// Clean up
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Error downloading report:', error);
|
||||
alert('Błąd podczas pobierania raportu');
|
||||
} finally {
|
||||
setDownloading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderCalendarGrid = () => {
|
||||
const monthStart = startOfMonth(currentDate);
|
||||
const monthEnd = endOfMonth(currentDate);
|
||||
@@ -383,6 +415,19 @@ export default function ProjectCalendarPage() {
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="mb-4 flex justify-end">
|
||||
<button
|
||||
onClick={handleDownloadReport}
|
||||
disabled={downloading}
|
||||
className="p-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed rounded transition-colors"
|
||||
title={downloading ? 'Pobieranie...' : 'Eksportuj raport nadchodzących projektów do Excel'}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth="2">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{viewMode === 'month' ? renderCalendarGrid() : renderUpcomingView()}
|
||||
</PageContainer>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user