mirror of
https://github.com/ceratic/project_vollidioten_website.git
synced 2026-05-14 00:16:47 +02:00
1895 lines
77 KiB
TypeScript
1895 lines
77 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import { Icons } from '../components/IconSet';
|
||
import { authService } from '../services/AuthService';
|
||
import NpcBannerManagementModal from '../components/NpcBannerManagementModal';
|
||
import NpcLogoManagementModal from '../components/NpcLogoManagementModal';
|
||
import NpcGalleryManagementModal from '../components/NpcGalleryManagementModal';
|
||
|
||
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 [bannerModalOpen, setBannerModalOpen] = useState(false);
|
||
const [logoModalOpen, setLogoModalOpen] = useState(false);
|
||
const [galleryModalOpen, setGalleryModalOpen] = useState(false);
|
||
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={() => setBannerModalOpen(true)}
|
||
className="text-blue-400 hover:text-blue-300 text-sm"
|
||
title="Banner bearbeiten"
|
||
>
|
||
<Icons.Layers className="w-4 h-4" />
|
||
</button>
|
||
<button
|
||
onClick={() => setLogoModalOpen(true)}
|
||
className="text-green-400 hover:text-green-300 text-sm"
|
||
title="Logo bearbeiten"
|
||
>
|
||
<Icons.Shield className="w-4 h-4" />
|
||
</button>
|
||
<button
|
||
onClick={() => setGalleryModalOpen(true)}
|
||
className="text-orange-400 hover:text-orange-300 text-sm"
|
||
title="Portfolio verwalten"
|
||
>
|
||
<Icons.Box className="w-4 h-4" />
|
||
</button>
|
||
<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>
|
||
|
||
{/* Image Management Modals */}
|
||
<NpcBannerManagementModal
|
||
isOpen={bannerModalOpen}
|
||
onClose={() => setBannerModalOpen(false)}
|
||
projectId={company.id}
|
||
currentBannerUrl={company.bannerUrl}
|
||
onUpdate={() => onUpdate()}
|
||
/>
|
||
|
||
<NpcLogoManagementModal
|
||
isOpen={logoModalOpen}
|
||
onClose={() => setLogoModalOpen(false)}
|
||
projectId={company.id}
|
||
currentLogoUrl={company.logoUrl}
|
||
onUpdate={() => onUpdate()}
|
||
/>
|
||
|
||
<NpcGalleryManagementModal
|
||
isOpen={galleryModalOpen}
|
||
onClose={() => setGalleryModalOpen(false)}
|
||
projectId={company.id}
|
||
onUpdate={() => onUpdate()}
|
||
/>
|
||
</>
|
||
);
|
||
};
|
||
|
||
const AdminPage: React.FC<AdminPageProps> = ({ onBack }) => {
|
||
const [user, setUser] = useState(authService.getUser());
|
||
const [activeTab, setActiveTab] = useState<'overview' | 'create-npc' | 'edit-npcs' | 'cities' | 'create-city' | 'manage-admins'>('overview');
|
||
const [npcs, setNpcs] = useState<any>({ citizens: [], companies: [] });
|
||
const [cities, setCities] = useState<any[]>([]);
|
||
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(() => {
|
||
loadNpcs();
|
||
loadCities();
|
||
}, [isAdmin]);
|
||
|
||
// Auto-refresh data every 30 seconds when on relevant tabs
|
||
useEffect(() => {
|
||
if (!isAdmin) return;
|
||
|
||
const interval = setInterval(() => {
|
||
if (activeTab === 'edit-npcs' || activeTab === 'create-npc') {
|
||
loadNpcs();
|
||
} else if (activeTab === 'cities' || activeTab === 'create-city') {
|
||
loadCities();
|
||
}
|
||
}, 30000); // 30 seconds
|
||
|
||
return () => clearInterval(interval);
|
||
}, [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);
|
||
}
|
||
};
|
||
|
||
const loadCities = async () => {
|
||
try {
|
||
setLoading(true);
|
||
const response = await fetch('https://vollidioten.ceraticsoft.de/api/orgs', {
|
||
credentials: 'include'
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
// Filter only cities (type === 'City')
|
||
const cityData = data.filter((org: any) => org.type === 'City');
|
||
setCities(cityData);
|
||
} else {
|
||
setError('Fehler beim Laden der Städte');
|
||
}
|
||
} catch (err) {
|
||
console.error('Error loading cities:', 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: ''
|
||
});
|
||
|
||
// City management
|
||
const [cityForm, setCityForm] = useState({
|
||
name: '',
|
||
description: '',
|
||
mayor: '',
|
||
establishedYear: '',
|
||
cityStats: JSON.stringify({
|
||
taxRate: 5.0,
|
||
biome: 'Ebene',
|
||
defenseRating: 5,
|
||
government: 'Demokratie',
|
||
specialty: 'Handel'
|
||
}, null, 2)
|
||
});
|
||
|
||
const [editingCity, setEditingCity] = useState<any>(null);
|
||
const [editCityForm, setEditCityForm] = useState({
|
||
name: '',
|
||
description: '',
|
||
mayor: '',
|
||
establishedYear: '',
|
||
cityStats: ''
|
||
});
|
||
|
||
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('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 verwalten ({npcs.citizens.length + npcs.companies.length})
|
||
</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>
|
||
<button
|
||
onClick={() => setActiveTab('cities')}
|
||
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${activeTab === 'cities' ? 'border-accentInfo text-white' : 'border-transparent text-textMuted hover:text-white'}`}
|
||
>
|
||
Städte verwalten
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('create-city')}
|
||
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${activeTab === 'create-city' ? 'border-accentInfo text-white' : 'border-transparent text-textMuted hover:text-white'}`}
|
||
>
|
||
Stadt 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 === '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>
|
||
)}
|
||
|
||
{activeTab === 'cities' && (
|
||
<div className="space-y-8">
|
||
{/* Cities Management */}
|
||
<div className="bg-surface border border-border rounded-xl p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h3 className="text-xl font-bold flex items-center gap-2">
|
||
<Icons.Map className="w-5 h-5 text-green-400" />
|
||
Städte verwalten ({cities.length})
|
||
</h3>
|
||
<button
|
||
onClick={() => loadCities()}
|
||
disabled={loading}
|
||
className="text-accentInfo hover:text-accentInfo/80 text-sm disabled:opacity-50"
|
||
title="Aktualisieren"
|
||
>
|
||
↻ {loading ? 'Lade...' : 'Aktualisieren'}
|
||
</button>
|
||
</div>
|
||
|
||
{cities.length === 0 ? (
|
||
<p className="text-textMuted">Keine Städte vorhanden.</p>
|
||
) : (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
{cities.map((city: any) => (
|
||
<div key={city.id} 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">
|
||
{city.logoUrl ? (
|
||
<img
|
||
src={city.logoUrl}
|
||
alt={`${city.name} Logo`}
|
||
className="w-8 h-8 rounded border border-white/10 object-cover"
|
||
/>
|
||
) : (
|
||
<div className="w-8 h-8 bg-green-500/20 rounded flex items-center justify-center text-xs font-bold text-green-400">
|
||
{city.name.charAt(0)}
|
||
</div>
|
||
)}
|
||
<div>
|
||
<h4 className="font-medium text-white">{city.name}</h4>
|
||
<p className="text-xs text-textMuted">Gegr. {city.establishedYear}</p>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => {
|
||
setEditingCity(city);
|
||
setEditCityForm({
|
||
name: city.name,
|
||
description: city.description || '',
|
||
mayor: city.mayor || '',
|
||
establishedYear: city.establishedYear || '',
|
||
cityStats: JSON.stringify(city.cityStats || {}, null, 2)
|
||
});
|
||
}}
|
||
className="text-blue-400 hover:text-blue-300 text-sm"
|
||
title="Stadt bearbeiten"
|
||
>
|
||
<Icons.Edit className="w-4 h-4" />
|
||
</button>
|
||
<button
|
||
onClick={async () => {
|
||
if (confirm(`Stadt "${city.name}" wirklich löschen?`)) {
|
||
try {
|
||
setLoading(true);
|
||
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/admin/cities/${city.id}`, {
|
||
method: 'DELETE',
|
||
credentials: 'include'
|
||
});
|
||
|
||
if (response.ok) {
|
||
alert('Stadt erfolgreich gelöscht!');
|
||
loadCities(); // Reload cities
|
||
} else {
|
||
const errorData = await response.json();
|
||
setError(errorData.error || 'Fehler beim Löschen');
|
||
}
|
||
} catch (err) {
|
||
console.error('Error deleting city:', err);
|
||
setError('Netzwerkfehler');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
}}
|
||
disabled={loading}
|
||
className="text-red-400 hover:text-red-300 text-sm disabled:opacity-50"
|
||
title="Stadt löschen"
|
||
>
|
||
🗑️
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<p className="text-sm text-textMuted mb-2">{city.description}</p>
|
||
<div className="text-xs text-textMuted">
|
||
Bürgermeister: {city.mayor || 'Unbekannt'} • {city.memberCount} Bürger
|
||
</div>
|
||
{city.cityStats && (
|
||
<div className="mt-2 text-xs text-accentInfo">
|
||
Spezialität: {city.cityStats.specialty}
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === 'create-city' && (
|
||
<div className="space-y-8">
|
||
{/* Create City */}
|
||
<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.Map className="w-5 h-5 text-green-400" />
|
||
Stadt 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">Stadtname *</label>
|
||
<input
|
||
type="text"
|
||
value={cityForm.name}
|
||
onChange={(e) => setCityForm({...cityForm, name: e.target.value})}
|
||
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
|
||
placeholder="z.B. Eldoria"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-textMain mb-1">Bürgermeister</label>
|
||
<input
|
||
type="text"
|
||
value={cityForm.mayor}
|
||
onChange={(e) => setCityForm({...cityForm, mayor: e.target.value})}
|
||
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
|
||
placeholder="z.B. Lord Eldric"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-textMain mb-1">Gründungsjahr</label>
|
||
<input
|
||
type="text"
|
||
value={cityForm.establishedYear}
|
||
onChange={(e) => setCityForm({...cityForm, establishedYear: e.target.value})}
|
||
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
|
||
placeholder="z.B. Ära 5"
|
||
/>
|
||
</div>
|
||
|
||
<div className="md:col-span-2">
|
||
<label className="block text-sm font-medium text-textMain mb-1">Beschreibung</label>
|
||
<textarea
|
||
value={cityForm.description}
|
||
onChange={(e) => setCityForm({...cityForm, description: e.target.value})}
|
||
className="w-full h-20 bg-[#0b0b0d] border border-border rounded p-2 text-sm"
|
||
placeholder="Beschreibung der Stadt..."
|
||
/>
|
||
</div>
|
||
|
||
<div className="md:col-span-2">
|
||
<label className="block text-sm font-medium text-textMain mb-1">Stadt-Statistiken (JSON-Format)</label>
|
||
<textarea
|
||
value={cityForm.cityStats}
|
||
onChange={(e) => setCityForm({...cityForm, cityStats: e.target.value})}
|
||
className="w-full h-24 bg-[#0b0b0d] border border-border rounded p-2 text-sm font-mono text-xs"
|
||
placeholder='{ "taxRate": 5.0, "biome": "Ebene", "defenseRating": 5, "government": "Demokratie", "specialty": "Handel" }'
|
||
/>
|
||
<p className="text-xs text-textMuted mt-1">JSON-Objekt mit Stadt-Statistiken</p>
|
||
</div>
|
||
|
||
<div className="md:col-span-2">
|
||
<label className="block text-sm font-medium text-textMain mb-2">Banner-Bild (optional)</label>
|
||
<div className="border-2 border-dashed border-border rounded-lg p-4 text-center hover:border-accentInfo/50 transition-colors">
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
onChange={(e) => {
|
||
const file = e.target.files?.[0];
|
||
if (file) {
|
||
setCityForm({...cityForm, bannerFile: file});
|
||
}
|
||
}}
|
||
className="hidden"
|
||
id="city-banner-upload"
|
||
disabled={loading}
|
||
/>
|
||
<label htmlFor="city-banner-upload" className="cursor-pointer">
|
||
<div className="w-8 h-8 mx-auto mb-2 bg-accentInfo/20 rounded flex items-center justify-center">
|
||
<Icons.Map className="w-4 h-4 text-accentInfo" />
|
||
</div>
|
||
<p className="text-xs text-textMain font-medium">
|
||
Banner auswählen
|
||
</p>
|
||
<p className="text-xs text-textMuted">PNG, JPG oder GIF • Max. 5MB</p>
|
||
</label>
|
||
</div>
|
||
{cityForm.bannerFile && (
|
||
<div className="mt-2 text-xs text-green-400">
|
||
Banner ausgewählt: {cityForm.bannerFile.name}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="md:col-span-2">
|
||
<label className="block text-sm font-medium text-textMain mb-2">Logo-Bild (optional)</label>
|
||
<div className="border-2 border-dashed border-border rounded-lg p-4 text-center hover:border-accentInfo/50 transition-colors">
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
onChange={(e) => {
|
||
const file = e.target.files?.[0];
|
||
if (file) {
|
||
setCityForm({...cityForm, logoFile: file});
|
||
}
|
||
}}
|
||
className="hidden"
|
||
id="city-logo-upload"
|
||
disabled={loading}
|
||
/>
|
||
<label htmlFor="city-logo-upload" className="cursor-pointer">
|
||
<div className="w-8 h-8 mx-auto mb-2 bg-green-500/20 rounded flex items-center justify-center">
|
||
<Icons.Layers className="w-4 h-4 text-green-400" />
|
||
</div>
|
||
<p className="text-xs text-textMain font-medium">
|
||
Logo auswählen
|
||
</p>
|
||
<p className="text-xs text-textMuted">PNG, JPG oder GIF • Max. 2MB • Quadratisch bevorzugt</p>
|
||
</label>
|
||
</div>
|
||
{cityForm.logoFile && (
|
||
<div className="mt-2 text-xs text-green-400">
|
||
Logo ausgewählt: {cityForm.logoFile.name}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="md:col-span-2 pt-2">
|
||
<button
|
||
onClick={async () => {
|
||
if (!cityForm.name.trim()) {
|
||
setError('Stadt-Name ist erforderlich');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setLoading(true);
|
||
setError(null);
|
||
|
||
let cityStats = {};
|
||
if (cityForm.cityStats.trim()) {
|
||
try {
|
||
cityStats = JSON.parse(cityForm.cityStats);
|
||
} catch (e) {
|
||
setError('Ungültiges JSON-Format für Stadt-Statistiken');
|
||
setLoading(false);
|
||
return;
|
||
}
|
||
}
|
||
|
||
const response = await fetch('https://vollidioten.ceraticsoft.de/api/admin/cities', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({
|
||
name: cityForm.name.trim(),
|
||
description: cityForm.description.trim() || undefined,
|
||
mayor: cityForm.mayor.trim() || undefined,
|
||
establishedYear: cityForm.establishedYear.trim() || undefined,
|
||
cityStats: cityStats
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
const cityData = await response.json();
|
||
const cityId = cityData.cityId;
|
||
|
||
// Upload banner if provided
|
||
if (cityForm.bannerImageId) {
|
||
try {
|
||
const bannerFormData = new FormData();
|
||
// We need to get the file from the input, but since we don't have it stored,
|
||
// we'll skip this for now and show a message
|
||
console.log('Banner upload would happen here for city:', cityId);
|
||
} catch (err) {
|
||
console.error('Banner upload failed:', err);
|
||
}
|
||
}
|
||
|
||
// Upload logo if provided
|
||
if (cityForm.logoImageId) {
|
||
try {
|
||
const logoFormData = new FormData();
|
||
// We need to get the file from the input, but since we don't have it stored,
|
||
// we'll skip this for now and show a message
|
||
console.log('Logo upload would happen here for city:', cityId);
|
||
} catch (err) {
|
||
console.error('Logo upload failed:', err);
|
||
}
|
||
}
|
||
|
||
alert('Stadt erfolgreich erstellt! Bilder können nach der Erstellung im Bearbeitungsmodus hochgeladen werden.');
|
||
setCityForm({
|
||
name: '',
|
||
description: '',
|
||
mayor: '',
|
||
establishedYear: '',
|
||
cityStats: JSON.stringify({
|
||
taxRate: 5.0,
|
||
biome: 'Ebene',
|
||
defenseRating: 5,
|
||
government: 'Demokratie',
|
||
specialty: 'Handel'
|
||
}, null, 2)
|
||
});
|
||
loadCities(); // Reload to show new city
|
||
} else {
|
||
const errorData = await response.json();
|
||
setError(errorData.error || 'Fehler beim Erstellen');
|
||
}
|
||
} catch (err) {
|
||
console.error('Error creating city:', err);
|
||
setError('Netzwerkfehler');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}}
|
||
disabled={loading || !cityForm.name.trim()}
|
||
className="w-full bg-green-500 hover:bg-green-600 disabled:opacity-50 text-white px-4 py-2 rounded text-sm font-medium disabled:cursor-not-allowed"
|
||
>
|
||
{loading ? 'Erstelle...' : 'Stadt erstellen'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Edit City Modal */}
|
||
{editingCity && (
|
||
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||
<div className="bg-surface border border-border rounded-xl w-full max-w-lg shadow-2xl max-h-[90vh] overflow-y-auto">
|
||
<div className="p-4 border-b border-border flex justify-between items-center">
|
||
<h3 className="font-bold text-textMain flex items-center gap-2">
|
||
<Icons.Edit className="w-5 h-5 text-blue-400" />
|
||
Stadt bearbeiten
|
||
</h3>
|
||
<button
|
||
onClick={() => setEditingCity(null)}
|
||
className="text-textMuted hover:text-white text-xl leading-none"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
|
||
<div className="p-6 space-y-4">
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-textMain mb-1">Stadtname *</label>
|
||
<input
|
||
type="text"
|
||
value={editCityForm.name}
|
||
onChange={(e) => setEditCityForm({...editCityForm, name: e.target.value})}
|
||
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-textMain mb-1">Bürgermeister</label>
|
||
<input
|
||
type="text"
|
||
value={editCityForm.mayor}
|
||
onChange={(e) => setEditCityForm({...editCityForm, mayor: e.target.value})}
|
||
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
|
||
/>
|
||
</div>
|
||
|
||
<div className="md:col-span-2">
|
||
<label className="block text-sm font-medium text-textMain mb-1">Gründungsjahr</label>
|
||
<input
|
||
type="text"
|
||
value={editCityForm.establishedYear}
|
||
onChange={(e) => setEditCityForm({...editCityForm, establishedYear: e.target.value})}
|
||
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
|
||
/>
|
||
</div>
|
||
|
||
<div className="md:col-span-2">
|
||
<label className="block text-sm font-medium text-textMain mb-1">Beschreibung</label>
|
||
<textarea
|
||
value={editCityForm.description}
|
||
onChange={(e) => setEditCityForm({...editCityForm, description: e.target.value})}
|
||
className="w-full h-20 bg-[#0b0b0d] border border-border rounded p-2 text-sm"
|
||
/>
|
||
</div>
|
||
|
||
<div className="md:col-span-2">
|
||
<label className="block text-sm font-medium text-textMain mb-1">Stadt-Statistiken (JSON)</label>
|
||
<textarea
|
||
value={editCityForm.cityStats}
|
||
onChange={(e) => setEditCityForm({...editCityForm, cityStats: e.target.value})}
|
||
className="w-full h-24 bg-[#0b0b0d] border border-border rounded p-2 text-sm font-mono text-xs"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Current Images */}
|
||
{(editingCity.bannerUrl || editingCity.logoUrl) && (
|
||
<div className="border-t border-border pt-4">
|
||
<h4 className="text-sm font-medium text-textMain mb-3">Aktuelle Bilder</h4>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
{editingCity.bannerUrl && (
|
||
<div className="bg-surfaceHighlight/30 border border-border rounded p-3">
|
||
<p className="text-xs text-textMuted mb-2">Banner</p>
|
||
<img
|
||
src={editingCity.bannerUrl}
|
||
alt="Banner"
|
||
className="w-full h-16 object-cover rounded border border-white/10"
|
||
/>
|
||
</div>
|
||
)}
|
||
{editingCity.logoUrl && (
|
||
<div className="bg-surfaceHighlight/30 border border-border rounded p-3">
|
||
<p className="text-xs text-textMuted mb-2">Logo</p>
|
||
<img
|
||
src={editingCity.logoUrl}
|
||
alt="Logo"
|
||
className="w-12 h-12 object-cover rounded border border-white/10"
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Image Uploads */}
|
||
<div className="border-t border-border pt-4 space-y-4">
|
||
<h4 className="text-sm font-medium text-textMain">Bilder ändern</h4>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-textMain mb-2">Banner-Bild</label>
|
||
<div className="border-2 border-dashed border-border rounded-lg p-4 text-center hover:border-accentInfo/50 transition-colors">
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
onChange={async (e) => {
|
||
const file = e.target.files?.[0];
|
||
if (file) {
|
||
try {
|
||
setLoading(true);
|
||
const formData = new FormData();
|
||
formData.append('banner', file);
|
||
|
||
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/admin/cities/${editingCity.id}/banner/upload`, {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
body: formData
|
||
});
|
||
|
||
if (response.ok) {
|
||
loadCities(); // Reload to show updated image
|
||
} else {
|
||
setError('Fehler beim Hochladen des Banners');
|
||
}
|
||
} catch (err) {
|
||
console.error('Error uploading banner:', err);
|
||
setError('Netzwerkfehler beim Banner-Upload');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
}}
|
||
className="hidden"
|
||
id="edit-city-banner-upload"
|
||
disabled={loading}
|
||
/>
|
||
<label htmlFor="edit-city-banner-upload" className="cursor-pointer">
|
||
<div className="w-8 h-8 mx-auto mb-2 bg-accentInfo/20 rounded flex items-center justify-center">
|
||
<Icons.Map className="w-4 h-4 text-accentInfo" />
|
||
</div>
|
||
<p className="text-xs text-textMain font-medium">
|
||
{loading ? 'Lädt...' : 'Banner ersetzen'}
|
||
</p>
|
||
<p className="text-xs text-textMuted">PNG, JPG oder GIF • Max. 5MB</p>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-textMain mb-2">Logo-Bild</label>
|
||
<div className="border-2 border-dashed border-border rounded-lg p-4 text-center hover:border-accentInfo/50 transition-colors">
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
onChange={async (e) => {
|
||
const file = e.target.files?.[0];
|
||
if (file) {
|
||
try {
|
||
setLoading(true);
|
||
const formData = new FormData();
|
||
formData.append('logo', file);
|
||
|
||
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/admin/cities/${editingCity.id}/logo/upload`, {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
body: formData
|
||
});
|
||
|
||
if (response.ok) {
|
||
loadCities(); // Reload to show updated image
|
||
} else {
|
||
setError('Fehler beim Hochladen des Logos');
|
||
}
|
||
} catch (err) {
|
||
console.error('Error uploading logo:', err);
|
||
setError('Netzwerkfehler beim Logo-Upload');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
}}
|
||
className="hidden"
|
||
id="edit-city-logo-upload"
|
||
disabled={loading}
|
||
/>
|
||
<label htmlFor="edit-city-logo-upload" className="cursor-pointer">
|
||
<div className="w-8 h-8 mx-auto mb-2 bg-green-500/20 rounded flex items-center justify-center">
|
||
<Icons.Layers className="w-4 h-4 text-green-400" />
|
||
</div>
|
||
<p className="text-xs text-textMain font-medium">
|
||
{loading ? 'Lädt...' : 'Logo ersetzen'}
|
||
</p>
|
||
<p className="text-xs text-textMuted">PNG, JPG oder GIF • Max. 2MB • Quadratisch bevorzugt</p>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex gap-2 pt-2 border-t border-border">
|
||
<button
|
||
onClick={async () => {
|
||
try {
|
||
setLoading(true);
|
||
let cityStats = {};
|
||
if (editCityForm.cityStats.trim()) {
|
||
cityStats = JSON.parse(editCityForm.cityStats);
|
||
}
|
||
|
||
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/admin/cities/${editingCity.id}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({
|
||
name: editCityForm.name.trim(),
|
||
description: editCityForm.description.trim() || undefined,
|
||
mayor: editCityForm.mayor.trim() || undefined,
|
||
establishedYear: editCityForm.establishedYear.trim() || undefined,
|
||
cityStats: cityStats
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
setEditingCity(null);
|
||
loadCities();
|
||
} else {
|
||
const errorData = await response.json();
|
||
setError(errorData.error || 'Fehler beim Aktualisieren');
|
||
}
|
||
} catch (err) {
|
||
console.error('Error updating city:', err);
|
||
setError('Netzwerkfehler');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}}
|
||
disabled={loading}
|
||
className="flex-1 bg-blue-500 hover:bg-blue-600 disabled:opacity-50 text-white px-3 py-2 rounded text-sm font-medium"
|
||
>
|
||
{loading ? '...' : 'Speichern'}
|
||
</button>
|
||
<button
|
||
onClick={() => setEditingCity(null)}
|
||
className="flex-1 bg-gray-500 hover:bg-gray-600 text-white px-3 py-1 rounded text-sm"
|
||
>
|
||
Abbrechen
|
||
</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;
|