316 lines
10 KiB
TypeScript
316 lines
10 KiB
TypeScript
/**
|
||
* 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;
|