CG-Feedback-Monitor/src/pages/FeedbackStructureConfig.tsx
2026-07-06 19:08:24 +02:00

245 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState } from 'react'
import {
type FeedbackDimension, type FeedbackCategory, type FeedbackCriterionItem,
loadFeedbackStructure,
addFeedbackDimension, updateFeedbackDimension, deleteFeedbackDimension,
addFeedbackCategory, updateFeedbackCategory, deleteFeedbackCategory,
addFeedbackCriterionItem, updateFeedbackCriterionItem, deleteFeedbackCriterionItem,
} from '../db'
export default function FeedbackStructureConfig() {
const [dimensions, setDimensions] = useState<FeedbackDimension[]>([])
const [categories, setCategories] = useState<FeedbackCategory[]>([])
const [items, setItems] = useState<FeedbackCriterionItem[]>([])
const [expanded, setExpanded] = useState<Set<number>>(new Set())
const [expandedCat, setExpandedCat] = useState<Set<number>>(new Set())
// Neue Einträge
const [newDimName, setNewDimName] = useState('')
const [newCatName, setNewCatName] = useState<Record<number, string>>({})
const [newItemName, setNewItemName] = useState<Record<number, string>>({})
const load = async () => {
const structure = await loadFeedbackStructure()
setDimensions(structure.dimensions)
setCategories(structure.categories)
setItems(structure.items)
}
useEffect(() => { load() }, [])
// ── Dimensionen ───────────────────────────────────────────────────────────
const addDimension = async () => {
if (!newDimName.trim()) return
const maxOrder = dimensions.reduce((m, d) => Math.max(m, d.order), 0)
const id = await addFeedbackDimension({ name: newDimName.trim(), order: maxOrder + 1 })
setNewDimName('')
await load()
setExpanded(prev => new Set([...prev, id]))
}
const updateDimension = async (id: number, name: string) => {
await updateFeedbackDimension(id, { name })
setDimensions(prev => prev.map(d => d.id === id ? { ...d, name } : d))
}
const deleteDimension = async (id: number) => {
await deleteFeedbackDimension(id)
await load()
}
const moveDim = async (dim: FeedbackDimension, dir: -1 | 1) => {
const sorted = [...dimensions].sort((a, b) => a.order - b.order)
const idx = sorted.findIndex(d => d.id === dim.id)
const other = sorted[idx + dir]
if (!other) return
await updateFeedbackDimension(dim.id!, { order: other.order })
await updateFeedbackDimension(other.id!, { order: dim.order })
await load()
}
// ── Kategorien ────────────────────────────────────────────────────────────
const addCategory = async (dimId: number) => {
const name = newCatName[dimId]?.trim()
if (!name) return
const dimCats = categories.filter(c => c.dimensionId === dimId)
const maxOrder = dimCats.reduce((m, c) => Math.max(m, c.order), 0)
const id = await addFeedbackCategory({ dimensionId: dimId, name, order: maxOrder + 1 })
setNewCatName(prev => ({ ...prev, [dimId]: '' }))
await load()
setExpandedCat(prev => new Set([...prev, id]))
}
const updateCategory = async (id: number, name: string) => {
await updateFeedbackCategory(id, { name })
setCategories(prev => prev.map(c => c.id === id ? { ...c, name } : c))
}
const deleteCategory = async (id: number) => {
await deleteFeedbackCategory(id)
await load()
}
// ── Kriterium-Items ───────────────────────────────────────────────────────
const addItem = async (catId: number) => {
const name = newItemName[catId]?.trim()
if (!name) return
const catItems = items.filter(i => i.categoryId === catId)
const maxOrder = catItems.reduce((m, i) => Math.max(m, i.order), 0)
await addFeedbackCriterionItem({ categoryId: catId, name, order: maxOrder + 1 })
setNewItemName(prev => ({ ...prev, [catId]: '' }))
await load()
}
const updateItem = async (id: number, name: string) => {
await updateFeedbackCriterionItem(id, { name })
setItems(prev => prev.map(i => i.id === id ? { ...i, name } : i))
}
const deleteItem = async (id: number) => {
await deleteFeedbackCriterionItem(id)
setItems(prev => prev.filter(i => i.id !== id))
}
const updateItemWeight = async (id: number, weight: number) => {
await updateFeedbackCriterionItem(id, { weight })
setItems(prev => prev.map(i => i.id === id ? { ...i, weight } : i))
}
const sorted = [...dimensions].sort((a, b) => a.order - b.order)
return (
<div className="space-y-4">
<h2 className="text-lg font-bold text-gray-800">Feedback-Dimensionen & Kategorien</h2>
{/* Neue Dimension */}
<div className="flex gap-2">
<input
className="flex-1 border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
placeholder="Neue Dimension…"
value={newDimName}
onChange={e => setNewDimName(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addDimension()}
/>
<button onClick={addDimension}
className="px-4 py-2 bg-brand text-white text-sm rounded-lg font-medium">
+ Dimension
</button>
</div>
{/* Dimensionen */}
{sorted.map((dim, di) => {
const isOpen = expanded.has(dim.id!)
const dimCats = [...categories].filter(c => c.dimensionId === dim.id).sort((a, b) => a.order - b.order)
return (
<div key={dim.id} className="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden">
{/* Dimension-Header */}
<div className="flex items-center gap-2 p-3 bg-gray-50 border-b border-gray-100">
<div className="flex flex-col gap-0.5">
<button onClick={() => moveDim(dim, -1)} disabled={di === 0}
className="text-xs text-gray-400 hover:text-gray-600 leading-none disabled:opacity-30"></button>
<button onClick={() => moveDim(dim, 1)} disabled={di === sorted.length - 1}
className="text-xs text-gray-400 hover:text-gray-600 leading-none disabled:opacity-30"></button>
</div>
<input
className="flex-1 bg-transparent font-semibold text-sm text-gray-800 focus:outline-none border-b border-transparent focus:border-brand"
value={dim.name}
onChange={e => updateDimension(dim.id!, e.target.value)}
/>
<button onClick={() => setExpanded(prev => {
const next = new Set(prev)
if (next.has(dim.id!)) next.delete(dim.id!); else next.add(dim.id!)
return next
})} className="text-xs text-gray-400 px-1">{isOpen ? '▲' : '▼'}</button>
<button onClick={() => deleteDimension(dim.id!)}
className="text-xs text-red-300 hover:text-red-500 px-1">🗑</button>
</div>
{isOpen && (
<div className="p-3 space-y-3">
{dimCats.map(cat => {
const isCatOpen = expandedCat.has(cat.id!)
const catItems = [...items].filter(i => i.categoryId === cat.id).sort((a, b) => a.order - b.order)
return (
<div key={cat.id} className="border border-gray-100 rounded-lg overflow-hidden">
<div className="flex items-center gap-2 px-3 py-2 bg-gray-50">
<input
className="flex-1 bg-transparent text-sm text-gray-700 focus:outline-none"
value={cat.name}
onChange={e => updateCategory(cat.id!, e.target.value)}
/>
<button onClick={() => setExpandedCat(prev => {
const next = new Set(prev)
if (next.has(cat.id!)) next.delete(cat.id!); else next.add(cat.id!)
return next
})} className="text-xs text-gray-400">{isCatOpen ? '▲' : '▼'}</button>
<button onClick={() => deleteCategory(cat.id!)}
className="text-xs text-red-300 hover:text-red-500">🗑</button>
</div>
{isCatOpen && (
<div className="p-2 space-y-1.5">
{catItems.map(item => (
<div key={item.id} className="flex items-center gap-2">
<span className="text-gray-300 text-xs">·</span>
<input
className="flex-1 text-xs text-gray-600 border-b border-transparent focus:border-gray-300 focus:outline-none bg-transparent py-0.5"
value={item.name}
onChange={e => updateItem(item.id!, e.target.value)}
/>
<input
type="number" min={0} max={2} step={0.1}
value={item.weight ?? 1}
onChange={e => updateItemWeight(item.id!, Number(e.target.value))}
title="Gewichtung"
className="w-14 text-xs border border-gray-200 rounded px-1 py-0.5 text-gray-500 bg-white focus:outline-none focus:border-brand"
/>
<button onClick={() => deleteItem(item.id!)}
className="text-xs text-red-200 hover:text-red-400">×</button>
</div>
))}
<div className="flex gap-1.5 mt-1">
<input
className="flex-1 border border-gray-200 rounded px-2 py-1 text-xs focus:outline-none focus:border-brand"
placeholder="Neues Kriterium…"
value={newItemName[cat.id!] ?? ''}
onChange={e => setNewItemName(p => ({ ...p, [cat.id!]: e.target.value }))}
onKeyDown={e => e.key === 'Enter' && addItem(cat.id!)}
/>
<button onClick={() => addItem(cat.id!)}
className="px-2 py-1 bg-gray-100 hover:bg-gray-200 text-gray-600 text-xs rounded">+</button>
</div>
</div>
)}
</div>
)
})}
{/* Neue Kategorie */}
<div className="flex gap-1.5">
<input
className="flex-1 border border-gray-200 rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:border-brand"
placeholder="Neue Kategorie…"
value={newCatName[dim.id!] ?? ''}
onChange={e => setNewCatName(p => ({ ...p, [dim.id!]: e.target.value }))}
onKeyDown={e => e.key === 'Enter' && addCategory(dim.id!)}
/>
<button onClick={() => addCategory(dim.id!)}
className="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 text-gray-600 text-sm rounded-lg">
+ Kat.
</button>
</div>
</div>
)}
</div>
)
})}
</div>
)
}