CG-Feedback-Monitor/src/pages/FeedbackStructureConfig.tsx

249 lines
12 KiB
TypeScript
Raw Normal View History

2026-07-02 10:10:06 +02:00
import { useEffect, useState } from 'react'
import { db, type FeedbackDimension, type FeedbackCategory, type FeedbackCriterionItem } from '../db'
2026-07-02 10:10:06 +02:00
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 [dims, cats, itms] = await Promise.all([
2026-07-02 10:10:06 +02:00
db.feedbackDimensions.orderBy('order').toArray(),
db.feedbackCategories.orderBy('order').toArray(),
db.feedbackCriterionItems.orderBy('order').toArray(),
])
setDimensions(dims)
setCategories(cats)
setItems(itms)
}
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 db.feedbackDimensions.add({ name: newDimName.trim(), order: maxOrder + 1 })
setNewDimName('')
await load()
setExpanded(prev => new Set([...prev, id]))
}
const updateDimension = async (id: number, name: string) => {
await db.feedbackDimensions.update(id, { name })
setDimensions(prev => prev.map(d => d.id === id ? { ...d, name } : d))
}
const deleteDimension = async (id: number) => {
const cats = await db.feedbackCategories.where('dimensionId').equals(id).toArray()
for (const cat of cats) {
await db.feedbackCriterionItems.where('categoryId').equals(cat.id!).delete()
}
await db.feedbackCategories.where('dimensionId').equals(id).delete()
await db.feedbackDimensions.delete(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 db.feedbackDimensions.update(dim.id!, { order: other.order })
await db.feedbackDimensions.update(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 db.feedbackCategories.add({ 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 db.feedbackCategories.update(id, { name })
setCategories(prev => prev.map(c => c.id === id ? { ...c, name } : c))
}
const deleteCategory = async (id: number) => {
await db.feedbackCriterionItems.where('categoryId').equals(id).delete()
await db.feedbackCategories.delete(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 db.feedbackCriterionItems.add({ categoryId: catId, name, order: maxOrder + 1 })
setNewItemName(prev => ({ ...prev, [catId]: '' }))
await load()
}
const updateItem = async (id: number, name: string) => {
await db.feedbackCriterionItems.update(id, { name })
setItems(prev => prev.map(i => i.id === id ? { ...i, name } : i))
}
const deleteItem = async (id: number) => {
await db.feedbackCriterionItems.delete(id)
setItems(prev => prev.filter(i => i.id !== id))
}
const updateItemWeight = async (id: number, weight: number) => {
await db.feedbackCriterionItems.update(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"
2026-07-02 10:10:06 +02:00
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">
2026-07-02 10:10:06 +02:00
+ 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"
2026-07-02 10:10:06 +02:00
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}
2026-07-02 10:10:06 +02:00
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"
/>
2026-07-02 10:10:06 +02:00
<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"
2026-07-02 10:10:06 +02:00
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"
2026-07-02 10:10:06 +02:00
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>
)
}