feat: confezioni (conf) chiede dimensione singola confezione
- Nuovo campo package_unit in DB (migrazione automatica) - Form aggiungi/modifica: quando si seleziona 'conf', appare campo per specificare il contenuto della singola confezione (es. 300g, 2L) - Visualizzazione: '3 conf (da 300g)' in inventario, dettaglio, butta - formatQuantity aggiornato con supporto package_unit - API: salva/restituisce package_unit in tutti gli endpoint - Ricette e chat: contesto arricchito con info confezione - CSS: stili per il nuovo campo conf-size
This commit is contained in:
@@ -16,6 +16,10 @@ function getDB(): PDO {
|
|||||||
if ($isNew) {
|
if ($isNew) {
|
||||||
initializeDB($db);
|
initializeDB($db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run migrations
|
||||||
|
migrateDB($db);
|
||||||
|
|
||||||
return $db;
|
return $db;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,3 +68,12 @@ function initializeDB(PDO $db): void {
|
|||||||
CREATE INDEX IF NOT EXISTS idx_transactions_date ON transactions(created_at);
|
CREATE INDEX IF NOT EXISTS idx_transactions_date ON transactions(created_at);
|
||||||
");
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function migrateDB(PDO $db): void {
|
||||||
|
// Add package_unit column if missing
|
||||||
|
$cols = $db->query("PRAGMA table_info(products)")->fetchAll();
|
||||||
|
$colNames = array_column($cols, 'name');
|
||||||
|
if (!in_array('package_unit', $colNames)) {
|
||||||
|
$db->exec("ALTER TABLE products ADD COLUMN package_unit TEXT DEFAULT ''");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+31
-10
@@ -303,28 +303,28 @@ function saveProduct(PDO $db): void {
|
|||||||
// Update existing
|
// Update existing
|
||||||
$stmt = $db->prepare("
|
$stmt = $db->prepare("
|
||||||
UPDATE products SET name=?, brand=?, category=?, image_url=?, unit=?,
|
UPDATE products SET name=?, brand=?, category=?, image_url=?, unit=?,
|
||||||
default_quantity=?, notes=?, barcode=?, updated_at=CURRENT_TIMESTAMP
|
default_quantity=?, notes=?, barcode=?, package_unit=?, updated_at=CURRENT_TIMESTAMP
|
||||||
WHERE id=?
|
WHERE id=?
|
||||||
");
|
");
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
$input['name'], $input['brand'] ?? '', $input['category'] ?? '',
|
$input['name'], $input['brand'] ?? '', $input['category'] ?? '',
|
||||||
$input['image_url'] ?? '', $input['unit'] ?? 'pz',
|
$input['image_url'] ?? '', $input['unit'] ?? 'pz',
|
||||||
$input['default_quantity'] ?? 1, $input['notes'] ?? '',
|
$input['default_quantity'] ?? 1, $input['notes'] ?? '',
|
||||||
$input['barcode'] ?? null, $input['id']
|
$input['barcode'] ?? null, $input['package_unit'] ?? '', $input['id']
|
||||||
]);
|
]);
|
||||||
echo json_encode(['success' => true, 'id' => $input['id']]);
|
echo json_encode(['success' => true, 'id' => $input['id']]);
|
||||||
} else {
|
} else {
|
||||||
// Insert new
|
// Insert new
|
||||||
$stmt = $db->prepare("
|
$stmt = $db->prepare("
|
||||||
INSERT INTO products (barcode, name, brand, category, image_url, unit, default_quantity, notes)
|
INSERT INTO products (barcode, name, brand, category, image_url, unit, default_quantity, notes, package_unit)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
");
|
");
|
||||||
$barcode = !empty($input['barcode']) ? $input['barcode'] : null;
|
$barcode = !empty($input['barcode']) ? $input['barcode'] : null;
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
$barcode, $input['name'], $input['brand'] ?? '',
|
$barcode, $input['name'], $input['brand'] ?? '',
|
||||||
$input['category'] ?? '', $input['image_url'] ?? '',
|
$input['category'] ?? '', $input['image_url'] ?? '',
|
||||||
$input['unit'] ?? 'pz', $input['default_quantity'] ?? 1,
|
$input['unit'] ?? 'pz', $input['default_quantity'] ?? 1,
|
||||||
$input['notes'] ?? ''
|
$input['notes'] ?? '', $input['package_unit'] ?? ''
|
||||||
]);
|
]);
|
||||||
echo json_encode(['success' => true, 'id' => $db->lastInsertId()]);
|
echo json_encode(['success' => true, 'id' => $db->lastInsertId()]);
|
||||||
}
|
}
|
||||||
@@ -369,7 +369,7 @@ function searchProducts(PDO $db): void {
|
|||||||
function listInventory(PDO $db): void {
|
function listInventory(PDO $db): void {
|
||||||
$location = $_GET['location'] ?? '';
|
$location = $_GET['location'] ?? '';
|
||||||
$query = "
|
$query = "
|
||||||
SELECT i.*, p.name, p.brand, p.category, p.image_url, p.unit, p.barcode, p.default_quantity
|
SELECT i.*, p.name, p.brand, p.category, p.image_url, p.unit, p.barcode, p.default_quantity, p.package_unit
|
||||||
FROM inventory i
|
FROM inventory i
|
||||||
JOIN products p ON i.product_id = p.id
|
JOIN products p ON i.product_id = p.id
|
||||||
";
|
";
|
||||||
@@ -404,6 +404,14 @@ function addToInventory(PDO $db): void {
|
|||||||
$stmt->execute([$unit, $quantity, $productId]);
|
$stmt->execute([$unit, $quantity, $productId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update package info if conf
|
||||||
|
$packageUnit = $input['package_unit'] ?? null;
|
||||||
|
$packageSize = $input['package_size'] ?? null;
|
||||||
|
if ($packageUnit !== null) {
|
||||||
|
$stmt = $db->prepare("UPDATE products SET package_unit = ?, default_quantity = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?");
|
||||||
|
$stmt->execute([$packageUnit, $packageSize ?: 0, $productId]);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if product already exists in this location
|
// Check if product already exists in this location
|
||||||
$stmt = $db->prepare("SELECT id, quantity FROM inventory WHERE product_id = ? AND location = ?");
|
$stmt = $db->prepare("SELECT id, quantity FROM inventory WHERE product_id = ? AND location = ?");
|
||||||
$stmt->execute([$productId, $location]);
|
$stmt->execute([$productId, $location]);
|
||||||
@@ -553,6 +561,12 @@ function updateInventory(PDO $db): void {
|
|||||||
$stmt->execute([$input['unit'], $input['product_id']]);
|
$stmt->execute([$input['unit'], $input['product_id']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update package info if provided
|
||||||
|
if (isset($input['package_unit']) && isset($input['product_id'])) {
|
||||||
|
$stmt = $db->prepare("UPDATE products SET package_unit = ?, default_quantity = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?");
|
||||||
|
$stmt->execute([$input['package_unit'], $input['package_size'] ?? 0, $input['product_id']]);
|
||||||
|
}
|
||||||
|
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,7 +625,7 @@ function getStats(PDO $db): void {
|
|||||||
|
|
||||||
// Expiring soonest (next 4 items to expire)
|
// Expiring soonest (next 4 items to expire)
|
||||||
$expiring = $db->query("
|
$expiring = $db->query("
|
||||||
SELECT i.*, p.name, p.brand, p.category, p.unit
|
SELECT i.*, p.name, p.brand, p.category, p.unit, p.default_quantity, p.package_unit
|
||||||
FROM inventory i JOIN products p ON i.product_id = p.id
|
FROM inventory i JOIN products p ON i.product_id = p.id
|
||||||
WHERE i.expiry_date IS NOT NULL AND i.expiry_date >= date('now') AND i.quantity > 0
|
WHERE i.expiry_date IS NOT NULL AND i.expiry_date >= date('now') AND i.quantity > 0
|
||||||
ORDER BY i.expiry_date ASC
|
ORDER BY i.expiry_date ASC
|
||||||
@@ -620,7 +634,7 @@ function getStats(PDO $db): void {
|
|||||||
|
|
||||||
// Expired
|
// Expired
|
||||||
$expired = $db->query("
|
$expired = $db->query("
|
||||||
SELECT i.*, p.name, p.brand, p.category, p.unit
|
SELECT i.*, p.name, p.brand, p.category, p.unit, p.default_quantity, p.package_unit
|
||||||
FROM inventory i JOIN products p ON i.product_id = p.id
|
FROM inventory i JOIN products p ON i.product_id = p.id
|
||||||
WHERE i.expiry_date IS NOT NULL AND i.expiry_date < date('now')
|
WHERE i.expiry_date IS NOT NULL AND i.expiry_date < date('now')
|
||||||
ORDER BY i.expiry_date ASC
|
ORDER BY i.expiry_date ASC
|
||||||
@@ -847,7 +861,7 @@ function geminiChat(PDO $db): void {
|
|||||||
|
|
||||||
// Fetch inventory context
|
// Fetch inventory context
|
||||||
$stmt = $db->query("
|
$stmt = $db->query("
|
||||||
SELECT p.name, p.brand, p.category, i.quantity, p.unit, i.location, i.expiry_date,
|
SELECT p.name, p.brand, p.category, i.quantity, p.unit, p.default_quantity, p.package_unit, i.location, i.expiry_date,
|
||||||
CASE WHEN i.expiry_date IS NOT NULL THEN julianday(i.expiry_date) - julianday('now') ELSE 999 END AS days_left
|
CASE WHEN i.expiry_date IS NOT NULL THEN julianday(i.expiry_date) - julianday('now') ELSE 999 END AS days_left
|
||||||
FROM inventory i
|
FROM inventory i
|
||||||
JOIN products p ON p.id = i.product_id
|
JOIN products p ON p.id = i.product_id
|
||||||
@@ -861,6 +875,9 @@ function geminiChat(PDO $db): void {
|
|||||||
$line = "- {$item['name']}";
|
$line = "- {$item['name']}";
|
||||||
if ($item['brand']) $line .= " ({$item['brand']})";
|
if ($item['brand']) $line .= " ({$item['brand']})";
|
||||||
$line .= ": {$item['quantity']} {$item['unit']}";
|
$line .= ": {$item['quantity']} {$item['unit']}";
|
||||||
|
if ($item['unit'] === 'conf' && !empty($item['package_unit']) && $item['default_quantity'] > 0) {
|
||||||
|
$line .= " (da {$item['default_quantity']} {$item['package_unit']} ciascuna)";
|
||||||
|
}
|
||||||
if ($item['expiry_date']) {
|
if ($item['expiry_date']) {
|
||||||
$daysLeft = intval($item['days_left']);
|
$daysLeft = intval($item['days_left']);
|
||||||
if ($daysLeft < 0) {
|
if ($daysLeft < 0) {
|
||||||
@@ -1004,7 +1021,7 @@ function generateRecipe(PDO $db): void {
|
|||||||
|
|
||||||
// Fetch all inventory items with expiry info
|
// Fetch all inventory items with expiry info
|
||||||
$stmt = $db->query("
|
$stmt = $db->query("
|
||||||
SELECT p.id AS product_id, p.name, p.brand, p.category, i.quantity, p.unit, i.location, i.expiry_date,
|
SELECT p.id AS product_id, p.name, p.brand, p.category, i.quantity, p.unit, p.default_quantity, p.package_unit, i.location, i.expiry_date,
|
||||||
CASE WHEN i.expiry_date IS NOT NULL THEN julianday(i.expiry_date) - julianday('now') ELSE 999 END AS days_left
|
CASE WHEN i.expiry_date IS NOT NULL THEN julianday(i.expiry_date) - julianday('now') ELSE 999 END AS days_left
|
||||||
FROM inventory i
|
FROM inventory i
|
||||||
JOIN products p ON p.id = i.product_id
|
JOIN products p ON p.id = i.product_id
|
||||||
@@ -1024,6 +1041,9 @@ function generateRecipe(PDO $db): void {
|
|||||||
$line = "- {$item['name']}";
|
$line = "- {$item['name']}";
|
||||||
if ($item['brand']) $line .= " ({$item['brand']})";
|
if ($item['brand']) $line .= " ({$item['brand']})";
|
||||||
$line .= ": {$item['quantity']} {$item['unit']}";
|
$line .= ": {$item['quantity']} {$item['unit']}";
|
||||||
|
if ($item['unit'] === 'conf' && !empty($item['package_unit']) && $item['default_quantity'] > 0) {
|
||||||
|
$line .= " (da {$item['default_quantity']} {$item['package_unit']} ciascuna, totale: " . ($item['quantity'] * $item['default_quantity']) . " {$item['package_unit']})";
|
||||||
|
}
|
||||||
if ($item['expiry_date']) {
|
if ($item['expiry_date']) {
|
||||||
$daysLeft = intval($item['days_left']);
|
$daysLeft = intval($item['days_left']);
|
||||||
if ($daysLeft < 0) {
|
if ($daysLeft < 0) {
|
||||||
@@ -1210,6 +1230,7 @@ PROMPT;
|
|||||||
$ing['inventory_unit'] = $bestMatch['unit'];
|
$ing['inventory_unit'] = $bestMatch['unit'];
|
||||||
$ing['inventory_qty'] = (float)$bestMatch['quantity'];
|
$ing['inventory_qty'] = (float)$bestMatch['quantity'];
|
||||||
$ing['default_quantity'] = (float)($bestMatch['default_quantity'] ?? 0);
|
$ing['default_quantity'] = (float)($bestMatch['default_quantity'] ?? 0);
|
||||||
|
$ing['package_unit'] = $bestMatch['package_unit'] ?? '';
|
||||||
$ing['available_qty'] = $bestMatch['quantity'] . ' ' . $bestMatch['unit'];
|
$ing['available_qty'] = $bestMatch['quantity'] . ' ' . $bestMatch['unit'];
|
||||||
if (!empty($bestMatch['brand'])) {
|
if (!empty($bestMatch['brand'])) {
|
||||||
$ing['brand'] = $bestMatch['brand'];
|
$ing['brand'] = $bestMatch['brand'];
|
||||||
|
|||||||
@@ -1919,6 +1919,45 @@ body {
|
|||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== CONF PACKAGE SIZE ROW ===== */
|
||||||
|
.conf-size-row {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #f0f9ff;
|
||||||
|
border: 1.5px solid #bae6fd;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conf-size-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: #0369a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conf-size-inputs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conf-size-input {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conf-size-unit {
|
||||||
|
width: 65px;
|
||||||
|
min-width: 65px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conf-size-info {
|
||||||
|
font-size: 0.75em;
|
||||||
|
color: var(--text-light);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== EXPIRY INPUT + CAMERA ROW ===== */
|
/* ===== EXPIRY INPUT + CAMERA ROW ===== */
|
||||||
.expiry-input-row {
|
.expiry-input-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
+102
-23
@@ -572,7 +572,7 @@ async function loadDashboard() {
|
|||||||
else if (days <= 7) { badgeText = `${days} giorni`; badgeClass = 'expiring'; }
|
else if (days <= 7) { badgeText = `${days} giorni`; badgeClass = 'expiring'; }
|
||||||
else if (days <= 30) { badgeText = `${days}g`; badgeClass = 'expiring-soon'; }
|
else if (days <= 30) { badgeText = `${days}g`; badgeClass = 'expiring-soon'; }
|
||||||
else { const m = Math.round(days/30); badgeText = m <= 1 ? `${days}g` : `~${m} mesi`; badgeClass = 'expiring-later'; }
|
else { const m = Math.round(days/30); badgeText = m <= 1 ? `${days}g` : `~${m} mesi`; badgeClass = 'expiring-later'; }
|
||||||
const qtyDisplay = formatQuantity(item.quantity, item.unit);
|
const qtyDisplay = formatQuantity(item.quantity, item.unit, item.default_quantity, item.package_unit);
|
||||||
return `
|
return `
|
||||||
<div class="alert-item alert-item-clickable" onclick="showAlertItemDetail(${item.id}, ${item.product_id})">
|
<div class="alert-item alert-item-clickable" onclick="showAlertItemDetail(${item.id}, ${item.product_id})">
|
||||||
<div class="alert-item-info">
|
<div class="alert-item-info">
|
||||||
@@ -602,7 +602,7 @@ async function loadDashboard() {
|
|||||||
else daysText = `Da ${days}g`;
|
else daysText = `Da ${days}g`;
|
||||||
const safety = getExpiredSafety(item, days);
|
const safety = getExpiredSafety(item, days);
|
||||||
const locIcon = item.location === 'freezer' ? '❄️' : item.location === 'frigo' ? '🧊' : '';
|
const locIcon = item.location === 'freezer' ? '❄️' : item.location === 'frigo' ? '🧊' : '';
|
||||||
const qtyDisplayExp = formatQuantity(item.quantity, item.unit);
|
const qtyDisplayExp = formatQuantity(item.quantity, item.unit, item.default_quantity, item.package_unit);
|
||||||
return `
|
return `
|
||||||
<div class="alert-item expired-item alert-item-clickable" onclick="showAlertItemDetail(${item.id}, ${item.product_id})">
|
<div class="alert-item expired-item alert-item-clickable" onclick="showAlertItemDetail(${item.id}, ${item.product_id})">
|
||||||
<div class="alert-item-info">
|
<div class="alert-item-info">
|
||||||
@@ -676,7 +676,7 @@ async function loadReviewItems() {
|
|||||||
section.style.display = 'block';
|
section.style.display = 'block';
|
||||||
list.innerHTML = suspicious.map(item => {
|
list.innerHTML = suspicious.map(item => {
|
||||||
const catIcon = CATEGORY_ICONS[mapToLocalCategory(item.category, item.name)] || '📦';
|
const catIcon = CATEGORY_ICONS[mapToLocalCategory(item.category, item.name)] || '📦';
|
||||||
const qtyDisplay = formatQuantity(item.quantity, item.unit);
|
const qtyDisplay = formatQuantity(item.quantity, item.unit, item.default_quantity, item.package_unit);
|
||||||
const locInfo = LOCATIONS[item.location] || { icon: '📦', label: item.location };
|
const locInfo = LOCATIONS[item.location] || { icon: '📦', label: item.location };
|
||||||
const t = QTY_THRESHOLDS[item.unit] || QTY_THRESHOLDS['pz'];
|
const t = QTY_THRESHOLDS[item.unit] || QTY_THRESHOLDS['pz'];
|
||||||
const isTooSmall = parseFloat(item.quantity) < t.min;
|
const isTooSmall = parseFloat(item.quantity) < t.min;
|
||||||
@@ -763,7 +763,7 @@ function renderDashItem(item) {
|
|||||||
const days = daysUntilExpiry(item.expiry_date);
|
const days = daysUntilExpiry(item.expiry_date);
|
||||||
const isExpired = days < 0;
|
const isExpired = days < 0;
|
||||||
const isExpiring = !isExpired && days <= 7;
|
const isExpiring = !isExpired && days <= 7;
|
||||||
const qtyDisplay = formatQuantity(item.quantity, item.unit);
|
const qtyDisplay = formatQuantity(item.quantity, item.unit, item.default_quantity, item.package_unit);
|
||||||
|
|
||||||
let expiryLabel = '';
|
let expiryLabel = '';
|
||||||
if (item.expiry_date) {
|
if (item.expiry_date) {
|
||||||
@@ -806,15 +806,21 @@ function showAlertItemDetail(inventoryId, productId) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatQuantity(qty, unit) {
|
function formatQuantity(qty, unit, defaultQty, packageUnit) {
|
||||||
if (!qty && qty !== 0) return '';
|
if (!qty && qty !== 0) return '';
|
||||||
const n = parseFloat(qty);
|
const n = parseFloat(qty);
|
||||||
const unitLabels = { 'pz': 'pz', 'kg': 'kg', 'g': 'g', 'l': 'L', 'ml': 'ml', 'conf': 'conf' };
|
const unitLabels = { 'pz': 'pz', 'kg': 'kg', 'g': 'g', 'l': 'L', 'ml': 'ml', 'conf': 'conf' };
|
||||||
const label = unitLabels[unit] || unit || 'pz';
|
const label = unitLabels[unit] || unit || 'pz';
|
||||||
if (n === Math.floor(n)) return `${Math.floor(n)} ${label}`;
|
let result;
|
||||||
// For pz/conf show whole number
|
if (n === Math.floor(n)) result = `${Math.floor(n)} ${label}`;
|
||||||
if (unit === 'pz' || unit === 'conf') return `${Math.round(n)} ${label}`;
|
else if (unit === 'pz' || unit === 'conf') result = `${Math.round(n)} ${label}`;
|
||||||
return `${n.toFixed(1)} ${label}`;
|
else result = `${n.toFixed(1)} ${label}`;
|
||||||
|
// Add package info for conf
|
||||||
|
if (unit === 'conf' && packageUnit && defaultQty > 0) {
|
||||||
|
const pkgLabel = unitLabels[packageUnit] || packageUnit;
|
||||||
|
result += ` <span class="conf-size-info">(da ${defaultQty}${pkgLabel})</span>`;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show package fraction: only ¼, ½, ¾ when there's a partial package.
|
// Show package fraction: only ¼, ½, ¾ when there's a partial package.
|
||||||
@@ -856,7 +862,7 @@ function renderInventoryItem(item) {
|
|||||||
const days = daysUntilExpiry(item.expiry_date);
|
const days = daysUntilExpiry(item.expiry_date);
|
||||||
const isExpired = days < 0;
|
const isExpired = days < 0;
|
||||||
const isExpiring = !isExpired && days <= 7;
|
const isExpiring = !isExpired && days <= 7;
|
||||||
const qtyDisplay = formatQuantity(item.quantity, item.unit);
|
const qtyDisplay = formatQuantity(item.quantity, item.unit, item.default_quantity, item.package_unit);
|
||||||
const pkgFrac = formatPackageFraction(item.quantity, item.default_quantity);
|
const pkgFrac = formatPackageFraction(item.quantity, item.default_quantity);
|
||||||
|
|
||||||
let expiryBadge = '';
|
let expiryBadge = '';
|
||||||
@@ -951,7 +957,7 @@ function showItemDetail(inventoryId, productId) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-detail-row">
|
<div class="modal-detail-row">
|
||||||
<span class="modal-detail-label">📦 Quantità</span>
|
<span class="modal-detail-label">📦 Quantità</span>
|
||||||
<span class="modal-detail-value">${item.quantity} ${item.unit}</span>
|
<span class="modal-detail-value">${formatQuantity(item.quantity, item.unit, item.default_quantity, item.package_unit)}</span>
|
||||||
</div>
|
</div>
|
||||||
${item.expiry_date ? `
|
${item.expiry_date ? `
|
||||||
<div class="modal-detail-row">
|
<div class="modal-detail-row">
|
||||||
@@ -1032,6 +1038,10 @@ function editInventoryItem(id) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isConf = (item.unit || 'pz') === 'conf';
|
||||||
|
const confSizeVal = (isConf && item.default_quantity > 0) ? item.default_quantity : '';
|
||||||
|
const confUnitVal = (isConf && item.package_unit) ? item.package_unit : 'g';
|
||||||
|
|
||||||
// Rebuild modal content for editing (don't close and reopen - just replace content)
|
// Rebuild modal content for editing (don't close and reopen - just replace content)
|
||||||
document.getElementById('modal-content').innerHTML = `
|
document.getElementById('modal-content').innerHTML = `
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -1049,10 +1059,19 @@ function editInventoryItem(id) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>📏 Unità di misura</label>
|
<label>📏 Unità di misura</label>
|
||||||
<select id="edit-unit" class="form-input">
|
<select id="edit-unit" class="form-input" onchange="onEditUnitChange()">
|
||||||
${['pz','g','kg','ml','l','conf'].map(u => `<option value="${u}" ${(item.unit||'pz') === u ? 'selected' : ''}>${u === 'pz' ? 'pz (pezzi)' : u === 'g' ? 'g (grammi)' : u === 'kg' ? 'kg (chilogrammi)' : u === 'ml' ? 'ml (millilitri)' : u === 'l' ? 'L (litri)' : u === 'conf' ? 'conf (confezioni)' : u}</option>`).join('')}
|
${['pz','g','kg','ml','l','conf'].map(u => `<option value="${u}" ${(item.unit||'pz') === u ? 'selected' : ''}>${u === 'pz' ? 'pz (pezzi)' : u === 'g' ? 'g (grammi)' : u === 'kg' ? 'kg (chilogrammi)' : u === 'ml' ? 'ml (millilitri)' : u === 'l' ? 'L (litri)' : u === 'conf' ? 'conf (confezioni)' : u}</option>`).join('')}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group" id="edit-conf-size-group" style="display:${isConf ? 'block' : 'none'}">
|
||||||
|
<label>📦 Ogni confezione contiene:</label>
|
||||||
|
<div class="conf-size-inputs">
|
||||||
|
<input type="number" id="edit-conf-size" class="form-input conf-size-input" min="1" step="any" value="${confSizeVal}" placeholder="es. 300">
|
||||||
|
<select id="edit-conf-unit" class="form-input conf-size-unit">
|
||||||
|
${['g','kg','ml','l'].map(u => `<option value="${u}" ${confUnitVal === u ? 'selected' : ''}>${u === 'l' ? 'L' : u}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>📍 Posizione</label>
|
<label>📍 Posizione</label>
|
||||||
<div class="location-selector">
|
<div class="location-selector">
|
||||||
@@ -1073,6 +1092,12 @@ function editInventoryItem(id) {
|
|||||||
document.getElementById('modal-overlay').style.display = 'flex';
|
document.getElementById('modal-overlay').style.display = 'flex';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onEditUnitChange() {
|
||||||
|
const unit = document.getElementById('edit-unit').value;
|
||||||
|
const confGroup = document.getElementById('edit-conf-size-group');
|
||||||
|
if (confGroup) confGroup.style.display = unit === 'conf' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
async function submitEditInventory(e, id, productId) {
|
async function submitEditInventory(e, id, productId) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const qty = parseFloat(document.getElementById('edit-qty').value);
|
const qty = parseFloat(document.getElementById('edit-qty').value);
|
||||||
@@ -1080,7 +1105,19 @@ async function submitEditInventory(e, id, productId) {
|
|||||||
const expiry = document.getElementById('edit-expiry').value || null;
|
const expiry = document.getElementById('edit-expiry').value || null;
|
||||||
const unit = document.getElementById('edit-unit').value;
|
const unit = document.getElementById('edit-unit').value;
|
||||||
|
|
||||||
await api('inventory_update', {}, 'POST', { id, quantity: qty, location: loc, expiry_date: expiry, unit, product_id: productId });
|
const payload = { id, quantity: qty, location: loc, expiry_date: expiry, unit, product_id: productId };
|
||||||
|
|
||||||
|
// Add package info if conf
|
||||||
|
if (unit === 'conf') {
|
||||||
|
payload.package_unit = document.getElementById('edit-conf-unit')?.value || '';
|
||||||
|
payload.package_size = parseFloat(document.getElementById('edit-conf-size')?.value) || 0;
|
||||||
|
} else {
|
||||||
|
// Clear package info if not conf
|
||||||
|
payload.package_unit = '';
|
||||||
|
payload.package_size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
await api('inventory_update', {}, 'POST', payload);
|
||||||
closeModal();
|
closeModal();
|
||||||
showToast('Aggiornato!', 'success');
|
showToast('Aggiornato!', 'success');
|
||||||
refreshCurrentPage();
|
refreshCurrentPage();
|
||||||
@@ -1608,17 +1645,25 @@ function onCategoryChange(fromAutoDetect = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onPfUnitChange() {
|
||||||
|
const unit = document.getElementById('pf-unit').value;
|
||||||
|
const confRow = document.getElementById('pf-conf-size-row');
|
||||||
|
if (confRow) confRow.style.display = unit === 'conf' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
async function submitProduct(e) {
|
async function submitProduct(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
showLoading(true);
|
showLoading(true);
|
||||||
|
|
||||||
|
const pfUnit = document.getElementById('pf-unit').value;
|
||||||
const productData = {
|
const productData = {
|
||||||
id: document.getElementById('pf-id').value || null,
|
id: document.getElementById('pf-id').value || null,
|
||||||
name: document.getElementById('pf-name').value,
|
name: document.getElementById('pf-name').value,
|
||||||
brand: document.getElementById('pf-brand').value,
|
brand: document.getElementById('pf-brand').value,
|
||||||
category: document.getElementById('pf-category').value,
|
category: document.getElementById('pf-category').value,
|
||||||
unit: document.getElementById('pf-unit').value,
|
unit: pfUnit,
|
||||||
default_quantity: parseFloat(document.getElementById('pf-defqty').value) || 1,
|
default_quantity: pfUnit === 'conf' ? (parseFloat(document.getElementById('pf-conf-size')?.value) || 1) : (parseFloat(document.getElementById('pf-defqty').value) || 1),
|
||||||
|
package_unit: pfUnit === 'conf' ? (document.getElementById('pf-conf-unit')?.value || '') : '',
|
||||||
notes: document.getElementById('pf-notes').value,
|
notes: document.getElementById('pf-notes').value,
|
||||||
barcode: document.getElementById('pf-barcode').value || null,
|
barcode: document.getElementById('pf-barcode').value || null,
|
||||||
image_url: document.getElementById('pf-image').value || '',
|
image_url: document.getElementById('pf-image').value || '',
|
||||||
@@ -1811,9 +1856,10 @@ function showProductAction() {
|
|||||||
let totalQty = 0;
|
let totalQty = 0;
|
||||||
const unit = inventoryItems[0].unit || 'pz';
|
const unit = inventoryItems[0].unit || 'pz';
|
||||||
const defQty = inventoryItems[0].default_quantity || 0;
|
const defQty = inventoryItems[0].default_quantity || 0;
|
||||||
|
const pkgUnit = inventoryItems[0].package_unit || '';
|
||||||
const invHtml = inventoryItems.map(inv => {
|
const invHtml = inventoryItems.map(inv => {
|
||||||
const locInfo = LOCATIONS[inv.location] || { icon: '📦', label: inv.location };
|
const locInfo = LOCATIONS[inv.location] || { icon: '📦', label: inv.location };
|
||||||
const qtyStr = formatQuantity(inv.quantity, inv.unit);
|
const qtyStr = formatQuantity(inv.quantity, inv.unit, inv.default_quantity, inv.package_unit);
|
||||||
const pkgF = formatPackageFraction(inv.quantity, inv.default_quantity);
|
const pkgF = formatPackageFraction(inv.quantity, inv.default_quantity);
|
||||||
totalQty += parseFloat(inv.quantity);
|
totalQty += parseFloat(inv.quantity);
|
||||||
let expiryStr = '';
|
let expiryStr = '';
|
||||||
@@ -1827,7 +1873,7 @@ function showProductAction() {
|
|||||||
return `<div class="inv-status-item"><span>${locInfo.icon} ${locInfo.label}${expiryStr}</span><span class="inv-status-qty">${qtyStr}${pkgF ? ' ' + pkgF : ''}</span></div>`;
|
return `<div class="inv-status-item"><span>${locInfo.icon} ${locInfo.label}${expiryStr}</span><span class="inv-status-qty">${qtyStr}${pkgF ? ' ' + pkgF : ''}</span></div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
const totalStr = formatQuantity(totalQty, unit);
|
const totalStr = formatQuantity(totalQty, unit, defQty, pkgUnit);
|
||||||
const totalFrac = formatPackageFraction(totalQty, defQty);
|
const totalFrac = formatPackageFraction(totalQty, defQty);
|
||||||
|
|
||||||
statusBar.innerHTML = `
|
statusBar.innerHTML = `
|
||||||
@@ -1894,11 +1940,13 @@ function showThrowForm() {
|
|||||||
|
|
||||||
const totalQty = items.reduce((sum, i) => sum + parseFloat(i.quantity), 0);
|
const totalQty = items.reduce((sum, i) => sum + parseFloat(i.quantity), 0);
|
||||||
const unit = items[0].unit || 'pz';
|
const unit = items[0].unit || 'pz';
|
||||||
const qtyDisplay = formatQuantity(totalQty, unit);
|
const defQty = items[0].default_quantity || 0;
|
||||||
|
const pkgUnit = items[0].package_unit || '';
|
||||||
|
const qtyDisplay = formatQuantity(totalQty, unit, defQty, pkgUnit);
|
||||||
|
|
||||||
let locOptionsHtml = items.map(inv => {
|
let locOptionsHtml = items.map(inv => {
|
||||||
const locInfo = LOCATIONS[inv.location] || { icon: '📦', label: inv.location };
|
const locInfo = LOCATIONS[inv.location] || { icon: '📦', label: inv.location };
|
||||||
return `<div class="inv-status-item"><span>${locInfo.icon} ${locInfo.label}</span><span class="inv-status-qty">${formatQuantity(inv.quantity, inv.unit)}</span></div>`;
|
return `<div class="inv-status-item"><span>${locInfo.icon} ${locInfo.label}</span><span class="inv-status-qty">${formatQuantity(inv.quantity, inv.unit, inv.default_quantity, inv.package_unit)}</span></div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
document.getElementById('modal-content').innerHTML = `
|
document.getElementById('modal-content').innerHTML = `
|
||||||
@@ -1929,7 +1977,7 @@ function showThrowForm() {
|
|||||||
<div class="location-selector" id="throw-location-selector">
|
<div class="location-selector" id="throw-location-selector">
|
||||||
${items.map((inv, idx) => {
|
${items.map((inv, idx) => {
|
||||||
const locInfo = LOCATIONS[inv.location] || { icon: '📦', label: inv.location };
|
const locInfo = LOCATIONS[inv.location] || { icon: '📦', label: inv.location };
|
||||||
return `<button type="button" class="loc-btn ${idx === 0 ? 'active' : ''}" onclick="selectThrowLocation(this, '${inv.location}')">${locInfo.icon} ${locInfo.label} (${formatQuantity(inv.quantity, inv.unit)})</button>`;
|
return `<button type="button" class="loc-btn ${idx === 0 ? 'active' : ''}" onclick="selectThrowLocation(this, '${inv.location}')">${locInfo.icon} ${locInfo.label} (${formatQuantity(inv.quantity, inv.unit, inv.default_quantity, inv.package_unit)})</button>`;
|
||||||
}).join('')}
|
}).join('')}
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" id="throw-location" value="${items[0].location}">
|
<input type="hidden" id="throw-location" value="${items[0].location}">
|
||||||
@@ -2066,9 +2114,22 @@ function showAddForm() {
|
|||||||
const unitSelect = document.getElementById('add-unit');
|
const unitSelect = document.getElementById('add-unit');
|
||||||
unitSelect.value = unit;
|
unitSelect.value = unit;
|
||||||
|
|
||||||
document.getElementById('add-quantity').value = currentProduct.default_quantity || 1;
|
document.getElementById('add-quantity').value = unit === 'conf' ? (currentProduct.last_qty || 1) : (currentProduct.default_quantity || 1);
|
||||||
document.getElementById('add-quantity').dataset.manuallySet = 'false';
|
document.getElementById('add-quantity').dataset.manuallySet = 'false';
|
||||||
|
|
||||||
|
// Show/hide conf size row and pre-fill
|
||||||
|
const confRow = document.getElementById('add-conf-size-row');
|
||||||
|
if (confRow) {
|
||||||
|
confRow.style.display = unit === 'conf' ? 'block' : 'none';
|
||||||
|
if (unit === 'conf' && currentProduct.package_unit && currentProduct.default_quantity > 0) {
|
||||||
|
document.getElementById('add-conf-size').value = currentProduct.default_quantity;
|
||||||
|
document.getElementById('add-conf-unit').value = currentProduct.package_unit;
|
||||||
|
} else if (unit === 'conf') {
|
||||||
|
document.getElementById('add-conf-size').value = '';
|
||||||
|
document.getElementById('add-conf-unit').value = 'g';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Track manual edits to quantity in add form
|
// Track manual edits to quantity in add form
|
||||||
const addQtyInput = document.getElementById('add-quantity');
|
const addQtyInput = document.getElementById('add-quantity');
|
||||||
addQtyInput.removeEventListener('input', markAddQtyManuallySet);
|
addQtyInput.removeEventListener('input', markAddQtyManuallySet);
|
||||||
@@ -2131,10 +2192,26 @@ function showAddForm() {
|
|||||||
|
|
||||||
function onAddUnitChange() {
|
function onAddUnitChange() {
|
||||||
updateAddQtyStep();
|
updateAddQtyStep();
|
||||||
// If switching units, suggest a sensible quantity
|
|
||||||
// BUT only if the user hasn't manually changed the quantity in this form
|
|
||||||
const unit = document.getElementById('add-unit').value;
|
const unit = document.getElementById('add-unit').value;
|
||||||
const qtyInput = document.getElementById('add-quantity');
|
const qtyInput = document.getElementById('add-quantity');
|
||||||
|
|
||||||
|
// Show/hide conf size row
|
||||||
|
const confRow = document.getElementById('add-conf-size-row');
|
||||||
|
if (confRow) {
|
||||||
|
confRow.style.display = unit === 'conf' ? 'block' : 'none';
|
||||||
|
// Pre-fill from currentProduct if available
|
||||||
|
if (unit === 'conf' && currentProduct) {
|
||||||
|
const sizeInput = document.getElementById('add-conf-size');
|
||||||
|
const unitSelect = document.getElementById('add-conf-unit');
|
||||||
|
if (currentProduct.package_unit && currentProduct.default_quantity > 1) {
|
||||||
|
sizeInput.value = currentProduct.default_quantity;
|
||||||
|
unitSelect.value = currentProduct.package_unit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If switching units, suggest a sensible quantity
|
||||||
|
// BUT only if the user hasn't manually changed the quantity in this form
|
||||||
if (qtyInput.dataset.manuallySet === 'true') return; // User already edited qty, don't overwrite
|
if (qtyInput.dataset.manuallySet === 'true') return; // User already edited qty, don't overwrite
|
||||||
|
|
||||||
const currentQty = parseFloat(qtyInput.value) || 1;
|
const currentQty = parseFloat(qtyInput.value) || 1;
|
||||||
@@ -2267,6 +2344,8 @@ async function submitAdd(e) {
|
|||||||
location: document.getElementById('add-location').value,
|
location: document.getElementById('add-location').value,
|
||||||
expiry_date: document.getElementById('add-expiry').value || null,
|
expiry_date: document.getElementById('add-expiry').value || null,
|
||||||
unit: selectedUnit !== productUnit ? selectedUnit : null,
|
unit: selectedUnit !== productUnit ? selectedUnit : null,
|
||||||
|
package_unit: selectedUnit === 'conf' ? (document.getElementById('add-conf-unit')?.value || null) : null,
|
||||||
|
package_size: selectedUnit === 'conf' ? (parseFloat(document.getElementById('add-conf-size')?.value) || null) : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
|
|||||||
Binary file not shown.
+25
-1
@@ -192,6 +192,18 @@
|
|||||||
<option value="l">L</option>
|
<option value="l">L</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="add-conf-size-row" class="conf-size-row" style="display:none">
|
||||||
|
<label class="conf-size-label">📦 Ogni confezione contiene:</label>
|
||||||
|
<div class="conf-size-inputs">
|
||||||
|
<input type="number" id="add-conf-size" class="form-input conf-size-input" min="1" step="any" placeholder="es. 300">
|
||||||
|
<select id="add-conf-unit" class="form-input conf-size-unit">
|
||||||
|
<option value="g">g</option>
|
||||||
|
<option value="kg">kg</option>
|
||||||
|
<option value="ml">ml</option>
|
||||||
|
<option value="l">L</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="add-weight-info" class="form-hint" style="display:none"></div>
|
<div id="add-weight-info" class="form-hint" style="display:none"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" id="add-expiry-section">
|
<div class="form-group" id="add-expiry-section">
|
||||||
@@ -380,7 +392,7 @@
|
|||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group flex-1">
|
<div class="form-group flex-1">
|
||||||
<label>📏 Unità di misura</label>
|
<label>📏 Unità di misura</label>
|
||||||
<select id="pf-unit" class="form-input">
|
<select id="pf-unit" class="form-input" onchange="onPfUnitChange()">
|
||||||
<option value="pz">Pezzi</option>
|
<option value="pz">Pezzi</option>
|
||||||
<option value="kg">Kg</option>
|
<option value="kg">Kg</option>
|
||||||
<option value="g">Grammi</option>
|
<option value="g">Grammi</option>
|
||||||
@@ -394,6 +406,18 @@
|
|||||||
<input type="number" id="pf-defqty" class="form-input" value="1" min="0.1" step="any">
|
<input type="number" id="pf-defqty" class="form-input" value="1" min="0.1" step="any">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="pf-conf-size-row" class="conf-size-row" style="display:none">
|
||||||
|
<label class="conf-size-label">📦 Ogni confezione contiene:</label>
|
||||||
|
<div class="conf-size-inputs">
|
||||||
|
<input type="number" id="pf-conf-size" class="form-input conf-size-input" min="1" step="any" placeholder="es. 300">
|
||||||
|
<select id="pf-conf-unit" class="form-input conf-size-unit">
|
||||||
|
<option value="g">g</option>
|
||||||
|
<option value="kg">kg</option>
|
||||||
|
<option value="ml">ml</option>
|
||||||
|
<option value="l">L</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>📝 Note</label>
|
<label>📝 Note</label>
|
||||||
<textarea id="pf-notes" class="form-input" rows="2" placeholder="Es: senza lattosio, bio, conservare in frigo dopo apertura..."></textarea>
|
<textarea id="pf-notes" class="form-input" rows="2" placeholder="Es: senza lattosio, bio, conservare in frigo dopo apertura..."></textarea>
|
||||||
|
|||||||
Reference in New Issue
Block a user