<?php
<section id="gx-prod-panel">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
/* --- CONFIGURACIÓN VISUAL (RESTAURADO) --- */
#gx-prod-panel {
--bg-app: #f8fafc; --bg-panel: #ffffff;
--color-text: #334155; --border: #e2e8f0;
/* Colores Vivos */
--c-teal: #0f766e; --bg-teal-light: #f0fdfa;
--c-blue: #2563eb; --c-green: #16a34a;
--c-red: #dc2626; --c-hold: #d97706; --c-purple: #7c3aed;
width: 100% !important; max-width: 100% !important; margin: 0 auto !important;
background: var(--bg-app); color: var(--color-text);
font-family: 'Inter', sans-serif; font-size: 12px;
box-sizing: border-box;
}
#gx-prod-panel * { box-sizing: border-box; outline: none; }
#gx-prod-panel .gx-main-card {
width: 98%; margin: 10px auto; background: var(--bg-panel);
border: 1px solid var(--border); border-radius: 6px;
display: flex; flex-direction: column; height: 92vh; overflow: hidden;
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);
}
/* HEADER COMPACTO */
.gx-header {
padding: 8px 15px; border-bottom: 1px solid var(--border); background: #fff;
display: flex; justify-content: space-between; align-items: center; flex-shrink: 0;
height: 40px;
}
.gx-header h1 { font-size: 1.1rem; margin: 0; color: #0f172a; font-weight: 700; }
.gx-kpi-row { display: flex; gap: 15px; font-size: 0.8rem; font-weight: 600; }
/* --- TOOLBAR HORIZONTAL --- */
.gx-toolbar {
padding: 8px 15px; background: #f1f5f9; border-bottom: 1px solid var(--border);
display: flex; flex-direction: row; align-items: center; gap: 10px;
flex-shrink: 0; overflow-x: auto; flex-wrap: nowrap; white-space: nowrap;
}
.gx-input {
height: 28px; padding: 0 8px; border: 1px solid #cbd5e1; border-radius: 4px;
font-size: 0.75rem; background: #fff; color: #334155;
border-left: 3px solid var(--c-teal);
}
.gx-input:focus { border-color: var(--c-teal); outline: none; }
#gx-search { width: 200px; flex-shrink: 0; }
#gx-filter-status { width: 130px; flex-shrink: 0; }
#gx-filter-date { width: 130px; flex-shrink: 0; }
#gx-filter-sort { width: 130px; flex-shrink: 0; }
.gx-btn-refresh {
height: 28px; padding: 0 15px; background: var(--c-teal); color: #fff;
border: none; border-radius: 4px; cursor: pointer; font-weight: 600;
white-space: nowrap; flex-shrink: 0; margin-left: auto;
}
/* --- TABLA --- */
.gx-table-wrap { flex: 1; overflow: auto; background: #fff; }
table { width: 100%; border-collapse: separate; border-spacing: 0; table-layout: fixed; min-width: 1850px; }
thead th {
position: sticky; top: 0; z-index: 20; background: #f8fafc; color: #475569;
font-weight: 700; font-size: 0.65rem; text-transform: uppercase; padding: 6px 4px;
border-bottom: 2px solid #cbd5e1; text-align: left; height: 28px;
}
tbody td {
padding: 4px 6px;
border-bottom: 1px solid #f1f5f9; vertical-align: middle;
font-size: 0.75rem; color: #334155; line-height: 1.2;
}
tbody tr:hover { background: #f0f9ff !important; }
.col-trunc { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; max-width: 100%; }
.date-box { font-size: 0.65rem; color: #64748b; line-height: 1.1; white-space: nowrap; }
.date-val { font-weight: 500; color: #334155; }
.date-highlight { color: var(--c-teal); font-weight: 700; }
.prod-cell-flex { display: flex; justify-content: space-between; align-items: center; width: 100%; }
.prod-info { display: flex; flex-direction: column; overflow: hidden; margin-right: 8px; }
.prod-main { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 0.75rem; color: #0f172a; }
.prod-sub { font-size: 0.65rem; color: #64748b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.btn-ver-mini {
background: #f1f5f9; color: var(--c-blue); border: 1px solid #e2e8f0;
font-weight: 700; font-size: 0.6rem; padding: 2px 6px; border-radius: 4px;
cursor: pointer; white-space: nowrap; flex-shrink: 0; text-transform: uppercase;
}
.btn-ver-mini:hover { background: var(--c-blue); color: #fff; border-color: var(--c-blue); }
/* TOGGLES */
.gx-toggle {
width: 100%; height: 20px; border: 1px solid #e2e8f0; background: #fff; border-radius: 3px;
font-size: 0.6rem; color: #cbd5e1; font-weight: 700; text-transform: uppercase;
cursor: pointer; display: flex; align-items: center; justify-content: center;
transition: all 0.1s;
}
.gx-toggle:hover { border-color: #94a3b8; color: #64748b; }
.gx-toggle.active { color: #fff !important; border-color: transparent !important; }
.t-blue.active { background: var(--c-blue) !important; }
.t-purple.active { background: var(--c-purple) !important; }
.t-green.active { background: var(--c-green) !important; }
.t-hold.active { background: var(--c-hold) !important; }
.t-red.active { background: var(--c-red) !important; }
.lbl-grp { display: flex; gap: 2px; }
.lbl-btn {
font-size: 8px; font-weight: 700; padding: 2px 4px; border-radius: 3px;
border: 1px solid #cbd5e1; background: #fff; color: #64748b;
cursor: pointer; text-transform: uppercase; min-width: 28px; text-align: center;
}
.lbl-btn:hover { color: var(--c-teal); border-color: var(--c-teal); background: #f0fdfa; }
.actions-cell { display: flex; gap: 4px; justify-content: flex-end; align-items: center; }
.icon-clean {
width: 24px; height: 24px; border-radius: 4px; border: none; background: transparent;
color: #94a3b8; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center;
}
.icon-clean:hover { background: #f1f5f9; color: var(--c-teal); }
.icon-clean.del:hover { background: #fef2f2; color: var(--c-red); }
.gx-pill { display: inline-block; padding: 2px 6px; border-radius: 3px; font-size: 0.6rem; font-weight: 800; text-transform: uppercase; }
.st-ok { background: #dcfce7; color: #15803d; }
.st-hold { background: #fef9c3; color: #a16207; }
.st-cancel { background: #fee2e2; color: #b91c1c; }
.st-proc { background: #e0f2fe; color: #0369a1; }
/* ETIQUETA APROBADO POR DOCTOR */
.gx-pill-doctor {
background: #10b981; color: white; border-radius: 4px;
font-size: 0.55rem; padding: 2px 5px; font-weight: 800;
margin-left: 5px; text-transform: uppercase; vertical-align: middle;
box-shadow: 0 2px 4px rgba(16, 185, 129, 0.3);
}
.gx-link-sala { color: var(--c-teal); font-weight: 700; text-decoration: none; font-size: 0.7rem; cursor: pointer; padding: 2px 6px; border-radius: 4px; background: #f0fdfa; border: 1px solid #ccfbf1; }
.gx-link-sala:hover { background: var(--c-teal); color: #fff; }
.gx-pg { padding: 5px 20px; background: #fff; border-top: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; height: 40px; }
.gx-pg-btn { background: #fff; border: 1px solid #cbd5e1; padding: 3px 10px; border-radius: 4px; font-size: 0.75rem; cursor: pointer; color: #334155; }
.gx-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 1000; }
.gx-card-modal { background: #fff; width: 450px; border-radius: 8px; overflow: hidden; box-shadow: 0 10px 25px rgba(0,0,0,0.2); }
.gx-c-body { padding: 20px; max-height: 50vh; overflow-y: auto; }
.gx-c-foot { padding: 10px 20px; background: #f8fafc; text-align: right; border-top: 1px solid #e2e8f0; }
.gx-item-list li { border-bottom: 1px dashed #e2e8f0; padding: 8px 0; font-size: 0.8rem; list-style: none; }
</style>
<div class="gx-main-card">
<div class="gx-header">
<h1>Gxpert Producción <span style="font-weight:400;color:#64748b;font-size:0.8em">v18.0 (Detalle Clínico)</span></h1>
<div class="gx-kpi-row">
<span style="color:#2563eb">Proc: <span id="kpi-proc">0</span></span>
<span style="color:#d97706">Hold: <span id="kpi-hold">0</span></span>
<span style="color:#dc2626">Cancel: <span id="kpi-cancel">0</span></span>
</div>
</div>
<div class="gx-toolbar">
<input type="text" id="gx-search" class="gx-input" placeholder="Buscar...">
<select id="gx-filter-status" class="gx-input"><option value="TODOS">Todos</option><option value="Produccion">Producción</option><option value="Entrega">Entrega</option><option value="Ok">OK</option><option value="Hold">Hold</option><option value="Cancel">Cancel</option></select>
<select id="gx-filter-date" class="gx-input"><option value="ALL">Todo</option><option value="TODAY">Hoy</option><option value="7">7 días</option><option value="30">30 días</option></select>
<select id="gx-filter-sort" class="gx-input"><option value="NEWEST">Recientes</option><option value="OLDEST">Antiguos</option></select>
<button class="gx-btn-refresh" id="gx-btn-refresh">↻ Refrescar</button>
</div>
<div class="gx-table-wrap">
<table>
<thead>
<tr>
<th style="width:60px">Orden</th>
<th style="width:80px">Estado</th>
<th style="width:160px">Paciente / Doctor</th>
<th style="width:80px">Fechas</th>
<th style="width:200px">Producto</th>
<th style="width:40px;text-align:center">Apr</th>
<th style="width:40px;text-align:center">Dis</th>
<th style="width:40px;text-align:center">DrA</th>
<th style="width:40px;text-align:center">Prd</th>
<th style="width:40px;text-align:center">Emp</th>
<th style="width:40px;text-align:center">Ent</th>
<th style="width:40px;text-align:center">OK</th>
<th style="width:40px;text-align:center">Hld</th>
<th style="width:40px;text-align:center">Urg</th>
<th style="width:40px;text-align:center">Cnl</th>
<th style="width:100px">Etiquetas</th>
<th style="width:45px;text-align:center">Sala</th>
<th style="width:40px;text-align:center">Nota</th>
<th style="width:100px;text-align:right">Acciones</th>
</tr>
</thead>
<tbody id="gx-tbody">
<tr><td colspan="20" style="text-align:center;padding:20px;color:#94a3b8">Cargando...</td></tr>
</tbody>
</table>
</div>
<div class="gx-pg">
<button class="gx-pg-btn" id="gx-pg-prev">Anterior</button>
<span style="color:#64748b;font-size:0.8rem" id="gx-pg-info">1</span>
<button class="gx-pg-btn" id="gx-pg-next">Siguiente</button>
</div>
</div>
<div class="gx-overlay" id="gx-modal-items">
<div class="gx-card-modal">
<div style="padding:15px;border-bottom:1px solid #eee;font-weight:700">Detalle Completo</div>
<div class="gx-c-body"><ul id="gx-items-list" class="gx-item-list" style="padding-left:0"></ul></div>
<div class="gx-c-foot"><button class="gx-pg-btn" id="gx-items-close">Cerrar</button></div>
</div>
</div>
<div class="gx-overlay" id="gx-modal-notes">
<div class="gx-card-modal">
<div style="padding:15px;border-bottom:1px solid #eee;font-weight:700">Nota</div>
<div class="gx-c-body"><textarea id="gx-notes-input" style="width:100%;height:100px;border:1px solid #ccc;padding:8px;font-family:inherit"></textarea></div>
<div class="gx-c-foot">
<button id="gx-notes-close" style="border:none;background:none;cursor:pointer;margin-right:10px;color:#666">Cancelar</button>
<button class="gx-btn-refresh" id="gx-notes-save" style="font-size:0.8rem;padding:0 10px;height:26px">Guardar</button>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.5/dist/JsBarcode.all.min.js"></script>
<script>
(function(){
const API_KEY = "AIzaSyClk6fa-1Pl9lGDfk4itUhxAmr67b2Y0wk";
const PROJECT_ID = "gx-orders";
const BASE_URL = `https://firestore.googleapis.com/v1/projects/${PROJECT_ID}/databases/(default)/documents/gx_orders`;
const SALA_BASE_URL = "https://sistema.grad.com.mx/sala-test/";
let allOrders = [], filteredOrders = [], currentPage = 1, currentEditingId = null;
const itemsPerPage = 15;
const val = (v) => v ? (v.stringValue || v.booleanValue || v.integerValue || v.timestampValue || "") : "";
const mapVal = (m) => {
let obj = {};
if(m && m.mapValue && m.mapValue.fields) for(let k in m.mapValue.fields) obj[k] = val(m.mapValue.fields[k]);
return obj;
};
const fmtDate = (iso) => {
if(!iso) return "-";
let d = new Date(iso);
return isNaN(d) ? "-" : d.toLocaleDateString('es-MX', {day:'2-digit', month:'2-digit'});
};
// --- FUNCIÓN QUE EXTRAE EL DETALLE CLÍNICO PARA LA TABLA ---
function formatDetailedItems(item) {
// Recibe el objeto del array 'items' de Firestore
let rawSpecs = [];
// Recopilamos todas las especificaciones complejas en un array
if(item.teeth) rawSpecs.push(`Dte: ${item.teeth}`);
if(item.material) rawSpecs.push(`Mat: ${item.material}`);
if(item.color) rawSpecs.push(`Color: ${item.color}`);
if(item.piezas && item.piezas !== '1') rawSpecs.push(`Piezas: ${item.piezas}`);
if(item.implante) rawSpecs.push(`Implante: ${item.implante}`);
if(item.ausencia) rawSpecs.push(`Ausencia: ${item.ausencia}`);
if (rawSpecs.length === 0) return { main: `${item.qty}x ${item.name}`, sub: '' };
// Unimos las especificaciones con el separador que ya estaba en el CSS original (.prod-sub)
let subDetails = rawSpecs.join(' • ');
return {
main: `${item.qty}x ${item.name}`,
sub: subDetails
};
}
// --- URLS Y JSON ---
const buildItemsString = (items) => {
return items.map(i => {
let props = [];
if(i.teeth) props.push(`Teeth=${i.teeth}`);
if(i.piezas) props.push(`Pieces=${i.piezas}`);
if(i.implante) props.push(`implante=${i.implante}`);
if(i.ausencia) props.push(`ausencia=${i.ausencia}`);
let fullTitle = i.name || "";
if(i.color) fullTitle += " - " + i.color;
if(i.material) fullTitle += " / " + i.material;
return `${fullTitle}~~${i.qty||1}~${props.join(';')}`;
}).join('||');
};
const buildSalaUrl = (o) => {
let cleanOrder = o.orderId.replace(/#/g, '');
const gxData = {
case: o.caseId, pn: o.patient, dob: o.dob, del: o.entregaFecha, time: o.entregaHora,
sit: "", links: "", folder: o.folder, note: o.notes,
doc: o.doctor.nombre, mail: o.doctor.email, phone: o.doctor.telefono,
addr: o.address, order: "#" + cleanOrder, items: buildItemsString(o.items)
};
return `${SALA_BASE_URL}?gx=${encodeURIComponent(JSON.stringify(gxData)).replace(/%20/g, '+')}`;
};
async function loadOrders(){
try {
let res = await fetch(`${BASE_URL}?pageSize=300&key=${API_KEY}`);
let data = await res.json();
if(data.documents) {
allOrders = data.documents.map(doc => {
let f = doc.fields;
let id = doc.name.split('/').pop();
let items = [];
// --- LECTURA DE ARRAY DE ITEMS ---
if(f.items && f.items.arrayValue && f.items.arrayValue.values){
items = f.items.arrayValue.values.map(iv => {
let obj = mapVal(iv);
return {
qty: obj.qty || 1,
name: obj.nombre || obj.name || "Item",
material: obj.variante || obj.material || "",
color: obj.color || "",
teeth: obj.teeth || "",
implante: obj.implante || "",
ausencia: obj.ausencia || "",
piezas: obj.piezas || ""
};
});
}
// --- LECTURA DE ITEM COMPLEJO DESDE GxRaw (BUSCA DETALLE PERDIDO) ---
if (items.length === 0 && f.gxRaw && f.gxRaw.stringValue) {
try {
const gxRawData = JSON.parse(f.gxRaw.stringValue);
if (gxRawData.items) {
// Esto intentará parsear la cadena compleja de ítems
const rawItemString = gxRawData.items;
const simpleProducts = rawItemString.split('||');
simpleProducts.forEach(rawItem => {
const parts = rawItem.split('~~');
if (parts.length >= 2) {
const title = parts[0];
const qtyAndProps = parts[1].split('~');
const rawProps = qtyAndProps[1] || '';
const parsedProps = {};
rawProps.split(';').forEach(prop => {
const [key, value] = prop.split('=');
if (key && value) parsedProps[key.trim().toLowerCase()] = value.trim();
});
// Reconstruir el formato de Array
items.push({
qty: qtyAndProps[0] || 1,
name: title,
teeth: parsedProps.teeth || '',
implante: parsedProps.implante || '',
ausencia: parsedProps.ausencia || '',
material: parsedProps.material || parsedProps.variante || '',
color: parsedProps.color || parsedProps.color || '',
piezas: parsedProps.pieces || ''
});
}
});
}
} catch (e) { console.error("Error parseando gxRaw.items:", e); }
}
// --- FIN LECTURA ITEMS ---
// Resto de lógica de lectura de mapas...
let states = f.states ? mapVal(f.states) : {};
let tstamps = f.timestamps ? mapVal(f.timestamps) : {};
let entrega = f.entrega ? mapVal(f.entrega) : {};
let paciente = f.paciente ? mapVal(f.paciente) : {};
let docObj = f.doctor ? mapVal(f.doctor) : {};
let doctor = {
nombre: val(f.doc) || docObj.nombre || "",
email: val(f.mail) || docObj.email || "",
telefono: val(f.phone) || docObj.telefono || "",
clinica: val(f.addr) || docObj.clinica || ""
};
let isDrApproved = (f.approved && f.approved.booleanValue) || (f.status && f.status.stringValue && f.status.stringValue.includes("Aprobado"));
return {
id: id, orderId: val(f.orderId) || id, caseId: val(f.case) || id,
patient: val(f.pn) || paciente.nombre || "Sin Nombre",
doctor: doctor,
address: val(f.addr) || doctor.clinica || "",
folder: val(f.folder), created: val(f.createdAt) || tstamps.entrada || doc.createTime,
entregaFecha: val(f.del) || entrega.fecha || "",
entregaHora: val(f.time) || entrega.hora || "",
items: items, states: states, notes: val(f.note) || "",
drApproved: isDrApproved
};
});
}
applyFilters();
} catch(e) { console.error(e); }
}
function applyFilters(){
let term = document.getElementById('gx-search').value.toLowerCase();
let fStatus = document.getElementById('gx-filter-status').value;
let fDate = document.getElementById('gx-filter-date').value;
filteredOrders = allOrders.filter(o => {
let text = (o.orderId + " " + o.patient + " " + o.doctor.nombre).toLowerCase();
if(term && !text.includes(term)) return false;
if(fStatus !== "TODOS"){
if(fStatus === "Hold" && !o.states.hold) return false;
if(fStatus === "Cancel" && !o.states.cancel) return false;
if(fStatus === "Ok" && !o.states.ok) return false;
if(fStatus === "Produccion" && !o.states.produccion) return false;
if(fStatus === "Entrega" && !o.states.entrega) return false;
}
if(fDate !== "ALL"){
let d = new Date(o.created);
let limit = new Date();
if(fDate === "TODAY") limit.setHours(0,0,0,0);
else limit.setDate(limit.getDate() - parseInt(fDate));
if(d < limit) return false;
}
return true;
});
filteredOrders.sort((a,b) => {
let da = new Date(a.created), db = new Date(b.created);
return db - da;
});
let kpi = { proc:0, draut:0, hold:0, cancel:0 };
filteredOrders.forEach(o => {
if(o.states.hold) kpi.hold++; else if(o.states.cancel) kpi.cancel++; else {
if(!o.states.ok) kpi.proc++;
if(!o.states.draut && !o.states.ok) kpi.draut++;
}
});
document.getElementById('kpi-proc').innerText = kpi.proc;
document.getElementById('kpi-hold').innerText = kpi.hold;
document.getElementById('kpi-cancel').innerText = kpi.cancel;
currentPage = 1; renderTable();
}
window.changePage = (dir) => { currentPage += dir; renderTable(); };
function renderTable(){
let tbody = document.getElementById('gx-tbody');
tbody.innerHTML = '';
const start = (currentPage - 1) * itemsPerPage;
const paginatedItems = filteredOrders.slice(start, start + itemsPerPage);
document.getElementById('gx-pg-info').textContent = `Pág ${currentPage}`;
document.getElementById('gx-pg-prev').disabled = currentPage === 1;
document.getElementById('gx-pg-next').disabled = paginatedItems.length < itemsPerPage;
if(paginatedItems.length === 0) {
tbody.innerHTML = `<tr><td colspan="20" style="text-align:center;padding:20px;color:#94a3b8;">Sin datos.</td></tr>`;
return;
}
paginatedItems.forEach(o => {
let tr = document.createElement('tr');
if(o.states.hold) tr.style.backgroundColor = "#fffbeb";
if(o.states.cancel) tr.style.backgroundColor = "#fef2f2";
if(o.states.urgente) tr.style.backgroundColor = "#eff6ff";
if(o.drApproved) tr.style.boxShadow = "inset 4px 0 0 #16a34a";
let itemsHtml = '<span style="color:#ccc">-</span>';
if(o.items.length > 0) {
let i = o.items[0];
// --- APLICAR FORMATO DETALLADO ---
let formattedItem = formatDetailedItems(i);
let extraText = o.items.length > 1 ? ` (+${o.items.length-1})` : '';
itemsHtml = `
<div class="prod-cell-flex">
<div class="prod-info">
<div class="prod-main" title="${i.name}">${formattedItem.main}${extraText}</div>
<div class="prod-sub">${formattedItem.sub}</div>
</div>
<button class="btn-ver-mini" onclick="openItems('${o.id}')">Ver</button>
</div>`;
}
const tg = (key, txt, colorClass) => {
let activeClass = o.states[key] ? `active ${colorClass}` : '';
return `<td style="padding:4px 2px; text-align:center"><div class="gx-toggle ${activeClass}" onclick="toggleState('${o.id}', '${key}')">${txt}</div></td>`;
};
let patientHtml = o.patient;
if(o.drApproved) {
patientHtml += ' <span class="gx-pill-doctor">✅ APROBADO POR DR</span>';
}
tr.innerHTML = `
<td><span style="font-family:monospace;font-weight:700">${o.orderId}</span></td>
<td><span class="gx-pill ${o.states.hold?'st-hold':(o.states.cancel?'st-cancel':(o.states.ok?'st-ok':'st-proc'))}">${o.states.hold?'HOLD':(o.states.ok?'OK':'PROD')}</span></td>
<td>
<div class="col-trunc" style="font-weight:600">${patientHtml}</div>
<div class="col-trunc" style="color:#64748b;font-size:0.7rem">Dr. ${o.doctor.nombre}</div>
</td>
<td>
<div class="date-box">
<div>In: <span class="date-val">${fmtDate(o.created).split(' ')[0]}</span></div>
<div>Out: <span class="date-val date-highlight">${o.entregaFecha||'-'}</span></div>
</div>
</td>
<td>${itemsHtml}</td>
${tg('aprobado','APR','t-blue')} ${tg('diseno','DIS','t-blue')} ${tg('draut','DRA','t-blue')} ${tg('produccion','PRD','t-blue')}
${tg('empaque','EMP','t-purple')} ${tg('entrega','ENT','t-purple')} ${tg('ok','OK','t-green')} ${tg('hold','HLD','t-hold')}
${tg('urgente','URG','t-red')} ${tg('cancel','CNL','t-red')}
<td>
<div class="lbl-grp">
<button class="lbl-btn" onclick="printLabel('${o.id}','prod')">PRD</button>
<button class="lbl-btn" onclick="printLabel('${o.id}','box')">EMP</button>
<button class="lbl-btn" onclick="printLabel('${o.id}','acuse')">ACU</button>
</div>
</td>
<td style="text-align:center"><a onclick="openSala('${o.id}')" class="gx-link-sala">Sala</a></td>
<td style="text-align:center"><button class="icon-clean" onclick="openNotes('${o.id}')" style="margin:0 auto;${o.notes?'color:#d97706':''}">✎</button></td>
<td style="text-align:right">
<div class="actions-cell">
<button class="icon-clean" onclick="sendMail('${o.id}')">✉</button>
<button class="icon-clean" onclick="sendWa('${o.id}')">✆</button>
<button class="icon-clean del" onclick="deleteOrder('${o.id}')">✕</button>
</div>
</td>
`;
tbody.appendChild(tr);
});
}
window.openSala = (id) => {
let o = allOrders.find(x => x.id === id);
let url = buildSalaUrl(o);
window.open(url, '_blank');
};
/* --- IMPRESIÓN REPARADA --- */
window.printLabel = (id, type) => {
let o = allOrders.find(x => x.id === id);
let w = window.open("","","width=500,height=600");
let html = "", style = "";
if(type === 'prod') {
style = "@page { size: 100mm 150mm; margin: 0; } body { margin: 0; font-family: sans-serif; } ul {padding-left:15px; margin:0;} li {margin-bottom:4px;}";
let itemsFull = o.items.map(i => {
let d = []; if(i.color) d.push(`Col:${i.color}`); if(i.teeth) d.push(`Dtes:${i.teeth}`); if(i.implante) d.push(`Imp:${i.implante}`);
return `<li style="font-size:11px;"><strong>${i.qty}x ${i.name}</strong> (${i.material}) ${d.join(' ')}</li>`;
}).join('');
html = `<div style="width:100mm;height:150mm;padding:5mm;box-sizing:border-box;"><div style="text-align:center;margin-bottom:10px;"><svg id="bc"></svg></div><div style="border-bottom:2px solid #000;padding-bottom:5px;margin-bottom:10px;"><h1 style="margin:0;font-size:22px;">#${o.orderId.replace('#','')}</h1><div style="font-size:10px;">Ent: ${fmtDate(o.created)}</div></div><div style="font-size:12px;margin-bottom:10px;line-height:1.5;"><div><strong>PACIENTE:</strong> ${o.patient}</div><div><strong>DOCTOR:</strong> ${o.doctor.nombre}</div><div><strong>ENTREGA:</strong> <b>${o.entregaFecha || "PENDIENTE"}</b></div></div>${o.notes ? `<div style="border:1px dashed #000;padding:5px;font-size:11px;margin-bottom:10px;"><strong>NOTA:</strong> ${o.notes}</div>` : ''}<div style="flex:1;border-top:1px solid #000;padding-top:5px;"><ul>${itemsFull}</ul></div></div>`;
} else if (type === 'box') {
style = "@page { size: 102mm 38mm; margin: 0; } body { margin: 0; font-family: sans-serif; }";
html = `<div style="width:102mm;height:38mm;padding:3mm;box-sizing:border-box;display:flex;justify-content:space-between;align-items:center;"><div style="line-height:1.2;"><strong style="font-size:14px;">GXPERT LAB</strong><br><span style="font-size:11px">${o.patient.substring(0,25)}</span><br><span style="font-size:10px">Dr. ${o.doctor.nombre.substring(0,25)}</span></div><div style="text-align:right"><h2 style="margin:0;font-size:24px;">#${o.orderId.replace('#','')}</h2></div></div>`;
} else {
style = "@page { size: 4in 6in; margin: 0; } body { margin: 0; font-family: sans-serif; }";
let itemsAcuse = o.items.map(i => `${i.qty}x ${i.name}`).join(', ');
html = `<div style="width:4in;height:6in;padding:0.5in;box-sizing:border-box;"><h2 style="text-align:center;margin:0">ACUSE RECIBO</h2><h1 style="text-align:center;margin:10px 0;font-size:32px;">#${o.orderId.replace('#','')}</h1><div style="line-height:1.6;font-size:12px;"><p><strong>Doctor:</strong> ${o.doctor.nombre}</p><p><strong>Paciente:</strong> ${o.patient}</p></div><hr><div style="font-size:11px;min-height:50px;"><strong>Items:</strong> ${itemsAcuse}</div><div style="margin-top:2in;border-top:1px solid #000;text-align:center;padding-top:5px;">Firma de Recibido</div></div>`;
}
w.document.write(`<html><head><style>${style}</style></head><body>${html}<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.5/dist/JsBarcode.all.min.js"><\/script><script>if(document.getElementById('bc')) JsBarcode('#bc','${o.orderId.replace('#','')}',{height:35,displayValue:true,fontSize:14,margin:0}); window.print();<\/script></body></html>`);
w.document.close();
};
// --- CORRECCIÓN CRÍTICA DE BORRADO ---
window.deleteOrder = async (id) => {
if(prompt("Código seguridad borrar (1235):") !== "1235") return;
allOrders = allOrders.filter(o => o.id !== id); applyFilters();
let safeId = encodeURIComponent(id);
await fetch(`${BASE_URL}/${safeId}?key=${API_KEY}`, { method: 'DELETE' });
};
// --- CORRECCIÓN CRÍTICA DE ESTADOS ---
window.toggleState = async (id, key) => {
let o = allOrders.find(x => x.id === id);
let newState = !o.states[key];
if((key === 'aprobado' || key === 'cancel') && newState) {
if(prompt("Clave Supervisor (9095):") !== "9095") return;
}
o.states[key] = newState; renderTable();
let body = { fields: { states: { mapValue: { fields: {} } } } };
for(let k in o.states) body.fields.states.mapValue.fields[k] = { booleanValue: !!o.states[k] };
let safeId = encodeURIComponent(id);
await fetch(`${BASE_URL}/${safeId}?updateMask.fieldPaths=states&key=${API_KEY}`, { method: 'PATCH', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body) });
};
window.openItems = (id) => {
let o = allOrders.find(x => x.id === id);
document.getElementById('gx-items-list').innerHTML = o.items.map(i => {
let d = []; if(i.material) d.push("Mat: "+i.material); if(i.color) d.push("Col: "+i.color); if(i.teeth) d.push("Dtes: "+i.teeth); if(i.implante) d.push("Imp: "+i.implante);
return `<li style="list-style:none;border-bottom:1px dashed #eee;padding:5px 0"><b>${i.qty}x ${i.name}</b><br><span style="font-size:0.8rem;color:#666">${d.join(' | ')}</span></li>`;
}).join('');
document.getElementById('gx-modal-items').style.display = 'flex';
};
document.getElementById('gx-items-close').onclick = () => document.getElementById('gx-modal-items').style.display = 'none';
window.openNotes = (id) => {
currentEditingId = id;
document.getElementById('gx-notes-input').value = allOrders.find(x => x.id === id).notes;
document.getElementById('gx-modal-notes').style.display = 'flex';
};
document.getElementById('gx-notes-save').onclick = async () => {
let txt = document.getElementById('gx-notes-input').value;
let o = allOrders.find(x => x.id === currentEditingId);
if(o) {
o.notes = txt;
let safeId = encodeURIComponent(currentEditingId); // Fix: Codificar ID
await fetch(`${BASE_URL}/${safeId}?updateMask.fieldPaths=note&key=${API_KEY}`, { method: 'PATCH', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({fields:{note:{stringValue:txt}}}) });
document.getElementById('gx-modal-notes').style.display = 'none';
renderTable();
}
};
document.getElementById('gx-notes-close').onclick = () => document.getElementById('gx-modal-notes').style.display = 'none';
window.sendMail = (id) => {
let o = allOrders.find(x=>x.id===id);
if(!o.doctor.email) { alert("Sin correo registrado"); return; }
let link = buildSalaUrl(o);
let body = `Hola Dr(a). ${o.doctor.nombre},\n\nLe enviamos la orden #${o.orderId.replace('#','')} del paciente ${o.patient}.\nPuede ver el detalle aquí: ${link}`;
window.open(`mailto:${o.doctor.email}?subject=Orden #${o.orderId.replace('#','')}&body=${encodeURIComponent(body)}`);
};
window.sendWa = (id) => {
let o = allOrders.find(x=>x.id===id);
if(!o.doctor.telefono) { alert("Sin WhatsApp registrado"); return; }
let num = o.doctor.telefono.replace(/\D/g,'');
let link = buildSalaUrl(o);
let msg = `Hola Dr(a). ${o.doctor.nombre}, orden #${o.orderId.replace('#','')} del paciente ${o.patient}. Ver detalle: ${link}`;
window.open(`https://wa.me/52${num}?text=${encodeURIComponent(msg)}`, '_blank');
};
document.getElementById('gx-btn-refresh').onclick = loadOrders;
document.getElementById('gx-search').onkeyup = applyFilters;
document.getElementById('gx-filter-status').onchange = applyFilters;
document.getElementById('gx-filter-date').onchange = applyFilters;
document.getElementById('gx-pg-prev').onclick = () => changePage(-1);
document.getElementById('gx-pg-next').onclick = () => changePage(1);
loadOrders();
})();
</script>
</section>ORDEN SHOPIFY
#1140
PENDING
04/Dec/2025 – 05:40 AM
Dirección de Envío
Avenida sebastian Lerdo de Tejada 1210
Mexicali, Baja California
CP: 21100
Mexico
Mexicali, Baja California
CP: 21100
Mexico
| Producto | Cant. | Total |
|---|---|---|
| Carilla | 1 | $450.00 |
| TOTAL PEDIDO: | $450.00 MXN | |
👇 CONTROL INTERNO: Usa los campos de abajo para actualizar el estado y la guía.