diff --git a/src/app/api/reports/upcoming-projects/route.js b/src/app/api/reports/upcoming-projects/route.js new file mode 100644 index 0000000..a00c58a --- /dev/null +++ b/src/app/api/reports/upcoming-projects/route.js @@ -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 } + ); + } +} diff --git a/src/app/calendar/page.js b/src/app/calendar/page.js index 67fc418..854eab1 100644 --- a/src/app/calendar/page.js +++ b/src/app/calendar/page.js @@ -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() { +