'use client'; import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { BarChart, Bar, XAxis, YAxis, Tooltip as RTooltip, ResponsiveContainer, CartesianGrid, } from 'recharts'; import { Plus, Pencil, Trash2, Copy, Loader2, Search, Eye, Globe, Lock, Tag, BookOpen, BarChart3, ListChecks, MessageSquareText, MessageSquare, type LucideIcon, } from 'lucide-react'; const LUCIDE_ICON_MAP_CB: Record = { MessageSquare, MessageSquareText, BookOpen, Plus, Pencil, Trash2, Copy, Loader2, Search, Eye, Globe, Lock, Tag, BarChart3, ListChecks, }; function renderCBIcon(name: string, className?: string): React.ReactNode { const IconComp = LUCIDE_ICON_MAP_CB[name]; if (!IconComp) return null; return ; } import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Switch } from '@/components/ui/switch'; import { toast } from 'sonner'; import { useAppStore } from '@/lib/store'; import { t } from '@/lib/i18n'; import { fetchCommentCategories, createCommentCategory, updateCommentCategory, deleteCommentCategory, fetchCommentBank, createCommentBankEntry, updateCommentBankEntry, deleteCommentBankEntry, fetchSubjects, type CommentCategory, type CommentBankEntry, type CommentBankEntryInput, type CommentBankEntryUpdate, type Subject as ApiSubject, } from '@/lib/api'; /* ── Constants ─────────────────────────────────────────────────────── */ const COLOR_PRESETS = [ '#10b981', // emerald '#14b8a6', // teal '#f59e0b', // amber '#8b5cf6', // violet '#f43f5e', // rose '#64748b', // slate '#84cc16', // lime '#ec4899', // pink ]; const GRADE_LEVEL_OPTIONS = ['all', '1', '2', '3', '3-4', '4', '5', '5-6', '6']; /* ── Helpers ───────────────────────────────────────────────────────── */ function hexToRgba(hex: string, alpha: number): string { const m = hex.replace('#', ''); if (m.length !== 6) return hex; const r = parseInt(m.slice(0, 2), 16); const g = parseInt(m.slice(2, 4), 16); const b = parseInt(m.slice(4, 6), 16); return `rgba(${r}, ${g}, ${b}, ${alpha})`; } /* ── Form state ────────────────────────────────────────────────────── */ interface EntryFormState { id?: string; categoryId: string; subjectId: string; title: string; text: string; gradeLevel: string; schoolType: string; isPublic: boolean; tags: string; } interface CategoryFormState { id?: string; name: string; color: string; icon: string; } function emptyEntryForm(categoryId = ''): EntryFormState { return { categoryId, subjectId: 'none', title: '', text: '', gradeLevel: 'all', schoolType: 'ELEMENTARY', isPublic: false, tags: '', }; } function emptyCategoryForm(): CategoryFormState { return { name: '', color: '#10b981', icon: '', }; } /* ── View Component ────────────────────────────────────────────────── */ export default function CommentBankView() { const currentUser = useAppStore((s) => s.currentUser); const locale = useAppStore((s) => s.locale); const schoolId = currentUser?.schoolId ?? ''; // Data state const [categories, setCategories] = useState([]); const [entries, setEntries] = useState([]); const [subjects, setSubjects] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Filter state const [searchQuery, setSearchQuery] = useState(''); const [filterCategory, setFilterCategory] = useState('all'); const [filterSubject, setFilterSubject] = useState('all'); const [filterGradeLevel, setFilterGradeLevel] = useState('all'); const [filterPublic, setFilterPublic] = useState('all'); // Active tab const [activeTab, setActiveTab] = useState('entries'); // Dialog state — entries const [entryDialogOpen, setEntryDialogOpen] = useState(false); const [entryForm, setEntryForm] = useState(emptyEntryForm()); const [entryDetailOpen, setEntryDetailOpen] = useState(false); const [entryDetailData, setEntryDetailData] = useState(null); const [deleteEntryOpen, setDeleteEntryOpen] = useState(false); const [deleteEntryId, setDeleteEntryId] = useState(''); // Dialog state — categories const [categoryDialogOpen, setCategoryDialogOpen] = useState(false); const [categoryForm, setCategoryForm] = useState(emptyCategoryForm()); const [deleteCategoryOpen, setDeleteCategoryOpen] = useState(false); const [deleteCategoryId, setDeleteCategoryId] = useState(''); /* ── Data loading ──────────────────────────────────────────────────── */ const loadAll = useCallback(async () => { if (!schoolId) return; setLoading(true); setError(null); try { const [cats, ents, subs] = await Promise.all([ fetchCommentCategories(schoolId), fetchCommentBank({ schoolId }), fetchSubjects(schoolId), ]); setCategories(cats); setEntries(ents); setSubjects(subs); } catch (err) { setError(String(err)); } finally { setLoading(false); } }, [schoolId]); useEffect(() => { loadAll(); }, [loadAll]); /* ── Filtered entries ─────────────────────────────────────────────── */ const filteredEntries = useMemo(() => { let list = entries; if (searchQuery.trim()) { const q = searchQuery.trim().toLowerCase(); list = list.filter( (e) => e.title.toLowerCase().includes(q) || e.text.toLowerCase().includes(q) || (e.tags && e.tags.toLowerCase().includes(q)) ); } if (filterCategory !== 'all') { list = list.filter((e) => e.categoryId === filterCategory); } if (filterSubject !== 'all') { list = list.filter((e) => e.subjectId === filterSubject); } if (filterGradeLevel !== 'all') { list = list.filter((e) => e.gradeLevel === filterGradeLevel); } if (filterPublic === 'true') { list = list.filter((e) => e.isPublic); } else if (filterPublic === 'false') { list = list.filter((e) => !e.isPublic); } return list; }, [entries, searchQuery, filterCategory, filterSubject, filterGradeLevel, filterPublic]); /* ── Statistics ────────────────────────────────────────────────────── */ const stats = useMemo(() => { const totalEntries = entries.length; const publicEntries = entries.filter((e) => e.isPublic).length; const totalCategories = categories.length; const mostUsed = entries.reduce((best, e) => (e.usageCount > (best?.usageCount ?? 0) ? e : best), null as CommentBankEntry | null); // Bar chart data: entries by category const byCategory = categories.map((cat) => ({ name: cat.icon ? `${cat.icon} ${cat.name}` : cat.name, count: entries.filter((e) => e.categoryId === cat.id).length, fill: cat.color, })); // Top 5 most-used const topUsed = [...entries] .sort((a, b) => b.usageCount - a.usageCount) .slice(0, 5); return { totalEntries, publicEntries, totalCategories, mostUsed, byCategory, topUsed }; }, [entries, categories]); /* ── Entry CRUD ────────────────────────────────────────────────────── */ const openEntryDialog = useCallback( (editEntry?: CommentBankEntry) => { if (editEntry) { setEntryForm({ id: editEntry.id, categoryId: editEntry.categoryId, subjectId: editEntry.subjectId ?? 'none', title: editEntry.title, text: editEntry.text, gradeLevel: editEntry.gradeLevel ?? 'all', schoolType: editEntry.schoolType ?? 'ELEMENTARY', isPublic: editEntry.isPublic, tags: editEntry.tags ?? '', }); } else { setEntryForm(emptyEntryForm(categories[0]?.id ?? '')); } setEntryDialogOpen(true); }, [categories] ); const saveEntry = useCallback(async () => { if (!entryForm.title.trim() || !entryForm.text.trim() || !entryForm.categoryId) { toast.error(t('comments.validation_required')); return; } try { const payload: CommentBankEntryInput | CommentBankEntryUpdate = { schoolId, categoryId: entryForm.categoryId, subjectId: entryForm.subjectId === 'none' ? null : entryForm.subjectId, title: entryForm.title.trim(), text: entryForm.text.trim(), gradeLevel: entryForm.gradeLevel === 'all' ? null : entryForm.gradeLevel, schoolType: entryForm.schoolType || null, isPublic: entryForm.isPublic, tags: entryForm.tags.trim() || null, }; if (entryForm.id) { // Update await updateCommentBankEntry(entryForm.id, payload); toast.success(t('comments.saved')); } else { // Create await createCommentBankEntry(payload as CommentBankEntryInput); toast.success(t('comments.created')); } setEntryDialogOpen(false); loadAll(); } catch (err) { toast.error(String(err)); } }, [entryForm, schoolId, loadAll]); const handleDeleteEntry = useCallback(async () => { try { await deleteCommentBankEntry(deleteEntryId); toast.success(t('comments.deleted')); setDeleteEntryOpen(false); loadAll(); } catch (err) { toast.error(String(err)); } }, [deleteEntryId, loadAll]); const copyEntryText = useCallback((text: string) => { navigator.clipboard.writeText(text); toast.success(t('comments.copy_success')); }, []); const incrementUsage = useCallback(async (id: string) => { try { await updateCommentBankEntry(id, { incrementUsage: true }); loadAll(); } catch { // silent fail } }, [loadAll]); /* ── Category CRUD ──────────────────────────────────────────────────── */ const openCategoryDialog = useCallback( (editCat?: CommentCategory) => { if (editCat) { setCategoryForm({ id: editCat.id, name: editCat.name, color: editCat.color, icon: editCat.icon ?? '', }); } else { setCategoryForm(emptyCategoryForm()); } setCategoryDialogOpen(true); }, [] ); const saveCategory = useCallback(async () => { if (!categoryForm.name.trim()) { toast.error(t('comments.category_name_required')); return; } try { if (categoryForm.id) { await updateCommentCategory(categoryForm.id, { name: categoryForm.name.trim(), color: categoryForm.color, icon: categoryForm.icon || null, }); toast.success(t('comments.category_saved')); } else { await createCommentCategory({ schoolId, name: categoryForm.name.trim(), color: categoryForm.color, icon: categoryForm.icon || null, }); toast.success(t('comments.category_created')); } setCategoryDialogOpen(false); loadAll(); } catch (err) { toast.error(String(err)); } }, [categoryForm, schoolId, loadAll]); const handleDeleteCategory = useCallback(async () => { try { await deleteCommentCategory(deleteCategoryId); toast.success(t('comments.category_deleted')); setDeleteCategoryOpen(false); loadAll(); } catch (err) { toast.error(String(err)); } }, [deleteCategoryId, loadAll]); /* ── Render ─────────────────────────────────────────────────────────── */ if (!currentUser) return null; if (loading) { return (
); } if (error) { return (

{t('comments.error')}

{error}

); } return ( {/* Header */}

{t('comments.title')}

{t('comments.subtitle')}

{/* Tabs */} {t('comments.tab_entries')} {t('comments.tab_categories')} {t('comments.tab_statistics')} {/* ── Tab 1: Entries ──────────────────────────────────────── */} {/* Search + Filters */}
setSearchQuery(e.target.value)} className="pl-9" />
{/* Grid */} {filteredEntries.length === 0 ? (

{t('comments.empty_title')}

{t('comments.empty_desc')}

) : (
{filteredEntries.map((entry, i) => ( { setEntryDetailData(entry); setEntryDetailOpen(true); incrementUsage(entry.id); }} >
{entry.title}
{entry.category.icon ? renderCBIcon(entry.category.icon) : null}{entry.category.name} {entry.isPublic ? ( {t('comments.public')} ) : ( {t('comments.private')} )}

{entry.text}

{entry.subject && ( {entry.subject.name} )} {entry.gradeLevel && ( Klasse {entry.gradeLevel} )} {entry.usageCount > 0 && ( {t('comments.usage_count')}: {entry.usageCount} )} {entry.teacher.firstName} {entry.teacher.lastName}
{/* Action buttons (visible on hover) */}
))}
)}
{/* ── Tab 2: Categories ───────────────────────────────────── */}

