livello dati C++ (datastore JSON, 33 controlli) e schermate Home, scheda ordine e cover; .gitignore per gli artefatti

This commit is contained in:
2026-10-09 09:09:31 +02:00
parent b1ee0c78b5
commit 40e4badbd4
17 changed files with 1248 additions and 56 deletions
+17
View File
@@ -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/
+29 -9
View File
@@ -18,11 +18,14 @@ Android (AdMob/File picker/Share). Nessun account, nessun cloud, come l'original
| parte | stato | | parte | stato |
|---|---| |---|---|
| Packaging (spec RPM, desktop SailJail, icone, i18n) | pronto | | 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 | | 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 | | 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 | | 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 ## 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 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/appData, lingua
src/settings.h/.cpp # QSettings su path esplicito AppConfigLocation + sync() 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/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/henflow.js # motore di calcolo (ES5, testabile con node)
qml/js/i18n.js # 126 chiavi x 4 lingue + t(chiave, arg, lingua) qml/js/i18n.js # 126 chiavi x 4 lingue + t(chiave, arg, lingua)
qml/js/catalog.js # 7 razze: nome/descrizione/colore uova/foto qml/js/catalog.js # 7 razze: nome/descrizione/colore uova/foto
assets/breeds/*.jpg # foto delle razze (installate nel datadir) assets/breeds/*.jpg # foto delle razze (installate nel datadir)
tools/gen_from_flutter.py # rigenera i18n.js e catalog.js dai sorgenti Flutter 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 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`) ## Motore di calcolo (portato fedelmente da `lib/main.dart`)
- **Produzione stimata** per razza = galline in deposizione x uova/gallina/giorno - **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 ## Aperti
1. **Repo Gitea `HenFlow`**: cancellata (era un upload parziale); da ricreare 1. **Schermate da fare**: razze disponibili / mio pollaio / anagrafica galline,
dalla UI, poi `workspace/henflow-flutter` va pushata li'. clienti, backup-ripristino, scelta lingua, introduzione iniziale, calendario.
2. **Schermate QML**: da implementare (solo modalita' allevatore, 4 lingue). 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) ## Correzioni rispetto all'originale (bug trovati nei sorgenti Flutter)
+4 -2
View File
@@ -10,10 +10,12 @@ QT += quick qml
SOURCES += \ SOURCES += \
src/main.cpp \ src/main.cpp \
src/settings.cpp src/settings.cpp \
src/datastore.cpp
HEADERS += \ HEADERS += \
src/settings.h src/settings.h \
src/datastore.h
OTHER_FILES += \ OTHER_FILES += \
qml/harbour-henflow.qml \ qml/harbour-henflow.qml \
+57 -3
View File
@@ -1,7 +1,40 @@
import QtQuick 2.2 import QtQuick 2.2
import Sailfish.Silica 1.0 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 { 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 { Label {
id: title id: title
anchors { anchors {
@@ -9,17 +42,38 @@ CoverBackground {
topMargin: Theme.paddingLarge topMargin: Theme.paddingLarge
horizontalCenter: parent.horizontalCenter horizontalCenter: parent.horizontalCenter
} }
text: qsTr("Henflow") text: cover.tr("app_title")
color: Theme.primaryColor color: Theme.primaryColor
} }
Label { Column {
anchors.centerIn: parent anchors.centerIn: parent
width: parent.width - 2 * Theme.paddingLarge width: parent.width - 2 * Theme.paddingLarge
text: qsTr("Eggs, hens and orders") 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 font.pixelSize: Theme.fontSizeSmall
color: Theme.secondaryColor color: Theme.secondaryColor
}
Label {
width: parent.width
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: cover.tr("reservations") + ": " + cover.pending
font.pixelSize: Theme.fontSizeExtraSmall
color: Theme.secondaryColor
}
} }
} }
+24 -1
View File
@@ -229,6 +229,26 @@ function statusKey(days) {
return "in_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 // Guard per i test node: in QML `module` non esiste, le funzioni top-level
// sono gia' visibili al file che importa questo modulo. // sono gia' visibili al file che importa questo modulo.
if (typeof module !== "undefined" && module.exports) { if (typeof module !== "undefined" && module.exports) {
@@ -250,6 +270,9 @@ if (typeof module !== "undefined" && module.exports) {
pendingEggs: pendingEggs, pendingEggs: pendingEggs,
estimateDeliveryDate: estimateDeliveryDate, estimateDeliveryDate: estimateDeliveryDate,
refreshDeliveryDates: refreshDeliveryDates, refreshDeliveryDates: refreshDeliveryDates,
statusKey: statusKey statusKey: statusKey,
formatDay: formatDay,
nowIso: nowIso,
newId: newId
}; };
} }
+12 -1
View File
@@ -546,10 +546,21 @@ function availableLanguages() {
return out.sort(); 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) { if (typeof module !== "undefined" && module.exports) {
module.exports = { module.exports = {
HF_STRINGS: HF_STRINGS, HF_STRINGS: HF_STRINGS,
t: t, t: t,
availableLanguages: availableLanguages availableLanguages: availableLanguages,
langFor: langFor
}; };
} }
+292 -30
View File
@@ -1,71 +1,333 @@
import QtQuick 2.2 import QtQuick 2.2
import Sailfish.Silica 1.0 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 { Page {
id: page id: page
allowedOrientations: Orientation.All 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 { SilicaListView {
id: listView id: listView
anchors.fill: parent anchors.fill: parent
model: orderModel
header: PageHeader { header: Column {
title: qsTr("Henflow") width: listView.width
PageHeader {
title: page.tr("app_title")
} }
model: ListModel { // card della produzione stimata
id: sections Rectangle {
// Struttura reale dell'app Flutter (lib/main.dart): la Home ha le tab x: Theme.horizontalPageMargin
// "Orders list" e "Calendar", la schermata Configuration le tab width: listView.width - 2 * Theme.horizontalPageMargin
// "Available breeds" e "My coop". Qui resta l'indice dei moduli height: productionColumn.height + 2 * Theme.paddingMedium
// finche' le schermate non sono implementate. radius: Theme.paddingSmall
Component.onCompleted: { color: Theme.rgba(Theme.highlightBackgroundColor, 0.12)
append({ "name": qsTr("Orders list"), "note": qsTr("daily production, reservations, delivery status") })
append({ "name": qsTr("Calendar"), "note": qsTr("undelivered reservations by day") }) Column {
append({ "name": qsTr("Warehouse"), "note": qsTr("eggs in stock per breed") }) id: productionColumn
append({ "name": qsTr("Customers"), "note": qsTr("first name, last name, phone") }) x: Theme.paddingMedium
append({ "name": qsTr("Available breeds"), "note": qsTr("catalog with photos, enable the ones you keep") }) y: Theme.paddingMedium
append({ "name": qsTr("My coop"), "note": qsTr("registered hens, laying, estimated production") }) width: parent.width - 2 * Theme.paddingMedium
append({ "name": qsTr("Backup"), "note": qsTr("export and import the database as JSON") }) 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 { delegate: ListItem {
id: item id: orderItem
contentHeight: Theme.itemSizeMedium 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 { Column {
anchors.verticalCenter: parent.verticalCenter id: itemColumn
x: Theme.horizontalPageMargin x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin y: Theme.paddingMedium
width: parent.width - 2 * Theme.horizontalPageMargin - statusLabel.width
Label { Label {
text: model.name
color: item.highlighted ? Theme.highlightColor : Theme.primaryColor
truncationMode: TruncationMode.Fade
width: parent.width width: parent.width
text: model.customer
truncationMode: TruncationMode.Fade
color: orderItem.highlighted ? Theme.highlightColor : Theme.primaryColor
font.pixelSize: Theme.fontSizeSmall
} }
Label { Label {
text: model.note width: parent.width
truncationMode: TruncationMode.Fade
font.pixelSize: Theme.fontSizeExtraSmall font.pixelSize: Theme.fontSizeExtraSmall
color: Theme.secondaryColor color: Theme.secondaryColor
truncationMode: TruncationMode.Fade text: page.tr("breed") + ": " + model.breed
width: parent.width + " " + 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
} }
} }
footer: Label { 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)
}
}
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 x: Theme.horizontalPageMargin
y: Theme.paddingMedium
width: parent.width - 2 * Theme.horizontalPageMargin width: parent.width - 2 * Theme.horizontalPageMargin
text: qsTr("Native port in progress") visible: page.hintMsg.length > 0
text: page.hintMsg
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Theme.fontSizeExtraSmall font.pixelSize: Theme.fontSizeExtraSmall
color: Theme.secondaryHighlightColor color: Theme.secondaryHighlightColor
horizontalAlignment: Text.AlignHCenter }
wrapMode: Text.WordWrap
} }
VerticalScrollDecorator {} 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()
} }
+301
View File
@@ -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()
}
+216
View File
@@ -0,0 +1,216 @@
#include "datastore.h"
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSaveFile>
#include <QStandardPaths>
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();
}
+66
View File
@@ -0,0 +1,66 @@
#ifndef DATASTORE_H
#define DATASTORE_H
#include <QObject>
#include <QString>
#include <QVariantList>
#include <QVariantMap>
// 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
+3
View File
@@ -7,6 +7,7 @@
#include <QTranslator> #include <QTranslator>
#include <sailfishapp.h> #include <sailfishapp.h>
#include "datastore.h"
#include "settings.h" #include "settings.h"
Q_DECL_EXPORT int main(int argc, char *argv[]) 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")); app->setApplicationVersion(QStringLiteral("0.1.0"));
Settings settings; Settings settings;
DataStore data;
// Lingua scelta dall'utente: "system" segue Sailfish, "en" e' la lingua // Lingua scelta dall'utente: "system" segue Sailfish, "en" e' la lingua
// delle stringhe sorgente, "it" carica il catalogo dal datadir dell'app. // 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__; qDebug() << "harbour-henflow v0.1.0 build" << __DATE__ << __TIME__;
view->rootContext()->setContextProperty(QStringLiteral("appSettings"), &settings); view->rootContext()->setContextProperty(QStringLiteral("appSettings"), &settings);
view->rootContext()->setContextProperty(QStringLiteral("appData"), &data);
view->rootContext()->setContextProperty(QStringLiteral("appVersion"), view->rootContext()->setContextProperty(QStringLiteral("appVersion"),
QCoreApplication::applicationVersion()); QCoreApplication::applicationVersion());
+23
View File
@@ -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
+4
View File
@@ -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("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("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("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"); console.log(failures === 0 ? "\ntutti i controlli superati" : "\n" + failures + " controlli falliti");
process.exit(failures === 0 ? 0 : 1); process.exit(failures === 0 ? 0 : 1);
+157
View File
@@ -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 <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QStandardPaths>
#include <QVariantList>
#include <QVariantMap>
#include <QDebug>
#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;
}
+16
View File
@@ -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
+6
View File
@@ -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() 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"); 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') // il testo con a capo e' una vera nuova riga (in main.dart e' '\n')
check("newline reale (no_breeds_sel)", check("newline reale (no_breeds_sel)",
S.it.no_breeds_sel.indexOf("\n") > 0, true); S.it.no_breeds_sel.indexOf("\n") > 0, true);
+12 -1
View File
@@ -131,11 +131,22 @@ function availableLanguages() {
return out.sort(); 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) { if (typeof module !== "undefined" && module.exports) {
module.exports = { module.exports = {
HF_STRINGS: HF_STRINGS, HF_STRINGS: HF_STRINGS,
t: t, t: t,
availableLanguages: availableLanguages availableLanguages: availableLanguages,
langFor: langFor
}; };
} }
""" % (nkeys, len(LANGS), js_value(data)) """ % (nkeys, len(LANGS), js_value(data))