Files
webshop/shop/app/admin/einstellungen/page.tsx
DanielS a9a5dd4e6f
Some checks failed
Staging Build / build (push) Failing after 25s
Cleanup admin settings page, remove duplicate imports and partner code
2026-06-24 23:37:40 +02:00

468 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Updated Admin Settings without Partner Management */
/* Admin Settings without Partner Management */
'use client';
import { useState, useEffect } from 'react';
import { Switch } from '@/components/ui/switch';
export default function AdminSettings() {
const [demoActive, setDemoActive] = useState(true);
useEffect(() => {
const state = localStorage.getItem('demo_banner_disabled') !== 'true';
setDemoActive(state);
}, []);
const toggleDemo = (checked: boolean) => {
setDemoActive(checked);
localStorage.setItem('demo_banner_disabled', (!checked).toString());
window.dispatchEvent(new Event('storage_demo_changed'));
};
return (
<div className="p-6 max-w-4xl mx-auto text-slate-900 dark:text-white space-y-8">
{/* Page Header */}
<div>
<h1 className="text-3xl font-extrabold tracking-tight">Admin Einstellungen</h1>
<p className="text-slate-500 dark:text-slate-400 text-sm mt-1">
Verwalten Sie globale Shopeinstellungen.
</p>
</div>
{/* Allgemeine Einstellungen */}
<div className="p-5 bg-white dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm space-y-4">
<h2 className="text-lg font-bold">Allgemeine Einstellungen</h2>
<div className="flex items-center justify-between p-4 bg-slate-50 dark:bg-white/5 rounded-lg border border-slate-100 dark:border-white/5">
<div>
<p className="font-medium text-sm text-slate-900 dark:text-white">Demo-Warnmeldungen</p>
<p className="text-xs text-slate-500 dark:text-slate-400">Schaltet gelbe Banner ein oder aus.</p>
</div>
<Switch checked={demoActive} onCheckedChange={toggleDemo} />
</div>
</div>
</div>
);
}
import { useState, useEffect } from 'react';
import { Switch } from '@/components/ui/switch';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
export default function AdminSettings() {
const [demoActive, setDemoActive] = useState(true);
useEffect(() => {
const state = localStorage.getItem('demo_banner_disabled') !== 'true';
setDemoActive(state);
}, []);
const toggleDemo = (checked: boolean) => {
setDemoActive(checked);
localStorage.setItem('demo_banner_disabled', (!checked).toString());
window.dispatchEvent(new Event('storage_demo_changed'));
};
return (
<div className="p-6 max-w-4xl mx-auto text-slate-900 dark:text-white space-y-8">
{/* Page Header */}
<div>
<h1 className="text-3xl font-extrabold tracking-tight">Admin Einstellungen</h1>
<p className="text-slate-500 dark:text-slate-400 text-sm mt-1">
Verwalten Sie globale Shopeinstellungen.
</p>
</div>
{/* Allgemeine Einstellungen */}
<div className="p-5 bg-white dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm space-y-4">
<h2 className="text-lg font-bold">Allgemeine Einstellungen</h2>
<div className="flex items-center justify-between p-4 bg-slate-50 dark:bg-white/5 rounded-lg border border-slate-100 dark:border-white/5">
<div>
<p className="font-medium text-sm text-slate-900 dark:text-white">Demo-Warnmeldungen</p>
<p className="text-xs text-slate-500 dark:text-slate-400">Schaltet gelbe Banner ein oder aus.</p>
</div>
<Switch checked={demoActive} onCheckedChange={toggleDemo} />
</div>
</div>
</div>
);
}
import { useState, useEffect } from 'react';
import { Switch } from '@/components/ui/switch';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { getPartners, createUser, updateUserRole } from '@/lib/actions/users';
import { getPartnerEndCustomers } from '@/lib/actions/end-customers';
import { Plus, Users, Mail, Eye, CheckCircle, ShieldAlert, Loader2, Ban, Unlock } from 'lucide-react';
export default function AdminSettings() {
const [demoActive, setDemoActive] = useState(true);
const [partners, setPartners] = useState<any[]>([]);
const [loadingPartners, setLoadingPartners] = useState(true);
// Modal States
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [isCustomersOpen, setIsCustomersOpen] = useState(false);
// Selected Partner / Customers State
const [selectedPartner, setSelectedPartner] = useState<any | null>(null);
const [partnerCustomers, setPartnerCustomers] = useState<any[]>([]);
const [loadingCustomers, setLoadingCustomers] = useState(false);
// Lock/Unlock State
const [lockingId, setLockingId] = useState<string | null>(null);
// Create Partner Form States
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [createError, setCreateError] = useState<string | null>(null);
const [creating, setCreating] = useState(false);
useEffect(() => {
// 1. Demo Banner Status auslesen
const state = localStorage.getItem('demo_banner_disabled') !== 'true';
setDemoActive(state);
// 2. Partner laden
loadPartners();
}, []);
const loadPartners = async () => {
setLoadingPartners(true);
try {
const data = await getPartners();
setPartners(data);
} catch (err) {
console.error('Fehler beim Laden der Partner:', err);
} finally {
setLoadingPartners(false);
}
};
const toggleDemo = (checked: boolean) => {
setDemoActive(checked);
localStorage.setItem('demo_banner_disabled', (!checked).toString());
window.dispatchEvent(new Event('storage_demo_changed'));
};
const handleCreatePartner = async (e: React.FormEvent) => {
e.preventDefault();
setCreating(true);
setCreateError(null);
try {
await createUser({
email,
password: password || undefined,
email_confirm: true,
});
setIsCreateOpen(false);
setEmail('');
setPassword('');
loadPartners();
} catch (err: any) {
console.error('Fehler beim Erstellen des Partners:', err);
setCreateError(err.message || 'Ein Fehler ist aufgetreten.');
} finally {
setCreating(false);
}
};
const handleToggleLock = async (partner: any) => {
const newRole = partner.role === 'gesperrt' ? 'partner' : 'gesperrt';
setLockingId(partner.id);
try {
await updateUserRole(partner.id, newRole);
await loadPartners();
} catch (err) {
console.error('Fehler beim Sperren/Entsperren:', err);
} finally {
setLockingId(null);
}
};
const handleShowCustomers = async (partner: any) => {
setSelectedPartner(partner);
setPartnerCustomers([]);
setLoadingCustomers(true);
setIsCustomersOpen(true);
try {
const customers = await getPartnerEndCustomers(partner.id);
setPartnerCustomers(customers);
} catch (err) {
console.error('Fehler beim Laden der Partner-Kunden:', err);
} finally {
setLoadingCustomers(false);
}
};
return (
<div className="p-6 max-w-4xl mx-auto text-slate-900 dark:text-white space-y-8">
{/* Page Header */}
<div>
<h1 className="text-3xl font-extrabold tracking-tight">Admin Einstellungen</h1>
<p className="text-slate-500 dark:text-slate-400 text-sm mt-1">
Verwalten Sie globale Shopeinstellungen, Partnerkonten und Kundenzuweisungen.
</p>
</div>
{/* Allgemeine Einstellungen */}
<div className="p-5 bg-white dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm space-y-4">
<h2 className="text-lg font-bold">Allgemeine Einstellungen</h2>
<div className="flex items-center justify-between p-4 bg-slate-50 dark:bg-white/5 rounded-lg border border-slate-100 dark:border-white/5">
<div>
<p className="font-medium text-sm text-slate-900 dark:text-white">Demo-Warnmeldungen</p>
<p className="text-xs text-slate-500 dark:text-slate-400">Schaltet gelbe Banner ein oder aus.</p>
</div>
<Switch checked={demoActive} onCheckedChange={toggleDemo} />
</div>
</div>
{/* Partnerverwaltung */}
<div className="p-5 bg-white dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm space-y-6">
<div className="flex items-center justify-between gap-4 flex-wrap">
<div className="flex items-center gap-2">
<Users className="w-5 h-5 text-blue-600 dark:text-blue-500" />
<h2 className="text-lg font-bold">Partner-Management</h2>
</div>
<Button onClick={() => setIsCreateOpen(true)} className="bg-blue-600 hover:bg-blue-700 text-white font-semibold">
<Plus className="w-4 h-4 mr-2" /> Partner hinzufügen
</Button>
</div>
{/* Partner Liste */}
{loadingPartners ? (
<div className="flex justify-center items-center py-12">
<Loader2 className="w-8 h-8 text-blue-600 dark:text-blue-500 animate-spin" />
<span className="ml-3 text-slate-500 text-sm">Lade Partner...</span>
</div>
) : partners.length === 0 ? (
<div className="text-center py-12 border-2 border-dashed border-slate-200 dark:border-white/5 rounded-xl text-slate-500">
Keine Partner registriert.
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead>
<tr className="border-b border-slate-200 dark:border-white/10 text-slate-400 text-xs font-semibold uppercase">
<th className="pb-3">Partner E-Mail</th>
<th className="pb-3">Firma</th>
<th className="pb-3">Status</th>
<th className="pb-3">Erstellt am</th>
<th className="pb-3 text-right">Aktionen</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 dark:divide-white/5">
{partners.map((partner) => {
const isGesperrt = partner.role === 'gesperrt';
const isLocking = lockingId === partner.id;
return (
<tr key={partner.id} className="hover:bg-slate-50 dark:hover:bg-white/3 transition-colors">
<td className="py-4 font-medium text-slate-900 dark:text-white">
<div className="flex items-center gap-2">
<Mail className="w-4 h-4 text-slate-400" />
{partner.email}
</div>
</td>
<td className="py-4 text-sm text-slate-700 dark:text-slate-300">
{partner.company_name || ''}
</td>
<td className="py-4">
{isGesperrt ? (
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-700 dark:bg-red-500/10 dark:text-red-400 border border-red-200 dark:border-red-500/20">
<Ban className="w-3.5 h-3.5" /> Gesperrt
</span>
) : (
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-500/10 dark:text-green-400 border border-green-200 dark:border-green-500/20">
<CheckCircle className="w-3.5 h-3.5" /> Aktiv
</span>
)}
</td>
<td className="py-4 text-sm text-slate-500 dark:text-slate-400">
{partner.created_at ? new Date(partner.created_at).toLocaleDateString('de-DE') : ''}
</td>
<td className="py-4 text-right">
<div className="flex items-center justify-end gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleShowCustomers(partner)}
className="border-slate-200 hover:bg-slate-100 dark:border-white/10 dark:hover:bg-white/5 gap-1.5"
>
<Eye className="w-4 h-4 text-blue-600 dark:text-blue-500" /> Kundenliste
</Button>
<Button
variant="outline"
size="sm"
disabled={isLocking}
onClick={() => handleToggleLock(partner)}
className={isGesperrt
? 'border-green-300 text-green-700 hover:bg-green-50 dark:border-green-500/30 dark:text-green-400 dark:hover:bg-green-500/10 gap-1.5'
: 'border-red-300 text-red-700 hover:bg-red-50 dark:border-red-500/30 dark:text-red-400 dark:hover:bg-red-500/10 gap-1.5'}
>
{isLocking
? <Loader2 className="w-4 h-4 animate-spin" />
: isGesperrt
? <><Unlock className="w-4 h-4" /> Entsperren</>
: <><Ban className="w-4 h-4" /> Sperren</>}
</Button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
{/* MODAL 1: Neuen Partner erstellen */}
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
<DialogContent className="bg-white dark:bg-slate-950 border-slate-200 dark:border-white/10 text-slate-900 dark:text-white shadow-2xl">
<DialogHeader>
<DialogTitle>Neuen Partner anlegen</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-slate-400">
Erstellt einen Partner-Zugang. Dieser kann sich mit den Daten im Shop einloggen.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleCreatePartner} className="space-y-4 mt-2">
<div className="space-y-2">
<Label htmlFor="email">E-Mail Adresse</Label>
<Input
id="email"
type="email"
required
placeholder="partner@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="bg-slate-50 dark:bg-white/5 border-slate-200 dark:border-white/10"
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Passwort (optional)</Label>
<Input
id="password"
type="password"
placeholder="Leer lassen für Zufallspasswort"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="bg-slate-50 dark:bg-white/5 border-slate-200 dark:border-white/10"
/>
</div>
{createError && (
<p className="text-sm text-red-600 dark:text-red-400 flex items-center gap-1.5">
<ShieldAlert className="w-4 h-4" /> {createError}
</p>
)}
<DialogFooter className="pt-2">
<Button type="button" variant="outline" onClick={() => setIsCreateOpen(false)} disabled={creating}>
Abbrechen
</Button>
<Button type="submit" disabled={creating} className="bg-blue-600 hover:bg-blue-700 text-white font-semibold">
{creating ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : null}
Partner erstellen
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
{/* MODAL 2: Kundenliste eines Partners anzeigen */}
<Dialog open={isCustomersOpen} onOpenChange={setIsCustomersOpen}>
<DialogContent className="bg-white dark:bg-slate-950 border-slate-200 dark:border-white/10 text-slate-900 dark:text-white shadow-2xl max-w-2xl max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Kunden von {selectedPartner?.email}</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-slate-400">
Liste aller angelegten Endkunden dieses Partners.
</DialogDescription>
</DialogHeader>
<div className="mt-4">
{loadingCustomers ? (
<div className="flex justify-center items-center py-12">
<Loader2 className="w-8 h-8 text-blue-600 dark:text-blue-500 animate-spin" />
<span className="ml-3 text-slate-500 text-sm">Lade Kunden...</span>
</div>
) : partnerCustomers.length === 0 ? (
<div className="text-center py-12 border border-dashed border-slate-200 dark:border-white/10 rounded-xl text-slate-500">
Dieser Partner hat noch keine Kunden angelegt.
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead>
<tr className="border-b border-slate-200 dark:border-white/10 text-slate-400 text-xs font-semibold uppercase">
<th className="pb-3">Unternehmen</th>
<th className="pb-3">Ansprechpartner</th>
<th className="pb-3">Ort</th>
<th className="pb-3">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 dark:divide-white/5">
{partnerCustomers.map((customer) => (
<tr key={customer.id} className="hover:bg-slate-50 dark:hover:bg-white/3 transition-colors">
<td className="py-4 font-semibold text-slate-900 dark:text-white">
{customer.company_name}
</td>
<td className="py-4 text-sm text-slate-700 dark:text-slate-300">
{[customer.first_name, customer.last_name].filter(Boolean).join(' ') || ''}
</td>
<td className="py-4 text-sm text-slate-500 dark:text-slate-400">
{[customer.zip, customer.city].filter(Boolean).join(' ') || ''}
</td>
<td className="py-4">
{customer.is_anonymized ? (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800 dark:bg-red-500/10 dark:text-red-400 border border-red-200 dark:border-red-500/20">
Anonymisiert
</span>
) : (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-500/10 dark:text-green-400 border border-green-200 dark:border-green-500/20">
Aktiv
</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
<DialogFooter className="pt-4 border-t border-slate-100 dark:border-white/5">
<Button type="button" onClick={() => setIsCustomersOpen(false)}>
Schließen
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}