added on local at 2026-07-01 13:48:03
| 1 | <!-- Fulfillment list. Backed by /fulfill.cgi. --> | |
| 2 | ||
| 3 | <style> | |
| 4 | .ful-head-actions { display:flex; gap:8px; flex-wrap:wrap; } | |
| 5 | .ful-tabs { display:flex; gap:4px; overflow-x:auto; padding-bottom:6px; margin-bottom:14px; border-bottom:1px solid var(--col-border); } | |
| 6 | .ful-tab { flex:0 0 auto; padding:8px 14px; border-radius:8px 8px 0 0; color:var(--col-text-3); font-size:12px; font-weight:600; letter-spacing:1px; text-transform:uppercase; text-decoration:none; border:1px solid transparent; border-bottom:none; transition:background .12s,color .12s; white-space:nowrap; display:inline-flex; gap:8px; align-items:center; } | |
| 7 | .ful-tab:hover { color:var(--col-text); background:rgba(124,58,237,0.10); } | |
| 8 | .ful-tab.is-active { background:var(--col-surface-1); color:var(--col-accent-bright); border-color:var(--col-border); border-bottom-color:var(--col-surface-1); position:relative; top:1px; } | |
| 9 | .ful-tab-n { display:inline-block; min-width:18px; padding:1px 6px; border-radius:10px; background:var(--col-surface-2); color:var(--col-text-2); font-size:11px; font-family:var(--font-mono,monospace); } | |
| 10 | .ful-tab.is-active .ful-tab-n { background:rgba(124,58,237,0.18); color:var(--col-accent-bright); } | |
| 11 | ||
| 12 | .ful-toolbar { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-bottom:12px; padding:10px 14px; background:var(--col-surface-1); border:1px solid var(--col-border); border-radius:10px; } | |
| 13 | .ful-toolbar .input.ful-search { width:340px; max-width:100%; } | |
| 14 | ||
| 15 | .ful-table-wrap { background:var(--col-surface-1); border:1px solid var(--col-border); border-radius:12px; overflow-x:auto; } | |
| 16 | table.ful-table { width:100%; border-collapse:separate; border-spacing:0; } | |
| 17 | .ful-table thead th { font-size:11px; text-transform:uppercase; letter-spacing:1px; color:var(--col-text-3); font-weight:700; text-align:left; padding:12px 14px; border-bottom:1px solid var(--col-border); background:var(--col-surface-2); } | |
| 18 | .ful-table tbody td { padding:12px 14px; border-bottom:1px solid var(--col-border); font-size:13px; color:var(--col-text); vertical-align:middle; } | |
| 19 | .ful-table tbody tr:hover { background:var(--col-surface-2); cursor:pointer; } | |
| 20 | .ful-table tbody tr:last-child td { border-bottom:none; } | |
| 21 | .ful-id { font-family:var(--font-mono,monospace); color:var(--col-text-3); font-size:12px; } | |
| 22 | .ful-buyer { font-weight:600; } | |
| 23 | .ful-addr { color:var(--col-text-2); font-size:12px; line-height:1.35; } | |
| 24 | .ful-price { font-family:var(--font-mono,monospace); font-weight:700; color:var(--col-text); } | |
| 25 | .ful-ship { font-family:var(--font-mono,monospace); color:var(--col-text-2); font-size:12px; } | |
| 26 | ||
| 27 | .ful-chip { display:inline-flex; align-items:center; padding:3px 10px; border-radius:999px; font-size:11px; font-weight:700; letter-spacing:0.5px; text-transform:uppercase; } | |
| 28 | .fs-new { background:rgba(96,165,250,0.18); color:#60a5fa; } | |
| 29 | .fs-processing { background:rgba(167,139,250,0.20); color:#a78bfa; } | |
| 30 | .fs-printing { background:rgba(251,191,36,0.20); color:#fbbf24; } | |
| 31 | .fs-printed { background:rgba(251,191,36,0.32); color:#fde68a; } | |
| 32 | .fs-packed { background:rgba(6,182,212,0.22); color:#22d3ee; } | |
| 33 | .fs-shipped { background:rgba(34,197,94,0.20); color:#4ade80; } | |
| 34 | .fs-partially_shipped { background:rgba(34,197,94,0.12); color:#86efac; } | |
| 35 | .fs-delivered { background:rgba(34,197,94,0.32); color:#86efac; } | |
| 36 | .fs-completed { background:rgba(156,163,175,0.20); color:#d1d5db; } | |
| 37 | .fs-on_hold { background:rgba(245,158,11,0.18); color:#fbbf24; } | |
| 38 | .fs-cancelled { background:rgba(248,113,113,0.18); color:#f87171; } | |
| 39 | .fs-returned { background:rgba(248,113,113,0.32); color:#fca5a5; } | |
| 40 | ||
| 41 | .ful-empty { padding:60px 24px; text-align:center; border:1px dashed var(--col-border-2); border-radius:14px; color:var(--col-text-2); margin-top:14px; } | |
| 42 | .ful-checkbox { width:16px; height:16px; } | |
| 43 | .ful-bulk-bar { display:none; gap:8px; align-items:center; padding:10px 14px; background:rgba(124,58,237,0.10); border:1px solid rgba(124,58,237,0.32); border-radius:10px; margin-bottom:12px; } | |
| 44 | .ful-bulk-bar.is-active { display:flex; } | |
| 45 | .ful-bulk-count { font-weight:700; color:var(--col-accent-bright); font-size:13px; margin-right:6px; } | |
| 46 | </style> | |
| 47 | ||
| 48 | <div class="page-head"> | |
| 49 | <div> | |
| 50 | <span class="page-eyebrow"><span class="dot"></span> Studio · Fulfillment</span> | |
| 51 | <h1 class="page-title">Fulfill Orders</h1> | |
| 52 | <p class="page-subtitle">Physical orders waiting to be printed, packed, and shipped. $row_count showing.</p> | |
| 53 | </div> | |
| 54 | <div class="page-actions ful-head-actions"> | |
| 55 | <a href="/shipping_settings.cgi" class="btn btn-secondary" title="Origin address, carriers, zones, default rates"> | |
| 56 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg> | |
| 57 | Shipping settings | |
| 58 | </a> | |
| 59 | </div> | |
| 60 | </div> | |
| 61 | ||
| 62 | [if:$has_flash] | |
| 63 | <div class="banner $flash_kind mb-3"> | |
| 64 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/></svg> | |
| 65 | <div class="text-sm fw-600">$flash_msg</div> | |
| 66 | </div> | |
| 67 | [/if] | |
| 68 | ||
| 69 | [if:$needs_setup] | |
| 70 | <div class="banner danger mb-3"> | |
| 71 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/></svg> | |
| 72 | <div class="text-sm fw-600">Set your origin address in <a href="/shipping_settings.cgi" class="text-brand">Shipping Settings</a> before printing labels — carriers need a return address.</div> | |
| 73 | </div> | |
| 74 | [/if] | |
| 75 | ||
| 76 | <div class="ful-tabs"> | |
| 77 | [loop:@tabs] | |
| 78 | <a href="$loop1.href" class="ful-tab[if:$loop1.active] is-active[/if]" title="$loop1.hint"> | |
| 79 | <span>$loop1.label</span> | |
| 80 | <span class="ful-tab-n">$loop1.count</span> | |
| 81 | </a> | |
| 82 | [/loop] | |
| 83 | </div> | |
| 84 | ||
| 85 | <form method="GET" action="/fulfill.cgi" class="ful-toolbar"> | |
| 86 | <input type="hidden" name="status" value="$status_filter"> | |
| 87 | <input type="text" name="q" class="input ful-search" placeholder="Search by buyer email, ship-to name, or order ID…" value="$q"> | |
| 88 | <button type="submit" class="btn btn-primary">Search</button> | |
| 89 | [if:$q]<a href="/fulfill.cgi?status=$status_filter" class="btn btn-secondary">Clear</a>[/if] | |
| 90 | </form> | |
| 91 | ||
| 92 | <div class="ful-bulk-bar" id="fulBulkBar"> | |
| 93 | <span class="ful-bulk-count" id="fulBulkCount">0 selected</span> | |
| 94 | <form method="POST" action="/fulfill.cgi" id="fulBulkForm" style="display:flex;gap:6px;align-items:center;flex-wrap:wrap"> | |
| 95 | <input type="hidden" name="_act" value="bulk"> | |
| 96 | <select name="new_status" class="select" style="height:36px"> | |
| 97 | <option value="processing">Mark Processing</option> | |
| 98 | <option value="printing">Mark Printing</option> | |
| 99 | <option value="printed">Mark Printed</option> | |
| 100 | <option value="packed">Mark Packed</option> | |
| 101 | <option value="shipped">Mark Shipped</option> | |
| 102 | <option value="delivered">Mark Delivered</option> | |
| 103 | <option value="completed">Mark Completed</option> | |
| 104 | <option value="on_hold">On hold</option> | |
| 105 | <option value="cancelled">Cancel</option> | |
| 106 | </select> | |
| 107 | <div id="fulBulkIds"></div> | |
| 108 | <button type="submit" class="btn btn-primary">Apply</button> | |
| 109 | </form> | |
| 110 | </div> | |
| 111 | ||
| 112 | [if:!$has_rows] | |
| 113 | <div class="ful-empty"> | |
| 114 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:48px;height:48px;margin-bottom:8px;color:var(--col-text-3)"><path d="M16 16h.01"/><path d="M3 9h18l-2 9H5z"/><path d="M3 9 5 5h14l2 4"/></svg> | |
| 115 | <h2 style="color:#fff;font-family:var(--font-display,inherit);margin-bottom:6px">No orders to fulfill</h2> | |
| 116 | <p>When a buyer purchases a physical (printed-and-shipped) listing it shows up here. Set the price/weight/dimensions on each model from the Edit form, and the rate estimator can quote shipping at checkout.</p> | |
| 117 | </div> | |
| 118 | [/if] | |
| 119 | ||
| 120 | [if:$has_rows] | |
| 121 | <div class="ful-table-wrap"> | |
| 122 | <table class="ful-table"> | |
| 123 | <thead> | |
| 124 | <tr> | |
| 125 | <th style="width:32px"><input type="checkbox" class="ful-checkbox" id="fulSelAll" title="Select all on page"></th> | |
| 126 | <th>Order</th> | |
| 127 | <th>Buyer / ship-to</th> | |
| 128 | <th style="text-align:right">Total</th> | |
| 129 | <th style="text-align:right">Ship</th> | |
| 130 | <th>Status</th> | |
| 131 | <th>Created</th> | |
| 132 | </tr> | |
| 133 | </thead> | |
| 134 | <tbody> | |
| 135 | [loop:@rows] | |
| 136 | <tr data-order-id="$loop1.id" onclick="window.location='$loop1.href'"> | |
| 137 | <td onclick="event.stopPropagation()"><input type="checkbox" class="ful-checkbox ful-row-cb" value="$loop1.id"></td> | |
| 138 | <td><div class="ful-id">#$loop1.id</div></td> | |
| 139 | <td> | |
| 140 | <div class="ful-buyer">$loop1.buyer_email</div> | |
| 141 | <div class="ful-addr">$loop1.ship_to</div> | |
| 142 | </td> | |
| 143 | <td style="text-align:right" class="ful-price">$loop1.total <span style="color:var(--col-text-3);font-weight:400">$loop1.currency</span></td> | |
| 144 | <td style="text-align:right" class="ful-ship">$loop1.shipping</td> | |
| 145 | <td><span class="ful-chip $loop1.status_class">$loop1.status_label</span></td> | |
| 146 | <td class="ful-addr">$loop1.created_at</td> | |
| 147 | </tr> | |
| 148 | [/loop] | |
| 149 | </tbody> | |
| 150 | </table> | |
| 151 | </div> | |
| 152 | [/if] | |
| 153 | ||
| 154 | <script> | |
| 155 | (function(){ | |
| 156 | var bar = document.getElementById('fulBulkBar'); | |
| 157 | if (!bar) return; | |
| 158 | var countEl = document.getElementById('fulBulkCount'); | |
| 159 | var idsEl = document.getElementById('fulBulkIds'); | |
| 160 | var selAll = document.getElementById('fulSelAll'); | |
| 161 | var cbs = document.querySelectorAll('.ful-row-cb'); | |
| 162 | ||
| 163 | function refresh() { | |
| 164 | var ids = []; | |
| 165 | cbs.forEach(function(c){ if (c.checked) ids.push(c.value); }); | |
| 166 | countEl.textContent = ids.length + (ids.length === 1 ? ' selected' : ' selected'); | |
| 167 | bar.classList.toggle('is-active', ids.length > 0); | |
| 168 | idsEl.innerHTML = ''; | |
| 169 | ids.forEach(function(id){ | |
| 170 | var i = document.createElement('input'); | |
| 171 | i.type = 'hidden'; i.name = 'order_ids'; i.value = id; | |
| 172 | idsEl.appendChild(i); | |
| 173 | }); | |
| 174 | } | |
| 175 | cbs.forEach(function(c){ c.addEventListener('change', refresh); }); | |
| 176 | if (selAll) selAll.addEventListener('change', function(){ | |
| 177 | cbs.forEach(function(c){ c.checked = selAll.checked; }); | |
| 178 | refresh(); | |
| 179 | }); | |
| 180 | })(); | |
| 181 | </script> |