import { FormEvent, useEffect, useMemo, useState } from "react"; import { ArrowDown, ArrowUp, Play, Save } from "lucide-react"; import type { AutomationFrequency, AutomationScheduleDto, AutomationSettingsDto, MetadataSourcesConfigDto } from "@readabook/shared"; import { api, getApiFallback } from "../api/client"; import { ErrorRibbon, LoadingState, Panel } from "../components/ui"; import { type AdminMetadataProviderId, type AdminMetadataSourcesConfig, defaultMetadataSources, metadataSourcesPayload, moveSource, normalizeMetadataSources, providerLabels, providerUiMessage, providerUiState, providerUiStateLabel, scheduleDays, scheduleSummary } from "./adminAutomation"; type AdminAutomationTab = "sources" | "automation"; type ApiState = { initial: T | null; draft: T | null; loading: boolean; saving: boolean; error?: string; success?: string; }; const defaultMetadataConfig: AdminMetadataSourcesConfig = { isbnPriorityEnabled: true, sources: defaultMetadataSources }; const defaultAutomationSettings: AutomationSettingsDto = { watchLibraries: false, autoEnrichNewBooks: false, scanSchedule: { frequency: "disabled", time: "03:00", dayOfWeek: 1 }, enrichSchedule: { frequency: "disabled", time: "04:00", dayOfWeek: 1 } }; export function AdminAutomationPage() { const [tab, setTab] = useState("sources"); const [metadataState, setMetadataState] = useState>({ initial: null, draft: null, loading: true, saving: false }); const [automationState, setAutomationState] = useState>({ initial: null, draft: null, loading: true, saving: false }); const [apiKeys, setApiKeys] = useState>>({}); const metadataDirty = useMemo( () => Boolean(metadataState.initial && metadataState.draft && JSON.stringify(metadataState.initial) !== JSON.stringify(metadataState.draft)), [metadataState.initial, metadataState.draft] ); const automationDirty = useMemo( () => Boolean(automationState.initial && automationState.draft && JSON.stringify(automationState.initial) !== JSON.stringify(automationState.draft)), [automationState.initial, automationState.draft] ); async function refreshMetadata() { setMetadataState((current) => ({ ...current, loading: true, error: undefined, success: undefined })); try { const next = normalizeMetadataSources(await api.metadataSources()); setMetadataState({ initial: next, draft: next, loading: false, saving: false }); setApiKeys({}); } catch (error) { const fallback = getApiFallback(error); const next = normalizeMetadataSources(fallback ?? defaultMetadataConfig); setMetadataState({ initial: next, draft: next, loading: false, saving: false, error: fallback ? "Sources chargees en mode degrade." : "Lecture des sources impossible." }); } } async function refreshAutomation() { setAutomationState((current) => ({ ...current, loading: true, error: undefined, success: undefined })); try { const next = await api.automationSettings(); setAutomationState({ initial: next, draft: next, loading: false, saving: false }); } catch (error) { const fallback = getApiFallback(error); const next = fallback ?? defaultAutomationSettings; setAutomationState({ initial: next, draft: next, loading: false, saving: false, error: fallback ? "Automatisation chargee en mode degrade." : "Lecture de l'automatisation impossible." }); } } useEffect(() => { void refreshMetadata(); void refreshAutomation(); }, []); async function saveMetadata(event: FormEvent) { event.preventDefault(); if (!metadataState.draft) return; setMetadataState((current) => ({ ...current, saving: true, error: undefined, success: undefined })); try { const payload = metadataSourcesPayload(metadataState.draft); payload.sources = payload.sources?.map((source) => { const apiKey = apiKeys[source.provider]?.trim(); return apiKey ? { ...source, apiKey } : source; }); const next = normalizeMetadataSources(await api.updateMetadataSources(payload)); setMetadataState({ initial: next, draft: next, loading: false, saving: false, success: "Sources enregistrees." }); setApiKeys({}); } catch (error) { setMetadataState((current) => ({ ...current, saving: false, error: error instanceof Error ? error.message : "Enregistrement des sources impossible." })); } } async function saveAutomation(event: FormEvent) { event.preventDefault(); if (!automationState.draft) return; setAutomationState((current) => ({ ...current, saving: true, error: undefined, success: undefined })); try { const next = await api.updateAutomationSettings(automationState.draft); setAutomationState({ initial: next, draft: next, loading: false, saving: false, success: "Automatisation enregistree." }); } catch (error) { setAutomationState((current) => ({ ...current, saving: false, error: error instanceof Error ? error.message : "Enregistrement de l'automatisation impossible." })); } } async function runNow(kind: "scan" | "enrich") { setAutomationState((current) => ({ ...current, error: undefined, success: undefined })); try { if (kind === "scan") await api.runAutomationScan(); else await api.runAutomationEnrich(); setAutomationState((current) => ({ ...current, success: kind === "scan" ? "Scan planifie demande." : "Enrichissement planifie demande." })); } catch (error) { setAutomationState((current) => ({ ...current, error: error instanceof Error ? error.message : "Demande impossible." })); } } return (

Automatisation & enrichissement

Sources, priorites et taches recurrentes.

{metadataDirty || automationDirty ? "modifications non enregistrees" : "a jour"}
{tab === "sources" ? ( setMetadataState((current) => ({ ...current, draft, success: undefined }))} onSubmit={saveMetadata} onRefresh={refreshMetadata} /> ) : ( setAutomationState((current) => ({ ...current, draft, success: undefined }))} onSubmit={saveAutomation} onRefresh={refreshAutomation} onRunNow={runNow} /> )}
); } function MetadataSourcesPanel({ state, dirty, apiKeys, setApiKeys, onChange, onSubmit, onRefresh }: { state: ApiState; dirty: boolean; apiKeys: Partial>; setApiKeys: (next: Partial>) => void; onChange: (draft: AdminMetadataSourcesConfig) => void; onSubmit: (event: FormEvent) => void; onRefresh: () => Promise; }) { const draft = state.draft; if (state.loading && !draft) return ; if (!draft) return null; const local = draft.sources.find((source) => source.provider === "local"); const external = draft.sources.filter((source) => source.provider !== "local"); return (

Sources de métadonnées

La source locale reste active en premier passage.

{state.success &&
{state.success}
}
{local && (
{providerLabels.local} Source locale
toujours active
)} {external.map((source, index) => (
{providerUiStateLabel(source)} {providerUiMessage(source)}
{source.provider === "comicvine" && (

Usage non commercial uniquement. Verifier la compatibilite avec l'usage de ReadaBook.

)}
))}
); } function AutomationPanel({ state, dirty, onChange, onSubmit, onRefresh, onRunNow }: { state: ApiState; dirty: boolean; onChange: (draft: AutomationSettingsDto) => void; onSubmit: (event: FormEvent) => void; onRefresh: () => Promise; onRunNow: (kind: "scan" | "enrich") => Promise; }) { const draft = state.draft; if (state.loading && !draft) return ; if (!draft) return null; return (

Automatisation

Surveillance des dossiers et traitements planifies.

{state.success &&
{state.success}
}
onRunNow("scan")} onChange={(scanSchedule) => onChange({ ...draft, scanSchedule })} /> onRunNow("enrich")} onChange={(enrichSchedule) => onChange({ ...draft, enrichSchedule })} /> ); } function SchedulePanel({ title, schedule, summary, runLabel, onRun, onChange }: { title: string; schedule: AutomationScheduleDto; summary: string; runLabel: string; onRun: () => Promise; onChange: (schedule: AutomationScheduleDto) => void; }) { function patch(next: Partial) { onChange({ ...schedule, ...next }); } return (

{title}

{summary}

{schedule.frequency === "weekly" && ( )}
); } function SaveBar({ dirty, saving, onRefresh }: { dirty: boolean; saving: boolean; onRefresh: () => Promise }) { return ( {dirty ? "Modifications en attente." : "Aucune modification en attente."}
); } function StatusText({ dirty, loading }: { dirty: boolean; loading: boolean }) { if (loading) return chargement; return {dirty ? "non enregistre" : "synchronise"}; } function LoadingPanel({ label }: { label: string }) { return ( ); }