motore di calcolo portato in JS ES5 con 30 test node; mappatura schermate dal codice Flutter e dagli screenshot

This commit is contained in:
Carlo Baratto
2026-10-09 08:56:09 +02:00
parent 7cef6954eb
commit 0ef017cbab
7 changed files with 530 additions and 104 deletions
+255
View File
@@ -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
};
}