Files
project_vollidioten_website/pages/Admin.tsx
Lars Behrends d3d7ec46e6 feat: Add DatabaseManager and LinkPlayer components, implement authentication and linking logic
- Created DatabaseManager component for managing database access via phpMyAdmin.
- Developed LinkPlayer component to link Discord accounts with game characters, including user authentication and error handling.
- Added mock data files for players, organizations, and projects to handle backend unavailability.
- Implemented AuthService for managing user authentication and session checks.
- Created DatabaseService to fetch and manage player, organization, and project data with fallback to mock data.
- Added HTML page for handling authentication unavailability.
- Developed a test script for validating Docker setup and required files.
2025-12-28 16:46:04 +01:00

1252 lines
48 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
import React, { useState, useEffect } from 'react';
import { Icons } from '../components/IconSet';
import { authService } from '../services/AuthService';
interface AdminPageProps {
onBack: () => void;
}
const EditNpcCitizenCard: React.FC<{ citizen: any; onUpdate: () => void; onError: (error: string) => void }> = ({ citizen, onUpdate, onError }) => {
const [isEditing, setIsEditing] = useState(false);
const [formData, setFormData] = useState({
username: citizen.username,
tags: citizen.tags.join(', '),
role: citizen.stats.role,
organizationId: citizen.stats.organizationId || '',
storyMarkdown: citizen.storyMarkdown || ''
});
const [loading, setLoading] = useState(false);
const handleSave = async () => {
try {
setLoading(true);
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/players/${citizen.uuid}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
tags: formData.tags.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0),
organizationId: formData.organizationId || null,
storyMarkdown: formData.storyMarkdown.trim() || null
})
});
if (response.ok) {
setIsEditing(false);
onUpdate();
} else {
const errorData = await response.json();
onError(errorData.error || response.json());
}
} catch (err) {
console.error('Error updating NPC citizen:', err);
onError('Netzwerkfehler');
} finally {
setLoading(false);
}
};
if (isEditing) {
return (
<div className="bg-surfaceHighlight/50 border border-border rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<Icons.Edit className="w-4 h-4 text-blue-400" />
<span className="font-medium text-white">{citizen.username} bearbeiten</span>
</div>
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-textMain mb-1">Tags</label>
<input
type="text"
value={formData.tags}
onChange={(e) => setFormData({...formData, tags: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-xs"
placeholder="#Bürger, #Händler"
/>
</div>
<div>
<label className="block text-xs font-medium text-textMain mb-1">Rolle</label>
<select
value={formData.role}
onChange={(e) => setFormData({...formData, role: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-xs"
>
<option value="Bürger">Bürger</option>
<option value="Händler">Händler</option>
<option value="Schmied">Schmied</option>
<option value="Bauer">Bauer</option>
<option value="Wächter">Wächter</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-textMain mb-1">Stadt</label>
<select
value={formData.organizationId}
onChange={(e) => setFormData({...formData, organizationId: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-xs"
>
<option value="">Keine Stadt</option>
<option value="org-3">Provisorium Null</option>
<option value="org-4">Sakura</option>
</select>
</div>
<div className="flex gap-2 pt-2">
<button
onClick={handleSave}
disabled={loading}
className="flex-1 bg-blue-500 hover:bg-blue-600 disabled:opacity-50 text-white px-3 py-1 rounded text-xs font-medium"
>
{loading ? '...' : 'Speichern'}
</button>
<button
onClick={() => setIsEditing(false)}
className="flex-1 bg-gray-500 hover:bg-gray-600 text-white px-3 py-1 rounded text-xs"
>
Abbrechen
</button>
</div>
</div>
</div>
);
}
return (
<div className="bg-surfaceHighlight/30 border border-border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-purple-500/20 rounded flex items-center justify-center text-xs font-bold text-purple-400">
NPC
</div>
<div>
<h4 className="font-medium text-white">{citizen.username}</h4>
<p className="text-xs text-textMuted">{citizen.stats.role}</p>
</div>
</div>
<button
onClick={() => setIsEditing(true)}
className="text-blue-400 hover:text-blue-300 text-sm"
>
<Icons.Edit className="w-4 h-4" />
</button>
</div>
<div className="text-xs text-textMuted">
Level {citizen.stats.level} {citizen.stats.playtimeHours}h Spielzeit
</div>
<div className="text-xs text-textMuted mt-1">
Tags: {citizen.tags.join(', ') || 'Keine'}
</div>
</div>
);
};
const ShopManagementModal: React.FC<{
isOpen: boolean;
onClose: () => void;
shopItems: any[];
onUpdate: (items: any[]) => void;
projectId: string;
onError: (error: string) => void;
}> = ({ isOpen, onClose, shopItems, onUpdate, projectId, onError }) => {
const [localItems, setLocalItems] = useState<any[]>(shopItems || []);
const [editingItem, setEditingItem] = useState<any>(null);
const [isAdding, setIsAdding] = useState(false);
const [loading, setLoading] = useState(false);
const resetForm = () => {
setEditingItem(null);
setIsAdding(false);
};
const handleSaveItem = async (itemData: any) => {
try {
setLoading(true);
let updatedItems = [...localItems];
if (editingItem) {
// Update existing item
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}/shop/${editingItem.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(itemData)
});
if (response.ok) {
const index = updatedItems.findIndex(item => item.id === editingItem.id);
if (index !== -1) {
updatedItems[index] = itemData;
}
} else {
throw new Error('Fehler beim Aktualisieren');
}
} else {
// Add new item
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}/shop`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(itemData)
});
if (response.ok) {
const newItem = await response.json();
updatedItems.push(newItem.item);
} else {
throw new Error('Fehler beim Hinzufügen');
}
}
setLocalItems(updatedItems);
onUpdate(updatedItems);
resetForm();
} catch (err) {
console.error('Error saving shop item:', err);
onError('Fehler beim Speichern des Artikels');
} finally {
setLoading(false);
}
};
const handleDeleteItem = async (itemId: string) => {
if (!confirm('Artikel wirklich löschen?')) return;
try {
setLoading(true);
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}/shop/${itemId}`, {
method: 'DELETE',
credentials: 'include'
});
if (response.ok) {
const updatedItems = localItems.filter(item => item.id !== itemId);
setLocalItems(updatedItems);
onUpdate(updatedItems);
} else {
throw new Error('Fehler beim Löschen');
}
} catch (err) {
console.error('Error deleting shop item:', err);
onError('Fehler beim Löschen des Artikels');
} finally {
setLoading(false);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-surface border border-border rounded-xl max-w-4xl w-full max-h-[80vh] overflow-hidden">
<div className="p-6 border-b border-border">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold text-white flex items-center gap-2">
<Icons.ShoppingBag className="w-5 h-5 text-accentInfo" />
Shop-Verwaltung
</h2>
<button onClick={onClose} className="text-textMuted hover:text-white">
×
</button>
</div>
</div>
<div className="p-6 overflow-y-auto max-h-[60vh]">
<div className="flex justify-between items-center mb-6">
<div>
<h3 className="font-semibold text-white">Shop-Artikel ({localItems.length})</h3>
<p className="text-xs text-textMuted">Verwalte die Produkte dieses Shops</p>
</div>
<button
onClick={() => setIsAdding(true)}
className="bg-accentInfo hover:bg-accentInfo/80 text-white px-4 py-2 rounded text-sm font-medium flex items-center gap-2"
>
+
Artikel hinzufügen
</button>
</div>
{localItems.length === 0 ? (
<div className="text-center py-12 text-textMuted">
<Icons.ShoppingBag className="w-12 h-12 mx-auto mb-4 opacity-20" />
<p>Noch keine Artikel im Shop.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
{localItems.map((item) => (
<div key={item.id} className="bg-surfaceHighlight/30 border border-border rounded-lg p-4">
<div className="flex justify-between items-start mb-3">
<div>
<h4 className="font-medium text-white">{item.name}</h4>
<p className="text-xs text-textMuted">{item.description}</p>
</div>
<div className="flex gap-2">
<button
onClick={() => setEditingItem(item)}
className="text-blue-400 hover:text-blue-300 text-sm"
>
<Icons.Edit className="w-4 h-4" />
</button>
<button
onClick={() => handleDeleteItem(item.id)}
className="text-red-400 hover:text-red-300 text-sm"
>
🗑
</button>
</div>
</div>
<div className="flex justify-between items-center text-xs">
<span className="text-accentInfo font-medium">
{item.price} {item.currency}
</span>
<span className="text-textMuted">
Lager: {item.stock}
</span>
</div>
</div>
))}
</div>
)}
{(editingItem || isAdding) && (
<ShopItemForm
item={editingItem}
onSave={handleSaveItem}
onCancel={resetForm}
loading={loading}
/>
)}
</div>
</div>
</div>
);
};
const ShopItemForm: React.FC<{
item?: any;
onSave: (itemData: any) => void;
onCancel: () => void;
loading: boolean;
}> = ({ item, onSave, onCancel, loading }) => {
const [formData, setFormData] = useState({
name: item?.name || '',
description: item?.description || '',
price: item?.price || '',
currency: item?.currency || 'Gold',
stock: item?.stock || '',
type: item?.type || 'item'
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSave({
...formData,
price: parseFloat(formData.price),
stock: parseInt(formData.stock),
id: item?.id // Keep existing ID for updates
});
};
return (
<div className="bg-surfaceHighlight/50 border border-border rounded-lg p-4">
<h4 className="font-medium text-white mb-4">
{item ? 'Artikel bearbeiten' : 'Neuer Artikel'}
</h4>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-textMain mb-1">Name *</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
required
/>
</div>
<div>
<label className="block text-xs font-medium text-textMain mb-1">Typ</label>
<select
value={formData.type}
onChange={(e) => setFormData({...formData, type: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
>
<option value="item">Artikel</option>
<option value="service">Dienstleistung</option>
<option value="resource">Ressource</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-textMain mb-1">Preis *</label>
<input
type="number"
step="0.01"
value={formData.price}
onChange={(e) => setFormData({...formData, price: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
required
/>
</div>
<div>
<label className="block text-xs font-medium text-textMain mb-1">Währung</label>
<select
value={formData.currency}
onChange={(e) => setFormData({...formData, currency: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
>
<option value="Gold">Gold</option>
<option value="Diamanten">Diamanten</option>
<option value="Eisen">Eisen</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-textMain mb-1">Lagerbestand *</label>
<input
type="number"
value={formData.stock}
onChange={(e) => setFormData({...formData, stock: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
required
/>
</div>
</div>
<div>
<label className="block text-xs font-medium text-textMain mb-1">Beschreibung</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({...formData, description: e.target.value})}
className="w-full h-20 bg-[#0b0b0d] border border-border rounded p-2 text-sm"
/>
</div>
<div className="flex gap-2 pt-2">
<button
type="submit"
disabled={loading}
className="flex-1 bg-accentInfo hover:bg-accentInfo/80 disabled:opacity-50 text-white px-3 py-2 rounded text-sm font-medium"
>
{loading ? '...' : (item ? 'Speichern' : 'Hinzufügen')}
</button>
<button
type="button"
onClick={onCancel}
className="flex-1 bg-gray-500 hover:bg-gray-600 text-white px-3 py-1 rounded text-sm"
>
Abbrechen
</button>
</div>
</form>
</div>
);
};
const EditNpcCompanyCard: React.FC<{ company: any; npcCitizens: any[]; onUpdate: () => void; onError: (error: string) => void; onOpenShopModal: (projectId: string, shopItems: any[]) => void }> = ({ company, npcCitizens, onUpdate, onError, onOpenShopModal }) => {
const [isEditing, setIsEditing] = useState(false);
const [isEditingShop, setIsEditingShop] = useState(false);
const [isManaging, setIsManaging] = useState(false);
const [shopItems, setShopItems] = useState<any[]>(company.shopCatalog || []);
const [formData, setFormData] = useState({
title: company.title,
description: company.description || '',
category: company.category,
owner: company.owner,
associatedOrgId: company.associatedOrgId || '',
shopItems: JSON.stringify(company.shopCatalog || [], null, 2)
});
const [loading, setLoading] = useState(false);
const handleShopUpdate = (updatedItems: any[]) => {
setShopItems(updatedItems);
setFormData(prev => ({
...prev,
shopItems: JSON.stringify(updatedItems, null, 2)
}));
};
const handleSave = async () => {
try {
setLoading(true);
// Parse shop items
let shopCatalog = [];
if (formData.shopItems.trim()) {
try {
shopCatalog = JSON.parse(formData.shopItems);
} catch (e) {
onError('Ungültiges JSON-Format für Shop-Artikel');
setLoading(false);
return;
}
}
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${company.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
title: formData.title,
description: formData.description,
category: formData.category,
associatedOrgId: formData.associatedOrgId || null,
shopCatalog: shopCatalog
})
});
if (response.ok) {
setIsEditing(false);
onUpdate();
} else {
const errorData = await response.json();
onError(errorData.error || 'Fehler beim Aktualisieren');
}
} catch (err) {
console.error('Error updating NPC company:', err);
onError('Netzwerkfehler');
} finally {
setLoading(false);
}
};
if (isEditing) {
return (
<div className="bg-surfaceHighlight/50 border border-border rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<Icons.Edit className="w-4 h-4 text-purple-400" />
<span className="font-medium text-white">{company.title} bearbeiten</span>
</div>
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-textMain mb-1">Name</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({...formData, title: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-xs"
/>
</div>
<div>
<label className="block text-xs font-medium text-textMain mb-1">Kategorie</label>
<select
value={formData.category}
onChange={(e) => setFormData({...formData, category: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-xs"
>
<option value="Enterprise">Enterprise</option>
<option value="Service">Service</option>
<option value="Story Arc">Story Arc</option>
<option value="Black Market">Black Market</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-textMain mb-1">Eigentümer</label>
<select
value={formData.owner}
onChange={(e) => setFormData({...formData, owner: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-xs"
>
{npcCitizens.map((citizen: any) => (
<option key={citizen.uuid} value={citizen.username}>
{citizen.username}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-medium text-textMain mb-1">Stadt</label>
<select
value={formData.associatedOrgId}
onChange={(e) => setFormData({...formData, associatedOrgId: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-xs"
>
<option value="">Keine Stadt</option>
<option value="org-3">Provisorium Null</option>
<option value="org-4">Sakura</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-textMain mb-1">Beschreibung</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({...formData, description: e.target.value})}
className="w-full h-16 bg-[#0b0b0d] border border-border rounded p-2 text-xs"
/>
</div>
<div className="flex gap-2 pt-2">
<button
onClick={handleSave}
disabled={loading}
className="flex-1 bg-purple-500 hover:bg-purple-600 disabled:opacity-50 text-white px-3 py-1 rounded text-xs font-medium"
>
{loading ? '...' : 'Speichern'}
</button>
<button
onClick={() => setIsEditing(false)}
className="flex-1 bg-gray-500 hover:bg-gray-600 text-white px-3 py-1 rounded text-xs"
>
Abbrechen
</button>
</div>
</div>
</div>
);
}
return (
<div className="bg-surfaceHighlight/30 border border-border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-purple-500/20 rounded flex items-center justify-center text-xs font-bold text-purple-400">
NPC
</div>
<div>
<h4 className="font-medium text-white">{company.title}</h4>
<p className="text-xs text-textMuted">{company.category}</p>
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => onOpenShopModal(company.id, company.shopCatalog || [])}
className="text-accentInfo hover:text-accentInfo/80 text-sm"
title="Shop verwalten"
>
<Icons.ShoppingBag className="w-4 h-4" />
</button>
<button
onClick={() => setIsEditing(true)}
className="text-purple-400 hover:text-purple-300 text-sm"
>
<Icons.Edit className="w-4 h-4" />
</button>
</div>
</div>
<div className="text-xs text-textMuted">
Eigentümer: {company.owner}
</div>
{company.shopCatalog && company.shopCatalog.length > 0 && (
<div className="text-xs text-accentInfo mt-1">
Shop: {company.shopCatalog.length} Artikel
</div>
)}
</div>
);
};
const AdminPage: React.FC<AdminPageProps> = ({ onBack }) => {
const [user, setUser] = useState(authService.getUser());
const [activeTab, setActiveTab] = useState<'overview' | 'npcs' | 'create-npc' | 'edit-npcs' | 'manage-admins'>('overview');
const [npcs, setNpcs] = useState<any>({ citizens: [], companies: [] });
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [shopModalState, setShopModalState] = useState<{
isOpen: boolean;
projectId: string;
shopItems: any[];
}>({ isOpen: false, projectId: '', shopItems: [] });
// Check if user is admin
const isAdmin = user?.isAdmin;
useEffect(() => {
const unsub = authService.subscribe(setUser);
return unsub;
}, []);
useEffect(() => {
if (activeTab === 'npcs' && isAdmin) {
loadNpcs();
}
}, [activeTab, isAdmin]);
const loadNpcs = async () => {
try {
setLoading(true);
const response = await fetch('https://vollidioten.ceraticsoft.de/api/admin/npcs', {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
setNpcs(data);
} else {
setError('Fehler beim Laden der NPCs');
}
} catch (err) {
console.error('Error loading NPCs:', err);
setError('Netzwerkfehler');
} finally {
setLoading(false);
}
};
// NPC Creation Forms
const [citizenForm, setCitizenForm] = useState({
username: '',
tags: '',
role: 'Bürger',
organizationId: '',
storyMarkdown: ''
});
const [companyForm, setCompanyForm] = useState({
title: '',
description: '',
category: 'Enterprise',
owner: '',
associatedOrgId: '',
shopItems: ''
});
const createNpcCitizen = async () => {
if (!citizenForm.username.trim()) {
setError('NPC-Name ist erforderlich');
return;
}
try {
setLoading(true);
setError(null);
const response = await fetch('https://vollidioten.ceraticsoft.de/api/admin/npc-citizen', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
username: citizenForm.username.trim(),
tags: citizenForm.tags.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0),
role: citizenForm.role,
organizationId: citizenForm.organizationId || null,
storyMarkdown: citizenForm.storyMarkdown.trim() || undefined
})
});
if (response.ok) {
alert('NPC-Bürger erfolgreich erstellt!');
setCitizenForm({
username: '',
tags: '',
role: 'Bürger',
organizationId: '',
storyMarkdown: ''
});
if (activeTab === 'npcs') loadNpcs();
} else {
const errorData = await response.json();
setError(errorData.error || 'Fehler beim Erstellen');
}
} catch (err) {
console.error('Error creating NPC citizen:', err);
setError('Netzwerkfehler');
} finally {
setLoading(false);
}
};
const createNpcCompany = async () => {
if (!companyForm.title.trim() || !companyForm.owner.trim()) {
setError('Titel und NPC-Eigentümer sind erforderlich');
return;
}
try {
setLoading(true);
setError(null);
// Parse shop items if provided
let shopCatalog = [];
if (companyForm.shopItems.trim()) {
try {
shopCatalog = JSON.parse(companyForm.shopItems);
} catch (e) {
setError('Ungültiges JSON-Format für Shop-Artikel');
setLoading(false);
return;
}
}
const response = await fetch('https://vollidioten.ceraticsoft.de/api/admin/npc-company', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
title: companyForm.title.trim(),
description: companyForm.description.trim() || undefined,
category: companyForm.category,
owner: companyForm.owner.trim(),
associatedOrgId: companyForm.associatedOrgId || null,
shopCatalog: shopCatalog
})
});
if (response.ok) {
alert('NPC-Firma erfolgreich erstellt!');
setCompanyForm({
title: '',
description: '',
category: 'Enterprise',
owner: '',
associatedOrgId: '',
shopItems: ''
});
if (activeTab === 'npcs') loadNpcs();
} else {
const errorData = await response.json();
setError(errorData.error || 'Fehler beim Erstellen');
}
} catch (err) {
console.error('Error creating NPC company:', err);
setError('Netzwerkfehler');
} finally {
setLoading(false);
}
};
if (!isAdmin) {
return (
<div className="max-w-4xl mx-auto animate-in slide-in-from-right-4 duration-300">
<div className="flex justify-between items-center mb-6">
<button onClick={onBack} className="flex items-center gap-2 text-sm text-textMuted hover:text-textMain transition-colors">
<span className="text-lg"></span> Zurück
</button>
</div>
<div className="bg-surface border border-border rounded-xl p-8 text-center">
<Icons.Shield className="w-16 h-16 text-red-400 mx-auto mb-4" />
<h2 className="text-2xl font-bold text-white mb-2">Admin-Berechtigung erforderlich</h2>
<p className="text-textMuted">Sie haben keine Berechtigung, auf die Admin-Funktionen zuzugreifen.</p>
</div>
</div>
);
}
return (
<div className="max-w-6xl mx-auto animate-in slide-in-from-right-4 duration-300">
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-3xl font-bold mb-1">Admin-Panel</h1>
<p className="text-textMuted">Verwaltung von NPCs und System-Einstellungen</p>
</div>
<button onClick={onBack} className="flex items-center gap-2 text-sm text-textMuted hover:text-textMain transition-colors">
<span className="text-lg"></span> Zurück zur Übersicht
</button>
</div>
{/* Navigation Tabs */}
<div className="flex gap-1 border-b border-border mb-8 overflow-x-auto">
<button
onClick={() => setActiveTab('overview')}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${activeTab === 'overview' ? 'border-accentInfo text-white' : 'border-transparent text-textMuted hover:text-white'}`}
>
Übersicht
</button>
<button
onClick={() => setActiveTab('npcs')}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${activeTab === 'npcs' ? 'border-accentInfo text-white' : 'border-transparent text-textMuted hover:text-white'}`}
>
NPCs verwalten ({npcs.citizens.length + npcs.companies.length})
</button>
<button
onClick={() => setActiveTab('edit-npcs')}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${activeTab === 'edit-npcs' ? 'border-accentInfo text-white' : 'border-transparent text-textMuted hover:text-white'}`}
>
NPCs bearbeiten
</button>
<button
onClick={() => setActiveTab('create-npc')}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${activeTab === 'create-npc' ? 'border-accentInfo text-white' : 'border-transparent text-textMuted hover:text-white'}`}
>
NPCs erstellen
</button>
</div>
{/* Error Display */}
{error && (
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-4 mb-6">
<p className="text-red-400">{error}</p>
</div>
)}
{/* Content */}
<div className="space-y-8">
{activeTab === 'overview' && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="bg-surface border border-border rounded-xl p-6">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 bg-blue-500/10 rounded text-blue-400">
<Icons.Users className="w-5 h-5" />
</div>
<div>
<h3 className="font-semibold text-white">NPC-Bürger</h3>
<p className="text-xs text-textMuted">Erstellte NPCs</p>
</div>
</div>
<div className="text-3xl font-bold text-white">{npcs.citizens.length}</div>
</div>
<div className="bg-surface border border-border rounded-xl p-6">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 bg-purple-500/10 rounded text-purple-400">
<Icons.ShoppingBag className="w-5 h-5" />
</div>
<div>
<h3 className="font-semibold text-white">NPC-Firmen</h3>
<p className="text-xs text-textMuted">Erstellte Unternehmen</p>
</div>
</div>
<div className="text-3xl font-bold text-white">{npcs.companies.length}</div>
</div>
<div className="bg-surface border border-border rounded-xl p-6">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 bg-green-500/10 rounded text-green-400">
<Icons.Shield className="w-5 h-5" />
</div>
<div>
<h3 className="font-semibold text-white">System-Status</h3>
<p className="text-xs text-textMuted">Admin-Berechtigung</p>
</div>
</div>
<div className="text-lg font-bold text-green-400">Aktiv</div>
</div>
</div>
)}
{activeTab === 'npcs' && (
<div className="space-y-8">
{/* NPC Citizens */}
<div className="bg-surface border border-border rounded-xl p-6">
<h3 className="text-xl font-bold mb-4 flex items-center gap-2">
<Icons.Users className="w-5 h-5 text-blue-400" />
NPC-Bürger ({npcs.citizens.length})
</h3>
{loading ? (
<div className="flex justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-accentInfo"></div>
</div>
) : npcs.citizens.length === 0 ? (
<p className="text-textMuted">Keine NPC-Bürger vorhanden.</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{npcs.citizens.map((citizen: any) => (
<div key={citizen.uuid} className="bg-surfaceHighlight/30 border border-border rounded-lg p-4">
<div className="flex items-center gap-3 mb-2">
<div className="w-8 h-8 bg-purple-500/20 rounded flex items-center justify-center text-xs font-bold text-purple-400">
NPC
</div>
<div>
<h4 className="font-medium text-white">{citizen.username}</h4>
<p className="text-xs text-textMuted">{citizen.stats.role}</p>
</div>
</div>
<div className="text-xs text-textMuted">
Level {citizen.stats.level} {citizen.stats.playtimeHours}h Spielzeit
</div>
</div>
))}
</div>
)}
</div>
{/* NPC Companies */}
<div className="bg-surface border border-border rounded-xl p-6">
<h3 className="text-xl font-bold mb-4 flex items-center gap-2">
<Icons.ShoppingBag className="w-5 h-5 text-purple-400" />
NPC-Firmen ({npcs.companies.length})
</h3>
{npcs.companies.length === 0 ? (
<p className="text-textMuted">Keine NPC-Firmen vorhanden.</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{npcs.companies.map((company: any) => (
<div key={company.id} className="bg-surfaceHighlight/30 border border-border rounded-lg p-4">
<div className="flex justify-between items-start mb-2">
<h4 className="font-medium text-white">{company.title}</h4>
<span className="text-xs px-2 py-1 bg-purple-500/20 text-purple-400 rounded">
{company.category}
</span>
</div>
<p className="text-sm text-textMuted mb-2">{company.description}</p>
<div className="text-xs text-textMuted">
Eigentümer: {company.owner} Gegründet: {company.foundedDate}
</div>
{company.shopCatalog && company.shopCatalog.length > 0 && (
<div className="mt-2 text-xs text-accentInfo">
Shop: {company.shopCatalog.length} Artikel
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
)}
{activeTab === 'edit-npcs' && (
<div className="space-y-8">
{/* Edit NPC Citizens */}
<div className="bg-surface border border-border rounded-xl p-6">
<h3 className="text-xl font-bold mb-4 flex items-center gap-2">
<Icons.Edit className="w-5 h-5 text-blue-400" />
NPC-Bürger bearbeiten
</h3>
{npcs.citizens.length === 0 ? (
<p className="text-textMuted">Keine NPC-Bürger vorhanden.</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{npcs.citizens.map((citizen: any) => (
<EditNpcCitizenCard
key={citizen.uuid}
citizen={citizen}
onUpdate={() => loadNpcs()}
onError={setError}
/>
))}
</div>
)}
</div>
{/* Edit NPC Companies */}
<div className="bg-surface border border-border rounded-xl p-6">
<h3 className="text-xl font-bold mb-4 flex items-center gap-2">
<Icons.Edit className="w-5 h-5 text-purple-400" />
NPC-Firmen bearbeiten
</h3>
{npcs.companies.length === 0 ? (
<p className="text-textMuted">Keine NPC-Firmen vorhanden.</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{npcs.companies.map((company: any) => (
<EditNpcCompanyCard
key={company.id}
company={company}
npcCitizens={npcs.citizens}
onUpdate={() => loadNpcs()}
onError={setError}
onOpenShopModal={(projectId, shopItems) =>
setShopModalState({ isOpen: true, projectId, shopItems })
}
/>
))}
</div>
)}
</div>
</div>
)}
{activeTab === 'create-npc' && (
<div className="space-y-8">
{/* Create NPC Citizen */}
<div className="bg-surface border border-border rounded-xl p-6">
<h3 className="text-xl font-bold mb-4 flex items-center gap-2">
<Icons.Users className="w-5 h-5 text-blue-400" />
NPC-Bürger erstellen
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-textMain mb-1">Name *</label>
<input
type="text"
value={citizenForm.username}
onChange={(e) => setCitizenForm({...citizenForm, username: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
placeholder="z.B. Händler Karl"
/>
</div>
<div>
<label className="block text-sm font-medium text-textMain mb-1">Rolle</label>
<select
value={citizenForm.role}
onChange={(e) => setCitizenForm({...citizenForm, role: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
>
<option value="Bürger">Bürger</option>
<option value="Händler">Händler</option>
<option value="Schmied">Schmied</option>
<option value="Bauer">Bauer</option>
<option value="Wächter">Wächter</option>
</select>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-textMain mb-1">Tags (komma-getrennt)</label>
<input
type="text"
value={citizenForm.tags}
onChange={(e) => setCitizenForm({...citizenForm, tags: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
placeholder="z.B. #Bürger, #Händler, #Fleischer"
/>
</div>
<div>
<label className="block text-sm font-medium text-textMain mb-1">Stadt-Zugehörigkeit</label>
<select
value={citizenForm.organizationId}
onChange={(e) => setCitizenForm({...citizenForm, organizationId: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
>
<option value="">Keine Stadt</option>
<option value="org-3">Provisorium Null</option>
<option value="org-4">Sakura</option>
</select>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-textMain mb-1">Charakter-Beschreibung</label>
<textarea
value={citizenForm.storyMarkdown}
onChange={(e) => setCitizenForm({...citizenForm, storyMarkdown: e.target.value})}
className="w-full h-20 bg-[#0b0b0d] border border-border rounded p-2 text-sm"
placeholder="Kurze Beschreibung des NPCs..."
/>
</div>
<div className="md:col-span-2 pt-2">
<button
onClick={createNpcCitizen}
disabled={loading || !citizenForm.username.trim()}
className="w-full bg-blue-500 hover:bg-blue-600 disabled:opacity-50 text-white px-4 py-2 rounded text-sm font-medium disabled:cursor-not-allowed"
>
{loading ? 'Erstelle...' : 'NPC-Bürger erstellen'}
</button>
</div>
</div>
</div>
{/* Create NPC Company */}
<div className="bg-surface border border-border rounded-xl p-6">
<h3 className="text-xl font-bold mb-4 flex items-center gap-2">
<Icons.ShoppingBag className="w-5 h-5 text-purple-400" />
NPC-Firma erstellen
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-textMain mb-1">Firmenname *</label>
<input
type="text"
value={companyForm.title}
onChange={(e) => setCompanyForm({...companyForm, title: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
placeholder="z.B. Karls Fleischerei"
/>
</div>
<div>
<label className="block text-sm font-medium text-textMain mb-1">Kategorie</label>
<select
value={companyForm.category}
onChange={(e) => setCompanyForm({...companyForm, category: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
>
<option value="Enterprise">Enterprise</option>
<option value="Service">Service</option>
<option value="Story Arc">Story Arc</option>
<option value="Black Market">Black Market</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-textMain mb-1">NPC-Eigentümer *</label>
<select
value={companyForm.owner}
onChange={(e) => setCompanyForm({...companyForm, owner: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
>
<option value="">NPC auswählen...</option>
{npcs.citizens.map((citizen: any) => (
<option key={citizen.uuid} value={citizen.username}>
{citizen.username} ({citizen.stats.role})
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-textMain mb-1">Stadt-Zugehörigkeit</label>
<select
value={companyForm.associatedOrgId}
onChange={(e) => setCompanyForm({...companyForm, associatedOrgId: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
>
<option value="">Keine Stadt</option>
<option value="org-3">Provisorium Null</option>
<option value="org-4">Sakura</option>
</select>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-textMain mb-1">Beschreibung</label>
<textarea
value={companyForm.description}
onChange={(e) => setCompanyForm({...companyForm, description: e.target.value})}
className="w-full h-20 bg-[#0b0b0d] border border-border rounded p-2 text-sm"
placeholder="Beschreibung der NPC-Firma..."
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-textMain mb-1">Shop-Artikel (JSON-Format)</label>
<textarea
value={companyForm.shopItems}
onChange={(e) => setCompanyForm({...companyForm, shopItems: e.target.value})}
className="w-full h-20 bg-[#0b0b0d] border border-border rounded p-2 text-sm font-mono text-xs"
placeholder='[{"name": "Steak", "description": "Frisches Fleisch", "price": 5, "currency": "Gold", "stock": 10, "type": "item"}]'
/>
<p className="text-xs text-textMuted mt-1">Optional: JSON-Array mit Shop-Artikeln</p>
</div>
<div className="md:col-span-2 pt-2">
<button
onClick={createNpcCompany}
disabled={loading || !companyForm.title.trim() || !companyForm.owner.trim()}
className="w-full bg-purple-500 hover:bg-purple-600 disabled:opacity-50 text-white px-4 py-2 rounded text-sm font-medium disabled:cursor-not-allowed"
>
{loading ? 'Erstelle...' : 'NPC-Firma erstellen'}
</button>
</div>
</div>
</div>
</div>
)}
{/* Shop Management Modal */}
<ShopManagementModal
isOpen={shopModalState.isOpen}
onClose={() => setShopModalState({ isOpen: false, projectId: '', shopItems: [] })}
shopItems={shopModalState.shopItems}
onUpdate={(updatedItems) => {
setShopModalState(prev => ({ ...prev, shopItems: updatedItems }));
// Trigger a reload to show updated data
loadNpcs();
}}
projectId={shopModalState.projectId}
onError={setError}
/>
</div>
</div>
);
};
export default AdminPage;