From 40e4badbd474187d6b5ca07f7f9f83d51fff9763 Mon Sep 17 00:00:00 2001 From: Carlo Baratto Date: Fri, 9 Oct 2026 09:09:31 +0200 Subject: [PATCH] livello dati C++ (datastore JSON, 33 controlli) e schermate Home, scheda ordine e cover; .gitignore per gli artefatti --- .gitignore | 17 ++ README.md | 38 +++-- harbour-henflow.pro | 6 +- qml/cover/CoverPage.qml | 68 +++++++- qml/js/henflow.js | 25 ++- qml/js/i18n.js | 13 +- qml/pages/MainPage.qml | 332 ++++++++++++++++++++++++++++++++++---- qml/pages/OrderPage.qml | 301 ++++++++++++++++++++++++++++++++++ src/datastore.cpp | 216 +++++++++++++++++++++++++ src/datastore.h | 66 ++++++++ src/main.cpp | 3 + tests/cpp_test.sh | 23 +++ tests/engine.test.js | 4 + tests/harness/harness.cpp | 157 ++++++++++++++++++ tests/harness/harness.pro | 16 ++ tests/i18n.test.js | 6 + tools/gen_from_flutter.py | 13 +- 17 files changed, 1248 insertions(+), 56 deletions(-) create mode 100644 .gitignore create mode 100644 qml/pages/OrderPage.qml create mode 100644 src/datastore.cpp create mode 100644 src/datastore.h create mode 100644 tests/cpp_test.sh create mode 100644 tests/harness/harness.cpp create mode 100644 tests/harness/harness.pro diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..98bb5bd --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# build Sailfish (sfdk / Qt Creator) +build-*/ +*.o +moc_*.cpp +moc_*.h +qrc_*.cpp +Makefile + +# banco di prova C++ (compilato da tests/cpp_test.sh): nel repo solo sorgenti +tests/harness/* +!tests/harness/harness.cpp +!tests/harness/harness.pro + +# artefatti di packaging +*.tar.gz +*.qm +RPMS/ diff --git a/README.md b/README.md index 1e8f8ca..3ad4c31 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,14 @@ Android (AdMob/File picker/Share). Nessun account, nessun cloud, come l'original | parte | stato | |---|---| | Packaging (spec RPM, desktop SailJail, icone, i18n) | pronto | -| Guscio app (finestra, cover, Settings con lingua) | pronto | +| Livello dati C++ (`src/datastore.*`, JSON compatibile con l'app Android) | fatto e verificato | | Motore di calcolo (`qml/js/henflow.js`) | portato e verificato con node | | Traduzioni 4 lingue (`qml/js/i18n.js`, 126 chiavi) | portate e verificate con node | | Catalogo razze con foto (`qml/js/catalog.js`) | portato e verificato con node | -| Schermate QML | da implementare | +| Home con produzione stimata e coda ordini | fatta | +| Scheda ordine (nuovo/modifica, data calcolata) | fatta | +| Cover con produzione e ordini pendenti | fatta | +| Razze / pollaio / galline, clienti, backup, lingua, intro | da fare | ## Struttura @@ -30,20 +33,36 @@ Android (AdMob/File picker/Share). Nessun account, nessun cloud, come l'original 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/main.cpp # SailfishApp, context property appSettings/appData, lingua src/settings.h/.cpp # QSettings su path esplicito AppConfigLocation + sync() +src/datastore.h/.cpp # dati in un JSON in AppConfigLocation (chiavi = SharedPreferences dell'app) qml/harbour-henflow.qml # ApplicationWindow: initialPage + cover -qml/pages/MainPage.qml # indice dei moduli (segnaposto) +qml/pages/MainPage.qml # Home: produzione stimata, coda ordini, menu nuovo ordine +qml/pages/OrderPage.qml # nuovo/modifica ordine con anteprima della data di consegna +qml/cover/CoverPage.qml # produzione del giorno e ordini pendenti qml/js/henflow.js # motore di calcolo (ES5, testabile con node) qml/js/i18n.js # 126 chiavi x 4 lingue + t(chiave, arg, lingua) qml/js/catalog.js # 7 razze: nome/descrizione/colore uova/foto assets/breeds/*.jpg # foto delle razze (installate nel datadir) tools/gen_from_flutter.py # rigenera i18n.js e catalog.js dai sorgenti Flutter -tests/run.sh # tests/engine.test.js + tests/i18n.test.js +tests/run.sh # motore + traduzioni (node) +tests/cpp_test.sh # livello dati (Qt 5.15 locale, config in cartella temporanea) icons/make_icons.py # icone 86/108/128/172 dall'artwork originale -translations/*.ts # stringhe statiche delle pagine QML (EN + IT) +translations/*.ts # stringhe statiche QML (il testo dell'app viene da i18n.js) ``` +## Test + +```sh +sh tests/run.sh # motore di calcolo + traduzioni e catalogo (node, nessuna dipendenza) +sh tests/cpp_test.sh # livello dati: compila src/datastore.cpp col Qt locale e prova + # persistenza, rilettura, compatibilita' delle chiavi, backup, file corrotto +``` + +Il testo dell'app vive in `i18n.js` (come il TranslationService dell'originale), quindi +la lingua cambia **a runtime** senza riavviare l'app: cosa che i `.ts` di Qt 5.6 non +permettono. La lingua predefinita segue quella del telefono. + ## Motore di calcolo (portato fedelmente da `lib/main.dart`) - **Produzione stimata** per razza = galline in deposizione x uova/gallina/giorno @@ -106,9 +125,10 @@ git archive --format=tar.gz --prefix=harbour-henflow-0.1.0/ -o harbour-henflow-0 ## Aperti -1. **Repo Gitea `HenFlow`**: cancellata (era un upload parziale); da ricreare - dalla UI, poi `workspace/henflow-flutter` va pushata li'. -2. **Schermate QML**: da implementare (solo modalita' allevatore, 4 lingue). +1. **Schermate da fare**: razze disponibili / mio pollaio / anagrafica galline, + clienti, backup-ripristino, scelta lingua, introduzione iniziale, calendario. +2. **Verifica su device**: la UI Silica non e' verificabile senza SDK/dispositivo + (qui si verificano solo motore, traduzioni, catalogo e livello dati). ## Correzioni rispetto all'originale (bug trovati nei sorgenti Flutter) diff --git a/harbour-henflow.pro b/harbour-henflow.pro index 1bb8e43..b0fd69d 100644 --- a/harbour-henflow.pro +++ b/harbour-henflow.pro @@ -10,10 +10,12 @@ QT += quick qml SOURCES += \ src/main.cpp \ - src/settings.cpp + src/settings.cpp \ + src/datastore.cpp HEADERS += \ - src/settings.h + src/settings.h \ + src/datastore.h OTHER_FILES += \ qml/harbour-henflow.qml \ diff --git a/qml/cover/CoverPage.qml b/qml/cover/CoverPage.qml index 01f3f14..e407057 100644 --- a/qml/cover/CoverPage.qml +++ b/qml/cover/CoverPage.qml @@ -1,7 +1,40 @@ import QtQuick 2.2 import Sailfish.Silica 1.0 +import "../js/henflow.js" as HF +import "../js/i18n.js" as I18N +import "../js/catalog.js" as Catalog +// Cover: produzione stimata del giorno e ordini ancora da consegnare. CoverBackground { + id: cover + + property string lang: "en" + property real production: 0 + property int pending: 0 + + function tr(key, arg) { return I18N.t(key, arg, cover.lang) } + + function refresh() { + cover.lang = I18N.langFor(appSettings.language, Qt.locale().name) + cover.production = HF.totalDailyProduction(appData.breedSettings(), Catalog.HF_BREEDS) + + var list = appData.reservations() + var n = 0 + for (var i = 0; i < list.length; i++) { + if (!list[i].isDelivered) + n++ + } + cover.pending = n + } + + Component.onCompleted: refresh() + + // i dati cambiano quando si salvano ordini o galline + Connections { + target: appData + onChanged: cover.refresh() + } + Label { id: title anchors { @@ -9,17 +42,38 @@ CoverBackground { topMargin: Theme.paddingLarge horizontalCenter: parent.horizontalCenter } - text: qsTr("Henflow") + text: cover.tr("app_title") color: Theme.primaryColor } - Label { + Column { anchors.centerIn: parent width: parent.width - 2 * Theme.paddingLarge - text: qsTr("Eggs, hens and orders") - font.pixelSize: Theme.fontSizeSmall - color: Theme.secondaryColor - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap + spacing: Theme.paddingSmall + + Label { + width: parent.width + horizontalAlignment: Text.AlignHCenter + text: cover.production.toFixed(1) + font.pixelSize: Theme.fontSizeHuge + color: Theme.highlightColor + } + + Label { + width: parent.width + horizontalAlignment: Text.AlignHCenter + text: cover.tr("eggs_per_day") + font.pixelSize: Theme.fontSizeSmall + color: Theme.secondaryColor + } + + Label { + width: parent.width + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + text: cover.tr("reservations") + ": " + cover.pending + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + } } } diff --git a/qml/js/henflow.js b/qml/js/henflow.js index 907bc38..d4efab9 100644 --- a/qml/js/henflow.js +++ b/qml/js/henflow.js @@ -229,6 +229,26 @@ function statusKey(days) { return "in_days"; } +// Data come nella card dell'app Android: gg/mm/aaaa. +function formatDay(iso) { + var d = parseIso(iso); + if (!d) return String(iso || ""); + return pad2(d.getDate()) + "/" + pad2(d.getMonth() + 1) + "/" + d.getFullYear(); +} + +// Istante corrente in ISO 8601 (usato per createdAt delle prenotazioni). +function nowIso() { + var n = new Date(); + return n.getFullYear() + "-" + pad2(n.getMonth() + 1) + "-" + pad2(n.getDate()) + + "T" + pad2(n.getHours()) + ":" + pad2(n.getMinutes()) + ":" + pad2(n.getSeconds()) + + ".000"; +} + +// Identificativo come nell'app Flutter (microsecondi dell'epoca). +function newId() { + return String(new Date().getTime()) + String(Math.floor(Math.random() * 1000)); +} + // 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) { @@ -250,6 +270,9 @@ if (typeof module !== "undefined" && module.exports) { pendingEggs: pendingEggs, estimateDeliveryDate: estimateDeliveryDate, refreshDeliveryDates: refreshDeliveryDates, - statusKey: statusKey + statusKey: statusKey, + formatDay: formatDay, + nowIso: nowIso, + newId: newId }; } diff --git a/qml/js/i18n.js b/qml/js/i18n.js index 447446a..353a02d 100644 --- a/qml/js/i18n.js +++ b/qml/js/i18n.js @@ -546,10 +546,21 @@ function availableLanguages() { return out.sort(); } +// Lingua effettiva: 'system' segue la lingua del telefono (nome tipo "it_IT"), +// altrimenti si usa il codice scelto. Fallback: inglese. +function langFor(setting, systemName) { + if (setting && setting !== "system") { + return HF_STRINGS[setting] ? setting : "en"; + } + var code = String(systemName || "").slice(0, 2).toLowerCase(); + return HF_STRINGS[code] ? code : "en"; +} + if (typeof module !== "undefined" && module.exports) { module.exports = { HF_STRINGS: HF_STRINGS, t: t, - availableLanguages: availableLanguages + availableLanguages: availableLanguages, + langFor: langFor }; } diff --git a/qml/pages/MainPage.qml b/qml/pages/MainPage.qml index bc5a035..d668277 100644 --- a/qml/pages/MainPage.qml +++ b/qml/pages/MainPage.qml @@ -1,71 +1,333 @@ import QtQuick 2.2 import Sailfish.Silica 1.0 +import "../js/henflow.js" as HF +import "../js/i18n.js" as I18N +import "../js/catalog.js" as Catalog +// Home: produzione stimata del giorno e coda degli ordini. +// Porting di HomePage + _buildOrdersListTab dell'app Flutter. Page { id: page allowedOrientations: Orientation.All + property var breeds: [] + property var reservations: [] + property var stock: ({}) + property var productionRows: [] + property real totalProduction: 0 + property string hintMsg: "" + property string banner: "" + property string lang: "en" + property bool ready: false + + function tr(key, arg) { return I18N.t(key, arg, page.lang) } + + function breedLabel(breedId) { + var c = Catalog.breedFromId(breedId) + return c ? page.tr(c.name) : breedId + } + + function reload() { + page.lang = I18N.langFor(appSettings.language, Qt.locale().name) + page.breeds = appData.breedSettings() + page.reservations = appData.reservations() + page.stock = appData.warehouseStock() + page.accrue() + page.totalProduction = HF.totalDailyProduction(page.breeds, Catalog.HF_BREEDS) + page.productionRows = HF.productionDetail(page.breeds, Catalog.HF_BREEDS) + page.rebuildList() + page.ready = true + } + + // Accrual del magazzino all'apertura (come _loadData in main.dart): i giorni + // passati dall'ultimo aggiornamento aggiungono la produzione giornaliera; + // poi le date di consegna vengono ricalcolate sulla coda FIFO. + function accrue() { + var today = HF.todayMidnight() + var last = appData.lastProductionUpdate() + var since = last ? HF.parseIso(last) : HF.addDays(today, -1) + var days = HF.dayDiff(since, today) + if (days > 0) { + page.stock = HF.accrueStock(page.stock, page.breeds, Catalog.HF_BREEDS, days) + appData.saveWarehouseStock(page.stock) + } + page.reservations = HF.refreshDeliveryDates(page.reservations, page.stock, + page.breeds, Catalog.HF_BREEDS, today) + appData.saveReservations(page.reservations) + appData.saveLastProductionUpdate(HF.isoDay(today)) + } + + function rebuildList() { + orderModel.clear() + var today = HF.todayMidnight() + for (var i = 0; i < page.reservations.length; i++) { + var r = page.reservations[i] + var days = HF.dayDiff(today, HF.parseIso(r.deliveryDate)) + var status + if (r.isDelivered) status = page.tr("ready_delivery") + else if (days < 0) status = page.tr("expired") + else if (days === 0) status = page.tr("today") + else if (days === 1) status = page.tr("in_day") + else status = page.tr("in_days", days) + + orderModel.append({ + "customer": r.customerName, + "breed": page.breedLabel(r.breedId), + "eggs": r.eggsRequested, + "delivery": HF.formatDay(r.deliveryDate), + "status": status, + "late": (!r.isDelivered && days < 0), + "delivered": r.isDelivered + }) + } + page.hintMsg = page.reservations.length === 0 + ? page.tr("no_reservations") + "\n" + page.tr("use_button") + : "" + } + + function showBanner(text) { + page.banner = text + bannerTimer.restart() + } + + function openOrder(index) { + if (page.breeds.length === 0) { + page.showBanner(page.tr("config_breed_first")) + return + } + var reservation = null + if (index >= 0 && index < page.reservations.length) { + reservation = page.reservations[index] + } else if (page.totalProduction <= 0) { + page.showBanner(page.tr("need_hen_error")) + return + } + pageStack.push(Qt.resolvedUrl("OrderPage.qml"), + { "reservation": reservation, + "onSaved": function () { page.reload() } }) + } + + function toggleDelivered(index) { + var list = page.reservations.slice() + if (index < 0 || index >= list.length) + return + list[index].isDelivered = !list[index].isDelivered + page.reservations = list + appData.saveReservations(list) + page.rebuildList() + } + + function deleteOrder(index, item) { + var list = page.reservations.slice() + if (index < 0 || index >= list.length) + return + list.splice(index, 1) + page.reservations = list + appData.saveReservations(list) + page.rebuildList() + remorse.execute(item, page.tr("delete"), function () {}, 3000) + } + + ListModel { id: orderModel } + + RemorseItem { id: remorse } + + Timer { + id: bannerTimer + interval: 3000 + onTriggered: page.banner = "" + } + SilicaListView { id: listView anchors.fill: parent + model: orderModel - header: PageHeader { - title: qsTr("Henflow") - } + header: Column { + width: listView.width - model: ListModel { - id: sections - // 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("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") }) + PageHeader { + title: page.tr("app_title") + } + + // card della produzione stimata + Rectangle { + x: Theme.horizontalPageMargin + width: listView.width - 2 * Theme.horizontalPageMargin + height: productionColumn.height + 2 * Theme.paddingMedium + radius: Theme.paddingSmall + color: Theme.rgba(Theme.highlightBackgroundColor, 0.12) + + Column { + id: productionColumn + x: Theme.paddingMedium + y: Theme.paddingMedium + width: parent.width - 2 * Theme.paddingMedium + spacing: Theme.paddingSmall + + Label { + text: page.tr("daily_prod_est") + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + } + + Label { + text: page.totalProduction.toFixed(1) + " " + page.tr("eggs_per_day") + font.pixelSize: Theme.fontSizeHuge + color: Theme.highlightColor + } + + Repeater { + model: page.productionRows.length + + delegate: Label { + width: parent.width + truncationMode: TruncationMode.Fade + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + text: page.breedLabel(page.productionRows[index].breedId) + + " (" + page.productionRows[index].hens + " " + + page.tr("hens") + ") " + + page.productionRows[index].daily.toFixed(1) + " " + + page.tr("eggs_per_g") + } + } + + Label { + visible: page.productionRows.length === 0 + width: parent.width + wrapMode: Text.WordWrap + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + text: page.tr("no_breeds") + } + } + } + + Label { + x: Theme.horizontalPageMargin + text: page.tr("reservations") + font.pixelSize: Theme.fontSizeSmall + color: Theme.secondaryColor } } delegate: ListItem { - id: item - contentHeight: Theme.itemSizeMedium + id: orderItem + contentHeight: itemColumn.height + 2 * Theme.paddingMedium + + menu: ContextMenu { + MenuItem { + text: orderItem.delivered ? page.tr("new_order") : page.tr("ready_delivery") + onClicked: page.toggleDelivered(index) + } + MenuItem { + text: page.tr("edit_order") + onClicked: page.openOrder(index) + } + MenuItem { + text: page.tr("delete") + onClicked: page.deleteOrder(index, orderItem) + } + } Column { - anchors.verticalCenter: parent.verticalCenter + id: itemColumn x: Theme.horizontalPageMargin - width: parent.width - 2 * Theme.horizontalPageMargin + y: Theme.paddingMedium + width: parent.width - 2 * Theme.horizontalPageMargin - statusLabel.width Label { - text: model.name - color: item.highlighted ? Theme.highlightColor : Theme.primaryColor - truncationMode: TruncationMode.Fade width: parent.width + text: model.customer + truncationMode: TruncationMode.Fade + color: orderItem.highlighted ? Theme.highlightColor : Theme.primaryColor + font.pixelSize: Theme.fontSizeSmall } Label { - text: model.note + width: parent.width + truncationMode: TruncationMode.Fade font.pixelSize: Theme.fontSizeExtraSmall color: Theme.secondaryColor - truncationMode: TruncationMode.Fade - width: parent.width + text: page.tr("breed") + ": " + model.breed + + " " + page.tr("eggs_req") + ": " + model.eggs } + + Label { + width: parent.width + truncationMode: TruncationMode.Fade + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + text: page.tr("delivery") + ": " + model.delivery + } + } + + Label { + id: statusLabel + anchors { + right: parent.right + rightMargin: Theme.horizontalPageMargin + verticalCenter: parent.verticalCenter + } + text: model.status + font.pixelSize: Theme.fontSizeExtraSmall + color: model.late ? Theme.errorColor + : (model.delivered ? Theme.secondaryColor : Theme.highlightColor) } } - footer: Label { - x: Theme.horizontalPageMargin - width: parent.width - 2 * Theme.horizontalPageMargin - text: qsTr("Native port in progress") - font.pixelSize: Theme.fontSizeExtraSmall - color: Theme.secondaryHighlightColor - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap + PullDownMenu { + MenuItem { + text: page.tr("new_order") + onClicked: page.openOrder(-1) + } + } + + footer: Item { + width: listView.width + height: footerLabel.height + 3 * Theme.paddingLarge + + Label { + id: footerLabel + x: Theme.horizontalPageMargin + y: Theme.paddingMedium + width: parent.width - 2 * Theme.horizontalPageMargin + visible: page.hintMsg.length > 0 + text: page.hintMsg + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryHighlightColor + } } VerticalScrollDecorator {} } + + // banner in basso: Label + Timer (in Silica non esiste un Toast) + Rectangle { + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + height: bannerLabel.height + 2 * Theme.paddingMedium + color: Theme.rgba(Theme.highlightBackgroundColor, 0.9) + visible: page.banner.length > 0 + + Label { + id: bannerLabel + x: Theme.horizontalPageMargin + y: Theme.paddingMedium + width: parent.width - 2 * Theme.horizontalPageMargin + text: page.banner + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.highlightColor + } + } + + Component.onCompleted: page.reload() } diff --git a/qml/pages/OrderPage.qml b/qml/pages/OrderPage.qml new file mode 100644 index 0000000..74a5a92 --- /dev/null +++ b/qml/pages/OrderPage.qml @@ -0,0 +1,301 @@ +import QtQuick 2.2 +import Sailfish.Silica 1.0 +import "../js/henflow.js" as HF +import "../js/i18n.js" as I18N +import "../js/catalog.js" as Catalog + +// Scheda ordine: nuovo ordine o modifica di uno esistente. +// Porting di AddReservationSheet + _calculateDeliveryDate: la data di consegna +// viene calcolata dalla coda FIFO e mostrata in anteprima mentre si digita. +Page { + id: page + allowedOrientations: Orientation.All + + property var reservation: null // null = nuovo ordine + property var onSaved: null // callback della pagina chiamante (ricarica la Home) + property var breeds: [] + property var choices: [] // razze attive con galline in deposizione + property int selected: -1 + property string lang: "en" + property string errorMsg: "" + + function tr(key, arg) { return I18N.t(key, arg, page.lang) } + + function load() { + page.lang = I18N.langFor(appSettings.language, Qt.locale().name) + page.breeds = appData.breedSettings() + + var list = [] + for (var i = 0; i < page.breeds.length; i++) { + if (HF.activeHens(page.breeds[i]) > 0) + list.push(page.breeds[i].breedId) + } + page.choices = list + + if (page.reservation) { + customerField.text = page.reservation.customerName + eggsField.text = String(page.reservation.eggsRequested) + for (var j = 0; j < list.length; j++) { + if (list[j] === page.reservation.breedId) + page.selected = j + } + } + if (page.selected < 0 && list.length > 0) + page.selected = 0 + + page.updatePreview() + } + + // Data prevista: parte dalle scorte e dalla coda corrente (_calculateDeliveryDate). + function updatePreview() { + if (page.selected < 0) { + previewLabel.text = page.tr("config_breed_first") + return + } + var eggs = parseInt(eggsField.text) + if (!eggs || eggs <= 0) + eggs = 0 + + var breedId = page.choices[page.selected] + var today = HF.todayMidnight() + var delivery = HF.estimateDeliveryDate(appData.reservations(), + appData.warehouseStock(), + page.breeds, Catalog.HF_BREEDS, + breedId, eggs, today) + var days = HF.dayDiff(today, HF.parseIso(delivery)) + var when + if (days <= 0) when = page.tr("today") + else if (days === 1) when = page.tr("in_day") + else when = page.tr("in_days", days) + + previewLabel.text = HF.formatDay(delivery) + " (" + when + ")" + } + + function save() { + if (page.selected < 0) { + page.errorMsg = page.tr("config_breed_first") + return + } + var name = customerField.text.trim() + if (name.length === 0) { + page.errorMsg = page.tr("customer_name") + return + } + var eggs = parseInt(eggsField.text) + if (!eggs || eggs <= 0) { + page.errorMsg = page.tr("eggs_req") + return + } + + var breedId = page.choices[page.selected] + var list = appData.reservations() + var today = HF.todayMidnight() + var delivery = HF.estimateDeliveryDate(list, appData.warehouseStock(), + page.breeds, Catalog.HF_BREEDS, + breedId, eggs, today) + + if (page.reservation) { + for (var i = 0; i < list.length; i++) { + if (list[i].id === page.reservation.id) { + list[i].customerName = name + list[i].breedId = breedId + list[i].eggsRequested = eggs + list[i].deliveryDate = delivery + break + } + } + } else { + list.push({ + "id": HF.newId(), + "customerName": name, + "breedId": breedId, + "eggsRequested": eggs, + "createdAt": HF.nowIso(), + "deliveryDate": delivery, + "isDelivered": false + }) + } + appData.saveReservations(list) + if (page.onSaved) + page.onSaved() + pageStack.pop() + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: formColumn.height + + Column { + id: formColumn + width: parent.width + + PageHeader { + title: page.reservation ? page.tr("edit_order") : page.tr("new_order") + } + + TextField { + id: customerField + anchors.left: parent.left + anchors.right: parent.right + label: page.tr("customer_name") + placeholderText: page.tr("customer_name") + inputMethodHints: Qt.ImhNoPredictiveText + onTextChanged: page.errorMsg = "" + } + + // suggerimenti dai clienti registrati + Repeater { + model: { + var out = [] + var q = customerField.text.toLowerCase() + if (q.length < 2) + return 0 + var customers = appData.customers() + for (var i = 0; i < customers.length && out.length < 3; i++) { + var full = (customers[i].firstName + " " + customers[i].lastName).trim() + if (full.toLowerCase().indexOf(q) === 0) + out.push(full) + } + return out.length + } + + delegate: BackgroundItem { + width: formColumn.width + height: Theme.itemSizeSmall + + Label { + x: Theme.horizontalPageMargin + anchors.verticalCenter: parent.verticalCenter + width: parent.width - 2 * Theme.horizontalPageMargin + truncationMode: TruncationMode.Fade + color: parent.highlighted ? Theme.highlightColor : Theme.secondaryColor + text: page.suggestions(index) + } + + onClicked: { + customerField.text = page.suggestions(index) + page.errorMsg = "" + } + } + } + + Label { + x: Theme.horizontalPageMargin + width: parent.width - 2 * Theme.horizontalPageMargin + text: page.tr("breed") + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + } + + Repeater { + model: page.choices.length + + delegate: BackgroundItem { + id: breedRow + width: formColumn.width + height: Theme.itemSizeSmall + + Label { + x: Theme.horizontalPageMargin + anchors.verticalCenter: parent.verticalCenter + width: parent.width - 2 * Theme.horizontalPageMargin + truncationMode: TruncationMode.Fade + color: (index === page.selected) + ? Theme.highlightColor + : (breedRow.highlighted ? Theme.highlightColor : Theme.primaryColor) + text: page.breedLabel(page.choices[index]) + + " ยท " + page.tr("in_laying") + ": " + + HF.activeHens(page.breedIndex(page.choices[index])) + } + + onClicked: { + page.selected = index + page.updatePreview() + } + } + } + + Label { + x: Theme.horizontalPageMargin + width: parent.width - 2 * Theme.horizontalPageMargin + visible: page.choices.length === 0 + wrapMode: Text.WordWrap + text: page.tr("config_breed_first") + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.errorColor + } + + TextField { + id: eggsField + anchors.left: parent.left + anchors.right: parent.right + label: page.tr("eggs_req") + inputMethodHints: Qt.ImhDigitsOnly + validator: IntValidator { bottom: 1; top: 9999 } + onTextChanged: { + page.errorMsg = "" + page.updatePreview() + } + } + + Label { + x: Theme.horizontalPageMargin + width: parent.width - 2 * Theme.horizontalPageMargin + text: page.tr("delivery_preview") + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + } + + Label { + id: previewLabel + x: Theme.horizontalPageMargin + width: parent.width - 2 * Theme.horizontalPageMargin + font.pixelSize: Theme.fontSizeMedium + color: Theme.highlightColor + } + + Label { + x: Theme.horizontalPageMargin + width: parent.width - 2 * Theme.horizontalPageMargin + visible: page.errorMsg.length > 0 + text: page.errorMsg + wrapMode: Text.WordWrap + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.errorColor + } + + Button { + anchors.horizontalCenter: parent.horizontalCenter + text: page.tr("save_order") + onClicked: page.save() + } + } + } + + function breedLabel(breedId) { + var c = Catalog.breedFromId(breedId) + return c ? page.tr(c.name) : breedId + } + + function breedIndex(breedId) { + for (var i = 0; i < page.breeds.length; i++) { + if (page.breeds[i].breedId === breedId) + return i + } + return -1 + } + + function suggestions(index) { + var out = [] + var q = customerField.text.toLowerCase() + var customers = appData.customers() + for (var i = 0; i < customers.length && out.length < 3; i++) { + var full = (customers[i].firstName + " " + customers[i].lastName).trim() + if (full.toLowerCase().indexOf(q) === 0) + out.push(full) + } + return out[index] + } + + Component.onCompleted: page.load() +} diff --git a/src/datastore.cpp b/src/datastore.cpp new file mode 100644 index 0000000..26de226 --- /dev/null +++ b/src/datastore.cpp @@ -0,0 +1,216 @@ +#include "datastore.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +// Chiavi del file: identiche a quelle SharedPreferences dell'app Flutter. +const char *kVersion = "version"; +const char *kBreedSettings = "breed_settings_v2"; +const char *kReservations = "reservations_v2"; +const char *kCustomers = "customers_v1"; +const char *kWarehouseStock = "warehouse_stock_v1"; +const char *kLastUpdate = "last_production_update_v1"; + +} // namespace + +DataStore::DataStore(QObject *parent) + : QObject(parent) + , m_dirty(false) +{ + load(); +} + +QString DataStore::dataPath() +{ + // AppConfigLocation dipende da organizationName/applicationName impostati + // nel main.cpp: ~/.config/harbour/henflow (zona persistente della sandbox). + const QString dir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); + if (!dir.isEmpty()) + QDir().mkpath(dir); + return dir + QStringLiteral("/henflow.json"); +} + +QString DataStore::dataFilePath() const +{ + return dataPath(); +} + +bool DataStore::load() +{ + QFile file(dataPath()); + if (!file.exists()) + return true; // primo avvio: dati vuoti + + if (!file.open(QIODevice::ReadOnly)) { + qWarning() << "henflow: cannot read" << file.fileName() << file.errorString(); + return false; + } + + QJsonParseError error; + const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error); + file.close(); + + if (error.error != QJsonParseError::NoError) { + // File corrotto: si tiene quello che c'e' in memoria e si avvisa. + qWarning() << "henflow: JSON non valido in" << file.fileName() + << "offset" << error.offset << error.errorString(); + return false; + } + + const QJsonObject root = doc.object(); + m_breedSettings = root.value(QLatin1String(kBreedSettings)).toArray().toVariantList(); + m_reservations = root.value(QLatin1String(kReservations)).toArray().toVariantList(); + m_customers = root.value(QLatin1String(kCustomers)).toArray().toVariantList(); + m_stock = root.value(QLatin1String(kWarehouseStock)).toObject().toVariantMap(); + m_lastProductionUpdate = root.value(QLatin1String(kLastUpdate)).toString(); + m_dirty = false; + return true; +} + +bool DataStore::store() +{ + QJsonObject root; + root.insert(QLatin1String(kVersion), 1); + root.insert(QLatin1String(kBreedSettings), QJsonArray::fromVariantList(m_breedSettings)); + root.insert(QLatin1String(kReservations), QJsonArray::fromVariantList(m_reservations)); + root.insert(QLatin1String(kCustomers), QJsonArray::fromVariantList(m_customers)); + root.insert(QLatin1String(kWarehouseStock), QJsonObject::fromVariantMap(m_stock)); + if (!m_lastProductionUpdate.isEmpty()) + root.insert(QLatin1String(kLastUpdate), m_lastProductionUpdate); + + // QSaveFile: scrittura atomica, un'interruzione non lascia il file a meta'. + QSaveFile file(dataPath()); + if (!file.open(QIODevice::WriteOnly)) { + qWarning() << "henflow: cannot write" << file.fileName() << file.errorString(); + return false; + } + file.write(QJsonDocument(root).toJson(QJsonDocument::Indented)); + if (!file.commit()) { + qWarning() << "henflow: commit failed" << file.errorString(); + return false; + } + + m_dirty = false; + return true; +} + +void DataStore::setDirty() +{ + m_dirty = true; + emit changed(); +} + +// --- lettura --------------------------------------------------------------- + +QVariantList DataStore::breedSettings() const { return m_breedSettings; } +QVariantList DataStore::reservations() const { return m_reservations; } +QVariantList DataStore::customers() const { return m_customers; } +QVariantMap DataStore::warehouseStock() const { return m_stock; } +QString DataStore::lastProductionUpdate() const { return m_lastProductionUpdate; } + +// --- scrittura ------------------------------------------------------------- + +bool DataStore::saveBreedSettings(const QVariantList &items) +{ + m_breedSettings = items; + setDirty(); + return store(); +} + +bool DataStore::saveReservations(const QVariantList &items) +{ + m_reservations = items; + setDirty(); + return store(); +} + +bool DataStore::saveCustomers(const QVariantList &items) +{ + m_customers = items; + setDirty(); + return store(); +} + +bool DataStore::saveWarehouseStock(const QVariantMap &stock) +{ + m_stock = stock; + setDirty(); + return store(); +} + +bool DataStore::saveLastProductionUpdate(const QString &day) +{ + m_lastProductionUpdate = day; + setDirty(); + return store(); +} + +bool DataStore::reload() +{ + const bool ok = load(); + emit changed(); + return ok; +} + +bool DataStore::exportTo(const QString &path) const +{ + QJsonObject root; + root.insert(QLatin1String(kVersion), 1); + root.insert(QLatin1String(kBreedSettings), QJsonArray::fromVariantList(m_breedSettings)); + root.insert(QLatin1String(kReservations), QJsonArray::fromVariantList(m_reservations)); + root.insert(QLatin1String(kCustomers), QJsonArray::fromVariantList(m_customers)); + root.insert(QLatin1String(kWarehouseStock), QJsonObject::fromVariantMap(m_stock)); + if (!m_lastProductionUpdate.isEmpty()) + root.insert(QLatin1String(kLastUpdate), m_lastProductionUpdate); + + QSaveFile out(path); + if (!out.open(QIODevice::WriteOnly)) { + qWarning() << "henflow: cannot write backup" << path << out.errorString(); + return false; + } + out.write(QJsonDocument(root).toJson(QJsonDocument::Indented)); + return out.commit(); +} + +bool DataStore::importFrom(const QString &path) +{ + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + qWarning() << "henflow: cannot read backup" << path << file.errorString(); + return false; + } + QJsonParseError error; + const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error); + if (error.error != QJsonParseError::NoError || !doc.isObject()) { + qWarning() << "henflow: backup non valido" << path << error.errorString(); + return false; + } + + const QJsonObject root = doc.object(); + m_breedSettings = root.value(QLatin1String(kBreedSettings)).toArray().toVariantList(); + m_reservations = root.value(QLatin1String(kReservations)).toArray().toVariantList(); + m_customers = root.value(QLatin1String(kCustomers)).toArray().toVariantList(); + m_stock = root.value(QLatin1String(kWarehouseStock)).toObject().toVariantMap(); + m_lastProductionUpdate = root.value(QLatin1String(kLastUpdate)).toString(); + setDirty(); + return store(); +} + +bool DataStore::clearAll() +{ + m_breedSettings.clear(); + m_reservations.clear(); + m_customers.clear(); + m_stock.clear(); + m_lastProductionUpdate.clear(); + setDirty(); + return store(); +} diff --git a/src/datastore.h b/src/datastore.h new file mode 100644 index 0000000..6433fe8 --- /dev/null +++ b/src/datastore.h @@ -0,0 +1,66 @@ +#ifndef DATASTORE_H +#define DATASTORE_H + +#include +#include +#include +#include + +// Persistenza dei dati di Henflow: un unico file JSON in AppConfigLocation +// (~/.config/harbour/henflow/henflow.json), con la STESSA struttura delle +// chiavi SharedPreferences dell'app Flutter, cosi' il backup resta +// compatibile con la versione Android. +// +// Il calcolo (produzione, coda FIFO, date) NON sta qui: e' in qml/js/henflow.js, +// dove e' verificabile con node. Questa classe legge e scrive soltanto. +class DataStore : public QObject +{ + Q_OBJECT + +public: + explicit DataStore(QObject *parent = nullptr); + + Q_INVOKABLE QVariantList breedSettings() const; + Q_INVOKABLE QVariantList reservations() const; + Q_INVOKABLE QVariantList customers() const; + Q_INVOKABLE QVariantMap warehouseStock() const; + Q_INVOKABLE QString lastProductionUpdate() const; + + Q_INVOKABLE bool saveBreedSettings(const QVariantList &items); + Q_INVOKABLE bool saveReservations(const QVariantList &items); + Q_INVOKABLE bool saveCustomers(const QVariantList &items); + Q_INVOKABLE bool saveWarehouseStock(const QVariantMap &stock); + Q_INVOKABLE bool saveLastProductionUpdate(const QString &day); + + // Rilegge dal disco (usato quando l'app torna in primo piano). + Q_INVOKABLE bool reload(); + + // Percorso del file dati, per diagnostica e per il backup. + Q_INVOKABLE QString dataFilePath() const; + + // Backup/ripristino: copia dell'intero file JSON. + Q_INVOKABLE bool exportTo(const QString &path) const; + Q_INVOKABLE bool importFrom(const QString &path); + + // Cancella tutto (voce "azzera dati" nelle impostazioni). + Q_INVOKABLE bool clearAll(); + +signals: + void changed(); + +private: + void setDirty(); + bool load(); + bool store(); + + static QString dataPath(); + + QVariantList m_breedSettings; + QVariantList m_reservations; + QVariantList m_customers; + QVariantMap m_stock; + QString m_lastProductionUpdate; + bool m_dirty; +}; + +#endif // DATASTORE_H diff --git a/src/main.cpp b/src/main.cpp index 2511e04..eacabda 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,6 +7,7 @@ #include #include +#include "datastore.h" #include "settings.h" Q_DECL_EXPORT int main(int argc, char *argv[]) @@ -20,6 +21,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[]) app->setApplicationVersion(QStringLiteral("0.1.0")); Settings settings; + DataStore data; // Lingua scelta dall'utente: "system" segue Sailfish, "en" e' la lingua // delle stringhe sorgente, "it" carica il catalogo dal datadir dell'app. @@ -49,6 +51,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[]) qDebug() << "harbour-henflow v0.1.0 build" << __DATE__ << __TIME__; view->rootContext()->setContextProperty(QStringLiteral("appSettings"), &settings); + view->rootContext()->setContextProperty(QStringLiteral("appData"), &data); view->rootContext()->setContextProperty(QStringLiteral("appVersion"), QCoreApplication::applicationVersion()); diff --git a/tests/cpp_test.sh b/tests/cpp_test.sh new file mode 100644 index 0000000..422b024 --- /dev/null +++ b/tests/cpp_test.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# Compila e gira il banco di prova del livello dati con il Qt locale +# (il device e' Qt 5.6: qui serve solo verificare la logica di persistenza). +# sh tests/cpp_test.sh +# Non tocca la configurazione vera: usa una XDG_CONFIG_HOME temporanea. +set -e +cd "$(dirname "$0")/harness" + +QTDIR=${QTDIR:-/home/kaneda/Qt/5.15.2/gcc_64} +PATH="$QTDIR/bin:$PATH" +LD_LIBRARY_PATH="$QTDIR/lib:$LD_LIBRARY_PATH" +export PATH LD_LIBRARY_PATH + +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT +XDG_CONFIG_HOME="$WORK/config" +XDG_DATA_HOME="$WORK/data" +export XDG_CONFIG_HOME XDG_DATA_HOME + +rm -f Makefile.harness harness +qmake harness.pro -o Makefile.harness >/dev/null +make -s -f Makefile.harness +./harness diff --git a/tests/engine.test.js b/tests/engine.test.js index dca9e37..557388e 100644 --- a/tests/engine.test.js +++ b/tests/engine.test.js @@ -115,6 +115,10 @@ check("stato: fra N giorni", HF.statusKey(4), "in_days"); 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"); +check("formatDay gg/mm/aaaa (come la card Android)", HF.formatDay("2026-04-28"), "28/04/2026"); +check("formatDay su stringa vuota", HF.formatDay(""), ""); +check("nowIso in formato ISO", /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000$/.test(HF.nowIso()), true); +check("newId unico", HF.newId() !== HF.newId(), true); console.log(failures === 0 ? "\ntutti i controlli superati" : "\n" + failures + " controlli falliti"); process.exit(failures === 0 ? 0 : 1); diff --git a/tests/harness/harness.cpp b/tests/harness/harness.cpp new file mode 100644 index 0000000..7b14b65 --- /dev/null +++ b/tests/harness/harness.cpp @@ -0,0 +1,157 @@ +// Banco di prova del livello dati (fuori dal pacchetto RPM). +// Compila con Qt 5.15 locale e gira con XDG_CONFIG_HOME su una cartella +// temporanea, cosi' NON tocca la configurazione vera: +// sh tests/cpp_test.sh +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../src/datastore.h" + +static int failures = 0; + +static void check(const QString &name, bool ok, const QString &detail = QString()) +{ + if (!ok) + failures++; + qInfo().noquote() << (ok ? "OK " : "FAIL") << name + << (ok || detail.isEmpty() ? QString() : "-> " + detail); +} + +static QVariantList makeBreeds() +{ + QVariantList hens; + QVariantMap m1; + m1.insert("id", "m1"); + m1.insert("name", "Marta"); + m1.insert("birthDate", "2025-01-01T00:00:00.000"); + m1.insert("isLaying", true); + hens.append(m1); + + QVariantMap marans; + marans.insert("breedId", "marans"); + marans.insert("hens", hens); + return QVariantList() << marans; +} + +static QVariantList makeReservations() +{ + QVariantMap r; + r.insert("id", "r1"); + r.insert("customerName", "Cli1"); + r.insert("breedId", "araucana"); + r.insert("eggsRequested", 2); + r.insert("createdAt", "2026-04-25T09:00:00.000"); + r.insert("deliveryDate", "2026-04-28"); + r.insert("isDelivered", false); + return QVariantList() << r; +} + +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc, argv); + app.setOrganizationName("harbour"); + app.setApplicationName("henflow"); + + qInfo().noquote() << "config dir:" << QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); + + // --- primo avvio: nessun file, stato vuoto ----------------------------- + { + DataStore store; + check("primo avvio: nessuna razza", store.breedSettings().isEmpty()); + check("primo avvio: nessuna prenotazione", store.reservations().isEmpty()); + check("primo avvio: magazzino vuoto", store.warehouseStock().isEmpty()); + check("primo avvio: nessuna data di aggiornamento", store.lastProductionUpdate().isEmpty()); + check("il file dati non esiste ancora", !QFile::exists(store.dataFilePath())); + } + + // --- scrittura e rilettura -------------------------------------------- + DataStore store; + check("salvataggio razze", store.saveBreedSettings(makeBreeds())); + check("salvataggio prenotazioni", store.saveReservations(makeReservations())); + + QVariantMap customers; + customers.insert("id", "c1"); + customers.insert("firstName", "Anna"); + customers.insert("lastName", "Rossi"); + customers.insert("phoneNumber", "+39 333 0000000"); + check("salvataggio clienti", store.saveCustomers(QVariantList() << customers)); + + QVariantMap stock; + stock.insert("marans", 6.0); + stock.insert("araucana", 1.5); + check("salvataggio magazzino", store.saveWarehouseStock(stock)); + check("salvataggio data aggiornamento", store.saveLastProductionUpdate("2026-10-09")); + + check("il file dati esiste", QFile::exists(store.dataFilePath())); + + { + DataStore reopened; + check("rilettura razze", reopened.breedSettings().size() == 1); + check("rilettura nidificata (galline)", reopened.breedSettings().at(0).toMap() + .value("hens").toList().size() == 1); + check("rilettura prenotazioni", reopened.reservations().size() == 1); + check("rilettura quantita' uova", reopened.reservations().at(0).toMap() + .value("eggsRequested").toInt() == 2); + check("rilettura flag consegna (bool)", reopened.reservations().at(0).toMap() + .value("isDelivered").toBool() == false); + check("rilettura clienti", reopened.customers().size() == 1 && + reopened.customers().at(0).toMap().value("lastName").toString() == "Rossi"); + check("rilettura magazzino (numeri)", qFuzzyCompare(reopened.warehouseStock() + .value("araucana").toDouble(), 1.5)); + check("rilettura data aggiornamento", reopened.lastProductionUpdate() == "2026-10-09"); + } + + // --- contratto con le chiavi dell'app Flutter -------------------------- + { + QFile f(store.dataFilePath()); + f.open(QIODevice::ReadOnly); + const QJsonObject root = QJsonDocument::fromJson(f.readAll()).object(); + f.close(); + check("chiave breed_settings_v2 (compatibile con l'app Android)", + root.contains("breed_settings_v2")); + check("chiave reservations_v2", root.contains("reservations_v2")); + check("chiave customers_v1", root.contains("customers_v1")); + check("chiave warehouse_stock_v1", root.contains("warehouse_stock_v1")); + check("chiave last_production_update_v1", root.contains("last_production_update_v1")); + } + + // --- backup / ripristino ---------------------------------------------- + { + const QString backup = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)) + .filePath("backup-test.json"); + check("export", store.exportTo(backup)); + check("clearAll", store.clearAll()); + check("dopo clearAll: vuoto", store.reservations().isEmpty() && store.customers().isEmpty()); + check("import", store.importFrom(backup)); + check("dopo import: prenotazioni ripristinate", store.reservations().size() == 1); + check("dopo import: clienti ripristinati", store.customers().size() == 1); + QFile::remove(backup); + } + + // --- file corrotto: non deve crashare e non deve perdere i dati --------- + { + QFile f(store.dataFilePath()); + f.open(QIODevice::WriteOnly | QIODevice::Truncate); + f.write("{ questo non e' json"); + f.close(); + + DataStore broken; + check("file corrotto: non crasha", true); + check("file corrotto: stato vuoto", broken.reservations().isEmpty()); + + // il backup di un file corrotto viene rifiutato + check("import di un file non valido rifiutato", !broken.importFrom(store.dataFilePath())); + } + + qInfo().noquote() << (failures == 0 ? "\ntutti i controlli superati" + : QString("\n%1 controlli falliti").arg(failures)); + return failures == 0 ? 0 : 1; +} diff --git a/tests/harness/harness.pro b/tests/harness/harness.pro new file mode 100644 index 0000000..07798f2 --- /dev/null +++ b/tests/harness/harness.pro @@ -0,0 +1,16 @@ +TEMPLATE = app +TARGET = harness +CONFIG += console c++11 +CONFIG -= app_bundle + +# Solo QtCore: il livello dati non tocca la GUI, cosi' il banco di prova +# compila su desktop senza QtQuick/Silica. +QT += core +QT -= gui + +SOURCES += \ + harness.cpp \ + ../../src/datastore.cpp + +HEADERS += \ + ../../src/datastore.h diff --git a/tests/i18n.test.js b/tests/i18n.test.js index 4d41d95..e249aee 100644 --- a/tests/i18n.test.js +++ b/tests/i18n.test.js @@ -53,6 +53,12 @@ check("t() con array", I18N.t("share_order_msg", ["Marans", "2", "28/04/2026"], check("t() lingua sconosciuta -> inglese", I18N.t("new_order", null, "xx"), S.en.new_order); check("t() chiave sconosciuta -> chiave", I18N.t("chiave_inesistente", null, "it"), "chiave_inesistente"); +// lingua effettiva: 'system' segue il telefono +check("langFor: scelta esplicita", I18N.langFor("fr", "it_IT"), "fr"); +check("langFor: system -> lingua del telefono", I18N.langFor("system", "de_DE"), "de"); +check("langFor: system non supportata -> inglese", I18N.langFor("system", "es_ES"), "en"); +check("langFor: scelta non valida -> inglese", I18N.langFor("xx", "it_IT"), "en"); + // il testo con a capo e' una vera nuova riga (in main.dart e' '\n') check("newline reale (no_breeds_sel)", S.it.no_breeds_sel.indexOf("\n") > 0, true); diff --git a/tools/gen_from_flutter.py b/tools/gen_from_flutter.py index b38f0ff..206ed5f 100644 --- a/tools/gen_from_flutter.py +++ b/tools/gen_from_flutter.py @@ -131,11 +131,22 @@ function availableLanguages() { return out.sort(); } +// Lingua effettiva: 'system' segue la lingua del telefono (nome tipo "it_IT"), +// altrimenti si usa il codice scelto. Fallback: inglese. +function langFor(setting, systemName) { + if (setting && setting !== "system") { + return HF_STRINGS[setting] ? setting : "en"; + } + var code = String(systemName || "").slice(0, 2).toLowerCase(); + return HF_STRINGS[code] ? code : "en"; +} + if (typeof module !== "undefined" && module.exports) { module.exports = { HF_STRINGS: HF_STRINGS, t: t, - availableLanguages: availableLanguages + availableLanguages: availableLanguages, + langFor: langFor }; } """ % (nkeys, len(LANGS), js_value(data))