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
+61 -7
View File
@@ -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
}
}
}
+24 -1
View File
@@ -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
};
}
+12 -1
View File
@@ -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
};
}
+297 -35
View File
@@ -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()
}
+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()
}