Öffnungszeiten: Team-Panel, Ersatz-HTML und Rechtliches-Karte

Panel unter der Zeile Öffnungszeiten live (Header-Selektor, Kontakt-/Hinweis-Leiste aus, Speichern mit teamVersion,
Ersatz-HTML kopieren je Plan), Meldung zum Ersatz-Ticket nach dem Abschalten, Hindernis an aktionen, Statustext der Zeiten,
Team-API (GET ersatz, PATCH team), Karte Leistungsbeschreibung Öffnungszeiten live in setup-legal (Schlüssel höchstens 36 Zeichen).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 07:12:04 +00:00
1 parent 994347408d
commit 3d67aa88cf
10 files changed
+498 -6

No files matched your search

+3
View File
@@ -60,6 +60,9 @@ export function createFinanceRequest({ baseUrl, createJWT, fetchImpl = (...args)
// Nach dem Festschreiben (502) liegt die Rechnung schon als "offen" vor
if (info.invoiceId) err.invoiceId = info.invoiceId
if (info.invoiceNumber) err.invoiceNumber = info.invoiceNumber
// Team-Einstellungen der Oeffnungszeiten (PATCH): code (409 ZEITEN_VERALTET) und fehler je Feld (400)
if (typeof info.code === 'string') err.code = info.code
if (info.fehler && typeof info.fehler === 'object') err.fehler = info.fehler
throw err
}
return data ?? {}
+8
View File
@@ -2,6 +2,7 @@ import { account } from './appwrite'
import { createFinanceRequest } from './financeRequest'
import { createJwtCache } from './serverpulsAuth'
import { anleitungPfad, featureBody } from './funktionenView'
import { ersatzPfad, zeitenTeamPfad } from './zeitenView'
/**
* Client fuer den Team-Eingang des Kundenportals (/api/team): alle Mitarbeiter, Appwrite-JWT (Bearer).
@@ -103,6 +104,13 @@ export const setNewsFeature = (customerId, body) => setFeature(customerId, 'news
/** Einbauanleitung des Terminplaners: { stand, warnungen, abschnitte, text, state, ticketWoid, ticketVersion, hindernis } (Aufbereitung: panelModell). */
export const getTermineAnleitung = (customerId, projectId) => request(anleitungPfad(customerId, projectId))
// --- Oeffnungszeiten live (Funktion zeiten): Ersatz-HTML und Team-Einstellungen ---
/** Ersatz-HTML (Wochentabelle) eines Plans fuer den Einbau: { html }; 404 unbekannter Plan, 409 "Der Kunde hat noch keine Zeiten eingetragen." */
export const getZeitenErsatz = (customerId, projectId, plan) => request(ersatzPfad(customerId, projectId, plan))
/** Team-Einstellungen (Rumpf: teamBody in zeitenView): { team, teamVersion }; 400 ungueltig, 409 veraltet (err.code ZEITEN_VERALTET). PATCH, weil das Ticketsystem kein PUT durchlaesst. */
export const patchZeitenTeam = (customerId, body) => request(zeitenTeamPfad(customerId), { method: 'PATCH', body })
// --- Abo-Status (alle Mitarbeiter) ---
/** { productKey, active, status, storageUnits, startedAt, serviceStart, currentPeriodEnd, cancelAt } */
+109
View File
@@ -75,3 +75,112 @@ export function teamBody({ projectId, teamVersion, navSelektor, kontaktleisteAus
hinweisleisteAus: hinweisleisteAus === true,
}
}
// --- Panel unter der Zeile "Oeffnungszeiten live": Pfade der Team-Routen, Formular, Speichern, Ersatz-HTML, Meldung nach dem Abschalten ---
const enc = encodeURIComponent
/** Pfad (relativ zu /api/team) von GET .../zeiten/ersatz; ohne plan faellt der Parameter weg (Hauptplan), ein leerer plan bleibt stehen (Portal: 404). */
export const ersatzPfad = (customerId, projectId, plan) =>
`/funktionen/${enc(customerId)}/zeiten/ersatz?projectId=${enc(projectId)}${plan === undefined || plan === null ? '' : `&plan=${enc(plan)}`}`
/** Pfad (relativ zu /api/team) von PATCH .../zeiten/team. */
export const zeitenTeamPfad = (customerId) => `/funktionen/${enc(customerId)}/zeiten/team`
/** Startwerte des Team-Formulars aus feature.team: Selektor als Text (null => leeres Feld), Schalter nur bei true an. */
export function teamFormular(feature) {
const team = feature?.team
return {
navSelektor: typeof team?.navSelektor === 'string' ? team.navSelektor : '',
kontaktleisteAus: team?.kontaktleisteAus === true,
hinweisleisteAus: team?.hinweisleisteAus === true,
}
}
/** true, wenn das Formular vom gespeicherten Stand (feature.team) abweicht; Leerraum um den Selektor zaehlt nicht. */
export function teamGeaendert(form, feature) {
const start = teamFormular(feature)
return (
String(form?.navSelektor ?? '').trim() !== start.navSelektor.trim() ||
(form?.kontaktleisteAus === true) !== start.kontaktleisteAus ||
(form?.hinweisleisteAus === true) !== start.hinweisleisteAus
)
}
/**
* Antwort des Team-PATCH ({ team, teamVersion }) in die Projektliste einarbeiten: nur team und teamVersion von features.zeiten dieses
* Projekts; alles andere bleibt. Eine unbrauchbare Antwort oder ein Projekt ohne Zeiten aendert nichts (gleiche Liste zurueck).
*/
export function mergeTeam(projects, projectId, antwort) {
const { team, teamVersion } = antwort || {}
if (!team || typeof team !== 'object' || !Number.isInteger(teamVersion)) return projects
return projects.map((p) => (p.projectId === projectId && p.features?.zeiten ? { ...p, features: { ...p.features, zeiten: { ...p.features.zeiten, team, teamVersion } } } : p))
}
const VERALTET_TEXT = 'Inzwischen geändert – bitte neu laden.'
/**
* Fehler des Team-PATCH -> { text, neuLaden }. 409 ZEITEN_VERALTET: "Inzwischen geändert – bitte neu laden." (die Ansicht ist veraltet,
* neuLaden); jeder andere 409 (z. B. Funktion nicht eingeschaltet) zeigt den Text des Portals und gilt ebenfalls als veraltet. Alles andere
* (400, Netz) zeigt seinen Text unveraendert.
*/
export function teamSpeichernFehler(err) {
if (err?.status === 409) return { text: err.code === 'ZEITEN_VERALTET' ? VERALTET_TEXT : err.message || VERALTET_TEXT, neuLaden: true }
return { text: err?.message || 'Speichern fehlgeschlagen.', neuLaden: false }
}
/** Text des Portals (409 von GET .../zeiten/ersatz), auch als Hinweis statt Knopf, solange feature.plaene leer ist. */
export const KEINE_ZEITEN_TEXT = 'Der Kunde hat noch keine Zeiten eingetragen.'
/**
* Knoepfe "Ersatz-HTML kopieren" je Plan aus feature.plaene (Portal: [{ kurz, name, haupt }], Hauptplan zuerst): { kurz, label, bezeichnung }.
* Ein Plan: schlicht "Ersatz-HTML kopieren"; mehrere: mit "Name (kurz)" dahinter. Plaene ohne Kurznamen werden uebergangen.
*/
export function ersatzKnoepfe(feature) {
const plaene = (Array.isArray(feature?.plaene) ? feature.plaene : []).filter((p) => p && typeof p.kurz === 'string' && p.kurz !== '')
return plaene.map((p) => {
const bezeichnung = typeof p.name === 'string' && p.name !== '' && p.name !== p.kurz ? `${p.name} (${p.kurz})` : p.kurz
return { kurz: p.kurz, label: plaene.length > 1 ? `Ersatz-HTML kopieren: ${bezeichnung}` : 'Ersatz-HTML kopieren', bezeichnung }
})
}
/**
* Schreibt das Ersatz-HTML in die Zwischenablage (clipboard = navigator.clipboard). 'kopiert'; 'manuell', wenn es die API nicht gibt oder der
* Browser das Schreiben ablehnt (dann zeigt das Panel das HTML zum Markieren); 'leer' ohne Text (nie eine leere Zwischenablage melden).
*/
export async function ersatzKopieren(html, clipboard) {
if (typeof html !== 'string' || html === '') return 'leer'
if (!clipboard || typeof clipboard.writeText !== 'function') return 'manuell'
try {
await clipboard.writeText(html)
return 'kopiert'
} catch {
return 'manuell'
}
}
export const ZWISCHENABLAGE_FEHLT = 'Die Zwischenablage ist in diesem Browser nicht verfügbar. Bitte das HTML unten markieren und von Hand kopieren.'
/** Sichtbare Rueckmeldung zum Kopieren: { ok, text }; bezeichnung (Plan) wird bei mehreren Plaenen genannt. */
export function ersatzMeldung(ergebnis, bezeichnung = '') {
if (ergebnis === 'kopiert') return { ok: true, text: bezeichnung ? `Ersatz-HTML kopiert: ${bezeichnung}.` : 'Ersatz-HTML kopiert.' }
if (ergebnis === 'manuell') return { ok: false, text: ZWISCHENABLAGE_FEHLT }
return { ok: false, text: 'Es gibt kein Ersatz-HTML zum Kopieren.' }
}
/**
* Meldung nach dem Abschalten von Oeffnungszeiten live aus ersatzTicket der Portal-Antwort: 'angelegt' (Ticket "Ersatz-Tabelle aktualisieren"),
* 'aktuell' (nichts zu tun), 'fehler' (Ticket nicht angelegt: Warnung, denn ein zweites Abschalten gibt es nicht, das Team legt es von Hand an).
* Ohne oder mit unbekanntem Wert schlicht "Gespeichert.".
*/
export function abschaltMeldung(ersatzTicket) {
if (ersatzTicket === 'angelegt') return { ok: true, text: 'Gespeichert. Ticket „Ersatz-Tabelle aktualisieren“ angelegt.' }
if (ersatzTicket === 'aktuell') return { ok: true, text: 'Gespeichert. Die Ersatz-Tabelle ist aktuell, kein Ticket nötig.' }
if (ersatzTicket === 'fehler') {
return {
ok: false,
text: 'Gespeichert, aber das Ticket „Ersatz-Tabelle aktualisieren“ konnte nicht angelegt werden. Bitte von Hand anlegen: Die feste Ersatz-Tabelle der Website muss zum Wochenplan des Kunden passen (Ersatz-HTML dazu im Panel unter der Zeile kopieren).',
}
}
return { ok: true, text: 'Gespeichert.' }
}