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