feat: CRM-Dashboard, Content-Verwaltung und Wort-Autocomplete
- Home-Seite nach Login mit Begrüßung und 3 Kacheln (Content erstellen, Content verwalten, User verwalten) - AuthContext speichert User-Profil + Rolle; AdminRoute blockt Nicht-Admins - Content verwalten (admin-only): Status-Dashboard pro Collection, Liste/Kachel-View, generisches Edit-Formular - Nur aktive db_-Collections im Dashboard (alte pictures/objects/words/questions entfernt) - Wort-Autocomplete in DrawIt: ab dem ersten Buchstaben Vorschläge aus db_words, Tastatur-Navigation, Duplikat-Filter - Backend: /users/me Proxy, db-words/search Endpoint, generische Collection-Endpoints Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
106
frontend/src/pages/ContentManage.tsx
Normal file
106
frontend/src/pages/ContentManage.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import Topbar from '../components/Topbar'
|
||||
import { getDashboardSummary, type DashboardEntry } from '../api'
|
||||
|
||||
const groupLabel = (g: 'alt' | 'neu') =>
|
||||
g === 'alt' ? 'Alt (vor DB-Refactor)' : 'Neu (DB-basiert)'
|
||||
|
||||
function StatusPill({ status, count, onClick }: { status: string | null; count: number; onClick?: () => void }) {
|
||||
const label = status ?? '—'
|
||||
return (
|
||||
<button type="button" className="cm-pill" onClick={onClick} title={`Status: ${label}`}>
|
||||
<span className={`cm-pill-dot cm-pill-dot-${(status || 'unknown').toLowerCase().replace(/[^a-z]/g, '')}`} />
|
||||
<span className="cm-pill-label">{label}</span>
|
||||
<span className="cm-pill-count">{count}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ContentManage() {
|
||||
const navigate = useNavigate()
|
||||
const { token } = useAuth()
|
||||
const [entries, setEntries] = useState<DashboardEntry[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) return
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
getDashboardSummary(token)
|
||||
.then(setEntries)
|
||||
.catch((e: Error) => setError(e.message))
|
||||
.finally(() => setLoading(false))
|
||||
}, [token])
|
||||
|
||||
const groups: ('neu' | 'alt')[] = ['neu', 'alt']
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<Topbar page="home" />
|
||||
<div className="cm-page">
|
||||
<div className="cm-header">
|
||||
<div>
|
||||
<h1 className="cm-title">Content verwalten</h1>
|
||||
<p className="cm-subtitle">Übersicht aller Collections nach Status. Klick auf eine Karte oder einen Status zum Drill-Down.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading && <div className="cm-empty">Wird geladen…</div>}
|
||||
{error && <div className="cm-error">{error}</div>}
|
||||
|
||||
{!loading && !error && groups.map(group => {
|
||||
const items = entries.filter(e => e.group === group)
|
||||
if (items.length === 0) return null
|
||||
return (
|
||||
<section key={group} className="cm-section">
|
||||
<h2 className="cm-section-title">{groupLabel(group)}</h2>
|
||||
<div className="cm-grid">
|
||||
{items.map(entry => (
|
||||
<article key={entry.name} className="cm-card">
|
||||
<button
|
||||
type="button"
|
||||
className="cm-card-head"
|
||||
onClick={() => navigate(`/content/${entry.name}`)}
|
||||
>
|
||||
<div className="cm-card-titlewrap">
|
||||
<span className={`cm-kind cm-kind-${entry.kind}`}>{entry.kind === 'image' ? 'Bilder' : 'Text'}</span>
|
||||
<span className="cm-card-label">{entry.label}</span>
|
||||
</div>
|
||||
<div className="cm-card-total">{entry.total.toLocaleString('de-DE')}</div>
|
||||
</button>
|
||||
|
||||
{entry.error && <div className="cm-card-error">{entry.error}</div>}
|
||||
|
||||
{entry.has_status && entry.by_status.length > 0 && (
|
||||
<div className="cm-pills">
|
||||
{entry.by_status.map(g => (
|
||||
<StatusPill
|
||||
key={String(g.status)}
|
||||
status={g.status}
|
||||
count={g.count}
|
||||
onClick={() => navigate(`/content/${entry.name}?status=${encodeURIComponent(g.status || '')}`)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!entry.has_status && !entry.error && (
|
||||
<div className="cm-pills cm-pills-muted">
|
||||
<span className="cm-pill cm-pill-static">
|
||||
<span className="cm-pill-label">Kein Status-Feld</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user