panels-origin/api/warehouse_http.ts
Cursor Agent e528d217d3
feat: almacén global, abrir/cerrar por obra y catálogo de materiales
El árbol de Almacén deja de listar todas las obras. El almacén global
siempre está disponible y consolida el stock de los almacenes abiertos.
Las obras abren almacén a demanda y al cerrarlo pueden devolver el
remanente al global. Entradas y traspasos usan el catálogo de materiales
con unidades de medida y uno o varios proveedores.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 20:41:18 +00:00

224 lines
9.6 KiB
TypeScript

import type { Context, Hono } from "hono";
import type { AuthUser } from "./auth.ts";
import { tenantScope } from "./auth.ts";
import { requireCoreAuth } from "./scope.ts";
import type { Db } from "./db.ts";
import { callCoreFn } from "./rpc.ts";
import { respondApiError, respondRpc, routeLabel } from "./http_errors.ts";
import { requirePermission } from "./permissions.ts";
import { denyUnlessProjectScope, denyUnlessWarehouseCentral } from "./scope_enforcement.ts";
import { getUserScope } from "./user_scope.ts";
type App = Hono<{ Variables: { user: AuthUser; db: Db } }>;
function tid(c: { get: (k: "user") => AuthUser }): number {
return tenantScope(c.get("user")) ?? 0;
}
function actor(c: Context): { id: number; name: string } {
const u = c.get("user");
return { id: u.id, name: u.display_name };
}
async function filterWarehouseList(c: Context, env: Awaited<ReturnType<typeof callCoreFn>>) {
const user = c.get("user") as AuthUser;
if (!env.ok || !env.data || user.realm === "platform" || user.is_owner) return env;
const scope = await getUserScope(user);
const warehouses = (env.data as { warehouses?: Array<{ id: number; kind?: string; project_id?: number | null }> }).warehouses ?? [];
const filtered = warehouses.filter((wh) => {
if (wh.kind === "central") return scope.warehouseCentral;
if (wh.kind === "project") {
if (!scope.warehouseProjects) return false;
if (scope.allProjects) return true;
return scope.projectIds.includes(Number(wh.project_id));
}
return true;
});
return { ...env, data: { ...(env.data as object), warehouses: filtered } };
}
export function registerWarehouseRoutes(app: App) {
app.get("/v1/warehouses", ...requireCoreAuth, requirePermission("warehouse.view"), async (c) => {
const db = c.get("db");
const env = await callCoreFn(db, "core.fn_warehouse_list", {
tenant_id: tid(c),
include_closed: c.req.query("include_closed") === "true",
}, { route: routeLabel(c) });
return respondRpc(c, await filterWarehouseList(c, env));
});
app.post("/v1/warehouses/central/ensure", ...requireCoreAuth, requirePermission("warehouse.update"), async (c) => {
if (!await denyUnlessWarehouseCentral(c)) return;
const db = c.get("db");
const env = await callCoreFn(db, "core.fn_warehouse_ensure_central", { tenant_id: tid(c) }, { route: routeLabel(c) });
return respondRpc(c, env);
});
app.post("/v1/projects/:id/warehouse/open", ...requireCoreAuth, requirePermission("warehouse.update"), async (c) => {
const projectId = Number(c.req.param("id"));
if (!await denyUnlessProjectScope(c, projectId)) return;
const db = c.get("db");
const env = await callCoreFn(db, "core.fn_warehouse_open_project", {
tenant_id: tid(c),
project_id: projectId,
}, { route: routeLabel(c) });
return respondRpc(c, env);
});
app.post("/v1/projects/:id/warehouse/close", ...requireCoreAuth, requirePermission("warehouse.update"), async (c) => {
const projectId = Number(c.req.param("id"));
if (!await denyUnlessProjectScope(c, projectId)) return;
const body = await c.req.json<{ force?: boolean; close_notes?: string; transfer_remaining?: boolean }>().catch(
() => ({} as { force?: boolean; close_notes?: string; transfer_remaining?: boolean }),
);
const db = c.get("db");
const a = actor(c);
const env = await callCoreFn(db, "core.fn_warehouse_close_project", {
project_id: projectId,
force: body.force,
transfer_remaining: body.transfer_remaining,
closed_by_name: a.name,
close_notes: body.close_notes,
}, { route: routeLabel(c) });
return respondRpc(c, env);
});
app.get("/v1/warehouses/:id/stock", ...requireCoreAuth, requirePermission("warehouse.view"), async (c) => {
const db = c.get("db");
const whId = Number(c.req.param("id"));
const wh = await db.prepare("SELECT kind, project_id FROM warehouses WHERE id = ?").get(whId) as
| { kind: string; project_id: number | null }
| undefined;
if (!wh) return respondApiError(c, "NOT_FOUND", "Almacén no encontrado", { route: routeLabel(c), warehouse_id: whId });
if (wh.kind === "central") {
if (!await denyUnlessWarehouseCentral(c)) return;
} else if (!await denyUnlessProjectScope(c, wh.project_id)) return;
const env = await callCoreFn(db, "core.fn_warehouse_stock_list", {
tenant_id: tid(c),
warehouse_id: whId,
global: false,
}, { route: routeLabel(c) });
return respondRpc(c, env);
});
app.get("/v1/warehouses/stock/global", ...requireCoreAuth, requirePermission("warehouse.view"), async (c) => {
if (!await denyUnlessWarehouseCentral(c)) return;
const db = c.get("db");
const env = await callCoreFn(db, "core.fn_warehouse_stock_list", {
tenant_id: tid(c),
global: true,
}, { route: routeLabel(c) });
return respondRpc(c, env);
});
app.get("/v1/warehouses/:id/movements", ...requireCoreAuth, requirePermission("warehouse.view"), async (c) => {
const db = c.get("db");
const whId = Number(c.req.param("id"));
const wh = await db.prepare("SELECT kind, project_id FROM warehouses WHERE id = ?").get(whId) as
| { kind: string; project_id: number | null }
| undefined;
if (!wh) return respondApiError(c, "NOT_FOUND", "Almacén no encontrado", { route: routeLabel(c), warehouse_id: whId });
if (wh.kind === "central") {
if (!await denyUnlessWarehouseCentral(c)) return;
} else if (!await denyUnlessProjectScope(c, wh.project_id)) return;
const env = await callCoreFn(db, "core.fn_warehouse_movement_list", {
tenant_id: tid(c),
warehouse_id: whId,
material_id: c.req.query("material_id"),
}, { route: routeLabel(c) });
return respondRpc(c, env);
});
app.get("/v1/warehouses/movements/global", ...requireCoreAuth, requirePermission("warehouse.view"), async (c) => {
if (!await denyUnlessWarehouseCentral(c)) return;
const db = c.get("db");
const env = await callCoreFn(db, "core.fn_warehouse_movement_list", {
tenant_id: tid(c),
global: true,
material_id: c.req.query("material_id"),
}, { route: routeLabel(c) });
return respondRpc(c, env);
});
app.get("/v1/warehouse-materials", ...requireCoreAuth, requirePermission("warehouse.view"), async (c) => {
const db = c.get("db");
const env = await callCoreFn(db, "core.fn_warehouse_material_list", {
tenant_id: tid(c),
}, { route: routeLabel(c) });
return respondRpc(c, env);
});
app.post("/v1/warehouse-materials", ...requireCoreAuth, requirePermission("warehouse.create"), async (c) => {
const body = await c.req.json<Record<string, unknown>>();
const db = c.get("db");
const env = await callCoreFn(db, "core.fn_warehouse_material_upsert", { ...body, tenant_id: tid(c) }, { route: routeLabel(c) });
return respondRpc(c, env);
});
app.patch("/v1/warehouse-materials/:id", ...requireCoreAuth, requirePermission("warehouse.update"), async (c) => {
const body = await c.req.json<Record<string, unknown>>();
const db = c.get("db");
const env = await callCoreFn(db, "core.fn_warehouse_material_upsert", {
...body,
id: Number(c.req.param("id")),
tenant_id: tid(c),
}, { route: routeLabel(c) });
return respondRpc(c, env);
});
app.post("/v1/warehouses/:id/entries", ...requireCoreAuth, requirePermission("warehouse.create"), async (c) => {
const whId = Number(c.req.param("id"));
const db = c.get("db");
const wh = await db.prepare("SELECT kind, project_id FROM warehouses WHERE id = ?").get(whId) as
| { kind: string; project_id: number | null }
| undefined;
if (!wh) return respondApiError(c, "NOT_FOUND", "Almacén no encontrado", { route: routeLabel(c), warehouse_id: whId });
if (wh.kind === "central") {
if (!await denyUnlessWarehouseCentral(c)) return;
} else if (!await denyUnlessProjectScope(c, wh.project_id)) return;
const body = await c.req.json<Record<string, unknown>>();
const a = actor(c);
const env = await callCoreFn(db, "core.fn_warehouse_entry_create", {
...body,
tenant_id: tid(c),
warehouse_id: whId,
created_by_id: a.id,
created_by_name: a.name,
}, { route: routeLabel(c) });
return respondRpc(c, env);
});
app.post("/v1/warehouses/:id/exits", ...requireCoreAuth, requirePermission("warehouse.update"), async (c) => {
const whId = Number(c.req.param("id"));
const db = c.get("db");
const wh = await db.prepare("SELECT kind, project_id FROM warehouses WHERE id = ?").get(whId) as
| { kind: string; project_id: number | null }
| undefined;
if (!wh) return respondApiError(c, "NOT_FOUND", "Almacén no encontrado", { route: routeLabel(c), warehouse_id: whId });
if (wh.kind === "central") {
if (!await denyUnlessWarehouseCentral(c)) return;
} else if (!await denyUnlessProjectScope(c, wh.project_id)) return;
const body = await c.req.json<Record<string, unknown>>();
const a = actor(c);
const env = await callCoreFn(db, "core.fn_warehouse_exit_create", {
...body,
tenant_id: tid(c),
warehouse_id: whId,
created_by_id: a.id,
created_by_name: a.name,
}, { route: routeLabel(c) });
return respondRpc(c, env);
});
app.post("/v1/warehouse-transfers", ...requireCoreAuth, requirePermission("warehouse.update"), async (c) => {
const body = await c.req.json<Record<string, unknown>>();
const db = c.get("db");
const a = actor(c);
const env = await callCoreFn(db, "core.fn_warehouse_transfer_create", {
...body,
tenant_id: tid(c),
created_by_name: a.name,
}, { route: routeLabel(c) });
return respondRpc(c, env);
});
}