feat(routes): implement / for current stock overview and /manage for tablet station with auto-return
Build and Push Fridger Container / Build & Push Fridger Image (push) Successful in 1m21s
Build and Push Fridger Container / Build & Push Fridger Image (push) Successful in 1m21s
This commit is contained in:
+194
-163
@@ -1,11 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* Fridger Frontend Application
|
* Fridger Frontend Application
|
||||||
* Tablet-optimized Fridge & Freezer Inventory Manager (No expiration dates)
|
* Route / : Current Stock Overview
|
||||||
* Main -> Scan -> Manage (Scan/Search -> Add/Remove -> Packet/Amount)
|
* Route /manage : Tablet Scanner & Manager Station
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: 'main', // 'main' or 'manage'
|
currentRoute: '/', // '/' or '/manage'
|
||||||
items: [],
|
items: [],
|
||||||
meta: {
|
meta: {
|
||||||
categories: [],
|
categories: [],
|
||||||
@@ -28,11 +28,19 @@ const state = {
|
|||||||
isScanning: false
|
isScanning: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize Application
|
// Initialize Application & Router
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
await loadMetadata();
|
await loadMetadata();
|
||||||
await loadStats();
|
await loadStats();
|
||||||
await loadItems();
|
await loadItems();
|
||||||
|
|
||||||
|
// Handle initial browser route
|
||||||
|
handleInitialRoute();
|
||||||
|
|
||||||
|
window.addEventListener('popstate', () => {
|
||||||
|
handleInitialRoute();
|
||||||
|
});
|
||||||
|
|
||||||
initLucide();
|
initLucide();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -42,6 +50,56 @@ function initLucide() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// Client-Side Router
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
function handleInitialRoute() {
|
||||||
|
const path = window.location.pathname;
|
||||||
|
if (path === '/manage' || path.startsWith('/manage')) {
|
||||||
|
setRoute('/manage');
|
||||||
|
} else {
|
||||||
|
setRoute('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateTo(path) {
|
||||||
|
window.history.pushState({}, '', path);
|
||||||
|
setRoute(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRoute(path) {
|
||||||
|
state.currentRoute = path === '/manage' ? '/manage' : '/';
|
||||||
|
|
||||||
|
const stockBtn = document.getElementById('nav-stock-btn');
|
||||||
|
const manageBtn = document.getElementById('nav-manage-btn');
|
||||||
|
const stockPage = document.getElementById('stock-view-page');
|
||||||
|
const managePage = document.getElementById('manage-view-page');
|
||||||
|
const subTitle = document.getElementById('page-subtitle');
|
||||||
|
|
||||||
|
const activeClass = 'flex items-center justify-center gap-2 px-3.5 sm:px-6 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all bg-teal-500 text-white shadow-md active:scale-95 touch-manipulation min-h-[44px]';
|
||||||
|
const inactiveClass = 'flex items-center justify-center gap-2 px-3.5 sm:px-6 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all text-slate-300 hover:text-white hover:bg-slate-800/80 active:scale-95 touch-manipulation min-h-[44px]';
|
||||||
|
|
||||||
|
if (state.currentRoute === '/') {
|
||||||
|
stockBtn.className = activeClass;
|
||||||
|
manageBtn.className = inactiveClass;
|
||||||
|
stockPage.classList.remove('hidden');
|
||||||
|
managePage.classList.add('hidden');
|
||||||
|
subTitle.textContent = 'Current Stock';
|
||||||
|
renderStockView();
|
||||||
|
} else {
|
||||||
|
manageBtn.className = activeClass;
|
||||||
|
stockBtn.className = inactiveClass;
|
||||||
|
managePage.classList.remove('hidden');
|
||||||
|
stockPage.classList.add('hidden');
|
||||||
|
subTitle.textContent = 'Tablet Station';
|
||||||
|
resetTabletManageStation();
|
||||||
|
renderTabletProductPicker();
|
||||||
|
}
|
||||||
|
|
||||||
|
initLucide();
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
// API Calls
|
// API Calls
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
@@ -84,49 +142,20 @@ async function loadItems() {
|
|||||||
const res = await fetch(`/api/items?${params.toString()}`);
|
const res = await fetch(`/api/items?${params.toString()}`);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
state.items = await res.json();
|
state.items = await res.json();
|
||||||
renderApp();
|
if (state.currentRoute === '/') {
|
||||||
renderManageQuickPicker();
|
renderStockView();
|
||||||
|
} else {
|
||||||
|
renderTabletProductPicker();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load items:', err);
|
console.error('Failed to load inventory:', err);
|
||||||
showToast('Failed to load inventory', 'error');
|
showToast('Failed to load inventory', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
// Navigation Tab Switching (Main -> Scan -> Manage)
|
// ROUTE 1: / (CURRENT STOCK DISPLAY)
|
||||||
// ----------------------------------------------------
|
|
||||||
|
|
||||||
function switchTab(tab) {
|
|
||||||
state.currentTab = tab;
|
|
||||||
|
|
||||||
const mainBtn = document.getElementById('nav-main-btn');
|
|
||||||
const manageBtn = document.getElementById('nav-manage-btn');
|
|
||||||
const mainView = document.getElementById('main-tab-view');
|
|
||||||
const manageView = document.getElementById('manage-tab-view');
|
|
||||||
|
|
||||||
const activeClasses = 'flex items-center justify-center gap-2 px-4 sm:px-7 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all bg-teal-500 text-white shadow-md active:scale-95 touch-manipulation min-h-[44px]';
|
|
||||||
const inactiveClasses = 'flex items-center justify-center gap-2 px-4 sm:px-7 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all text-slate-300 hover:text-white hover:bg-slate-800/80 active:scale-95 touch-manipulation min-h-[44px]';
|
|
||||||
|
|
||||||
if (tab === 'main') {
|
|
||||||
mainBtn.className = activeClasses;
|
|
||||||
manageBtn.className = inactiveClasses;
|
|
||||||
mainView.classList.remove('hidden');
|
|
||||||
manageView.classList.add('hidden');
|
|
||||||
renderApp();
|
|
||||||
} else if (tab === 'manage') {
|
|
||||||
manageBtn.className = activeClasses;
|
|
||||||
mainBtn.className = inactiveClasses;
|
|
||||||
manageView.classList.remove('hidden');
|
|
||||||
mainView.classList.add('hidden');
|
|
||||||
renderManageQuickPicker();
|
|
||||||
}
|
|
||||||
|
|
||||||
initLucide();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------
|
|
||||||
// Filters & Search (Main Tab)
|
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
|
|
||||||
let searchDebounceTimer = null;
|
let searchDebounceTimer = null;
|
||||||
@@ -200,22 +229,10 @@ function renderCategoryFilters() {
|
|||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateModalSelects() {
|
function renderStockView() {
|
||||||
const catSelect = document.getElementById('modal-category');
|
const container = document.getElementById('stock-grid');
|
||||||
if (catSelect && state.meta.categories) {
|
const emptyState = document.getElementById('empty-stock');
|
||||||
catSelect.innerHTML = state.meta.categories.map(c => `
|
if (!container) return;
|
||||||
<option value="${escapeHtml(c.id)}">${escapeHtml(c.name)}</option>
|
|
||||||
`).join('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------
|
|
||||||
// UI Rendering (Main Tab: Fridge / Freezer Cards)
|
|
||||||
// ----------------------------------------------------
|
|
||||||
|
|
||||||
function renderApp() {
|
|
||||||
const emptyState = document.getElementById('empty-state');
|
|
||||||
const container = document.getElementById('fridge-view-content');
|
|
||||||
|
|
||||||
if (state.items.length === 0) {
|
if (state.items.length === 0) {
|
||||||
emptyState.classList.remove('hidden');
|
emptyState.classList.remove('hidden');
|
||||||
@@ -236,16 +253,16 @@ function renderApp() {
|
|||||||
else if (percent <= 50) progressBarColor = 'bg-amber-500';
|
else if (percent <= 50) progressBarColor = 'bg-amber-500';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="shelf-card rounded-2xl sm:rounded-3xl p-4 sm:p-5 flex flex-col justify-between transition hover:border-teal-500/40 space-y-3.5">
|
<div class="shelf-card rounded-2xl sm:rounded-3xl p-4 sm:p-5 flex flex-col justify-between space-y-3.5">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<!-- Header: Brand & Status Pill -->
|
<!-- Header: Brand & Status -->
|
||||||
<div class="flex items-center justify-between gap-2 mb-2">
|
<div class="flex items-center justify-between gap-2 mb-2">
|
||||||
<span class="text-xs font-bold text-teal-400 bg-teal-500/10 px-2.5 py-1 rounded-lg truncate max-w-[140px]">
|
<span class="text-xs font-bold text-teal-400 bg-teal-500/10 px-2.5 py-1 rounded-lg truncate max-w-[140px]">
|
||||||
${escapeHtml(item.brand || item.category)}
|
${escapeHtml(item.brand || item.category)}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-xs font-bold px-2.5 py-1 rounded-lg flex items-center gap-1.5 shrink-0 ${isOpened ? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/30' : 'bg-emerald-500/15 text-emerald-300 border border-emerald-500/20'}">
|
<span class="text-xs font-bold px-2.5 py-1 rounded-lg flex items-center gap-1.5 shrink-0 ${isOpened ? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/30' : 'bg-emerald-500/15 text-emerald-300 border border-emerald-500/20'}">
|
||||||
<span>${isOpened ? 'Opened / In Use' : 'Full Pack'}</span>
|
<span>${isOpened ? 'Opened' : 'Full Pack'}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -265,7 +282,7 @@ function renderApp() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Grams / Quantity Progress -->
|
<!-- Stock & Grams Indicator -->
|
||||||
<div class="mt-3 bg-slate-950/80 rounded-2xl p-3 border border-slate-800/80">
|
<div class="mt-3 bg-slate-950/80 rounded-2xl p-3 border border-slate-800/80">
|
||||||
<div class="flex items-center justify-between text-xs sm:text-sm mb-1.5">
|
<div class="flex items-center justify-between text-xs sm:text-sm mb-1.5">
|
||||||
<span class="font-bold text-white flex items-center gap-1">
|
<span class="font-bold text-white flex items-center gap-1">
|
||||||
@@ -282,12 +299,6 @@ function renderApp() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Touch Manage Button -->
|
|
||||||
<button onclick="goToManageItem(${item.id})" class="w-full flex items-center justify-center gap-2 py-3 px-3 rounded-2xl bg-teal-500/15 hover:bg-teal-500/25 text-teal-300 border border-teal-500/40 text-xs sm:text-sm font-bold transition active:scale-95 touch-manipulation min-h-[44px]" title="Manage this item">
|
|
||||||
<i data-lucide="sliders-horizontal" class="w-4 h-4"></i>
|
|
||||||
<span>Manage Item</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
@@ -296,11 +307,31 @@ function renderApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
// MANAGE TAB: 3-Step Flow (Scan/Search -> Add/Remove -> Packet/Amount)
|
// ROUTE 2: /manage (TABLET STATION WORKFLOW)
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
|
|
||||||
function renderManageQuickPicker(filterQuery = '') {
|
function resetTabletManageStation() {
|
||||||
const container = document.getElementById('manage-quick-picker');
|
state.manage.selectedItem = null;
|
||||||
|
state.manage.action = null;
|
||||||
|
document.getElementById('tablet-step-select').classList.remove('hidden');
|
||||||
|
document.getElementById('tablet-step-action').classList.add('hidden');
|
||||||
|
document.getElementById('tablet-search-input').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTabletSearch(val) {
|
||||||
|
renderTabletProductPicker(val);
|
||||||
|
|
||||||
|
const clean = val.trim();
|
||||||
|
if (/^\d{8,14}$/.test(clean)) {
|
||||||
|
const found = state.items.find(i => i.barcode === clean);
|
||||||
|
if (found) {
|
||||||
|
forwardToProductAction(found);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTabletProductPicker(filterQuery = '') {
|
||||||
|
const container = document.getElementById('tablet-product-picker');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
const query = filterQuery.toLowerCase().trim();
|
const query = filterQuery.toLowerCase().trim();
|
||||||
@@ -314,7 +345,7 @@ function renderManageQuickPicker(filterQuery = '') {
|
|||||||
if (matched.length === 0) {
|
if (matched.length === 0) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="col-span-full py-4 text-center text-xs text-slate-500">
|
<div class="col-span-full py-4 text-center text-xs text-slate-500">
|
||||||
No matching items found. Scan barcode or add as new product!
|
No matching product found. Scan barcode or tap "Add Manually"!
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
@@ -322,8 +353,8 @@ function renderManageQuickPicker(filterQuery = '') {
|
|||||||
|
|
||||||
container.innerHTML = matched.map(item => `
|
container.innerHTML = matched.map(item => `
|
||||||
<button
|
<button
|
||||||
onclick="selectManageItemById(${item.id})"
|
onclick="forwardToProductActionById(${item.id})"
|
||||||
class="p-3 rounded-2xl bg-slate-950 hover:bg-slate-800/80 border border-slate-800/90 text-left flex items-center gap-3 transition active:scale-95 touch-manipulation min-h-[50px] ${state.manage.selectedItem?.id === item.id ? 'ring-2 ring-teal-500 border-teal-500' : ''}"
|
class="p-3 rounded-2xl bg-slate-950 hover:bg-slate-800/80 border border-slate-800/90 text-left flex items-center gap-3 transition active:scale-95 touch-manipulation min-h-[52px]"
|
||||||
>
|
>
|
||||||
${item.image_url ? `
|
${item.image_url ? `
|
||||||
<img src="${escapeHtml(item.image_url)}" class="w-9 h-9 object-contain rounded-xl bg-white/5 p-0.5 shrink-0">
|
<img src="${escapeHtml(item.image_url)}" class="w-9 h-9 object-contain rounded-xl bg-white/5 p-0.5 shrink-0">
|
||||||
@@ -344,90 +375,71 @@ function renderManageQuickPicker(filterQuery = '') {
|
|||||||
initLucide();
|
initLucide();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleManageSearch(val) {
|
function forwardToProductActionById(id) {
|
||||||
renderManageQuickPicker(val);
|
|
||||||
|
|
||||||
const clean = val.trim();
|
|
||||||
if (/^\d{8,14}$/.test(clean)) {
|
|
||||||
const found = state.items.find(i => i.barcode === clean);
|
|
||||||
if (found) {
|
|
||||||
selectManageItem(found);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectManageItemById(id) {
|
|
||||||
const item = state.items.find(i => i.id === id);
|
const item = state.items.find(i => i.id === id);
|
||||||
if (item) selectManageItem(item);
|
if (item) forwardToProductAction(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectManageItem(item) {
|
// Forwards to the action screen on /manage
|
||||||
|
function forwardToProductAction(item) {
|
||||||
state.manage.selectedItem = item;
|
state.manage.selectedItem = item;
|
||||||
state.manage.action = null; // reset step 2
|
state.manage.action = null; // reset action
|
||||||
state.manage.packetCount = 1;
|
state.manage.packetCount = 1;
|
||||||
state.manage.amountValue = item.quantity;
|
state.manage.amountValue = item.quantity;
|
||||||
|
|
||||||
document.getElementById('manage-selected-name').textContent = item.name;
|
// Populate header banner
|
||||||
document.getElementById('manage-selected-details').textContent = `Currently: ${item.quantity} ${item.unit} in stock • 1 Full Pack = ${item.initial_quantity} ${item.unit}`;
|
document.getElementById('action-product-name').textContent = item.name;
|
||||||
document.getElementById('manage-pack-size-label').textContent = `${item.initial_quantity} ${item.unit}`;
|
document.getElementById('action-product-sub').textContent = `Current Stock: ${item.quantity} ${item.unit} • 1 Full Pack = ${item.initial_quantity} ${item.unit}`;
|
||||||
document.getElementById('manage-amount-unit').textContent = item.unit;
|
document.getElementById('action-pack-size-label').textContent = `${item.initial_quantity} ${item.unit}`;
|
||||||
document.getElementById('manage-amount-val').textContent = item.quantity;
|
document.getElementById('action-amount-unit').textContent = item.unit;
|
||||||
|
document.getElementById('action-amount-val').textContent = item.quantity;
|
||||||
|
|
||||||
const slider = document.getElementById('manage-amount-slider');
|
const slider = document.getElementById('action-amount-slider');
|
||||||
slider.max = Math.max(item.initial_quantity * 2, item.quantity + item.initial_quantity, 100);
|
slider.max = Math.max(item.initial_quantity * 2, item.quantity + item.initial_quantity, 100);
|
||||||
slider.value = item.quantity;
|
slider.value = item.quantity;
|
||||||
slider.step = (item.unit === 'g' || item.unit === 'ml') ? '5' : '1';
|
slider.step = (item.unit === 'g' || item.unit === 'ml') ? '5' : '1';
|
||||||
|
|
||||||
const imgBox = document.getElementById('manage-selected-img');
|
const imgBox = document.getElementById('action-product-img');
|
||||||
if (item.image_url) {
|
if (item.image_url) {
|
||||||
imgBox.innerHTML = `<img src="${escapeHtml(item.image_url)}" class="w-full h-full object-contain p-1">`;
|
imgBox.innerHTML = `<img src="${escapeHtml(item.image_url)}" class="w-full h-full object-contain p-1">`;
|
||||||
} else {
|
} else {
|
||||||
imgBox.innerHTML = `<i data-lucide="${getCategoryIcon(item.category)}" class="w-7 h-7 text-teal-400"></i>`;
|
imgBox.innerHTML = `<i data-lucide="${getCategoryIcon(item.category)}" class="w-7 h-7 text-teal-400"></i>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('manage-action-card').classList.remove('hidden');
|
// Switch view state
|
||||||
document.getElementById('manage-qty-type-section').classList.add('hidden');
|
document.getElementById('tablet-step-select').classList.add('hidden');
|
||||||
|
document.getElementById('tablet-step-action').classList.remove('hidden');
|
||||||
|
document.getElementById('action-qty-section').classList.add('hidden');
|
||||||
|
|
||||||
resetActionButtonsStyle();
|
resetActionButtonsStyle();
|
||||||
renderManageQuickPicker();
|
|
||||||
initLucide();
|
initLucide();
|
||||||
|
|
||||||
document.getElementById('manage-action-card').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
document.getElementById('tablet-step-action').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSelectedManageItem() {
|
function cancelActionAndReturnToManage() {
|
||||||
state.manage.selectedItem = null;
|
resetTabletManageStation();
|
||||||
state.manage.action = null;
|
renderTabletProductPicker();
|
||||||
document.getElementById('manage-action-card').classList.add('hidden');
|
|
||||||
renderManageQuickPicker();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToManageItem(itemId) {
|
function setActionType(action) {
|
||||||
switchTab('manage');
|
|
||||||
selectManageItemById(itemId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setManageAction(action) {
|
|
||||||
state.manage.action = action; // 'add' or 'remove'
|
state.manage.action = action; // 'add' or 'remove'
|
||||||
const btnAdd = document.getElementById('manage-btn-add');
|
const btnAdd = document.getElementById('action-btn-add');
|
||||||
const btnRemove = document.getElementById('manage-btn-remove');
|
const btnRemove = document.getElementById('action-btn-remove');
|
||||||
const qtySection = document.getElementById('manage-qty-type-section');
|
const qtySection = document.getElementById('action-qty-section');
|
||||||
const confirmBtn = document.getElementById('manage-confirm-btn');
|
const confirmBtn = document.getElementById('action-confirm-btn');
|
||||||
const confirmLabel = document.getElementById('manage-confirm-label');
|
const confirmLabel = document.getElementById('action-confirm-label');
|
||||||
const step3Title = document.getElementById('manage-step3-title');
|
|
||||||
|
|
||||||
if (action === 'add') {
|
if (action === 'add') {
|
||||||
btnAdd.className = 'p-4 sm:p-5 rounded-2xl border-2 text-center font-extrabold transition-all active:scale-95 touch-manipulation flex flex-col items-center justify-center gap-2 min-h-[80px] bg-emerald-500/20 border-emerald-500 text-white shadow-lg shadow-emerald-500/20 ring-2 ring-emerald-500/40';
|
btnAdd.className = 'p-4 sm:p-5 rounded-2xl border-2 text-center font-extrabold transition-all active:scale-95 touch-manipulation flex flex-col items-center justify-center gap-2 min-h-[84px] bg-emerald-500/20 border-emerald-500 text-white shadow-lg shadow-emerald-500/20 ring-2 ring-emerald-500/40';
|
||||||
btnRemove.className = 'p-4 sm:p-5 rounded-2xl border-2 text-center font-extrabold transition-all active:scale-95 touch-manipulation flex flex-col items-center justify-center gap-2 min-h-[80px] bg-slate-950 border-slate-800 text-slate-400 hover:border-slate-700';
|
btnRemove.className = 'p-4 sm:p-5 rounded-2xl border-2 text-center font-extrabold transition-all active:scale-95 touch-manipulation flex flex-col items-center justify-center gap-2 min-h-[84px] bg-slate-950 border-slate-800 text-slate-400 hover:border-slate-700';
|
||||||
step3Title.textContent = `How much ${state.manage.selectedItem.name} to ADD?`;
|
confirmBtn.className = 'flex-1 py-4 rounded-2xl bg-emerald-500 hover:bg-emerald-400 text-white font-black text-base sm:text-lg shadow-xl shadow-emerald-500/25 active:scale-95 transition-all touch-manipulation flex items-center justify-center gap-2';
|
||||||
confirmBtn.className = 'w-full py-4 rounded-2xl bg-emerald-500 hover:bg-emerald-400 text-white font-black text-base sm:text-lg shadow-xl shadow-emerald-500/25 active:scale-95 transition-all touch-manipulation flex items-center justify-center gap-2';
|
confirmLabel.textContent = `Confirm Add & Done`;
|
||||||
confirmLabel.textContent = `Confirm Add to Stock`;
|
|
||||||
} else {
|
} else {
|
||||||
btnRemove.className = 'p-4 sm:p-5 rounded-2xl border-2 text-center font-extrabold transition-all active:scale-95 touch-manipulation flex flex-col items-center justify-center gap-2 min-h-[80px] bg-rose-500/20 border-rose-500 text-white shadow-lg shadow-rose-500/20 ring-2 ring-rose-500/40';
|
btnRemove.className = 'p-4 sm:p-5 rounded-2xl border-2 text-center font-extrabold transition-all active:scale-95 touch-manipulation flex flex-col items-center justify-center gap-2 min-h-[84px] bg-rose-500/20 border-rose-500 text-white shadow-lg shadow-rose-500/20 ring-2 ring-rose-500/40';
|
||||||
btnAdd.className = 'p-4 sm:p-5 rounded-2xl border-2 text-center font-extrabold transition-all active:scale-95 touch-manipulation flex flex-col items-center justify-center gap-2 min-h-[80px] bg-slate-950 border-slate-800 text-slate-400 hover:border-slate-700';
|
btnAdd.className = 'p-4 sm:p-5 rounded-2xl border-2 text-center font-extrabold transition-all active:scale-95 touch-manipulation flex flex-col items-center justify-center gap-2 min-h-[84px] bg-slate-950 border-slate-800 text-slate-400 hover:border-slate-700';
|
||||||
step3Title.textContent = `How much ${state.manage.selectedItem.name} to REMOVE?`;
|
confirmBtn.className = 'flex-1 py-4 rounded-2xl bg-rose-500 hover:bg-rose-400 text-white font-black text-base sm:text-lg shadow-xl shadow-rose-500/25 active:scale-95 transition-all touch-manipulation flex items-center justify-center gap-2';
|
||||||
confirmBtn.className = 'w-full py-4 rounded-2xl bg-rose-500 hover:bg-rose-400 text-white font-black text-base sm:text-lg shadow-xl shadow-rose-500/25 active:scale-95 transition-all touch-manipulation flex items-center justify-center gap-2';
|
confirmLabel.textContent = `Confirm Remove & Done`;
|
||||||
confirmLabel.textContent = `Confirm Remove from Stock`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
qtySection.classList.remove('hidden');
|
qtySection.classList.remove('hidden');
|
||||||
@@ -435,19 +447,19 @@ function setManageAction(action) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetActionButtonsStyle() {
|
function resetActionButtonsStyle() {
|
||||||
const btnAdd = document.getElementById('manage-btn-add');
|
const btnAdd = document.getElementById('action-btn-add');
|
||||||
const btnRemove = document.getElementById('manage-btn-remove');
|
const btnRemove = document.getElementById('action-btn-remove');
|
||||||
const defClasses = 'p-4 sm:p-5 rounded-2xl border-2 text-center font-extrabold transition-all active:scale-95 touch-manipulation flex flex-col items-center justify-center gap-2 min-h-[80px] bg-slate-950 border-slate-800 text-slate-300 hover:border-slate-700';
|
const def = 'p-4 sm:p-5 rounded-2xl border-2 text-center font-extrabold transition-all active:scale-95 touch-manipulation flex flex-col items-center justify-center gap-2 min-h-[84px] bg-slate-950 border-slate-800 text-slate-300 hover:border-slate-700';
|
||||||
btnAdd.className = defClasses;
|
btnAdd.className = def;
|
||||||
btnRemove.className = defClasses;
|
btnRemove.className = def;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setQtyMode(mode) {
|
function setActionQtyMode(mode) {
|
||||||
state.manage.qtyMode = mode; // 'packet' or 'amount'
|
state.manage.qtyMode = mode;
|
||||||
const btnPack = document.getElementById('qty-mode-packet-btn');
|
const btnPack = document.getElementById('btn-mode-packet');
|
||||||
const btnAmount = document.getElementById('qty-mode-amount-btn');
|
const btnAmount = document.getElementById('btn-mode-amount');
|
||||||
const packControls = document.getElementById('qty-packet-controls');
|
const packControls = document.getElementById('controls-packet');
|
||||||
const amountControls = document.getElementById('qty-amount-controls');
|
const amountControls = document.getElementById('controls-amount');
|
||||||
|
|
||||||
const active = 'py-3 px-3 rounded-xl font-bold text-xs sm:text-sm transition bg-teal-500 text-white shadow-md touch-manipulation flex items-center justify-center gap-2';
|
const active = 'py-3 px-3 rounded-xl font-bold text-xs sm:text-sm transition bg-teal-500 text-white shadow-md touch-manipulation flex items-center justify-center gap-2';
|
||||||
const inactive = 'py-3 px-3 rounded-xl font-bold text-xs sm:text-sm transition text-slate-300 hover:text-white touch-manipulation flex items-center justify-center gap-2';
|
const inactive = 'py-3 px-3 rounded-xl font-bold text-xs sm:text-sm transition text-slate-300 hover:text-white touch-manipulation flex items-center justify-center gap-2';
|
||||||
@@ -467,35 +479,36 @@ function setQtyMode(mode) {
|
|||||||
initLucide();
|
initLucide();
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePacketCount(delta) {
|
function changeActionPackCount(delta) {
|
||||||
state.manage.packetCount = Math.max(1, state.manage.packetCount + delta);
|
state.manage.packetCount = Math.max(1, state.manage.packetCount + delta);
|
||||||
document.getElementById('manage-pack-count').textContent = state.manage.packetCount;
|
document.getElementById('action-pack-count-val').textContent = state.manage.packetCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPacketCount(num) {
|
function setActionPackCount(num) {
|
||||||
state.manage.packetCount = num;
|
state.manage.packetCount = num;
|
||||||
document.getElementById('manage-pack-count').textContent = num;
|
document.getElementById('action-pack-count-val').textContent = num;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onManageAmountSlider(val) {
|
function onActionAmountSlider(val) {
|
||||||
state.manage.amountValue = parseFloat(val);
|
state.manage.amountValue = parseFloat(val);
|
||||||
document.getElementById('manage-amount-val').textContent = state.manage.amountValue;
|
document.getElementById('action-amount-val').textContent = state.manage.amountValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyManageDelta(delta) {
|
function applyActionDelta(delta) {
|
||||||
state.manage.amountValue = Math.max(0, state.manage.amountValue + delta);
|
state.manage.amountValue = Math.max(0, state.manage.amountValue + delta);
|
||||||
document.getElementById('manage-amount-slider').value = state.manage.amountValue;
|
document.getElementById('action-amount-slider').value = state.manage.amountValue;
|
||||||
document.getElementById('manage-amount-val').textContent = state.manage.amountValue;
|
document.getElementById('action-amount-val').textContent = state.manage.amountValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setManageAmountRatio(ratio) {
|
function setActionAmountRatio(ratio) {
|
||||||
if (!state.manage.selectedItem) return;
|
if (!state.manage.selectedItem) return;
|
||||||
state.manage.amountValue = Math.round(state.manage.selectedItem.initial_quantity * ratio);
|
state.manage.amountValue = Math.round(state.manage.selectedItem.initial_quantity * ratio);
|
||||||
document.getElementById('manage-amount-slider').value = state.manage.amountValue;
|
document.getElementById('action-amount-slider').value = state.manage.amountValue;
|
||||||
document.getElementById('manage-amount-val').textContent = state.manage.amountValue;
|
document.getElementById('action-amount-val').textContent = state.manage.amountValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmManageExecution() {
|
// Confirm & finish execution -> Returns right back to /manage ready for next scan!
|
||||||
|
async function confirmAndFinishAction() {
|
||||||
const item = state.manage.selectedItem;
|
const item = state.manage.selectedItem;
|
||||||
const action = state.manage.action;
|
const action = state.manage.action;
|
||||||
const mode = state.manage.qtyMode;
|
const mode = state.manage.qtyMode;
|
||||||
@@ -517,14 +530,17 @@ async function confirmManageExecution() {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const updated = await res.json();
|
const updated = await res.json();
|
||||||
playBeepSound();
|
playBeepSound();
|
||||||
showToast(`✓ ${action === 'add' ? 'Added' : 'Removed'} ${state.manage.packetCount} pack(s) of ${updated.name} (Now: ${updated.quantity} ${updated.unit})`, 'success');
|
showToast(`✓ ${action === 'add' ? 'Added' : 'Removed'} ${state.manage.packetCount} pack(s) of ${updated.name}`, 'success');
|
||||||
clearSelectedManageItem();
|
|
||||||
|
// Return to manage station
|
||||||
|
resetTabletManageStation();
|
||||||
await loadStats();
|
await loadStats();
|
||||||
await loadItems();
|
await loadItems();
|
||||||
} else {
|
} else {
|
||||||
showToast('Failed to update packet', 'error');
|
showToast('Failed to update packet', 'error');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Amount / Grams mode
|
||||||
const newQty = state.manage.amountValue;
|
const newQty = state.manage.amountValue;
|
||||||
const res = await fetch(`/api/items/${item.id}/quantity`, {
|
const res = await fetch(`/api/items/${item.id}/quantity`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
@@ -536,7 +552,9 @@ async function confirmManageExecution() {
|
|||||||
const updated = await res.json();
|
const updated = await res.json();
|
||||||
playBeepSound();
|
playBeepSound();
|
||||||
showToast(`✓ Set ${updated.name} to ${updated.quantity} ${updated.unit}`, 'success');
|
showToast(`✓ Set ${updated.name} to ${updated.quantity} ${updated.unit}`, 'success');
|
||||||
clearSelectedManageItem();
|
|
||||||
|
// Return to manage station
|
||||||
|
resetTabletManageStation();
|
||||||
await loadStats();
|
await loadStats();
|
||||||
await loadItems();
|
await loadItems();
|
||||||
} else {
|
} else {
|
||||||
@@ -544,13 +562,13 @@ async function confirmManageExecution() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Execution error:', err);
|
console.error('Action error:', err);
|
||||||
showToast('Network error while updating item', 'error');
|
showToast('Network error while updating item', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
// Add / Edit Product Modal Logic
|
// Add Product Manually Modal
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
|
|
||||||
function openItemModal(itemId = null) {
|
function openItemModal(itemId = null) {
|
||||||
@@ -582,7 +600,7 @@ function openItemModal(itemId = null) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('modal-item-id').value = '';
|
document.getElementById('modal-item-id').value = '';
|
||||||
document.getElementById('item-modal-title').textContent = 'Add Product';
|
document.getElementById('item-modal-title').textContent = 'Add Product Manually';
|
||||||
document.getElementById('modal-unit').value = 'g';
|
document.getElementById('modal-unit').value = 'g';
|
||||||
document.getElementById('modal-quantity').value = '';
|
document.getElementById('modal-quantity').value = '';
|
||||||
document.getElementById('modal-initial-quantity').value = '';
|
document.getElementById('modal-initial-quantity').value = '';
|
||||||
@@ -679,8 +697,10 @@ async function handleItemFormSubmit(event) {
|
|||||||
closeItemModal();
|
closeItemModal();
|
||||||
await loadStats();
|
await loadStats();
|
||||||
await loadItems();
|
await loadItems();
|
||||||
selectManageItem(saved);
|
|
||||||
switchTab('manage');
|
// Forward directly to action screen for this new product on /manage!
|
||||||
|
setRoute('/manage');
|
||||||
|
forwardToProductAction(saved);
|
||||||
} else {
|
} else {
|
||||||
const err = await res.json();
|
const err = await res.json();
|
||||||
showToast(err.error || 'Failed to save product', 'error');
|
showToast(err.error || 'Failed to save product', 'error');
|
||||||
@@ -691,6 +711,15 @@ async function handleItemFormSubmit(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function populateModalSelects() {
|
||||||
|
const catSelect = document.getElementById('modal-category');
|
||||||
|
if (catSelect && state.meta.categories) {
|
||||||
|
catSelect.innerHTML = state.meta.categories.map(c => `
|
||||||
|
<option value="${escapeHtml(c.id)}">${escapeHtml(c.name)}</option>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
// Tablet Live Camera Barcode Scanner
|
// Tablet Live Camera Barcode Scanner
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
@@ -758,25 +787,27 @@ async function onBarcodeScannedSuccess(decodedText) {
|
|||||||
stopCameraScanner();
|
stopCameraScanner();
|
||||||
closeScannerModal();
|
closeScannerModal();
|
||||||
|
|
||||||
|
// If item exists in stock -> forward directly to product action screen!
|
||||||
const existing = state.items.find(i => i.barcode === decodedText);
|
const existing = state.items.find(i => i.barcode === decodedText);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
switchTab('manage');
|
setRoute('/manage');
|
||||||
selectManageItem(existing);
|
forwardToProductAction(existing);
|
||||||
showToast(`Scanned: ${existing.name}`, 'success');
|
showToast(`Scanned: ${existing.name}`, 'success');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Not yet in stock -> lookup in Open Food Facts and open modal to confirm
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/barcode/${encodeURIComponent(decodedText)}`);
|
const res = await fetch(`/api/barcode/${encodeURIComponent(decodedText)}`);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
openItemModal();
|
openItemModal();
|
||||||
applyProductDataToForm(data);
|
applyProductDataToForm(data);
|
||||||
showToast(`Scanned: ${data.name}. Click Save to add!`, 'success');
|
showToast(`Scanned: ${data.name}. Click Save to add to stock!`, 'success');
|
||||||
} else {
|
} else {
|
||||||
openItemModal();
|
openItemModal();
|
||||||
document.getElementById('modal-barcode').value = decodedText;
|
document.getElementById('modal-barcode').value = decodedText;
|
||||||
showToast(`Scanned barcode: ${decodedText}. Enter details.`, 'info');
|
showToast(`Scanned barcode: ${decodedText}. Enter product name.`, 'info');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
+299
-300
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<title>Fridger - Inventory & Stock Manager</title>
|
<title>Fridger - Current Stock & Tablet Manager</title>
|
||||||
|
|
||||||
<!-- Tailwind CSS -->
|
<!-- Tailwind CSS -->
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
@@ -34,354 +34,353 @@
|
|||||||
</head>
|
</head>
|
||||||
<body class="h-full flex flex-col bg-slate-950 text-slate-100 antialiased selection:bg-teal-500 selection:text-white select-none">
|
<body class="h-full flex flex-col bg-slate-950 text-slate-100 antialiased selection:bg-teal-500 selection:text-white select-none">
|
||||||
|
|
||||||
<!-- TOP TABLET HEADER (MAIN -> SCAN -> MANAGE) -->
|
<!-- TOP HEADER -->
|
||||||
<header class="sticky top-0 z-30 bg-slate-900/95 backdrop-blur-md border-b border-slate-800 shadow-md">
|
<header class="sticky top-0 z-30 bg-slate-900/95 backdrop-blur-md border-b border-slate-800 shadow-md">
|
||||||
<div class="max-w-4xl mx-auto px-3 sm:px-6">
|
<div class="max-w-4xl mx-auto px-3 sm:px-6">
|
||||||
<div class="flex items-center justify-between h-16 sm:h-20 gap-2">
|
<div class="flex items-center justify-between h-16 sm:h-20 gap-3">
|
||||||
|
|
||||||
<!-- Logo -->
|
<!-- Logo & Title -->
|
||||||
<div class="flex items-center gap-2.5 shrink-0">
|
<a href="/" onclick="navigateTo('/'); return false;" class="flex items-center gap-2.5 shrink-0 group">
|
||||||
<div class="w-10 h-10 sm:w-11 sm:h-11 rounded-2xl bg-gradient-to-tr from-teal-500 to-cyan-400 flex items-center justify-center shadow-lg shadow-teal-500/20 text-white font-black text-xl">
|
<div class="w-10 h-10 sm:w-11 sm:h-11 rounded-2xl bg-gradient-to-tr from-teal-500 to-cyan-400 flex items-center justify-center shadow-lg shadow-teal-500/20 text-white font-black text-xl group-hover:scale-105 transition-transform">
|
||||||
🧊
|
🧊
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden xs:block">
|
<div>
|
||||||
<h1 class="font-extrabold text-lg sm:text-xl leading-none bg-gradient-to-r from-teal-300 to-cyan-300 bg-clip-text text-transparent">Fridger</h1>
|
<h1 class="font-extrabold text-lg sm:text-xl leading-none bg-gradient-to-r from-teal-300 to-cyan-300 bg-clip-text text-transparent">Fridger</h1>
|
||||||
<p class="text-[11px] text-slate-400 font-medium">Fridge & Freezer</p>
|
<p id="page-subtitle" class="text-[11px] text-slate-400 font-medium">Current Stock</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
|
|
||||||
<!-- 3 MAIN TABS: MAIN -> SCAN -> MANAGE -->
|
<!-- TOP NAVIGATION SWITCHER -->
|
||||||
<nav class="flex items-center bg-slate-950 p-1.5 rounded-2xl border border-slate-800 shadow-inner gap-1">
|
<div class="flex items-center bg-slate-950 p-1.5 rounded-2xl border border-slate-800 shadow-inner gap-1">
|
||||||
|
<!-- 1. Current Stock (/) -->
|
||||||
<!-- 1. MAIN (What's in the Fridge / Freezer) -->
|
|
||||||
<button
|
<button
|
||||||
id="nav-main-btn"
|
id="nav-stock-btn"
|
||||||
onclick="switchTab('main')"
|
onclick="navigateTo('/')"
|
||||||
class="flex items-center justify-center gap-2 px-4 sm:px-7 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all bg-teal-500 text-white shadow-md active:scale-95 touch-manipulation min-h-[44px]"
|
class="flex items-center justify-center gap-2 px-3.5 sm:px-6 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all bg-teal-500 text-white shadow-md active:scale-95 touch-manipulation min-h-[44px]"
|
||||||
>
|
>
|
||||||
<i data-lucide="home" class="w-4 h-4 sm:w-5 sm:h-5"></i>
|
<i data-lucide="layout-grid" class="w-4 h-4 sm:w-5 sm:h-5"></i>
|
||||||
<span>Main</span>
|
<span>Current Stock</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- 2. SCAN (Live Camera Barcode) -->
|
<!-- 2. Tablet Manage (/manage) -->
|
||||||
<button
|
|
||||||
id="nav-scan-btn"
|
|
||||||
onclick="openScannerModal()"
|
|
||||||
class="flex items-center justify-center gap-2 px-4 sm:px-7 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all text-slate-300 hover:text-white hover:bg-slate-800/80 active:scale-95 touch-manipulation min-h-[44px]"
|
|
||||||
>
|
|
||||||
<i data-lucide="scan-barcode" class="w-4 h-4 sm:w-5 sm:h-5 text-teal-400"></i>
|
|
||||||
<span>Scan</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- 3. MANAGE (Scan/Search -> Add/Remove -> Packet/Amount) -->
|
|
||||||
<button
|
<button
|
||||||
id="nav-manage-btn"
|
id="nav-manage-btn"
|
||||||
onclick="switchTab('manage')"
|
onclick="navigateTo('/manage')"
|
||||||
class="flex items-center justify-center gap-2 px-4 sm:px-7 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all text-slate-300 hover:text-white hover:bg-slate-800/80 active:scale-95 touch-manipulation min-h-[44px]"
|
class="flex items-center justify-center gap-2 px-3.5 sm:px-6 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all text-slate-300 hover:text-white hover:bg-slate-800/80 active:scale-95 touch-manipulation min-h-[44px]"
|
||||||
>
|
>
|
||||||
<i data-lucide="sliders-horizontal" class="w-4 h-4 sm:w-5 sm:h-5"></i>
|
<i data-lucide="tablet" class="w-4 h-4 sm:w-5 sm:h-5 text-teal-400"></i>
|
||||||
<span>Manage</span>
|
<span>Tablet Manage</span>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Quick Add New Product -->
|
|
||||||
<button
|
|
||||||
onclick="openItemModal()"
|
|
||||||
class="flex items-center justify-center gap-1.5 px-3 sm:px-4 py-2.5 sm:py-3 rounded-xl bg-slate-800 hover:bg-slate-700 border border-slate-700 text-slate-200 text-xs sm:text-sm font-semibold transition active:scale-95 min-h-[44px] touch-manipulation shrink-0"
|
|
||||||
title="New Product"
|
|
||||||
>
|
|
||||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
|
||||||
<span class="hidden sm:inline">New</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- QUICK STATS BAR -->
|
<!-- ======================================================== -->
|
||||||
<section class="bg-slate-900/50 border-b border-slate-800/60 py-2 sm:py-2.5">
|
<!-- 1. ROOT VIEW: / (CURRENT STOCK ONLY) -->
|
||||||
<div class="max-w-4xl mx-auto px-3 sm:px-6">
|
<!-- ======================================================== -->
|
||||||
<div class="flex items-center justify-between text-xs sm:text-sm text-slate-400 overflow-x-auto gap-4 scrollbar-none">
|
<div id="stock-view-page" class="flex-1 flex flex-col">
|
||||||
<div class="flex items-center gap-2 shrink-0">
|
|
||||||
<span class="w-2.5 h-2.5 rounded-full bg-teal-400"></span>
|
|
||||||
<span>In Stock: <strong id="stat-total" class="text-white font-black text-sm sm:text-base">0</strong> items</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 shrink-0">
|
|
||||||
<span class="w-2.5 h-2.5 rounded-full bg-emerald-400"></span>
|
|
||||||
<span>Full Packs: <strong id="stat-full" class="text-emerald-300 font-bold">0</strong></span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 shrink-0">
|
|
||||||
<span class="w-2.5 h-2.5 rounded-full bg-indigo-400"></span>
|
|
||||||
<span>In Use / Partial: <strong id="stat-opened" class="text-indigo-300 font-bold">0</strong></span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 shrink-0">
|
|
||||||
<span class="w-2.5 h-2.5 rounded-full bg-amber-400"></span>
|
|
||||||
<span>Low (≤25%): <strong id="stat-low" class="text-amber-300 font-bold">0</strong></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- MAIN TAB CONTENT (FRIDGE / FREEZER VIEW) -->
|
|
||||||
<main id="main-tab-view" class="flex-1 max-w-4xl mx-auto px-3 sm:px-6 py-4 w-full space-y-3">
|
|
||||||
|
|
||||||
<!-- Search & Filter Ribbon in Main tab -->
|
<!-- Quick Stock Summary Bar -->
|
||||||
<div class="space-y-2.5">
|
<section class="bg-slate-900/50 border-b border-slate-800/60 py-2 sm:py-2.5">
|
||||||
<div class="flex flex-col sm:flex-row gap-2.5 items-stretch sm:items-center justify-between">
|
<div class="max-w-4xl mx-auto px-3 sm:px-6">
|
||||||
<div class="relative flex-1">
|
<div class="flex items-center justify-between text-xs sm:text-sm text-slate-400 overflow-x-auto gap-4 scrollbar-none">
|
||||||
<i data-lucide="search" class="w-4 h-4 text-slate-400 absolute left-3.5 top-1/2 -translate-y-1/2"></i>
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
<input
|
<span class="w-2.5 h-2.5 rounded-full bg-teal-400"></span>
|
||||||
type="text"
|
<span>Total Stock: <strong id="stat-total" class="text-white font-black text-sm sm:text-base">0</strong> items</span>
|
||||||
id="search-input"
|
</div>
|
||||||
placeholder="Search items in fridge or freezer..."
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
oninput="handleSearch(this.value)"
|
<span class="w-2.5 h-2.5 rounded-full bg-emerald-400"></span>
|
||||||
class="w-full bg-slate-900 border border-slate-800 focus:border-teal-500 rounded-xl pl-9 pr-9 py-2.5 text-sm sm:text-base text-slate-100 placeholder-slate-500 outline-none transition"
|
<span>Full Packs: <strong id="stat-full" class="text-emerald-300 font-bold">0</strong></span>
|
||||||
>
|
</div>
|
||||||
<button id="clear-search" onclick="clearSearch()" class="hidden absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-white p-1">
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
<i data-lucide="x" class="w-4 h-4"></i>
|
<span class="w-2.5 h-2.5 rounded-full bg-indigo-400"></span>
|
||||||
</button>
|
<span>Opened / Partial: <strong id="stat-opened" class="text-indigo-300 font-bold">0</strong></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
|
<span class="w-2.5 h-2.5 rounded-full bg-amber-400"></span>
|
||||||
|
<span>Low (≤25%): <strong id="stat-low" class="text-amber-300 font-bold">0</strong></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Stock Search & Filters -->
|
||||||
|
<main class="flex-1 max-w-4xl mx-auto px-3 sm:px-6 py-4 w-full space-y-3">
|
||||||
|
<div class="space-y-2.5">
|
||||||
|
<div class="flex flex-col sm:flex-row gap-2.5 items-stretch sm:items-center justify-between">
|
||||||
|
<div class="relative flex-1">
|
||||||
|
<i data-lucide="search" class="w-4 h-4 text-slate-400 absolute left-3.5 top-1/2 -translate-y-1/2"></i>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="search-input"
|
||||||
|
placeholder="Search current stock by name, brand, or barcode..."
|
||||||
|
oninput="handleSearch(this.value)"
|
||||||
|
class="w-full bg-slate-900 border border-slate-800 focus:border-teal-500 rounded-xl pl-9 pr-9 py-2.5 text-sm sm:text-base text-slate-100 placeholder-slate-500 outline-none transition"
|
||||||
|
>
|
||||||
|
<button id="clear-search" onclick="clearSearch()" class="hidden absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-white p-1">
|
||||||
|
<i data-lucide="x" class="w-4 h-4"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1.5 overflow-x-auto pb-0.5 text-xs sm:text-sm font-medium scrollbar-none">
|
||||||
|
<button onclick="setStatusFilter('all')" data-status-btn="all" class="status-chip active px-3.5 py-2 rounded-xl bg-slate-800 border border-slate-700 text-white font-bold min-h-[38px] touch-manipulation">All</button>
|
||||||
|
<button onclick="setStatusFilter('full')" data-status-btn="full" class="status-chip px-3.5 py-2 rounded-xl bg-slate-900 border border-slate-800 text-slate-400 hover:text-slate-200 min-h-[38px] touch-manipulation">Full Packs</button>
|
||||||
|
<button onclick="setStatusFilter('opened')" data-status-btn="opened" class="status-chip px-3.5 py-2 rounded-xl bg-slate-900 border border-slate-800 text-indigo-400 hover:text-indigo-300 min-h-[38px] touch-manipulation">Opened</button>
|
||||||
|
<button onclick="setStatusFilter('low')" data-status-btn="low" class="status-chip px-3.5 py-2 rounded-xl bg-slate-900 border border-slate-800 text-amber-400 hover:text-amber-300 min-h-[38px] touch-manipulation">≤25% Low</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-1.5 overflow-x-auto pb-0.5 text-xs sm:text-sm font-medium scrollbar-none">
|
<div id="category-filter-bar" class="flex items-center gap-1.5 overflow-x-auto pb-1 scrollbar-none text-xs sm:text-sm font-medium">
|
||||||
<button onclick="setStatusFilter('all')" data-status-btn="all" class="status-chip active px-3.5 py-2 rounded-xl bg-slate-800 border border-slate-700 text-white font-bold min-h-[38px] touch-manipulation">All</button>
|
<!-- Injected by JS -->
|
||||||
<button onclick="setStatusFilter('full')" data-status-btn="full" class="status-chip px-3.5 py-2 rounded-xl bg-slate-900 border border-slate-800 text-slate-400 hover:text-slate-200 min-h-[38px] touch-manipulation">Full Packs</button>
|
|
||||||
<button onclick="setStatusFilter('opened')" data-status-btn="opened" class="status-chip px-3.5 py-2 rounded-xl bg-slate-900 border border-slate-800 text-indigo-400 hover:text-indigo-300 min-h-[38px] touch-manipulation">In Use / Partial</button>
|
|
||||||
<button onclick="setStatusFilter('low')" data-status-btn="low" class="status-chip px-3.5 py-2 rounded-xl bg-slate-900 border border-slate-800 text-amber-400 hover:text-amber-300 min-h-[38px] touch-manipulation">≤25% Low</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="category-filter-bar" class="flex items-center gap-1.5 overflow-x-auto pb-1 scrollbar-none text-xs sm:text-sm font-medium">
|
<!-- Current Stock Grid (Display only) -->
|
||||||
<!-- Injected by JS -->
|
<div id="stock-grid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3.5 sm:gap-4 pt-2">
|
||||||
|
<!-- Rendered items -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Fridge Cards Grid -->
|
<!-- Empty Stock State -->
|
||||||
<div id="fridge-view-content" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3.5 sm:gap-4 pt-2">
|
<div id="empty-stock" class="hidden text-center py-16 px-4">
|
||||||
<!-- Rendered items -->
|
<div class="w-16 h-16 sm:w-20 sm:h-20 rounded-2xl bg-slate-900 text-slate-500 flex items-center justify-center mx-auto mb-3 border border-slate-800 text-3xl">
|
||||||
</div>
|
🧊
|
||||||
|
|
||||||
<!-- Empty State -->
|
|
||||||
<div id="empty-state" class="hidden text-center py-16 px-4">
|
|
||||||
<div class="w-16 h-16 sm:w-20 sm:h-20 rounded-2xl bg-slate-900 text-slate-500 flex items-center justify-center mx-auto mb-3 border border-slate-800 text-3xl">
|
|
||||||
🧊
|
|
||||||
</div>
|
|
||||||
<h3 class="text-lg font-bold text-white mb-1">Your inventory is empty</h3>
|
|
||||||
<p class="text-xs sm:text-sm text-slate-400 max-w-xs mx-auto mb-6">Scan a barcode or add an item to track what you have in stock.</p>
|
|
||||||
<button onclick="switchTab('manage')" class="px-6 py-3 rounded-xl bg-teal-500 hover:bg-teal-400 text-white text-sm font-bold flex items-center gap-2 shadow-lg shadow-teal-500/20 active:scale-95 touch-manipulation mx-auto">
|
|
||||||
<i data-lucide="sliders-horizontal" class="w-5 h-5"></i>
|
|
||||||
<span>Go to Manage</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- MANAGE TAB CONTENT (INTERACTIVE STEP FLOW: SCAN/SEARCH -> ADD/REMOVE -> PACKET/AMOUNT) -->
|
|
||||||
<main id="manage-tab-view" class="hidden flex-1 max-w-4xl mx-auto px-3 sm:px-6 py-4 w-full space-y-5">
|
|
||||||
|
|
||||||
<!-- STEP 1: SCAN OR SEARCH / SELECT PRODUCT -->
|
|
||||||
<div class="bg-slate-900 border border-slate-800 rounded-3xl p-4 sm:p-6 shadow-xl space-y-4">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-2.5">
|
|
||||||
<div class="w-7 h-7 rounded-xl bg-teal-500/20 text-teal-300 flex items-center justify-center text-xs font-black">1</div>
|
|
||||||
<h2 class="text-base sm:text-lg font-extrabold text-white">Scan Barcode or Search Product</h2>
|
|
||||||
</div>
|
</div>
|
||||||
<button onclick="openItemModal()" class="text-xs text-teal-400 hover:text-teal-300 font-semibold flex items-center gap-1 touch-manipulation">
|
<h3 class="text-lg font-bold text-white mb-1">No items in stock</h3>
|
||||||
<i data-lucide="plus-circle" class="w-4 h-4"></i>
|
<p class="text-xs sm:text-sm text-slate-400 max-w-xs mx-auto mb-6">Use the tablet manage station to scan or add items.</p>
|
||||||
<span>Add New</span>
|
<button onclick="navigateTo('/manage')" class="px-6 py-3 rounded-xl bg-teal-500 hover:bg-teal-400 text-white text-sm font-bold flex items-center gap-2 shadow-lg shadow-teal-500/20 active:scale-95 touch-manipulation mx-auto">
|
||||||
|
<i data-lucide="tablet" class="w-5 h-5"></i>
|
||||||
|
<span>Go to Tablet Manage (/manage)</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
<!-- Live Scan & Search Big Controls -->
|
</div>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
||||||
<!-- Big Camera Scan Button -->
|
|
||||||
<button
|
|
||||||
onclick="openScannerModal()"
|
|
||||||
class="sm:col-span-1 p-4 rounded-2xl bg-gradient-to-r from-teal-500 to-emerald-600 hover:from-teal-400 hover:to-emerald-500 text-white font-extrabold flex items-center justify-center gap-3 shadow-lg shadow-teal-500/25 active:scale-95 touch-manipulation min-h-[56px]"
|
|
||||||
>
|
|
||||||
<i data-lucide="camera" class="w-6 h-6"></i>
|
|
||||||
<span class="text-sm sm:text-base">Scan Barcode</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Search / Barcode Input -->
|
|
||||||
<div class="sm:col-span-2 relative flex items-center">
|
|
||||||
<i data-lucide="search" class="w-5 h-5 text-slate-400 absolute left-4"></i>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="manage-search-input"
|
|
||||||
placeholder="Type name, brand, or barcode number..."
|
|
||||||
oninput="handleManageSearch(this.value)"
|
|
||||||
class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-2xl pl-12 pr-4 py-3.5 text-sm sm:text-base text-white placeholder-slate-500 outline-none transition min-h-[56px]"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Quick Product Pick List -->
|
<!-- ======================================================== -->
|
||||||
<div class="space-y-1.5 pt-2">
|
<!-- 2. TABLET STATION: /manage -->
|
||||||
<label class="text-xs font-bold text-slate-400 uppercase tracking-wider block">Or tap an item from stock:</label>
|
<!-- ======================================================== -->
|
||||||
<div id="manage-quick-picker" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2.5 max-h-48 overflow-y-auto p-1 scrollbar-none">
|
<div id="manage-view-page" class="hidden flex-1 flex flex-col">
|
||||||
<!-- Rendered dynamically -->
|
|
||||||
</div>
|
<main class="flex-1 max-w-4xl mx-auto px-3 sm:px-6 py-4 w-full space-y-4">
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- STEP 2 & 3: ACTION SELECTION (ADD / REMOVE) & QUANTITY TYPE (PACKET / AMOUNT) -->
|
|
||||||
<div id="manage-action-card" class="hidden bg-slate-900 border border-slate-700/80 rounded-3xl p-5 sm:p-6 shadow-2xl space-y-6 animate-scale-up">
|
|
||||||
|
|
||||||
<!-- Selected Product Banner -->
|
<!-- STEP 1: SCAN / SEARCH / ADD MANUALLY -->
|
||||||
<div class="bg-slate-950/80 p-4 rounded-2xl border border-slate-800 flex items-center justify-between gap-3">
|
<div id="tablet-step-select" class="bg-slate-900 border border-slate-800 rounded-3xl p-4 sm:p-6 shadow-xl space-y-4">
|
||||||
<div class="flex items-center gap-3.5 min-w-0">
|
|
||||||
<div id="manage-selected-img" class="w-14 h-14 rounded-xl bg-slate-800 flex items-center justify-center text-teal-400 shrink-0 overflow-hidden">
|
<div class="flex items-center justify-between">
|
||||||
<i data-lucide="package" class="w-7 h-7"></i>
|
<div class="flex items-center gap-2">
|
||||||
</div>
|
<div class="w-8 h-8 rounded-xl bg-teal-500/20 text-teal-300 flex items-center justify-center text-sm font-black">1</div>
|
||||||
<div class="min-w-0">
|
<div>
|
||||||
<h3 id="manage-selected-name" class="font-extrabold text-white text-base sm:text-lg truncate">Selected Product</h3>
|
<h2 class="text-base sm:text-lg font-extrabold text-white">Tablet Scanner & Product Finder</h2>
|
||||||
<p id="manage-selected-details" class="text-xs sm:text-sm text-slate-400 mt-0.5">Currently: 250g in stock • 1 Pack = 250g</p>
|
<p class="text-xs text-slate-400">Scan product barcode, search display, or add manually</p>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button onclick="clearSelectedManageItem()" class="text-slate-400 hover:text-white p-2 rounded-xl bg-slate-800/80 touch-manipulation">
|
|
||||||
<i data-lucide="x" class="w-5 h-5"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- STEP 2: CHOOSE ADD OR REMOVE -->
|
|
||||||
<div class="space-y-3">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="w-7 h-7 rounded-xl bg-teal-500/20 text-teal-300 flex items-center justify-center text-xs font-black">2</div>
|
|
||||||
<h4 class="text-sm sm:text-base font-bold text-white">Choose Action</h4>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-3 sm:gap-4">
|
|
||||||
<button
|
|
||||||
id="manage-btn-add"
|
|
||||||
onclick="setManageAction('add')"
|
|
||||||
class="p-4 sm:p-5 rounded-2xl border-2 text-center font-extrabold transition-all active:scale-95 touch-manipulation flex flex-col items-center justify-center gap-2 min-h-[80px] bg-slate-950 border-slate-800 text-slate-300 hover:border-emerald-500/60 hover:text-white"
|
|
||||||
>
|
|
||||||
<i data-lucide="plus-circle" class="w-7 h-7 text-emerald-400"></i>
|
|
||||||
<span class="text-sm sm:text-base">➕ Add to Stock</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
id="manage-btn-remove"
|
|
||||||
onclick="setManageAction('remove')"
|
|
||||||
class="p-4 sm:p-5 rounded-2xl border-2 text-center font-extrabold transition-all active:scale-95 touch-manipulation flex flex-col items-center justify-center gap-2 min-h-[80px] bg-slate-950 border-slate-800 text-slate-300 hover:border-rose-500/60 hover:text-white"
|
|
||||||
>
|
|
||||||
<i data-lucide="minus-circle" class="w-7 h-7 text-rose-400"></i>
|
|
||||||
<span class="text-sm sm:text-base">➖ Remove from Stock</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- STEP 3: CHOOSE QUANTITY TYPE (WHOLE PACKET OR SPECIFIC GRAMS/AMOUNT) -->
|
|
||||||
<div id="manage-qty-type-section" class="hidden space-y-4 pt-2 border-t border-slate-800">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="w-7 h-7 rounded-xl bg-teal-500/20 text-teal-300 flex items-center justify-center text-xs font-black">3</div>
|
|
||||||
<h4 id="manage-step3-title" class="text-sm sm:text-base font-bold text-white">How much to Add / Remove?</h4>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Mode Toggle: Whole Packet vs Specific Grams -->
|
|
||||||
<div class="grid grid-cols-2 gap-2 p-1.5 bg-slate-950 rounded-2xl border border-slate-800">
|
|
||||||
<button
|
|
||||||
id="qty-mode-packet-btn"
|
|
||||||
onclick="setQtyMode('packet')"
|
|
||||||
class="py-3 px-3 rounded-xl font-bold text-xs sm:text-sm transition bg-teal-500 text-white shadow-md touch-manipulation flex items-center justify-center gap-2"
|
|
||||||
>
|
|
||||||
<i data-lucide="package" class="w-4 h-4"></i>
|
|
||||||
<span>Whole Packet(s)</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
id="qty-mode-amount-btn"
|
|
||||||
onclick="setQtyMode('amount')"
|
|
||||||
class="py-3 px-3 rounded-xl font-bold text-xs sm:text-sm transition text-slate-300 hover:text-white touch-manipulation flex items-center justify-center gap-2"
|
|
||||||
>
|
|
||||||
<i data-lucide="scale" class="w-4 h-4"></i>
|
|
||||||
<span>Specific Grams / Amount</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Option A: Whole Packet Controls -->
|
|
||||||
<div id="qty-packet-controls" class="bg-slate-950 p-4 sm:p-5 rounded-2xl border border-slate-800 space-y-4 text-center">
|
|
||||||
<div class="text-xs sm:text-sm text-slate-400 font-medium">
|
|
||||||
1 Packet = <strong id="manage-pack-size-label" class="text-teal-300 font-bold">250g</strong>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-center gap-4">
|
|
||||||
<button onclick="changePacketCount(-1)" class="w-12 h-12 rounded-2xl bg-slate-800 hover:bg-slate-700 text-white font-extrabold text-2xl flex items-center justify-center active:scale-95 touch-manipulation border border-slate-700">-</button>
|
|
||||||
<div class="text-3xl sm:text-4xl font-black text-white min-w-[90px]">
|
|
||||||
<span id="manage-pack-count">1</span> <span class="text-sm font-normal text-slate-400">pack</span>
|
|
||||||
</div>
|
</div>
|
||||||
<button onclick="changePacketCount(1)" class="w-12 h-12 rounded-2xl bg-slate-800 hover:bg-slate-700 text-white font-extrabold text-2xl flex items-center justify-center active:scale-95 touch-manipulation border border-slate-700">+</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-center gap-2">
|
<!-- Add Manually Button (Prominent when scan or search don't work) -->
|
||||||
<button onclick="setPacketCount(1)" class="px-3.5 py-1.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs text-slate-200 border border-slate-700 font-bold">1 Pack</button>
|
<button
|
||||||
<button onclick="setPacketCount(2)" class="px-3.5 py-1.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs text-slate-200 border border-slate-700 font-bold">2 Packs</button>
|
onclick="openItemModal()"
|
||||||
<button onclick="setPacketCount(3)" class="px-3.5 py-1.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs text-slate-200 border border-slate-700 font-bold">3 Packs</button>
|
class="px-3.5 sm:px-4 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-teal-300 border border-teal-500/30 text-xs sm:text-sm font-bold flex items-center gap-1.5 active:scale-95 touch-manipulation shadow-sm shrink-0"
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Option B: Specific Grams Controls (Slider & Quick Chips) -->
|
|
||||||
<div id="qty-amount-controls" class="hidden bg-slate-950 p-4 sm:p-5 rounded-2xl border border-slate-800 space-y-4 text-center">
|
|
||||||
<div>
|
|
||||||
<div class="text-3xl sm:text-4xl font-black text-white flex items-center justify-center gap-1.5">
|
|
||||||
<span id="manage-amount-val">150</span>
|
|
||||||
<span id="manage-amount-unit" class="text-base font-normal text-teal-400">g</span>
|
|
||||||
</div>
|
|
||||||
<div id="manage-amount-subtext" class="text-xs text-slate-400 mt-1 font-medium">New remaining amount in stock</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="manage-amount-slider"
|
|
||||||
min="0"
|
|
||||||
max="500"
|
|
||||||
step="5"
|
|
||||||
value="150"
|
|
||||||
oninput="onManageAmountSlider(this.value)"
|
|
||||||
class="w-full accent-teal-500 cursor-pointer h-3 bg-slate-800 rounded-xl"
|
|
||||||
>
|
>
|
||||||
|
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||||
|
<span>Add Manually</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-3 sm:grid-cols-6 gap-2">
|
<!-- Integrated Camera Scanner & Live Search Box -->
|
||||||
<button onclick="applyManageDelta(-10)" class="py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs sm:text-sm text-slate-200 border border-slate-700 font-bold touch-manipulation">-10</button>
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 pt-1">
|
||||||
<button onclick="applyManageDelta(-25)" class="py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs sm:text-sm text-slate-200 border border-slate-700 font-bold touch-manipulation">-25</button>
|
<!-- Big Camera Scanner Touch Launch -->
|
||||||
<button onclick="applyManageDelta(-50)" class="py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs sm:text-sm text-slate-200 border border-slate-700 font-bold touch-manipulation">-50</button>
|
<button
|
||||||
<button onclick="applyManageDelta(-100)" class="py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs sm:text-sm text-slate-200 border border-slate-700 font-bold touch-manipulation">-100</button>
|
onclick="openScannerModal()"
|
||||||
<button onclick="setManageAmountRatio(0.5)" class="py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs sm:text-sm text-slate-200 border border-slate-700 font-bold touch-manipulation">Half</button>
|
class="sm:col-span-1 p-4 rounded-2xl bg-gradient-to-r from-teal-500 to-emerald-600 hover:from-teal-400 hover:to-emerald-500 text-white font-extrabold flex items-center justify-center gap-3 shadow-lg shadow-teal-500/25 active:scale-95 touch-manipulation min-h-[58px]"
|
||||||
<button onclick="setManageAmountRatio(0)" class="py-2.5 rounded-xl bg-rose-950/40 hover:bg-rose-900 text-xs sm:text-sm text-rose-300 border border-rose-800 font-bold touch-manipulation">0 (Empty)</button>
|
>
|
||||||
|
<i data-lucide="scan-barcode" class="w-6 h-6"></i>
|
||||||
|
<span class="text-sm sm:text-base">Scan Barcode</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Search or Barcode Input -->
|
||||||
|
<div class="sm:col-span-2 relative flex items-center">
|
||||||
|
<i data-lucide="search" class="w-5 h-5 text-slate-400 absolute left-4"></i>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="tablet-search-input"
|
||||||
|
placeholder="Type product name, brand, or barcode..."
|
||||||
|
oninput="handleTabletSearch(this.value)"
|
||||||
|
class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-2xl pl-12 pr-4 py-3.5 text-sm sm:text-base text-white placeholder-slate-500 outline-none transition min-h-[58px]"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Touch Display Product Picker -->
|
||||||
|
<div class="space-y-1.5 pt-2">
|
||||||
|
<label class="text-xs font-bold text-slate-400 uppercase tracking-wider block">Or tap a product from stock:</label>
|
||||||
|
<div id="tablet-product-picker" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2.5 max-h-56 overflow-y-auto p-1 scrollbar-none">
|
||||||
|
<!-- Populated dynamically -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Big Confirm Button -->
|
|
||||||
<button
|
|
||||||
id="manage-confirm-btn"
|
|
||||||
onclick="confirmManageExecution()"
|
|
||||||
class="w-full py-4 rounded-2xl bg-teal-500 hover:bg-teal-400 text-white font-black text-base sm:text-lg shadow-xl shadow-teal-500/25 active:scale-95 transition-all touch-manipulation flex items-center justify-center gap-2"
|
|
||||||
>
|
|
||||||
<i data-lucide="check" class="w-6 h-6"></i>
|
|
||||||
<span id="manage-confirm-label">Confirm Update</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
<!-- STEP 2 & 3: PRODUCT ACTION SCREEN (ADD / REMOVE -> PACKET / AMOUNT) -->
|
||||||
|
<div id="tablet-step-action" class="hidden bg-slate-900 border border-teal-500/40 rounded-3xl p-5 sm:p-6 shadow-2xl space-y-5 animate-scale-up">
|
||||||
|
|
||||||
|
<!-- Product Header Banner -->
|
||||||
|
<div class="bg-slate-950 p-4 rounded-2xl border border-slate-800 flex items-center justify-between gap-3">
|
||||||
|
<div class="flex items-center gap-3.5 min-w-0">
|
||||||
|
<div id="action-product-img" class="w-14 h-14 rounded-xl bg-slate-800 flex items-center justify-center text-teal-400 shrink-0 overflow-hidden">
|
||||||
|
<i data-lucide="package" class="w-7 h-7"></i>
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<h3 id="action-product-name" class="font-extrabold text-white text-base sm:text-lg truncate">Selected Product</h3>
|
||||||
|
<p id="action-product-sub" class="text-xs sm:text-sm text-slate-400 mt-0.5">Current Stock: 250g • 1 Pack = 250g</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</main>
|
<button onclick="cancelActionAndReturnToManage()" class="text-slate-400 hover:text-white p-2 rounded-xl bg-slate-800/80 touch-manipulation">
|
||||||
|
<i data-lucide="x" class="w-5 h-5"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ADD / EDIT ITEM MODAL (NO EXPIRY REQUIRED) -->
|
<!-- Choose Action: Add or Remove -->
|
||||||
|
<div class="space-y-2.5">
|
||||||
|
<label class="text-xs font-bold text-slate-400 uppercase tracking-wider block">Choose Action:</label>
|
||||||
|
<div class="grid grid-cols-2 gap-3 sm:gap-4">
|
||||||
|
<button
|
||||||
|
id="action-btn-add"
|
||||||
|
onclick="setActionType('add')"
|
||||||
|
class="p-4 sm:p-5 rounded-2xl border-2 text-center font-extrabold transition-all active:scale-95 touch-manipulation flex flex-col items-center justify-center gap-2 min-h-[84px] bg-slate-950 border-slate-800 text-slate-300 hover:border-emerald-500/60 hover:text-white"
|
||||||
|
>
|
||||||
|
<i data-lucide="plus-circle" class="w-7 h-7 text-emerald-400"></i>
|
||||||
|
<span class="text-sm sm:text-base">➕ Add to Stock</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="action-btn-remove"
|
||||||
|
onclick="setActionType('remove')"
|
||||||
|
class="p-4 sm:p-5 rounded-2xl border-2 text-center font-extrabold transition-all active:scale-95 touch-manipulation flex flex-col items-center justify-center gap-2 min-h-[84px] bg-slate-950 border-slate-800 text-slate-300 hover:border-rose-500/60 hover:text-white"
|
||||||
|
>
|
||||||
|
<i data-lucide="minus-circle" class="w-7 h-7 text-rose-400"></i>
|
||||||
|
<span class="text-sm sm:text-base">➖ Remove from Stock</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Choose Mode: Whole Packet vs Specific Grams -->
|
||||||
|
<div id="action-qty-section" class="hidden space-y-4 pt-2 border-t border-slate-800">
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-2 p-1.5 bg-slate-950 rounded-2xl border border-slate-800">
|
||||||
|
<button
|
||||||
|
id="btn-mode-packet"
|
||||||
|
onclick="setActionQtyMode('packet')"
|
||||||
|
class="py-3 px-3 rounded-xl font-bold text-xs sm:text-sm transition bg-teal-500 text-white shadow-md touch-manipulation flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<i data-lucide="package" class="w-4 h-4"></i>
|
||||||
|
<span>Whole Packet(s)</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="btn-mode-amount"
|
||||||
|
onclick="setActionQtyMode('amount')"
|
||||||
|
class="py-3 px-3 rounded-xl font-bold text-xs sm:text-sm transition text-slate-300 hover:text-white touch-manipulation flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<i data-lucide="scale" class="w-4 h-4"></i>
|
||||||
|
<span>Specific Grams / Amount</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Packet Controls -->
|
||||||
|
<div id="controls-packet" class="bg-slate-950 p-4 sm:p-5 rounded-2xl border border-slate-800 space-y-4 text-center">
|
||||||
|
<div class="text-xs sm:text-sm text-slate-400 font-medium">
|
||||||
|
1 Full Packet = <strong id="action-pack-size-label" class="text-teal-300 font-bold">250g</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center gap-4">
|
||||||
|
<button onclick="changeActionPackCount(-1)" class="w-12 h-12 rounded-2xl bg-slate-800 hover:bg-slate-700 text-white font-extrabold text-2xl flex items-center justify-center active:scale-95 touch-manipulation border border-slate-700">-</button>
|
||||||
|
<div class="text-3xl sm:text-4xl font-black text-white min-w-[90px]">
|
||||||
|
<span id="action-pack-count-val">1</span> <span class="text-sm font-normal text-slate-400">pack</span>
|
||||||
|
</div>
|
||||||
|
<button onclick="changeActionPackCount(1)" class="w-12 h-12 rounded-2xl bg-slate-800 hover:bg-slate-700 text-white font-extrabold text-2xl flex items-center justify-center active:scale-95 touch-manipulation border border-slate-700">+</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center gap-2">
|
||||||
|
<button onclick="setActionPackCount(1)" class="px-3.5 py-1.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs text-slate-200 border border-slate-700 font-bold">1 Pack</button>
|
||||||
|
<button onclick="setActionPackCount(2)" class="px-3.5 py-1.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs text-slate-200 border border-slate-700 font-bold">2 Packs</button>
|
||||||
|
<button onclick="setActionPackCount(3)" class="px-3.5 py-1.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs text-slate-200 border border-slate-700 font-bold">3 Packs</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Specific Grams Controls (Slider & Quick Chips) -->
|
||||||
|
<div id="controls-amount" class="hidden bg-slate-950 p-4 sm:p-5 rounded-2xl border border-slate-800 space-y-4 text-center">
|
||||||
|
<div>
|
||||||
|
<div class="text-3xl sm:text-4xl font-black text-white flex items-center justify-center gap-1.5">
|
||||||
|
<span id="action-amount-val">150</span>
|
||||||
|
<span id="action-amount-unit" class="text-base font-normal text-teal-400">g</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-slate-400 mt-1 font-medium">New remaining amount in stock</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="action-amount-slider"
|
||||||
|
min="0"
|
||||||
|
max="500"
|
||||||
|
step="5"
|
||||||
|
value="150"
|
||||||
|
oninput="onActionAmountSlider(this.value)"
|
||||||
|
class="w-full accent-teal-500 cursor-pointer h-3.5 bg-slate-800 rounded-xl"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-3 sm:grid-cols-6 gap-2">
|
||||||
|
<button onclick="applyActionDelta(-10)" class="py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs sm:text-sm text-slate-200 border border-slate-700 font-bold touch-manipulation">-10</button>
|
||||||
|
<button onclick="applyActionDelta(-25)" class="py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs sm:text-sm text-slate-200 border border-slate-700 font-bold touch-manipulation">-25</button>
|
||||||
|
<button onclick="applyActionDelta(-50)" class="py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs sm:text-sm text-slate-200 border border-slate-700 font-bold touch-manipulation">-50</button>
|
||||||
|
<button onclick="applyActionDelta(-100)" class="py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs sm:text-sm text-slate-200 border border-slate-700 font-bold touch-manipulation">-100</button>
|
||||||
|
<button onclick="setActionAmountRatio(0.5)" class="py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs sm:text-sm text-slate-200 border border-slate-700 font-bold touch-manipulation">Half</button>
|
||||||
|
<button onclick="setActionAmountRatio(0)" class="py-2.5 rounded-xl bg-rose-950/40 hover:bg-rose-900 text-xs sm:text-sm text-rose-300 border border-rose-800 font-bold touch-manipulation">0 (Empty)</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirm & Finish (Returns back to /manage) -->
|
||||||
|
<div class="flex items-center gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
onclick="cancelActionAndReturnToManage()"
|
||||||
|
class="px-5 py-4 rounded-2xl bg-slate-800 hover:bg-slate-700 text-slate-300 font-bold text-sm touch-manipulation"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="action-confirm-btn"
|
||||||
|
onclick="confirmAndFinishAction()"
|
||||||
|
class="flex-1 py-4 rounded-2xl bg-teal-500 hover:bg-teal-400 text-white font-black text-base sm:text-lg shadow-xl shadow-teal-500/25 active:scale-95 transition-all touch-manipulation flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<i data-lucide="check" class="w-6 h-6"></i>
|
||||||
|
<span id="action-confirm-label">Confirm & Done</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ADD / EDIT ITEM MODAL -->
|
||||||
<div id="item-modal" class="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm hidden flex items-center justify-center p-3 sm:p-4 overflow-y-auto">
|
<div id="item-modal" class="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm hidden flex items-center justify-center p-3 sm:p-4 overflow-y-auto">
|
||||||
<div class="bg-slate-900 border border-slate-800 rounded-3xl max-w-lg w-full p-5 sm:p-6 shadow-2xl space-y-4 my-8 animate-scale-up">
|
<div class="bg-slate-900 border border-slate-800 rounded-3xl max-w-lg w-full p-5 sm:p-6 shadow-2xl space-y-4 my-8 animate-scale-up">
|
||||||
|
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 id="item-modal-title" class="text-base sm:text-lg font-bold text-white">Add Product</h3>
|
<h3 id="item-modal-title" class="text-base sm:text-lg font-bold text-white">Add Product Manually</h3>
|
||||||
<p class="text-xs text-slate-400">Scan or enter product details</p>
|
<p class="text-xs text-slate-400">Enter product name and package size</p>
|
||||||
</div>
|
</div>
|
||||||
<button onclick="closeItemModal()" class="text-slate-400 hover:text-white p-2 rounded-xl bg-slate-800/60 touch-manipulation">
|
<button onclick="closeItemModal()" class="text-slate-400 hover:text-white p-2 rounded-xl bg-slate-800/60 touch-manipulation">
|
||||||
<i data-lucide="x" class="w-5 h-5"></i>
|
<i data-lucide="x" class="w-5 h-5"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Barcode Search strip -->
|
<!-- Barcode Lookup row -->
|
||||||
<div class="bg-slate-950 p-2.5 sm:p-3 rounded-2xl border border-slate-800 flex items-center gap-2">
|
<div class="bg-slate-950 p-2.5 sm:p-3 rounded-2xl border border-slate-800 flex items-center gap-2">
|
||||||
<div class="relative flex-1">
|
<div class="relative flex-1">
|
||||||
<i data-lucide="barcode" class="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>
|
<i data-lucide="barcode" class="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>
|
||||||
@@ -415,22 +414,22 @@
|
|||||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2.5">
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2.5">
|
||||||
<div class="sm:col-span-2">
|
<div class="sm:col-span-2">
|
||||||
<label class="block text-slate-300 font-semibold mb-1">Product Name *</label>
|
<label class="block text-slate-300 font-semibold mb-1">Product Name *</label>
|
||||||
<input type="text" id="modal-name" required placeholder="e.g. Frozen Peas" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
<input type="text" id="modal-name" required placeholder="e.g. Frozen Berries" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-slate-300 font-semibold mb-1">Brand</label>
|
<label class="block text-slate-300 font-semibold mb-1">Brand</label>
|
||||||
<input type="text" id="modal-brand" placeholder="e.g. Iglo" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
<input type="text" id="modal-brand" placeholder="e.g. Bio" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-3 gap-2.5">
|
<div class="grid grid-cols-3 gap-2.5">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-slate-300 font-semibold mb-1">Current Amount *</label>
|
<label class="block text-slate-300 font-semibold mb-1">Current Amount *</label>
|
||||||
<input type="number" id="modal-quantity" step="any" min="0" required placeholder="450" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
<input type="number" id="modal-quantity" step="any" min="0" required placeholder="500" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-slate-300 font-semibold mb-1">1 Full Pack Size *</label>
|
<label class="block text-slate-300 font-semibold mb-1">1 Full Pack Size *</label>
|
||||||
<input type="number" id="modal-initial-quantity" step="any" min="0" required placeholder="450" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
<input type="number" id="modal-initial-quantity" step="any" min="0" required placeholder="500" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-slate-300 font-semibold mb-1">Unit</label>
|
<label class="block text-slate-300 font-semibold mb-1">Unit</label>
|
||||||
@@ -455,7 +454,7 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-slate-300 font-semibold mb-1">Notes (Optional)</label>
|
<label class="block text-slate-300 font-semibold mb-1">Notes (Optional)</label>
|
||||||
<input type="text" id="modal-notes" placeholder="e.g. In top freezer basket" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
<input type="text" id="modal-notes" placeholder="e.g. In freezer top basket" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-end gap-3 pt-3 border-t border-slate-800">
|
<div class="flex items-center justify-end gap-3 pt-3 border-t border-slate-800">
|
||||||
@@ -475,14 +474,14 @@
|
|||||||
<div class="w-8 h-8 rounded-xl bg-teal-500/10 text-teal-400 flex items-center justify-center">
|
<div class="w-8 h-8 rounded-xl bg-teal-500/10 text-teal-400 flex items-center justify-center">
|
||||||
<i data-lucide="scan-barcode" class="w-5 h-5"></i>
|
<i data-lucide="scan-barcode" class="w-5 h-5"></i>
|
||||||
</div>
|
</div>
|
||||||
<span class="font-extrabold text-white text-base">Barcode Scanner</span>
|
<span class="font-extrabold text-white text-base">Tablet Barcode Scanner</span>
|
||||||
</div>
|
</div>
|
||||||
<button onclick="closeScannerModal()" class="text-slate-400 hover:text-white p-2 rounded-xl bg-slate-800/60 touch-manipulation">
|
<button onclick="closeScannerModal()" class="text-slate-400 hover:text-white p-2 rounded-xl bg-slate-800/60 touch-manipulation">
|
||||||
<i data-lucide="x" class="w-5 h-5"></i>
|
<i data-lucide="x" class="w-5 h-5"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Camera Preview -->
|
<!-- Camera Viewfinder -->
|
||||||
<div class="relative bg-black flex items-center justify-center min-h-[300px] sm:min-h-[360px] w-full overflow-hidden">
|
<div class="relative bg-black flex items-center justify-center min-h-[300px] sm:min-h-[360px] w-full overflow-hidden">
|
||||||
<div id="barcode-reader" class="w-full"></div>
|
<div id="barcode-reader" class="w-full"></div>
|
||||||
<div id="scanner-overlay" class="absolute inset-0 pointer-events-none border-2 border-teal-400/50 rounded-3xl m-8 sm:m-12 flex items-center justify-center">
|
<div id="scanner-overlay" class="absolute inset-0 pointer-events-none border-2 border-teal-400/50 rounded-3xl m-8 sm:m-12 flex items-center justify-center">
|
||||||
@@ -490,10 +489,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Scanner Status & Manual Input -->
|
<!-- Status & Manual fallback -->
|
||||||
<div class="p-4 bg-slate-900 space-y-3">
|
<div class="p-4 bg-slate-900 space-y-3">
|
||||||
<div id="scanner-status" class="text-xs sm:text-sm text-center text-slate-400 font-medium">
|
<div id="scanner-status" class="text-xs sm:text-sm text-center text-slate-400 font-medium">
|
||||||
Point camera at barcode
|
Hold product barcode in front of tablet camera
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user