diff --git a/src/lib/tables.js b/src/lib/tables.js index e119562..69a6e0a 100644 --- a/src/lib/tables.js +++ b/src/lib/tables.js @@ -273,6 +273,24 @@ export const TABLES = { }, fetchRelated: [], }, + + audios: { + label: 'Audios', + icon: '🔊', + endpoint: '/audios', + statusField: 'status', + primaryLabel: 'text', + columns: ['text', 'voice_id', 'status', 'audio_link', 'created_at'], + linkedFields: {}, + editableFields: { + text: { type: 'textarea' }, + status: { type: 'select', options: ['generated', 'published', 'blocked'] }, + voice_id: { type: 'text' }, + model_id: { type: 'text' }, + audio_link: { type: 'text' }, + }, + fetchRelated: [], + }, }; export const STATUS_COLORS = { diff --git a/src/pages/TableView.jsx b/src/pages/TableView.jsx index f04129d..55f2203 100644 --- a/src/pages/TableView.jsx +++ b/src/pages/TableView.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useMemo, useCallback } from 'react'; +import { useEffect, useState, useMemo, useCallback, useRef } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import Layout from '../components/Layout'; import RecordModal from '../components/RecordModal'; @@ -12,25 +12,29 @@ function truncate(str, n = 60) { return s.length > n ? s.slice(0, n) + '…' : s; } -function CellValue({ col, value, linkedFields, navigate }) { +function CellValue({ col, value, linkedFields, fkLookup, navigate }) { if (value == null || value === '') return —; // Array of IDs (e.g. picture_ids, word_ids) if (Array.isArray(value)) { if (value.length === 0) return []; - const targetTable = linkedFields[col]; + const targetTableKey = linkedFields[col]; + const lookup = targetTableKey && fkLookup[targetTableKey]; return (
- {value.slice(0, 3).map(id => ( - - ))} + {value.slice(0, 3).map(id => { + const label = lookup?.[id]; + return ( + + ); + })} {value.length > 3 && ( +{value.length - 3} )} @@ -40,14 +44,20 @@ function CellValue({ col, value, linkedFields, navigate }) { // Single UUID FK (e.g. question_id) if (typeof value === 'string' && linkedFields[col]) { - const targetTable = linkedFields[col]; + const targetTableKey = linkedFields[col]; + const lookup = fkLookup[targetTableKey]; + const label = lookup?.[value]; return ( ); } @@ -63,18 +73,68 @@ function CellValue({ col, value, linkedFields, navigate }) { return {new Date(value).toLocaleDateString('de-DE')}; } - // Picture link — show as thumbnail + link - if (col === 'picture_link') { + // Picture/audio link + if (col === 'picture_link' || col === 'audio_link') { return ( - Bild öffnen + {col === 'audio_link' ? 'Audio öffnen' : 'Bild öffnen'} ); } + // Boolean + if (value === true || value === false) { + return {String(value)}; + } + return {truncate(value)}; } +function ColPanel({ allColumns, visibleSet, onToggle, onReset, anchorRef, onClose }) { + const panelRef = useRef(null); + + useEffect(() => { + function handleClick(e) { + if (panelRef.current && !panelRef.current.contains(e.target) && + anchorRef.current && !anchorRef.current.contains(e.target)) { + onClose(); + } + } + document.addEventListener('mousedown', handleClick); + return () => document.removeEventListener('mousedown', handleClick); + }, [onClose, anchorRef]); + + return ( +
+
+ Spalten + +
+
+ {allColumns.map(col => ( + + ))} +
+
+ ); +} + export default function TableView() { const { tableKey } = useParams(); const navigate = useNavigate(); @@ -90,6 +150,23 @@ export default function TableView() { const [showCreate, setShowCreate] = useState(false); const [confirmDeleteId, setConfirmDeleteId] = useState(null); const [deleting, setDeleting] = useState(false); + const [showColPanel, setShowColPanel] = useState(false); + const [sortCol, setSortCol] = useState(null); + const [sortDir, setSortDir] = useState('asc'); + const [fkLookup, setFkLookup] = useState({}); + const colBtnRef = useRef(null); + + const COLS_KEY = `cols_${tableKey}`; + + const [visibleCols, setVisibleCols] = useState(null); + + // Load column prefs from localStorage whenever tableKey changes + useEffect(() => { + try { + const saved = localStorage.getItem(`cols_${tableKey}`); + setVisibleCols(saved ? JSON.parse(saved) : null); + } catch { setVisibleCols(null); } + }, [tableKey]); useEffect(() => { const params = new URLSearchParams(window.location.search); @@ -100,12 +177,69 @@ export default function TableView() { useEffect(() => { if (!meta) return; setLoading(true); + setRows([]); + setSortCol(null); + setFkLookup({}); fetchAll(meta.endpoint) .then(data => setRows(Array.isArray(data) ? data : [])) .catch(err => setError(err.message)) .finally(() => setLoading(false)); }, [meta]); + // Derive all available columns from first loaded row + const allColumns = useMemo(() => { + if (rows.length === 0) return meta?.columns ?? []; + return Object.keys(rows[0]); + }, [rows, meta]); + + // Effective visible columns + const visibleSet = useMemo(() => { + return new Set(visibleCols ?? meta?.columns ?? []); + }, [visibleCols, meta]); + + const effectiveCols = useMemo(() => { + return allColumns.filter(c => visibleSet.has(c)); + }, [allColumns, visibleSet]); + + function toggleCol(col) { + const current = visibleCols ?? meta.columns; + const next = current.includes(col) + ? current.filter(c => c !== col) + : [...current, col]; + setVisibleCols(next); + localStorage.setItem(COLS_KEY, JSON.stringify(next)); + } + + function resetCols() { + setVisibleCols(null); + localStorage.removeItem(COLS_KEY); + setShowColPanel(false); + } + + // Load FK lookup tables when rows are available + useEffect(() => { + if (!meta || rows.length === 0) return; + const targets = new Set( + Object.values(meta.linkedFields) + .map(v => typeof v === 'string' ? v : null) + .filter(Boolean) + ); + targets.forEach(async targetKey => { + if (fkLookup[targetKey]) return; + const targetMeta = TABLES[targetKey]; + if (!targetMeta) return; + try { + const data = await fetchAll(targetMeta.endpoint); + setFkLookup(prev => ({ + ...prev, + [targetKey]: Object.fromEntries( + data.map(r => [r.id, r[targetMeta.primaryLabel] || r.id]) + ), + })); + } catch {} + }); + }, [rows, meta]); // eslint-disable-line react-hooks/exhaustive-deps + const statuses = useMemo(() => { const set = new Set(rows.map(r => r.status).filter(Boolean)); return [...set].sort(); @@ -124,6 +258,23 @@ export default function TableView() { }); }, [rows, statusFilter, textFilter]); + const sortedFiltered = useMemo(() => { + if (!sortCol) return filtered; + return [...filtered].sort((a, b) => { + const av = a[sortCol], bv = b[sortCol]; + if (av == null && bv == null) return 0; + if (av == null) return 1; + if (bv == null) return -1; + const cmp = String(av).localeCompare(String(bv), 'de', { numeric: true }); + return sortDir === 'asc' ? cmp : -cmp; + }); + }, [filtered, sortCol, sortDir]); + + function handleSort(col) { + if (sortCol === col) setSortDir(d => d === 'asc' ? 'desc' : 'asc'); + else { setSortCol(col); setSortDir('asc'); } + } + function handleRecordSaved(updated) { setRows(prev => prev.map(r => (r.id === updated.id ? { ...r, ...updated } : r))); } @@ -151,21 +302,44 @@ export default function TableView() { return ( -
+

{meta.icon} {meta.label} - ({filtered.length}{rows.length === 500 ? '+' : ''} von {rows.length}{rows.length === 500 ? '+' : ''}) + ({sortedFiltered.length}{rows.length === 500 ? '+' : ''} von {rows.length}{rows.length === 500 ? '+' : ''})

- {hasCreateForm(tableKey) && ( - - )} +
+ {/* Column toggle */} +
+ + {showColPanel && ( + setShowColPanel(false)} + /> + )} +
+ + {hasCreateForm(tableKey) && ( + + )} +
{/* Filters */} @@ -210,35 +384,47 @@ export default function TableView() { - {meta.columns.map(col => ( - ))} - {filtered.length === 0 && ( + {sortedFiltered.length === 0 && ( - )} - {filtered.map((row, i) => ( + {sortedFiltered.map((row, i) => ( setModalRecord(row)} className={`group border-b border-slate-100 hover:bg-slate-50 transition-colors cursor-pointer ${highlightId && row.id === highlightId ? 'bg-indigo-50 ring-1 ring-indigo-300' : ''}`} > - {meta.columns.map(col => ( + {effectiveCols.map(col => (
- {col} + {effectiveCols.map(col => ( + handleSort(col)} + className="text-left px-4 py-2.5 text-xs font-semibold text-slate-500 uppercase tracking-wide whitespace-nowrap cursor-pointer hover:bg-slate-100 select-none transition-colors" + > + + {col} + {sortCol === col ? ( + {sortDir === 'asc' ? 'â–²' : 'â–¼'} + ) : ( + â–² + )} +
+ Keine Einträge gefunden