{"id":57,"date":"2025-09-02T07:31:58","date_gmt":"2025-09-02T07:31:58","guid":{"rendered":"https:\/\/co.ecchotag.com\/?page_id=57"},"modified":"2025-09-02T09:34:12","modified_gmt":"2025-09-02T09:34:12","slug":"calculator","status":"publish","type":"page","link":"https:\/\/co.ecchotag.com\/ja\/calculator\/","title":{"rendered":"Calculator"},"content":{"rendered":"<div data-elementor-type=\"wp-page\" data-elementor-id=\"57\" class=\"elementor elementor-57\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-92b5a11 e-flex e-con-boxed wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-parent\" data-id=\"92b5a11\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-5742e57 elementor-widget elementor-widget-html\" data-id=\"5742e57\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<div id=\"palletCalcWP\" style=\"--bg:#f7f7f8; --card:#ffffff; --muted:#64748b; --text:#0f172a; --accent:#2563eb; --ok:#16a34a; --warn:#d97706; --err:#dc2626;\">\n  <style>\n    #palletCalcWP *{ box-sizing:border-box; }\n    #palletCalcWP{ background:var(--bg); color:var(--text); font:14px\/1.4 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,\"Helvetica Neue\",Arial; padding-bottom:2px; }\n    #palletCalcWP header{ padding:16px 20px; border-bottom:1px solid #e5e7eb; display:flex; align-items:center; gap:12px; background:#fff; }\n    #palletCalcWP header h1{ margin:0; font-size:18px; font-weight:650; }\n    #palletCalcWP header small{ color:var(--muted); }\n    #palletCalcWP .container{ display:grid; grid-template-columns: 360px 1fr; gap:16px; padding:16px; }\n    #palletCalcWP .card{ background:var(--card); border:1px solid #e5e7eb; border-radius:12px; padding:14px; }\n    #palletCalcWP .card h2{ margin:0 0 8px 0; font-size:15px; }\n    #palletCalcWP .row{ display:grid; grid-template-columns:1fr 1fr; gap:8px; }\n    #palletCalcWP .row-3{ display:grid; grid-template-columns:repeat(3,1fr); gap:8px; }\n    #palletCalcWP label{ display:flex; flex-direction:column; gap:6px; font-size:12px; color:var(--muted); }\n    #palletCalcWP input[type=\"number\"], #palletCalcWP select{ background:#ffffff; color:#111827; padding:8px 10px; border-radius:8px; border:1px solid #d0d7de; width:100%; }\n    #palletCalcWP input[type=\"checkbox\"]{ transform: translateY(2px); }\n    #palletCalcWP .inline{ display:flex; align-items:center; gap:8px; font-size:12px; color:var(--muted); }\n    #palletCalcWP .btn{ cursor:pointer; background:#ffffff; border:1px solid #cbd5e1; color:#0f172a; padding:8px 10px; border-radius:8px; font-weight:600; }\n    #palletCalcWP .btn:hover{ border-color:#94a3b8; }\n    #palletCalcWP .btn.primary{ background:#f1f5f9; border-color:#cbd5e1; }\n    #palletCalcWP .btn.good{ border-color:#86efac; background:#f0fdf4; }\n    #palletCalcWP .btn.warn{ border-color:#fed7aa; background:#fff7ed; }\n    #palletCalcWP .stack{ display:flex; flex-wrap:wrap; gap:8px; }\n    #palletCalcWP .stat{ background:#ffffff; border:1px solid #e5e7eb; border-radius:8px; padding:8px 10px; display:flex; flex-direction:column; gap:2px; min-width:120px; }\n    #palletCalcWP .stat b{ font-size:16px; }\n    #palletCalcWP .pallets{ display:grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap:12px; }\n    #palletCalcWP .pallet{ position:relative; }\n    #palletCalcWP .pallet h3{ margin:0 0 8px 0; font-size:14px; display:flex; align-items:center; justify-content:space-between; }\n    #palletCalcWP .pill{ font-size:11px; padding:2px 6px; border-radius:999px; border:1px solid #e5e7eb; color:var(--muted); background:#fff; }\n    #palletCalcWP .meter{ height:8px; background:#f1f5f9; border:1px solid #e5e7eb; border-radius:999px; overflow:hidden; }\n    #palletCalcWP .meter > i{ display:block; height:100%; width:0%; background:linear-gradient(90deg, #60a5fa, #22c55e); }\n    #palletCalcWP .grid-preview{ display:grid; gap:2px; background:#ffffff; border:1px dashed #cbd5e1; padding:6px; border-radius:8px; }\n    #palletCalcWP .grid-preview i{ display:block; width:100%; aspect-ratio:1.4\/1; background:#f1f5f9; border:1px solid #e5e7eb; border-radius:4px; }\n    #palletCalcWP .grid-preview i.empty{ opacity:.35; border-style:dashed; background:#ffffff; }\n    #palletCalcWP .grid-preview i.filled{ background:#e2e8f0; border-color:#cbd5e1; cursor:grab; }\n    #palletCalcWP .grid-preview.drop-ok{ outline:2px solid #60a5fa; outline-offset:2px; }\n    #palletCalcWP .mini{ font-size:12px; color:var(--muted); }\n    #palletCalcWP .right{ text-align:right; }\n    #palletCalcWP .kbd{ font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",monospace; background:#f1f5f9; padding:2px 6px; border:1px solid #e5e7eb; border-radius:6px; font-size:11px; color:#0f172a; }\n    #palletCalcWP .footer-note{ color:var(--muted); font-size:12px; margin-top:8px; }\n    #palletCalcWP .err{ color:var(--err); }\n    #palletCalcWP hr{ border:none; border-top:1px solid #e5e7eb; margin:12px 0; }\n    @media (max-width: 900px){ #palletCalcWP .container{ grid-template-columns:1fr; } }\n  <\/style>\n\n  <header>\n    <h1>Air Freight Euro Pallet Planner<\/h1>\n    <small>Add or remove boxes. Drag between pallets to balance weight.<\/small>\n  <\/header>\n\n  <div class=\"container\">\n    <section class=\"card\" data-id=\"inputs\">\n      <h2>Inputs<\/h2>\n\n      <div class=\"row-3\" style=\"margin-top:6px\">\n        <label>Box width cm\n          <input type=\"number\" data-id=\"boxW\" step=\"0.1\" value=\"15.5\" \/>\n        <\/label>\n        <label>Box depth cm\n          <input type=\"number\" data-id=\"boxD\" step=\"0.1\" value=\"23\" \/>\n        <\/label>\n        <label>Box height cm\n          <input type=\"number\" data-id=\"boxH\" step=\"0.1\" value=\"33\" \/>\n        <\/label>\n      <\/div>\n\n      <div class=\"row\" style=\"margin-top:8px\">\n        <label>Bottles per box\n          <input type=\"number\" data-id=\"bpb\" step=\"1\" min=\"1\" value=\"6\" \/>\n        <\/label>\n        <label>Allow box rotation to maximize per layer\n          <select data-id=\"allowRotation\">\n            <option value=\"yes\" selected>yes<\/option>\n            <option value=\"no\">no<\/option>\n          <\/select>\n        <\/label>\n      <\/div>\n\n      <hr style=\"border:none; border-top:1px solid #1f2937; margin:12px 0\" \/>\n\n      <div class=\"row-3\">\n        <label>Pallet width cm\n          <input type=\"number\" data-id=\"palW\" step=\"0.1\" value=\"120\" \/>\n        <\/label>\n        <label>Pallet depth cm\n          <input type=\"number\" data-id=\"palD\" step=\"0.1\" value=\"80\" \/>\n        <\/label>\n        <label>Pallet height cm\n          <input type=\"number\" data-id=\"palH\" step=\"0.1\" value=\"15\" \/>\n        <\/label>\n      <\/div>\n      \n      <div class=\"row\" style=\"margin-top:8px\">\n        <label>Max total stack height cm\n          <input type=\"number\" data-id=\"maxH\" step=\"0.1\" value=\"100\" \/>\n        <\/label>\n        <label class=\"inline\" style=\"margin-top:22px\">\n          <input type=\"checkbox\" data-id=\"limitTwoLayers\" checked \/> limit to two box layers\n        <\/label>\n      <\/div>\n\n      <hr style=\"border:none; border-top:1px solid #1f2937; margin:12px 0\" \/>\n\n      <div class=\"row-3\">\n        <label>Bottle weight kg\n          <input type=\"number\" data-id=\"kgBottle\" step=\"0.01\" value=\"1.25\" placeholder=\"enter\" \/>\n        <\/label>\n        <label>Empty box weight kg\n          <input type=\"number\" data-id=\"kgBox\" step=\"0.01\" value=\"0.6\" placeholder=\"enter\" \/>\n        <\/label>\n        <label>Pallet weight kg\n          <input type=\"number\" data-id=\"kgPallet\" step=\"0.1\" value=\"25\" \/>\n        <\/label>\n      <\/div>\n\n      <hr style=\"border:none; border-top:1px solid #1f2937; margin:12px 0\" \/>\n\n      <div class=\"row\">\n        <label>Target mode\n          <select data-id=\"targetMode\">\n            <option value=\"boxes\">boxes<\/option>\n            <option value=\"bottles\" selected>bottles<\/option>\n          <\/select>\n        <\/label>\n        <label>Target quantity\n          <input type=\"number\" data-id=\"targetQty\" step=\"1\" min=\"0\" value=\"100\" \/>\n        <\/label>\n      <\/div>\n\n      <div class=\"stack\" style=\"margin-top:10px\">\n        <button class=\"btn primary\" data-act=\"calc\">Recompute layout<\/button>\n        <button class=\"btn good\" data-act=\"auto\">Auto allocate to pallets<\/button>\n        <button class=\"btn\" data-act=\"addPallet\">Add an empty pallet<\/button>\n        <button class=\"btn\" data-act=\"reset\">Reset pallets<\/button>\n        <button class=\"btn\" data-act=\"exportCsv\">Export CSV<\/button>\n      <\/div>\n\n      <div class=\"footer-note\">\n        Drag any filled cell to another pallet to move 1 box. Hold Alt while dropping to move 5 boxes.\n      <\/div>\n    <\/section>\n\n    <section class=\"card\" data-id=\"results\">\n      <h2>Layout and capacity<\/h2>\n      <div class=\"stack\" data-id=\"stats\"><\/div>\n      <div data-id=\"notice\" class=\"footer-note\" style=\"margin-top:8px\"><\/div>\n\n      <h2 style=\"margin-top:14px\">Pallet allocation<\/h2>\n      <div class=\"footer-note\" data-id=\"allocInfo\"><\/div>\n      <div class=\"pallets\" data-id=\"pallets\"><\/div>\n\n      <div style=\"margin-top:12px\" class=\"stack\">\n        <div class=\"stat\"><span>Total pallets<\/span><b data-id=\"totalPallets\">0<\/b><\/div>\n        <div class=\"stat\"><span>Assigned boxes<\/span><b data-id=\"assignedBoxes\">0<\/b><\/div>\n        <div class=\"stat\"><span>Assigned bottles<\/span><b data-id=\"assignedBottles\">0<\/b><\/div>\n        <div class=\"stat\"><span>Remaining to assign<\/span><b data-id=\"remaining\">0<\/b><\/div>\n        <div class=\"stat\"><span>Total weight kg<\/span><b data-id=\"totalWeight\">0<\/b><\/div>\n      <\/div>\n    <\/section>\n  <\/div>\n\n  <script>\n  (() => {\n    const root = document.getElementById('palletCalcWP');\n    if (!root) return;\n\n    const $ = sel => root.querySelector(sel);\n    const el = id => root.querySelector(`[data-id=\"${id}\"]`);\n    const num = v => { const n = Number(v); return Number.isFinite(n) ? n : 0; };\n    const fmt = (n,p=0) => Number(n).toFixed(p);\n    const state = { pallets: [], layout: null };\n    let dragCtx = null;\n\n    const showErr = msg => { const n = el('notice'); if (n) n.innerHTML = `<span class=\"err\">${msg}<\/span>`; };\n\n    function computeLayout(){\n      const boxW = num(el('boxW').value);\n      const boxD = num(el('boxD').value);\n      const boxH = num(el('boxH').value);\n      const bpb  = Math.max(1, num(el('bpb').value));\n      const palW = num(el('palW').value);\n      const palD = num(el('palD').value);\n      const palH = num(el('palH').value);\n      const maxH = num(el('maxH').value);\n      const limitTwo = el('limitTwoLayers').checked;\n      const allowRot = el('allowRotation').value === 'yes';\n\n      if([boxW, boxD, boxH, palW, palD, palH, maxH].some(x => x <= 0)){\n        return { error: 'All dimensions must be positive' };\n      }\n\n      const fitA_w = Math.floor(palW \/ boxD);\n      const fitA_d = Math.floor(palD \/ boxW);\n      const perLayerA = fitA_w * fitA_d;\n\n      const fitB_w = Math.floor(palW \/ boxW);\n      const fitB_d = Math.floor(palD \/ boxD);\n      const perLayerB = fitB_w * fitB_d;\n\n      let using = 'A', perLayer = perLayerA, fitW = fitA_w, fitD = fitA_d, useW = boxD, useD = boxW;\n      if(allowRot && perLayerB > perLayerA){ using='B'; perLayer=perLayerB; fitW=fitB_w; fitD=fitB_d; useW=boxW; useD=boxD; }\n\n      const spaceUsedW = fitW * useW;\n      const spaceUsedD = fitD * useD;\n\n      let maxLayers = Math.floor((maxH - palH) \/ boxH);\n      if(limitTwo) maxLayers = Math.min(maxLayers, 2);\n      if(maxLayers < 0) maxLayers = 0;\n\n      const capacityBoxes = perLayer * maxLayers;\n      const capacityBottles = capacityBoxes * bpb;\n      const stackHeight = palH + maxLayers * boxH;\n\n      return {\n        boxW, boxD, boxH, bpb, palW, palD, palH, maxH,\n        limitTwo, allowRot, orientation: using,\n        perLayer, fitW, fitD, useW, useD, spaceUsedW, spaceUsedD,\n        maxLayers, capacityBoxes, capacityBottles, stackHeight,\n        error: capacityBoxes <= 0 ? 'Box does not fit or height limit is zero. Adjust inputs.' : null\n      };\n    }\n\n    function renderStats(layout){\n      const stats = el('stats'); if (!stats) return;\n      stats.innerHTML = '';\n      el('notice').textContent = '';\n      if(layout.error){ showErr(layout.error); return; }\n      const orientationText = layout.orientation === 'A'\n        ? `along ${layout.palW} use box depth ${layout.useW} cm \u00d7 along ${layout.palD} use box width ${layout.useD} cm`\n        : `along ${layout.palW} use box width ${layout.useW} cm \u00d7 along ${layout.palD} use box depth ${layout.useD} cm`;\n\n      [\n        { k:'Boxes per layer', v:`${layout.perLayer}  (grid ${layout.fitW} \u00d7 ${layout.fitD})` },\n        { k:'Max layers', v: layout.maxLayers },\n        { k:'Capacity per pallet', v:`${layout.capacityBoxes} boxes \u00b7 ${layout.capacityBottles} bottles` },\n        { k:'Stack height', v:`${fmt(layout.stackHeight,1)} cm` },\n        { k:'Orientation', v:`${layout.orientation} \u2022 ${orientationText}` },\n        { k:'Used footprint', v:`${fmt(layout.spaceUsedW,1)} \u00d7 ${fmt(layout.spaceUsedD,1)} cm` },\n      ].forEach(it =>{\n        const d = document.createElement('div');\n        d.className = 'stat';\n        d.innerHTML = `<span>${it.k}<\/span><b>${it.v}<\/b>`;\n        stats.appendChild(d);\n      });\n    }\n\n    function updateAllocInfo(){\n      const l = state.layout; if(!l || l.error){ el('allocInfo').textContent = ''; return; }\n      el('allocInfo').textContent = `Per pallet capacity ${l.capacityBoxes} boxes which is ${l.capacityBottles} bottles. Stack height ${fmt(l.stackHeight,1)} cm.`;\n    }\n\n    function palletCard(p){\n      const l = state.layout;\n      const bottlesPerBox = l.bpb;\n      const kgBottle = num(el('kgBottle').value);\n      const kgBox = num(el('kgBox').value);\n      const kgPallet = num(el('kgPallet').value);\n\n      const capacity = l.capacityBoxes;\n      const perLayer = l.perLayer;\n      const maxLayers = l.maxLayers;\n\n      const usedLayers = capacity === 0 ? 0 : Math.min(maxLayers, Math.ceil(p.boxes \/ Math.max(1, perLayer)));\n      const usedHeight = l.palH + usedLayers * l.boxH;\n      const utilization = capacity === 0 ? 0 : Math.min(1, p.boxes \/ capacity);\n\n      const bottleCount = p.boxes * bottlesPerBox;\n      const kgFromBottles = kgBottle > 0 ? bottleCount * kgBottle : 0;\n      const kgFromBoxes = kgBox > 0 ? p.boxes * kgBox : 0;\n      const totalKg = kgFromBottles + kgFromBoxes + (kgPallet > 0 ? kgPallet : 0);\n\n      const gridCols = l.fitW || 1;\n      const totalSlots = l.perLayer * l.maxLayers;\n      const grid = document.createElement('div');\n      grid.className = 'grid-preview';\n      grid.style.gridTemplateColumns = `repeat(${gridCols}, 1fr)`;\n\n      for(let i=0;i<totalSlots;i++){\n        const cell = document.createElement('i');\n        if(i < p.boxes){\n          cell.classList.add('filled');\n          cell.setAttribute('draggable','true');\n          cell.addEventListener('dragstart', e => {\n            dragCtx = { fromIdx: p.idx, qty: e.altKey ? Math.min(5, p.boxes) : 1 };\n            try { e.dataTransfer.setData('text\/plain', JSON.stringify(dragCtx)); } catch {}\n          });\n        } else {\n          cell.classList.add('empty');\n        }\n        grid.appendChild(cell);\n      }\n\n      const card = document.createElement('div');\n      card.className = 'card pallet';\n      card.innerHTML = `\n        <h3>\n          <span>Pallet ${p.idx+1}<\/span>\n          <span class=\"pill\">cap ${capacity} boxes<\/span>\n        <\/h3>\n        <div class=\"row\" style=\"margin-bottom:8px\">\n          <div>\n            <div class=\"mini\">assigned boxes<\/div>\n            <div class=\"stack\">\n              <button class=\"btn\" data-act=\"dec1\">\u22121<\/button>\n              <button class=\"btn\" data-act=\"dec5\">\u22125<\/button>\n              <span class=\"stat\" style=\"min-width:80px; text-align:center\"><b>${p.boxes}<\/b><\/span>\n              <button class=\"btn\" data-act=\"inc1\">+1<\/button>\n              <button class=\"btn\" data-act=\"inc5\">+5<\/button>\n            <\/div>\n          <\/div>\n          <div>\n            <div class=\"mini right\">bottles on this pallet<\/div>\n            <div class=\"stat right\"><b>${bottleCount}<\/b><\/div>\n          <\/div>\n        <\/div>\n\n        <div class=\"row\">\n          <div>\n            <div class=\"mini\">layers used<\/div>\n            <div class=\"stat\"><b>${usedLayers} of ${maxLayers}<\/b><\/div>\n          <\/div>\n          <div>\n            <div class=\"mini right\">stack height<\/div>\n            <div class=\"stat right\"><b>${fmt(usedHeight,1)} cm<\/b><\/div>\n          <\/div>\n        <\/div>\n\n        <div class=\"mini\" style=\"margin:8px 0 6px\">utilization<\/div>\n        <div class=\"meter\" aria-label=\"utilization\"><i style=\"width:${fmt(utilization*100,0)}%\"><\/i><\/div>\n        <div class=\"mini right\" style=\"margin-top:4px\">${fmt(utilization*100,0)}%<\/div>\n\n        <div class=\"\" style=\"margin-top:10px\"><\/div>\n\n        <div class=\"row\" style=\"margin-top:10px\">\n          <div>\n            <div class=\"mini\">weight kg of this pallet<\/div>\n            <div class=\"stat\"><b>${fmt(totalKg,2)}<\/b><\/div>\n          <\/div>\n          <div class=\"right\">\n            <button class=\"btn warn\" data-act=\"clear\">clear<\/button>\n            <button class=\"btn\" data-act=\"remove\">remove pallet<\/button>\n          <\/div>\n        <\/div>\n      `;\n      card.querySelector('div[style^=\"margin-top:10px\"]').appendChild(grid);\n\n      const setupDrop = (node, targetIdx) => {\n        node.addEventListener('dragover', e => { e.preventDefault(); grid.classList.add('drop-ok'); });\n        node.addEventListener('dragleave', () => grid.classList.remove('drop-ok'));\n        node.addEventListener('drop', e => {\n          e.preventDefault(); grid.classList.remove('drop-ok');\n          const qty = (dragCtx && dragCtx.fromIdx !== undefined) ? (e.altKey ? Math.min(5, state.pallets[dragCtx.fromIdx].boxes) : dragCtx.qty) : 1;\n          if (dragCtx && dragCtx.fromIdx !== undefined) moveBoxes(dragCtx.fromIdx, targetIdx, qty);\n          dragCtx = null;\n        });\n      };\n      setupDrop(grid, p.idx);\n      setupDrop(card.querySelector('h3'), p.idx);\n\n      card.querySelectorAll('button').forEach(btn =>{\n        btn.addEventListener('click', () => {\n          const act = btn.getAttribute('data-act');\n          let delta = 0;\n          if(act === 'inc1') delta = 1;\n          if(act === 'inc5') delta = 5;\n          if(act === 'dec1') delta = -1;\n          if(act === 'dec5') delta = -5;\n          if(act === 'clear') { p.boxes = 0; updateTotals(); renderPallets(); return; }\n          if(act === 'remove') { state.pallets = state.pallets.filter(x => x !== p); renumber(); renderPallets(); updateTotals(); return; }\n          p.boxes = Math.max(0, Math.min(state.layout.capacityBoxes, p.boxes + delta));\n          updateTotals(); renderPallets();\n        });\n      });\n\n      return card;\n    }\n\n    function moveBoxes(fromIdx, toIdx, qty){\n      const l = state.layout; if(!l || l.error) return;\n      if(fromIdx === toIdx) return;\n      const from = state.pallets[fromIdx], to = state.pallets[toIdx];\n      if(!from || !to) return;\n      const movable = Math.max(0, Math.min(qty, from.boxes));\n      const freeCap = Math.max(0, l.capacityBoxes - to.boxes);\n      const willMove = Math.min(movable, freeCap);\n      if(willMove <= 0) return;\n      from.boxes -= willMove; to.boxes += willMove;\n      updateTotals(); renderPallets();\n    }\n\n    function renumber(){ state.pallets.forEach((p,i)=> p.idx = i); }\n\n    function renderPallets(){\n      const wrap = el('pallets'); if (!wrap) return;\n      wrap.innerHTML = '';\n      if(state.layout?.error){ wrap.innerHTML = `<div class=\"footer-note err\">${state.layout.error}<\/div>`; return; }\n      state.pallets.forEach(p => wrap.appendChild(palletCard(p)));\n      el('totalPallets').textContent = state.pallets.length;\n    }\n\n    function updateTotals(){\n      const l = state.layout; if(!l) return;\n      const bpb = l.bpb;\n      const kgBottle = num(el('kgBottle').value);\n      const kgBox = num(el('kgBox').value);\n      const kgPallet = num(el('kgPallet').value);\n\n      const assignedBoxes = state.pallets.reduce((s,p)=> s + p.boxes, 0);\n      const assignedBottles = assignedBoxes * bpb;\n\n      const totalWeight = state.pallets.reduce((s,p)=>{\n        const perPallet = (kgBottle>0? p.boxes*bpb*kgBottle : 0) + (kgBox>0? p.boxes*kgBox : 0) + (kgPallet>0? kgPallet : 0);\n        return s + perPallet;\n      }, 0);\n\n      const mode = el('targetMode').value;\n      const qty = Math.max(0, num(el('targetQty').value));\n      const targetBoxes = mode === 'boxes' ? qty : Math.ceil(qty \/ Math.max(1, bpb));\n      const remaining = targetBoxes - assignedBoxes;\n\n      el('assignedBoxes').textContent = assignedBoxes;\n      el('assignedBottles').textContent = assignedBottles;\n      el('remaining').textContent = remaining;\n      el('totalWeight').textContent = fmt(totalWeight, 2);\n    }\n\n    function autoAllocate(){\n      if (!state.layout) recompute();\n      const l = state.layout; if(!l || l.error) return;\n      const mode = el('targetMode').value;\n      let qty = Math.max(0, num(el('targetQty').value));\n      let targetBoxes = mode === 'boxes' ? qty : Math.ceil(qty \/ Math.max(1, l.bpb));\n      const cap = Math.max(1, l.capacityBoxes);\n      const palletsNeeded = Math.ceil(targetBoxes \/ cap);\n      state.pallets = [];\n      for(let i=0;i<palletsNeeded;i++){\n        const remaining = targetBoxes - i*cap;\n        const fill = Math.min(cap, Math.max(0, remaining));\n        state.pallets.push({ idx:i, boxes: fill });\n      }\n      renderPallets(); updateTotals();\n    }\n\n    function addPallet(withBoxes=0){\n      if (!state.layout) recompute();\n      if(state.layout?.error) return;\n      state.pallets.push({ idx: state.pallets.length, boxes: Math.max(0, Math.min(withBoxes, state.layout.capacityBoxes)) });\n      renderPallets(); updateTotals();\n    }\n\n    function exportCsv(){\n      const l = state.layout; if(!l) return;\n      const lines = [];\n      lines.push(['Pallet','Boxes','Bottles','Layers used','Stack height cm','Weight kg'].join(','));\n      const bpb = l.bpb;\n      const kgBottle = num(el('kgBottle').value);\n      const kgBox = num(el('kgBox').value);\n      const kgPallet = num(el('kgPallet').value);\n\n      let totalBoxes=0, totalBottles=0, totalKg=0;\n      state.pallets.forEach((p,i)=>{\n        const usedLayers = l.capacityBoxes === 0 ? 0 : Math.min(l.maxLayers, Math.ceil(p.boxes \/ Math.max(1, l.perLayer)));\n        const usedHeight = l.palH + usedLayers * l.boxH;\n        const bottleCount = p.boxes * bpb;\n        const kgFromBottles = kgBottle>0 ? bottleCount*kgBottle : 0;\n        const kgFromBoxes = kgBox>0 ? p.boxes*kgBox : 0;\n        const sumKg = kgFromBottles + kgFromBoxes + (kgPallet>0 ? kgPallet : 0);\n        lines.push([i+1, p.boxes, bottleCount, usedLayers, fmt(usedHeight,1), fmt(sumKg,2)].join(','));\n        totalBoxes += p.boxes; totalBottles += bottleCount; totalKg += sumKg;\n      });\n      lines.push(['TOTAL', totalBoxes, totalBottles, '', '', fmt(totalKg,2)].join(','));\n      const csv = lines.join('\\n');\n      const blob = new Blob([csv], { type: 'text\/csv' });\n      const a = document.createElement('a');\n      a.href = URL.createObjectURL(blob);\n      a.download = 'pallet_plan.csv';\n      a.click();\n    }\n\n    function recompute(){\n      try{\n        state.layout = computeLayout();\n        renderStats(state.layout);\n        updateAllocInfo();\n        renderPallets();\n        updateTotals();\n      }catch(e){\n        showErr('Script error. Check console.'); console.error(e);\n      }\n    }\n\n    \/\/ hooks\n    root.querySelector('[data-act=\"calc\"]').addEventListener('click', recompute);\n    root.querySelector('[data-act=\"addPallet\"]').addEventListener('click', () => addPallet(0));\n    root.querySelector('[data-act=\"auto\"]').addEventListener('click', autoAllocate);\n    root.querySelector('[data-act=\"reset\"]').addEventListener('click', () => { state.pallets = []; renderPallets(); updateTotals(); });\n    root.querySelector('[data-act=\"exportCsv\"]').addEventListener('click', exportCsv);\n\n    ['boxW','boxD','boxH','bpb','palW','palD','palH','maxH','allowRotation','limitTwoLayers','targetMode','targetQty','kgBottle','kgBox','kgPallet']\n      .forEach(id => el(id).addEventListener('change', recompute));\n\n    \/\/ init\n    recompute();\n  })();\n  <\/script>\n<\/div>\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>","protected":false},"excerpt":{"rendered":"<p>Air Freight Euro Pallet Planner Add or remove boxes. Drag between pallets to balance weight. Inputs Box width cm Box depth cm Box height cm Bottles per box Allow box rotation to maximize per layer yesno Pallet width cm Pallet depth cm Pallet height cm Max total stack height cm limit to two box layers Bottle weight kg Empty box weight kg Pallet weight kg Target mode boxesbottles Target quantity Recompute layout Auto allocate to pallets Add an empty pallet Reset pallets Export CSV Drag any filled cell to another pallet to move 1 box. Hold Alt while dropping to move 5 boxes. Layout and capacity Pallet allocation Total pallets0 Assigned boxes0 Assigned bottles0 Remaining to assign0 Total weight kg0<\/p>","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-57","page","type-page","status-publish","hentry"],"_hostinger_reach_plugin_has_subscription_block":false,"_hostinger_reach_plugin_is_elementor":false,"_links":{"self":[{"href":"https:\/\/co.ecchotag.com\/ja\/wp-json\/wp\/v2\/pages\/57","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/co.ecchotag.com\/ja\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/co.ecchotag.com\/ja\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/co.ecchotag.com\/ja\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/co.ecchotag.com\/ja\/wp-json\/wp\/v2\/comments?post=57"}],"version-history":[{"count":31,"href":"https:\/\/co.ecchotag.com\/ja\/wp-json\/wp\/v2\/pages\/57\/revisions"}],"predecessor-version":[{"id":91,"href":"https:\/\/co.ecchotag.com\/ja\/wp-json\/wp\/v2\/pages\/57\/revisions\/91"}],"wp:attachment":[{"href":"https:\/\/co.ecchotag.com\/ja\/wp-json\/wp\/v2\/media?parent=57"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}