feat(items): add server field and improve item management

refactor(ai): enhance item structure generation with schema validation
feat(ai): add token usage tracking and cost calculation
fix(ui): improve item card number formatting with locale support
docs(items): add documentation for item value separation
test(items): add tests for value field validation
This commit is contained in:
Evandro Fonseca Junior committed 2025-11-11 03:04:49 -03:00
1 parent dc169f9eba
commit d3704602fd
22 files changed
+1317 -95

No files matched your search

+122 -16
View File
@@ -55,6 +55,7 @@ export function AdminItemManager({ initialData, locale }: AdminItemManagerProps)
const [data, setData] = useState<AdminItemsResponse>(initialData);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [apiStatus, setApiStatus] = useState<{ code: number | null; ok: boolean | null; durationMs: number | null; checkedAt: string | null }>({ code: null, ok: null, durationMs: null, checkedAt: null });
const [serverFilter, setServerFilter] = useState<string>("all");
const [search, setSearch] = useState<string>("");
@@ -66,33 +67,132 @@ export function AdminItemManager({ initialData, locale }: AdminItemManagerProps)
const [modalOpen, setModalOpen] = useState(false);
const [selectedItem, setSelectedItem] = useState<AdminItemRecord | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [activeServers, setActiveServers] = useState<string[]>([]);
const [serversLoading, setServersLoading] = useState<boolean>(false);
const searchDebounce = useRef<number | undefined>(undefined);
function isAdminItemsResponse(val: any): val is AdminItemsResponse {
return (
val &&
Array.isArray(val.items) &&
typeof val.total === "number" &&
typeof val.page === "number" &&
typeof val.pageSize === "number" &&
Array.isArray(val.servers) &&
Array.isArray(val.categories)
);
}
// Carrega servidores ativos independentemente dos itens cadastrados
useEffect(() => {
let cancelled = false;
async function loadServers() {
try {
setServersLoading(true);
const res = await fetch("/api/servers", {
credentials: "include",
headers: { Accept: "application/json" },
cache: "no-store",
});
if (!res.ok) {
console.warn("[AdminItemManager] falha ao buscar servidores ativos:", res.status);
return;
}
const json = await res.json();
const names: string[] = Array.isArray(json?.servers)
? json.servers
.map((s: any) => (typeof s?.serverName === "string" ? s.serverName : null))
.filter((n: string | null): n is string => Boolean(n))
: [];
if (!cancelled) {
setActiveServers(names);
}
} catch (e) {
console.warn("[AdminItemManager] erro ao buscar /api/servers:", e);
} finally {
if (!cancelled) setServersLoading(false);
}
}
loadServers();
return () => {
cancelled = true;
};
}, []);
const fetchItems = useCallback(async () => {
setLoading(true);
setError(null);
try {
const params = new URLSearchParams();
if (search.trim()) params.set("q", search.trim());
if (serverFilter !== "all") params.set("server", serverFilter);
if (categoryFilter !== "all") params.set("category", categoryFilter);
if (rarityFilter !== "all") params.set("rarity", rarityFilter);
params.set("page", String(page));
params.set("pageSize", String(pageSize));
const params = new URLSearchParams();
if (search.trim()) params.set("q", search.trim());
if (serverFilter !== "all") params.set("server", serverFilter);
if (categoryFilter !== "all") params.set("category", categoryFilter);
if (rarityFilter !== "all") params.set("rarity", rarityFilter);
params.set("page", String(page));
params.set("pageSize", String(pageSize));
const res = await fetch(`/api/admin/items?${params.toString()}`, {
const url = `/api/admin/items?${params.toString()}`;
const controller = new AbortController();
const timeoutId = window.setTimeout(() => controller.abort(), 15000);
const start = performance.now();
console.group("[AdminItemManager] fetchItems");
console.log("URL:", url);
console.log("QueryParams:", Object.fromEntries(params.entries()));
try {
const res = await fetch(url, {
credentials: "include",
headers: { Accept: "application/json" },
cache: "no-store",
signal: controller.signal,
});
const durationMs = Math.round(performance.now() - start);
setApiStatus({ code: res.status, ok: res.ok, durationMs, checkedAt: new Date().toISOString() });
console.log("Status:", res.status, res.statusText, "Duration:", `${durationMs}ms`);
console.log("Headers:", {
"content-type": res.headers.get("content-type"),
"x-powered-by": res.headers.get("x-powered-by"),
});
if (!res.ok) {
throw new Error("Não foi possível carregar os itens.");
let errorPayload: any = null;
try {
errorPayload = await res.json();
} catch {
// pode não ser JSON
}
const baseMsg =
res.status === 401
? "Sessão expirada ou não autenticado. Faça login e tente novamente."
: res.status === 403
? "Acesso restrito ao administrador."
: "Não foi possível carregar os itens.";
const apiMsg = errorPayload?.error || errorPayload?.message || null;
const composed = apiMsg ? `${baseMsg} (${apiMsg})` : `${baseMsg} (HTTP ${res.status})`;
console.error("Erro na API:", composed, "Payload:", errorPayload);
throw new Error(composed);
}
const json = await res.json();
if (!isAdminItemsResponse(json)) {
console.error("Resposta inválida da API:", json);
throw new Error("Resposta inválida da API de itens.");
}
const json = (await res.json()) as AdminItemsResponse;
setData(json);
} catch (err) {
console.error(err);
setError(err instanceof Error ? err.message : "Falha ao carregar itens.");
console.log("Itens carregados:", { total: json.total, page: json.page, pageSize: json.pageSize });
} catch (err: any) {
const durationMs = Math.round(performance.now() - start);
setApiStatus((prev) => ({ code: prev.code, ok: false, durationMs, checkedAt: new Date().toISOString() }));
if (err?.name === "AbortError") {
console.error("Requisição abortada por timeout");
setError("Tempo de resposta excedido. Tente novamente.");
} else {
console.error("Falha ao carregar itens:", err);
setError(err instanceof Error ? err.message : "Falha ao carregar itens.");
}
} finally {
console.groupEnd();
window.clearTimeout(timeoutId);
setLoading(false);
}
}, [search, serverFilter, categoryFilter, rarityFilter, page, pageSize]);
@@ -235,8 +335,14 @@ export function AdminItemManager({ initialData, locale }: AdminItemManagerProps)
))}
</SelectContent>
</Select>
<div className="text-sm text-muted-foreground flex items-center">
<div className="text-sm text-muted-foreground flex items-center gap-2">
{loading ? "Carregando..." : `${data.total} itens encontrados`}
{apiStatus.code !== null && (
<Badge variant="outline" className="text-xs">
API: {apiStatus.code} {apiStatus.ok ? "OK" : "Erro"}
{apiStatus.durationMs !== null ? ` • ${apiStatus.durationMs}ms` : ""}
</Badge>
)}
</div>
</div>
</CardContent>
@@ -397,7 +503,7 @@ export function AdminItemManager({ initialData, locale }: AdminItemManagerProps)
mode="edit"
item={selectedItem ?? undefined}
categories={data.categories}
serverOptions={data.servers}
serverOptions={activeServers.length ? activeServers : data.servers}
onSubmit={handleSubmit}
submitting={isSubmitting}
/>