frontend_nyla/src/pages/basedata/PromptsPage.tsx
2026-04-26 18:11:52 +02:00

316 lines
10 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.

/**
* PromptsPage
*
* Page for managing prompt templates using FormGeneratorTable.
* Follows the pattern established in AdminUsersPage/WorkflowsPage.
*/
import React, { useState, useMemo, useEffect, useCallback } from 'react';
import { usePrompts, usePromptOperations } from '../../hooks/usePrompts';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
import { FaSync, FaPlus } from 'react-icons/fa';
import styles from '../admin/Admin.module.css';
import { useLanguage } from '../../providers/language/LanguageContext';
import { resolveColumnTypes } from '../../utils/columnTypeResolver';
interface Prompt {
id: string;
name: string;
content: string;
[key: string]: any;
}
export const PromptsPage: React.FC = () => {
const { t } = useLanguage();
// Data hook
const {
prompts,
attributes,
permissions,
pagination,
loading,
error,
refetch,
fetchPromptById,
updateOptimistically,
} = usePrompts();
// Operations hook
const {
handlePromptCreate,
handlePromptUpdate,
handlePromptDelete,
handleInlineUpdate,
deletingPrompts,
} = usePromptOperations();
const [showCreateModal, setShowCreateModal] = useState(false);
const [editingPrompt, setEditingPrompt] = useState<Prompt | null>(null);
// ── Table refetch wrapper (stable signature used by FormGeneratorTable) ──
const _tableRefetch = useCallback(async (params?: any) => {
await refetch(params);
}, [refetch]);
// ── Refresh-All for the header "Aktualisieren" button ────────────────────
// Forces a paginated request so the cache key matches what the table uses
// internally. This guarantees fresh (non-cached) data is pulled in.
const _refreshAll = useCallback(async () => {
await _tableRefetch({ page: 1, pageSize: 25 });
}, [_tableRefetch]);
// Initial fetch
useEffect(() => {
_tableRefetch({ page: 1, pageSize: 25 });
}, [_tableRefetch]);
// Generate columns from attributes - exclude ID fields from display
const columns = useMemo(() => {
// Fields to hide in table view
const hiddenColumns = ['id', 'mandateId', 'sysCreatedAt', 'sysModifiedAt', '_hideDelete', '_permissions'];
const cols = (attributes || [])
.filter(attr => !hiddenColumns.includes(attr.name))
.map(attr => ({
key: attr.name,
label: attr.label || attr.name,
sortable: attr.sortable !== false,
filterable: attr.filterable !== false,
searchable: attr.searchable !== false,
width: attr.name === 'content' ? 300 : attr.width || 150,
minWidth: attr.minWidth || 100,
maxWidth: attr.name === 'content' ? 500 : attr.maxWidth || 400,
displayField: (attr as any).displayField,
frontendFormat: (attr as any).frontendFormat,
frontendFormatLabels: (attr as any).frontendFormatLabels,
}));
// Add sysCreatedBy column with FK resolution to show username
cols.push({
key: 'sysCreatedBy',
label: t('Erstellt von'),
sortable: true,
filterable: true,
searchable: true,
width: 150,
minWidth: 100,
maxWidth: 250,
displayField: 'sysCreatedByLabel',
frontendFormat: undefined,
frontendFormatLabels: undefined,
});
return resolveColumnTypes(cols, attributes || []);
}, [attributes, t]);
// Check permissions
const canCreate = permissions?.create !== 'n';
const canUpdate = permissions?.update !== 'n';
const canDelete = permissions?.delete !== 'n';
// Handle edit click
const handleEditClick = async (prompt: Prompt) => {
const fullPrompt = await fetchPromptById(prompt.id);
if (fullPrompt) {
setEditingPrompt(fullPrompt as Prompt);
}
};
// Handle create submit
const handleCreateSubmit = async (data: Partial<Prompt>) => {
const result = await handlePromptCreate({
name: data.name || '',
content: data.content || ''
});
if (result?.success) {
setShowCreateModal(false);
_refreshAll();
}
};
// Handle edit submit
const handleEditSubmit = async (data: Partial<Prompt>) => {
if (!editingPrompt) return;
const result = await handlePromptUpdate(editingPrompt.id, {
name: data.name || editingPrompt.name,
content: data.content || editingPrompt.content
});
if (result.success) {
setEditingPrompt(null);
_refreshAll();
}
};
// Handle delete single prompt (confirmation handled by DeleteActionButton)
const handleDelete = async (prompt: Prompt) => {
const success = await handlePromptDelete(prompt.id);
if (success) {
_refreshAll();
}
};
// Form attributes for create/edit modal
const formAttributes = useMemo(() => {
const excludedFields = ['id', 'mandateId', 'isSystem', 'sysCreatedBy', 'sysCreatedAt', 'sysModifiedAt', '_hideDelete', '_permissions'];
return (attributes || [])
.filter(attr => !excludedFields.includes(attr.name));
}, [attributes]);
if (error) {
return (
<div className={styles.adminPage}>
<div className={styles.errorContainer}>
<span className={styles.errorIcon}></span>
<p className={styles.errorMessage}>{t('Fehler beim Laden der Prompts: {detail}', { detail: String(error) })}</p>
<button className={styles.secondaryButton} onClick={() => _refreshAll()}>
<FaSync /> {t('Erneut versuchen')}
</button>
</div>
</div>
);
}
return (
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>{t('Prompts')}</h1>
<p className={styles.pageSubtitle}>{t('Prompt-Templates verwalten')}</p>
</div>
<div className={styles.headerActions}>
<button
className={styles.secondaryButton}
onClick={() => _refreshAll()}
disabled={loading}
>
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
</button>
{canCreate && (
<button
className={styles.primaryButton}
onClick={() => setShowCreateModal(true)}
>
<FaPlus /> {t('Neuer Prompt')}
</button>
)}
</div>
</div>
<div className={styles.tableContainer}>
<FormGeneratorTable
data={prompts}
columns={columns}
apiEndpoint="/api/prompts"
loading={loading}
pagination={true}
pageSize={25}
searchable={true}
filterable={true}
sortable={true}
selectable={true}
actionButtons={[
{
type: 'copy' as const,
title: t('Inhalt kopieren'),
contentField: 'content',
},
...(canUpdate ? [{
type: 'edit' as const,
onAction: handleEditClick,
title: t('Bearbeiten'),
}] : []),
...(canDelete ? [{
type: 'delete' as const,
title: t('Löschen'),
loading: (row: Prompt) => deletingPrompts.has(row.id),
}] : []),
]}
onDelete={handleDelete}
hookData={{
refetch: _tableRefetch,
permissions,
pagination,
handleDelete: handlePromptDelete,
handleInlineUpdate,
updateOptimistically,
}}
emptyMessage={t('Keine Prompts gefunden')}
/>
</div>
{/* Create Modal */}
{showCreateModal && (
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Neuer Prompt')}</h2>
<button
className={styles.modalClose}
onClick={() => setShowCreateModal(false)}
>
</button>
</div>
<div className={styles.modalContent}>
{formAttributes.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('Formular laden')}</span>
</div>
) : (
<FormGeneratorForm
attributes={formAttributes}
mode="create"
onSubmit={handleCreateSubmit}
onCancel={() => setShowCreateModal(false)}
submitButtonText={t('Erstellen')}
cancelButtonText={t('Abbrechen')}
/>
)}
</div>
</div>
</div>
)}
{/* Edit Modal */}
{editingPrompt && (
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Prompt bearbeiten')}</h2>
<button
className={styles.modalClose}
onClick={() => setEditingPrompt(null)}
>
</button>
</div>
<div className={styles.modalContent}>
{formAttributes.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('Formular laden')}</span>
</div>
) : (
<FormGeneratorForm
attributes={formAttributes}
data={editingPrompt}
mode="edit"
onSubmit={handleEditSubmit}
onCancel={() => setEditingPrompt(null)}
submitButtonText={t('Speichern')}
cancelButtonText={t('Abbrechen')}
/>
)}
</div>
</div>
</div>
)}
</div>
);
};
export default PromptsPage;