Initial commit: mini-erp (backend FastAPI + frontend React/Vite)
Base para el flujo de ramas por cliente. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
commit
ab3c613165
154 files changed
+16872
No files matched your search
@@ -0,0 +1,91 @@
|
||||
const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
|
||||
|
||||
export const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8000";
|
||||
|
||||
interface SessionData {
|
||||
token?: string;
|
||||
username?: string;
|
||||
company?: string;
|
||||
lastActivity?: number;
|
||||
}
|
||||
|
||||
function getSession(): SessionData {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem("sap_session") || "{}");
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function saveSession(data: SessionData): void {
|
||||
localStorage.setItem("sap_session", JSON.stringify(data));
|
||||
}
|
||||
|
||||
function clearSession(): void {
|
||||
localStorage.removeItem("sap_session");
|
||||
}
|
||||
|
||||
export function getToken(): string | null {
|
||||
return getSession().token || null;
|
||||
}
|
||||
|
||||
export function getUsername(): string | null {
|
||||
return getSession().username || null;
|
||||
}
|
||||
|
||||
export function getCompany(): string | null {
|
||||
return getSession().company || null;
|
||||
}
|
||||
|
||||
export function setSession(token: string, username: string, company?: string): void {
|
||||
saveSession({ token, username, company, lastActivity: Date.now() });
|
||||
}
|
||||
|
||||
function hasTimedOut(): boolean {
|
||||
const session = getSession();
|
||||
if (!session.lastActivity) return false;
|
||||
return Date.now() - session.lastActivity > SESSION_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
function updateLastActivity(): void {
|
||||
const session = getSession();
|
||||
if (session.token) {
|
||||
session.lastActivity = Date.now();
|
||||
saveSession(session);
|
||||
}
|
||||
}
|
||||
|
||||
function expireSession(): void {
|
||||
clearSession();
|
||||
window.location.href = "/login";
|
||||
}
|
||||
|
||||
export async function apiFetch(url: string, options: RequestInit = {}): Promise<Response> {
|
||||
if (hasTimedOut()) {
|
||||
expireSession();
|
||||
throw new Error("Sesión expirada");
|
||||
}
|
||||
|
||||
const token = getToken();
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
...(token ? { "X-Session-Token": token } : {}),
|
||||
};
|
||||
|
||||
const customHeaders = options.headers as Record<string, string> | undefined;
|
||||
if (customHeaders) {
|
||||
for (const key of Object.keys(customHeaders)) {
|
||||
headers[key] = customHeaders[key];
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(url, { ...options, headers });
|
||||
|
||||
if (response.status === 401) {
|
||||
expireSession();
|
||||
throw new Error("Sesión expirada");
|
||||
}
|
||||
|
||||
updateLastActivity();
|
||||
return response;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const PAGE_SIZE = 50;
|
||||
export const DATE_FORMAT = "dd/MM/yyyy";
|
||||
export const DATE_FORMAT_REGEX = /^(\d{2})\/(\d{2})\/(\d{4})$/;
|
||||
export const DEFAULT_CURRENCY = "SOL";
|
||||
export const DEFAULT_SYMBOL = "S/.";
|
||||
@@ -0,0 +1,20 @@
|
||||
import { DATE_FORMAT } from "./constants";
|
||||
|
||||
/**
|
||||
* Formatea una fecha (ISO "yyyy-MM-dd", con o sin hora) para presentación en
|
||||
* tablas y modales, usando el patrón único definido en DATE_FORMAT. Lee los
|
||||
* componentes con getUTC* para no interpretar la fecha en hora local: una
|
||||
* fecha "solo día" como "2026-01-15" se parsea como medianoche UTC, y leerla
|
||||
* con getters locales en husos detrás de UTC (ej. America/Lima) la corre un
|
||||
* día atrás.
|
||||
*/
|
||||
export function formatDisplayDate(value: any): string {
|
||||
if (value === null || value === undefined || value === "") return "";
|
||||
const dateOnly = String(value).split("T")[0];
|
||||
const d = new Date(dateOnly);
|
||||
if (isNaN(d.getTime())) return String(value);
|
||||
const day = String(d.getUTCDate()).padStart(2, "0");
|
||||
const month = String(d.getUTCMonth() + 1).padStart(2, "0");
|
||||
const year = d.getUTCFullYear();
|
||||
return DATE_FORMAT.replace("dd", day).replace("MM", month).replace("yyyy", String(year));
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
export const DOC_TYPE = {
|
||||
dDocument_Items: "Artículos",
|
||||
dDocument_Service: "Servicios",
|
||||
} as const;
|
||||
|
||||
export const DOCUMENT_STATUS = {
|
||||
bost_Open: "Abierto",
|
||||
bost_Close: "Cerrado",
|
||||
} as const;
|
||||
|
||||
export const CARD_TYPE = {
|
||||
cCustomer: "Cliente",
|
||||
cSupplier: "Proveedor",
|
||||
} as const;
|
||||
|
||||
export const ADDRESS_TYPE = {
|
||||
bo_ShipTo: "Dirección de Envío",
|
||||
bo_BillTo: "Dirección de Facturación",
|
||||
} as const;
|
||||
|
||||
export const DOCUMENT_SUB_TYPE = {
|
||||
bod_None: "Ninguno",
|
||||
} as const;
|
||||
|
||||
export const YES_NO = {
|
||||
tYES: "Sí",
|
||||
tNO: "No",
|
||||
} as const;
|
||||
|
||||
export const LINE_STATUS = {
|
||||
bost_Open: "Abierto",
|
||||
bost_Close: "Cerrado",
|
||||
} as const;
|
||||
|
||||
export const ITEM_TYPE = {
|
||||
itItems: "Artículos",
|
||||
itLabor: "Mano de Obra",
|
||||
itTravel: "Viajes",
|
||||
itFixedAssets: "Activos Fijos",
|
||||
} as const;
|
||||
|
||||
export const BATCH_STATUS = {
|
||||
bdsStatus_Released: "Liberado",
|
||||
bdsStatus_NotAccessible: "No Accesible",
|
||||
bdsStatus_Locked: "Bloqueado",
|
||||
} as const;
|
||||
|
||||
export const BATCH_STATUS_APROBACION = {
|
||||
"101": "Cuarentena",
|
||||
"102": "Aprobado",
|
||||
"103": "Rechazado",
|
||||
"104": "Inmovilizado"
|
||||
}
|
||||
|
||||
export const BATCH_STATUS_LIBERACION = {
|
||||
"101": "Liberado",
|
||||
"102": "No liberado"
|
||||
}
|
||||
|
||||
|
||||
export const GL_METHOD = {
|
||||
glm_WH: "Por Almacén",
|
||||
glm_ItemClass: "Por Clase de Artículo",
|
||||
glm_ItemLevel: "Por Artículo",
|
||||
} as const;
|
||||
|
||||
|
||||
export function enumLabel<T extends Record<string, string>>(
|
||||
enumObj: T,
|
||||
key: string | undefined | null,
|
||||
fallback?: string,
|
||||
): string {
|
||||
if (key && key in enumObj) return enumObj[key as keyof T];
|
||||
return fallback ?? key ?? "";
|
||||
}
|
||||
|
||||
export function enumOptions<T extends Record<string, string>>(
|
||||
enumObj: T,
|
||||
): { value: string; label: string }[] {
|
||||
return Object.entries(enumObj).map(([value, label]) => ({ value, label }));
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Column } from "../types/table";
|
||||
import { formatDisplayDate } from "./dateFormat";
|
||||
|
||||
export function formatValue(value: any, column: Column) {
|
||||
if (value === null || value === undefined) {
|
||||
return "---";
|
||||
}
|
||||
|
||||
switch (column.type) {
|
||||
case "currency":
|
||||
return Number(value).toLocaleString("en-US", {
|
||||
minimumFractionDigits: column.decimals ?? 2,
|
||||
|
||||
maximumFractionDigits: column.decimals ?? 2,
|
||||
});
|
||||
|
||||
case "date":
|
||||
return formatDisplayDate(value);
|
||||
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export const OBJ_TYPE: Record<string, number> = {
|
||||
Orders: 17,
|
||||
Invoices: 13,
|
||||
PurchaseInvoices: 18,
|
||||
DeliveryNotes: 15,
|
||||
Quotations: 23,
|
||||
PurchaseOrders: 22,
|
||||
PurchaseDeliveryNotes: 20,
|
||||
PurchaseRequests: 1470000113,
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
const STATUS_COLORS_MAP: Record<string, Record<string, { bg: string; text: string; icon?: string }>> = {
|
||||
BATCH_STATUS_APROBACION: {
|
||||
"101": { bg: "#fff8e1", text: "#f57f17", icon: "⏳" },
|
||||
"102": { bg: "#e8f5e9", text: "#2e7d32", icon: "✅" },
|
||||
"103": { bg: "#fce4ec", text: "#c62828", icon: "❌" },
|
||||
"104": { bg: "#f5f5f5", text: "#616161", icon: "🔒" },
|
||||
},
|
||||
BATCH_STATUS_LIBERACION: {
|
||||
"101": { bg: "#e8f5e9", text: "#2e7d32", icon: "✅" },
|
||||
"102": { bg: "#fff8e1", text: "#f57f17" },
|
||||
},
|
||||
DOCUMENT_STATUS: {
|
||||
"bost_Open": { bg: "#e8f5e9", text: "#2e7d32", icon: "✅" },
|
||||
"bost_Close": { bg: "#fce4ec", text: "#c62828", icon: "🔒" },
|
||||
},
|
||||
};
|
||||
|
||||
export function getStatusColors(
|
||||
enumName: string,
|
||||
value: string | undefined | null,
|
||||
): { bg: string; text: string; icon?: string } | null {
|
||||
if (!value) return null;
|
||||
const group = STATUS_COLORS_MAP[enumName];
|
||||
return group?.[value] ?? null;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
export const DETRACCION_CODIGOS: Record<
|
||||
string,
|
||||
{ descripcion: string; porcentaje: number }
|
||||
> = {
|
||||
"020": {
|
||||
descripcion: "Mantenimiento y reparación de bienes muebles",
|
||||
porcentaje: 12,
|
||||
},
|
||||
"021": { descripcion: "Movimiento de carga", porcentaje: 10 },
|
||||
"022": {
|
||||
descripcion: "Otros servicios empresariales",
|
||||
porcentaje: 12,
|
||||
},
|
||||
"025": {
|
||||
descripcion: "Servicio de transporte de personas",
|
||||
porcentaje: 10,
|
||||
},
|
||||
"027": {
|
||||
descripcion: "Transporte de bienes por vía terrestre",
|
||||
porcentaje: 4,
|
||||
},
|
||||
"037": {
|
||||
descripcion: "Demás servicios gravados con el IGV (uso general)",
|
||||
porcentaje: 12,
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user