Merge branch 'develop' into main
This commit is contained in:
@@ -45,6 +45,7 @@
|
||||
- **Banner aperto vs scaduto** — I prodotti con `opened_at` mostrano "Aperto da N giorni in [posizione]" invece di "Scaduto!", con la posizione (frigo/dispensa/freezer) esplicitamente indicata.
|
||||
- **Shelf life latte UHT** — Il latte generico è ora trattato come UHT (7 giorni dopo apertura) invece che fresco (4 giorni).
|
||||
- **Niente più false anomalie di consumo** — Il rilevatore ora ignora i casi in cui `expected = 0` (prodotto probabilmente ricomprato) e alza la soglia "more than expected" al 400%. Le notifiche rimangono solo per consumi significativamente inferiori al previsto.
|
||||
- **Previsioni consumo adattive e meno rumorose** — Le previsioni ora pesano anche il comportamento recente dopo l'ultimo rifornimento; i banner "più del previsto" non vengono più mostrati (spesso erano falsi positivi), mentre i casi "meno del previsto" appaiono solo con evidenza sufficiente.
|
||||
- **Scaduti nascondono prodotti già buttati** — La sezione scaduti ora filtra correttamente i prodotti con `quantity = 0`.
|
||||
- **Docker: fix permessi DB al primo avvio** — `_ensureDataDir()` crea la directory `data/` se mancante e tenta `chmod(0775)` se non scrivibile, risolvendo `SQLSTATE[HY000][14]` su volumi Docker freschi.
|
||||
- **AI price estimation for shopping list** — Each Bring! shopping item now shows an estimated retail price badge (unit price + total). Prices are fetched via Gemini AI, cached server-side for 3 months, and stored client-side in `sessionStorage` to survive navigation. The dashboard shopping stat card shows a live green `ca. €X.XX` badge that updates in real-time as prices are calculated — even in background when you're on another tab.
|
||||
|
||||
+29
-8
@@ -2132,9 +2132,9 @@ function getConsumptionPredictions(PDO $db): void {
|
||||
$daySpan = ($lastDate - $firstDate) / 86400;
|
||||
// If all transactions are clustered within a week, the rate is unreliable
|
||||
if ($daySpan < 7) continue;
|
||||
$dailyRate = $totalUsed / $daySpan;
|
||||
$historicalRate = $totalUsed / $daySpan;
|
||||
|
||||
if ($dailyRate < 0.01) continue; // negligible consumption
|
||||
if ($historicalRate < 0.01) continue; // negligible consumption
|
||||
|
||||
// Get the most recent restock (last 'in' transaction)
|
||||
$lastIn = $db->prepare("
|
||||
@@ -2166,16 +2166,35 @@ function getConsumptionPredictions(PDO $db): void {
|
||||
$baselineQty = floatval($item['quantity']) + $usedSinceRestock;
|
||||
$daysSinceRestock = max(1, (time() - $restockDate) / 86400);
|
||||
|
||||
// Recalculate the expected consumption with an adaptive rate:
|
||||
// blend long-term history with post-restock behavior when available.
|
||||
$txSinceRestock = 0;
|
||||
foreach ($rows as $r) {
|
||||
if (strtotime($r['created_at']) >= $restockDate) $txSinceRestock++;
|
||||
}
|
||||
$observedRate = $daysSinceRestock > 0 ? ($usedSinceRestock / $daysSinceRestock) : 0;
|
||||
$dailyRate = $historicalRate;
|
||||
if ($observedRate > 0) {
|
||||
if ($txSinceRestock >= 3) {
|
||||
$dailyRate = ($historicalRate * 0.45) + ($observedRate * 0.55);
|
||||
} elseif ($txSinceRestock >= 1) {
|
||||
$dailyRate = ($historicalRate * 0.70) + ($observedRate * 0.30);
|
||||
}
|
||||
}
|
||||
|
||||
// If the model predicts you should have consumed less than 15% of baseline
|
||||
// in this period, the daily rate is too low to make reliable predictions:
|
||||
// any single normal use will look like an anomaly. Skip it.
|
||||
$predictedConsumption = $dailyRate * $daysSinceRestock;
|
||||
if ($baselineQty > 0 && $predictedConsumption < $baselineQty * 0.15) continue;
|
||||
|
||||
// Predicted remaining qty = baseline - (daily rate * days since restock)
|
||||
// Predicted remaining qty = baseline - (adaptive daily rate * days since restock)
|
||||
$expectedQty = max(0, $baselineQty - ($dailyRate * $daysSinceRestock));
|
||||
$actualQty = floatval($item['quantity']);
|
||||
|
||||
// Need at least some post-restock usage observations before warning.
|
||||
if ($txSinceRestock < 2) continue;
|
||||
|
||||
// Flag if deviation > 30% and absolute diff > meaningful threshold
|
||||
$deviation = abs($actualQty - $expectedQty);
|
||||
$threshold = max($dailyRate * 3, 0.5); // at least 3 days worth or 0.5 units
|
||||
@@ -2188,10 +2207,12 @@ function getConsumptionPredictions(PDO $db): void {
|
||||
|
||||
$pctDev = $expectedQty > 0 ? ($deviation / $expectedQty) : ($actualQty > 0 ? 1 : 0);
|
||||
|
||||
// "more than expected" is almost always a restock the model doesn't know about yet.
|
||||
// Only flag it at very high deviation (>400%) to catch truly impossible values.
|
||||
// "less than expected" is more actionable: user may have consumed without registering.
|
||||
$flagThreshold = ($actualQty > $expectedQty) ? 4.0 : 0.30;
|
||||
// "More than expected" usually means slower real consumption, not bad data.
|
||||
// Suppress this direction to avoid noisy/accusatory banners.
|
||||
if ($actualQty > $expectedQty) continue;
|
||||
|
||||
// Only keep meaningful "less than expected" deviations.
|
||||
$flagThreshold = 0.45;
|
||||
|
||||
if ($pctDev > $flagThreshold && $deviation > $threshold) {
|
||||
$unit = $item['unit'];
|
||||
@@ -2220,7 +2241,7 @@ function getConsumptionPredictions(PDO $db): void {
|
||||
'daily_rate' => round($dailyRate, 3),
|
||||
'deviation_pct' => round($pctDev * 100),
|
||||
'days_since_restock' => (int)round($daysSinceRestock),
|
||||
'direction' => $actualQty > $expectedQty ? 'more' : 'less',
|
||||
'direction' => 'less',
|
||||
'tx_count' => count($rows),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -91,8 +91,8 @@
|
||||
"banner_review_action_edit": "Korrigieren",
|
||||
"banner_review_action_weigh": "Wiegen",
|
||||
"banner_review_dismiss": "Ignorieren",
|
||||
"banner_prediction_title": "Ungewöhnlicher Verbrauch",
|
||||
"banner_prediction_hint": "Laut Vorhersage stimmt diese Menge nicht mit dem erwarteten Verbrauch überein.",
|
||||
"banner_prediction_title": "Verbrauch zur Prüfung",
|
||||
"banner_prediction_hint": "Die Verbrauchsschätzung passt sich aktuellen Daten an: bitte nur die aktuelle Menge bestätigen.",
|
||||
"banner_prediction_action_confirm": "{qty} {unit} bestätigen",
|
||||
"banner_prediction_action_weigh": "Jetzt wiegen",
|
||||
"banner_prediction_action_edit": "Menge aktualisieren",
|
||||
@@ -128,8 +128,8 @@
|
||||
"banner_prediction_rate_day": "Durchschnitt ~{n} {unit}/Tag",
|
||||
"banner_prediction_rate_week": "Durchschnitt ~{n} {unit}/Woche",
|
||||
"banner_prediction_days_ago": "Vor {n} Tagen aufgefüllt",
|
||||
"banner_prediction_more": "Ich erwartete {expected} {unit}{time}, du hast aber {actual} {unit}. Hast du Bestand ohne Buchung hinzugefügt?",
|
||||
"banner_prediction_less": "Ich erwartete {expected} {unit}{time}, du hast aber nur {actual} {unit}. Hast du mehr als üblich verbraucht?",
|
||||
"banner_prediction_more": "frühere Schätzung: {expected} {unit}{time}; aktuelle Menge: {actual} {unit}.",
|
||||
"banner_prediction_less": "Schätzung: {expected} {unit}{time}; aktuelle Menge: {actual} {unit}. Wenn sich dein Verbrauch geändert hat, aktualisiert sich die Prognose automatisch.",
|
||||
"banner_finished_zero": "Bestand zeigt null, aber gespeicherte Buchungen deuten an, dass es nicht leer sein sollte.",
|
||||
"banner_finished_expected": "Laut Aufzeichnungen solltest du noch {qty} {unit} haben.",
|
||||
"banner_finished_check": "Kannst du nachschauen?",
|
||||
|
||||
@@ -91,9 +91,9 @@
|
||||
"banner_review_action_edit": "Correct",
|
||||
"banner_review_action_weigh": "Weigh",
|
||||
"banner_review_dismiss": "Dismiss",
|
||||
"banner_prediction_title": "Anomalous consumption",
|
||||
"banner_prediction_hint": "Based on predictions, this quantity doesn't match expected consumption.",
|
||||
"banner_prediction_action_confirm": "Confirm {qty} {unit} is correct",
|
||||
"banner_prediction_title": "Consumption to review",
|
||||
"banner_prediction_hint": "The consumption estimate adapts to recent data: confirm only if the current quantity is correct.",
|
||||
"banner_prediction_action_confirm": "Confirm {qty} {unit}",
|
||||
"banner_prediction_action_weigh": "Weigh now",
|
||||
"banner_prediction_action_edit": "Update quantity",
|
||||
"banner_expired_title": "Expired product",
|
||||
@@ -128,8 +128,8 @@
|
||||
"banner_prediction_rate_day": "Average ~{n} {unit}/day",
|
||||
"banner_prediction_rate_week": "Average ~{n} {unit}/week",
|
||||
"banner_prediction_days_ago": "{n} days ago you restocked",
|
||||
"banner_prediction_more": "I expected {expected} {unit}{time}, but you have {actual} {unit}. Did you add stock without recording it?",
|
||||
"banner_prediction_less": "I expected {expected} {unit}{time}, but you only have {actual} {unit}. Did you use more than usual?",
|
||||
"banner_prediction_more": "previous estimate: {expected} {unit}{time}; current quantity: {actual} {unit}.",
|
||||
"banner_prediction_less": "estimate: {expected} {unit}{time}; current quantity: {actual} {unit}. If your usage pace changed, the forecast updates automatically.",
|
||||
"banner_finished_zero": "Inventory shows zero, but recorded movements suggest it shouldn't be empty.",
|
||||
"banner_finished_expected": "According to records you should still have {qty} {unit}.",
|
||||
"banner_finished_check": "Can you check?",
|
||||
|
||||
@@ -91,9 +91,9 @@
|
||||
"banner_review_action_edit": "Correggi",
|
||||
"banner_review_action_weigh": "Pesa",
|
||||
"banner_review_dismiss": "Ignora",
|
||||
"banner_prediction_title": "Consumo anomalo",
|
||||
"banner_prediction_hint": "Secondo le previsioni, questa quantità non corrisponde al consumo previsto.",
|
||||
"banner_prediction_action_confirm": "Confermo la quantità di {qty} {unit}",
|
||||
"banner_prediction_title": "Consumo da verificare",
|
||||
"banner_prediction_hint": "La stima di consumo si adatta ai dati recenti: verifica solo se la quantità corrente è corretta.",
|
||||
"banner_prediction_action_confirm": "Confermo {qty} {unit}",
|
||||
"banner_prediction_action_weigh": "Pesa ora",
|
||||
"banner_prediction_action_edit": "Aggiorna quantità",
|
||||
"banner_expired_title": "Prodotto scaduto",
|
||||
@@ -128,8 +128,8 @@
|
||||
"banner_prediction_rate_day": "Media ~{n} {unit}/giorno",
|
||||
"banner_prediction_rate_week": "Media ~{n} {unit}/settimana",
|
||||
"banner_prediction_days_ago": "{n} giorni fa hai rifornito",
|
||||
"banner_prediction_more": "mi aspettavo {expected} {unit}{time}, ne hai invece {actual} {unit}. Hai aggiunto scorte senza registrarle?",
|
||||
"banner_prediction_less": "mi aspettavo {expected} {unit}{time}, ne hai solo {actual} {unit}. Hai consumato di più del solito?",
|
||||
"banner_prediction_more": "stima precedente: {expected} {unit}{time}; quantità attuale: {actual} {unit}.",
|
||||
"banner_prediction_less": "stima: {expected} {unit}{time}; quantità attuale: {actual} {unit}. Se hai cambiato ritmo d'uso, la previsione si aggiorna automaticamente.",
|
||||
"banner_finished_zero": "L'inventario segna zero, ma i movimenti registrati dicono che non dovrebbe essere finito.",
|
||||
"banner_finished_expected": "Secondo le registrazioni dovresti averne ancora {qty} {unit}.",
|
||||
"banner_finished_check": "Puoi controllare?",
|
||||
|
||||
Reference in New Issue
Block a user