Files
EMERGENCY/skel/inventory/data/static/index.html
T
2026-06-30 18:30:24 +00:00

227 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Inventory - The Dark Elite</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
:root { --bg:#060708; --border:rgba(80,90,100,0.2); --accent:#22d3ee; --text:#e5e7eb; --muted:#9ca3af; --danger:#dc2626; --warn:#f59e0b; --ok:#22c55e; }
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;min-height:100vh}
.container{max-width:1300px;margin:0 auto;padding:2rem}
header{display:flex;justify-content:space-between;align-items:center;padding-bottom:1.5rem;border-bottom:1px solid var(--border);margin-bottom:2rem}
header h1{margin:0;font-size:1.5rem;letter-spacing:0.2em;text-transform:uppercase;color:var(--accent);text-shadow:0 0 12px rgba(34,211,238,0.4)}
header a{color:var(--muted);text-decoration:none;font-size:0.85rem}
header a:hover{color:var(--accent)}
.section-title{font-size:0.75rem;letter-spacing:0.25em;text-transform:uppercase;color:var(--accent);border-left:3px solid var(--accent);padding-left:0.75rem;margin:2rem 0 1rem 0;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:0.5rem}
.controls{display:flex;gap:0.5rem;flex-wrap:wrap}
.controls input,.controls select{background:rgba(10,11,13,0.6);border:1px solid var(--border);color:var(--text);padding:0.4rem 0.7rem;border-radius:4px;font-size:0.8rem;font-family:ui-monospace,monospace}
.controls input:focus,.controls select:focus{outline:none;border-color:var(--accent)}
.add-form{background:linear-gradient(135deg,rgba(18,20,24,0.7),rgba(10,11,13,0.85));border:1px solid var(--border);border-radius:8px;padding:1.25rem;display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:0.75rem;margin-bottom:2rem}
.add-form input,.add-form select{background:rgba(10,11,13,0.6);border:1px solid var(--border);color:var(--text);padding:0.55rem 0.7rem;border-radius:4px;font-size:0.85rem;font-family:ui-monospace,monospace}
.add-form input:focus,.add-form select:focus{outline:none;border-color:var(--accent)}
.add-form .full{grid-column:1/-1}
.add-form button{background:rgba(34,211,238,0.15);border:1px solid var(--accent);color:var(--accent);padding:0.55rem;border-radius:4px;font-size:0.8rem;cursor:pointer;letter-spacing:0.1em;text-transform:uppercase;font-family:ui-monospace,monospace}
.add-form button:hover{background:rgba(34,211,238,0.25)}
table{width:100%;border-collapse:collapse;font-size:0.85rem;font-family:ui-monospace,monospace}
th{text-align:left;color:var(--muted);font-weight:400;text-transform:uppercase;letter-spacing:0.1em;font-size:0.7rem;padding:0.7rem 0.5rem;border-bottom:1px solid var(--border);position:sticky;top:0;background:var(--bg);cursor:pointer;user-select:none}
th:hover{color:var(--accent)}
td{padding:0.5rem 0.5rem;border-bottom:1px solid rgba(80,90,100,0.1);vertical-align:middle}
tr:hover td{background:rgba(34,211,238,0.04)}
td.qty{color:var(--accent);font-weight:600;text-align:center;white-space:nowrap}
td.editable{cursor:text}
td.editable:hover{background:rgba(34,211,238,0.08)}
td input{background:rgba(10,11,13,0.9);border:1px solid var(--accent);color:var(--accent);padding:0.2rem 0.4rem;border-radius:3px;font:inherit;width:100%;outline:none}
.qty-btn{background:rgba(18,20,24,0.9);border:1px solid var(--border);color:var(--text);width:22px;height:22px;border-radius:3px;font-size:0.85rem;cursor:pointer;font-family:ui-monospace,monospace;line-height:1;padding:0}
.qty-btn:hover{border-color:var(--accent);color:var(--accent)}
.qty-btn.minus:hover{border-color:var(--warn);color:var(--warn)}
.qty-val{display:inline-block;min-width:2.5rem;text-align:center;padding:0 0.4rem}
.del{color:var(--muted);cursor:pointer;font-size:0.7rem;text-transform:uppercase;letter-spacing:0.1em}
.del:hover{color:var(--danger)}
.empty{color:var(--muted);text-align:center;padding:2rem;font-style:italic}
.toast{position:fixed;bottom:2rem;right:2rem;background:rgba(18,20,24,0.95);border:1px solid var(--accent);color:var(--accent);padding:0.8rem 1.2rem;border-radius:4px;font-family:ui-monospace,monospace;font-size:0.85rem;z-index:100;display:none}
.cat-pill{display:inline-block;background:rgba(34,211,238,0.12);border:1px solid rgba(34,211,238,0.3);color:rgb(165,235,247);font-size:0.65rem;padding:0.1rem 0.4rem;border-radius:3px;text-transform:uppercase;letter-spacing:0.1em}
.expired{color:var(--danger)}
.expiring{color:var(--warn)}
.count{font-size:0.75rem;color:var(--muted);font-family:ui-monospace,monospace}
</style>
</head>
<body>
<div class="container">
<header>
<div><h1>Inventory</h1><div class="count" id="count">Loading...</div></div>
<a href="/" style="color:#9ca3af;text-decoration:none;font-size:0.85rem">← Hub</a>
</header>
<div class="section-title">Add Item</div>
<form class="add-form" id="addForm">
<input name="name" placeholder="Item name" required>
<select name="category" id="catInput">
<option value="">Category</option>
<option>Food - Canned</option><option>Food - Dry Goods</option><option>Food - Frozen</option><option>Food - Fresh</option>
<option>Water</option><option>Medical</option><option>Hygiene</option><option>Cleaning</option>
<option>Tools</option><option>Hardware</option><option>Batteries</option><option>Fuel</option>
<option>Ammunition</option><option>Clothing</option><option>Camping</option><option>Electronics</option>
<option>Office</option><option>Pet/Livestock</option><option>Seeds</option><option>Other</option>
</select>
<input name="quantity" type="number" step="0.01" value="1" placeholder="Qty">
<input name="unit" placeholder="Unit (cans, lbs, ea)">
<input name="location" placeholder="Location (pantry, garage)">
<input name="expires" type="date" placeholder="Expires">
<input name="notes" class="full" placeholder="Notes">
<button type="submit" class="full">+ Add Item</button>
</form>
<div class="section-title">
Inventory
<div class="controls">
<select id="filterCat"><option value="">All Categories</option></select>
<select id="filterExp">
<option value="">All Items</option>
<option value="expired">Expired</option>
<option value="soon">Expiring &lt; 30d</option>
<option value="no-exp">No Expiration</option>
</select>
<input type="text" id="search" placeholder="Filter...">
</div>
</div>
<table>
<thead>
<tr>
<th data-sort="name">Item</th>
<th data-sort="category">Category</th>
<th data-sort="quantity">Qty</th>
<th data-sort="unit">Unit</th>
<th data-sort="location">Location</th>
<th data-sort="expires">Expires</th>
<th data-sort="notes">Notes</th>
<th></th>
</tr>
</thead>
<tbody id="rows"><tr><td colspan="8" class="empty">Loading...</td></tr></tbody>
</table>
</div>
<div id="toast" class="toast"></div>
<script>
let all=[]; let sortKey="category"; let sortDir=1;
const FIELDS=["name","category","quantity","unit","location","expires","notes"];
function toast(m){const t=document.getElementById("toast");t.textContent=m;t.style.display="block";setTimeout(()=>t.style.display="none",1800);}
function dayDiff(d){if(!d)return null;const ms=new Date(d).getTime()-Date.now();return Math.floor(ms/86400000);}
function expClass(d){const dd=dayDiff(d);if(dd===null)return"";if(dd<0)return"expired";if(dd<30)return"expiring";return"";}
async function load(){
const r=await fetch("/inventory/api/list");
all=await r.json();
populateCats();
render();
}
function populateCats(){
const cats=[...new Set(all.map(x=>x.category).filter(Boolean))].sort();
const sel=document.getElementById("filterCat");
const cur=sel.value;
sel.innerHTML='<option value="">All Categories</option>'+cats.map(c=>'<option>'+c+'</option>').join("");
sel.value=cur;
}
function render(){
const q=document.getElementById("search").value.toLowerCase();
const cat=document.getElementById("filterCat").value;
const expF=document.getElementById("filterExp").value;
let items=all.filter(x=>{
if(cat && x.category!==cat)return false;
if(expF==="expired"){const dd=dayDiff(x.expires);if(dd===null||dd>=0)return false;}
if(expF==="soon"){const dd=dayDiff(x.expires);if(dd===null||dd<0||dd>=30)return false;}
if(expF==="no-exp" && x.expires)return false;
if(q){const s=FIELDS.map(f=>(x[f]||"")).join(" ").toLowerCase();if(!s.includes(q))return false;}
return true;
});
items.sort((a,b)=>{
let va=a[sortKey], vb=b[sortKey];
if(sortKey==="quantity"){return ((parseFloat(va)||0)-(parseFloat(vb)||0))*sortDir;}
return String(va||"").localeCompare(String(vb||""))*sortDir;
});
document.getElementById("count").textContent=items.length+" of "+all.length+" items";
const tbody=document.getElementById("rows");
if(!items.length){tbody.innerHTML='<tr><td colspan="8" class="empty">No items. Add one above.</td></tr>';return;}
tbody.innerHTML=items.map(x=>{
const ec=expClass(x.expires);
return '<tr data-id="'+x.id+'">'+
'<td class="editable" data-f="name">'+(x.name||"")+'</td>'+
'<td class="editable" data-f="category">'+(x.category?'<span class="cat-pill">'+x.category+'</span>':"")+'</td>'+
'<td class="qty"><button class="qty-btn minus" data-d="-1"></button><span class="qty-val">'+(x.quantity||0)+'</span><button class="qty-btn plus" data-d="1">+</button></td>'+
'<td class="editable" data-f="unit">'+(x.unit||"")+'</td>'+
'<td class="editable" data-f="location">'+(x.location||"")+'</td>'+
'<td class="editable '+ec+'" data-f="expires">'+(x.expires||"")+'</td>'+
'<td class="editable" data-f="notes" style="max-width:280px">'+(x.notes||"")+'</td>'+
'<td><span class="del">Del</span></td>'+
'</tr>';
}).join("");
}
document.getElementById("addForm").addEventListener("submit",async e=>{
e.preventDefault();
const fd=new FormData(e.target);
const r=await fetch("/inventory/api/add",{method:"POST",body:fd});
if(r.ok){toast("Added");e.target.reset();e.target.quantity.value=1;load();}
else toast("Failed");
});
document.querySelectorAll("th[data-sort]").forEach(th=>{
th.addEventListener("click",()=>{
const k=th.dataset.sort;
if(sortKey===k)sortDir*=-1; else {sortKey=k;sortDir=1;}
render();
});
});
document.getElementById("search").addEventListener("input",render);
document.getElementById("filterCat").addEventListener("change",render);
document.getElementById("filterExp").addEventListener("change",render);
document.getElementById("rows").addEventListener("click",async e=>{
const tr=e.target.closest("tr"); if(!tr)return;
const id=tr.dataset.id;
if(e.target.classList.contains("del")){
if(!confirm("Delete this item?"))return;
await fetch("/inventory/api/delete/"+id,{method:"DELETE"});
toast("Deleted");load();
return;
}
if(e.target.classList.contains("qty-btn")){
const delta=e.target.dataset.d;
const fd=new FormData(); fd.append("delta",delta);
const r=await fetch("/inventory/api/adjust/"+id,{method:"POST",body:fd});
if(r.ok){
const d=await r.json();
tr.querySelector(".qty-val").textContent=d.quantity;
const it=all.find(x=>x.id===id); if(it) it.quantity=d.quantity;
}
return;
}
const td=e.target.closest("td.editable");
if(!td || td.querySelector("input"))return;
const field=td.dataset.f;
const it=all.find(x=>x.id===id);
const cur=field==="category"?(it.category||""):(it[field]||"");
td.innerHTML='<input type="text" value="'+String(cur).replace(/"/g,"&quot;")+'">';
const inp=td.querySelector("input"); inp.focus(); inp.select();
const save=async()=>{
const val=inp.value;
const fd=new FormData(); fd.append(field,val);
await fetch("/inventory/api/update/"+id,{method:"PATCH",body:fd});
toast("Updated");load();
};
inp.addEventListener("blur",save);
inp.addEventListener("keydown",ev=>{if(ev.key==="Enter")inp.blur();if(ev.key==="Escape"){td.textContent=cur;}});
});
load();
</script>
</body>
</html>