326 lines
13 KiB
JavaScript
326 lines
13 KiB
JavaScript
import { useState, useEffect, useCallback } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { databases, DATABASE_ID } from '@/lib/appwrite';
|
|
import { ID, Query } from 'appwrite';
|
|
import Header from './Header';
|
|
import { useToast } from '@/hooks/useToast';
|
|
import FilialDetail from './FilialDetail';
|
|
import UserCreateForm from './UserCreateForm';
|
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
|
|
const ROLE_LABELS = {
|
|
admin: 'Admin',
|
|
firmenleiter: 'Firmenleiter',
|
|
filialleiter: 'Filialleiter',
|
|
service: 'Service',
|
|
lager: 'Lager',
|
|
};
|
|
|
|
export default function AdminPanel() {
|
|
const { showToast } = useToast();
|
|
const navigate = useNavigate();
|
|
|
|
const [stats, setStats] = useState({ users: 0, locations: 0, assets: 0, lagerstandorte: 0, locationsWithoutFilialleiter: 0 });
|
|
const [locations, setLocations] = useState([]);
|
|
const [users, setUsers] = useState([]);
|
|
const [userSearchQuery, setUserSearchQuery] = useState('');
|
|
const [showUserForm, setShowUserForm] = useState(false);
|
|
const [newFiliale, setNewFiliale] = useState({ name: '', address: '' });
|
|
const [addingFiliale, setAddingFiliale] = useState(false);
|
|
const [editingId, setEditingId] = useState(null);
|
|
const [editForm, setEditForm] = useState({ name: '', address: '' });
|
|
|
|
const loadData = useCallback(async () => {
|
|
try {
|
|
const [locsRes, usersRes, assetsRes, lsRes] = await Promise.all([
|
|
databases.listDocuments(DATABASE_ID, 'locations', [Query.limit(100)]),
|
|
databases.listDocuments(DATABASE_ID, 'users_meta', [Query.limit(500)]),
|
|
databases.listDocuments(DATABASE_ID, 'assets', [Query.limit(1)]),
|
|
databases.listDocuments(DATABASE_ID, 'lagerstandorte', [Query.limit(1)]),
|
|
]);
|
|
setLocations(locsRes.documents);
|
|
setUsers(usersRes.documents);
|
|
const locationsWithoutFilialleiter = locsRes.documents.filter(
|
|
(loc) => !usersRes.documents.some((u) => u.locationId === loc.$id && u.role === 'filialleiter')
|
|
).length;
|
|
setStats({
|
|
users: usersRes.total,
|
|
locations: locsRes.total,
|
|
assets: assetsRes.total,
|
|
lagerstandorte: lsRes.total,
|
|
locationsWithoutFilialleiter,
|
|
});
|
|
} catch (err) {
|
|
console.error('Admin-Daten laden fehlgeschlagen:', err);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => { loadData(); }, [loadData]);
|
|
|
|
async function handleAddFiliale(e) {
|
|
e.preventDefault();
|
|
if (!newFiliale.name.trim()) return;
|
|
setAddingFiliale(true);
|
|
try {
|
|
const doc = await databases.createDocument(DATABASE_ID, 'locations', ID.unique(), {
|
|
name: newFiliale.name.trim(),
|
|
address: newFiliale.address.trim(),
|
|
isActive: true,
|
|
});
|
|
setLocations((prev) => [...prev, doc]);
|
|
setStats((s) => ({ ...s, locations: s.locations + 1 }));
|
|
setNewFiliale({ name: '', address: '' });
|
|
showToast(`Filiale "${doc.name}" erstellt`);
|
|
} catch (err) {
|
|
showToast('Fehler beim Erstellen: ' + (err.message || err), '#C62828');
|
|
} finally {
|
|
setAddingFiliale(false);
|
|
}
|
|
}
|
|
|
|
async function handleToggleFiliale(id) {
|
|
const loc = locations.find((l) => l.$id === id);
|
|
if (!loc) return;
|
|
try {
|
|
const updated = await databases.updateDocument(DATABASE_ID, 'locations', id, {
|
|
isActive: !loc.isActive,
|
|
});
|
|
setLocations((prev) => prev.map((l) => l.$id === id ? updated : l));
|
|
showToast(`Filiale "${loc.name}" ${updated.isActive ? 'aktiviert' : 'deaktiviert'}`);
|
|
} catch (err) {
|
|
showToast('Fehler: ' + (err.message || err), '#C62828');
|
|
}
|
|
}
|
|
|
|
async function handleDeleteFiliale(id) {
|
|
const loc = locations.find((l) => l.$id === id);
|
|
if (!window.confirm(`Filiale "${loc?.name}" wirklich löschen?`)) return;
|
|
try {
|
|
await databases.deleteDocument(DATABASE_ID, 'locations', id);
|
|
setLocations((prev) => prev.filter((l) => l.$id !== id));
|
|
setStats((s) => ({ ...s, locations: s.locations - 1 }));
|
|
setEditingId((current) => (current === id ? null : current));
|
|
showToast(`Filiale "${loc.name}" gelöscht`, '#607D8B');
|
|
} catch (err) {
|
|
showToast('Fehler beim Löschen: ' + (err.message || err), '#C62828');
|
|
}
|
|
}
|
|
|
|
function startEdit(loc) {
|
|
setEditingId(loc.$id);
|
|
setEditForm({ name: loc.name, address: loc.address || '' });
|
|
}
|
|
|
|
async function handleSaveEdit() {
|
|
if (!editForm.name.trim()) return;
|
|
try {
|
|
const updated = await databases.updateDocument(DATABASE_ID, 'locations', editingId, {
|
|
name: editForm.name.trim(),
|
|
address: editForm.address.trim(),
|
|
});
|
|
setLocations((prev) => prev.map((l) => l.$id === editingId ? updated : l));
|
|
setEditForm((f) => ({ ...f, name: updated.name, address: updated.address || '' }));
|
|
showToast(`Filiale "${updated.name}" gespeichert`);
|
|
} catch (err) {
|
|
showToast('Fehler beim Speichern: ' + (err.message || err), '#C62828');
|
|
}
|
|
}
|
|
|
|
const filteredUsers = users.filter(
|
|
(u) =>
|
|
!userSearchQuery.trim() ||
|
|
(u.userName || '').toLowerCase().includes(userSearchQuery.toLowerCase()) ||
|
|
(u.userId || '').toLowerCase().includes(userSearchQuery.toLowerCase())
|
|
);
|
|
|
|
const getLocationName = (locationId) => {
|
|
if (!locationId) return 'Nicht zugeordnet';
|
|
const loc = locations.find((l) => l.$id === locationId);
|
|
return loc?.name || 'Unbekannte Filiale';
|
|
};
|
|
|
|
const statItems = [
|
|
{ label: 'Benutzer', value: stats.users },
|
|
{ label: 'Filialen', value: stats.locations },
|
|
{ label: 'Assets gesamt', value: stats.assets },
|
|
{ label: 'Lagerstandorte', value: stats.lagerstandorte },
|
|
{ label: 'Filialen ohne Filialleiter', value: stats.locationsWithoutFilialleiter ?? 0 },
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<Header showToast={showToast} />
|
|
<div className="mx-auto max-w-7xl p-6">
|
|
<div className="mb-8">
|
|
<h1 className="text-3xl font-bold tracking-tight">Admin Panel</h1>
|
|
<p className="mt-1 text-muted-foreground">System-Übersicht und Verwaltung</p>
|
|
</div>
|
|
|
|
<div className="mb-8 grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-5">
|
|
{statItems.map((item) => (
|
|
<Card key={item.label}>
|
|
<CardContent className="pt-2">
|
|
<div className="text-3xl font-bold">{item.value}</div>
|
|
<p className="text-sm text-muted-foreground">{item.label}</p>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Filialen verwalten</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form className="flex flex-col gap-3 sm:flex-row" onSubmit={handleAddFiliale}>
|
|
<Input
|
|
value={newFiliale.name}
|
|
onChange={(e) => setNewFiliale((f) => ({ ...f, name: e.target.value }))}
|
|
placeholder="Filialname (z.B. Kaiserslautern)"
|
|
/>
|
|
<Input
|
|
value={newFiliale.address}
|
|
onChange={(e) => setNewFiliale((f) => ({ ...f, address: e.target.value }))}
|
|
placeholder="Adresse (optional)"
|
|
/>
|
|
<Button type="submit" disabled={addingFiliale || !newFiliale.name.trim()}>
|
|
{addingFiliale ? '...' : 'Hinzufügen'}
|
|
</Button>
|
|
</form>
|
|
|
|
<Separator className="my-4" />
|
|
|
|
<div className="space-y-3">
|
|
{locations.length === 0 && (
|
|
<p className="py-4 text-center text-sm text-muted-foreground">Keine Filialen vorhanden</p>
|
|
)}
|
|
{locations.map((loc) => (
|
|
<div key={loc.$id} className="space-y-0">
|
|
<div
|
|
className={`flex flex-col gap-3 rounded-lg border p-3 sm:flex-row sm:items-center sm:justify-between ${!loc.isActive ? 'opacity-60' : ''}`}
|
|
>
|
|
{editingId === loc.$id ? (
|
|
<div className="flex w-full flex-col gap-2 sm:flex-row sm:items-center">
|
|
<Input
|
|
value={editForm.name}
|
|
onChange={(e) => setEditForm((f) => ({ ...f, name: e.target.value }))}
|
|
placeholder="Filialname"
|
|
className="flex-1"
|
|
/>
|
|
<Input
|
|
value={editForm.address}
|
|
onChange={(e) => setEditForm((f) => ({ ...f, address: e.target.value }))}
|
|
placeholder="Adresse"
|
|
className="flex-1"
|
|
/>
|
|
<div className="flex gap-2">
|
|
<Button size="sm" onClick={handleSaveEdit}>Speichern</Button>
|
|
<Button size="sm" variant="outline" onClick={() => setEditingId(null)}>Abbrechen</Button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="flex items-center gap-3">
|
|
<span className="font-medium">{loc.name}</span>
|
|
{loc.address && <span className="text-sm text-muted-foreground">{loc.address}</span>}
|
|
<Badge variant={loc.isActive ? 'default' : 'outline'}>
|
|
{loc.isActive ? 'Aktiv' : 'Inaktiv'}
|
|
</Badge>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button size="sm" variant="outline" onClick={() => startEdit(loc)}>Bearbeiten</Button>
|
|
<Button size="sm" variant="secondary" onClick={() => handleToggleFiliale(loc.$id)}>
|
|
{loc.isActive ? 'Deaktivieren' : 'Aktivieren'}
|
|
</Button>
|
|
<Button size="sm" variant="destructive" onClick={() => handleDeleteFiliale(loc.$id)}>Löschen</Button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
{editingId === loc.$id && (
|
|
<FilialDetail
|
|
location={loc}
|
|
onClose={() => setEditingId(null)}
|
|
showToast={showToast}
|
|
onUserAdded={loadData}
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="mt-6">
|
|
<CardHeader>
|
|
<CardTitle>Benutzer verwaltung</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="mb-4 flex gap-2">
|
|
<Input
|
|
placeholder="Benutzer suchen..."
|
|
value={userSearchQuery}
|
|
onChange={(e) => setUserSearchQuery(e.target.value)}
|
|
className="max-w-xs"
|
|
/>
|
|
<Button size="sm" variant="outline" onClick={() => setShowUserForm(true)}>
|
|
Neuer Benutzer
|
|
</Button>
|
|
</div>
|
|
<ScrollArea className="h-[280px] rounded-lg border">
|
|
<div className="space-y-1 p-2">
|
|
{users
|
|
.filter((u) =>
|
|
(u.userName || u.userId || '')
|
|
.toLowerCase()
|
|
.includes(userSearchQuery.toLowerCase())
|
|
)
|
|
.map((u) => (
|
|
<button
|
|
key={u.$id}
|
|
type="button"
|
|
onClick={() => navigate(`/admin/user/${u.userId}`)}
|
|
className="flex w-full items-center justify-between rounded border px-3 py-2 text-left text-sm hover:bg-muted/50"
|
|
>
|
|
<span className="font-medium">{u.userName || u.userId}</span>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-muted-foreground">
|
|
{locations.find((l) => l.$id === u.locationId)?.name || 'Nicht zugeordnet'}
|
|
</span>
|
|
<Badge variant="secondary">{ROLE_LABELS[u.role] || u.role}</Badge>
|
|
</div>
|
|
</button>
|
|
))}
|
|
{users.filter((u) =>
|
|
(u.userName || u.userId || '').toLowerCase().includes(userSearchQuery.toLowerCase())
|
|
).length === 0 && (
|
|
<p className="py-4 text-center text-sm text-muted-foreground">
|
|
{userSearchQuery ? 'Keine Benutzer gefunden' : 'Keine Benutzer vorhanden'}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</ScrollArea>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{showUserForm && (
|
|
<UserCreateForm
|
|
locations={locations}
|
|
onSuccess={() => {
|
|
setShowUserForm(false);
|
|
loadData();
|
|
}}
|
|
onCancel={() => setShowUserForm(false)}
|
|
showToast={showToast}
|
|
/>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|