/** * AdminInvitationsPage * * Admin page for managing invitations within a mandate. * Allows creating, viewing, and revoking invitations. */ import React, { useState, useEffect, useMemo } from 'react'; import { useInvitations, type Invitation, type InvitationCreate } from '../../hooks/useInvitations'; import { useUserMandates, type Mandate, type Role } from '../../hooks/useUserMandates'; import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable'; import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm'; import { FaPlus, FaSync, FaEnvelopeOpenText, FaBuilding, FaCopy, FaLink } from 'react-icons/fa'; import { useToast } from '../../contexts/ToastContext'; import api from '../../api'; import styles from './Admin.module.css'; export const AdminInvitationsPage: React.FC = () => { const { showError } = useToast(); const { invitations, loading, error, pagination, fetchInvitations, createInvitation, revokeInvitation, } = useInvitations(); const { fetchMandates, fetchRoles } = useUserMandates(); // State const [mandates, setMandates] = useState([]); const [selectedMandateId, setSelectedMandateId] = useState(''); const [roles, setRoles] = useState([]); const [showCreateModal, setShowCreateModal] = useState(false); const [showUrlModal, setShowUrlModal] = useState(null); const [showExpired, setShowExpired] = useState(false); const [showUsed, setShowUsed] = useState(false); const [_isSubmitting, setIsSubmitting] = useState(false); // Prefixed with _ to suppress warning const [copySuccess, setCopySuccess] = useState(false); const [backendAttributes, setBackendAttributes] = useState([]); // Load mandates and attributes on mount useEffect(() => { const loadMandates = async () => { const data = await fetchMandates(); setMandates(data); if (data.length > 0 && !selectedMandateId) { setSelectedMandateId(data[0].id); } }; loadMandates(); // Fetch Invitation attributes from backend api.get('/api/attributes/Invitation').then(response => { const attrs = response.data?.attributes || response.data || []; setBackendAttributes(Array.isArray(attrs) ? attrs : []); }).catch(() => setBackendAttributes([])); }, [fetchMandates]); // Load invitations and roles when mandate changes (same roles as AdminUserMandatesPage: user, viewer, admin) useEffect(() => { if (selectedMandateId) { fetchInvitations(selectedMandateId, { includeExpired: showExpired, includeUsed: showUsed }); fetchRoles(selectedMandateId).then(setRoles); } }, [selectedMandateId, showExpired, showUsed, fetchInvitations, fetchRoles]); // Format timestamp const formatDate = (timestamp: number) => { if (!timestamp) return '-'; const date = new Date(timestamp * 1000); return date.toLocaleString('de-CH', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); }; // Table columns const columns = useMemo(() => [ { key: 'targetUsername', label: 'Benutzername', type: 'string' as const, sortable: true, filterable: true, searchable: true, width: 150, }, { key: 'email', label: 'E-Mail', type: 'string' as const, sortable: true, filterable: true, width: 180, render: (value: string, row: Invitation) => { const emailText = value || '-'; const emailSent = (row as any).emailSent; return ( {emailText} {emailSent && '✓'} ); } }, { key: 'roleIds', label: 'Rollen', type: 'string', // Array rendered as string sortable: false, filterable: false, width: 150, render: (value: string[]) => { if (!value || value.length === 0) return '-'; return value.map(roleId => { const role = roles.find(r => r.id === roleId); return role?.roleLabel || roleId; }).join(', '); } } as any, { key: 'expiresAt', label: 'Gültig bis', type: 'number' as const, sortable: true, width: 150, render: (value: number) => { const text = formatDate(value); const isExpired = value < Date.now() / 1000; return ( {text} {isExpired && '(abgelaufen)'} ); } }, { key: 'currentUses', label: 'Verwendet', type: 'string' as const, sortable: true, width: 100, render: (value: number, row: Invitation) => `${value || 0} / ${row.maxUses || 1}` }, { key: 'createdAt', label: 'Erstellt', type: 'number' as const, sortable: true, width: 150, render: (value: number) => formatDate(value) }, ], [roles]); // Form attributes - same role options as AdminUserMandatesPage (user, viewer, admin) const createFields: AttributeDefinition[] = useMemo(() => { const excludedFields = ['id', 'mandateId', 'token', 'createdBy', 'createdAt', 'expiresAt', 'currentUses', 'inviteUrl', 'featureInstanceId']; // Mandate-level roles (user, viewer, admin) - same as when adding mandate members const roleOptions = roles .filter(r => !r.featureInstanceId) .map(r => ({ value: r.id, label: r.roleLabel })); const fields = backendAttributes .filter(attr => !excludedFields.includes(attr.name)) .map(attr => ({ ...attr, options: attr.name === 'roleIds' ? roleOptions : attr.options, })) as AttributeDefinition[]; // Add helper field expiresInHours if not in model but fields exist if (fields.length > 0 && !fields.find(f => f.name === 'expiresInHours')) { fields.push({ name: 'expiresInHours', label: 'Gültigkeitsdauer (Stunden)', type: 'number', required: true, default: 72 } as any); } // Override required for targetUsername and email (both required for invitations) return fields.map(f => { if (f.name === 'targetUsername' || f.name === 'email') { return { ...f, required: true }; } return f; }); }, [roles, backendAttributes]); // Handle create invitation const handleCreateInvitation = async (data: InvitationCreate) => { if (!selectedMandateId) return; setIsSubmitting(true); try { const result = await createInvitation(selectedMandateId, data); if (result.success && result.data) { setShowCreateModal(false); setShowUrlModal(result.data); fetchInvitations(selectedMandateId, { includeExpired: showExpired, includeUsed: showUsed }); } else { showError('Fehler', result.error || 'Fehler beim Erstellen der Einladung'); } } finally { setIsSubmitting(false); } }; // Handle delete invitation by ID (for DeleteActionButton) // Note: DeleteActionButton handles confirmation UI, so no window.confirm here const handleDeleteInvitation = async (invitationId: string): Promise => { if (!selectedMandateId) return false; const result = await revokeInvitation(selectedMandateId, invitationId); if (!result.success) { showError('Fehler', result.error || 'Fehler beim Widerrufen der Einladung'); } return result.success; }; // Handle show URL const handleShowUrl = (invitation: Invitation) => { setShowUrlModal(invitation); }; // Copy URL to clipboard const handleCopyUrl = async (url: string) => { try { await navigator.clipboard.writeText(url); setCopySuccess(true); setTimeout(() => setCopySuccess(false), 2000); } catch (err) { console.error('Failed to copy:', err); } }; // Get mandate name const getMandateName = (mandate: Mandate) => { if (mandate.label) return mandate.label; if (typeof mandate.name === 'object') { return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id; } return mandate.name || mandate.id; }; if (error && !selectedMandateId) { return (
⚠️

Fehler: {error}

); } return (

Einladungen

Erstellen und verwalten Sie Einladungen für neue Benutzer

{/* Mandate Selector and Filters */}
{selectedMandateId && (
)}
{/* Content */} {!selectedMandateId ? (

Kein Mandant ausgewählt

Wählen Sie einen Mandanten aus, um dessen Einladungen zu verwalten.

) : loading && invitations.length === 0 ? (
Lade Einladungen...
) : invitations.length === 0 ? (

Keine Einladungen

Es gibt noch keine aktiven Einladungen für diesen Mandanten.

) : (
, onClick: handleShowUrl, title: 'Einladungs-Link anzeigen', } ]} hookData={{ handleDelete: handleDeleteInvitation, refetch: () => fetchInvitations(selectedMandateId, { includeExpired: showExpired, includeUsed: showUsed }), pagination, }} emptyMessage="Keine Einladungen gefunden" />
)} {/* Create Invitation Modal */} {showCreateModal && (
setShowCreateModal(false)}>
e.stopPropagation()}>

Neue Einladung erstellen

{roles.filter(r => !r.featureInstanceId).length === 0 ? (
Lade Rollen...
) : createFields.length === 0 ? (
Lade Formular...
) : ( setShowCreateModal(false)} submitButtonText="Einladung erstellen" cancelButtonText="Abbrechen" /> )}
)} {/* URL Display Modal */} {showUrlModal && (
setShowUrlModal(null)}>
e.stopPropagation()}>

Einladungs-Link

Einladung für Benutzer {showUrlModal.targetUsername}:

(e.target as HTMLInputElement).select()} />

Dieser Link kann nur von Benutzer {showUrlModal.targetUsername} verwendet werden.

{showUrlModal.email && (

{showUrlModal.emailSent ? `✓ Email wurde an ${showUrlModal.email} gesendet` : `Email-Adresse: ${showUrlModal.email} (nicht gesendet)`}

)}

Gültig bis: {formatDate(showUrlModal.expiresAt)}

)}
); }; export default AdminInvitationsPage;