242 lines
7 KiB
TypeScript
242 lines
7 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useState, useEffect } from 'react';
|
||
|
|
|
||
|
|
interface Aufgabe {
|
||
|
|
id: number;
|
||
|
|
beschreibung: string;
|
||
|
|
erledigt: boolean;
|
||
|
|
erstellt: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface Pagination {
|
||
|
|
page: number;
|
||
|
|
limit: number;
|
||
|
|
total: number;
|
||
|
|
totalPages: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
type SortOption = 'text_asc' | 'text_desc' | 'datum_asc' | 'datum_desc';
|
||
|
|
|
||
|
|
export default function TodoApp() {
|
||
|
|
const [aufgaben, setAufgaben] = useState<Aufgabe[]>([]);
|
||
|
|
const [neueAufgabe, setNeueAufgabe] = useState('');
|
||
|
|
const [sortierung, setSortierung] = useState<SortOption>('datum_desc');
|
||
|
|
const [isLoading, setIsLoading] = useState(true);
|
||
|
|
const [pagination, setPagination] = useState<Pagination>({
|
||
|
|
page: 1,
|
||
|
|
limit: 10,
|
||
|
|
total: 0,
|
||
|
|
totalPages: 1
|
||
|
|
});
|
||
|
|
|
||
|
|
// Lade Aufgaben vom Server mit Paginierung und Sortierung
|
||
|
|
const loadAufgaben = async (page: number = 1) => {
|
||
|
|
try {
|
||
|
|
setIsLoading(true);
|
||
|
|
const response = await fetch(`/api/tasks?page=${page}&limit=10&sort=${sortierung}`);
|
||
|
|
if (response.ok) {
|
||
|
|
const data = await response.json();
|
||
|
|
setAufgaben(data.aufgaben);
|
||
|
|
setPagination(data.pagination);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Fehler beim Laden der Aufgaben:', error);
|
||
|
|
} finally {
|
||
|
|
setIsLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Initial laden
|
||
|
|
useEffect(() => {
|
||
|
|
loadAufgaben(1);
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
// Aufgaben werden bereits serverseitig sortiert, keine clientseitige Sortierung nötig
|
||
|
|
const sortierteAufgaben = aufgaben;
|
||
|
|
|
||
|
|
const handleHinzufuegen = async (e: React.FormEvent) => {
|
||
|
|
e.preventDefault();
|
||
|
|
if (neueAufgabe.trim()) {
|
||
|
|
try {
|
||
|
|
const response = await fetch('/api/tasks', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
},
|
||
|
|
body: JSON.stringify({ beschreibung: neueAufgabe.trim() }),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
setNeueAufgabe('');
|
||
|
|
// Zur ersten Seite zurück, wenn neue Aufgabe hinzugefügt wird
|
||
|
|
await loadAufgaben(1);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Fehler beim Hinzufügen:', error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleLoeschen = async (id: number) => {
|
||
|
|
if (confirm('Möchtest du diese Aufgabe wirklich löschen?')) {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/tasks/${id}`, {
|
||
|
|
method: 'DELETE',
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
// Aktuelle Seite neu laden oder zur vorherigen, wenn leer
|
||
|
|
await loadAufgaben(pagination.page);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Fehler beim Löschen:', error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleErledigen = async (id: number) => {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/tasks/${id}`, {
|
||
|
|
method: 'PUT',
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
await loadAufgaben(pagination.page);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Fehler beim Aktualisieren:', error);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Sortierung ändern und Aufgaben neu laden
|
||
|
|
const handleSortierungAendern = async (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||
|
|
const neueSortierung = e.target.value as SortOption;
|
||
|
|
setSortierung(neueSortierung);
|
||
|
|
await loadAufgaben(1);
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="container">
|
||
|
|
<h1>To-Do-Liste</h1>
|
||
|
|
|
||
|
|
{/* Sortieroptionen und Export */}
|
||
|
|
<div className="sort-container">
|
||
|
|
<span>Sortieren nach:</span>
|
||
|
|
<select
|
||
|
|
value={sortierung}
|
||
|
|
onChange={handleSortierungAendern}
|
||
|
|
className="sort-dropdown"
|
||
|
|
>
|
||
|
|
<option value="text_asc">Name (A-Z)</option>
|
||
|
|
<option value="text_desc">Name (Z-A)</option>
|
||
|
|
<option value="datum_asc">Datum (älteste)</option>
|
||
|
|
<option value="datum_desc">Datum (neueste)</option>
|
||
|
|
</select>
|
||
|
|
<button
|
||
|
|
onClick={() => window.location.href = '/api/tasks/export'}
|
||
|
|
className="export-button"
|
||
|
|
disabled={isLoading}
|
||
|
|
>
|
||
|
|
CSV Export
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Formular zum Hinzufügen */}
|
||
|
|
<form onSubmit={handleHinzufuegen} className="form-group">
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
value={neueAufgabe}
|
||
|
|
onChange={(e) => setNeueAufgabe(e.target.value)}
|
||
|
|
placeholder="Neue Aufgabe eingeben..."
|
||
|
|
required
|
||
|
|
disabled={isLoading}
|
||
|
|
/>
|
||
|
|
<button type="submit" disabled={isLoading}>
|
||
|
|
{isLoading ? 'Lädt...' : 'Hinzufügen'}
|
||
|
|
</button>
|
||
|
|
</form>
|
||
|
|
|
||
|
|
{/* To-Do-Liste */}
|
||
|
|
{isLoading ? (
|
||
|
|
<p className="empty-message">Lädt Aufgaben...</p>
|
||
|
|
) : sortierteAufgaben.length > 0 ? (
|
||
|
|
<ul className="todo-list">
|
||
|
|
{sortierteAufgaben.map((aufgabe) => (
|
||
|
|
<li
|
||
|
|
key={aufgabe.id}
|
||
|
|
className={`todo-item ${aufgabe.erledigt ? 'completed' : ''}`}
|
||
|
|
>
|
||
|
|
<div className="content">
|
||
|
|
<span>{aufgabe.beschreibung}</span>
|
||
|
|
<span className="datum">
|
||
|
|
{new Date(aufgabe.erstellt).toLocaleString('de-DE', {
|
||
|
|
day: '2-digit',
|
||
|
|
month: '2-digit',
|
||
|
|
year: 'numeric',
|
||
|
|
hour: '2-digit',
|
||
|
|
minute: '2-digit',
|
||
|
|
})}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div className="actions">
|
||
|
|
<a
|
||
|
|
href="#"
|
||
|
|
className="erledigen-btn"
|
||
|
|
onClick={(e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
handleErledigen(aufgabe.id);
|
||
|
|
}}
|
||
|
|
title={aufgabe.erledigt ? 'Rückgängig' : 'Erledigt'}
|
||
|
|
>
|
||
|
|
{aufgabe.erledigt ? '↩' : '✓'}
|
||
|
|
</a>
|
||
|
|
<a
|
||
|
|
href="#"
|
||
|
|
className="loeschen-btn"
|
||
|
|
onClick={(e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
handleLoeschen(aufgabe.id);
|
||
|
|
}}
|
||
|
|
title="Löschen"
|
||
|
|
>
|
||
|
|
❌
|
||
|
|
</a>
|
||
|
|
</div>
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
) : (
|
||
|
|
<p className="empty-message">
|
||
|
|
Die To-Do-Liste ist leer. Füge eine neue Aufgabe hinzu!
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Paginierung */}
|
||
|
|
{pagination.totalPages > 1 && (
|
||
|
|
<div className="pagination">
|
||
|
|
<button
|
||
|
|
onClick={() => loadAufgaben(Math.max(1, pagination.page - 1))}
|
||
|
|
disabled={pagination.page <= 1 || isLoading}
|
||
|
|
>
|
||
|
|
← Zurück
|
||
|
|
</button>
|
||
|
|
<span className="pagination-info">
|
||
|
|
Seite {pagination.page} von {pagination.totalPages} ({pagination.total} Aufgaben)
|
||
|
|
</span>
|
||
|
|
<button
|
||
|
|
onClick={() => loadAufgaben(Math.min(pagination.totalPages, pagination.page + 1))}
|
||
|
|
disabled={pagination.page >= pagination.totalPages || isLoading}
|
||
|
|
>
|
||
|
|
Weiter →
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<footer className="copyright">
|
||
|
|
<p>© 2026 GSTools</p>
|
||
|
|
</footer>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|