mirror of
				https://github.com/Djeeberjr/fw-anwesenheit.git
				synced 2025-11-03 23:24:10 +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