Files
ReadaBook/apps/web/src/pages/AdminPage.tsx
Git Agent a30971f1e9 fix(web): admin bibliothèques — correctifs complémentaires client et AdminPage
- web: AdminPage, client API (+ tests), styles

Refs: #15
2026-08-23 11:36:17 +02:00

184 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { FormEvent, useEffect, useState } from "react";
import { Play, Plus, Trash2 } from "lucide-react";
import type { JobDto, LibraryDto, UserDto } from "@readabook/shared";
import { api, getApiFallback } from "../api/client";
import { EmptyState, ErrorRibbon, LoadingState, Panel } from "../components/ui";
export function AdminPage() {
const [libraries, setLibraries] = useState<LibraryDto[]>([]);
const [jobs, setJobs] = useState<JobDto[]>([]);
const [users, setUsers] = useState<UserDto[]>([]);
const [loading, setLoading] = useState(true);
const [name, setName] = useState("Bibliotheque locale");
const [path, setPath] = useState("/library");
const [error, setError] = useState<string>();
const [success, setSuccess] = useState<string>();
async function refresh() {
setLoading(true);
setError(undefined);
const [libraryResult, jobResult, userResult] = await Promise.allSettled([api.libraries(), api.jobs(), api.users()]);
const errors: string[] = [];
if (libraryResult.status === "fulfilled") {
setLibraries(libraryResult.value);
} else {
setLibraries(getApiFallback<LibraryDto[]>(libraryResult.reason) ?? []);
errors.push("bibliotheques");
}
if (jobResult.status === "fulfilled") {
setJobs(jobResult.value);
} else {
setJobs(getApiFallback<JobDto[]>(jobResult.reason) ?? []);
errors.push("travaux");
}
if (userResult.status === "fulfilled") {
setUsers(userResult.value);
} else {
setUsers(getApiFallback<UserDto[]>(userResult.reason) ?? []);
errors.push("comptes");
}
setError(errors.length ? `Donnees admin degradees : ${errors.join(", ")}.` : undefined);
setLoading(false);
}
useEffect(() => {
void refresh();
}, []);
async function createLibrary(event: FormEvent) {
event.preventDefault();
setError(undefined);
setSuccess(undefined);
try {
await api.createLibrary({ name, path, enabled: true });
await refresh();
setSuccess(`Bibliothèque "${name}" ajoutée.`);
} catch (createError) {
const fallback = getApiFallback<LibraryDto>(createError);
if (fallback) setLibraries((current) => [fallback, ...current]);
setError(fallback ? "Creation en mode secours, synchronisation a retenter." : "Creation impossible");
}
}
async function scan(id: number) {
setError(undefined);
setSuccess(undefined);
try {
await api.scanLibrary(id);
await refresh();
setSuccess("Scan demandé.");
} catch (scanError) {
const fallback = getApiFallback<JobDto>(scanError);
if (fallback) setJobs((current) => [fallback, ...current]);
setError(fallback ? "Scan place en file de secours, statut a verifier." : "Scan impossible");
}
}
async function deleteLibrary(library: LibraryDto) {
const confirmed = window.confirm(
`Supprimer la bibliothèque "${library.name}" ?\n\nLes livres restent sur le disque. ReadaBook supprimera seulement cette bibliothèque du catalogue.`
);
if (!confirmed) return;
setError(undefined);
setSuccess(undefined);
try {
await api.deleteLibrary(library.id);
setLibraries((current) => current.filter((item) => item.id !== library.id));
setSuccess(`Bibliothèque "${library.name}" supprimée. Les fichiers disque n'ont pas été supprimés.`);
} catch (deleteError) {
setError(deleteError instanceof Error ? deleteError.message : "Suppression impossible");
}
}
return (
<div className="page-grid">
<Panel className="span-2">
<div className="section-heading">
<h1>Administration</h1>
<span>{loading ? "chargement" : `${users.length} comptes`}</span>
</div>
<ErrorRibbon message={error} />
{success && <div className="success-ribbon">{success}</div>}
{error && (
<div className="retry-row">
<span>Les formulaires restent disponibles.</span>
<button className="ghost-button" onClick={() => void refresh()}>
Reessayer
</button>
</div>
)}
<form className="admin-form" onSubmit={createLibrary}>
<label>
Nom de la bibliothèque
<input value={name} onChange={(event) => setName(event.target.value)} required />
<small>Nom affiché dans lapplication, par exemple : Romans, BD, Documentation.</small>
</label>
<label>
Chemin du dossier
<input value={path} onChange={(event) => setPath(event.target.value)} required />
<small>Emplacement réel sur le serveur ReadaBook lit les livres.</small>
</label>
<button className="primary-button" type="submit">
<Plus size={17} />
Ajouter
</button>
</form>
</Panel>
<Panel>
<div className="section-heading">
<h2>Travaux</h2>
<span>{jobs.length}</span>
</div>
{loading && !jobs.length ? (
<LoadingState label="Lecture des travaux" />
) : jobs.length ? (
<div className="job-list">
{jobs.map((job) => (
<div key={job.id}>
<strong>{job.type}</strong>
<span>{job.status}</span>
</div>
))}
</div>
) : (
<EmptyState title="Aucun travail" detail="Les scans apparaitront ici." />
)}
</Panel>
<Panel className="span-3">
{loading && !libraries.length ? (
<LoadingState label="Lecture des rayons" />
) : libraries.length ? (
<div className="library-table">
{libraries.map((library) => (
<div key={library.id}>
<div className="library-copy">
<strong>{library.name}</strong>
<span>Chemin : {library.path}</span>
</div>
<span>{library.enabled ? "actif" : "pause"}</span>
<button className="ghost-button" onClick={() => scan(library.id)}>
<Play size={16} />
Scanner
</button>
<button className="ghost-button danger-button" onClick={() => void deleteLibrary(library)}>
<Trash2 size={16} />
Supprimer
</button>
</div>
))}
</div>
) : (
<EmptyState title="Aucun rayon" detail="Ajoute un chemin puis relance la synchronisation." />
)}
</Panel>
</div>
);
}