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'}
)}
+