diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e308071..359ecae 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -23,8 +23,11 @@ qt_add_qml_module(${PROJECT_NAME} Main.qml MnemonicAction.qml MnemonicLabel.qml + PermanentScrollBar.qml ReservationsPage.qml + ReservationsTimeline.qml SelectableLabel.qml + Separator.qml TimelineDayRow.qml TimelineMonthRow.qml ) diff --git a/src/PermanentScrollBar.qml b/src/PermanentScrollBar.qml new file mode 100644 index 0000000..5bc4d87 --- /dev/null +++ b/src/PermanentScrollBar.qml @@ -0,0 +1,5 @@ +import QtQuick.Controls + +ScrollBar { + policy: size < 1 ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff +} diff --git a/src/ReservationsPage.qml b/src/ReservationsPage.qml index be9178e..0283120 100644 --- a/src/ReservationsPage.qml +++ b/src/ReservationsPage.qml @@ -1,5 +1,4 @@ pragma ComponentBehavior: Bound -import QtQuick import QtQuick.Controls import QtQuick.Layouts import Camper @@ -7,10 +6,6 @@ import Camper Page { id: page - property real dayWidth: 24 - property date fromDate: "2024-11-01" - property date toDate: "2025-01-31" - title: qsTr("Reservations") header: ToolBar { @@ -23,118 +18,15 @@ Page { } } - ListView { - id: lodgingList + ReservationsTimeline { + anchors.fill: parent + anchors.margins: 32 + dayWidth: 24 - ScrollBar.vertical: verticalScroll - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.top: parent.top - headerPositioning: ListView.OverlayHeader - model: timelineListModel - width: 100 - - delegate: Label { - required property string name - - text: name + model: TimelineListModel { + fromDate: "2024-11-01" + toDate: "2025-01-31" } - header: Pane { - height: timelineList.headerItem.height - z: 2 - - Label { - text: qsTr("Lodging") - } - } - } - - ListView { - id: timelineList - - anchors.bottom: parent.bottom - anchors.left: lodgingList.right - anchors.right: parent.right - anchors.top: parent.top - clip: true - contentWidth: headerItem.implicitWidth - flickableDirection: Flickable.AutoFlickDirection - headerPositioning: ListView.OverlayHeader - model: timelineListModel - - ScrollBar.horizontal: ScrollBar { - } - ScrollBar.vertical: ScrollBar { - id: verticalScroll - - } - delegate: TimelineView { - required property TimelineModel timeline - - dayWidth: page.dayWidth - fromDate: page.fromDate - height: 16 - model: timeline - toDate: page.toDate - viewportWidth: ListView.view.width - viewportX: ListView.view.contentX - - delegate: Rectangle { - id: booking - - required property string holder - required property string reservationStatus - - function reservationStatusColor(status) { - switch (status) { - case "created": - return "#cbebff"; - case "cancelled": - return "#ffbaa6"; - case "confirmed": - return "#ffe673"; - case "checked-in": - return "#9fefb9"; - case "invoiced": - return "#e1dbd6"; - } - } - - border.color: "black" - border.width: 1 - color: reservationStatusColor(reservationStatus) - - Label { - anchors.fill: parent - elide: Text.ElideRight - text: booking.holder - } - } - } - header: Pane { - leftPadding: 0 - rightPadding: 0 - z: 2 - - Column { - TimelineMonthRow { - dayWidth: page.dayWidth - fromDate: page.fromDate - toDate: page.toDate - } - - TimelineDayRow { - dayWidth: page.dayWidth - fromDate: page.fromDate - toDate: page.toDate - } - } - } - } - - TimelineListModel { - id: timelineListModel - } MnemonicAction { diff --git a/src/ReservationsTimeline.qml b/src/ReservationsTimeline.qml new file mode 100644 index 0000000..7abc4fb --- /dev/null +++ b/src/ReservationsTimeline.qml @@ -0,0 +1,235 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Fusion + +Control { + id: control + + required property real dayWidth + property date fromDate: model.fromDate + required property TimelineListModel model + property real rowHeight: 32 + property date toDate: model.toDate + + Rectangle { + id: lodgingList + + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.top: parent.top + border.color: Fusion.outline(control.palette) + color: control.palette.base + width: 100 + + ListView { + ScrollBar.vertical: verticalScroll + anchors.fill: parent + boundsBehavior: Flickable.StopAtBounds + clip: true + headerPositioning: ListView.OverlayHeader + model: control.model + + delegate: Column { + required property string name + + width: ListView.view.width + + Label { + anchors.left: parent.left + anchors.leftMargin: 6 + anchors.right: parent.right + height: control.rowHeight - 1 + text: parent.name + verticalAlignment: Text.AlignVCenter + } + + Separator { + anchors.left: parent.left + anchors.right: parent.right + palette: control.palette + } + } + header: Rectangle { + border.color: Fusion.outline(control.palette) + color: control.palette.base + height: timelineList.headerItem.height + width: parent.width + z: 2 + + Label { + anchors.centerIn: parent + font.bold: true + text: qsTr("Lodging") + } + + Separator { + anchors.bottom: parent.bottom + anchors.bottomMargin: 1 + anchors.left: parent.left + anchors.right: parent.right + palette: control.palette + } + } + } + } + + Rectangle { + anchors.bottom: parent.bottom + anchors.left: lodgingList.right + anchors.right: parent.right + anchors.top: parent.top + border.color: Fusion.outline(control.palette) + color: control.palette.base + + ListView { + id: timelineList + + anchors.fill: parent + boundsBehavior: Flickable.StopAtBounds + clip: true + contentWidth: headerItem.implicitWidth + flickableDirection: Flickable.AutoFlickDirection + headerPositioning: ListView.OverlayHeader + model: control.model + + ScrollBar.horizontal: PermanentScrollBar { + } + ScrollBar.vertical: PermanentScrollBar { + id: verticalScroll + + } + delegate: TimelineView { + property real contentWidth: ListView.view.contentWidth + required property TimelineModel timeline + + dayWidth: control.dayWidth + fromDate: control.fromDate + height: control.rowHeight + model: timeline + toDate: control.toDate + viewportWidth: ListView.view.width + viewportX: ListView.view.contentX + + delegate: Label { + id: reservation + + required property string holder + required property string reservationStatus + + elide: Text.ElideRight + padding: 2 + text: holder + verticalAlignment: Text.AlignVCenter + z: 1 + + background: Rectangle { + function backgroundColor(status) { + switch (status) { + case "created": + return "#cbebff"; + case "cancelled": + return "#ffbaa6"; + case "confirmed": + return "#ffe673"; + case "checked-in": + return "#9fefb9"; + case "invoiced": + return "#e1dbd6"; + } + } + + function borderColor(status) { + switch (status) { + case "created": + return "#6fc7fe"; + case "cancelled": + return "#ff7851"; + case "confirmed": + return "#ffd829"; + case "checked-in": + return "#5ae387"; + case "invoiced": + return "#bbaea3"; + } + } + + border.color: borderColor(reservation.reservationStatus) + color: backgroundColor(reservation.reservationStatus) + radius: 5 + } + } + + Separator { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + palette: control.palette + } + } + header: Rectangle { + border.color: Fusion.outline(control.palette) + color: control.palette.base + implicitHeight: headerLayout.implicitHeight + 1 + implicitWidth: headerLayout.implicitWidth + z: 2 + + Column { + id: headerLayout + + TimelineMonthRow { + dayWidth: control.dayWidth + fromDate: control.fromDate + height: 24 + toDate: control.toDate + } + + Separator { + anchors.left: parent.left + anchors.right: parent.right + palette: control.palette + } + + TimelineDayRow { + dayWidth: control.dayWidth + fromDate: control.fromDate + height: 24 + toDate: control.toDate + } + } + + Separator { + anchors.bottom: parent.bottom + anchors.bottomMargin: 1 + anchors.left: parent.left + anchors.right: parent.right + palette: control.palette + } + } + + Row { + height: parent.height + parent: timelineList.contentItem + + Repeater { + delegate: Rectangle { + color: "transparent" + height: parent.height + width: control.dayWidth + + Separator { + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.top: parent.top + palette: control.palette + } + } + model: TimelineDayModel { + fromDate: control.fromDate + toDate: control.toDate + } + } + } + } + } +} diff --git a/src/Separator.qml b/src/Separator.qml new file mode 100644 index 0000000..1c1716e --- /dev/null +++ b/src/Separator.qml @@ -0,0 +1,12 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls.Fusion + +Rectangle { + required property Palette palette + + Accessible.role: Accessible.Separator + color: Fusion.outline(palette) + implicitHeight: 1 + implicitWidth: 1 +} diff --git a/src/TimelineDayRow.qml b/src/TimelineDayRow.qml index 26f029c..ce4dea1 100644 --- a/src/TimelineDayRow.qml +++ b/src/TimelineDayRow.qml @@ -16,13 +16,19 @@ Control { delegate: Label { required property string display + anchors.bottom: parent.bottom + anchors.top: parent.top + font.bold: true + horizontalAlignment: Text.AlignHCenter text: display + verticalAlignment: Text.AlignVCenter width: control.dayWidth - background: Rectangle { - border.color: "red" - border.width: 1 - color: "yellow" + Separator { + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.top: parent.top + palette: control.palette } } } diff --git a/src/TimelineMonthRow.qml b/src/TimelineMonthRow.qml index 9310529..719cabb 100644 --- a/src/TimelineMonthRow.qml +++ b/src/TimelineMonthRow.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Controls +import QtQuick.Controls.Fusion Control { id: control @@ -14,18 +15,32 @@ Control { model: model delegate: Label { + id: label + required property int dayCount required property string display + required property int index + anchors.bottom: parent.bottom + anchors.top: parent.top elide: Text.ElideRight + font.bold: true horizontalAlignment: Text.AlignHCenter + leftInset: index == 0 ? 1 : 0 text: display + topInset: 1 + verticalAlignment: Text.AlignVCenter width: control.dayWidth * dayCount background: Rectangle { - border.color: "red" - border.width: 1 - color: "lightgreen" + color: label.index % 2 ? control.palette.alternateBase : control.palette.base + } + + Separator { + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.top: parent.top + palette: control.palette } } } diff --git a/src/timelinelistmodel.cpp b/src/timelinelistmodel.cpp index 28a2abe..5af16e9 100644 --- a/src/timelinelistmodel.cpp +++ b/src/timelinelistmodel.cpp @@ -5,6 +5,8 @@ #include "database.h" #include "timelinemodel.h" +using namespace Qt::Literals::StringLiterals; + struct TimelineListModel::Item { Item(int id, const QString name, QObject *parent = nullptr) @@ -23,9 +25,9 @@ struct TimelineListModel::Item TimelineListModel::TimelineListModel(QObject *parent) : QAbstractListModel{parent} , m_items{} -{ - fetch(); -} + , m_fromDate{} + , m_toDate{} +{} TimelineListModel::~TimelineListModel() { @@ -57,6 +59,47 @@ QHash TimelineListModel::roleNames() const return roles; } +void TimelineListModel::clear() +{ + if (m_items.isEmpty()) { + return; + } + beginResetModel(); + qDeleteAll(m_items); + m_items.clear(); + endResetModel(); +} + +QDate TimelineListModel::fromDate() const +{ + return m_fromDate; +} + +void TimelineListModel::setFromDate(QDate date) +{ + if (date == m_fromDate) { + return; + } + m_fromDate = date; + emit fromDateChanged(m_fromDate); + refill(); +} + +QDate TimelineListModel::toDate() const +{ + return m_toDate; +} + +void TimelineListModel::setToDate(QDate date) +{ + if (date == m_toDate) { + return; + } + m_toDate = date; + emit toDateChanged(m_toDate); + refill(); +} + QModelIndex TimelineListModel::findIndexById(int id) { for (qsizetype row = 0; row < m_items.count(); ++row) { @@ -67,8 +110,13 @@ QModelIndex TimelineListModel::findIndexById(int id) return {}; } -void TimelineListModel::fetch() +void TimelineListModel::refill() { + if (!m_fromDate.isValid() || !m_toDate.isValid()) { + clear(); + return; + } + struct Lodging { int id; @@ -81,7 +129,9 @@ void TimelineListModel::fetch() QHash> bookings; }; - Database::query([]() { + QDate fromDate = m_fromDate; + QDate toDate = m_toDate; + Database::query([fromDate, toDate]() { Results results; { QSqlQuery query("select campsite_id, label from campsite order by label, campsite_id"); @@ -92,19 +142,26 @@ void TimelineListModel::fetch() } } { - QSqlQuery query(" select " - " campsite_id " - " , booking_id " - " , lower(booking_campsite.stay) " - " , upper(booking_campsite.stay) - lower(booking_campsite.stay)" - " , holder_name " - " , booking_status " - " , true" - " , true" - " from booking_campsite " - " join booking using (booking_id) " - " order by lower(booking_campsite.stay), booking_id " - ""); + QSqlQuery query; + query.setForwardOnly(true); + query.prepare(" select " + " campsite_id " + " , booking_id " + " , lower(booking_campsite.stay) " + " , upper(booking_campsite.stay) - lower(booking_campsite.stay)" + " , holder_name " + " , booking_status " + " , true" + " , true" + " from booking_campsite " + " join booking using (booking_id) " + " where booking_campsite.stay && daterange(:fromDate, :toDate)" + " order by lower(booking_campsite.stay), booking_id " + ""_L1); + Database::checkError(query); + query.bindValue(":fromDate"_L1, fromDate); + query.bindValue(":toDate"_L1, toDate); + query.exec(); Database::checkError(query); while (query.next()) { int lodgingId = query.value(0).toInt(); diff --git a/src/timelinelistmodel.h b/src/timelinelistmodel.h index 441b96d..455b743 100644 --- a/src/timelinelistmodel.h +++ b/src/timelinelistmodel.h @@ -9,6 +9,8 @@ class TimelineListModel : public QAbstractListModel { Q_OBJECT QML_ELEMENT + Q_PROPERTY(QDate fromDate READ fromDate WRITE setFromDate NOTIFY fromDateChanged REQUIRED) + Q_PROPERTY(QDate toDate READ toDate WRITE setToDate NOTIFY toDateChanged REQUIRED) public: enum Roles { @@ -22,22 +24,33 @@ public: QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QHash roleNames() const override; + void clear(); + + QDate fromDate() const; + void setFromDate(QDate date); + + QDate toDate() const; + void setToDate(QDate date); + +signals: + void fromDateChanged(QDate date); + void toDateChanged(QDate date); + private: Q_DISABLE_COPY_MOVE(TimelineListModel) struct Item; QModelIndex findIndexById(int id); - void fetch(); + void refill(); QList m_items; + QDate m_fromDate; + QDate m_toDate; }; #endif // TIMELINELISTMODEL_H