frontend_nyla/src/pages/views/trustee/TrusteePositionsView.tsx
2026-02-03 23:42:19 +01:00

306 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* TrusteePositionsView
*
* Positions-Verwaltung für eine Trustee-Instanz.
* Verwendet FormGeneratorTable für konsistentes UI.
*/
import React, { useState, useMemo, useEffect } from 'react';
import { useTrusteePositions, useTrusteePositionOperations, TrusteePosition } from '../../../hooks/useTrustee';
import { useInstanceId } from '../../../hooks/useCurrentInstance';
import { FormGeneratorTable } from '../../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../../components/FormGenerator/FormGeneratorForm';
import { FaSync, FaReceipt } from 'react-icons/fa';
import styles from '../../admin/Admin.module.css';
export const TrusteePositionsView: React.FC = () => {
const instanceId = useInstanceId();
// Entity hook
const {
items: positions,
attributes,
permissions,
pagination,
loading,
error,
refetch,
fetchById,
updateOptimistically,
removeOptimistically,
} = useTrusteePositions();
// Operations hook
const {
handleDelete,
handleCreate,
handleUpdate,
deletingItems,
} = useTrusteePositionOperations();
// Modal state
const [editingPosition, setEditingPosition] = useState<TrusteePosition | null>(null);
const [isCreateMode, setIsCreateMode] = useState(false);
// Initial fetch
useEffect(() => {
if (instanceId) {
refetch();
}
}, [instanceId]);
// Hidden columns (not shown in table view, but available in form)
const hiddenColumns = ['desc', 'featureInstanceId', 'mandateId'];
// Generate columns from attributes + add system columns
const columns = useMemo(() => {
const attrColumns = (attributes || [])
.filter(attr => !hiddenColumns.includes(attr.name))
.map(attr => ({
key: attr.name,
label: attr.label || attr.name,
type: attr.type as any,
sortable: attr.sortable !== false,
filterable: attr.filterable !== false,
searchable: attr.searchable !== false,
width: attr.width || 150,
minWidth: attr.minWidth || 100,
maxWidth: attr.maxWidth || 400,
}));
// Add _createdAt system column
attrColumns.push({
key: '_createdAt',
label: 'Erstellt am',
type: 'timestamp' as any,
sortable: true,
filterable: false,
searchable: false,
width: 150,
minWidth: 120,
maxWidth: 200,
});
return attrColumns;
}, [attributes]);
// Check permissions
const canCreate = permissions?.create !== 'n';
const canUpdate = permissions?.update !== 'n';
const canDelete = permissions?.delete !== 'n';
// Handle edit click
const handleEditClick = async (pos: TrusteePosition) => {
const fullPos = await fetchById(pos.id);
if (fullPos) {
setEditingPosition(fullPos);
setIsCreateMode(false);
}
};
// Handle create click
const handleCreateClick = () => {
setEditingPosition(null);
setIsCreateMode(true);
};
// Handle form submit
const handleFormSubmit = async (data: Partial<TrusteePosition>) => {
// Auto-calculate VAT if provided
const processedData = { ...data };
if (processedData.bookingAmount && processedData.vatPercentage && !processedData.vatAmount) {
processedData.vatAmount = processedData.bookingAmount * (processedData.vatPercentage / 100);
}
if (isCreateMode) {
const result = await handleCreate(processedData);
if (result.success) {
setIsCreateMode(false);
refetch();
}
} else if (editingPosition) {
const result = await handleUpdate(editingPosition.id, processedData);
if (result.success) {
setEditingPosition(null);
refetch();
}
}
};
// Handle delete (confirmation handled by DeleteActionButton)
const handleDeletePos = async (pos: TrusteePosition) => {
removeOptimistically(pos.id);
const success = await handleDelete(pos.id);
if (!success) {
refetch(); // Revert on error
}
};
// Close modal
const handleCloseModal = () => {
setEditingPosition(null);
setIsCreateMode(false);
};
// Form attributes (exclude system fields)
const formAttributes = useMemo(() => {
const excludedFields = ['id', 'mandateId', 'instanceId', '_createdBy', '_createdAt', '_modifiedAt', '_modifiedBy'];
return (attributes || []).filter(attr => !excludedFields.includes(attr.name));
}, [attributes]);
// Handle inline update
const handleInlineUpdate = async (itemId: string, updateData: Partial<TrusteePosition>, row: TrusteePosition) => {
updateOptimistically(itemId, updateData);
const result = await handleUpdate(itemId, { ...row, ...updateData });
if (!result.success) {
refetch(); // Revert on error
}
};
if (error) {
return (
<div className={styles.adminPage}>
<div className={styles.errorContainer}>
<span className={styles.errorIcon}></span>
<p className={styles.errorMessage}>Fehler beim Laden der Positionen: {error}</p>
<button className={styles.secondaryButton} onClick={() => refetch()}>
<FaSync /> Erneut versuchen
</button>
</div>
</div>
);
}
return (
<div className={styles.adminPage}>
<div className={styles.pageHeader}>
<div>
<p className={styles.pageSubtitle}>Buchungspositionen verwalten</p>
</div>
<div className={styles.headerActions}>
<button
className={styles.secondaryButton}
onClick={() => refetch()}
disabled={loading}
>
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
</button>
{canCreate && (
<button
className={styles.primaryButton}
onClick={handleCreateClick}
>
+ Neue Position
</button>
)}
</div>
</div>
<div className={styles.tableContainer}>
{loading && (!positions || positions.length === 0) ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>Lade Positionen...</span>
</div>
) : !positions || positions.length === 0 ? (
<div className={styles.emptyState}>
<FaReceipt className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>Keine Positionen vorhanden</h3>
<p className={styles.emptyDescription}>
Erstellen Sie eine neue Position, um zu beginnen.
</p>
{canCreate && (
<button
className={styles.primaryButton}
onClick={handleCreateClick}
>
+ Neue Position
</button>
)}
</div>
) : (
<FormGeneratorTable
data={positions}
columns={columns}
loading={loading}
pagination={true}
pageSize={25}
searchable={true}
filterable={true}
sortable={true}
selectable={false}
actionButtons={[
...(canUpdate ? [{
type: 'edit' as const,
onAction: handleEditClick,
title: 'Bearbeiten',
}] : []),
...(canDelete ? [{
type: 'delete' as const,
title: 'Löschen',
loading: (row: TrusteePosition) => deletingItems.has(row.id),
}] : []),
]}
onDelete={handleDeletePos}
hookData={{
refetch,
permissions,
pagination,
handleDelete,
handleInlineUpdate,
updateOptimistically,
}}
emptyMessage="Keine Positionen gefunden"
/>
)}
</div>
{/* Create/Edit Modal */}
{(editingPosition || isCreateMode) && (
<div className={styles.modalOverlay} onClick={handleCloseModal}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>
{isCreateMode ? 'Neue Position' : 'Position bearbeiten'}
</h2>
<button
className={styles.modalClose}
onClick={handleCloseModal}
>
</button>
</div>
<div className={styles.modalContent}>
{formAttributes.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>Lade Formular...</span>
</div>
) : (
<FormGeneratorForm
attributes={formAttributes}
data={editingPosition || {
bookingCurrency: 'CHF',
originalCurrency: 'CHF',
bookingAmount: 0,
originalAmount: 0,
vatPercentage: 0,
vatAmount: 0,
}}
mode={isCreateMode ? 'create' : 'edit'}
onSubmit={handleFormSubmit}
onCancel={handleCloseModal}
submitButtonText={isCreateMode ? 'Erstellen' : 'Speichern'}
cancelButtonText="Abbrechen"
instanceId={instanceId}
/>
)}
</div>
</div>
</div>
)}
</div>
);
};
export default TrusteePositionsView;