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
+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("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);
+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() 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);