diff --git a/src/components/AdminCalendar.tsx b/src/components/AdminCalendar.tsx new file mode 100644 index 0000000..42f98ee --- /dev/null +++ b/src/components/AdminCalendar.tsx @@ -0,0 +1,112 @@ +'use client'; + +import React from 'react'; +import FullCalendar from '@fullcalendar/react'; +import dayGridPlugin from '@fullcalendar/daygrid'; +import timeGridPlugin from '@fullcalendar/timegrid'; +import interactionPlugin from '@fullcalendar/interaction'; +import { Paper, Box, Typography } from '@mui/material'; +import { useReservationStore } from '../services/reservation-store'; +import { MOCK_TOOLS } from '../services/mock-data'; + +const STATUS_COLOR: Record = { + requested: '#F59E0B', + approved: '#22C55E', + active: '#3B82F6', + completed: '#9CA3AF', + overdue: '#EF4444', +}; + +const STATUS_LABEL: Record = { + requested: 'Angefragt', + approved: 'Genehmigt', + active: 'Aktiv', + completed: 'Erledigt', + overdue: 'Überfällig', +}; + +export default function AdminCalendar() { + const reservations = useReservationStore((state) => state.reservations); + + const events = reservations.map((res) => { + const tool = MOCK_TOOLS.find((t) => t.id === res.toolId); + const color = STATUS_COLOR[res.status] ?? '#FF6B00'; + return { + id: res.id, + title: tool ? tool.name : 'Werkzeug', + start: res.startDate, + end: res.endDate, + backgroundColor: color, + borderColor: color, + extendedProps: { status: res.status }, + }; + }); + + return ( + + + + Reservierungskalender + + + {Object.entries(STATUS_LABEL).map(([key, label]) => ( + + + + {label} + + + ))} + + + + + ( + + + {eventInfo.event.title} + + + )} + /> + + + ); +} diff --git a/src/components/AdminDashboard.tsx b/src/components/AdminDashboard.tsx new file mode 100644 index 0000000..0c40a10 --- /dev/null +++ b/src/components/AdminDashboard.tsx @@ -0,0 +1,816 @@ +'use client'; + +import { + Box, + Container, + Typography, + Tab, + Tabs, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Button, + IconButton, + Chip, + useMediaQuery, + Tooltip, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + MenuItem, + Stack, + Switch, + FormControlLabel, +} from '@mui/material'; +import { useTheme } from '@mui/material/styles'; +import { useState, lazy, Suspense, useMemo } from 'react'; +import { motion } from 'framer-motion'; +import { + Plus, + Edit2, + Trash2, + FileText, + Users, + Hammer, + Calendar as CalendarIcon, + Activity, + TrendingUp, + ShieldCheck, + Check, + X, +} from 'lucide-react'; +import { useI18n } from '../services/i18n'; +import { CATEGORY_LABEL, type Tool, type ToolCategory } from '../types'; +import { useToolsStore } from '../services/tools-store'; +import { useReservationStore } from '../services/reservation-store'; +import { MOCK_USERS } from '../services/mock-data'; + +const AdminCalendar = lazy(() => import('./AdminCalendar')); + +const STATUS_COLOR: Record = { + requested: 'warning', + approved: 'success', + active: 'info', + completed: 'default', + overdue: 'error', +}; + +export default function AdminDashboard() { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down('sm')); + const [activeTab, setActiveTab] = useState(0); + const { t } = useI18n(); + + const tools = useToolsStore((s) => s.tools); + const removeTool = useToolsStore((s) => s.removeTool); + const addTool = useToolsStore((s) => s.addTool); + const updateTool = useToolsStore((s) => s.updateTool); + const manuals = useToolsStore((s) => s.manuals); + const addManual = useToolsStore((s) => s.addManual); + const removeManual = useToolsStore((s) => s.removeManual); + + const reservations = useReservationStore((s) => s.reservations); + const updateStatus = useReservationStore((s) => s.updateStatus); + const removeReservation = useReservationStore((s) => s.removeReservation); + + const [toolDialogOpen, setToolDialogOpen] = useState(false); + const [editingTool, setEditingTool] = useState(null); + const [confirmDelete, setConfirmDelete] = useState<{ + kind: 'tool' | 'reservation'; + id: string; + label: string; + } | null>(null); + const [manualDialogOpen, setManualDialogOpen] = useState(false); + + const stats = useMemo(() => { + const active = reservations.filter( + (r) => r.status === 'approved' || r.status === 'active' + ).length; + const pending = reservations.filter((r) => r.status === 'requested').length; + return { tools: tools.length, users: MOCK_USERS.length, active, pending }; + }, [reservations, tools]); + + const pendingReservations = reservations.filter((r) => r.status === 'requested'); + + const openAdd = () => { + setEditingTool(null); + setToolDialogOpen(true); + }; + + const openEdit = (tool: Tool) => { + setEditingTool(tool); + setToolDialogOpen(true); + }; + + const handleDeleteConfirmed = () => { + if (!confirmDelete) return; + if (confirmDelete.kind === 'tool') removeTool(confirmDelete.id); + if (confirmDelete.kind === 'reservation') removeReservation(confirmDelete.id); + setConfirmDelete(null); + }; + + return ( + + + + + + + + {t('adminDashboard')} + + + {t('adminSubtitle')} + + + + + {/* Stats */} + + } label={t('statsTools')} value={stats.tools} accent="#FF6B00" /> + } label={t('statsUsers')} value={stats.users} accent="#2D5A27" /> + } label={t('statsActive')} value={stats.active} accent="#3B82F6" /> + } label={t('statsPending')} value={stats.pending} accent="#F59E0B" /> + + + {/* Pending requests banner */} + {pendingReservations.length > 0 && ( + + + + + + + {t('pendingReservations')} ({pendingReservations.length}) + + + + {pendingReservations.map((r) => { + const tool = tools.find((tt) => tt.id === r.toolId); + const requester = MOCK_USERS.find((u) => u.id === r.userId); + return ( + + {tool?.emoji ?? '🔧'} + + + {tool?.name ?? '—'} + + + {t('requestedBy')}: {requester?.username ?? '?'} · {r.startDate} → {r.endDate} + + + + + + + + ); + })} + + + )} + + + setActiveTab(val)} + variant={isSmall ? 'scrollable' : 'standard'} + scrollButtons="auto" + allowScrollButtonsMobile + > + } iconPosition="start" label={t('manageTools')} /> + } iconPosition="start" label={t('calendar')} /> + } iconPosition="start" label={t('manageRAG')} /> + } iconPosition="start" label={t('manageUsers')} /> + + + + + {activeTab === 0 && ( + + + + {t('manageTools')} + + + + + + + + {t('name')} + {t('category')} + {t('location')} + {t('status')} + {t('actions')} + + + + {tools.map((tool) => ( + + + + {tool.emoji} + + {tool.name} + + + + + + + {tool.location} + + + + + + openEdit(tool)}> + + + + + + setConfirmDelete({ kind: 'tool', id: tool.id, label: tool.name }) + } + > + + + + + + ))} + +
+
+
+ )} + + {activeTab === 1 && ( + + + {t('calendar')} … + +
+ } + > + + + )} + + {activeTab === 2 && ( + + + + {t('manageRAG')} + + + + + + + + {t('forTool')} + {t('manualPreview')} + {t('actions')} + + + + {manuals.map((manual, idx) => { + const tool = tools.find((tt) => tt.id === manual.toolId); + return ( + + + + {tool?.emoji ?? '📄'} + + {manual.title ?? tool?.name ?? 'Allgemein'} + + + + + {manual.content} + + + removeManual(manual.toolId, idx)} + > + + + + + ); + })} + +
+
+
+ )} + + {activeTab === 3 && ( + + + {t('manageUsers')} + + + + + + {t('username')} + {t('role')} + {t('certifications')} + {t('actions')} + + + + {MOCK_USERS.map((user) => ( + + {user.username} + + + + + {user.trainings.length > 0 ? ( + + {user.trainings.map((tr, i) => ( + + ))} + + ) : ( + + {t('none')} + + )} + + + + + + + + ))} + +
+
+
+ )} + + + setToolDialogOpen(false)} + onSave={(values) => { + if (editingTool) { + updateTool(editingTool.id, values); + } else { + addTool(values); + } + setToolDialogOpen(false); + }} + /> + + setManualDialogOpen(false)} + onSave={(manual) => { + addManual(manual); + setManualDialogOpen(false); + }} + /> + + setConfirmDelete(null)} + PaperProps={{ sx: { borderRadius: 3 } }} + > + {t('confirmDelete')} + + + {confirmDelete?.label} + + + + + + + + + ); +} + +/* ------------ Tool Editor Dialog ------------ */ + +const EMPTY_TOOL: Omit = { + name: '', + description: '', + location: '', + category: 'hand', + trainingRequired: false, + requiredTrainingId: '', + homeAllowed: true, + maxRentalDays: 3, + emoji: '🔧', +}; + +function ToolEditorDialog({ + open, + tool, + onClose, + onSave, +}: { + open: boolean; + tool: Tool | null; + onClose: () => void; + onSave: (values: Omit) => void; +}) { + const { t } = useI18n(); + const [values, setValues] = useState>(EMPTY_TOOL); + + // Reset when opening or switching tool + useMemo(() => { + if (open) { + setValues(tool ? { ...tool } : EMPTY_TOOL); + } + }, [open, tool]); + + const update = >(key: K, val: Omit[K]) => + setValues((v) => ({ ...v, [key]: val })); + + const canSave = values.name.trim().length > 0 && values.location.trim().length > 0; + + return ( + + + {tool ? t('editTool') : t('addTool')} + + + + + update('emoji', e.target.value)} + size="small" + inputProps={{ maxLength: 2, style: { fontSize: 24, textAlign: 'center' } }} + /> + update('name', e.target.value)} + size="small" + fullWidth + autoFocus + /> + + update('description', e.target.value)} + size="small" + multiline + minRows={2} + maxRows={4} + fullWidth + /> + + update('location', e.target.value)} + size="small" + fullWidth + /> + update('maxRentalDays', Math.max(1, Number(e.target.value)))} + size="small" + fullWidth + inputProps={{ min: 1, max: 30 }} + /> + + update('category', e.target.value as ToolCategory)} + size="small" + fullWidth + > + {(Object.entries(CATEGORY_LABEL) as [ToolCategory, string][]).map(([key, label]) => ( + + {label} + + ))} + + + update('trainingRequired', e.target.checked)} + /> + } + label={t('trainingRequired')} + /> + update('homeAllowed', e.target.checked)} + /> + } + label={t('homeUse')} + /> + + {values.trainingRequired && ( + update('requiredTrainingId', e.target.value)} + size="small" + helperText="z. B. drill, 3d-print, laser, soldering, cnc" + fullWidth + /> + )} + + + + + + + + ); +} + +/* ------------ Manual Editor Dialog ------------ */ + +function ManualEditorDialog({ + open, + tools, + onClose, + onSave, +}: { + open: boolean; + tools: Tool[]; + onClose: () => void; + onSave: (manual: { toolId: string; title: string; content: string }) => void; +}) { + const { t } = useI18n(); + const [toolId, setToolId] = useState(''); + const [title, setTitle] = useState(''); + const [content, setContent] = useState(''); + + useMemo(() => { + if (open) { + setToolId(tools[0]?.id ?? ''); + setTitle(''); + setContent(''); + } + }, [open, tools]); + + const canSave = Boolean(toolId) && content.trim().length > 0; + + return ( + + {t('addManual')} + + + setToolId(e.target.value)} + size="small" + fullWidth + > + {tools.map((tool) => ( + + {tool.emoji} {tool.name} + + ))} + + setTitle(e.target.value)} + size="small" + fullWidth + /> + setContent(e.target.value)} + size="small" + multiline + minRows={4} + maxRows={10} + fullWidth + placeholder="Beschreibe Bedienung, Sicherheitshinweise, häufige Probleme…" + /> + + + + + + + + ); +} + +function StatCard({ + icon, + label, + value, + accent, + delay, +}: { + icon: React.ReactNode; + label: string; + value: number; + accent: string; + delay: number; +}) { + return ( + + + + + + {icon} + + + {label} + + + + {value} + + + + ); +}