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
+21 -9
View File
@@ -137,15 +137,26 @@ function accrueStock(stock, breedSettings, catalog, daysPassed) {
// --------------------------------------------------------- prenotazioni
function cloneReservation(r) {
return {
id: r.id,
customerName: r.customerName,
breedId: r.breedId,
eggsRequested: r.eggsRequested,
createdAt: r.createdAt,
deliveryDate: r.deliveryDate,
isDelivered: r.isDelivered
};
// Copia TUTTI i campi: l'ordine dell'app Android ne ha anche di piu'
// (source, clientNote) e il ricalcolo delle date non deve perderli.
var out = {}, k;
for (k in r) {
if (r.hasOwnProperty(k)) out[k] = r[k];
}
return out;
}
// 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) {
@@ -268,6 +279,7 @@ if (typeof module !== "undefined" && module.exports) {
isProductive: isProductive,
accrueStock: accrueStock,
pendingEggs: pendingEggs,
applyDelivery: applyDelivery,
estimateDeliveryDate: estimateDeliveryDate,
refreshDeliveryDates: refreshDeliveryDates,
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)
return
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
appData.saveReservations(list)
page.rebuildList()
@@ -281,6 +285,10 @@ Page {
text: page.tr("customers_list")
onClicked: pageStack.push(Qt.resolvedUrl("CustomersPage.qml"))
}
MenuItem {
text: page.tr("calendar")
onClicked: pageStack.push(Qt.resolvedUrl("CalendarPage.qml"))
}
MenuItem {
text: page.tr("settings")
onClicked: pageStack.push(Qt.resolvedUrl("SettingsPage.qml"))
+4
View File
@@ -43,6 +43,10 @@ Page {
if (page.selected < 0 && list.length > 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()
}