{"id":23,"date":"2026-08-12T06:59:28","date_gmt":"2026-08-12T06:59:28","guid":{"rendered":"https:\/\/tpvking.com\/?page_id=23"},"modified":"2026-08-25T23:53:47","modified_gmt":"2026-08-25T23:53:47","slug":"crear-qr-ticket-de-tienda","status":"publish","type":"page","link":"https:\/\/tpvking.com\/","title":{"rendered":"CREAR QR TICKET DE TIENDA"},"content":{"rendered":"\n<!DOCTYPE html>\n<html lang=\"es\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Armador de Ticket<\/title>\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/qrcodejs\/1.0.0\/qrcode.min.js\"><\/script>\n<style>\n  :root{\n    --ink:#1c1b19;\n    --paper:#f7f4ee;\n    --line:#d8d2c4;\n    --accent:#2f6f4f;\n    --accent-dark:#204d37;\n    --danger:#a63d33;\n    --mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;\n  }\n  *{box-sizing:border-box;}\n  body{\n    margin:0;\n    background:var(--paper);\n    color:var(--ink);\n    font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif;\n    min-height:100vh;\n    padding:20px 16px 60px;\n  }\n  .wrap{max-width:600px;margin:0 auto;}\n  .sidebar{\n    background:#fff;\n    border:1px solid var(--line);\n    border-radius:10px;\n    padding:14px;\n    margin-bottom:16px;\n  }\n  .sidebar-title{\n    font-size:11px;\n    font-weight:700;\n    text-transform:uppercase;\n    letter-spacing:.08em;\n    color:#57523f;\n    margin-bottom:10px;\n  }\n  .quick-list{display:flex;flex-direction:row;flex-wrap:wrap;gap:8px;}\n  .quick-btn{\n    text-align:left;\n    padding:9px 11px;\n    background:#fdfcfa;\n    border:1px solid var(--line);\n    border-radius:7px;\n    font-size:13px;\n    font-weight:600;\n    color:var(--ink);\n    line-height:1.3;\n  }\n  .quick-btn:active{background:#eee8d8;border-color:var(--accent);}\n  .quick-price{\n    font-family:var(--mono);\n    font-size:11px;\n    font-weight:400;\n    color:var(--accent-dark);\n  }\n  .quick-empty{\n    font-size:12px;\n    color:#8a8474;\n    line-height:1.5;\n  }\n  header{margin-bottom:20px;display:flex;justify-content:space-between;align-items:flex-start;gap:10px;}\n  .header-text{flex:1;min-width:0;}\n  .eyebrow{\n    font-family:var(--mono);\n    font-size:11px;\n    letter-spacing:.14em;\n    text-transform:uppercase;\n    color:var(--accent-dark);\n    margin-bottom:6px;\n  }\n  h1{\n    font-size:26px;\n    margin:0;\n    font-weight:700;\n    letter-spacing:-0.01em;\n  }\n  .sub{\n    font-size:13px;\n    color:#6b665a;\n    margin-top:4px;\n  }\n  .card{\n    background:#fff;\n    border:1px solid var(--line);\n    border-radius:10px;\n    padding:16px;\n    margin-bottom:16px;\n  }\n  .field{margin-bottom:12px;}\n  label{\n    display:block;\n    font-size:12px;\n    font-weight:600;\n    color:#57523f;\n    margin-bottom:5px;\n  }\n  input{\n    width:100%;\n    padding:11px 12px;\n    border:1px solid var(--line);\n    border-radius:7px;\n    font-size:16px;\n    background:#fdfcfa;\n    color:var(--ink);\n  }\n  input:focus{\n    outline:none;\n    border-color:var(--accent);\n    box-shadow:0 0 0 3px rgba(47,111,79,0.12);\n  }\n  .row-single{display:grid;grid-template-columns:2fr 0.8fr 1fr;gap:8px;align-items:end;}\n  .btn-new{\n    padding:8px 12px;\n    background:transparent;\n    color:var(--danger);\n    border:1px solid var(--danger);\n    border-radius:7px;\n    font-size:12px;\n    font-weight:600;\n    white-space:nowrap;\n  }\n  .btn-new:active{background:rgba(166,61,51,0.08);}\n  .header-actions{display:flex;gap:8px;flex-shrink:0;}\n  .btn-fullscreen{\n    padding:8px 10px;\n    background:transparent;\n    color:var(--ink);\n    border:1px solid var(--line);\n    border-radius:7px;\n    font-size:16px;\n    line-height:1;\n  }\n  .btn-fullscreen:active{background:#eee8d8;}\n  button{\n    font-family:inherit;\n    cursor:pointer;\n    border:none;\n  }\n  .btn-add{\n    width:100%;\n    padding:12px;\n    background:var(--ink);\n    color:#fdfcfa;\n    border-radius:7px;\n    font-size:14px;\n    font-weight:600;\n    margin-top:4px;\n  }\n  .btn-add:active{background:#000;}\n  .items{margin-top:4px;}\n  .item{\n    display:flex;\n    justify-content:space-between;\n    align-items:center;\n    padding:10px 0;\n    border-bottom:1px solid var(--line);\n    font-size:14px;\n  }\n  .item:last-child{border-bottom:none;}\n  .item-name{font-weight:600;}\n  .item-meta{color:#6b665a;font-size:12px;margin-top:2px;}\n  .item-actions{display:flex;gap:6px;flex-shrink:0;}\n  .item-edit{\n    background:none;\n    color:var(--accent-dark);\n    font-size:12px;\n    font-weight:600;\n    padding:6px 8px;\n    border:1px solid var(--line);\n    border-radius:6px;\n  }\n  .item-edit:active{background:#eee8d8;}\n  .item-remove{\n    background:none;\n    color:var(--danger);\n    font-size:12px;\n    font-weight:600;\n    padding:6px 8px;\n    border:1px solid var(--line);\n    border-radius:6px;\n  }\n  .empty{\n    color:#8a8474;\n    font-size:13px;\n    text-align:center;\n    padding:14px 0;\n  }\n  .total-row{\n    display:flex;\n    justify-content:space-between;\n    align-items:baseline;\n    padding-top:12px;\n    margin-top:8px;\n    border-top:2px solid var(--ink);\n  }\n  .total-label{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#6b665a;}\n  .total-value{font-size:24px;font-weight:700;font-family:var(--mono);}\n  .btn-generate{\n    width:100%;\n    padding:15px;\n    background:var(--accent);\n    color:#fff;\n    border-radius:8px;\n    font-size:15px;\n    font-weight:700;\n    letter-spacing:.01em;\n  }\n  .btn-generate:active{background:var(--accent-dark);}\n  .btn-generate:disabled{background:#b9b4a4;}\n  #qr-area{\n    display:none;\n    text-align:center;\n    padding:20px 16px;\n  }\n  #qr-area.show{display:block;}\n  #qrcode{\n    display:inline-block;\n    padding:14px;\n    background:#fff;\n    border:1px solid var(--line);\n    border-radius:10px;\n  }\n  .qr-hint{\n    font-size:12px;\n    color:#6b665a;\n    margin-top:12px;\n    line-height:1.5;\n  }\n  .btn-back{\n    margin-top:16px;\n    width:100%;\n    padding:11px;\n    background:transparent;\n    color:var(--ink);\n    border:1px solid var(--line);\n    border-radius:7px;\n    font-size:13px;\n  }\n  .badge{\n    font-family:var(--mono);\n    font-size:11px;\n    background:#eee8d8;\n    color:var(--accent-dark);\n    padding:2px 7px;\n    border-radius:4px;\n  }\n<\/style>\n<\/head>\n<body>\n<div class=\"wrap\">\n  <header>\n    <div class=\"header-text\">\n      <div class=\"eyebrow\">Pre-ticket \u00b7 para caja<\/div>\n      <h1>Armador de ticket<\/h1>\n      <div class=\"sub\">Captura los art\u00edculos y genera el QR para que caja lo escanee y cobre.<\/div>\n    <\/div>\n    <div class=\"header-actions\">\n      <button class=\"btn-fullscreen\" id=\"btn-fullscreen\" title=\"Pantalla completa\">\u26f6<\/button>\n      <button class=\"btn-new\" id=\"btn-new\">Nuevo ticket<\/button>\n    <\/div>\n  <\/header>\n\n  <aside class=\"sidebar\">\n    <div class=\"sidebar-title\">Accesos r\u00e1pidos<\/div>\n    <div class=\"quick-list\" id=\"quick-list\">\n      <div class=\"quick-empty\" id=\"quick-empty\">A\u00fan no hay accesos r\u00e1pidos. Se van guardando conforme agregas art\u00edculos.<\/div>\n    <\/div>\n  <\/aside>\n\n  <div class=\"main-col\">\n    <div class=\"card\" id=\"form-card\">\n      <div class=\"row-single\">\n        <div class=\"field\">\n          <label for=\"nombre\">Art\u00edculo<\/label>\n          <input type=\"text\" id=\"nombre\" placeholder=\"Ej. Corte de cabello caballero\" autocomplete=\"off\">\n        <\/div>\n        <div class=\"field\">\n          <label for=\"cantidad\">Cant.<\/label>\n          <input type=\"number\" id=\"cantidad\" inputmode=\"numeric\" placeholder=\"1\" min=\"1\" value=\"1\">\n        <\/div>\n        <div class=\"field\">\n          <label for=\"precio\">Precio<\/label>\n          <input type=\"number\" id=\"precio\" inputmode=\"decimal\" placeholder=\"0.00\" step=\"0.01\" min=\"0\">\n        <\/div>\n      <\/div>\n      <button class=\"btn-add\" id=\"btn-add\">Agregar<\/button>\n    <\/div>\n\n    <div class=\"card\">\n      <div class=\"items\" id=\"items-list\">\n        <div class=\"empty\" id=\"empty-msg\">A\u00fan no hay art\u00edculos en el ticket.<\/div>\n      <\/div>\n      <div class=\"total-row\">\n        <span class=\"total-label\">Total<\/span>\n        <span class=\"total-value\" id=\"total-value\">$0.00<\/span>\n      <\/div>\n    <\/div>\n\n    <button class=\"btn-generate\" id=\"btn-generate\" disabled>Generar QR para caja<\/button>\n\n    <div id=\"qr-area\">\n      <span class=\"badge\" id=\"item-count-badge\">0 art\u00edculos<\/span>\n      <div style=\"margin-top:14px;\">\n        <div id=\"qrcode\"><\/div>\n      <\/div>\n      <div class=\"qr-hint\">Muestra esta pantalla en caja para que se escanee el c\u00f3digo.<br>El ticket no se cobra hasta que caja lo escanea y confirma.<\/div>\n      <button class=\"btn-back\" id=\"btn-back\">Volver a editar<\/button>\n    <\/div>\n  <\/div>\n<\/div>\n\n<script>\n(function(){\n  const items = [];\n  const nombreEl = document.getElementById('nombre');\n  const precioEl = document.getElementById('precio');\n  const cantidadEl = document.getElementById('cantidad');\n  const itemsList = document.getElementById('items-list');\n  const emptyMsg = document.getElementById('empty-msg');\n  const totalValue = document.getElementById('total-value');\n  const btnAdd = document.getElementById('btn-add');\n  const btnGenerate = document.getElementById('btn-generate');\n  const btnBack = document.getElementById('btn-back');\n  const btnNew = document.getElementById('btn-new');\n  const btnFullscreen = document.getElementById('btn-fullscreen');\n  const formCard = document.getElementById('form-card');\n  const itemsCard = document.querySelector('.card:nth-of-type(2)');\n  const qrArea = document.getElementById('qr-area');\n  const itemCountBadge = document.getElementById('item-count-badge');\n  const quickList = document.getElementById('quick-list');\n  let qr = null;\n\n  \/\/ --- Accesos r\u00e1pidos (\u00faltimos 10 art\u00edculos distintos, con su \u00faltimo\n  \/\/     precio usado, por dispositivo) ---\n  const RECENT_KEY = 'ticketRecentArticulos';\n  function loadRecent(){\n    try{\n      const raw = JSON.parse(localStorage.getItem(RECENT_KEY)) || [];\n      \/\/ Compatibilidad: versiones anteriores guardaban solo el nombre (string)\n      return raw.map(it => typeof it === 'string' ? {nombre: it, precio: null} : it);\n    }\n    catch(e){ return []; }\n  }\n  function saveRecent(arr){\n    try{ localStorage.setItem(RECENT_KEY, JSON.stringify(arr)); }\n    catch(e){ \/* si el navegador bloquea localStorage, simplemente no persiste *\/ }\n  }\n  let recentItems = loadRecent();\n\n  function money(n){\n    return '$' + n.toLocaleString('es-MX', {minimumFractionDigits:2, maximumFractionDigits:2});\n  }\n\n  function renderQuick(){\n    quickList.innerHTML = '';\n    if(recentItems.length === 0){\n      const empty = document.createElement('div');\n      empty.className = 'quick-empty';\n      empty.textContent = 'A\u00fan no hay accesos r\u00e1pidos. Se van guardando conforme agregas art\u00edculos.';\n      quickList.appendChild(empty);\n      return;\n    }\n    recentItems.forEach(it => {\n      const b = document.createElement('button');\n      b.type = 'button';\n      b.className = 'quick-btn';\n      b.innerHTML = escapeHtml(it.nombre) + (it.precio != null ? '<br><span class=\"quick-price\">' + money(it.precio) + '<\/span>' : '');\n      b.addEventListener('click', () => {\n        nombreEl.value = it.nombre;\n        if(it.precio != null) precioEl.value = it.precio;\n        cantidadEl.focus();\n        cantidadEl.select();\n      });\n      quickList.appendChild(b);\n    });\n  }\n\n  function pushRecent(nombre, precio){\n    const lower = nombre.toLowerCase();\n    recentItems = recentItems.filter(it => it.nombre.toLowerCase() !== lower);\n    recentItems.unshift({nombre, precio});\n    recentItems = recentItems.slice(0, 10);\n    saveRecent(recentItems);\n    renderQuick();\n  }\n\n  function render(){\n    itemsList.innerHTML = '';\n    if(items.length === 0){\n      itemsList.appendChild(emptyMsg);\n      emptyMsg.style.display = 'block';\n      btnGenerate.disabled = true;\n    } else {\n      btnGenerate.disabled = false;\n      items.forEach((it, idx) => {\n        const row = document.createElement('div');\n        row.className = 'item';\n        row.innerHTML = `\n          <div>\n            <div class=\"item-name\">${escapeHtml(it.nombre)}<\/div>\n            <div class=\"item-meta\">${it.cantidad} \u00d7 ${money(it.precio)} = ${money(it.cantidad*it.precio)}<\/div>\n          <\/div>\n          <div class=\"item-actions\">\n            <button class=\"item-edit\" data-idx=\"${idx}\">Editar<\/button>\n            <button class=\"item-remove\" data-idx=\"${idx}\">Quitar<\/button>\n          <\/div>\n        `;\n        itemsList.appendChild(row);\n      });\n    }\n    const total = items.reduce((s,it)=>s+it.cantidad*it.precio,0);\n    totalValue.textContent = money(total);\n  }\n\n  function escapeHtml(s){\n    const d = document.createElement('div');\n    d.textContent = s;\n    return d.innerHTML;\n  }\n\n  btnAdd.addEventListener('click', () => {\n    const nombre = nombreEl.value.trim();\n    const precio = parseFloat(precioEl.value);\n    const cantidad = parseInt(cantidadEl.value, 10) || 1;\n    if(!nombre){ nombreEl.focus(); return; }\n    if(isNaN(precio) || precio < 0){ precioEl.focus(); return; }\n    items.push({nombre, precio, cantidad});\n    pushRecent(nombre, precio);\n    nombreEl.value = '';\n    precioEl.value = '';\n    cantidadEl.value = '1';\n    nombreEl.focus();\n    render();\n  });\n\n  itemsList.addEventListener('click', (e) => {\n    if(e.target.classList.contains('item-remove')){\n      const idx = parseInt(e.target.getAttribute('data-idx'), 10);\n      items.splice(idx, 1);\n      render();\n    }\n    if(e.target.classList.contains('item-edit')){\n      const idx = parseInt(e.target.getAttribute('data-idx'), 10);\n      const it = items[idx];\n      nombreEl.value = it.nombre;\n      cantidadEl.value = it.cantidad;\n      precioEl.value = it.precio;\n      items.splice(idx, 1);\n      render();\n      nombreEl.focus();\n      nombreEl.select();\n    }\n  });\n\n  \/\/ Encoding: usa palabras clave como separador (no s\u00edmbolos) para evitar\n  \/\/ problemas de layout de teclado (US vs. Espa\u00f1ol) en lectores QR\/barras.\n  \/\/ NOMBREFSEPCANTIDADFSEPPRECIOISEPNOMBRE2FSEPCANTIDAD2FSEPPRECIO2\n  function buildPayload(){\n    return items.map(it => [it.nombre.replace(\/FSEP|ISEP\/gi,' '), it.cantidad, it.precio.toFixed(2)].join('FSEP')).join('ISEP');\n  }\n\n  btnGenerate.addEventListener('click', () => {\n    const payload = buildPayload();\n    document.getElementById('qrcode').innerHTML = '';\n    qr = new QRCode(document.getElementById('qrcode'), {\n      text: payload,\n      width: 220,\n      height: 220,\n      colorDark: '#1c1b19',\n      colorLight: '#ffffff',\n      correctLevel: QRCode.CorrectLevel.M\n    });\n    itemCountBadge.textContent = items.length + (items.length === 1 ? ' art\u00edculo' : ' art\u00edculos');\n    formCard.style.display = 'none';\n    itemsCard.style.display = 'none';\n    btnGenerate.style.display = 'none';\n    qrArea.classList.add('show');\n  });\n\n  btnBack.addEventListener('click', () => {\n    formCard.style.display = '';\n    itemsCard.style.display = '';\n    btnGenerate.style.display = '';\n    qrArea.classList.remove('show');\n  });\n\n  btnNew.addEventListener('click', () => {\n    if(items.length > 0){\n      const ok = confirm('Esto borrar\u00e1 el ticket que est\u00e1s editando (' + items.length + (items.length === 1 ? ' art\u00edculo' : ' art\u00edculos') + '). \u00bfContinuar?');\n      if(!ok) return;\n    }\n    items.length = 0;\n    nombreEl.value = '';\n    precioEl.value = '';\n    cantidadEl.value = '1';\n    formCard.style.display = '';\n    itemsCard.style.display = '';\n    btnGenerate.style.display = '';\n    qrArea.classList.remove('show');\n    render();\n    nombreEl.focus();\n  });\n\n  btnFullscreen.addEventListener('click', () => {\n    if(!document.fullscreenElement){\n      const el = document.documentElement;\n      const req = el.requestFullscreen || el.webkitRequestFullscreen || el.msRequestFullscreen;\n      if(req) req.call(el);\n    } else {\n      const exit = document.exitFullscreen || document.webkitExitFullscreen || document.msExitFullscreen;\n      if(exit) exit.call(document);\n    }\n  });\n\n  document.addEventListener('fullscreenchange', () => {\n    btnFullscreen.textContent = document.fullscreenElement ? '\u2715' : '\u26f6';\n  });\n\n  render();\n  renderQuick();\n})();\n<\/script>\n<\/body>\n<\/html>\n","protected":false},"excerpt":{"rendered":"<p>Armador de Ticket Pre-ticket \u00b7 para caja Armador de ticket Captura los art\u00edculos y genera el QR para que caja lo escanee y cobre. \u26f6 Nuevo ticket Accesos r\u00e1pidos A\u00fan no hay accesos r\u00e1pidos. Se van guardando conforme agregas art\u00edculos. Art\u00edculo Cant. Precio Agregar A\u00fan no hay art\u00edculos en el ticket. Total $0.00 Generar QR [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-23","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/tpvking.com\/index.php?rest_route=\/wp\/v2\/pages\/23","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tpvking.com\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/tpvking.com\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/tpvking.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tpvking.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=23"}],"version-history":[{"count":10,"href":"https:\/\/tpvking.com\/index.php?rest_route=\/wp\/v2\/pages\/23\/revisions"}],"predecessor-version":[{"id":20081,"href":"https:\/\/tpvking.com\/index.php?rest_route=\/wp\/v2\/pages\/23\/revisions\/20081"}],"wp:attachment":[{"href":"https:\/\/tpvking.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=23"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}