ui-nyla/src/pages/views/realestate/RealEstateParcelsView.tsx

266 lines
8 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.

/**
* RealEstateParcelsView
*
* Parzellen-Verwaltung für eine Real Estate/PEK-Instanz.
* Verwendet FormGeneratorTable analog zu TrusteeDocumentsView.
*/
import React, { useState, useMemo, useEffect } from 'react';
import {
useRealEstateParcels,
useRealEstateParcelOperations,
type RealEstateParcel,
} from '../../../hooks/useRealEstate';
import { useInstanceId } from '../../../hooks/useCurrentInstance';
import { FormGeneratorTable } from '../../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../../components/FormGenerator/FormGeneratorForm';
import { FaSync, FaMapMarkerAlt } from 'react-icons/fa';
import styles from '../../admin/Admin.module.css';
export const RealEstateParcelsView: React.FC = () => {
const instanceId = useInstanceId();
const {
items: parcels,
attributes,
permissions,
pagination,
loading,
error,
refetch,
fetchById,
updateOptimistically,
removeOptimistically,
} = useRealEstateParcels();
const {
handleDelete,
handleCreate,
handleUpdate,
deletingItems,
} = useRealEstateParcelOperations();
const [editingParcel, setEditingParcel] = useState<RealEstateParcel | null>(null);
const [isCreateMode, setIsCreateMode] = useState(false);
useEffect(() => {
if (instanceId) {
refetch();
}
}, [instanceId, refetch]);
const columns = useMemo(() => {
return (attributes || []).map(attr => ({
key: attr.name,
label: attr.label || attr.name,
type: attr.type as 'string' | 'number' | 'date' | 'boolean',
sortable: attr.sortable !== false,
filterable: attr.filterable !== false,
searchable: attr.searchable !== false,
width: attr.width || 150,
minWidth: attr.minWidth || 100,
maxWidth: attr.maxWidth || 400,
}));
}, [attributes]);
const canCreate = permissions?.create !== 'n';
const canUpdate = permissions?.update !== 'n';
const canDelete = permissions?.delete !== 'n';
const handleEditClick = async (parcel: RealEstateParcel) => {
const full = await fetchById(parcel.id);
if (full) {
setEditingParcel(full);
setIsCreateMode(false);
}
};
const handleCreateClick = () => {
setEditingParcel(null);
setIsCreateMode(true);
};
const handleFormSubmit = async (data: Partial<RealEstateParcel>) => {
if (isCreateMode) {
const result = await handleCreate(data);
if (result.success) {
setIsCreateMode(false);
refetch();
}
} else if (editingParcel) {
const result = await handleUpdate(editingParcel.id, data);
if (result.success) {
setEditingParcel(null);
refetch();
}
}
};
const handleDeleteParcel = async (parcel: RealEstateParcel) => {
removeOptimistically(parcel.id);
const success = await handleDelete(parcel.id);
if (!success) {
refetch();
}
};
const handleCloseModal = () => {
setEditingParcel(null);
setIsCreateMode(false);
};
const formAttributes = useMemo(() => {
const excluded = ['id', 'mandateId', 'instanceId', '_createdBy', '_createdAt', '_modifiedAt', '_modifiedBy'];
return (attributes || []).filter(attr => !excluded.includes(attr.name));
}, [attributes]);
const handleInlineUpdate = async (
itemId: string,
updateData: Partial<RealEstateParcel>,
row: RealEstateParcel
) => {
updateOptimistically(itemId, updateData);
const result = await handleUpdate(itemId, { ...row, ...updateData });
if (!result.success) {
refetch();
}
};
if (error) {
return (
<div className={styles.adminPage}>
<div className={styles.errorContainer}>
<span className={styles.errorIcon}></span>
<p className={styles.errorMessage}>Fehler beim Laden der Parzellen: {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}>Parzellen 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 Parzelle
</button>
)}
</div>
</div>
<div className={styles.tableContainer}>
{loading && (!parcels || parcels.length === 0) ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>Lade Parzellen...</span>
</div>
) : !parcels || parcels.length === 0 ? (
<div className={styles.emptyState}>
<FaMapMarkerAlt className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>Keine Parzellen vorhanden</h3>
<p className={styles.emptyDescription}>
Erstellen Sie eine neue Parzelle, um zu beginnen.
</p>
{canCreate && (
<button className={styles.primaryButton} onClick={handleCreateClick}>
+ Neue Parzelle
</button>
)}
</div>
) : (
<FormGeneratorTable
data={parcels}
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: RealEstateParcel) => deletingItems.has(row.id),
},
]
: []),
]}
onDelete={handleDeleteParcel}
hookData={{
refetch,
permissions,
pagination,
handleDelete,
handleInlineUpdate,
updateOptimistically,
}}
emptyMessage="Keine Parzellen gefunden"
/>
)}
</div>
{(editingParcel || 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 Parzelle' : 'Parzelle 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={editingParcel || {}}
mode={isCreateMode ? 'create' : 'edit'}
onSubmit={handleFormSubmit}
onCancel={handleCloseModal}
submitButtonText={isCreateMode ? 'Erstellen' : 'Speichern'}
cancelButtonText="Abbrechen"
instanceId={instanceId}
/>
)}
</div>
</div>
</div>
)}
</div>
);
};
export default RealEstateParcelsView;