Files
spritzschema-next/app/components/SpritzClient.tsx
2026-03-16 09:20:30 +01:00

236 lines
6.6 KiB
TypeScript

'use client';
import { useEffect, useState, useCallback, useRef } from 'react';
import { DateTime } from 'luxon';
interface DataItem {
day: string;
status: boolean;
einheit: number;
}
interface Schema {
curdate: string;
months: string[];
years: string[];
data: DataItem[];
einheit: number;
}
export interface SysParams {
testing: boolean;
doinit: boolean;
einheit: number;
version: string;
date: string;
}
interface Props {
sysParams: SysParams;
}
function CellContent({ day, einheit }: { day: DateTime; einheit: number }) {
return (
<div className="inner">
{day.toFormat('d')}
<div className="lowline small">
<div className="wtg">{day.setLocale('de').toFormat('ccc')}</div>
<div className="eh">{einheit !== 0 ? einheit : ''}</div>
</div>
</div>
);
}
function buildMonthsLabel(s: Schema): string {
let months = s.months.join(' - ');
months += ' ';
months += s.years.join('/');
return months;
}
export default function SpritzClient({ sysParams }: Props) {
const [schema, setSchema] = useState<Schema | null>(null);
const [curEinheit, setCurEinheit] = useState<number>(sysParams.einheit);
const [monthsLabel, setMonthsLabel] = useState<string>('');
const [einheitInput, setEinheitInput] = useState<number>(sysParams.einheit);
const schemaRef = useRef<Schema | null>(null);
const curEinheitRef = useRef<number>(sysParams.einheit);
schemaRef.current = schema;
curEinheitRef.current = curEinheit;
const apiUrl = sysParams.testing ? '/api/data?test=true' : '/api/data';
async function fetchData(): Promise<{ data: Schema | null; err: string | null }> {
const res = await fetch(apiUrl);
return res.json();
}
async function storeData(data: Schema): Promise<unknown> {
const response = await fetch(apiUrl, {
method: 'POST',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' },
});
return response.json();
}
async function initSchema(startdate: string): Promise<Schema> {
const setArray: DataItem[] = [];
const monthArray: string[] = [];
const yearsArray: string[] = [];
const ld0 = DateTime.fromISO(startdate);
let k = 0;
for (let i = 0; i < 35; i++) {
const elem: DataItem = { status: false, einheit: 0, day: '' };
if (i === 17) {
elem.day = '';
} else {
const ld = ld0.plus({ day: k });
elem.day = ld.toFormat('y-LL-dd');
const month = ld.setLocale('de').toFormat('LLLL');
const year = ld.toFormat('y');
if (!monthArray.includes(month)) monthArray.push(month);
if (!yearsArray.includes(year)) yearsArray.push(year);
k++;
}
setArray.push(elem);
}
const newSchema: Schema = {
curdate: startdate,
months: monthArray,
years: yearsArray,
data: setArray,
einheit: curEinheitRef.current,
};
await storeData(newSchema);
return newSchema;
}
function applySchema(s: Schema) {
// Update einheit input to last non-zero einheit found in data
let lastEinheit = s.einheit;
for (const item of s.data) {
if (item.einheit !== 0) lastEinheit = item.einheit;
}
if (lastEinheit !== 0 && curEinheitRef.current === 0) {
setCurEinheit(lastEinheit);
curEinheitRef.current = lastEinheit;
setEinheitInput(lastEinheit);
}
setMonthsLabel(buildMonthsLabel(s));
setSchema(s);
}
useEffect(() => {
async function init() {
let s: Schema | null = null;
if (sysParams.doinit) {
s = await initSchema('2023-05-01');
}
if (!s) {
const ret = await fetchData();
s = ret.data;
}
if (!s) {
// DB leer → neues Schema mit aktuellem Datum anlegen
const today = DateTime.now().toFormat('y-LL-dd');
s = await initSchema(today);
}
if (s) {
applySchema(s);
}
}
init();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleClick = useCallback(async (e: React.MouseEvent<HTMLDivElement>) => {
const button = (e.target as HTMLElement).closest('button') as HTMLButtonElement | null;
if (!button || button.disabled) return;
const currentSchema = schemaRef.current;
if (!currentSchema) return;
const idNum = parseInt(button.id.slice(2)) - 1; // 0-basierter Index
const lastDay = currentSchema.data[34].day;
const newSchema: Schema = {
...currentSchema,
data: currentSchema.data.map((item, idx) => {
if (idx === idNum) {
return { ...item, status: !item.status, einheit: curEinheitRef.current };
}
return item;
}),
};
await storeData(newSchema);
applySchema(newSchema);
// Letzten Button (bt35) geklickt → nächste Periode anlegen
if (button.id === 'bt35') {
const nextDate = DateTime.fromISO(lastDay).plus({ day: 1 }).toFormat('y-LL-dd');
const nextSchema = await initSchema(nextDate);
applySchema(nextSchema);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleEinheitChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const val = parseInt(e.target.value) || 0;
setCurEinheit(val);
curEinheitRef.current = val;
setEinheitInput(val);
if (schemaRef.current) {
schemaRef.current = { ...schemaRef.current, einheit: val };
}
}, []);
if (!schema) {
return <div style={{ padding: '30px' }}>Lade</div>;
}
return (
<div className="spritztab">
<h1>Spritz-Tabelle</h1>
<h2 id="curmon">{monthsLabel}</h2>
<div id="sptab" onClick={handleClick}>
{schema.data.map((item, idx) => {
const btId = `bt${idx + 1}`;
const day = item.day ? DateTime.fromISO(item.day) : null;
const isDisabled = item.day === '';
const ariaLabel = isDisabled ? 'o' : item.status ? 'x' : '';
const displayEinheit = item.einheit !== 0 ? item.einheit : (item.status ? schema.einheit : 0);
return (
<button
key={btId}
id={btId}
disabled={isDisabled}
aria-label={ariaLabel}
>
{day && <CellContent day={day} einheit={displayEinheit} />}
</button>
);
})}
</div>
<div id="infeld">
<label htmlFor="einheiten">Einheiten:</label>
<input
id="einheiten"
type="number"
min={0}
max={100}
value={einheitInput}
onChange={handleEinheitChange}
/>
</div>
<footer>
<div id="v">
Version {sysParams.version} vom {sysParams.date}
</div>
</footer>
</div>
);
}