mirror of
https://github.com/Djeeberjr/fw-anwesenheit.git
synced 2025-11-03 15:24:09 +00:00
added csv exporting logic
This commit is contained in:
parent
cd63dd3ee4
commit
7346b47816
31
web/src/lib/Day.ts
Normal file
31
web/src/lib/Day.ts
Normal file
@ -0,0 +1,31 @@
|
||||
export type Day = number;
|
||||
|
||||
export interface AttendanceDay {
|
||||
date: Day,
|
||||
ids: string[],
|
||||
}
|
||||
|
||||
export function dayToDate(day: Day): Date {
|
||||
const SEC_PER_DAY = 86_400;
|
||||
|
||||
return new Date(day * SEC_PER_DAY * 1000);
|
||||
}
|
||||
|
||||
export async function fetchDay(day: Day): Promise<AttendanceDay> {
|
||||
let res = await fetch("/api/day?" + (new URLSearchParams({ day: day.toString() }).toString()));
|
||||
|
||||
let json = await res.json();
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
export async function fetchDays(from: Date, to: Date): Promise<Day[]> {
|
||||
let q = new URLSearchParams({ from: (from.getTime() / 1000).toString(), to: (to.getTime() / 1000).toString() });
|
||||
|
||||
let res = await fetch("/api/days?" + q);
|
||||
|
||||
let json = await res.json();
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
106
web/src/lib/csv.ts
Normal file
106
web/src/lib/csv.ts
Normal file
@ -0,0 +1,106 @@
|
||||
interface CSVOptions {
|
||||
delimiter?: string; // default: ","
|
||||
includeHeader?: boolean; // default: true for object input, false for array-of-arrays unless headers provided
|
||||
headerOrder?: string[]; // explicit ordering for columns that should come first
|
||||
eol?: string; // default: "\r\n"
|
||||
includeBOM?: boolean; // default: false (useful for Excel)
|
||||
nullString?: string; // default: "" (how to render null/undefined)
|
||||
}
|
||||
|
||||
type RowObject = Record<string, any>;
|
||||
type InputRows = RowObject[];
|
||||
|
||||
export function generateCSVString(input: InputRows, opts: CSVOptions = {}): string {
|
||||
const {
|
||||
delimiter = ",",
|
||||
includeHeader,
|
||||
headerOrder,
|
||||
eol = "\r\n",
|
||||
includeBOM = false,
|
||||
nullString = "",
|
||||
} = opts;
|
||||
|
||||
// Check if the value need quoting
|
||||
// Usually not needed in our use case but still in case
|
||||
const needsQuoting = (s: string) =>
|
||||
s.includes(delimiter) || s.includes('"') || s.includes("\n") || s.includes("\r") || /^\s|\s$/.test(s);
|
||||
|
||||
// Transform the value of a cell in a SAFE string
|
||||
const escapeCell = (raw: any): string => {
|
||||
if (raw === null || raw === undefined) return nullString;
|
||||
|
||||
let s = defaultStringify(raw);
|
||||
|
||||
// Replace quotes
|
||||
if (s.includes('"')) s = s.replace(/"/g, '""');
|
||||
|
||||
return needsQuoting(s) ? `"${s}"` : s;
|
||||
};
|
||||
|
||||
// Transform the value of a cell into a string
|
||||
function defaultStringify(v: any): string {
|
||||
if (v === null || v === undefined) return nullString;
|
||||
if (v instanceof Date) return v.toLocaleDateString();
|
||||
if (typeof v === "boolean") return v ? "X" : "";
|
||||
|
||||
if (typeof v === "object") {
|
||||
// CHeck if array and join with "|"
|
||||
if (Array.isArray(v)) return v.map(item => (item === null || item === undefined ? nullString : String(item))).join("|");
|
||||
|
||||
// If all fails parse it via json
|
||||
// Should also not happen in our use case
|
||||
try { return JSON.stringify(v); } catch { return String(v); }
|
||||
}
|
||||
return String(v);
|
||||
}
|
||||
|
||||
const objs = (input as RowObject[]) || [];
|
||||
|
||||
// Derive headers in the order keys are first encountered (fixed: no reduce)
|
||||
const seen = new Set<string>();
|
||||
const derivedOrder: string[] = [];
|
||||
for (const obj of objs) {
|
||||
if (!obj || typeof obj !== "object") continue;
|
||||
for (const k of Object.keys(obj)) {
|
||||
if (!seen.has(k)) {
|
||||
seen.add(k);
|
||||
derivedOrder.push(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply headerOrder if provided: put listed columns first (in the order provided),
|
||||
// then append the remaining derived headers in their derived order.
|
||||
let finalHeaders = derivedOrder.slice();
|
||||
if (Array.isArray(headerOrder) && headerOrder.length > 0) {
|
||||
const headSet = new Set(headerOrder);
|
||||
const first = headerOrder.filter(h => seen.has(h)); // keep only headers that actually exist
|
||||
const rest = derivedOrder.filter(h => !headSet.has(h));
|
||||
finalHeaders = first.concat(rest);
|
||||
}
|
||||
|
||||
const shouldIncludeHeader = typeof includeHeader === "boolean" ? includeHeader : finalHeaders.length > 0;
|
||||
const rowsOut: string[] = [];
|
||||
|
||||
if (shouldIncludeHeader && finalHeaders.length) {
|
||||
rowsOut.push(finalHeaders.map(h => escapeCell(h)).join(delimiter));
|
||||
}
|
||||
|
||||
// Parse every row
|
||||
for (const obj of objs) {
|
||||
|
||||
// Check if obj is null or somthing else we can't convert
|
||||
if (!obj || typeof obj !== "object") {
|
||||
|
||||
// produce empty row with same number of columns
|
||||
rowsOut.push(finalHeaders.map(() => escapeCell(null)).join(delimiter));
|
||||
continue;
|
||||
}
|
||||
|
||||
// For every header check if a value on our row exist and print it
|
||||
const row = finalHeaders.map(col => escapeCell(col in obj ? (obj as any)[col] : null)).join(delimiter);
|
||||
rowsOut.push(row);
|
||||
}
|
||||
|
||||
return (includeBOM ? "\uFEFF" : "") + rowsOut.join(eol);
|
||||
}
|
||||
53
web/src/lib/exporting.ts
Normal file
53
web/src/lib/exporting.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { generateCSVString } from "./csv";
|
||||
import { dayToDate, fetchDay, fetchDays, type AttendanceDay, type Day } from "./Day";
|
||||
import type { IDMap } from "./IDMapping";
|
||||
|
||||
interface CSVRow {
|
||||
ID: string
|
||||
Vorname: string
|
||||
Nachname: string
|
||||
[key: string]: string | boolean
|
||||
}
|
||||
|
||||
function prepareRows(mapping: IDMap, days: AttendanceDay[]): CSVRow[] {
|
||||
let csvData: CSVRow[] = [];
|
||||
|
||||
const allIDs = Object.keys(mapping);
|
||||
|
||||
for (const id of allIDs) {
|
||||
const name = mapping[id];
|
||||
const row: CSVRow = {
|
||||
ID: id,
|
||||
Vorname: name.first,
|
||||
Nachname: name.last,
|
||||
};
|
||||
|
||||
for (const day of days) {
|
||||
const dayKey = dayToDate(day.date).toLocaleDateString();
|
||||
row[dayKey] = day.ids.includes(id);
|
||||
}
|
||||
|
||||
csvData.push(row);
|
||||
}
|
||||
|
||||
return csvData;
|
||||
}
|
||||
|
||||
async function getDays(from: Date, to: Date): Promise<AttendanceDay[]> {
|
||||
const recordedDays: Day[] = await fetchDays(from, to);
|
||||
let days: AttendanceDay[] = [];
|
||||
|
||||
for (const day of recordedDays) {
|
||||
days.push(await fetchDay(day))
|
||||
}
|
||||
|
||||
return days;
|
||||
}
|
||||
|
||||
export async function generateCSVFile(from: Date, to: Date, mapping: IDMap): Promise<string> {
|
||||
const days = await getDays(from, to);
|
||||
const rows = prepareRows(mapping, days);
|
||||
const csvString = generateCSVString(rows);
|
||||
|
||||
return csvString;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user