From 78ccea8bac059c0d8b12064273a94c46a0f8412e Mon Sep 17 00:00:00 2001 From: Ida Date: Thu, 4 Jun 2026 14:28:52 +0200 Subject: [PATCH] mandate information panel in expanded tenant --- src/components/admin/InlineEditableField.tsx | 163 +++++++++ .../admin/MandateExpandDashboard.module.css | 30 ++ .../admin/MandateExpandDashboard.tsx | 52 +++ .../admin/MandateInfoPanel.module.css | 194 +++++++++++ src/components/admin/MandateInfoPanel.tsx | 329 ++++++++++++++++++ .../admin/MandateUsersPanel.module.css | 13 + src/components/admin/MandateUsersPanel.tsx | 11 +- src/pages/admin/AdminMandatesPage.tsx | 149 +------- src/utils/mandateBillingFormMerge.ts | 43 ++- 9 files changed, 850 insertions(+), 134 deletions(-) create mode 100644 src/components/admin/InlineEditableField.tsx create mode 100644 src/components/admin/MandateExpandDashboard.module.css create mode 100644 src/components/admin/MandateExpandDashboard.tsx create mode 100644 src/components/admin/MandateInfoPanel.module.css create mode 100644 src/components/admin/MandateInfoPanel.tsx diff --git a/src/components/admin/InlineEditableField.tsx b/src/components/admin/InlineEditableField.tsx new file mode 100644 index 0000000..2aa2a5c --- /dev/null +++ b/src/components/admin/InlineEditableField.tsx @@ -0,0 +1,163 @@ +import React, { useState, useRef, useEffect } from 'react'; +import styles from './MandateInfoPanel.module.css'; + +export type InlineFieldType = 'text' | 'number' | 'boolean' | 'textarea' | 'email'; + +export interface InlineEditableFieldProps { + label: string; + value: unknown; + fieldKey: string; + type?: InlineFieldType; + editable?: boolean; + saving?: boolean; + onSave: (fieldKey: string, value: unknown) => Promise; +} + +function displayValue(value: unknown, type: InlineFieldType, t: (k: string) => string): string { + if (type === 'boolean') { + if (value === true) return t('Ja'); + if (value === false) return t('Nein'); + return '—'; + } + if (value === null || value === undefined || value === '') return '—'; + return String(value); +} + +export const InlineEditableField: React.FC< + InlineEditableFieldProps & { t: (key: string) => string } +> = ({ + label, + value, + fieldKey, + type = 'text', + editable = false, + saving = false, + onSave, + t, +}) => { + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(''); + const inputRef = useRef(null); + + useEffect(() => { + if (editing && inputRef.current) { + inputRef.current.focus(); + if (type !== 'boolean' && inputRef.current instanceof HTMLInputElement) { + inputRef.current.select(); + } + } + }, [editing, type]); + + const startEdit = () => { + if (!editable || saving || type === 'boolean') return; + setDraft(value === null || value === undefined ? '' : String(value)); + setEditing(true); + }; + + const commit = async () => { + if (!editing) return; + setEditing(false); + let next: unknown = draft; + if (type === 'number') { + const n = Number(draft); + if (Number.isNaN(n)) return; + next = n; + } + if (String(value ?? '') === String(next ?? '')) return; + await onSave(fieldKey, next); + }; + + const cancel = () => { + setEditing(false); + setDraft(''); + }; + + const handleBooleanToggle = async () => { + if (!editable || saving) return; + const next = value !== true; + await onSave(fieldKey, next); + }; + + const shown = displayValue(value, type, t); + const muted = shown === '—'; + + if (type === 'boolean') { + return ( + <> +
{label}
+
+ +
+ + ); + } + + return ( + <> +
{label}
+
+ {editing ? ( + type === 'textarea' ? ( +