This commit is contained in:
Lucas Tettamanti
2026-01-18 20:07:40 -03:00
parent 23c3d44490
commit 9754347a36
11 changed files with 1516 additions and 6 deletions

View File

@@ -8,6 +8,8 @@ import "./components/products-crud.js";
import "./components/aliases-crud.js";
import "./components/recommendations-crud.js";
import "./components/quantities-crud.js";
import "./components/orders-crud.js";
import "./components/test-panel.js";
import { connectSSE } from "./lib/sse.js";
connectSSE();

View File

@@ -45,6 +45,8 @@ class OpsShell extends HTMLElement {
<button class="nav-btn" data-view="aliases">Equivalencias</button>
<button class="nav-btn" data-view="crosssell">Cross-sell</button>
<button class="nav-btn" data-view="quantities">Cantidades</button>
<button class="nav-btn" data-view="orders">Pedidos</button>
<button class="nav-btn" data-view="test">Test</button>
</nav>
<div class="spacer"></div>
<div class="status" id="sseStatus">SSE: connecting…</div>
@@ -93,6 +95,18 @@ class OpsShell extends HTMLElement {
<quantities-crud></quantities-crud>
</div>
</div>
<div id="viewOrders" class="view">
<div class="layout-crud">
<orders-crud></orders-crud>
</div>
</div>
<div id="viewTest" class="view">
<div class="layout-crud">
<test-panel></test-panel>
</div>
</div>
</div>
`;
}

View File

@@ -0,0 +1,468 @@
import { api } from "../lib/api.js";
function formatDate(dateStr) {
if (!dateStr) return "—";
const d = new Date(dateStr);
return d.toLocaleString("es-AR", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
}
function statusLabel(status) {
const map = {
pending: "Pendiente",
processing: "Procesando",
"on-hold": "En espera",
completed: "Completado",
cancelled: "Cancelado",
refunded: "Reembolsado",
failed: "Fallido",
};
return map[status] || status;
}
function statusColor(status) {
const map = {
pending: "#f59e0b",
processing: "#3b82f6",
"on-hold": "#8b5cf6",
completed: "#22c55e",
cancelled: "#6b7280",
refunded: "#ec4899",
failed: "#ef4444",
};
return map[status] || "#8aa0b5";
}
class OrdersCrud extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.orders = [];
this.selectedOrder = null;
this.loading = false;
this.shadowRoot.innerHTML = `
<style>
:host {
--bg: #0b0f14;
--panel: #121823;
--muted: #8aa0b5;
--text: #e7eef7;
--line: #1e2a3a;
--blue: #1f6feb;
--green: #238636;
--red: #da3633;
--orange: #f59e0b;
}
* { box-sizing: border-box; font-family: system-ui, Segoe UI, Roboto, Arial; }
.container {
height: 100%;
background: var(--bg);
color: var(--text);
display: grid;
grid-template-columns: 1fr 400px;
gap: 16px;
padding: 16px;
overflow: hidden;
}
.panel {
background: var(--panel);
border-radius: 8px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
overflow: hidden;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--line);
padding-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
button {
background: var(--blue);
border: none;
color: white;
padding: 8px 16px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
transition: opacity 0.15s;
}
button:hover { opacity: 0.9; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
button.secondary {
background: transparent;
border: 1px solid var(--line);
color: var(--muted);
}
button.secondary:hover { border-color: var(--blue); color: var(--text); }
button.small { padding: 4px 8px; font-size: 11px; }
.empty { color: var(--muted); font-size: 12px; text-align: center; padding: 40px; }
/* Orders table */
.orders-table {
flex: 1;
overflow-y: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
th {
text-align: left;
padding: 10px 8px;
border-bottom: 2px solid var(--line);
color: var(--muted);
font-weight: 600;
text-transform: uppercase;
font-size: 10px;
position: sticky;
top: 0;
background: var(--panel);
}
td {
padding: 10px 8px;
border-bottom: 1px solid var(--line);
vertical-align: middle;
}
tr { cursor: pointer; transition: background 0.15s; }
tr:hover { background: rgba(31,111,235,0.1); }
tr.selected { background: rgba(31,111,235,0.2); }
.order-id { font-weight: 700; }
.badges { display: flex; gap: 4px; flex-wrap: wrap; }
.badge {
padding: 2px 6px;
border-radius: 4px;
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
}
.badge.test { background: var(--orange); color: #000; }
.badge.real { background: var(--green); color: #fff; }
.badge.whatsapp { background: #25d366; color: #fff; }
.badge.web { background: var(--muted); color: #000; }
.status-badge {
padding: 3px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
color: #fff;
display: inline-block;
}
.total { font-weight: 600; }
.customer-name {
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Detail panel */
.detail-section {
margin-bottom: 16px;
}
.detail-title {
font-size: 11px;
font-weight: 600;
color: var(--blue);
text-transform: uppercase;
margin-bottom: 8px;
}
.detail-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid var(--line);
font-size: 12px;
}
.detail-row:last-child { border-bottom: none; }
.detail-label { color: var(--muted); }
.detail-value { font-weight: 500; }
.items-list {
background: var(--bg);
border: 1px solid var(--line);
border-radius: 6px;
padding: 8px;
max-height: 200px;
overflow-y: auto;
}
.item-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid var(--line);
font-size: 12px;
}
.item-row:last-child { border-bottom: none; }
.item-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.item-qty { color: var(--muted); margin: 0 8px; }
.item-total { font-weight: 600; }
.detail-empty {
color: var(--muted);
font-size: 12px;
text-align: center;
padding: 60px 20px;
}
</style>
<div class="container">
<div class="panel">
<div class="panel-title">
<span>Pedidos de WooCommerce</span>
<button id="btnRefresh" class="secondary small">Actualizar</button>
</div>
<div class="orders-table" id="ordersTable">
<div class="empty">Cargando pedidos...</div>
</div>
</div>
<div class="panel">
<div class="panel-title">Detalle del Pedido</div>
<div id="orderDetail">
<div class="detail-empty">Seleccioná un pedido para ver los detalles</div>
</div>
</div>
</div>
`;
}
connectedCallback() {
this.shadowRoot.getElementById("btnRefresh").onclick = () => this.loadOrders();
this.loadOrders();
}
async loadOrders() {
const container = this.shadowRoot.getElementById("ordersTable");
container.innerHTML = `<div class="empty">Cargando pedidos...</div>`;
try {
const result = await api.listRecentOrders({ limit: 50 });
this.orders = result.items || [];
this.renderTable();
} catch (e) {
console.error("[orders-crud] Error loading orders:", e);
container.innerHTML = `<div class="empty">Error cargando pedidos: ${e.message}</div>`;
}
}
renderTable() {
const container = this.shadowRoot.getElementById("ordersTable");
if (this.orders.length === 0) {
container.innerHTML = `<div class="empty">No hay pedidos</div>`;
return;
}
container.innerHTML = `
<table>
<thead>
<tr>
<th>#</th>
<th>Tipo</th>
<th>Estado</th>
<th>Envío</th>
<th>Pago</th>
<th>Cliente</th>
<th>Total</th>
<th>Fecha</th>
</tr>
</thead>
<tbody>
${this.orders.map(order => {
const isSelected = this.selectedOrder?.id === order.id;
const customerName = [order.billing.first_name, order.billing.last_name].filter(Boolean).join(" ") || order.billing.phone || "—";
return `
<tr class="${isSelected ? "selected" : ""}" data-order-id="${order.id}">
<td class="order-id">${order.id}</td>
<td>
<div class="badges">
${order.is_test ? '<span class="badge test">TEST</span>' : '<span class="badge real">REAL</span>'}
${order.source === "whatsapp" ? '<span class="badge whatsapp">WA</span>' : '<span class="badge web">WEB</span>'}
</div>
</td>
<td><span class="status-badge" style="background:${statusColor(order.status)}">${statusLabel(order.status)}</span></td>
<td><span class="badge" style="background:${order.is_delivery ? '#3b82f6' : '#8b5cf6'};color:#fff">${order.is_delivery ? 'DEL' : 'RET'}</span></td>
<td>
<div class="badges">
<span class="badge" style="background:${order.is_cash ? '#f59e0b' : '#1f6feb'};color:${order.is_cash ? '#000' : '#fff'}">${order.is_cash ? '$' : 'MP'}</span>
<span class="badge" style="background:${order.is_paid ? '#22c55e' : '#ef4444'};color:#fff">${order.is_paid ? '✓' : '✗'}</span>
</div>
</td>
<td class="customer-name" title="${customerName}">${customerName}</td>
<td class="total">$${Number(order.total || 0).toLocaleString("es-AR")}</td>
<td>${formatDate(order.date_created)}</td>
</tr>
`;
}).join("")}
</tbody>
</table>
`;
container.querySelectorAll("tr[data-order-id]").forEach(row => {
row.onclick = () => {
const orderId = parseInt(row.dataset.orderId);
const order = this.orders.find(o => o.id === orderId);
if (order) {
this.selectOrder(order);
}
};
});
}
selectOrder(order) {
this.selectedOrder = order;
this.renderTable();
this.renderDetail();
}
renderDetail() {
const container = this.shadowRoot.getElementById("orderDetail");
if (!this.selectedOrder) {
container.innerHTML = `<div class="detail-empty">Seleccioná un pedido para ver los detalles</div>`;
return;
}
const order = this.selectedOrder;
const customerName = [order.billing.first_name, order.billing.last_name].filter(Boolean).join(" ") || "—";
// Construir dirección de envío
const shippingAddr = [
order.shipping?.address_1,
order.shipping?.address_2,
order.shipping?.city,
order.shipping?.state,
order.shipping?.postcode
].filter(Boolean).join(", ");
const billingAddr = [
order.billing?.address_1,
order.billing?.address_2,
order.billing?.city,
order.billing?.state,
order.billing?.postcode
].filter(Boolean).join(", ");
const address = shippingAddr || billingAddr || "—";
container.innerHTML = `
<div class="detail-section">
<div class="detail-title">Información General</div>
<div class="detail-row">
<span class="detail-label">Pedido #</span>
<span class="detail-value">${order.id}</span>
</div>
<div class="detail-row">
<span class="detail-label">Estado</span>
<span class="status-badge" style="background:${statusColor(order.status)}">${statusLabel(order.status)}</span>
</div>
<div class="detail-row">
<span class="detail-label">Tipo</span>
<span class="detail-value">${order.is_test ? "Test" : "Real"}${order.source === "whatsapp" ? "WhatsApp" : "Web"}</span>
</div>
<div class="detail-row">
<span class="detail-label">Fecha</span>
<span class="detail-value">${formatDate(order.date_created)}</span>
</div>
<div class="detail-row">
<span class="detail-label">Total</span>
<span class="detail-value" style="font-size:16px;color:var(--green)">$${Number(order.total || 0).toLocaleString("es-AR")} ${order.currency || ""}</span>
</div>
</div>
<div class="detail-section">
<div class="detail-title">Envío</div>
<div class="detail-row">
<span class="detail-label">Método</span>
<span class="detail-value">
<span class="badge ${order.is_delivery ? 'delivery' : 'pickup'}" style="background:${order.is_delivery ? '#3b82f6' : '#8b5cf6'};color:#fff;padding:3px 8px;border-radius:4px;font-size:10px;">
${order.is_delivery ? 'DELIVERY' : 'RETIRO'}
</span>
${order.shipping_method ? `<span style="margin-left:8px;color:var(--muted);font-size:11px;">${order.shipping_method}</span>` : ''}
</span>
</div>
${order.is_delivery && address !== "—" ? `
<div class="detail-row">
<span class="detail-label">Dirección</span>
<span class="detail-value" style="font-size:11px;">${address}</span>
</div>
` : ''}
</div>
<div class="detail-section">
<div class="detail-title">Pago</div>
<div class="detail-row">
<span class="detail-label">Método</span>
<span class="detail-value">
<span class="badge" style="background:${order.is_cash ? '#f59e0b' : '#1f6feb'};color:${order.is_cash ? '#000' : '#fff'};padding:3px 8px;border-radius:4px;font-size:10px;">
${order.is_cash ? 'EFECTIVO' : 'LINK'}
</span>
${order.payment_method_title ? `<span style="margin-left:8px;color:var(--muted);font-size:11px;">${order.payment_method_title}</span>` : ''}
</span>
</div>
<div class="detail-row">
<span class="detail-label">Estado</span>
<span class="detail-value">
<span class="badge" style="background:${order.is_paid ? '#22c55e' : '#ef4444'};color:#fff;padding:3px 8px;border-radius:4px;font-size:10px;">
${order.is_paid ? 'PAGADO' : 'PENDIENTE'}
</span>
${order.date_paid ? `<span style="margin-left:8px;color:var(--muted);font-size:11px;">${formatDate(order.date_paid)}</span>` : ''}
</span>
</div>
</div>
<div class="detail-section">
<div class="detail-title">Cliente</div>
<div class="detail-row">
<span class="detail-label">Nombre</span>
<span class="detail-value">${customerName}</span>
</div>
<div class="detail-row">
<span class="detail-label">Teléfono</span>
<span class="detail-value">${order.billing.phone || "—"}</span>
</div>
<div class="detail-row">
<span class="detail-label">Email</span>
<span class="detail-value">${order.billing.email || "—"}</span>
</div>
</div>
<div class="detail-section">
<div class="detail-title">Productos (${order.line_items.length})</div>
<div class="items-list">
${order.line_items.length === 0 ? '<div style="color:var(--muted);text-align:center;padding:20px;">Sin productos</div>' :
order.line_items.map(item => `
<div class="item-row">
<span class="item-name" title="${item.name}">${item.name}</span>
<span class="item-qty">x${item.quantity}</span>
<span class="item-total">$${Number(item.total || 0).toLocaleString("es-AR")}</span>
</div>
`).join("")
}
</div>
</div>
${order.run_id ? `
<div class="detail-section">
<div class="detail-title">Metadata</div>
<div class="detail-row">
<span class="detail-label">Run ID</span>
<span class="detail-value" style="font-family:monospace;font-size:10px;">${order.run_id}</span>
</div>
</div>
` : ""}
`;
}
}
customElements.define("orders-crud", OrdersCrud);

View File

@@ -0,0 +1,533 @@
import { api } from "../lib/api.js";
// Datos aleatorios para generar usuarios de prueba
const NOMBRES = ["Juan", "María", "Carlos", "Ana", "Pedro", "Laura", "Diego", "Sofía"];
const APELLIDOS = ["García", "Rodríguez", "Martínez", "López", "González", "Fernández", "Pérez"];
const CALLES = ["Av. Corrientes", "Av. Santa Fe", "Calle Florida", "Av. Rivadavia", "Av. Cabildo", "Av. Libertador"];
function randomItem(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function generateTestUser() {
const randomPhone = `549${randomInt(1000000000, 9999999999)}`;
const wa_chat_id = `${randomPhone}@s.whatsapp.net`;
const nombre = randomItem(NOMBRES);
const apellido = randomItem(APELLIDOS);
return {
wa_chat_id,
phone: randomPhone,
name: `${nombre} ${apellido}`,
address: {
first_name: nombre,
last_name: apellido,
address_1: `${randomItem(CALLES)} ${randomInt(100, 9000)}`,
city: "CABA",
state: "Buenos Aires",
postcode: `${randomInt(1000, 1999)}`,
country: "AR",
phone: randomPhone,
email: `${randomPhone}@no-email.local`,
},
};
}
class TestPanel extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.products = [];
this.selectedProducts = [];
this.testUser = null;
this.lastOrder = null;
this.lastPaymentLink = null;
this.loading = false;
this.shadowRoot.innerHTML = `
<style>
:host {
--bg: #0b0f14;
--panel: #121823;
--muted: #8aa0b5;
--text: #e7eef7;
--line: #1e2a3a;
--blue: #1f6feb;
--green: #238636;
--red: #da3633;
}
* { box-sizing: border-box; font-family: system-ui, Segoe UI, Roboto, Arial; }
.container {
height: 100%;
background: var(--bg);
color: var(--text);
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
padding: 16px;
overflow: auto;
}
.panel {
background: var(--panel);
border-radius: 8px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--line);
padding-bottom: 8px;
}
.section {
display: flex;
flex-direction: column;
gap: 8px;
}
.section-title {
font-size: 12px;
font-weight: 600;
color: var(--blue);
}
button {
background: var(--blue);
border: none;
color: white;
padding: 8px 16px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
transition: opacity 0.15s;
}
button:hover { opacity: 0.9; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
button.secondary {
background: transparent;
border: 1px solid var(--line);
color: var(--muted);
}
button.secondary:hover { border-color: var(--blue); color: var(--text); }
button.success { background: var(--green); }
input, select {
background: var(--bg);
border: 1px solid var(--line);
color: var(--text);
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
}
input:focus, select:focus {
outline: none;
border-color: var(--blue);
}
.product-list {
max-height: 200px;
overflow-y: auto;
border: 1px solid var(--line);
border-radius: 6px;
}
.product-item {
display: grid;
grid-template-columns: 1fr 80px 60px 30px;
gap: 8px;
padding: 8px;
border-bottom: 1px solid var(--line);
align-items: center;
font-size: 12px;
}
.product-item:last-child { border-bottom: none; }
.product-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.product-qty { text-align: right; }
.product-unit { color: var(--muted); }
.remove-btn {
background: var(--red);
padding: 4px 8px;
font-size: 10px;
}
.user-info {
background: var(--bg);
border: 1px solid var(--line);
border-radius: 6px;
padding: 12px;
font-size: 12px;
}
.user-info div { margin-bottom: 4px; }
.user-info span { color: var(--muted); }
.result {
background: var(--bg);
border: 1px solid var(--line);
border-radius: 6px;
padding: 12px;
font-size: 12px;
}
.result.success { border-color: var(--green); }
.result.error { border-color: var(--red); }
.result-label { color: var(--muted); font-size: 10px; text-transform: uppercase; }
.result-value { font-weight: 600; margin-top: 4px; }
.result-value.big { font-size: 18px; }
.result-link {
color: var(--blue);
text-decoration: underline;
cursor: pointer;
word-break: break-all;
}
.row { display: flex; gap: 8px; align-items: center; }
.flex-1 { flex: 1; }
.loading { opacity: 0.5; pointer-events: none; }
.empty { color: var(--muted); font-size: 12px; text-align: center; padding: 20px; }
</style>
<div class="container">
<div class="panel">
<div class="panel-title">1. Generar Orden de Prueba</div>
<div class="section">
<div class="row">
<button id="btnGenerate">Generar Orden Aleatoria</button>
<button id="btnClear" class="secondary">Limpiar</button>
</div>
</div>
<div class="section">
<div class="section-title">Productos seleccionados</div>
<div class="product-list" id="productList">
<div class="empty">Click "Generar Orden Aleatoria" para comenzar</div>
</div>
</div>
<div class="section">
<div class="section-title">Datos del usuario</div>
<div class="user-info" id="userInfo">
<div class="empty">Se generarán automáticamente</div>
</div>
</div>
<div class="section">
<button id="btnCreateOrder" class="success" disabled>Crear Orden en WooCommerce</button>
</div>
<div class="section" id="orderResult" style="display:none;">
<div class="result success">
<div class="result-label">Orden creada</div>
<div class="result-value big" id="orderIdValue">—</div>
<div style="margin-top:8px;">
<span class="result-label">Total:</span>
<span id="orderTotalValue">—</span>
</div>
</div>
</div>
</div>
<div class="panel">
<div class="panel-title">2. Link de Pago (MercadoPago)</div>
<div class="section">
<div class="section-title">Monto</div>
<div class="row">
<input type="number" id="inputAmount" placeholder="Monto en ARS" class="flex-1" />
<button id="btnPaymentLink" disabled>Generar Link de Pago</button>
</div>
</div>
<div class="section" id="paymentResult" style="display:none;">
<div class="result success">
<div class="result-label">Link de pago</div>
<a class="result-link" id="paymentLinkValue" href="#" target="_blank">—</a>
<div style="margin-top:8px;">
<span class="result-label">Preference ID:</span>
<span id="preferenceIdValue">—</span>
</div>
</div>
</div>
<div class="panel-title" style="margin-top:24px;">3. Simular Pago Exitoso</div>
<div class="section">
<p style="font-size:12px;color:var(--muted);margin:0;">
Simula el webhook de MercadoPago con status "approved".
Esto actualiza la orden en WooCommerce a "processing".
</p>
<button id="btnSimulateWebhook" disabled>Simular Pago Exitoso</button>
</div>
<div class="section" id="webhookResult" style="display:none;">
<div class="result success">
<div class="result-label">Pago simulado</div>
<div class="result-value" id="webhookStatusValue">—</div>
<div style="margin-top:8px;">
<span class="result-label">Orden status:</span>
<span id="webhookOrderStatusValue">—</span>
</div>
</div>
</div>
</div>
</div>
`;
}
connectedCallback() {
this.shadowRoot.getElementById("btnGenerate").onclick = () => this.generateRandomOrder();
this.shadowRoot.getElementById("btnClear").onclick = () => this.clearAll();
this.shadowRoot.getElementById("btnCreateOrder").onclick = () => this.createOrder();
this.shadowRoot.getElementById("btnPaymentLink").onclick = () => this.createPaymentLink();
this.shadowRoot.getElementById("btnSimulateWebhook").onclick = () => this.simulateWebhook();
this.loadProducts();
}
async loadProducts() {
try {
const result = await api.getProductsWithStock();
this.products = result.items || [];
console.log(`[test-panel] Loaded ${this.products.length} products with stock`);
} catch (e) {
console.error("[test-panel] Error loading products:", e);
}
}
async generateRandomOrder() {
if (this.products.length === 0) {
await this.loadProducts();
}
if (this.products.length === 0) {
alert("No hay productos con stock disponible");
return;
}
// Generar usuario aleatorio
this.testUser = generateTestUser();
// Seleccionar 1-3 productos aleatorios
const numProducts = randomInt(1, Math.min(3, this.products.length));
const shuffled = [...this.products].sort(() => Math.random() - 0.5);
this.selectedProducts = [];
for (let i = 0; i < numProducts; i++) {
const product = shuffled[i];
const isKg = product.sell_unit === "kg";
const quantity = isKg ? randomInt(200, 2000) : randomInt(1, 5);
this.selectedProducts.push({
product_id: product.woo_product_id,
name: product.name,
quantity,
unit: isKg ? "kg" : "unit",
price: product.price,
});
}
this.renderProductList();
this.renderUserInfo();
this.updateButtonStates();
}
renderProductList() {
const container = this.shadowRoot.getElementById("productList");
if (this.selectedProducts.length === 0) {
container.innerHTML = `<div class="empty">Click "Generar Orden Aleatoria" para comenzar</div>`;
return;
}
container.innerHTML = this.selectedProducts.map((p, i) => `
<div class="product-item">
<div class="product-name" title="${p.name}">${p.name}</div>
<div class="product-qty">${p.unit === "kg" ? `${p.quantity}g` : `${p.quantity}u`}</div>
<div class="product-unit">$${Number(p.price || 0).toFixed(0)}</div>
<button class="remove-btn" data-index="${i}">X</button>
</div>
`).join("");
container.querySelectorAll(".remove-btn").forEach(btn => {
btn.onclick = (e) => {
const idx = parseInt(e.target.dataset.index);
this.selectedProducts.splice(idx, 1);
this.renderProductList();
this.updateButtonStates();
};
});
}
renderUserInfo() {
const container = this.shadowRoot.getElementById("userInfo");
if (!this.testUser) {
container.innerHTML = `<div class="empty">Se generarán automáticamente</div>`;
return;
}
const addr = this.testUser.address;
container.innerHTML = `
<div><span>Nombre:</span> ${addr.first_name} ${addr.last_name}</div>
<div><span>Dirección:</span> ${addr.address_1}</div>
<div><span>Ciudad:</span> ${addr.city}, ${addr.state}</div>
<div><span>Teléfono:</span> ${addr.phone}</div>
<div><span>Email:</span> ${addr.email}</div>
`;
}
updateButtonStates() {
const hasProducts = this.selectedProducts.length > 0;
const hasOrder = this.lastOrder?.woo_order_id;
const hasPaymentLink = this.lastPaymentLink?.init_point;
this.shadowRoot.getElementById("btnCreateOrder").disabled = !hasProducts;
this.shadowRoot.getElementById("btnPaymentLink").disabled = !hasOrder;
this.shadowRoot.getElementById("btnSimulateWebhook").disabled = !hasOrder;
if (hasOrder) {
this.shadowRoot.getElementById("inputAmount").value = this.lastOrder.total || "";
}
}
async createOrder() {
if (this.loading) return;
this.loading = true;
const btn = this.shadowRoot.getElementById("btnCreateOrder");
btn.disabled = true;
btn.textContent = "Creando...";
try {
const basket = {
items: this.selectedProducts.map(p => ({
product_id: p.product_id,
quantity: p.quantity,
unit: p.unit,
})),
};
const result = await api.createTestOrder({
basket,
address: this.testUser?.address || null,
wa_chat_id: this.testUser?.wa_chat_id || null,
});
if (result.ok) {
this.lastOrder = result;
this.shadowRoot.getElementById("orderIdValue").textContent = `#${result.woo_order_id}`;
this.shadowRoot.getElementById("orderTotalValue").textContent = `$${Number(result.total || 0).toFixed(2)}`;
this.shadowRoot.getElementById("orderResult").style.display = "block";
this.shadowRoot.getElementById("inputAmount").value = result.total || "";
} else {
alert("Error: " + (result.error || "Error desconocido"));
}
} catch (e) {
console.error("[test-panel] createOrder error:", e);
alert("Error creando orden: " + e.message);
} finally {
this.loading = false;
btn.textContent = "Crear Orden en WooCommerce";
this.updateButtonStates();
}
}
async createPaymentLink() {
if (this.loading) return;
if (!this.lastOrder?.woo_order_id) {
alert("Primero creá una orden");
return;
}
const amount = parseFloat(this.shadowRoot.getElementById("inputAmount").value);
if (!amount || amount <= 0) {
alert("Ingresá un monto válido");
return;
}
this.loading = true;
const btn = this.shadowRoot.getElementById("btnPaymentLink");
btn.disabled = true;
btn.textContent = "Generando...";
try {
const result = await api.createPaymentLink({
woo_order_id: this.lastOrder.woo_order_id,
amount,
});
if (result.ok) {
this.lastPaymentLink = result;
const linkEl = this.shadowRoot.getElementById("paymentLinkValue");
linkEl.href = result.init_point || result.sandbox_init_point || "#";
linkEl.textContent = result.init_point || result.sandbox_init_point || "—";
this.shadowRoot.getElementById("preferenceIdValue").textContent = result.preference_id || "—";
this.shadowRoot.getElementById("paymentResult").style.display = "block";
} else {
alert("Error: " + (result.error || "Error desconocido"));
}
} catch (e) {
console.error("[test-panel] createPaymentLink error:", e);
alert("Error generando link: " + e.message);
} finally {
this.loading = false;
btn.textContent = "Generar Link de Pago";
this.updateButtonStates();
}
}
async simulateWebhook() {
if (this.loading) return;
if (!this.lastOrder?.woo_order_id) {
alert("Primero creá una orden");
return;
}
this.loading = true;
const btn = this.shadowRoot.getElementById("btnSimulateWebhook");
btn.disabled = true;
btn.textContent = "Simulando...";
try {
const amount = parseFloat(this.shadowRoot.getElementById("inputAmount").value) || this.lastOrder.total || 0;
const result = await api.simulateMpWebhook({
woo_order_id: this.lastOrder.woo_order_id,
amount,
});
if (result.ok) {
this.shadowRoot.getElementById("webhookStatusValue").textContent = `Payment ${result.payment_id} - ${result.status}`;
this.shadowRoot.getElementById("webhookOrderStatusValue").textContent = result.order_status || "processing";
this.shadowRoot.getElementById("webhookResult").style.display = "block";
} else {
alert("Error: " + (result.error || "Error desconocido"));
}
} catch (e) {
console.error("[test-panel] simulateWebhook error:", e);
alert("Error simulando webhook: " + e.message);
} finally {
this.loading = false;
btn.textContent = "Simular Pago Exitoso";
this.updateButtonStates();
}
}
clearAll() {
this.selectedProducts = [];
this.testUser = null;
this.lastOrder = null;
this.lastPaymentLink = null;
this.renderProductList();
this.renderUserInfo();
this.updateButtonStates();
this.shadowRoot.getElementById("orderResult").style.display = "none";
this.shadowRoot.getElementById("paymentResult").style.display = "none";
this.shadowRoot.getElementById("webhookResult").style.display = "none";
this.shadowRoot.getElementById("inputAmount").value = "";
}
}
customElements.define("test-panel", TestPanel);

View File

@@ -184,4 +184,39 @@ export const api = {
async syncFromWoo() {
return fetch("/products/sync-from-woo", { method: "POST" }).then(r => r.json());
},
// --- Testing ---
async listRecentOrders({ limit = 20 } = {}) {
const u = new URL("/test/orders", location.origin);
u.searchParams.set("limit", String(limit));
return fetch(u).then(r => r.json());
},
async getProductsWithStock() {
return fetch("/test/products-with-stock").then(r => r.json());
},
async createTestOrder({ basket, address, wa_chat_id }) {
return fetch("/test/order", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ basket, address, wa_chat_id }),
}).then(r => r.json());
},
async createPaymentLink({ woo_order_id, amount }) {
return fetch("/test/payment-link", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ woo_order_id, amount }),
}).then(r => r.json());
},
async simulateMpWebhook({ woo_order_id, amount }) {
return fetch("/test/simulate-webhook", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ woo_order_id, amount }),
}).then(r => r.json());
},
};