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:
@@ -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 }
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
export type ReportPeriodKey = 'thisMonth' | 'lastMonth' | 'custom'
|
||||
|
||||
export type ReportPeriodRange = {
|
||||
from: string
|
||||
to: string
|
||||
}
|
||||
|
||||
/** Formats a Date as an ISO "YYYY-MM-DD" string (local time, no timezone shift). */
|
||||
function toIsoDate(date: Date): string {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
/** Returns the {from, to} range (inclusive) for a given month offset relative to now. */
|
||||
function getMonthRange(offset: number): ReportPeriodRange {
|
||||
const now = new Date()
|
||||
const start = new Date(now.getFullYear(), now.getMonth() + offset, 1)
|
||||
const end = new Date(now.getFullYear(), now.getMonth() + offset + 1, 0)
|
||||
return { from: toIsoDate(start), to: toIsoDate(end) }
|
||||
}
|
||||
|
||||
export function useReportingFilters() {
|
||||
const thisMonth = (): ReportPeriodRange => getMonthRange(0)
|
||||
const lastMonth = (): ReportPeriodRange => getMonthRange(-1)
|
||||
|
||||
/** Resolves a preset key to a concrete range. `custom` keeps the provided range. */
|
||||
function rangeForPreset(key: ReportPeriodKey, current: ReportPeriodRange): ReportPeriodRange {
|
||||
switch (key) {
|
||||
case 'thisMonth':
|
||||
return thisMonth()
|
||||
case 'lastMonth':
|
||||
return lastMonth()
|
||||
case 'custom':
|
||||
return current
|
||||
}
|
||||
}
|
||||
|
||||
return { thisMonth, lastMonth, rangeForPreset, toIsoDate }
|
||||
}
|
||||
Reference in New Issue
Block a user