calendario delle consegne (griglia del mese + ordini del giorno) e fedelta' dei dati: gli ordini conservano nota del cliente e origine, la consegna scala le uova dal magazzino come nell'originale

This commit is contained in:
2026-10-09 09:26:58 +02:00
parent 248e0db899
commit 727e7f8d38
6 changed files with 471 additions and 14 deletions
+12 -5
View File
@@ -30,7 +30,7 @@ Android (AdMob/File picker/Share). Nessun account, nessun cloud, come l'original
| Backup e ripristino (JSON dell'app Android) | fatto | | Backup e ripristino (JSON dell'app Android) | fatto |
| Scelta lingua a runtime (4 lingue) | fatta | | Scelta lingua a runtime (4 lingue) | fatta |
| Introduzione (primo avvio e dalle impostazioni) | fatta | | Introduzione (primo avvio e dalle impostazioni) | fatta |
| Calendario delle consegne | da fare | | Calendario delle consegne | fatto |
## Struttura ## Struttura
@@ -44,6 +44,7 @@ src/datastore.h/.cpp # dati in un JSON in AppConfigLocation + backup nel
qml/harbour-henflow.qml # ApplicationWindow: initialPage + cover qml/harbour-henflow.qml # ApplicationWindow: initialPage + cover
qml/pages/MainPage.qml # Home: produzione stimata, coda ordini, menu (ordine, clienti, impostazioni) qml/pages/MainPage.qml # Home: produzione stimata, coda ordini, menu (ordine, clienti, impostazioni)
qml/pages/OrderPage.qml # nuovo/modifica ordine con anteprima della data di consegna qml/pages/OrderPage.qml # nuovo/modifica ordine con anteprima della data di consegna
qml/pages/CalendarPage.qml # calendario delle consegne: griglia del mese + ordini del giorno
qml/pages/SettingsPage.qml # indice delle impostazioni qml/pages/SettingsPage.qml # indice delle impostazioni
qml/pages/BreedsPage.qml # razze disponibili (attiva/disattiva con conferma) qml/pages/BreedsPage.qml # razze disponibili (attiva/disattiva con conferma)
qml/pages/CoopPage.qml # mio pollaio: conteggi e produzione per razza qml/pages/CoopPage.qml # mio pollaio: conteggi e produzione per razza
@@ -135,7 +136,7 @@ scritto esattamente come lo esporta l'app Android.
| `OnboardingPage` | 4 passi introduttivi | `IntroPage` (primo avvio + dalle impostazioni) | | `OnboardingPage` | 4 passi introduttivi | `IntroPage` (primo avvio + dalle impostazioni) |
| `ModeSelectionPage` | modalita' allevatore / cliente | non portata: solo allevatore, come deciso | | `ModeSelectionPage` | modalita' allevatore / cliente | non portata: solo allevatore, come deciso |
| `HomePage` | card produzione stimata, ordini con stato e azioni, FAB *New order* | `MainPage` + `OrderPage` | | `HomePage` | card produzione stimata, ordini con stato e azioni, FAB *New order* | `MainPage` + `OrderPage` |
| `HomePage` tab *Calendar* | calendario delle prenotazioni | da fare | | `HomePage` tab *Calendar* | calendario delle prenotazioni | `CalendarPage` |
| `SettingsPage` tab *Available breeds* | catalogo con foto e spunta, conferma alla disattivazione | `BreedsPage` | | `SettingsPage` tab *Available breeds* | catalogo con foto e spunta, conferma alla disattivazione | `BreedsPage` |
| `SettingsPage` tab *My coop* | razze allevate, galline registrate/in deposizione, produzione | `CoopPage` + `BreedDetailPage` | | `SettingsPage` tab *My coop* | razze allevate, galline registrate/in deposizione, produzione | `CoopPage` + `BreedDetailPage` |
| `BreedDetailPage` + `AddHenDialog` | galline della razza, eta' in mesi, stato deposizione | `BreedDetailPage` + `HenPage` (data con tre selettori) | | `BreedDetailPage` + `AddHenDialog` | galline della razza, eta' in mesi, stato deposizione | `BreedDetailPage` + `HenPage` (data con tre selettori) |
@@ -166,9 +167,7 @@ git archive --format=tar.gz --prefix=harbour-henflow-0.2.0/ -o harbour-henflow-0
## Aperti ## Aperti
1. **Calendario delle consegne** (il tab *Calendar* dell'originale): griglia del 1. **Verifica su device**: la UI Silica non e' verificabile senza SDK/dispositivo
mese con gli ordini per giorno e l'elenco del giorno scelto.
2. **Verifica su device**: la UI Silica non e' verificabile senza SDK/dispositivo
(qui si verificano motore, traduzioni, catalogo, livello dati, chiavi e sintassi (qui si verificano motore, traduzioni, catalogo, livello dati, chiavi e sintassi
della QML, ma non il comportamento a schermo). della QML, ma non il comportamento a schermo).
@@ -185,3 +184,11 @@ git archive --format=tar.gz --prefix=harbour-henflow-0.2.0/ -o harbour-henflow-0
non ha il plugin webp. Riconvertito in JPEG. non ha il plugin webp. Riconvertito in JPEG.
- Niente annunci (AdMob non esiste su Sailfish) e niente `share_plus`/`file_picker`: - Niente annunci (AdMob non esiste su Sailfish) e niente `share_plus`/`file_picker`:
export/import JSON in Download. export/import JSON in Download.
- Il tab *Calendar* ha i messaggi del dialogo di consegna **scritti a mano in
italiano** ("Conferma Consegna", ...): non tradotti in nessuna delle 4 lingue.
Nel porting la consegna si segna con l'interruttore della riga, come nella Home.
- Gli ordini dell'app Android hanno anche `clientNote` e `source`: il ricalcolo
della coda ricopia **tutti** i campi, cosi' un ordine che arriva dall'Android
non perde la nota del cliente (c'e' un test apposta).
- La consegna scala le uova dal magazzino della razza, come nell'originale; nel
porting annullarla le rimette (l'originale non prevedeva l'annulla).
+21 -9
View File
@@ -137,15 +137,26 @@ function accrueStock(stock, breedSettings, catalog, daysPassed) {
// --------------------------------------------------------- prenotazioni // --------------------------------------------------------- prenotazioni
function cloneReservation(r) { function cloneReservation(r) {
return { // Copia TUTTI i campi: l'ordine dell'app Android ne ha anche di piu'
id: r.id, // (source, clientNote) e il ricalcolo delle date non deve perderli.
customerName: r.customerName, var out = {}, k;
breedId: r.breedId, for (k in r) {
eggsRequested: r.eggsRequested, if (r.hasOwnProperty(k)) out[k] = r[k];
createdAt: r.createdAt, }
deliveryDate: r.deliveryDate, return out;
isDelivered: r.isDelivered }
};
// Consegna: l'app originale, quando un ordine viene segnato come consegnato,
// SCALA le uova dal magazzino di quella razza; annullando la consegna le
// rimette. Restituisce un magazzino nuovo (l'input non viene toccato).
function applyDelivery(stock, reservation, delivered) {
var out = {}, k;
for (k in stock) {
if (stock.hasOwnProperty(k)) out[k] = stock[k];
}
var eggs = reservation.eggsRequested || 0;
out[reservation.breedId] = (out[reservation.breedId] || 0) + (delivered ? -eggs : eggs);
return out;
} }
function pendingEggs(reservations, breedId) { function pendingEggs(reservations, breedId) {
@@ -268,6 +279,7 @@ if (typeof module !== "undefined" && module.exports) {
isProductive: isProductive, isProductive: isProductive,
accrueStock: accrueStock, accrueStock: accrueStock,
pendingEggs: pendingEggs, pendingEggs: pendingEggs,
applyDelivery: applyDelivery,
estimateDeliveryDate: estimateDeliveryDate, estimateDeliveryDate: estimateDeliveryDate,
refreshDeliveryDates: refreshDeliveryDates, refreshDeliveryDates: refreshDeliveryDates,
statusKey: statusKey, statusKey: statusKey,
+407
View File
@@ -0,0 +1,407 @@
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
// Calendario delle consegne (il tab *Calendar* della Home dell'originale):
// griglia del mese con un puntino sui giorni che hanno ordini e, sotto,
// l'elenco degli ordini del giorno scelto.
// Nomi di mesi e giorni nella lingua del TELEFONO (Qt.locale()), come faceva
// TableCalendar nell'app Flutter: non sono nelle 126 chiavi dell'app.
// Tutto sta dentro la lista (griglia compresa): su questa Silica e' il layout
// che si comporta bene con i menu a tendina.
Page {
id: page
allowedOrientations: Orientation.All
property string lang: "en"
property var reservations: []
property var orders: []
property string selectedIso: ""
property int viewYear: 0
property int viewMonth: 1
property string todayIso: ""
property string monthTitle: ""
property var weekdayNames: []
property bool ready: false
// Larghezza di una cella del calendario: 7 colonne piene.
readonly property real cellWidth: width / 7
readonly property real cellHeight: Theme.itemSizeSmall
function tr(key, arg) { return I18N.t(key, arg, page.lang) }
ListModel { id: gridModel }
ListModel { id: ordersModel }
function isoOf(y, m, d) {
return HF.isoDay(new Date(y, m - 1, d))
}
function countFor(iso) {
var n = 0
for (var i = 0; i < page.reservations.length; i++) {
var d = HF.parseIso(page.reservations[i].deliveryDate)
if (d && HF.isoDay(d) === iso)
n++
}
return n
}
function buildWeekdays() {
var names = []
for (var i = 1; i <= 7; i++)
names.push(Qt.locale().dayName(i, Locale.ShortFormat))
page.weekdayNames = names
}
function updateTitle() {
page.monthTitle = Qt.locale().monthName(page.viewMonth, Locale.LongFormat)
+ " " + page.viewYear
}
function reload() {
page.lang = I18N.langFor(appSettings.language, Qt.locale().name)
page.reservations = appData.reservations()
var today = HF.todayMidnight()
page.todayIso = HF.isoDay(today)
if (page.viewYear === 0) {
page.viewYear = today.getFullYear()
page.viewMonth = today.getMonth() + 1
}
if (page.selectedIso.length === 0)
page.selectedIso = page.todayIso
page.buildWeekdays()
page.updateTitle()
page.buildGrid()
page.buildOrders()
}
function buildGrid() {
gridModel.clear()
var first = new Date(page.viewYear, page.viewMonth - 1, 1)
// settimana che inizia di lunedi: getDay() da' 0 = domenica
var offset = (first.getDay() + 6) % 7
var daysInMonth = new Date(page.viewYear, page.viewMonth, 0).getDate()
for (var i = 0; i < offset; i++)
gridModel.append({ "day": 0, "iso": "", "count": 0, "today": false, "selected": false })
for (var d = 1; d <= daysInMonth; d++) {
var iso = page.isoOf(page.viewYear, page.viewMonth, d)
gridModel.append({
"day": d,
"iso": iso,
"count": page.countFor(iso),
"today": iso === page.todayIso,
"selected": iso === page.selectedIso
})
}
while (gridModel.count % 7 !== 0)
gridModel.append({ "day": 0, "iso": "", "count": 0, "today": false, "selected": false })
}
function buildOrders() {
ordersModel.clear()
var today = HF.todayMidnight()
for (var i = 0; i < page.reservations.length; i++) {
var r = page.reservations[i]
var d = HF.parseIso(r.deliveryDate)
if (!d || HF.isoDay(d) !== page.selectedIso)
continue
var days = HF.dayDiff(today, d)
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)
var breed = Catalog.breedFromId(r.breedId)
ordersModel.append({
"id": r.id,
"customer": r.customerName,
"breed": breed ? page.tr(breed.name) : r.breedId,
"eggs": r.eggsRequested,
"note": r.clientNote ? r.clientNote : "",
"status": status,
"late": (!r.isDelivered && days < 0),
"delivered": r.isDelivered
})
}
}
function shiftMonth(delta) {
var m = page.viewMonth + delta
var y = page.viewYear
if (m < 1) {
m = 12
y--
} else if (m > 12) {
m = 1
y++
}
page.viewMonth = m
page.viewYear = y
page.updateTitle()
page.buildGrid()
}
function selectDay(iso) {
if (iso.length === 0)
return
page.selectedIso = iso
page.buildGrid()
page.buildOrders()
}
function goToday() {
var today = HF.todayMidnight()
page.viewYear = today.getFullYear()
page.viewMonth = today.getMonth() + 1
page.selectedIso = page.todayIso
page.updateTitle()
page.buildGrid()
page.buildOrders()
}
function openOrder(index) {
var id = page.orders[index].id
var list = appData.reservations()
var reservation = null
for (var i = 0; i < list.length; i++) {
if (list[i].id === id)
reservation = list[i]
}
pageStack.push(Qt.resolvedUrl("OrderPage.qml"),
{ "reservation": reservation,
"onSaved": function () { page.reload() } })
}
// Consegnato: come l'originale, segnare la consegna SCALA le uova dal
// magazzino (e annullarla le rimette).
function toggleDelivered(index) {
var id = page.orders[index].id
var list = appData.reservations().slice()
for (var i = 0; i < list.length; i++) {
if (list[i].id === id) {
list[i].isDelivered = !list[i].isDelivered
appData.saveWarehouseStock(
HF.applyDelivery(appData.warehouseStock(), list[i], list[i].isDelivered))
}
}
appData.saveReservations(list)
page.reload()
}
Component.onCompleted: page.reload()
onStatusChanged: {
if (status !== PageStatus.Active)
return
if (page.ready)
page.reload()
page.ready = true
}
SilicaListView {
id: dayList
anchors.fill: parent
model: ordersModel
header: Column {
width: dayList.width
PageHeader { title: page.tr("calendar") }
// navigazione fra i mesi
Row {
x: Theme.horizontalPageMargin
width: dayList.width - 2 * Theme.horizontalPageMargin
spacing: Theme.paddingSmall
IconButton {
id: prevButton
icon.source: "image://theme/icon-m-left"
onClicked: page.shiftMonth(-1)
}
Label {
anchors.verticalCenter: prevButton.verticalCenter
width: parent.width - 2 * (prevButton.width + parent.spacing)
horizontalAlignment: Text.AlignHCenter
text: page.monthTitle
color: Theme.highlightColor
font.pixelSize: Theme.fontSizeSmall
truncationMode: TruncationMode.Fade
}
IconButton {
icon.source: "image://theme/icon-m-right"
onClicked: page.shiftMonth(1)
}
}
// intestazione dei giorni della settimana
Row {
x: Theme.horizontalPageMargin
width: dayList.width - 2 * Theme.horizontalPageMargin
Repeater {
model: page.weekdayNames.length
delegate: Label {
width: page.cellWidth
horizontalAlignment: Text.AlignHCenter
text: page.weekdayNames[index]
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall
truncationMode: TruncationMode.Fade
}
}
}
// griglia del mese
Grid {
x: Theme.horizontalPageMargin
width: dayList.width - 2 * Theme.horizontalPageMargin
height: (gridModel.count / 7) * page.cellHeight
columns: 7
Repeater {
model: gridModel
delegate: BackgroundItem {
id: cell
width: page.cellWidth
height: page.cellHeight
onClicked: page.selectDay(model.iso)
Rectangle {
anchors.centerIn: parent
width: Math.min(parent.width, parent.height) - Theme.paddingMedium
height: width
radius: width / 2
color: model.selected ? Theme.highlightBackgroundColor
: (model.today ? Theme.rgba(Theme.highlightBackgroundColor, 0.35)
: "transparent")
Label {
anchors.centerIn: parent
text: model.day > 0 ? String(model.day) : ""
color: model.selected ? Theme.highlightColor
: (model.day > 0 ? Theme.primaryColor : Theme.secondaryColor)
font.pixelSize: Theme.fontSizeSmall
}
}
// puntino: quel giorno ha ordini
Rectangle {
visible: model.count > 0
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: Theme.paddingSmall
}
width: Theme.paddingSmall
height: width
radius: width / 2
color: Theme.highlightColor
}
}
}
}
Item { width: parent.width; height: Theme.paddingMedium }
SectionHeader {
text: HF.formatDay(page.selectedIso) + " (" + ordersModel.count + ")"
}
}
delegate: ListItem {
id: orderItem
contentHeight: orderText.height + Theme.paddingMedium
Column {
id: orderText
x: Theme.horizontalPageMargin
y: Theme.paddingMedium
width: parent.width - 2 * Theme.horizontalPageMargin - deliveredSwitch.width
- Theme.paddingMedium
spacing: Theme.paddingSmall
Label {
width: parent.width
text: model.customer
color: orderItem.highlighted ? Theme.highlightColor : Theme.primaryColor
truncationMode: TruncationMode.Fade
}
Label {
width: parent.width
text: page.tr("breed") + ": " + model.breed + " · " + page.tr("eggs_req") + ": " + model.eggs
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall
truncationMode: TruncationMode.Fade
}
Label {
width: parent.width
text: model.status
color: model.late ? Theme.errorColor : Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall
truncationMode: TruncationMode.Fade
}
Label {
width: parent.width
visible: model.note.length > 0
text: page.tr("client_note") + ": " + model.note
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall
wrapMode: Text.Wrap
}
}
Switch {
id: deliveredSwitch
anchors {
right: parent.right
rightMargin: Theme.horizontalPageMargin
verticalCenter: parent.verticalCenter
}
checked: model.delivered
onClicked: page.toggleDelivered(index)
}
onClicked: page.openOrder(index)
}
ViewPlaceholder {
enabled: ordersModel.count === 0
text: page.tr("no_reservations")
}
PullDownMenu {
MenuItem {
text: page.tr("today")
onClicked: page.goToday()
}
MenuItem {
text: page.tr("new_order")
onClicked: pageStack.push(Qt.resolvedUrl("OrderPage.qml"),
{ "reservation": null,
"onSaved": function () { page.reload() } })
}
}
VerticalScrollDecorator {}
}
}
+8
View File
@@ -111,6 +111,10 @@ Page {
if (index < 0 || index >= list.length) if (index < 0 || index >= list.length)
return return
list[index].isDelivered = !list[index].isDelivered list[index].isDelivered = !list[index].isDelivered
// Come l'originale: segnare la consegna SCALA le uova dal magazzino
// (annullarla le rimette).
appData.saveWarehouseStock(
HF.applyDelivery(appData.warehouseStock(), list[index], list[index].isDelivered))
page.reservations = list page.reservations = list
appData.saveReservations(list) appData.saveReservations(list)
page.rebuildList() page.rebuildList()
@@ -281,6 +285,10 @@ Page {
text: page.tr("customers_list") text: page.tr("customers_list")
onClicked: pageStack.push(Qt.resolvedUrl("CustomersPage.qml")) onClicked: pageStack.push(Qt.resolvedUrl("CustomersPage.qml"))
} }
MenuItem {
text: page.tr("calendar")
onClicked: pageStack.push(Qt.resolvedUrl("CalendarPage.qml"))
}
MenuItem { MenuItem {
text: page.tr("settings") text: page.tr("settings")
onClicked: pageStack.push(Qt.resolvedUrl("SettingsPage.qml")) onClicked: pageStack.push(Qt.resolvedUrl("SettingsPage.qml"))
+4
View File
@@ -43,6 +43,10 @@ Page {
if (page.selected < 0 && list.length > 0) if (page.selected < 0 && list.length > 0)
page.selected = 0 page.selected = 0
// Come l'originale: un nuovo ordine parte da 6 uova.
if (!page.reservation && eggsField.text.length === 0)
eggsField.text = "6"
page.updatePreview() page.updatePreview()
} }
+19
View File
@@ -120,5 +120,24 @@ 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("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); check("newId unico", HF.newId() !== HF.newId(), true);
// --- fedelta' dei campi degli ordini (app Android) ------------------------
var withNote = { id: "n1", customerName: "Anna", breedId: "marans", eggsRequested: 2,
createdAt: "2026-04-25T09:00:00.000", deliveryDate: "2026-04-28T00:00:00.000",
isDelivered: false, source: "client", clientNote: "guscio chiaro" };
var kept = HF.refreshDeliveryDates([withNote], {}, BREEDS, CATALOG, TODAY);
check("nota del cliente conservata nel ricalcolo delle date", kept[0].clientNote, "guscio chiaro");
check("origine dell'ordine conservata", kept[0].source, "client");
// --- consegna: le uova escono dal magazzino -------------------------------
var stock0 = { "marans": 10, "araucana": 3 };
var afterDelivery = HF.applyDelivery(stock0, { breedId: "marans", eggsRequested: 4 }, true);
check("consegna: uova scalate dal magazzino della razza", afterDelivery.marans, 6);
check("consegna: le altre razze non si toccano", afterDelivery.araucana, 3);
check("consegna: magazzino di partenza non modificato", stock0.marans, 10);
check("consegna annullata: uova rimesse nel magazzino",
HF.applyDelivery(afterDelivery, { breedId: "marans", eggsRequested: 4 }, false).marans, 10);
check("consegna su razza senza scorte: il magazzino va sotto zero",
HF.applyDelivery({}, { breedId: "olive", eggsRequested: 3 }, true).olive, -3);
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);