mirror of
https://github.com/Djeeberjr/fw-anwesenheit.git
synced 2026-04-30 18:49:09 +00:00
Compare commits
5 Commits
7346b47816
...
1ea70e4993
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ea70e4993 | |||
| 770dca5b0f | |||
| 2e75ba2908 | |||
| 141c1aa9cb | |||
|
|
4abbd844d2 |
@@ -4,6 +4,7 @@ use embassy_executor::Spawner;
|
|||||||
use embassy_net::Stack;
|
use embassy_net::Stack;
|
||||||
use embassy_time::{Duration, Timer};
|
use embassy_time::{Duration, Timer};
|
||||||
use esp_hal::Blocking;
|
use esp_hal::Blocking;
|
||||||
|
use esp_hal::delay::Delay;
|
||||||
use esp_hal::gpio::Input;
|
use esp_hal::gpio::Input;
|
||||||
use esp_hal::i2c::master::Config;
|
use esp_hal::i2c::master::Config;
|
||||||
use esp_hal::peripherals::{
|
use esp_hal::peripherals::{
|
||||||
@@ -12,6 +13,7 @@ use esp_hal::peripherals::{
|
|||||||
};
|
};
|
||||||
use esp_hal::rmt::{ConstChannelAccess, Rmt};
|
use esp_hal::rmt::{ConstChannelAccess, Rmt};
|
||||||
use esp_hal::spi::master::{Config as Spi_config, Spi};
|
use esp_hal::spi::master::{Config as Spi_config, Spi};
|
||||||
|
use esp_hal::system::software_reset;
|
||||||
use esp_hal::time::Rate;
|
use esp_hal::time::Rate;
|
||||||
use esp_hal::timer::timg::TimerGroup;
|
use esp_hal::timer::timg::TimerGroup;
|
||||||
use esp_hal::{
|
use esp_hal::{
|
||||||
@@ -54,9 +56,10 @@ static SD_DET: Mutex<RefCell<Option<Input>>> = Mutex::new(RefCell::new(None));
|
|||||||
|
|
||||||
#[panic_handler]
|
#[panic_handler]
|
||||||
fn panic(info: &core::panic::PanicInfo) -> ! {
|
fn panic(info: &core::panic::PanicInfo) -> ! {
|
||||||
loop {
|
let delay = Delay::new();
|
||||||
error!("PANIC: {info}");
|
error!("PANIC: {info}");
|
||||||
}
|
delay.delay(esp_hal::time::Duration::from_secs(30));
|
||||||
|
software_reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_bootloader_esp_idf::esp_app_desc!();
|
esp_bootloader_esp_idf::esp_app_desc!();
|
||||||
|
|||||||
@@ -3,15 +3,21 @@
|
|||||||
import IDTable from "./lib/IDTable.svelte";
|
import IDTable from "./lib/IDTable.svelte";
|
||||||
import LastId from "./lib/LastID.svelte";
|
import LastId from "./lib/LastID.svelte";
|
||||||
import AddIDModal from "./lib/AddIDModal.svelte";
|
import AddIDModal from "./lib/AddIDModal.svelte";
|
||||||
|
import ExportModal from "./lib/ExportModal.svelte";
|
||||||
|
import { generateCSVFile } from "./lib/exporting";
|
||||||
|
import { fetchMapping, type IDMap } from "./lib/IDMapping";
|
||||||
|
import { downloadBlob } from "./lib/downloadBlob";
|
||||||
|
|
||||||
let lastID: string = $state("");
|
let lastID: string = $state("");
|
||||||
|
let mapping: IDMap | null = $state(null);
|
||||||
|
|
||||||
let addModal: AddIDModal;
|
let addModal: AddIDModal;
|
||||||
let idTable: IDTable;
|
let exportModal: ExportModal;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
mapping = await fetchMapping();
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
let sse = new EventSource("/api/idevent");
|
let sse = new EventSource("/api/idevent");
|
||||||
|
|
||||||
sse.onmessage = (e) => {
|
sse.onmessage = (e) => {
|
||||||
lastID = e.data;
|
lastID = e.data;
|
||||||
};
|
};
|
||||||
@@ -25,13 +31,14 @@
|
|||||||
<h1 class="text-3xl sm:text-4xl font-bold text-gray-800">Anwesenheit</h1>
|
<h1 class="text-3xl sm:text-4xl font-bold text-gray-800">Anwesenheit</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a
|
<button
|
||||||
class="px-6 py-3 text-lg font-semibold text-white bg-indigo-600 rounded-2xl shadow-md hover:bg-indigo-700 transition"
|
class="px-6 py-3 text-lg font-semibold text-white bg-indigo-600 rounded-2xl shadow-md hover:bg-indigo-700 transition"
|
||||||
href="/api/csv"
|
onclick={() => {
|
||||||
download="anwesenheit.csv"
|
exportModal.open();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Download CSV
|
Export CSV
|
||||||
</a>
|
</button>
|
||||||
|
|
||||||
<div class="pt-3 pb-2">
|
<div class="pt-3 pb-2">
|
||||||
<LastId
|
<LastId
|
||||||
@@ -42,15 +49,32 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<IDTable bind:this={idTable} onEdit={(id,firstName,lastName)=>{
|
{#if mapping}
|
||||||
|
<IDTable
|
||||||
|
data={mapping}
|
||||||
|
onEdit={(id, firstName, lastName) => {
|
||||||
addModal.open(id, firstName, lastName);
|
addModal.open(id, firstName, lastName);
|
||||||
}}/>
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AddIDModal
|
<AddIDModal
|
||||||
bind:this={addModal}
|
bind:this={addModal}
|
||||||
onSubmitted={() => {
|
onSubmitted={async () => {
|
||||||
idTable.reloadData();
|
mapping = await fetchMapping();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ExportModal
|
||||||
|
bind:this={exportModal}
|
||||||
|
onSubmitted={async (from, to) => {
|
||||||
|
if (!mapping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let csvFile = await generateCSVFile(from, to, mapping);
|
||||||
|
|
||||||
|
downloadBlob("export.csv",csvFile,"text/csv");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { type IDMap } from "./IDMapping";
|
||||||
import { fetchMapping, type IDMap } from "./IDMapping";
|
|
||||||
let data: IDMap | undefined = $state();
|
|
||||||
|
|
||||||
let {
|
let {
|
||||||
onEdit,
|
onEdit,
|
||||||
}: { onEdit?: (id: string, firstName: string, lastName: string) => void } =
|
data,
|
||||||
$props();
|
}: {
|
||||||
|
onEdit?: (id: string, firstName: string, lastName: string) => void;
|
||||||
export async function reloadData() {
|
data: IDMap;
|
||||||
data = await fetchMapping();
|
} = $props();
|
||||||
}
|
|
||||||
|
|
||||||
let rows = $derived(
|
let rows = $derived(
|
||||||
data
|
data
|
||||||
@@ -44,15 +41,8 @@
|
|||||||
if (sortKey !== key) return "";
|
if (sortKey !== key) return "";
|
||||||
return sortDirection === "asc" ? "▲" : "▼";
|
return sortDirection === "asc" ? "▲" : "▼";
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
await reloadData();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if data == null}
|
|
||||||
Loading...
|
|
||||||
{:else}
|
|
||||||
<div class="bg-indigo-500 py-2 rounded-2xl overflow-x-auto">
|
<div class="bg-indigo-500 py-2 rounded-2xl overflow-x-auto">
|
||||||
<table class="px-10">
|
<table class="px-10">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -106,7 +96,6 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
@reference "../app.css";
|
@reference "../app.css";
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
interface CSVOptions {
|
interface CSVOptions {
|
||||||
delimiter?: string; // default: ","
|
delimiter?: string;
|
||||||
includeHeader?: boolean; // default: true for object input, false for array-of-arrays unless headers provided
|
headerOrder?: string[];
|
||||||
headerOrder?: string[]; // explicit ordering for columns that should come first
|
eol?: string;
|
||||||
eol?: string; // default: "\r\n"
|
includeBOM?: boolean;
|
||||||
includeBOM?: boolean; // default: false (useful for Excel)
|
nullString?: string;
|
||||||
nullString?: string; // default: "" (how to render null/undefined)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RowObject = Record<string, any>;
|
type RowObject = Record<string, any>;
|
||||||
@@ -13,7 +12,6 @@ type InputRows = RowObject[];
|
|||||||
export function generateCSVString(input: InputRows, opts: CSVOptions = {}): string {
|
export function generateCSVString(input: InputRows, opts: CSVOptions = {}): string {
|
||||||
const {
|
const {
|
||||||
delimiter = ",",
|
delimiter = ",",
|
||||||
includeHeader,
|
|
||||||
headerOrder,
|
headerOrder,
|
||||||
eol = "\r\n",
|
eol = "\r\n",
|
||||||
includeBOM = false,
|
includeBOM = false,
|
||||||
@@ -29,7 +27,7 @@ export function generateCSVString(input: InputRows, opts: CSVOptions = {}): stri
|
|||||||
const escapeCell = (raw: any): string => {
|
const escapeCell = (raw: any): string => {
|
||||||
if (raw === null || raw === undefined) return nullString;
|
if (raw === null || raw === undefined) return nullString;
|
||||||
|
|
||||||
let s = defaultStringify(raw);
|
let s = stringify(raw);
|
||||||
|
|
||||||
// Replace quotes
|
// Replace quotes
|
||||||
if (s.includes('"')) s = s.replace(/"/g, '""');
|
if (s.includes('"')) s = s.replace(/"/g, '""');
|
||||||
@@ -38,7 +36,7 @@ export function generateCSVString(input: InputRows, opts: CSVOptions = {}): stri
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Transform the value of a cell into a string
|
// Transform the value of a cell into a string
|
||||||
function defaultStringify(v: any): string {
|
function stringify(v: any): string {
|
||||||
if (v === null || v === undefined) return nullString;
|
if (v === null || v === undefined) return nullString;
|
||||||
if (v instanceof Date) return v.toLocaleDateString();
|
if (v instanceof Date) return v.toLocaleDateString();
|
||||||
if (typeof v === "boolean") return v ? "X" : "";
|
if (typeof v === "boolean") return v ? "X" : "";
|
||||||
@@ -79,10 +77,9 @@ export function generateCSVString(input: InputRows, opts: CSVOptions = {}): stri
|
|||||||
finalHeaders = first.concat(rest);
|
finalHeaders = first.concat(rest);
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldIncludeHeader = typeof includeHeader === "boolean" ? includeHeader : finalHeaders.length > 0;
|
|
||||||
const rowsOut: string[] = [];
|
const rowsOut: string[] = [];
|
||||||
|
|
||||||
if (shouldIncludeHeader && finalHeaders.length) {
|
if (finalHeaders.length > 0) {
|
||||||
rowsOut.push(finalHeaders.map(h => escapeCell(h)).join(delimiter));
|
rowsOut.push(finalHeaders.map(h => escapeCell(h)).join(delimiter));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
web/src/lib/downloadBlob.ts
Normal file
8
web/src/lib/downloadBlob.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export function downloadBlob(filename: string, content: string, mimeType = "text/plain") {
|
||||||
|
const blob = new Blob([content], { type: mimeType });
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = filename;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(a.href);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user