InventoryManagement / index.html
SolarumAsteridion
fixes
6912049
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HomeStack | Digital Inventory Curator</title>
<script src="https://cdn.tailwindcss.com"></script>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.js"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#6750A4',
'on-primary': '#FFFFFF',
'primary-container': '#EADDFF',
'on-primary-container': '#21005D',
secondary: '#625B71',
'on-secondary': '#FFFFFF',
'secondary-container': '#E8DEF8',
'on-secondary-container': '#1D192B',
tertiary: '#7D5260',
'on-tertiary': '#FFFFFF',
'tertiary-container': '#FFD8E4',
'on-tertiary-container': '#31111D',
error: '#B3261E',
'on-error': '#FFFFFF',
'error-container': '#F9DEDC',
'on-error-container': '#410E0B',
surface: '#FEF7FF',
'on-surface': '#1D1B20',
'surface-variant': '#E7E0EB',
'on-surface-variant': '#49454F',
outline: '#79747E',
'outline-variant': '#CAC4D0',
'surface-container-lowest': '#FFFFFF',
'surface-container-low': '#F7F2FA',
'surface-container': '#F3EDF7',
'surface-container-high': '#ECE6F0',
'surface-container-highest': '#E6E0E9',
},
fontFamily: {
sans: ['Inter', 'sans-serif'],
headline: ['Inter', 'sans-serif'],
}
}
}
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.material-symbols-outlined.fill {
font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.editorial-shadow {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
}
.screen {
display: none;
}
.screen.active {
display: flex;
}
</style>
</head>
<body class="bg-surface text-on-surface min-h-screen font-sans">
<!-- Login Screen -->
<main id="login-screen"
class="screen active flex-col items-center justify-center px-6 py-12 relative overflow-hidden min-h-screen">
<div class="absolute -top-24 -right-24 w-96 h-96 bg-primary-container/20 rounded-full blur-3xl"></div>
<div class="absolute -bottom-24 -left-24 w-80 h-80 bg-secondary-container/30 rounded-full blur-3xl"></div>
<div class="w-full max-w-md z-10">
<div class="text-center mb-12">
<div
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-container text-primary mb-6 editorial-shadow">
<span class="material-symbols-outlined text-3xl">inventory_2</span>
</div>
<h1 class="font-headline font-extrabold text-4xl tracking-tight text-primary mb-2">HomeStack</h1>
<p class="text-on-surface-variant font-medium text-lg">Digital Inventory Curator</p>
</div>
<div
class="bg-surface-container-lowest p-8 md:p-10 rounded-[2rem] editorial-shadow border border-outline-variant/10">
<div class="mb-8">
<h2 class="text-2xl font-headline font-bold text-on-surface mb-3 tracking-tight">Welcome Back</h2>
<p class="text-on-surface-variant leading-relaxed">Enter Password to access your digital inventory
</p>
</div>
<form id="login-form" class="space-y-8">
<div class="space-y-2">
<label class="block text-sm font-semibold text-on-surface ml-1">Access Key</label>
<div class="relative">
<div
class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-outline">
<span class="material-symbols-outlined text-xl">lock</span>
</div>
<input type="password" id="password-input"
class="block w-full min-h-[4rem] pl-12 pr-4 bg-surface-container-low border-0 rounded-xl focus:ring-2 focus:ring-primary/40 focus:bg-surface-container-lowest transition-all duration-300 text-lg placeholder:text-outline/50"
placeholder="••••••••••••" required>
</div>
<p id="login-error" class="text-error text-sm font-medium ml-1 hidden"></p>
</div>
<div class="flex items-center gap-3 ml-1">
<label class="relative inline-flex items-center cursor-pointer group">
<input type="checkbox" id="remember-me" class="sr-only peer">
<div class="w-11 h-6 bg-surface-container-high peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary transition-colors duration-300"></div>
<span class="ms-3 text-sm font-bold text-on-surface-variant group-hover:text-primary transition-colors cursor-pointer">Remember me for 30 days</span>
</label>
</div>
<button type="submit" id="login-btn"
class="group w-full min-h-[4rem] bg-gradient-to-r from-primary to-primary text-on-primary font-bold text-lg rounded-xl flex items-center justify-center gap-2 hover:opacity-95 transition-all duration-300 active:scale-[0.98] editorial-shadow">
<span id="login-btn-text">Unlock Vault</span>
<span
class="material-symbols-outlined group-hover:translate-x-1 transition-transform">arrow_forward</span>
</button>
</form>
</div>
<div class="mt-10 text-center space-y-4">
<p class="text-sm text-on-surface-variant">Need help? Contact your administrator.</p>
<div class="flex items-center justify-center gap-6">
<a href="#"
class="text-xs font-semibold text-outline uppercase tracking-widest hover:text-primary transition-colors">Privacy</a>
<div class="w-1 h-1 bg-outline-variant/40 rounded-full"></div>
<a href="#"
class="text-xs font-semibold text-outline uppercase tracking-widest hover:text-primary transition-colors">Security</a>
<div class="w-1 h-1 bg-outline-variant/40 rounded-full"></div>
<a href="#"
class="text-xs font-semibold text-outline uppercase tracking-widest hover:text-primary transition-colors">Terms</a>
</div>
</div>
</div>
<footer
class="absolute bottom-0 w-full py-6 px-8 flex justify-between items-center text-[11px] font-bold text-outline/50 uppercase tracking-widest">
<div class="flex items-center gap-2">
<div class="w-2 h-2 bg-primary rounded-full"></div>
<span>System Status: Operational</span>
</div>
<span>Version 3.0.0-Python</span>
</footer>
</main>
<!-- Dashboard Screen -->
<div id="dashboard-screen" class="screen flex-col pb-32 min-h-screen">
<header
class="bg-surface/90 backdrop-blur-lg top-0 sticky z-50 flex justify-between items-center w-full px-6 py-4">
<div class="flex items-center gap-4">
<button
class="text-on-surface hover:bg-surface-container-high transition-colors p-3 rounded-full min-w-[48px] min-h-[48px] flex items-center justify-center active:scale-95">
<span class="material-symbols-outlined">menu</span>
</button>
<h1 class="text-primary font-extrabold tracking-tighter text-xl">HomeStack</h1>
</div>
<button onclick="showScreen('settings')"
class="text-on-surface hover:bg-surface-container-high transition-colors p-3 rounded-full min-w-[48px] min-h-[48px] flex items-center justify-center active:scale-95">
<span class="material-symbols-outlined">account_circle</span>
</button>
</header>
<main class="px-6 pt-4 max-w-2xl mx-auto space-y-8 w-full">
<section class="space-y-4">
<div>
<h2 class="text-3xl font-extrabold tracking-tight text-on-surface" id="greeting-text">Good Morning.</h2>
<p class="text-on-surface-variant font-medium mt-1">You have <span class="text-primary font-bold"
id="item-count">0 items</span> in inventory.</p>
</div>
<div class="relative group z-50">
<div class="absolute inset-y-0 left-4 flex items-center pointer-events-none text-outline">
<span class="material-symbols-outlined">search</span>
</div>
<input type="text" id="search-input"
class="w-full bg-surface-container-lowest border-2 border-surface-container-high focus:border-primary focus:ring-0 rounded-2xl py-5 pl-12 pr-4 text-lg font-medium placeholder:text-outline-variant transition-all shadow-sm group-focus-within:shadow-md"
placeholder="Search across items..." oninput="handleSearch(this.value)"
onfocus="document.getElementById('search-dropdown').classList.remove('hidden')"
onblur="setTimeout(() => document.getElementById('search-dropdown').classList.add('hidden'), 200)">
<div class="absolute inset-y-0 right-4 flex items-center">
<button
class="bg-surface-container-high p-2 rounded-lg text-on-surface-variant hover:text-primary transition-colors z-10">
<span class="material-symbols-outlined text-[20px]">tune</span>
</button>
</div>
<!-- Autocomplete Dropdown -->
<div id="search-dropdown"
class="absolute top-full left-0 right-0 mt-2 bg-surface-container-lowest rounded-2xl shadow-2xl border border-outline-variant/10 max-h-80 overflow-y-auto hidden editorial-shadow">
</div>
</div>
</section>
<section class="flex gap-4">
<button onclick="openQuickAdd()"
class="flex-grow bg-primary text-on-primary py-6 rounded-2xl font-bold text-lg flex items-center justify-center gap-3 shadow-lg shadow-primary/20 active:scale-[0.98] transition-all min-h-[72px]">
<span class="material-symbols-outlined fill text-[28px]">add_circle</span>
<span>Quick Add</span>
</button>
<button id="mic-btn"
class="w-20 bg-secondary-container text-on-secondary-container rounded-2xl flex items-center justify-center shadow-lg active:scale-[0.98] transition-all relative overflow-hidden">
<span
class="material-symbols-outlined text-[32px] z-10 transition-transform duration-300 pointer-events-none"
id="mic-icon">mic</span>
<div id="mic-pulse"
class="absolute inset-0 bg-primary/30 scale-0 rounded-2xl transition-transform duration-300 origin-center pointer-events-none">
</div>
</button>
</section>
<section class="space-y-4">
<div class="flex justify-between items-center">
<h3 class="text-xl font-bold">Recently Added</h3>
<button onclick="showScreen('inventory', true)"
class="text-primary font-bold text-sm min-h-[44px] px-2 flex items-center">View All</button>
</div>
<div id="recent-items" class="flex gap-4 overflow-x-auto no-scrollbar pb-2 -mx-6 px-6">
<!-- Recent items injected here -->
</div>
</section>
<section class="space-y-6">
<div class="flex justify-between items-end">
<h3 class="text-xl font-bold">Locations</h3>
<button onclick="openAddLocationModal()"
class="text-primary font-bold text-sm min-h-[44px] flex items-center px-3 hover:bg-primary-container/10 rounded-xl transition-colors active:scale-95">Edit
Map</button>
</div>
<div id="locations-grid" class="grid grid-cols-2 gap-4">
<!-- Dynamic locations injected here -->
</div>
</section>
</main>
</div>
<!-- Inventory Screen -->
<div id="inventory-screen" class="screen flex-col pb-32 min-h-screen">
<header
class="bg-surface/80 backdrop-blur-lg top-0 sticky z-50 flex justify-between items-center w-full px-6 py-4">
<div class="flex items-center gap-4">
<button onclick="showScreen('dashboard')"
class="material-symbols-outlined text-primary hover:bg-primary-container/30 transition-colors p-2 rounded-full active:scale-95">arrow_back</button>
<h1 class="text-primary font-headline font-bold text-lg tracking-tight" id="inventory-header-title">All Items</h1>
</div>
<div class="bg-primary-container text-on-primary-container px-3 py-1 rounded-full text-xs font-bold"
id="inv-count-badge">0 Items</div>
</header>
<main class="max-w-md mx-auto px-6 w-full">
<section class="mt-4 mb-6">
<h2 class="text-3xl font-extrabold text-on-surface tracking-tighter">Inventory List</h2>
<p class="text-on-surface-variant mt-1 text-sm">Efficiently manage your items.</p>
</section>
<div id="inventory-list" class="space-y-8">
<!-- Inventory categories and items injected here -->
</div>
</main>
</div>
<!-- AI Assistant Screen -->
<div id="ai-screen" class="screen flex-col h-screen pb-32">
<header class="bg-surface/90 backdrop-blur-lg top-0 sticky z-50 flex items-center w-full px-6 py-4 gap-4">
<button onclick="showScreen('dashboard')" class="text-primary p-2 rounded-full active:scale-95">
<span class="material-symbols-outlined">arrow_back</span>
</button>
<h1 class="text-primary font-extrabold tracking-tighter text-xl">AI Curator</h1>
</header>
<div id="chat-messages" class="flex-grow overflow-y-auto px-6 py-6 space-y-6 no-scrollbar">
<div class="text-center py-20 space-y-6" id="chat-empty">
<div
class="inline-flex items-center justify-center w-20 h-20 rounded-3xl bg-primary-container text-primary editorial-shadow">
<span class="material-symbols-outlined text-4xl">auto_awesome</span>
</div>
<div class="space-y-2">
<h3 class="text-2xl font-headline font-bold">How can I help?</h3>
<p class="text-on-surface-variant max-w-[240px] mx-auto">Ask me about your inventory, stock levels,
or storage optimization.</p>
</div>
</div>
</div>
<div class="p-6 bg-surface/80 backdrop-blur-xl border-t border-outline-variant/10">
<form id="chat-form" class="flex gap-3">
<input type="text" id="chat-input" placeholder="Ask HomeStack..."
class="flex-grow min-h-[3.5rem] px-6 bg-surface-container border-none rounded-2xl focus:ring-2 focus:ring-primary/40 text-on-surface placeholder:text-outline transition-all">
<button type="submit"
class="w-14 h-14 bg-primary text-on-primary rounded-2xl flex items-center justify-center shadow-lg shadow-primary/20 active:scale-90 transition-all">
<span class="material-symbols-outlined text-xl">send</span>
</button>
</form>
</div>
</div>
<!-- Settings Screen -->
<div id="settings-screen" class="screen flex-col pb-32 min-h-screen">
<header class="bg-surface/90 backdrop-blur-lg top-0 sticky z-50 flex items-center w-full px-6 py-4 gap-4">
<button onclick="showScreen('dashboard')" class="text-primary p-2 rounded-full active:scale-95">
<span class="material-symbols-outlined">arrow_back</span>
</button>
<h1 class="text-primary font-extrabold tracking-tighter text-xl">Settings</h1>
</header>
<main class="px-6 py-8 space-y-10 max-w-2xl mx-auto w-full">
<div class="space-y-4">
<h3 class="text-xs font-bold text-outline uppercase tracking-[0.2em] px-2">Vault Configuration</h3>
<div
class="bg-surface-container-lowest rounded-3xl overflow-hidden editorial-shadow border border-outline-variant/10">
<button onclick="openProfileModal()"
class="w-full flex items-center gap-4 p-6 hover:bg-surface-container-low transition-colors text-on-surface">
<span class="material-symbols-outlined text-primary">person</span>
<span class="flex-grow text-left font-bold">Profile Details</span>
<span class="material-symbols-outlined text-outline">chevron_right</span>
</button>
</div>
</div>
<button onclick="logout()"
class="w-full min-h-[4.5rem] bg-error/10 text-error font-bold rounded-2xl flex items-center justify-center gap-3 active:scale-95 transition-all border border-error/20">
<span class="material-symbols-outlined">logout</span>
<span>Lock Vault Session</span>
</button>
<div class="text-center pt-4">
<p class="text-[10px] font-bold text-outline uppercase tracking-widest">HomeStack v3.0.0-Python</p>
<p class="text-[10px] text-outline/60 mt-1">© 2026 Digital Inventory Curator</p>
</div>
</main>
</div>
<!-- Navigation Bar -->
<nav id="bottom-nav"
class="fixed bottom-0 left-0 w-full z-50 flex justify-around items-center px-4 pb-8 pt-4 bg-white/95 backdrop-blur-xl shadow-[0_-8px_24px_rgba(45,51,53,0.1)] rounded-t-[2.5rem] hidden">
<button onclick="showScreen('dashboard')"
class="nav-btn flex flex-col items-center justify-center rounded-2xl px-8 py-3 transition-all active:scale-90 text-outline"
data-screen="dashboard">
<span class="material-symbols-outlined text-[28px]">home</span>
<span class="font-headline font-bold text-[12px] mt-1">Inventory</span>
</button>
<button onclick="showScreen('ai')"
class="nav-btn flex flex-col items-center justify-center rounded-2xl px-8 py-3 transition-all active:scale-90 text-outline"
data-screen="ai">
<span class="material-symbols-outlined text-[28px]">auto_awesome</span>
<span class="font-headline font-bold text-[12px] mt-1">AI Assistant</span>
</button>
<button onclick="showScreen('settings')"
class="nav-btn flex flex-col items-center justify-center rounded-2xl px-8 py-3 transition-all active:scale-90 text-outline"
data-screen="settings">
<span class="material-symbols-outlined text-[28px]">settings</span>
<span class="font-headline font-bold text-[12px] mt-1">Settings</span>
</button>
</nav>
<!-- Quick Add Modal -->
<div id="quick-add-modal"
class="fixed inset-0 z-[100] bg-black/60 backdrop-blur-sm hidden flex-col justify-end transition-all duration-300">
<div class="bg-surface rounded-t-[3rem] p-8 space-y-8 animate-slide-up max-h-[90vh] overflow-y-auto">
<div class="flex justify-between items-center">
<h3 class="text-2xl font-extrabold text-primary">Quick Add Item</h3>
<button onclick="closeQuickAdd()" class="bg-surface-container-high p-2 rounded-full">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<form id="quick-add-form" class="space-y-6">
<div class="space-y-2">
<label class="text-sm font-bold ml-1">Item Name</label>
<input type="text" id="add-name" required
class="w-full bg-surface-container p-4 rounded-xl border-none focus:ring-2 focus:ring-primary/40"
placeholder="e.g. Superglue">
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<label class="text-sm font-bold ml-1">Quantity</label>
<input type="number" id="add-qty" required
class="w-full bg-surface-container p-4 rounded-xl border-none focus:ring-2 focus:ring-primary/40"
placeholder="1">
</div>
<div class="space-y-2">
<label class="text-sm font-bold ml-1">Unit</label>
<input type="text" id="add-unit"
class="w-full bg-surface-container p-4 rounded-xl border-none focus:ring-2 focus:ring-primary/40"
placeholder="UNIT">
</div>
</div>
<button type="submit"
class="w-full bg-primary text-on-primary py-5 rounded-2xl font-extrabold text-lg shadow-lg active:scale-95 transition-all">
Add to Inventory
</button>
</form>
</div>
</div>
<!-- Add Location Modal -->
<div id="location-modal"
class="fixed inset-0 z-[100] bg-black/60 backdrop-blur-sm hidden flex-col justify-end transition-all duration-300">
<div class="bg-surface rounded-t-[3rem] p-8 space-y-8 animate-slide-up max-h-[90vh] overflow-y-auto">
<div class="flex justify-between items-center">
<h3 class="text-2xl font-extrabold text-primary tracking-tight">Map a Storage Place</h3>
<button onclick="closeAddLocationModal()" class="bg-surface-container-high p-2 rounded-full">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<form id="location-form" class="space-y-6">
<div class="space-y-2">
<label class="text-sm font-bold ml-1">Place Name</label>
<input type="text" id="loc-name" required
class="w-full bg-surface-container p-4 rounded-xl border-none focus:ring-2 focus:ring-primary/40"
placeholder="e.g. Master Bedroom Closet">
</div>
<button type="submit"
class="w-full bg-primary text-on-primary py-5 rounded-2xl font-extrabold text-lg shadow-lg active:scale-95 transition-all">
Establish Connection
</button>
</form>
</div>
</div>
<!-- Profile Modal -->
<div id="profile-modal"
class="fixed inset-0 z-[100] bg-black/60 backdrop-blur-sm hidden flex-col items-center justify-center p-6 transition-all duration-300">
<div class="bg-surface w-full max-w-sm rounded-[2.5rem] p-8 space-y-6 text-center animate-scale-up">
<div
class="w-24 h-24 bg-primary-container text-primary rounded-full mx-auto flex items-center justify-center">
<span class="material-symbols-outlined text-5xl">person_pin</span>
</div>
<div class="space-y-1">
<h3 class="text-2xl font-extrabold tracking-tight">Paarth's HomeStack</h3>
<p class="text-on-surface-variant font-medium">Vault Admin</p>
</div>
<div class="grid grid-cols-2 gap-3 py-4">
<div class="bg-surface-container-low p-4 rounded-2xl">
<span class="block text-primary font-extrabold text-xl" id="profile-items">0</span>
<span class="text-[10px] font-bold text-outline uppercase">Total Items</span>
</div>
<div class="bg-surface-container-low p-4 rounded-2xl">
<span class="block text-primary font-extrabold text-xl" id="profile-locs">0</span>
<span class="text-[10px] font-bold text-outline uppercase">Locations</span>
</div>
</div>
<button onclick="closeProfileModal()"
class="w-full bg-surface-container-high py-4 rounded-2xl font-bold active:scale-95 transition-all">
Close Profile
</button>
</div>
</div>
<style>
@keyframes slide-up {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
@keyframes scale-up {
from {
transform: scale(0.9);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.animate-slide-up {
animation: slide-up 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.animate-scale-up {
animation: scale-up 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
</style>
<script>
let inventory = [];
let chatMessages = [];
let locationsData = [];
let containersData = [];
let sessionToken = null;
let currentLocationFilter = null; // { id, name } or null for all items
// Set time-based greeting
function setGreeting() {
const hour = new Date().getHours();
const greetingEl = document.getElementById('greeting-text');
if (!greetingEl) return;
if (hour < 12) greetingEl.textContent = 'Good Morning.';
else if (hour < 17) greetingEl.textContent = 'Good Afternoon.';
else greetingEl.textContent = 'Good Evening.';
}
setGreeting();
// Check for stored session on load
function checkPersistedSession() {
const storedToken = localStorage.getItem('vault_token');
const expiry = localStorage.getItem('vault_token_expiry');
if (storedToken && expiry) {
if (Date.now() < parseInt(expiry)) {
sessionToken = storedToken;
showScreen('dashboard');
return true;
} else {
// Session expired
localStorage.removeItem('vault_token');
localStorage.removeItem('vault_token_expiry');
}
}
return false;
}
// Run check on startup
window.addEventListener('DOMContentLoaded', () => {
if (!checkPersistedSession()) {
showScreen('login');
}
});
function getHeaders() {
const headers = { 'Content-Type': 'application/json' };
if (sessionToken) {
headers['Authorization'] = `Bearer ${sessionToken}`;
}
return headers;
}
function showScreen(screenId, resetLocationFilter = false) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(screenId + '-screen').classList.add('active');
const nav = document.getElementById('bottom-nav');
if (screenId === 'login') {
nav.classList.add('hidden');
// Ensure form is visible if coming from a different screen
document.getElementById('login-screen').classList.add('active');
} else {
nav.classList.remove('hidden');
updateNav(screenId);
if (resetLocationFilter) {
currentLocationFilter = null;
const headerTitle = document.getElementById('inventory-header-title');
if (headerTitle) headerTitle.textContent = 'All Items';
}
if (inventory.length === 0 || containersData.length === 0) {
fetchAll();
} else if (screenId === 'inventory') {
renderInventory();
}
}
}
async function fetchAll() {
await Promise.all([fetchInventory(), fetchLocations(), fetchContainers()]);
}
function updateNav(screenId) {
document.querySelectorAll('.nav-btn').forEach(btn => {
if (btn.dataset.screen === screenId) {
btn.classList.add('bg-primary-container', 'text-on-primary-container');
btn.classList.remove('text-outline');
btn.querySelector('.material-symbols-outlined').classList.add('fill');
} else {
btn.classList.remove('bg-primary-container', 'text-on-primary-container');
btn.classList.add('text-outline');
btn.querySelector('.material-symbols-outlined').classList.remove('fill');
}
});
}
async function fetchInventory() {
try {
const res = await fetch('/api/inventory', {
headers: getHeaders()
});
inventory = await res.json();
renderDashboard();
renderInventory();
renderLocations();
} catch (err) {
console.error("Failed to fetch inventory", err);
}
}
function renderDashboard() {
document.getElementById('item-count').innerText = `${inventory.length} items`;
const recentContainer = document.getElementById('recent-items');
// Show last 8 items as simple badges
recentContainer.innerHTML = inventory.slice(0, 8).map(item => `
<div class="flex-none bg-surface-container-low p-4 rounded-2xl min-w-[140px] border border-outline-variant/10">
<p class="font-bold text-sm truncate">${item.name}</p>
<div class="mt-3 flex items-center justify-between">
<span class="text-xs font-bold text-outline">${item.quantity} ${item.unit || 'UNIT'}</span>
<span class="material-symbols-outlined text-outline/30 text-sm">inventory_2</span>
</div>
</div>
`).join('');
}
async function fetchLocations() {
try {
const res = await fetch('/api/locations', {
headers: getHeaders()
});
locationsData = await res.json();
renderLocations();
} catch (err) {
console.error("Failed to fetch locations", err);
}
}
async function fetchContainers() {
try {
const res = await fetch('/api/containers', {
headers: getHeaders()
});
const data = await res.json();
containersData = Array.isArray(data) ? data : [];
} catch (err) {
console.error("Failed to fetch containers", err);
}
}
function renderLocations() {
const container = document.getElementById('locations-grid');
if (!container) return;
if (!locationsData || locationsData.length === 0) {
container.innerHTML = `<p class="text-sm text-on-surface-variant col-span-2">No locations created yet.</p>`;
return;
}
container.innerHTML = locationsData.map((loc, idx) => {
const locContainers = containersData.filter(c => c.location_id === loc.id);
const locContainerIds = locContainers.map(c => c.id);
const locItems = inventory.filter(i => i.location_id === loc.id || locContainerIds.includes(i.container_id));
// Use a rotating set of soft gradients for variety
const gradients = [
'from-primary/20 to-primary/5',
'from-secondary/20 to-secondary/5',
'from-tertiary/20 to-tertiary/5',
'from-error/10 to-error/5'
];
const gradient = gradients[idx % gradients.length];
return `
<div onclick="showLocationInventory('${loc.name}', '${loc.id}')" class="col-span-2 relative p-6 rounded-3xl overflow-hidden group cursor-pointer bg-surface-container-low border border-outline-variant/10 active:scale-[0.98] transition-all">
<div class="absolute inset-0 bg-gradient-to-br ${gradient} opacity-50"></div>
<div class="relative z-10 flex justify-between items-start">
<div class="space-y-1">
<h4 class="text-xl font-black text-on-surface tracking-tight">${loc.name}</h4>
<p class="text-xs font-bold text-outline uppercase tracking-widest">${locItems.length} items • ${locContainers.length} boxes</p>
</div>
<div class="w-12 h-12 rounded-2xl bg-white/50 backdrop-blur-sm flex items-center justify-center text-primary group-hover:scale-110 transition-transform">
<span class="material-symbols-outlined text-2xl">home_storage</span>
</div>
</div>
</div>
`;
}).join('');
}
function renderInventory() {
const listContainer = document.getElementById('inventory-list');
if (!listContainer) return;
// 1. Determine which items to show based on currentLocationFilter
let filteredInventory;
if (currentLocationFilter) {
const locContainers = containersData.filter(c => c.location_id === currentLocationFilter.id);
const locContainerIds = locContainers.map(c => c.id);
filteredInventory = inventory.filter(i =>
i.location_id === currentLocationFilter.id ||
locContainerIds.includes(i.container_id)
);
} else {
filteredInventory = inventory;
}
document.getElementById('inv-count-badge').innerText = `${filteredInventory.length} Items`;
if (filteredInventory.length === 0) {
listContainer.innerHTML = `
<div class="py-20 text-center space-y-4">
<span class="material-symbols-outlined text-outline/20 text-6xl">inventory_2</span>
<p class="text-on-surface-variant font-medium">No items in this location yet.</p>
</div>`;
return;
}
// 2. Group by Container (Box)
const uniqueContainerIds = [...new Set(filteredInventory.map(i => i.container_id))];
listContainer.innerHTML = uniqueContainerIds.map(contId => {
const container = containersData.find(c => c.id === contId);
const contItems = filteredInventory.filter(i => i.container_id === contId);
// Location name for this container
const containerLocName = container
? (locationsData.find(l => l.id === container.location_id)?.name || '')
: (currentLocationFilter ? currentLocationFilter.name : '');
return `
<section class="space-y-6 mb-10 pt-2 group">
<!-- Container/Box Header -->
<div class="flex items-center gap-4 px-2 border-b border-outline-variant/30 pb-3 bg-surface sticky top-[84px] z-20">
<div class="w-12 h-12 rounded-2xl bg-primary/10 flex items-center justify-center transition-transform group-hover:scale-105">
<span class="material-symbols-outlined text-primary text-[24px]">
${contId ? 'package_2' : 'home_storage'}
</span>
</div>
<div class="flex-grow">
<h3 class="text-lg font-black text-on-surface uppercase tracking-tight leading-none mb-1">
${contId ? (container ? container.name : 'Unknown Box') : 'General Storage'}
</h3>
<p class="text-[10px] font-black text-outline uppercase tracking-widest flex items-center gap-2">
<span>${contItems.length} ITEMS</span>
${containerLocName ? `<span class="w-1 h-1 rounded-full bg-outline/30"></span><span>${containerLocName}</span>` : ''}
</p>
</div>
</div>
<div class="grid gap-3 pl-4 border-l-2 border-primary/5 ml-6">
${contItems.map(item => `
<div class="p-5 rounded-3xl flex items-center justify-between bg-surface-container-lowest shadow-sm active:scale-[0.98] transition-all border border-outline-variant/10 hover:border-primary/20 hover:shadow-md">
<div class="min-w-0 pr-4">
<h4 class="font-extrabold text-on-surface text-lg leading-tight truncate">${item.name}</h4>
</div>
<div class="flex items-center gap-6">
<div class="text-right flex-shrink-0">
<span class="block text-2xl font-black text-primary tabular-nums leading-none">
${item.quantity}
</span>
<span class="text-[10px] uppercase font-black text-outline tracking-widest">
${item.unit || 'UNIT'}
</span>
</div>
<button onclick="deleteItem('${item.id}', event)" class="w-12 h-12 rounded-2xl flex items-center justify-center text-outline/20 hover:text-error hover:bg-error/10 transition-all">
<span class="material-symbols-outlined text-xl">delete</span>
</button>
</div>
</div>
`).join('')}
</div>
</section>
`;
}).join('');
}
// Auth Logic
document.getElementById('login-form').addEventListener('submit', async (e) => {
e.preventDefault();
const password = document.getElementById('password-input').value;
const btn = document.getElementById('login-btn');
const btnText = document.getElementById('login-btn-text');
const errorEl = document.getElementById('login-error');
btn.disabled = true;
btnText.innerText = 'Unlocking...';
errorEl.classList.add('hidden');
try {
const res = await fetch('/api/auth/unlock', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password })
});
const data = await res.json();
if (data.success) {
sessionToken = data.token;
// Handle Remember Me
const rememberMe = document.getElementById('remember-me').checked;
if (rememberMe) {
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
localStorage.setItem('vault_token', sessionToken);
localStorage.setItem('vault_token_expiry', (Date.now() + thirtyDays).toString());
}
showScreen('dashboard');
} else {
errorEl.innerText = data.detail || "Invalid Access Key";
errorEl.classList.remove('hidden');
}
} catch (err) {
errorEl.innerText = "Connection error";
errorEl.classList.remove('hidden');
} finally {
btn.disabled = false;
btnText.innerText = 'Unlock Vault';
}
});
// Chat Logic
document.getElementById('chat-form').addEventListener('submit', async (e) => {
e.preventDefault();
const input = document.getElementById('chat-input');
const content = input.value.trim();
if (!content) return;
addMessage('user', content);
input.value = '';
const typingId = addTypingIndicator();
try {
const res = await fetch('/api/ai/chat', {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify({ messages: chatMessages })
});
const data = await res.json();
console.log("AI Assistant Response:", data);
removeTypingIndicator(typingId);
if (data.choices && data.choices[0]) {
const content = data.choices[0].message.content;
if (!content) {
addMessage('assistant', 'I am processing your complex request. Please give me a moment or check the system logs.');
} else {
try {
const parsed = JSON.parse(content);
addMessage('assistant', parsed.reply || content);
} catch (e) {
addMessage('assistant', content);
}
}
// Refresh all data in case AI mutated inventory/containers/locations
fetchAll();
}
} catch (err) {
removeTypingIndicator(typingId);
addMessage('assistant', "Sorry, I encountered an error.");
}
});
function addMessage(role, content) {
chatMessages.push({ role, content });
const container = document.getElementById('chat-messages');
const empty = document.getElementById('chat-empty');
if (empty) empty.remove();
const msgDiv = document.createElement('div');
msgDiv.className = `flex flex-col ${role === 'user' ? 'items-end' : 'items-start'}`;
// Use marked for assistant messages
const renderedContent = role === 'assistant' ? marked.parse(content) : `<p>${content}</p>`;
msgDiv.innerHTML = `
<div class="max-w-[85%] p-5 rounded-2xl shadow-sm ${role === 'user' ? 'bg-primary text-on-primary rounded-tr-none' : 'bg-surface-container-low text-on-surface rounded-tl-none border border-outline-variant/10'}">
<div class="text-[15px] leading-relaxed prose prose-sm ${role === 'user' ? 'prose-invert' : ''}">${renderedContent}</div>
</div>
`;
container.appendChild(msgDiv);
container.scrollTop = container.scrollHeight;
}
function addTypingIndicator() {
const container = document.getElementById('chat-messages');
const id = 'typing-' + Date.now();
const div = document.createElement('div');
div.id = id;
div.className = "flex items-start";
div.innerHTML = `
<div class="bg-surface-container-low p-5 rounded-2xl rounded-tl-none border border-outline-variant/10">
<div class="flex gap-1.5">
<div class="w-2 h-2 bg-primary/40 rounded-full animate-bounce"></div>
<div class="w-2 h-2 bg-primary/40 rounded-full animate-bounce [animation-delay:0.2s]"></div>
<div class="w-2 h-2 bg-primary/40 rounded-full animate-bounce [animation-delay:0.4s]"></div>
</div>
</div>
`;
container.appendChild(div);
container.scrollTop = container.scrollHeight;
return id;
}
function showLocationInventory(locName, locId) {
currentLocationFilter = { id: locId, name: locName };
const headerTitle = document.getElementById('inventory-header-title');
if (headerTitle) headerTitle.textContent = locName;
showScreen('inventory');
renderInventory();
}
function removeTypingIndicator(id) {
const el = document.getElementById(id);
if (el) el.remove();
}
function logout() {
// Clear persistent storage
localStorage.removeItem('vault_token');
localStorage.removeItem('vault_token_expiry');
sessionToken = null;
showScreen('login');
document.getElementById('password-input').value = '';
chatMessages = [];
document.getElementById('chat-messages').innerHTML = `
<div class="text-center py-20 space-y-6" id="chat-empty">
<div class="inline-flex items-center justify-center w-20 h-20 rounded-3xl bg-primary-container text-primary editorial-shadow">
<span class="material-symbols-outlined text-4xl">auto_awesome</span>
</div>
<div class="space-y-2">
<h3 class="text-2xl font-headline font-bold">How can I help?</h3>
<p class="text-on-surface-variant max-w-[240px] mx-auto">Ask me about your inventory, stock levels, or storage optimization.</p>
</div>
</div>
`;
}
function openQuickAdd() {
document.getElementById('quick-add-modal').classList.remove('hidden');
document.getElementById('quick-add-modal').classList.add('flex');
}
function closeQuickAdd() {
document.getElementById('quick-add-modal').classList.add('hidden');
document.getElementById('quick-add-modal').classList.remove('flex');
}
async function deleteItem(id, event) {
event.stopPropagation();
if (!confirm("Delete this item?")) return;
try {
const res = await fetch(`/api/inventory/${id}`, {
method: 'DELETE',
headers: getHeaders()
});
if (res.ok) {
inventory = inventory.filter(i => i.id !== id);
renderDashboard();
renderInventory();
}
} catch (err) {
console.error("Delete failed", err);
}
}
function handleSearch(val) {
const dropdown = document.getElementById('search-dropdown');
if (val.trim() === '') {
dropdown.innerHTML = '';
dropdown.classList.add('hidden');
return;
}
dropdown.classList.remove('hidden');
const fuse = new Fuse(inventory, {
keys: [
{ name: 'name', weight: 1.0 }
],
threshold: 0.35
});
const results = fuse.search(val).map(r => r.item).slice(0, 10);
if (results.length === 0) {
dropdown.innerHTML = `
<div class="p-6 text-center text-on-surface-variant">
<span class="material-symbols-outlined block text-3xl opacity-50 mb-2">search_off</span>
<p class="text-sm font-medium">No items found</p>
</div>
`;
return;
}
dropdown.innerHTML = results.map(item => {
const container = containersData.find(c => c.id === item.container_id);
const loc = locationsData.find(l => l.id === (container ? container.location_id : item.location_id));
const locName = loc ? loc.name : 'Unknown Location';
const contName = container ? container.name : 'Unboxed';
return `
<div onclick="showLocationInventory('${locName}')" class="p-5 hover:bg-surface-container-low cursor-pointer border-b border-outline-variant/5 last:border-0 transition-colors flex items-center gap-5">
<div class="w-12 h-12 rounded-2xl bg-primary/5 flex items-center justify-center text-primary/40 flex-shrink-0">
<span class="material-symbols-outlined">inventory_2</span>
</div>
<div class="flex-grow min-w-0">
<h4 class="font-bold text-on-surface truncate leading-tight mb-1">${item.name}</h4>
<div class="flex items-center gap-1.5 text-[10px] font-black uppercase text-outline tracking-widest">
<span class="truncate">${locName}</span>
<span class="text-outline-variant/60">•</span>
<span class="truncate text-primary/70">${contName}</span>
</div>
</div>
<div class="text-right flex-shrink-0 pl-2">
<span class="block font-black text-lg text-primary leading-none">${item.quantity}</span>
<span class="text-[9px] font-bold text-outline-variant uppercase tracking-widest">${item.unit || 'UNIT'}</span>
</div>
</div>
`;
}).join('');
}
document.getElementById('quick-add-form').addEventListener('submit', async (e) => {
e.preventDefault();
const item = {
name: document.getElementById('add-name').value,
quantity: parseFloat(document.getElementById('add-qty').value),
unit: document.getElementById('add-unit').value || "UNIT"
};
try {
const res = await fetch('/api/inventory', {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify(item)
});
if (res.ok) {
await fetchInventory(); // Refresh data
closeQuickAdd();
document.getElementById('quick-add-form').reset();
}
} catch (err) {
console.error("Add failed", err);
}
});
function openAddLocationModal() {
document.getElementById('location-modal').classList.remove('hidden');
document.getElementById('location-modal').classList.add('flex');
}
function closeAddLocationModal() {
document.getElementById('location-modal').classList.add('hidden');
document.getElementById('location-modal').classList.remove('flex');
}
function openProfileModal() {
document.getElementById('profile-items').innerText = inventory.length;
document.getElementById('profile-locs').innerText = locationsData.length;
document.getElementById('profile-modal').classList.remove('hidden');
document.getElementById('profile-modal').classList.add('flex');
}
function closeProfileModal() {
document.getElementById('profile-modal').classList.add('hidden');
document.getElementById('profile-modal').classList.remove('flex');
}
document.getElementById('location-form').addEventListener('submit', async (e) => {
e.preventDefault();
const location = {
name: document.getElementById('loc-name').value
};
try {
const res = await fetch('/api/locations', {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify(location)
});
if (res.ok) {
await fetchLocations(); // Refresh data
closeAddLocationModal();
document.getElementById('location-form').reset();
}
} catch (err) {
console.error("Add location failed", err);
}
});
// Voice Add / Mic Logic
let mediaRecorder;
let audioChunks = [];
let isRecording = false;
const micBtn = document.getElementById('mic-btn');
const micIcon = document.getElementById('mic-icon');
const micPulse = document.getElementById('mic-pulse');
micBtn.addEventListener('click', async () => {
console.log("Mic button clicked! isRecording:", isRecording);
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
alert("Microphone is blocked or not supported! Ensure you are using HTTPS or localhost.");
return;
}
if (!isRecording) {
// UI changes indicating we are asking for permission
micPulse.classList.replace('scale-0', 'scale-150');
micPulse.classList.add('animate-pulse', 'bg-outline/20');
micIcon.innerText = 'pending';
// Start Recording
try {
// Timeout getUserMedia so it doesn't hang forever if the user misses the hidden browser prompt
const stream = await Promise.race([
navigator.mediaDevices.getUserMedia({ audio: true }),
new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout_Permission")), 5000))
]);
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];
mediaRecorder.ondataavailable = e => {
if (e.data.size > 0) audioChunks.push(e.data);
};
mediaRecorder.onstop = async () => {
// Create Blob
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
const formData = new FormData();
// We give it an extension so the backend knows what to do
formData.append('file', audioBlob, 'voice_note.webm');
// Visual feedback that we're transcribing
micIcon.innerText = 'hourglass_empty';
micIcon.classList.add('animate-spin');
// Stop pulsing since we are transcribing
micPulse.classList.replace('scale-150', 'scale-0');
micPulse.classList.remove('animate-pulse', 'bg-error/20');
try {
const res = await fetch('/api/audio/transcribe', {
method: 'POST',
headers: sessionToken ? { 'Authorization': `Bearer ${sessionToken}` } : {},
body: formData
});
if (res.ok) {
const data = await res.json();
if (data.text) {
// Successfully transcribed. Swap to AI screen and submit transcript
showScreen('ai');
document.getElementById('chat-input').value = data.text;
// Trigger submit event on the chat form to process the voice command automatically
document.getElementById('chat-form').dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
} else {
alert("Transcription returned no text.");
}
} else {
alert("Transcription API failed with status: " + res.status);
}
} catch (err) {
console.error("Transcription failed", err);
alert("Transcription failed: Failed to reach the server.");
} finally {
micIcon.classList.remove('animate-spin');
micIcon.innerText = 'mic';
micBtn.classList.replace('bg-error/20', 'bg-secondary-container');
micBtn.classList.replace('text-error', 'text-on-secondary-container');
}
};
mediaRecorder.start();
isRecording = true;
// Full recording UI changes
micPulse.classList.remove('bg-outline/20');
micPulse.classList.add('bg-error/20');
micBtn.classList.replace('bg-secondary-container', 'bg-error/20');
micBtn.classList.replace('text-on-secondary-container', 'text-error');
micIcon.innerText = 'stop';
} catch (err) {
console.error("Mic access denied or timed out", err);
if (err.message === "Timeout_Permission") {
alert("The microphone request timed out. Your browser is hiding the permission prompt in the address bar. Please click the lock/settings icon next to the URL and allow microphone access.");
} else {
alert("Please allow microphone access in your browser's address bar to use voice commands.");
}
// Revert UI changes
micPulse.classList.replace('scale-150', 'scale-0');
micPulse.classList.remove('animate-pulse', 'bg-outline/20');
micIcon.innerText = 'mic';
}
} else {
// Stop Recording
mediaRecorder.stop();
mediaRecorder.stream.getTracks().forEach(track => track.stop());
isRecording = false;
// Revert UI changes
micBtn.classList.replace('bg-error/20', 'bg-secondary-container');
micBtn.classList.replace('text-error', 'text-on-secondary-container');
micPulse.classList.replace('scale-150', 'scale-0');
micPulse.classList.remove('animate-pulse');
}
});
</script>
</body>
</html>