'use client'; import { CUSTOM_MODULE_CATEGORIES, CUSTOM_MODULE_CATEGORY } from '@tessera/shared'; import { useTranslations } from 'next-intl'; import { useState } from 'react'; import { type CustomModule, CustomModuleRequestError, checkCustomModuleUrl, createCustomModule, updateCustomModule, } from '@/lib/custom-modules-api'; import { useCustomModuleCacheStore } from '@/lib/stores/custom-module-cache-store'; import { useCategoryLabel } from '@/lib/use-category-label'; interface CustomModuleFormModalProps { /** `null` = neues Modul anlegen, sonst bearbeiten. */ mod: CustomModule | null; /** * Beim Anlegen: `true` = gemeinsamer Eintrag fuer alle Benutzer (Verwaltung), * sonst persoenlicher Eintrag (Einstellungen). Beim Bearbeiten ohne Wirkung — * ob ein Eintrag gemeinsam ist, aendert sich nie. */ shared?: boolean; onClose: () => void; onSaved: () => void; } const NAME_MAX = 100; const URL_MAX = 2048; /** * Fehlertext fuer Speichern und Loeschen: 403 = keine Berechtigung, 400 = die * API hat die Angaben abgelehnt (mit ihrer Meldung), alles andere (500, * Netzwerk) = der allgemeine „bitte erneut versuchen“-Text `fallbackKey`. */ export function customModuleErrorText( err: unknown, t: (key: string, values?: Record) => string, fallbackKey: 'saveError' | 'deleteError', ): string { if (err instanceof CustomModuleRequestError) { if (err.status === 403) return t('forbidden'); if (err.status === 400) return t('invalidInput'); } return t(fallbackKey); } /** * Formular „Eigenes Modul“ (quick-260929-9wc, Vorbild `GroupFormModal`; * quick-260929-dzu: ein Formular, zwei Aufrufer — Verwaltung legt gemeinsame, * Einstellungen persoenliche Eintraege an). * Die Adresse wird VOR dem Senden geprueft (`checkCustomModuleUrl`, dieselbe * Regel wie die API): nur https, keine Zugangsdaten. `noValidate`, damit die * eigenen, uebersetzten Meldungen erscheinen statt der des Browsers. */ export function CustomModuleFormModal({ mod, shared = false, onClose, onSaved, }: CustomModuleFormModalProps) { const t = useTranslations('customModules.form'); const tCommon = useTranslations('common'); const categoryLabel = useCategoryLabel(); const [name, setName] = useState(mod?.name ?? ''); const [url, setUrl] = useState(mod?.url ?? ''); // Neue Eintraege landen vorbelegt in der Gruppe „Eigene Module“; beim // Bearbeiten bleibt die gespeicherte Kategorie. const [category, setCategory] = useState(mod?.category ?? CUSTOM_MODULE_CATEGORY); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); const trimmedName = name.trim(); if (trimmedName === '') { setError(t('nameRequired')); return; } const trimmedUrl = url.trim(); const check = checkCustomModuleUrl(trimmedUrl); if (check === 'notHttps') { setError(t('urlNotHttps')); return; } if (check === 'credentials') { setError(t('urlCredentials')); return; } setSaving(true); try { const input = { name: trimmedName, url: trimmedUrl, category }; if (mod) { await updateCustomModule(mod.id, input); // Sitzungsspeicher der offen gehaltenen Module nachziehen — nur wenn // das Modul dort schon liegt; eine neue Adresse laedt dessen