motore di calcolo portato in JS ES5 con 30 test node; mappatura schermate dal codice Flutter e dagli screenshot
This commit is contained in:
@@ -1,34 +1,87 @@
|
|||||||
# harbour-henflow
|
# harbour-henflow
|
||||||
|
|
||||||
Porting **nativo Sailfish OS** dell'app Android *Henflow* (`it.carlobaratto.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++,
|
UI in QML/Silica, calcolo e dati locali, pubblicazione prevista sullo
|
||||||
pubblicazione prevista sullo **store Jolla Harbour**.
|
**store Jolla Harbour**.
|
||||||
|
|
||||||
Il porting **non** usa il runtime Flutter community (`flutter-sailfishos`): quello
|
Il porting **non** usa il runtime Flutter community (`flutter-sailfishos`):
|
||||||
resta una via OpenRepos, non Harbour-safe, e non supporta i plugin nativi Android
|
quello resta una via OpenRepos, non Harbour-safe, e non supporta i plugin nativi
|
||||||
(AdMob/Firebase/Billing). Dati locali sul telefono: nessun account, nessun cloud.
|
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
|
## Stato
|
||||||
|
|
||||||
Guscio completo e pronto alla prima build (finestra, pagina principale, cover,
|
| parte | stato |
|
||||||
impostazioni con lingua, icone, catalogo traduzioni, spec RPM). Le schermate sono
|
|---|---|
|
||||||
segnaposto: vanno mappate 1:1 dai sorgenti Flutter.
|
| 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
|
## Struttura
|
||||||
|
|
||||||
```
|
```
|
||||||
harbour-henflow.pro # QT += quick qml, INSTALLS icone
|
harbour-henflow.pro # QT += quick qml, INSTALLS icone
|
||||||
rpm/harbour-henflow.spec # lrelease + desktop cercati con find (layout sorgente variabile)
|
rpm/harbour-henflow.spec # lrelease + desktop cercati con find (layout sorgente variabile)
|
||||||
rpm/harbour-henflow.desktop # profilo [X-Sailjail] esplicito (dati persistenti)
|
rpm/harbour-henflow.desktop # profilo [X-Sailjail] esplicito (dati persistenti)
|
||||||
src/main.cpp # SailfishApp, context property appSettings, lingua
|
src/main.cpp # SailfishApp, context property appSettings, lingua
|
||||||
src/settings.h/.cpp # QSettings su path esplicito AppConfigLocation + sync()
|
src/settings.h/.cpp # QSettings su path esplicito AppConfigLocation + sync()
|
||||||
qml/harbour-henflow.qml # ApplicationWindow: initialPage + cover
|
qml/harbour-henflow.qml # ApplicationWindow: initialPage + cover
|
||||||
qml/pages/MainPage.qml # segnaposto con i moduli previsti
|
qml/pages/MainPage.qml # indice dei moduli (segnaposto)
|
||||||
qml/cover/CoverPage.qml
|
qml/js/henflow.js # motore di calcolo (ES5, testabile con node)
|
||||||
icons/make_icons.py # icone 86/108/128/172 (PIL)
|
tests/engine.test.js # 30 controlli: node tests/engine.test.js
|
||||||
translations/*.ts # stringhe sorgente INGLESI + catalogo italiano
|
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
|
## Build e deploy
|
||||||
|
|
||||||
Sul PC con l'SDK Sailfish (qui non c'e' sfdk):
|
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
|
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`)
|
1. **Asset**: nel materiale di Nextcloud mancano `assets/breeds/*.jpg` (7 foto)
|
||||||
e' sufficiente: nessuna dipendenza extra e nessun driver SQL da verificare sul
|
e `pubspec.yaml`.
|
||||||
device. SQLite resta l'alternativa se servono query piu' ricche.
|
2. **Repo Gitea `HenFlow`**: cancellata (era un upload parziale); per ricrearla
|
||||||
|
serve un permesso che il token attuale non ha (`write:user`).
|
||||||
| entita' | campi |
|
3. **Ambito**: modalita' cliente e 4 lingue (EN/IT/FR/DE) da confermare.
|
||||||
|---|---|
|
|
||||||
| `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/
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ OTHER_FILES += \
|
|||||||
qml/harbour-henflow.qml \
|
qml/harbour-henflow.qml \
|
||||||
qml/pages/*.qml \
|
qml/pages/*.qml \
|
||||||
qml/cover/*.qml \
|
qml/cover/*.qml \
|
||||||
|
qml/js/henflow.js \
|
||||||
rpm/harbour-henflow.spec \
|
rpm/harbour-henflow.spec \
|
||||||
rpm/harbour-henflow.desktop \
|
rpm/harbour-henflow.desktop \
|
||||||
translations/harbour-henflow.ts \
|
translations/harbour-henflow.ts \
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
+11
-8
@@ -15,15 +15,18 @@ Page {
|
|||||||
|
|
||||||
model: ListModel {
|
model: ListModel {
|
||||||
id: sections
|
id: sections
|
||||||
// Moduli previsti dall'app Android, da mappare sulle schermate
|
// Struttura reale dell'app Flutter (lib/main.dart): la Home ha le tab
|
||||||
// Flutter quando i sorgenti arrivano sul server.
|
// "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: {
|
Component.onCompleted: {
|
||||||
append({ "name": qsTr("Hens"), "note": qsTr("name, breed, hatch date, estimated production") })
|
append({ "name": qsTr("Orders list"), "note": qsTr("daily production, reservations, delivery status") })
|
||||||
append({ "name": qsTr("Available eggs"), "note": qsTr("eggs ready today") })
|
append({ "name": qsTr("Calendar"), "note": qsTr("undelivered reservations by day") })
|
||||||
append({ "name": qsTr("Orders"), "note": qsTr("customer, quantity, automatic delivery date") })
|
append({ "name": qsTr("Warehouse"), "note": qsTr("eggs in stock per breed") })
|
||||||
append({ "name": qsTr("Calendar"), "note": qsTr("orders and pickups") })
|
append({ "name": qsTr("Customers"), "note": qsTr("first name, last name, phone") })
|
||||||
append({ "name": qsTr("Customers"), "note": qsTr("regular buyers") })
|
append({ "name": qsTr("Available breeds"), "note": qsTr("catalog with photos, enable the ones you keep") })
|
||||||
append({ "name": qsTr("Dashboard"), "note": qsTr("summary of the day") })
|
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") })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -19,27 +19,11 @@
|
|||||||
<translation></translation>
|
<translation></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>Hens</source>
|
<source>Orders list</source>
|
||||||
<translation></translation>
|
<translation></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>name, breed, hatch date, estimated production</source>
|
<source>daily production, reservations, delivery status</source>
|
||||||
<translation></translation>
|
|
||||||
</message>
|
|
||||||
<message>
|
|
||||||
<source>Available eggs</source>
|
|
||||||
<translation></translation>
|
|
||||||
</message>
|
|
||||||
<message>
|
|
||||||
<source>eggs ready today</source>
|
|
||||||
<translation></translation>
|
|
||||||
</message>
|
|
||||||
<message>
|
|
||||||
<source>Orders</source>
|
|
||||||
<translation></translation>
|
|
||||||
</message>
|
|
||||||
<message>
|
|
||||||
<source>customer, quantity, automatic delivery date</source>
|
|
||||||
<translation></translation>
|
<translation></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
@@ -47,7 +31,15 @@
|
|||||||
<translation></translation>
|
<translation></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>orders and pickups</source>
|
<source>undelivered reservations by day</source>
|
||||||
|
<translation></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Warehouse</source>
|
||||||
|
<translation></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>eggs in stock per breed</source>
|
||||||
<translation></translation>
|
<translation></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
@@ -55,15 +47,31 @@
|
|||||||
<translation></translation>
|
<translation></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>regular buyers</source>
|
<source>first name, last name, phone</source>
|
||||||
<translation></translation>
|
<translation></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>Dashboard</source>
|
<source>Available breeds</source>
|
||||||
<translation></translation>
|
<translation></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>summary of the day</source>
|
<source>catalog with photos, enable the ones you keep</source>
|
||||||
|
<translation></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>My coop</source>
|
||||||
|
<translation></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>registered hens, laying, estimated production</source>
|
||||||
|
<translation></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Backup</source>
|
||||||
|
<translation></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>export and import the database as JSON</source>
|
||||||
<translation></translation>
|
<translation></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
|
|||||||
@@ -19,52 +19,60 @@
|
|||||||
<translation>Henflow</translation>
|
<translation>Henflow</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>Hens</source>
|
<source>Orders list</source>
|
||||||
<translation>Galline</translation>
|
<translation>Elenco ordini</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>name, breed, hatch date, estimated production</source>
|
<source>daily production, reservations, delivery status</source>
|
||||||
<translation>nome, razza, data di nascita, produzione stimata</translation>
|
<translation>produzione giornaliera, prenotazioni, stato consegne</translation>
|
||||||
</message>
|
|
||||||
<message>
|
|
||||||
<source>Available eggs</source>
|
|
||||||
<translation>Uova disponibili</translation>
|
|
||||||
</message>
|
|
||||||
<message>
|
|
||||||
<source>eggs ready today</source>
|
|
||||||
<translation>uova pronte oggi</translation>
|
|
||||||
</message>
|
|
||||||
<message>
|
|
||||||
<source>Orders</source>
|
|
||||||
<translation>Ordini</translation>
|
|
||||||
</message>
|
|
||||||
<message>
|
|
||||||
<source>customer, quantity, automatic delivery date</source>
|
|
||||||
<translation>cliente, quantità, data di consegna automatica</translation>
|
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>Calendar</source>
|
<source>Calendar</source>
|
||||||
<translation>Calendario</translation>
|
<translation>Calendario</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>orders and pickups</source>
|
<source>undelivered reservations by day</source>
|
||||||
<translation>ordini e ritiri</translation>
|
<translation>prenotazioni da consegnare per giorno</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Warehouse</source>
|
||||||
|
<translation>Magazzino</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>eggs in stock per breed</source>
|
||||||
|
<translation>uova in scorta per razza</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>Customers</source>
|
<source>Customers</source>
|
||||||
<translation>Clienti</translation>
|
<translation>Clienti</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>regular buyers</source>
|
<source>first name, last name, phone</source>
|
||||||
<translation>acquirenti abituali</translation>
|
<translation>nome, cognome, telefono</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>Dashboard</source>
|
<source>Available breeds</source>
|
||||||
<translation>Dashboard</translation>
|
<translation>Razze disponibili</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>summary of the day</source>
|
<source>catalog with photos, enable the ones you keep</source>
|
||||||
<translation>riepilogo della giornata</translation>
|
<translation>catalogo con foto, attiva quelle che allevi</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>My coop</source>
|
||||||
|
<translation>Il mio pollaio</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>registered hens, laying, estimated production</source>
|
||||||
|
<translation>galline registrate, in deposizione, produzione stimata</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Backup</source>
|
||||||
|
<translation>Backup</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>export and import the database as JSON</source>
|
||||||
|
<translation>esporta e importa il database in JSON</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>Native port in progress</source>
|
<source>Native port in progress</source>
|
||||||
|
|||||||
Reference in New Issue
Block a user