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:
1 parent
dc169f9eba
commit
d3704602fd
22 files changed
+1317
-95
No files matched your search
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user