diff --git a/README.md b/README.md index 94b67e6..b77e172 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,12 @@ A comprehensive project management system built with Next.js for managing constr - System activity monitoring - Audit trail for compliance and debugging +### 馃搳 Data Export + +- Export projects to Excel format grouped by status +- Includes project name, address, plot, WP, and finish date +- Separate sheets for each project status (registered, in progress, fulfilled, etc.) + ## Tech Stack - **Framework**: Next.js 15.1.8 @@ -178,6 +184,7 @@ src/ - `npm run build` - Build for production - `npm run start` - Start production server - `npm run lint` - Run ESLint +- `npm run export-projects` - Export all projects to Excel file grouped by status ## Docker Commands diff --git a/export-projects-to-excel.mjs b/export-projects-to-excel.mjs new file mode 100644 index 0000000..923c9f5 --- /dev/null +++ b/export-projects-to-excel.mjs @@ -0,0 +1,58 @@ +import * as XLSX from 'xlsx'; +import { getAllProjects } from './src/lib/queries/projects.js'; + +function exportProjectsToExcel() { + try { + // Get all projects + const projects = getAllProjects(); + + // Group projects by status + const groupedProjects = projects.reduce((acc, project) => { + const status = project.project_status || 'unknown'; + if (!acc[status]) { + acc[status] = []; + } + acc[status].push({ + 'Nazwa projektu': project.project_name, + 'Adres': project.address || '', + 'Dzia艂ka': project.plot || '', + 'WP': project.wp || '', + 'Data zako艅czenia': project.finish_date || '' + }); + return acc; + }, {}); + + // Polish status translations for sheet names + const statusTranslations = { + 'registered': 'Zarejestrowany', + 'in_progress_design': 'W realizacji (projektowanie)', + 'in_progress_construction': 'W realizacji (budowa)', + 'fulfilled': 'Zako艅czony', + 'cancelled': 'Wycofany', + 'unknown': 'Nieznany' + }; + + // Create workbook + const workbook = XLSX.utils.book_new(); + + // Create a sheet for each status + Object.keys(groupedProjects).forEach(status => { + const sheetName = statusTranslations[status] || status; + const worksheet = XLSX.utils.json_to_sheet(groupedProjects[status]); + XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); + }); + + // Write to file + const filename = `projects_export_${new Date().toISOString().split('T')[0]}.xlsx`; + XLSX.writeFile(workbook, filename); + + console.log(`Excel file created: ${filename}`); + console.log(`Sheets created for statuses: ${Object.keys(groupedProjects).join(', ')}`); + + } catch (error) { + console.error('Error exporting projects to Excel:', error); + } +} + +// Run the export +exportProjectsToExcel(); \ No newline at end of file diff --git a/package.json b/package.json index 8044179..ae4db69 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "start": "next start", "lint": "next lint", "create-admin": "node scripts/create-admin.js", + "export-projects": "node export-projects-to-excel.mjs", "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage", @@ -45,6 +46,7 @@ "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.5.0", "@types/leaflet": "^1.9.18", + "concurrently": "^9.2.1", "eslint": "^9", "eslint-config-next": "15.1.8", "jest": "^29.7.0", diff --git a/src/app/api/projects/export/route.js b/src/app/api/projects/export/route.js new file mode 100644 index 0000000..b46ba6a --- /dev/null +++ b/src/app/api/projects/export/route.js @@ -0,0 +1,96 @@ +// Force this API route to use Node.js runtime for database access and file operations +export const runtime = "nodejs"; + +import * as XLSX from 'xlsx'; +import { getAllProjects } from "@/lib/queries/projects"; +import initializeDatabase from "@/lib/init-db"; +import { NextResponse } from "next/server"; +import { withReadAuth } from "@/lib/middleware/auth"; +import { + logApiActionSafe, + AUDIT_ACTIONS, + RESOURCE_TYPES, +} from "@/lib/auditLogSafe.js"; + +// Make sure the DB is initialized before queries run +initializeDatabase(); + +async function exportProjectsHandler(req) { + try { + // Get all projects + const projects = getAllProjects(); + + // Group projects by status + const groupedProjects = projects.reduce((acc, project) => { + const status = project.project_status || 'unknown'; + if (!acc[status]) { + acc[status] = []; + } + acc[status].push({ + 'Nazwa projektu': project.project_name, + 'Adres': project.address || '', + 'Dzia艂ka': project.plot || '', + 'WP': project.wp || '', + 'Data zako艅czenia': project.finish_date || '' + }); + return acc; + }, {}); + + // Polish status translations for sheet names + const statusTranslations = { + 'registered': 'Zarejestrowany', + 'in_progress_design': 'W realizacji (projektowanie)', + 'in_progress_construction': 'W realizacji (budowa)', + 'fulfilled': 'Zako艅czony', + 'cancelled': 'Wycofany', + 'unknown': 'Nieznany' + }; + + // Create workbook + const workbook = XLSX.utils.book_new(); + + // Create a sheet for each status + Object.keys(groupedProjects).forEach(status => { + const sheetName = statusTranslations[status] || status; + const worksheet = XLSX.utils.json_to_sheet(groupedProjects[status]); + XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); + }); + + // Generate buffer + const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }); + + // Generate filename with current date + const filename = `projects_export_${new Date().toISOString().split('T')[0]}.xlsx`; + + // Log the export action + await logApiActionSafe( + req, + AUDIT_ACTIONS.DATA_EXPORT, + RESOURCE_TYPES.PROJECT, + null, + req.auth, + { + exportType: 'excel', + totalProjects: projects.length, + statuses: Object.keys(groupedProjects) + } + ); + + // Return the 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 exporting projects to Excel:', error); + return NextResponse.json( + { error: 'Failed to export projects' }, + { status: 500 } + ); + } +} + +export const GET = withReadAuth(exportProjectsHandler); \ No newline at end of file diff --git a/src/app/projects/page.js b/src/app/projects/page.js index b1f1746..4f131e4 100644 --- a/src/app/projects/page.js +++ b/src/app/projects/page.js @@ -123,6 +123,28 @@ export default function ProjectListPage() { setSearchTerm(''); }; + const handleExportExcel = async () => { + try { + const response = await fetch('/api/projects/export'); + if (response.ok) { + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `projects_export_${new Date().toISOString().split('T')[0]}.xlsx`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + } else { + alert('Failed to export projects. Please try again.'); + } + } catch (error) { + console.error('Export error:', error); + alert('An error occurred during export. Please try again.'); + } + }; + const toggleFilters = () => { setFiltersExpanded(!filtersExpanded); }; @@ -203,6 +225,27 @@ export default function ProjectListPage() { {t('projects.mine') || 'Moje'} )} +