frontend_nyla/src/pages/views/realestate/RealEstateProjectsView.tsx
ValueOn AG 77e7eba711 BREAKING CHANGE
API and persisted records use PowerOnModel system fields:
- sysCreatedAt, sysCreatedBy, sysModifiedAt, sysModifiedBy
Removed legacy JSON/DB field names:
- _createdAt, _createdBy, _modifiedAt, _modifiedBy
Frontend (frontend_nyla) and gateway call sites were updated accordingly.
Database:
- Bootstrap runs idempotent backfill (_migrateSystemFieldColumns) from old
  underscore columns and selected business duplicates into sys* where sys* IS NULL.
- Re-run app bootstrap against each PostgreSQL database after deploy.
- Optional: DROP INDEX IF EXISTS "idx_invitation_createdby" if an old index remains;
  new index: idx_invitation_syscreatedby on Invitation(sysCreatedBy).
Tests:
- RBAC integration tests aligned with current GROUP mandate filter and UserMandate-based
  UserConnection GROUP clause; buildRbacWhereClause(..., mandateId=...) must be passed
  explicitly (same as production request context).
2026-03-28 18:13:18 +01:00

205 lines
6.7 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.

/**
* RealEstateProjectsView
*
* Projekt-Verwaltung für eine Real Estate/PEK-Instanz.
* Verwendet FormGeneratorTable analog zu TrusteeDocumentsView.
*/
import React, { useState, useMemo, useEffect } from 'react';
import {
useRealEstateProjects,
useRealEstateProjectOperations,
type RealEstateProject,
} from '../../../hooks/useRealEstate';
import { useInstanceId } from '../../../hooks/useCurrentInstance';
import { FormGeneratorTable } from '../../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../../components/FormGenerator/FormGeneratorForm';
import { FaSync } from 'react-icons/fa';
import styles from '../../admin/Admin.module.css';
export const RealEstateProjectsView: React.FC = () => {
const instanceId = useInstanceId();
const {
items: projects,
attributes,
permissions,
pagination,
loading,
error,
refetch,
fetchById,
updateOptimistically,
removeOptimistically,
} = useRealEstateProjects();
const {
handleDelete,
handleCreate,
handleUpdate,
deletingItems,
} = useRealEstateProjectOperations();
const [editingProject, setEditingProject] = useState<RealEstateProject | 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 || 'string') 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 (project: RealEstateProject) => {
const full = await fetchById(project.id);
if (full) {
setEditingProject(full);
setIsCreateMode(false);
}
};
const handleCreateClick = () => {
setEditingProject(null);
setIsCreateMode(true);
};
const handleFormSubmit = async (data: Partial<RealEstateProject>) => {
if (isCreateMode) {
const result = await handleCreate(data);
if (result.success) {
setIsCreateMode(false);
refetch();
}
} else if (editingProject) {
const result = await handleUpdate(editingProject.id, data);
if (result.success) {
setEditingProject(null);
refetch();
}
}
};
const handleDeleteProject = async (project: RealEstateProject) => {
removeOptimistically(project.id);
const success = await handleDelete(project.id);
if (!success) refetch();
};
const handleCloseModal = () => {
setEditingProject(null);
setIsCreateMode(false);
};
const formAttributes = useMemo(() => {
const excluded = ['id', 'mandateId', 'instanceId', 'sysCreatedBy', 'sysCreatedAt', 'sysModifiedAt', 'sysModifiedBy'];
return (attributes || []).filter(attr => !excluded.includes(attr.name));
}, [attributes]);
const handleInlineUpdate = async (itemId: string, updateData: Partial<RealEstateProject>, row: RealEstateProject) => {
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 Projekte: {error}</p>
<button className={styles.secondaryButton} onClick={() => refetch()}>
<FaSync /> Erneut versuchen
</button>
</div>
</div>
);
}
return (
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.pageHeader}>
<div>
<p className={styles.pageSubtitle}>Projekte 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}>
+ Neues Projekt
</button>
)}
</div>
</div>
<div className={styles.tableContainer}>
<FormGeneratorTable
data={projects}
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: RealEstateProject) => deletingItems.has(row.id) }] : []),
]}
onDelete={handleDeleteProject}
hookData={{ refetch, permissions, pagination, handleDelete, handleInlineUpdate, updateOptimistically }}
emptyMessage="Keine Projekte gefunden"
/>
</div>
{(editingProject || isCreateMode) && (
<div className={styles.modalOverlay} onClick={handleCloseModal}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{isCreateMode ? 'Neues Projekt' : 'Projekt 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={editingProject || {}}
mode={isCreateMode ? 'create' : 'edit'}
onSubmit={handleFormSubmit}
onCancel={handleCloseModal}
submitButtonText={isCreateMode ? 'Erstellen' : 'Speichern'}
cancelButtonText="Abbrechen"
instanceId={instanceId}
/>
)}
</div>
</div>
</div>
)}
</div>
);
};
export default RealEstateProjectsView;