diff --git a/README.md b/README.md
index 513f572..a3de85a 100644
--- a/README.md
+++ b/README.md
@@ -1,34 +1,87 @@
# harbour-henflow
-Porting **nativo Sailfish OS** dell'app Android *Henflow* (`it.carlobaratto.henflow`,
-Gestione ordini uova, galline e consegne). UI in QML/Silica, logica e dati in C++,
-pubblicazione prevista sullo **store Jolla Harbour**.
+Porting **nativo Sailfish OS** dell'app Android *Henflow* (`it.carlobaratto.henflow`).
+UI in QML/Silica, calcolo e dati locali, pubblicazione prevista sullo
+**store Jolla Harbour**.
-Il porting **non** usa il runtime Flutter community (`flutter-sailfishos`): quello
-resta una via OpenRepos, non Harbour-safe, e non supporta i plugin nativi Android
-(AdMob/Firebase/Billing). Dati locali sul telefono: nessun account, nessun cloud.
+Il porting **non** usa il runtime Flutter community (`flutter-sailfishos`):
+quello resta una via OpenRepos, non Harbour-safe, e non supporta i plugin nativi
+Android (AdMob/File picker/Share). Nessun account, nessun cloud, come l'originale.
+
+## Sorgenti di riferimento
+
+- Flutter (originale): `workspace/henflow-flutter` — export da Nextcloud, repo Gitea `HenFlow`.
+- Screenshot dello store usati come riferimento UI.
## Stato
-Guscio completo e pronto alla prima build (finestra, pagina principale, cover,
-impostazioni con lingua, icone, catalogo traduzioni, spec RPM). Le schermate sono
-segnaposto: vanno mappate 1:1 dai sorgenti Flutter.
+| parte | stato |
+|---|---|
+| Packaging (spec RPM, desktop SailJail, icone, i18n) | pronto |
+| Guscio app (finestra, cover, Settings con lingua) | pronto |
+| Motore di calcolo (`qml/js/henflow.js`) | portato e verificato con node |
+| Schermate QML | da implementare |
## Struttura
```
-harbour-henflow.pro # QT += quick qml, INSTALLS icone
-rpm/harbour-henflow.spec # lrelease + desktop cercati con find (layout sorgente variabile)
+harbour-henflow.pro # QT += quick qml, INSTALLS icone
+rpm/harbour-henflow.spec # lrelease + desktop cercati con find (layout sorgente variabile)
rpm/harbour-henflow.desktop # profilo [X-Sailjail] esplicito (dati persistenti)
-src/main.cpp # SailfishApp, context property appSettings, lingua
-src/settings.h/.cpp # QSettings su path esplicito AppConfigLocation + sync()
-qml/harbour-henflow.qml # ApplicationWindow: initialPage + cover
-qml/pages/MainPage.qml # segnaposto con i moduli previsti
-qml/cover/CoverPage.qml
-icons/make_icons.py # icone 86/108/128/172 (PIL)
-translations/*.ts # stringhe sorgente INGLESI + catalogo italiano
+src/main.cpp # SailfishApp, context property appSettings, lingua
+src/settings.h/.cpp # QSettings su path esplicito AppConfigLocation + sync()
+qml/harbour-henflow.qml # ApplicationWindow: initialPage + cover
+qml/pages/MainPage.qml # indice dei moduli (segnaposto)
+qml/js/henflow.js # motore di calcolo (ES5, testabile con node)
+tests/engine.test.js # 30 controlli: node tests/engine.test.js
+icons/make_icons.py # icone 86/108/128/172 (PIL)
+translations/*.ts # stringhe sorgente INGLESI + catalogo italiano
```
+## Motore di calcolo (portato fedelmente da `lib/main.dart`)
+
+- **Produzione stimata** per razza = galline in deposizione x uova/gallina/giorno
+ (catalogo: tutte 1.0). Una gallina conta da **6 mesi** in su (`_ageInMonths`).
+- **Magazzino**: all'apertura, i giorni passati dall'ultimo aggiornamento
+ aggiungono la produzione giornaliera alle scorte di ogni razza attiva.
+- **Coda FIFO**: ordini in ordine di creazione; ognuno consuma le scorte virtuali
+ della sua razza, chi le trova consegna oggi, gli altri aspettano
+ `ceil(uova_mancanti / produzione)` giorni. Gli ordini gia' consegnati non
+ consumano scorte.
+- **Stima per un nuovo ordine**: `ceil((pendenti + richieste - scorte) / produzione)`.
+
+Le date sono sempre normalizzate a `YYYY-MM-DD` e le funzioni non mutano l'input.
+
+## Modello dati (fedele a `StorageService`, SharedPreferences + JSON)
+
+| chiave | contenuto |
+|---|---|
+| `breed_settings_v2` | `[{breedId, hens:[{id, name, birthDate, isLaying}]}]` |
+| `reservations_v2` | `[{id, customerName, breedId, eggsRequested, createdAt, deliveryDate, isDelivered}]` |
+| `customers_v1` | `[{id, firstName, lastName, phoneNumber}]` |
+| `warehouse_stock_v1` | `{breedId: uova}` |
+| `last_production_update_v1` | data ISO dell'ultimo accrual |
+
+Nel porting: un unico file JSON in `~/.config/harbour/henflow/` (nessuna
+dipendenza extra, nessun driver SQL da verificare sul device) con la stessa
+struttura, cosi' il backup/ripristino resta compatibile con l'app Android.
+
+## Schermate mappate (da sorgenti + screenshot)
+
+| schermata Flutter | contenuto | porting |
+|---|---|---|
+| `OnboardingPage` | 4 passi introduttivi | da fare |
+| `ModeSelectionPage` | modalita' allevatore / cliente | da decidere |
+| `HomePage` | tab *Orders list* / *Calendar* (+ magazzino), card produzione stimata, card ordini con stato e azioni (condividi/modifica/elimina), FAB *New order* | da fare |
+| `SettingsPage` | tab *Available breeds* (catalogo con foto e spunta) / *My coop* (galline registrate, in deposizione, produzione) + backup/ripristino JSON + lingua | da fare |
+| `BreedDetailPage` | galline della razza, eta' in mesi, stato deposizione | da fare |
+| `CustomersPage` + dialoghi | anagrafica clienti, nuovo ordine | da fare |
+| `AboutPage` | info app | da fare |
+| `CoopIcon` (CustomPainter) | icona pollaio disegnata a mano | da ridisegnare (Canvas QML o PNG) |
+
+Differenze da tenere presenti: **niente annunci** (AdMob non esiste su Sailfish),
+**share/export** via file JSON in Download invece di `share_plus`/`file_picker`.
+
## Build e deploy
Sul PC con l'SDK Sailfish (qui non c'e' sfdk):
@@ -45,32 +98,10 @@ Tarball per lo store (con `--prefix`, esclude .git e i file non tracciati):
git archive --format=tar.gz --prefix=harbour-henflow-0.1.0/ -o harbour-henflow-0.1.0.tar.gz HEAD
```
-## Modello dati proposto (da confermare sui sorgenti Flutter)
+## Aperti
-Un file JSON in `AppConfigLocation` (`~/.config/harbour/henflow/henflow.json`)
-e' sufficiente: nessuna dipendenza extra e nessun driver SQL da verificare sul
-device. SQLite resta l'alternativa se servono query piu' ricche.
-
-| entita' | campi |
-|---|---|
-| `hens` | id, nome, razza, data_nascita, uova_settimana (stima), attiva, note |
-| `egg_log` | data, uova_raccolte (+ eventuale id gallina) |
-| `customers` | id, nome, telefono, quantita_abituale, giorno_consegna, note |
-| `orders` | id, id_cliente, quantita, data_ordine, data_consegna, stato, note |
-
-Derivati: **uova disponibili oggi** = produzione stimata/registrata meno gli ordini
-con data di consegna odierna; **data consegna automatica** = primo giorno di
-consegna del cliente successivo all'ordine.
-
-## Prossimi passi
-
-1. Copiare i sorgenti Flutter sul server (vedi sotto).
-2. Mappare schermate e campi reali (non la descrizione dello store).
-3. Implementare storage + schermate, verifica su device (192.168.2.15).
-4. Asset store (icona, cover 1080x540), descrizioni EN, upload Harbour.
-
-```sh
-# dal PC con i sorgenti Flutter (LAN o VPN)
-rsync -av --exclude build --exclude .dart_tool --exclude .git \
- ~/percorso/henflow/ kaneda@192.168.3.21:/home/kaneda/workspace/henflow-flutter/
-```
+1. **Asset**: nel materiale di Nextcloud mancano `assets/breeds/*.jpg` (7 foto)
+ e `pubspec.yaml`.
+2. **Repo Gitea `HenFlow`**: cancellata (era un upload parziale); per ricrearla
+ serve un permesso che il token attuale non ha (`write:user`).
+3. **Ambito**: modalita' cliente e 4 lingue (EN/IT/FR/DE) da confermare.
diff --git a/harbour-henflow.pro b/harbour-henflow.pro
index 3f94eb2..2664c31 100644
--- a/harbour-henflow.pro
+++ b/harbour-henflow.pro
@@ -19,6 +19,7 @@ OTHER_FILES += \
qml/harbour-henflow.qml \
qml/pages/*.qml \
qml/cover/*.qml \
+ qml/js/henflow.js \
rpm/harbour-henflow.spec \
rpm/harbour-henflow.desktop \
translations/harbour-henflow.ts \
diff --git a/qml/js/henflow.js b/qml/js/henflow.js
new file mode 100644
index 0000000..907bc38
--- /dev/null
+++ b/qml/js/henflow.js
@@ -0,0 +1,255 @@
+// Henflow — motore di calcolo.
+// Porting fedele della logica di lib/main.dart dell'app Flutter:
+// produzione stimata per razza, magazzino uova (accrual giornaliero),
+// coda FIFO delle prenotazioni e data di consegna calcolata.
+//
+// ES5 PURO: il motore JS di QML su Qt 5.6 non supporta let/const, arrow
+// function, template literal, class, destructuring. Funzioni TOP-LEVEL
+// (un `var HF = (function(){...})()` non sarebbe visibile da QML).
+// Banchi di prova: tests/engine.test.js (node).
+
+// ---------------------------------------------------------------- date
+
+function pad2(n) {
+ return (n < 10 ? "0" : "") + n;
+}
+
+// "2026-04-27T10:11:12.000" -> Date a mezzanotte locale (Qt 5.6 non e'
+// affidabile sul parsing ISO di Date.parse: si estrae a mano).
+function parseIso(s) {
+ if (!s) return null;
+ var m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(s));
+ if (!m) return null;
+ return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
+}
+
+function isoDay(d) {
+ return d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate());
+}
+
+function todayMidnight() {
+ var n = new Date();
+ return new Date(n.getFullYear(), n.getMonth(), n.getDate());
+}
+
+function addDays(d, n) {
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
+}
+
+// Giorni interi tra due date (ignora l'ora).
+function dayDiff(from, to) {
+ var a = new Date(from.getFullYear(), from.getMonth(), from.getDate());
+ var b = new Date(to.getFullYear(), to.getMonth(), to.getDate());
+ return Math.round((b.getTime() - a.getTime()) / 86400000);
+}
+
+// ------------------------------------------------- razze e produzione
+
+function catalogEntry(catalog, breedId) {
+ for (var i = 0; i < catalog.length; i++) {
+ if (catalog[i].id === breedId) return catalog[i];
+ }
+ return null;
+}
+
+function eggsPerHenPerDay(catalog, breedId) {
+ var c = catalogEntry(catalog, breedId);
+ return c ? c.eggsPerHenPerDay : 0;
+}
+
+function activeHens(breed) {
+ var n = 0;
+ for (var i = 0; i < breed.hens.length; i++) {
+ if (breed.hens[i].isLaying) n++;
+ }
+ return n;
+}
+
+// Produzione giornaliera di una razza = galline in deposizione x uova/gallina.
+function breedDailyProduction(breedSettings, catalog, breedId) {
+ for (var i = 0; i < breedSettings.length; i++) {
+ if (breedSettings[i].breedId === breedId) {
+ return activeHens(breedSettings[i]) * eggsPerHenPerDay(catalog, breedId);
+ }
+ }
+ return 0;
+}
+
+function totalDailyProduction(breedSettings, catalog) {
+ var total = 0;
+ for (var i = 0; i < breedSettings.length; i++) {
+ total += activeHens(breedSettings[i]) * eggsPerHenPerDay(catalog, breedSettings[i].breedId);
+ }
+ return total;
+}
+
+// Dettaglio per la card "produzione stimata": una voce per razza attiva.
+function productionDetail(breedSettings, catalog) {
+ var out = [];
+ for (var i = 0; i < breedSettings.length; i++) {
+ var b = breedSettings[i];
+ var hens = activeHens(b);
+ if (hens > 0) {
+ out.push({
+ breedId: b.breedId,
+ hens: hens,
+ daily: hens * eggsPerHenPerDay(catalog, b.breedId)
+ });
+ }
+ }
+ return out;
+}
+
+// Eta' in mesi compiuti (stessa formula di _ageInMonths in main.dart).
+function ageInMonths(birthDate, today) {
+ var months = (today.getFullYear() - birthDate.getFullYear()) * 12
+ + (today.getMonth() - birthDate.getMonth());
+ if (today.getDate() < birthDate.getDate()) months--;
+ return months;
+}
+
+// Una gallina contribuisce alla produzione da 6 mesi in su.
+function isProductive(birthDate, today) {
+ return ageInMonths(birthDate, today) >= 6;
+}
+
+// ----------------------------------------------------------- magazzino
+
+// Accrual all'apertura: ogni giorno passato dall'ultimo aggiornamento
+// aggiunge la produzione giornaliera delle razze con galline in deposizione.
+function accrueStock(stock, breedSettings, catalog, daysPassed) {
+ var out = {}, k;
+ for (k in stock) {
+ if (stock.hasOwnProperty(k)) out[k] = stock[k];
+ }
+ if (daysPassed <= 0) return out;
+
+ for (var i = 0; i < breedSettings.length; i++) {
+ var b = breedSettings[i];
+ if (activeHens(b) > 0) {
+ var prod = breedDailyProduction(breedSettings, catalog, b.breedId);
+ out[b.breedId] = (out[b.breedId] || 0) + prod * daysPassed;
+ }
+ }
+ return out;
+}
+
+// --------------------------------------------------------- prenotazioni
+
+function cloneReservation(r) {
+ return {
+ id: r.id,
+ customerName: r.customerName,
+ breedId: r.breedId,
+ eggsRequested: r.eggsRequested,
+ createdAt: r.createdAt,
+ deliveryDate: r.deliveryDate,
+ isDelivered: r.isDelivered
+ };
+}
+
+function pendingEggs(reservations, breedId) {
+ var sum = 0;
+ for (var i = 0; i < reservations.length; i++) {
+ var r = reservations[i];
+ if (r.breedId === breedId && !r.isDelivered) sum += r.eggsRequested;
+ }
+ return sum;
+}
+
+// Stima per una NUOVA prenotazione (_calculateDeliveryDate in main.dart):
+// uova pendenti della razza + richieste - scorte, divise per la produzione.
+function estimateDeliveryDate(reservations, stock, breedSettings, catalog,
+ breedId, eggsRequested, today) {
+ var production = breedDailyProduction(breedSettings, catalog, breedId);
+ var available = stock[breedId] || 0;
+ var needed = pendingEggs(reservations, breedId) + eggsRequested - available;
+ if (needed <= 0) return isoDay(today);
+ if (production <= 0) return isoDay(today);
+ return isoDay(addDays(today, Math.ceil(needed / production)));
+}
+
+// Ricalcolo della coda (_refreshDates in main.dart): ordine FIFO per data di
+// creazione, ogni ordine consuma le scorte virtuali della sua razza; chi trova
+// uova sufficienti consegna oggi, gli altri aspettano i giorni necessari.
+// Le prenotazioni consegnate non consumano scorte e tengono la loro data.
+function refreshDeliveryDates(reservations, stock, breedSettings, catalog, today) {
+ var list = reservations.slice();
+ list.sort(function (a, b) {
+ return parseIso(a.createdAt).getTime() - parseIso(b.createdAt).getTime();
+ });
+
+ var virtual = {}, k;
+ for (k in stock) {
+ if (stock.hasOwnProperty(k)) virtual[k] = stock[k];
+ }
+
+ var out = [];
+ for (var i = 0; i < list.length; i++) {
+ var r = list[i];
+ if (r.isDelivered) {
+ // Consegnata: nessun consumo di scorte, data invariata (giorno).
+ var done = cloneReservation(r);
+ done.deliveryDate = isoDay(parseIso(r.deliveryDate));
+ out.push(done);
+ continue;
+ }
+
+ var production = breedDailyProduction(breedSettings, catalog, r.breedId);
+ virtual[r.breedId] = (virtual[r.breedId] || 0) - r.eggsRequested;
+
+ if (virtual[r.breedId] >= 0) {
+ var ok = cloneReservation(r);
+ ok.deliveryDate = isoDay(today);
+ out.push(ok);
+ } else if (production <= 0) {
+ // Razza senza galline in deposizione: data non toccata.
+ var stuck = cloneReservation(r);
+ stuck.deliveryDate = isoDay(parseIso(r.deliveryDate));
+ out.push(stuck);
+ } else {
+ var missing = -virtual[r.breedId];
+ var late = cloneReservation(r);
+ late.deliveryDate = isoDay(addDays(today, Math.ceil(missing / production)));
+ out.push(late);
+ }
+ }
+
+ out.sort(function (a, b) {
+ return parseIso(a.deliveryDate).getTime() - parseIso(b.deliveryDate).getTime();
+ });
+ return out;
+}
+
+// Stato mostrato nella card: giorni mancanti alla consegna.
+function statusKey(days) {
+ if (days < 0) return "late";
+ if (days === 0) return "today";
+ if (days === 1) return "tomorrow";
+ return "in_days";
+}
+
+// Guard per i test node: in QML `module` non esiste, le funzioni top-level
+// sono gia' visibili al file che importa questo modulo.
+if (typeof module !== "undefined" && module.exports) {
+ module.exports = {
+ parseIso: parseIso,
+ isoDay: isoDay,
+ todayMidnight: todayMidnight,
+ addDays: addDays,
+ dayDiff: dayDiff,
+ catalogEntry: catalogEntry,
+ eggsPerHenPerDay: eggsPerHenPerDay,
+ activeHens: activeHens,
+ breedDailyProduction: breedDailyProduction,
+ totalDailyProduction: totalDailyProduction,
+ productionDetail: productionDetail,
+ ageInMonths: ageInMonths,
+ isProductive: isProductive,
+ accrueStock: accrueStock,
+ pendingEggs: pendingEggs,
+ estimateDeliveryDate: estimateDeliveryDate,
+ refreshDeliveryDates: refreshDeliveryDates,
+ statusKey: statusKey
+ };
+}
diff --git a/qml/pages/MainPage.qml b/qml/pages/MainPage.qml
index c0bcd73..bc5a035 100644
--- a/qml/pages/MainPage.qml
+++ b/qml/pages/MainPage.qml
@@ -15,15 +15,18 @@ Page {
model: ListModel {
id: sections
- // Moduli previsti dall'app Android, da mappare sulle schermate
- // Flutter quando i sorgenti arrivano sul server.
+ // Struttura reale dell'app Flutter (lib/main.dart): la Home ha le tab
+ // "Orders list" e "Calendar", la schermata Configuration le tab
+ // "Available breeds" e "My coop". Qui resta l'indice dei moduli
+ // finche' le schermate non sono implementate.
Component.onCompleted: {
- append({ "name": qsTr("Hens"), "note": qsTr("name, breed, hatch date, estimated production") })
- append({ "name": qsTr("Available eggs"), "note": qsTr("eggs ready today") })
- append({ "name": qsTr("Orders"), "note": qsTr("customer, quantity, automatic delivery date") })
- append({ "name": qsTr("Calendar"), "note": qsTr("orders and pickups") })
- append({ "name": qsTr("Customers"), "note": qsTr("regular buyers") })
- append({ "name": qsTr("Dashboard"), "note": qsTr("summary of the day") })
+ append({ "name": qsTr("Orders list"), "note": qsTr("daily production, reservations, delivery status") })
+ append({ "name": qsTr("Calendar"), "note": qsTr("undelivered reservations by day") })
+ append({ "name": qsTr("Warehouse"), "note": qsTr("eggs in stock per breed") })
+ append({ "name": qsTr("Customers"), "note": qsTr("first name, last name, phone") })
+ append({ "name": qsTr("Available breeds"), "note": qsTr("catalog with photos, enable the ones you keep") })
+ append({ "name": qsTr("My coop"), "note": qsTr("registered hens, laying, estimated production") })
+ append({ "name": qsTr("Backup"), "note": qsTr("export and import the database as JSON") })
}
}
diff --git a/tests/engine.test.js b/tests/engine.test.js
new file mode 100644
index 0000000..dca9e37
--- /dev/null
+++ b/tests/engine.test.js
@@ -0,0 +1,120 @@
+// Banchi di prova del motore Henflow (node, fuori dal pacchetto RPM).
+// node tests/engine.test.js
+// I casi replicano il comportamento dell'app Flutter (lib/main.dart) e i dati
+// visibili nello screenshot dello store: Marans 2 galline, Araucana 2,
+// Leghorn 3 (di cui 2 in deposizione) -> 6.0 uova/giorno.
+
+var HF = require("../qml/js/henflow.js");
+
+var failures = 0;
+function check(name, got, expected) {
+ var ok = JSON.stringify(got) === JSON.stringify(expected);
+ if (!ok) failures++;
+ console.log((ok ? "OK " : "FAIL ") + name + " got=" + JSON.stringify(got)
+ + (ok ? "" : " expected=" + JSON.stringify(expected)));
+}
+
+// catalogo (dal breed_catalog.dart: tutte le razze 1.0 uovo/gallina/giorno)
+var CATALOG = [
+ { id: "marans", eggsPerHenPerDay: 1.0 },
+ { id: "araucana", eggsPerHenPerDay: 1.0 },
+ { id: "olive", eggsPerHenPerDay: 1.0 },
+ { id: "livorno", eggsPerHenPerDay: 1.0 }
+];
+
+function hen(id, laying) {
+ return { id: id, name: id, birthDate: "2025-01-01T00:00:00.000", isLaying: laying };
+}
+
+var BREEDS = [
+ { breedId: "marans", hens: [hen("m1", true), hen("m2", true)] },
+ { breedId: "araucana", hens: [hen("a1", true), hen("a2", true)] },
+ { breedId: "livorno", hens: [hen("l1", true), hen("l2", true), hen("l3", false)] }
+];
+
+var TODAY = new Date(2026, 3, 27); // 27 aprile 2026, come nello screenshot
+
+// --- produzione ---------------------------------------------------------
+check("produzione marans (2 galline)", HF.breedDailyProduction(BREEDS, CATALOG, "marans"), 2.0);
+check("produzione livorno (2 su 3 in deposizione)", HF.breedDailyProduction(BREEDS, CATALOG, "livorno"), 2.0);
+check("produzione totale = 6.0 uova/giorno", HF.totalDailyProduction(BREEDS, CATALOG), 6.0);
+check("razza non configurata -> 0", HF.breedDailyProduction(BREEDS, CATALOG, "olive"), 0);
+check("dettaglio razze attive", HF.productionDetail(BREEDS, CATALOG).map(function (e) { return e.daily; }), [2, 2, 2]);
+
+// --- eta' e produttivita' (soglia 6 mesi) --------------------------------
+check("eta' 6 mesi compiuti", HF.ageInMonths(new Date(2025, 9, 27), TODAY), 6);
+check("un giorno prima dei 6 mesi -> 5", HF.ageInMonths(new Date(2025, 9, 28), TODAY), 5);
+check("6 mesi -> produttiva", HF.isProductive(new Date(2025, 9, 27), TODAY), true);
+check("5 mesi -> non ancora produttiva", HF.isProductive(new Date(2025, 10, 28), TODAY), false);
+
+// --- magazzino: accrual all'apertura -------------------------------------
+check("3 giorni di produzione", HF.accrueStock({}, BREEDS, CATALOG, 3),
+ { marans: 6, araucana: 6, livorno: 6 });
+check("scorte esistenti incrementate", HF.accrueStock({ marans: 2 }, BREEDS, CATALOG, 1),
+ { marans: 4, araucana: 2, livorno: 2 });
+check("nessun giorno passato -> invariate", HF.accrueStock({ marans: 1 }, BREEDS, CATALOG, 0),
+ { marans: 1 });
+
+// --- data di consegna per una nuova prenotazione -------------------------
+check("scorte sufficienti -> oggi",
+ HF.estimateDeliveryDate([], { araucana: 3 }, BREEDS, CATALOG, "araucana", 2, TODAY), "2026-04-27");
+check("2 uova senza scorte, 2/giorno -> domani",
+ HF.estimateDeliveryDate([], {}, BREEDS, CATALOG, "araucana", 2, TODAY), "2026-04-28");
+check("5 uova senza scorte, 2/giorno -> 3 giorni (ceil)",
+ HF.estimateDeliveryDate([], {}, BREEDS, CATALOG, "araucana", 5, TODAY), "2026-04-30");
+check("ordini pendenti contano nelle scorte richieste",
+ HF.estimateDeliveryDate([res("p", "araucana", 4)], {}, BREEDS, CATALOG, "araucana", 2, TODAY),
+ "2026-04-30");
+check("razza senza galline in deposizione -> oggi (produzione 0)",
+ HF.estimateDeliveryDate([], {}, BREEDS, CATALOG, "olive", 3, TODAY), "2026-04-27");
+
+// --- coda FIFO ------------------------------------------------------------
+function res(id, breedId, eggs, createdDay, delivered) {
+ return {
+ id: id,
+ customerName: "Cli",
+ breedId: breedId,
+ eggsRequested: eggs,
+ createdAt: "2026-04-2" + createdDay + "T09:00:00.000",
+ deliveryDate: "2026-04-20T00:00:00.000",
+ isDelivered: !!delivered
+ };
+}
+
+var queue = HF.refreshDeliveryDates(
+ [res("b", "araucana", 2, 5), res("a", "araucana", 1, 1)],
+ { araucana: 0 }, BREEDS, CATALOG, TODAY);
+// ordine FIFO: "a" (creato prima) consuma la prima produzione
+check("FIFO: ordine piu' vecchio prima", [queue[0].id, queue[1].id], ["a", "b"]);
+check("FIFO: date calcolate in ordine di arrivo",
+ [queue[0].deliveryDate, queue[1].deliveryDate], ["2026-04-28", "2026-04-29"]);
+check("ordine evaso subito con scorte",
+ HF.refreshDeliveryDates([res("c", "araucana", 2, 1)], { araucana: 2 }, BREEDS, CATALOG, TODAY)[0].deliveryDate,
+ "2026-04-27");
+check("ordine consegnato: data invariata e nessun consumo di scorte",
+ HF.refreshDeliveryDates([res("d", "araucana", 99, 1, true), res("e", "araucana", 1, 2)],
+ { araucana: 1 }, BREEDS, CATALOG, TODAY)[0].deliveryDate,
+ "2026-04-20");
+check("razza a produzione 0: data non toccata",
+ HF.refreshDeliveryDates([res("f", "olive", 3, 1)], {}, BREEDS, CATALOG, TODAY)[0].deliveryDate,
+ "2026-04-20");
+check("la coda modificata non altera l'input (immutabilita')",
+ (function () {
+ var input = [res("g", "araucana", 1, 1)];
+ HF.refreshDeliveryDates(input, {}, BREEDS, CATALOG, TODAY);
+ return input[0].deliveryDate;
+ })(), "2026-04-20T00:00:00.000");
+
+// --- stato della card -----------------------------------------------------
+check("stato: scaduto", HF.statusKey(-1), "late");
+check("stato: oggi", HF.statusKey(0), "today");
+check("stato: domani", HF.statusKey(1), "tomorrow");
+check("stato: fra N giorni", HF.statusKey(4), "in_days");
+
+// --- date -----------------------------------------------------------------
+check("dayDiff ignora l'ora", HF.dayDiff(new Date(2026, 3, 27, 23, 59), new Date(2026, 3, 28, 0, 1)), 1);
+check("parseIso -> solo il giorno", HF.isoDay(HF.parseIso("2026-04-28T09:30:00.000")), "2026-04-28");
+check("isoDay con padding", HF.isoDay(new Date(2026, 0, 5)), "2026-01-05");
+
+console.log(failures === 0 ? "\ntutti i controlli superati" : "\n" + failures + " controlli falliti");
+process.exit(failures === 0 ? 0 : 1);
diff --git a/translations/harbour-henflow.ts b/translations/harbour-henflow.ts
index 8c38603..7b6d9c4 100644
--- a/translations/harbour-henflow.ts
+++ b/translations/harbour-henflow.ts
@@ -19,27 +19,11 @@
- Hens
+ Orders list
- name, breed, hatch date, estimated production
-
-
-
- Available eggs
-
-
-
- eggs ready today
-
-
-
- Orders
-
-
-
- customer, quantity, automatic delivery date
+ daily production, reservations, delivery status
@@ -47,7 +31,15 @@
- orders and pickups
+ undelivered reservations by day
+
+
+
+ Warehouse
+
+
+
+ eggs in stock per breed
@@ -55,15 +47,31 @@
- regular buyers
+ first name, last name, phone
- Dashboard
+ Available breeds
- summary of the day
+ catalog with photos, enable the ones you keep
+
+
+
+ My coop
+
+
+
+ registered hens, laying, estimated production
+
+
+
+ Backup
+
+
+
+ export and import the database as JSON
diff --git a/translations/harbour-henflow_it.ts b/translations/harbour-henflow_it.ts
index a120e09..59ce513 100644
--- a/translations/harbour-henflow_it.ts
+++ b/translations/harbour-henflow_it.ts
@@ -19,52 +19,60 @@
Henflow
- Hens
- Galline
+ Orders list
+ Elenco ordini
- name, breed, hatch date, estimated production
- nome, razza, data di nascita, produzione stimata
-
-
- Available eggs
- Uova disponibili
-
-
- eggs ready today
- uova pronte oggi
-
-
- Orders
- Ordini
-
-
- customer, quantity, automatic delivery date
- cliente, quantità, data di consegna automatica
+ daily production, reservations, delivery status
+ produzione giornaliera, prenotazioni, stato consegne
Calendar
Calendario
- orders and pickups
- ordini e ritiri
+ undelivered reservations by day
+ prenotazioni da consegnare per giorno
+
+
+ Warehouse
+ Magazzino
+
+
+ eggs in stock per breed
+ uova in scorta per razza
Customers
Clienti
- regular buyers
- acquirenti abituali
+ first name, last name, phone
+ nome, cognome, telefono
- Dashboard
- Dashboard
+ Available breeds
+ Razze disponibili
- summary of the day
- riepilogo della giornata
+ catalog with photos, enable the ones you keep
+ catalogo con foto, attiva quelle che allevi
+
+
+ My coop
+ Il mio pollaio
+
+
+ registered hens, laying, estimated production
+ galline registrate, in deposizione, produzione stimata
+
+
+ Backup
+ Backup
+
+
+ export and import the database as JSON
+ esporta e importa il database in JSON
Native port in progress