feat: implement project export to Excel with API endpoint and UI button

This commit is contained in:
2025-10-09 20:43:53 +02:00
parent ce3c53b4a8
commit ac5fedb61a
6 changed files with 208 additions and 0 deletions

View File

@@ -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

View File

@@ -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();

View File

@@ -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",

View File

@@ -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);

View File

@@ -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'}
</Button>
)}
<Button
variant="outline"
size="lg"
className="w-full sm:w-auto"
onClick={handleExportExcel}
>
<svg
className="w-5 h-5 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
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>
{t('projects.exportExcel') || 'Export to Excel'}
</Button>
<Link href="/projects/new" className="w-full sm:w-auto">
<Button variant="primary" size="lg" className="w-full">
<svg

View File

@@ -178,6 +178,7 @@ const translations = {
projects: "projektów",
mapView: "Widok mapy",
mine: "Moje",
exportExcel: "Eksport do Excela",
createFirstProject: "Utwórz pierwszy projekt",
noMatchingResults: "Brak projektów pasujących do kryteriów wyszukiwania. Spróbuj zmienić wyszukiwane frazy.",
showingResults: "Wyświetlono {shown} z {total} projektów",
@@ -742,6 +743,7 @@ const translations = {
projects: "projects",
mapView: "Map View",
mine: "Mine",
exportExcel: "Export to Excel",
createFirstProject: "Create first project",
noMatchingResults: "No projects match the search criteria. Try changing your search terms.",
showingResults: "Showing {shown} of {total} projects",