{t('comments.categories_title')}

{categories.length === 0 ? (

{t('comments.category_empty_title')}

{t('comments.category_empty_desc')}

) : (
{categories.map((cat, i) => (
{cat.icon && {cat.icon}} {cat.name} {cat._count?.comments ?? 0} {t('comments.entries_count')}
))}
)}
{/* ── Tab 3: Statistics ───────────────────────────────────── */} {/* KPI tiles */}

{stats.totalEntries}

{t('comments.stat_total')}

{stats.publicEntries}

{t('comments.stat_public')}

{stats.totalCategories}

{t('comments.stat_categories')}

{stats.mostUsed ? stats.mostUsed.title : '—'}

{t('comments.stat_most_used')}

{/* Bar chart */} {stats.byCategory.length > 0 && ( {t('comments.chart_by_category')}
{stats.byCategory.map((entry, index) => ( ))}
)} {/* Top 5 most-used */} {stats.topUsed.length > 0 && ( {t('comments.top_used_title')}
{stats.topUsed.map((entry, i) => (
#{i + 1}

{entry.title}

{entry.text.slice(0, 60)}…

{entry.category.icon ? renderCBIcon(entry.category.icon) : null}{entry.category.name} {entry.usageCount}×
))}
)}
{/* ── Entry Create/Edit Dialog ──────────────────────────────────── */} {entryForm.id ? t('comments.edit_entry') : t('comments.create_entry')} {entryForm.id ? t('comments.edit_entry_desc') : t('comments.create_entry_desc')}
setEntryForm({ ...entryForm, title: e.target.value })} placeholder={t('comments.title_placeholder')} />