Files
Webklar/src/components/home/demos/zeiten/ablauf.ts
T
rootandClaude Opus 5.5 8f5d522682 Relaunch: Portal-Bild für Öffnungszeiten live
Echte Aufnahme des Portal-Editors (Handy 402x874, Hinweis-Leiste mit
Betriebsurlaub, Knopf "Hinweis-Leiste anlegen") ersetzt den Platzhalter
(Kopie von phone-9-termine). Tippunkt aus der gemessenen Knopfmitte
(135 / 488) in HINWEIS_PUNKT und HINWEIS_ANLEGEN_PUNKT; Test pinnt, dass
beide gleich sind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:17:48 +00:00

192 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useMemo, useState } from "react";
import { useAblauf } from "../gemeinsam";
/**
* Ablauf der Station „Öffnungszeiten live“ (Bäckerei Sonnenschein), Spezifikation 10.
* Reine Logik, kein CSS und keine Adresse: die Komponenten werden später per React.lazy nachgeladen.
*
* Geschichte (Demo-Datum: Montag 5. Oktober 2026, die Uhr zählt Minuten ab Mo 00:00):
* start (kurz Ruhe bei 05:50, „Geschlossen · öffnet heute um 6 Uhr“)
* -> lauf (die Uhr läuft in Zehn-Minuten-Schritten bis 18:10; um 06:00 springt der Status auf „Jetzt geöffnet“,
* um 18:00 auf „Geschlossen · öffnet morgen um 6 Uhr“)
* -> tippen (auf dem Handy gleitet der Finger zu „Hinweis-Leiste anlegen“ und tippt)
* -> leiste (die Hinweis-Leiste „Betriebsurlaub 12.–26. Oktober“ gleitet auf der Website ein, bleibt stehen) -> von vorn.
*
* Bedienen: Wer den Uhr-Regler anfasst (`setzeUhr(minute)`), stoppt den Ablauf. Die Demo zeigt dann den ganzen Stand
* (mit Leiste) bei der gewählten Uhrzeit, ohne Timer. `setzeUhr(null)` gibt an den Ablauf zurück (von vorn).
*/
export type ZeitenPhase = "start" | "lauf" | "tippen" | "leiste";
export type ZeitenStatus = {
offen: boolean;
/** Langtext der Status-Pille, z. B. „Jetzt geöffnet · schließt um 18 Uhr“ (nie „24“). */
text: string;
/** Kurztext: „Geöffnet“ oder „Geschlossen“. */
kurz: string;
};
export type ZeitenZustand = {
phase: ZeitenPhase;
/** Minuten ab Montag, 5. Oktober 2026, 00:00. */
uhr: number;
status: ZeitenStatus;
/** Ob die Hinweis-Leiste auf der Website steht (Phase „leiste“, im Endzustand und beim Bedienen). */
leiste: boolean;
/**
* Zähler der Tipps für DemoZeiger: 0 solange der Finger nur gleitet oder nicht zu sehen ist, ab dem Tipp eine je
* Durchlauf wachsende Zahl (so erscheint die Klickwelle in jeder Runde neu). Der Finger ist nur in Phase „tippen“ zu sehen.
*/
klick: number;
/** true = der Regler wurde angefasst: der Ablauf steht, die Uhr zeigt den gewählten Wert. */
bedient: boolean;
/** true = der Automatiklauf spielt gerade (nicht beim Bedienen, nicht ohne Bewegung). */
laeuft: boolean;
/** true = keine Bewegung erlaubt: die Demo zeigt ihren Endzustand (18:10 mit Leiste). */
statisch: boolean;
/** Uhr setzen (Regler bedient) oder mit `null` zurück an den Ablauf geben. Die Funktion bleibt über Renderings gleich. */
setzeUhr: (min: number | null) => void;
};
// ── Konstanten ─────────────────────────────────────────────────────────────────
/** Reglerbereich in Minuten ab Mo 00:00: 05:00 bis 22:00 in Schritten von 10 Minuten. */
export const ZEIT_MIN = 300;
export const ZEIT_MAX = 1320;
export const ZEIT_SCHRITT = 10;
/** Uhrzeit, mit der der Automatiklauf beginnt (05:50) und bei der er endet (18:10). */
export const START_UHR = 350;
export const ENDE_UHR = 1090;
/** Text der Hinweis-Leiste (der Demo-Tag liegt im 7-Tage-Vorlauf des Urlaubs). */
export const HINWEIS_TEXT = "Betriebsurlaub 12.–26. Oktober";
/**
* Mitte des Knopfes „Hinweis-Leiste anlegen“ im Portal-Screenshot phone-12-zeiten (Designpunkte der Handy-Fläche 402 x 874;
* gemessen 135,3 / 487,9, gerundet). Die Bühne (Kundenportal.tsx, HINWEIS_PUNKT) übergibt denselben Wert als Prop `punkt`.
*/
export const HINWEIS_ANLEGEN_PUNKT = { x: 135, y: 488 } as const;
// ── Status: Bäckerei Sonnenschein, Montag 6 bis 18 Uhr ──────────────────────────
const OEFFNET = 6 * 60;
const SCHLIESST = 18 * 60;
const ZU_HEUTE: ZeitenStatus = Object.freeze({ offen: false, text: "Geschlossen · öffnet heute um 6 Uhr", kurz: "Geschlossen" });
const OFFEN: ZeitenStatus = Object.freeze({ offen: true, text: "Jetzt geöffnet · schließt um 18 Uhr", kurz: "Geöffnet" });
const ZU_MORGEN: ZeitenStatus = Object.freeze({ offen: false, text: "Geschlossen · öffnet morgen um 6 Uhr", kurz: "Geschlossen" });
/** Status am Montag (Minute 0 bis 1439 ab Mo 00:00): vor 06:00 zu, bis 17:59 offen, ab 18:00 zu bis morgen früh. */
export const statusFuer = (minute: number): ZeitenStatus =>
minute < OEFFNET ? ZU_HEUTE : minute < SCHLIESST ? OFFEN : ZU_MORGEN;
/** Minuten ab Mitternacht als „05:50“ (die Stunde läuft 00 bis 23, nie „24:00“). */
export const formatUhr = (min: number): string => {
const tag = ((Math.round(min) % 1440) + 1440) % 1440;
return `${String(Math.floor(tag / 60)).padStart(2, "0")}:${String(tag % 60).padStart(2, "0")}`;
};
// ── Schritte ───────────────────────────────────────────────────────────────────
/** Alle Dauern sind Vielfache von 50 ms (die Tests spulen in 50-ms-Schritten vor). */
const START_HALTEN = 1000;
/** Dauer eines Uhr-Schrittes (10 Minuten) im Lauf. */
const TAKT = 100;
/** Wie lange 18:10 („Geschlossen · öffnet morgen um 6 Uhr“) stehen bleibt, bevor der Finger kommt. */
const ENDE_HALTEN = 1500;
/** Der Finger gleitet zum Knopf (Phase „tippen“), dann folgt der Tipp (ebenfalls Phase „tippen“). */
const GLEITEN = 1000;
const TIPPEN = 450;
/** Die Leiste gleitet ein und bleibt stehen. */
const LEISTE_HALTEN = 5000;
type Schritt = { phase: ZeitenPhase; uhr: number; dauer: number; tipp?: boolean };
const SCHRITTE: readonly Schritt[] = [
{ phase: "start", uhr: START_UHR, dauer: START_HALTEN },
...Array.from({ length: (ENDE_UHR - START_UHR) / ZEIT_SCHRITT }, (_, i): Schritt => {
const uhr = START_UHR + (i + 1) * ZEIT_SCHRITT;
return { phase: "lauf", uhr, dauer: uhr === ENDE_UHR ? ENDE_HALTEN : TAKT };
}),
{ phase: "tippen", uhr: ENDE_UHR, dauer: GLEITEN },
{ phase: "tippen", uhr: ENDE_UHR, dauer: TIPPEN, tipp: true },
{ phase: "leiste", uhr: ENDE_UHR, dauer: LEISTE_HALTEN },
];
const DAUERN = SCHRITTE.map((s) => s.dauer);
const TIPP_SCHRITT = SCHRITTE.findIndex((s) => s.tipp);
/** Gesamtdauer eines Durchlaufs in Millisekunden (für Tests und die Feinabstimmung der Bühne). */
export const ZEITEN_RUNDE_MS = DAUERN.reduce((a, b) => a + b, 0);
/** Anfangszustand (Phase „start“, 05:50, ohne Leiste): die Bühne zeigt die Website damit sofort, bevor der Taktgeber geladen ist. */
export const ZEITEN_START: ZeitenZustand = {
phase: "start",
uhr: START_UHR,
status: statusFuer(START_UHR),
leiste: false,
klick: 0,
bedient: false,
laeuft: false,
statisch: false,
setzeUhr: () => {},
};
// ── Hook ───────────────────────────────────────────────────────────────────────
/**
* Taktgeber der Station. `laeuft` = Station aktiv, im Bild, Tab sichtbar, Bewegung erlaubt.
* Läuft sie nicht, steht die Demo bei „start“ (05:50, nichts bewegt sich, kein Timer); eine Bedienung verfällt dann.
* Mit `statisch` (keine Bewegung erlaubt) zeigt sie sofort den Endzustand (18:10 mit Leiste), ohne Timer;
* der Regler bleibt auch dann bedienbar (eine Eingabe ist keine Animation).
*/
export const useZeitenAblauf = (laeuft: boolean, statisch: boolean): ZeitenZustand => {
const [manuell, setManuell] = useState<number | null>(null);
// Eine Bedienung gilt nur, solange die Station läuft (oder statisch steht); sonst verfällt sie
const gewaehlt = laeuft || statisch ? manuell : null;
const spielt = laeuft && !statisch && gewaehlt === null;
const { schritt, runde } = useAblauf(spielt, DAUERN, true);
useEffect(() => {
if (!laeuft && !statisch && manuell !== null) setManuell(null);
}, [laeuft, statisch, manuell]);
const setzeUhr = useCallback((min: number | null) => {
if (min === null) {
setManuell(null);
return;
}
if (!Number.isFinite(min)) return;
setManuell(Math.min(ZEIT_MAX, Math.max(ZEIT_MIN, Math.round(min))));
}, []);
return useMemo<ZeitenZustand>(() => {
if (statisch || gewaehlt !== null) {
// Endzustand bzw. bedient: die Demo steht, die Leiste ist da
const uhr = gewaehlt ?? ENDE_UHR;
return {
phase: "leiste",
uhr,
status: statusFuer(uhr),
leiste: true,
klick: 0,
bedient: gewaehlt !== null,
laeuft: false,
statisch,
setzeUhr,
};
}
const aktuell = SCHRITTE[spielt ? Math.min(schritt, SCHRITTE.length - 1) : 0];
return {
phase: aktuell.phase,
uhr: aktuell.uhr,
status: statusFuer(aktuell.uhr),
leiste: aktuell.phase === "leiste",
klick: spielt && schritt >= TIPP_SCHRITT ? runde + 1 : 0,
bedient: false,
laeuft: spielt,
statisch: false,
setzeUhr,
};
}, [statisch, gewaehlt, spielt, schritt, runde, setzeUhr]);
};