245 lines
12 KiB
TypeScript
245 lines
12 KiB
TypeScript
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>
|
||
)
|
||
}
|