DDMRP-MONITOR / src /api.ts
dyandun's picture
Opening this demo for the public
dbe0232
Raw
History Blame Contribute Delete
3.64 kB
/**
* Cliente API centralizado.
* En dev: Vite hace proxy de /api → http://localhost:8000
* En prod: FastAPI sirve desde el mismo origen.
*
* Modo demo público: las rutas de lectura y el chat LLM no requieren key.
* Las acciones de administración (upload, run-analysis, config) exigen
* X-API-Key; la key se pide solo al ejecutar una de ellas y se guarda
* en localStorage.
*/
const BASE = '/api';
const STORAGE_KEY = 'ddmrp_api_key';
export function hasAdminKey(): boolean {
return !!localStorage.getItem(STORAGE_KEY);
}
export function setAdminKey(key: string) {
localStorage.setItem(STORAGE_KEY, key.trim());
}
export function clearAdminKey() {
localStorage.removeItem(STORAGE_KEY);
}
function requireAdminKey(): string {
let key = localStorage.getItem(STORAGE_KEY) || '';
if (!key) {
key = (window.prompt('Acción de administrador: ingresa la API key.') || '').trim();
if (key) localStorage.setItem(STORAGE_KEY, key);
}
return key;
}
async function apiFetch(
path: string,
init: RequestInit = {},
opts: { admin?: boolean } = {},
): Promise<Response> {
const headers = new Headers(init.headers || {});
if (opts.admin) {
headers.set('X-API-Key', requireAdminKey());
} else {
// Si hay key guardada la adjuntamos (inofensivo en rutas públicas).
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) headers.set('X-API-Key', stored);
}
const res = await fetch(`${BASE}${path}`, { ...init, headers });
if (res.status === 401 && opts.admin) {
clearAdminKey();
alert('API key inválida. Reintenta la acción para ingresarla de nuevo.');
}
return res;
}
async function _json(res: Response) {
if (!res.ok) throw new Error(`API error ${res.status}: ${await res.text()}`);
return res.json();
}
export const api = {
// Dashboard
getSkus: () =>
apiFetch(`/skus`).then(_json),
analyze: (selectedSkus: string[], demandAdjustment: number) =>
apiFetch(`/analyze`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ selectedSkus, demandAdjustment }),
}).then(_json),
// DataUpload (admin)
uploadFile: (tipo: 'demanda-pt' | 'materiales' | 'capacidad', file: File) => {
const form = new FormData();
form.append('file', file);
return apiFetch(`/upload/${tipo}`, { method: 'POST', body: form }, { admin: true }).then(_json);
},
validateAll: () => apiFetch(`/validate`, { method: 'POST' }, { admin: true }).then(_json),
runAnalysis: () =>
apiFetch(`/run-analysis`, { method: 'POST' }, { admin: true }).then(_json),
getRunStatus: (runId: string) =>
apiFetch(`/run-status/${runId}`).then(_json),
// AgenteMeta / BotConfig
getAgentsStatus: () => apiFetch(`/agents/status`).then(_json),
getLogs: () => apiFetch(`/logs`).then(_json),
getProposals: () => apiFetch(`/proposals`).then(_json),
approveProposal: (id: string) =>
apiFetch(`/proposals/${id}/approve`, { method: 'POST' }, { admin: true }).then(_json),
rejectProposal: (id: string) =>
apiFetch(`/proposals/${id}/reject`, { method: 'POST' }, { admin: true }).then(_json),
saveTelegramConfig: (config: {
team_token: string;
team_chat_id: string;
admin_chat_id: string;
proactive_alerts: boolean;
meta_notifications: boolean;
xyz_threshold: string;
}) =>
apiFetch(`/config/telegram`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config),
}, { admin: true }).then(_json),
newAnalysis: () =>
apiFetch(`/new-analysis`, { method: 'POST' }, { admin: true }).then(_json),
};