236 lines
6.6 KiB
TypeScript
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>
|
|
);
|
|
}
|