FluxInator / static /components /CategoryManager.js
Scalino84
Initial commit
1e7308f
raw
history blame
18.3 kB
import React, { useState, useEffect, useCallback } from 'react';
import { API, APIHandler, Validators } from './api.js';
const CategoryManager = () => {
const [categories, setCategories] = useState([]);
const [newCategoryName, setNewCategoryName] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchCategories();
}, []);
const fetchCategories = async () => {
try {
const response = await fetch('/backend/categories');
const data = await response.json();
setCategories(data);
} catch (error) {
console.error('Fehler beim Laden der Kategorien:', error);
} finally {
setLoading(false);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await fetch('/create_category', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `name=${encodeURIComponent(newCategoryName)}`
});
if (!response.ok) throw new Error('Fehler beim Erstellen');
await fetchCategories();
setNewCategoryName('');
} catch (error) {
console.error('Fehler:', error);
}
};
const handleDelete = async (id) => {
if (!confirm('Kategorie wirklich löschen?')) return;
try {
const response = await fetch(`/delete_category/${id}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Fehler beim Löschen');
await fetchCategories();
} catch (error) {
console.error('Fehler:', error);
}
};
if (loading) return <div>Lade...</div>;
return (
<div className="card-body">
<form onSubmit={handleSubmit} className="mb-3">
<div className="input-group">
<input
type="text"
className="form-control"
value={newCategoryName}
onChange={(e) => setNewCategoryName(e.target.value)}
placeholder="Neue Kategorie"
required
/>
<button type="submit" className="btn btn-primary">Erstellen</button>
</div>
</form>
<div className="table-responsive">
<table className="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Bilder</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{categories.map(category => (
<tr key={category.id}>
<td>{category.name}</td>
<td>{category.imageCount}</td>
<td>
<button
onClick={() => handleDelete(category.id)}
className="btn btn-sm btn-outline-danger"
>
Löschen
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};
export default CategoryManager;
### END: category-manager.txt
### START: category-manager-updated.txt
import React, { useState, useEffect, useCallback } from 'react';
import { API, APIHandler, Validators } from '@/api';
const CategoryManager = () => {
const [categories, setCategories] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [selectedCategories, setSelectedCategories] = useState(new Set());
const [editingCategory, setEditingCategory] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
const [sortConfig, setSortConfig] = useState({ key: 'name', direction: 'asc' });
// Daten laden
const fetchCategories = useCallback(async () => {
try {
setLoading(true);
const data = await APIHandler.get(API.categories.list);
setCategories(data);
setError(null);
} catch (err) {
setError('Fehler beim Laden der Kategorien: ' + err.message);
console.error('Fetch error:', err);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchCategories();
}, [fetchCategories]);
// Sortierung
const sortedCategories = React.useMemo(() => {
const sorted = [...categories];
sorted.sort((a, b) => {
if (sortConfig.key === 'imageCount') {
return sortConfig.direction === 'asc'
? a.imageCount - b.imageCount
: b.imageCount - a.imageCount;
}
return sortConfig.direction === 'asc'
? a[sortConfig.key].localeCompare(b[sortConfig.key])
: b[sortConfig.key].localeCompare(a[sortConfig.key]);
});
return sorted;
}, [categories, sortConfig]);
// Kategorie erstellen
const handleCreate = async (name) => {
try {
//const validationErrors = Validators.category({ name });
//if (Object.keys(validationErrors).length > 0) {
// throw new Error(Object.values(validationErrors).join(', '));
//}
await APIHandler.post(API.categories.create, { name });
await fetchCategories();
window.showToast('Erfolg', 'Kategorie wurde erstellt', 'success');
} catch (err) {
window.showToast('Fehler', err.message, 'danger');
}
};
// Kategorie löschen
const handleDelete = async (id) => {
if (!window.confirm('Möchten Sie diese Kategorie wirklich löschen?')) return;
try {
await APIHandler.delete(API.categories.delete(id));
await fetchCategories();
window.showToast('Erfolg', 'Kategorie wurde gelöscht', 'success');
} catch (err) {
window.showToast('Fehler', err.message, 'danger');
}
};
// Kategorie aktualisieren
const handleUpdate = async (id, updates) => {
try {
//const validationErrors = Validators.category(updates);
//if (Object.keys(validationErrors).length > 0) {
// throw new Error(Object.values(validationErrors).join(', '));
//}
await APIHandler.put(API.categories.update(id), updates);
await fetchCategories();
setEditingCategory(null);
window.showToast('Erfolg', 'Kategorie wurde aktualisiert', 'success');
} catch (err) {
window.showToast('Fehler', err.message, 'danger');
}
};
// Massenbearbeitung
const handleBulkDelete = async () => {
if (selectedCategories.size === 0) return;
if (!window.confirm(`Möchten Sie ${selectedCategories.size} Kategorien wirklich löschen?`)) return;
try {
await Promise.all(
Array.from(selectedCategories).map(id =>
APIHandler.delete(API.categories.delete(id))
)
);
setSelectedCategories(new Set());
await fetchCategories();
window.showToast('Erfolg', 'Ausgewählte Kategorien wurden gelöscht', 'success');
} catch (err) {
window.showToast('Fehler', err.message, 'danger');
}
};
// Massenbearbeitung - Kategorien zusammenführen
const handleMergeCategories = async (targetId) => {
const selectedIds = Array.from(selectedCategories);
if (selectedIds.length < 2) return;
try {
await APIHandler.post(API.categories.merge, {
targetId,
sourceIds: selectedIds.filter(id => id !== targetId)
});
setSelectedCategories(new Set());
await fetchCategories();
window.showToast('Erfolg', 'Kategorien wurden zusammengeführt', 'success');
} catch (err) {
window.showToast('Fehler', err.message, 'danger');
}
};
const handleSelectAll = (event) => {
if (event.target.checked) {
setSelectedCategories(new Set(categories.map(cat => cat.id)));
} else {
setSelectedCategories(new Set());
}
};
const handleSort = (key) => {
setSortConfig(current => ({
key,
direction: current.key === key && current.direction === 'asc' ? 'desc' : 'asc'
}));
};
if (loading) {
return (
<div className="d-flex justify-content-center p-5">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Laden...</span>
</div>
</div>
);
}
if (error) {
return (
<div className="alert alert-danger m-3" role="alert">
<h4 className="alert-heading">Fehler</h4>
<p>{error}</p>
<button
className="btn btn-outline-danger"
onClick={fetchCategories}
>
Erneut versuchen
</button>
</div>
);
}
// Filtern basierend auf Suchbegriff
const filteredCategories = searchTerm
? sortedCategories.filter(cat =>
cat.name.toLowerCase().includes(searchTerm.toLowerCase())
)
: sortedCategories;
return (
<div className="card-body">
{/* Toolbar */}
<div className="d-flex flex-wrap justify-content-between mb-3 gap-2">
<div className="d-flex gap-2">
<button
className="btn btn-primary"
onClick={() => setEditingCategory({ name: '' })}
>
<i className="bi bi-plus-lg"></i> Neue Kategorie
</button>
{selectedCategories.size > 0 && (
<div className="btn-group">
<button
className="btn btn-danger"
onClick={handleBulkDelete}
>
<i className="bi bi-trash"></i>
{selectedCategories.size} löschen
</button>
{selectedCategories.size > 1 && (
<button
className="btn btn-warning"
onClick={() => {
const firstId = Array.from(selectedCategories)[0];
handleMergeCategories(firstId);
}}
>
<i className="bi bi-arrow-join"></i>
Zusammenführen
</button>
)}
</div>
)}
</div>
<div className="flex-grow-1 max-w-xs">
<input
type="search"
className="form-control"
placeholder="Kategorien durchsuchen..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</div>
{/* Kategorie Liste */}
<div className="table-responsive">
<table className="table table-hover">
<thead>
<tr>
<th>
<input
type="checkbox"
className="form-check-input"
onChange={handleSelectAll}
checked={selectedCategories.size === categories.length}
/>
</th>
<th
className="cursor-pointer"
onClick={() => handleSort('name')}
>
Name {sortConfig.key === 'name' && (
<i className={`bi bi-arrow-${sortConfig.direction === 'asc' ? 'up' : 'down'}`}></i>
)}
</th>
<th
className="cursor-pointer"
onClick={() => handleSort('imageCount')}
>
Bilder {sortConfig.key === 'imageCount' && (
<i className={`bi bi-arrow-${sortConfig.direction === 'asc' ? 'up' : 'down'}`}></i>
)}
</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{filteredCategories.map(category => (
<tr key={category.id}>
<td>
<input
type="checkbox"
className="form-check-input"
checked={selectedCategories.has(category.id)}
onChange={(e) => {
const newSelected = new Set(selectedCategories);
if (e.target.checked) {
newSelected.add(category.id);
} else {
newSelected.delete(category.id);
}
setSelectedCategories(newSelected);
}}
/>
</td>
<td>
{editingCategory?.id === category.id ? (
<input
type="text"
className="form-control"
value={editingCategory.name}
onChange={(e) => setEditingCategory({
...editingCategory,
name: e.target.value
})}
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleUpdate(category.id, editingCategory);
}
}}
/>
) : category.name}
</td>
<td>{category.imageCount}</td>
<td>
<div className="btn-group">
{editingCategory?.id === category.id ? (
<>
<button
className="btn btn-sm btn-success"
onClick={() => handleUpdate(category.id, editingCategory)}
>
<i className="bi bi-check"></i>
</button>
<button
className="btn btn-sm btn-secondary"
onClick={() => setEditingCategory(null)}
>
<i className="bi bi-x"></i>
</button>
</>
) : (
<>
<button
className="btn btn-sm btn-outline-primary"
onClick={() => setEditingCategory(category)}
>
<i className="bi bi-pencil"></i>
</button>
<button
className="btn btn-sm btn-outline-danger"
onClick={() => handleDelete(category.id)}
>
<i className="bi bi-trash"></i>
</button>
</>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{filteredCategories.length === 0 && (
<div className="text-center text-muted p-5">
<i className="bi bi-tags display-4"></i>
<p className="mt-3">
{searchTerm
? 'Keine Kategorien gefunden'
: 'Keine Kategorien vorhanden'}
</p>
</div>
)}
</div>
);
};
export default CategoryManager;
window.CategoryManager = CategoryManager; // <-- Diese Zeile ans Ende setzen