/** * 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, FaBuilding, FaCopy, FaLink } from 'react-icons/fa'; import { useToast } from '../../contexts/ToastContext'; import { useApiRequest } from '../../hooks/useApi'; import { fetchAttributes } from '../../api/attributesApi'; import { resolveColumnTypes } from '../../utils/columnTypeResolver'; import type { ColumnConfig } from '../../components/FormGenerator/FormGeneratorTable'; import styles from './Admin.module.css'; import { useLanguage } from '../../providers/language/LanguageContext'; import { mandateDisplayLabel } from '../../utils/mandateDisplayUtils'; export const AdminInvitationsPage: React.FC = () => { const { t } = useLanguage(); const { showError } = useToast(); const { request } = useApiRequest(); 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(); fetchAttributes(request, 'Invitation') .then(setBackendAttributes) .catch(() => setBackendAttributes([])); }, [fetchMandates, request]); // 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' }); }; const _rawColumns: ColumnConfig[] = useMemo(() => [ { key: 'targetUsername', label: t('Benutzername'), sortable: true, filterable: true, searchable: true, width: 150, }, { key: 'email', label: t('E-Mail'), sortable: true, filterable: true, width: 180, formatter: (value: string, row: Invitation) => { const emailText = value || '-'; const emailSent = (row as any).emailSent; return ( {emailText} {emailSent && '✓'} ); }, }, { key: 'roleIds', label: t('Rollen'), sortable: false, filterable: false, width: 150, formatter: (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(', '); }, }, { key: 'expiresAt', label: t('Gültig bis'), sortable: true, width: 150, formatter: (value: number) => { const text = formatDate(value); const isExpired = value < Date.now() / 1000; return ( {text} {isExpired && '(abgelaufen)'} ); }, }, { key: 'currentUses', label: t('Verwendet'), sortable: true, width: 100, formatter: (value: number, row: Invitation) => `${value || 0} / ${row.maxUses || 1}`, }, { key: 'sysCreatedAt', label: t('Erstellt'), sortable: true, width: 150, formatter: (value: number) => formatDate(value), }, ], [roles, t]); const columns = useMemo( () => resolveColumnTypes(_rawColumns, backendAttributes), [_rawColumns, backendAttributes], ); // Form attributes - same role options as AdminUserMandatesPage (user, viewer, admin) const createFields: AttributeDefinition[] = useMemo(() => { const excludedFields = ['id', 'mandateId', 'token', 'sysCreatedBy', 'sysCreatedAt', 'sysUpdatedAt', 'sysUpdatedBy', '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: t('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, t]); // 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(t('Fehler'), result.error || t('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(t('Fehler'), result.error || t('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); } }; if (error && !selectedMandateId) { return (
⚠️

{t('Fehler')}: {error}

); } return (

{t('Einladungen')}

{t('Erstellen und verwalten Sie Einladungen')}

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

{t('Kein Mandant ausgewählt')}

{t('Wählen Sie einen Mandanten aus, um dessen Einladungen zu verwalten.')}

) : (
, onClick: handleShowUrl, title: t('Einladungslink anzeigen'), } ]} hookData={{ handleDelete: handleDeleteInvitation, refetch: (params?: any) => fetchInvitations(params || selectedMandateId, { includeExpired: showExpired, includeUsed: showUsed }), pagination, }} emptyMessage={t('Keine Einladungen gefunden')} />
)} {/* Create Invitation Modal */} {showCreateModal && (

{t('Neue Einladung erstellen')}

{roles.filter(r => !r.featureInstanceId).length === 0 ? (
{t('Rollen laden')}
) : createFields.length === 0 ? (
{t('Formular laden')}
) : ( setShowCreateModal(false)} submitButtonText={t('Einladung erstellen')} cancelButtonText={t('Abbrechen')} /> )}
)} {/* URL Display Modal */} {showUrlModal && (

{t('Einladungs-Link')}

{t('Einladung für Benutzer')} {showUrlModal.targetUsername}:

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

{t('Dieser Link kann nur von Benutzer')} {showUrlModal.targetUsername}{' '} {t('verwendet werden.')}

{showUrlModal.email && (

{showUrlModal.emailSent ? `✓ ${t('E-Mail wurde an {email} gesendet', { email: showUrlModal.email })}` : `${t('E-Mail-Adresse')}: ${showUrlModal.email} (${t('nicht gesendet')})`}

)}

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

)}
); }; export default AdminInvitationsPage;