All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m57s
Pricing: 'deepl' removed from plan provider lists and the comparison table row. Landing and pricing copy (13 locales) no longer mention DeepL (65 values cleaned, 39 dead keys deleted: pricing.comparison.deepl, providerTheme.classic.deepl.*). Providers: available-provider responses are filtered so a DeepL entry from the backend can never surface in the engine picker or the services page; DeepL theme entry and static lib/api.ts list entry removed. Admin: DeepL config card, type fields, defaults and fallback-chain mentions removed; stats/chart/status maps and mock data cleaned. Backend adapter untouched — DeepL is invisible app-wide via the UI filter; removing the Python adapter itself is a separate step if wanted. Verified: build exit 0, vitest 9/9, eslint clean on touched files, zero 'deepl' occurrences outside the two intentional UI filters.
159 lines
5.0 KiB
TypeScript
159 lines
5.0 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useTranslationStore } from "@/lib/store";
|
|
import { API_BASE } from "@/lib/config";
|
|
import type { StatsPeriod, TranslationStatsResponse } from "./types";
|
|
|
|
const TIMEOUT_MS = 15000;
|
|
export const REFETCH_INTERVAL_MS = 30000;
|
|
|
|
export const QUERY_KEY = (period: StatsPeriod) => ["admin", "stats", "translations", period];
|
|
|
|
async function fetchTranslationStats(
|
|
adminToken: string | null | undefined,
|
|
period: StatsPeriod
|
|
): Promise<TranslationStatsResponse> {
|
|
if (!adminToken) {
|
|
throw new Error("AUTH_REQUIRED");
|
|
}
|
|
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
|
|
try {
|
|
const response = await fetch(
|
|
`${API_BASE}/api/v1/admin/stats/translations?period=${period}`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${adminToken}`,
|
|
},
|
|
signal: controller.signal,
|
|
}
|
|
);
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401) {
|
|
throw new Error("UNAUTHORIZED");
|
|
}
|
|
if (response.status === 404) {
|
|
throw new Error("ENDPOINT_NOT_FOUND");
|
|
}
|
|
throw new Error(`HTTP_ERROR_${response.status}`);
|
|
}
|
|
|
|
return await response.json();
|
|
} catch (err) {
|
|
clearTimeout(timeoutId);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
function getMockData(period: StatsPeriod): TranslationStatsResponse {
|
|
const baseCount = period === "today" ? 42 : period === "week" ? 287 : 1156;
|
|
const lastPeriodCount = period === "today" ? 38 : period === "week" ? 254 : 1023;
|
|
|
|
return {
|
|
data: {
|
|
period,
|
|
total_translations: baseCount,
|
|
total_translations_last_period: lastPeriodCount,
|
|
error_rate: 2.3,
|
|
error_count: Math.floor(baseCount * 0.023),
|
|
success_count: Math.floor(baseCount * 0.977),
|
|
top_users: [
|
|
{ user_id: "user_1", email: "sarah.chen@acme.com", translation_count: 15 },
|
|
{ user_id: "user_2", email: "marc.dubois@example.fr", translation_count: 12 },
|
|
{ user_id: "user_3", email: "anna.mueller@corp.de", translation_count: 8 },
|
|
{ user_id: "user_4", email: "john.smith@company.uk", translation_count: 6 },
|
|
{ user_id: "user_5", email: "lisa.wong@startup.io", translation_count: 5 },
|
|
{ user_id: "user_6", email: "pierre.leroux@mail.fr", translation_count: 4 },
|
|
{ user_id: "user_7", email: "emma.johnson@tech.us", translation_count: 3 },
|
|
{ user_id: "user_8", email: "klaus.weber@firm.de", translation_count: 2 },
|
|
{ user_id: "user_9", email: "sofia.garcia@empresa.es", translation_count: 2 },
|
|
{ user_id: "user_10", email: "yuki.tanaka@office.jp", translation_count: 1 },
|
|
],
|
|
provider_breakdown: {
|
|
google: { count: Math.floor(baseCount * 0.476), percentage: 47.6 },
|
|
ollama: { count: Math.floor(baseCount * 0.119), percentage: 11.9 },
|
|
openai: { count: Math.floor(baseCount * 0.048), percentage: 4.8 },
|
|
},
|
|
format_breakdown: {
|
|
xlsx: { count: Math.floor(baseCount * 0.595), percentage: 59.5 },
|
|
docx: { count: Math.floor(baseCount * 0.286), percentage: 28.6 },
|
|
pptx: { count: Math.floor(baseCount * 0.119), percentage: 11.9 },
|
|
},
|
|
},
|
|
meta: {
|
|
generated_at: new Date().toISOString(),
|
|
},
|
|
};
|
|
}
|
|
|
|
export function useTranslationStats(period: StatsPeriod = "today") {
|
|
const { settings } = useTranslationStore();
|
|
|
|
const [isMockData, setIsMockData] = React.useState(false);
|
|
|
|
const { data, isLoading, error, refetch } = useQuery({
|
|
queryKey: QUERY_KEY(period),
|
|
queryFn: async () => {
|
|
try {
|
|
const result = await fetchTranslationStats(settings.adminToken, period);
|
|
setIsMockData(false);
|
|
return result;
|
|
} catch (err) {
|
|
if ((err as Error).message === "ENDPOINT_NOT_FOUND") {
|
|
setIsMockData(true);
|
|
return getMockData(period);
|
|
}
|
|
throw err;
|
|
}
|
|
},
|
|
enabled: !!settings.adminToken,
|
|
refetchInterval: REFETCH_INTERVAL_MS,
|
|
staleTime: 10000,
|
|
retry: 1,
|
|
});
|
|
|
|
const getErrorMessage = (err: Error | null): string | null => {
|
|
if (!err) return null;
|
|
|
|
const errorMap: Record<string, string> = {
|
|
AUTH_REQUIRED: "Veuillez vous connecter pour accéder aux statistiques",
|
|
UNAUTHORIZED: "Session expirée. Veuillez vous reconnecter.",
|
|
HTTP_ERROR_403: "Accès refusé. Droits administrateur requis.",
|
|
HTTP_ERROR_500: "Erreur serveur. Veuillez réessayer plus tard.",
|
|
};
|
|
|
|
const code = err.message;
|
|
if (errorMap[code]) {
|
|
return errorMap[code];
|
|
}
|
|
|
|
if (err.name === "AbortError") {
|
|
return "Le serveur met trop de temps à répondre.";
|
|
}
|
|
|
|
if (err.message.includes("fetch") || err.message.includes("network")) {
|
|
return "Impossible de se connecter au serveur.";
|
|
}
|
|
|
|
return "Une erreur inattendue s'est produite.";
|
|
};
|
|
|
|
const errorMessage = error ? getErrorMessage(error as Error) : null;
|
|
|
|
return {
|
|
data: data?.data ?? null,
|
|
isLoading,
|
|
error: errorMessage,
|
|
refetch,
|
|
queryKey: QUERY_KEY(period),
|
|
isMockData,
|
|
};
|
|
}
|