feat(reporting) : add Reporting dashboard front layer

LST-59 (3.1) front. Completes the Reporting module.

- New frontend/modules/reporting/ layer (auto-detected): /reporting page
  (admin middleware) consuming the 4 read-only report endpoints.
- Filters (period presets + custom dates, project, user). 4 sections (time per
  project, time per user, tasks by status, absences by type) each with a
  DataTable + a Chart.js chart (reused global registration).
- Front-side CSV export per section (useCsvExport: BOM UTF-8, ; separator).
- i18n keys (reporting.*, sidebar.admin.reporting).

nuxt build passes; /reporting routed; no route regression.
This commit is contained in:
Matthieu
2026-06-21 00:16:03 +02:00
parent b3b29fd753
commit f4ffc02028
9 changed files with 713 additions and 1 deletions
@@ -0,0 +1,44 @@
export type CsvColumn<T> = {
header: string
value: (row: T) => string | number
}
/** Escapes a single CSV cell (quotes wrapping + doubled inner quotes). */
function escapeCell(value: string | number): string {
const str = String(value ?? '')
if (/[",\n;]/.test(str)) {
return `"${str.replace(/"/g, '""')}"`
}
return str
}
export function useCsvExport() {
/** Builds a CSV string from rows + column definitions (semicolon separator, FR-friendly). */
function toCsv<T>(rows: T[], columns: CsvColumn<T>[]): string {
const header = columns.map(c => escapeCell(c.header)).join(';')
const lines = rows.map(row =>
columns.map(c => escapeCell(c.value(row))).join(';'),
)
return [header, ...lines].join('\n')
}
/** Triggers a browser download of the given CSV content. */
function downloadCsv<T>(filename: string, rows: T[], columns: CsvColumn<T>[]): void {
if (typeof window === 'undefined') {
return
}
const csv = toCsv(rows, columns)
// Prepend BOM so Excel detects UTF-8.
const blob = new Blob([`${csv}`], { type: 'text/csv;charset=utf-8;' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename.endsWith('.csv') ? filename : `${filename}.csv`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
return { toCsv, downloadCsv }
}