Show the timeline more like a Excel-like grid

This commit is contained in:
jordi fita mas 2025-01-15 15:22:35 +01:00
parent 2ba3167d10
commit ff27ea89d9
9 changed files with 382 additions and 144 deletions

View File

@ -23,8 +23,11 @@ qt_add_qml_module(${PROJECT_NAME}
Main.qml Main.qml
MnemonicAction.qml MnemonicAction.qml
MnemonicLabel.qml MnemonicLabel.qml
PermanentScrollBar.qml
ReservationsPage.qml ReservationsPage.qml
ReservationsTimeline.qml
SelectableLabel.qml SelectableLabel.qml
Separator.qml
TimelineDayRow.qml TimelineDayRow.qml
TimelineMonthRow.qml TimelineMonthRow.qml
) )

View File

@ -0,0 +1,5 @@
import QtQuick.Controls
ScrollBar {
policy: size < 1 ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff
}

View File

@ -1,5 +1,4 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import Camper import Camper
@ -7,10 +6,6 @@ import Camper
Page { Page {
id: page id: page
property real dayWidth: 24
property date fromDate: "2024-11-01"
property date toDate: "2025-01-31"
title: qsTr("Reservations") title: qsTr("Reservations")
header: ToolBar { header: ToolBar {
@ -23,118 +18,15 @@ Page {
} }
} }
ListView { ReservationsTimeline {
id: lodgingList anchors.fill: parent
anchors.margins: 32
dayWidth: 24
ScrollBar.vertical: verticalScroll model: TimelineListModel {
anchors.bottom: parent.bottom fromDate: "2024-11-01"
anchors.left: parent.left toDate: "2025-01-31"
anchors.top: parent.top
headerPositioning: ListView.OverlayHeader
model: timelineListModel
width: 100
delegate: Label {
required property string name
text: name
} }
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 { MnemonicAction {

View File

@ -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
}
}
}
}
}
}

12
src/Separator.qml Normal file
View File

@ -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
}

View File

@ -16,13 +16,19 @@ Control {
delegate: Label { delegate: Label {
required property string display required property string display
anchors.bottom: parent.bottom
anchors.top: parent.top
font.bold: true
horizontalAlignment: Text.AlignHCenter
text: display text: display
verticalAlignment: Text.AlignVCenter
width: control.dayWidth width: control.dayWidth
background: Rectangle { Separator {
border.color: "red" anchors.bottom: parent.bottom
border.width: 1 anchors.right: parent.right
color: "yellow" anchors.top: parent.top
palette: control.palette
} }
} }
} }

View File

@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.Fusion
Control { Control {
id: control id: control
@ -14,18 +15,32 @@ Control {
model: model model: model
delegate: Label { delegate: Label {
id: label
required property int dayCount required property int dayCount
required property string display required property string display
required property int index
anchors.bottom: parent.bottom
anchors.top: parent.top
elide: Text.ElideRight elide: Text.ElideRight
font.bold: true
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
leftInset: index == 0 ? 1 : 0
text: display text: display
topInset: 1
verticalAlignment: Text.AlignVCenter
width: control.dayWidth * dayCount width: control.dayWidth * dayCount
background: Rectangle { background: Rectangle {
border.color: "red" color: label.index % 2 ? control.palette.alternateBase : control.palette.base
border.width: 1 }
color: "lightgreen"
Separator {
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.top: parent.top
palette: control.palette
} }
} }
} }

View File

@ -5,6 +5,8 @@
#include "database.h" #include "database.h"
#include "timelinemodel.h" #include "timelinemodel.h"
using namespace Qt::Literals::StringLiterals;
struct TimelineListModel::Item struct TimelineListModel::Item
{ {
Item(int id, const QString name, QObject *parent = nullptr) Item(int id, const QString name, QObject *parent = nullptr)
@ -23,9 +25,9 @@ struct TimelineListModel::Item
TimelineListModel::TimelineListModel(QObject *parent) TimelineListModel::TimelineListModel(QObject *parent)
: QAbstractListModel{parent} : QAbstractListModel{parent}
, m_items{} , m_items{}
{ , m_fromDate{}
fetch(); , m_toDate{}
} {}
TimelineListModel::~TimelineListModel() TimelineListModel::~TimelineListModel()
{ {
@ -57,6 +59,47 @@ QHash<int, QByteArray> TimelineListModel::roleNames() const
return roles; 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) QModelIndex TimelineListModel::findIndexById(int id)
{ {
for (qsizetype row = 0; row < m_items.count(); ++row) { for (qsizetype row = 0; row < m_items.count(); ++row) {
@ -67,8 +110,13 @@ QModelIndex TimelineListModel::findIndexById(int id)
return {}; return {};
} }
void TimelineListModel::fetch() void TimelineListModel::refill()
{ {
if (!m_fromDate.isValid() || !m_toDate.isValid()) {
clear();
return;
}
struct Lodging struct Lodging
{ {
int id; int id;
@ -81,7 +129,9 @@ void TimelineListModel::fetch()
QHash<int, QList<TimelineModel::Item *>> bookings; QHash<int, QList<TimelineModel::Item *>> bookings;
}; };
Database::query([]() { QDate fromDate = m_fromDate;
QDate toDate = m_toDate;
Database::query([fromDate, toDate]() {
Results results; Results results;
{ {
QSqlQuery query("select campsite_id, label from campsite order by label, campsite_id"); QSqlQuery query("select campsite_id, label from campsite order by label, campsite_id");
@ -92,19 +142,26 @@ void TimelineListModel::fetch()
} }
} }
{ {
QSqlQuery query(" select " QSqlQuery query;
" campsite_id " query.setForwardOnly(true);
" , booking_id " query.prepare(" select "
" , lower(booking_campsite.stay) " " campsite_id "
" , upper(booking_campsite.stay) - lower(booking_campsite.stay)" " , booking_id "
" , holder_name " " , lower(booking_campsite.stay) "
" , booking_status " " , upper(booking_campsite.stay) - lower(booking_campsite.stay)"
" , true" " , holder_name "
" , true" " , booking_status "
" from booking_campsite " " , true"
" join booking using (booking_id) " " , true"
" order by lower(booking_campsite.stay), booking_id " " 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); Database::checkError(query);
while (query.next()) { while (query.next()) {
int lodgingId = query.value(0).toInt(); int lodgingId = query.value(0).toInt();

View File

@ -9,6 +9,8 @@ class TimelineListModel : public QAbstractListModel
{ {
Q_OBJECT Q_OBJECT
QML_ELEMENT 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: public:
enum Roles { enum Roles {
@ -22,22 +24,33 @@ public:
QVariant headerData(int section, QVariant headerData(int section,
Qt::Orientation orientation, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override; int role = Qt::DisplayRole) const override;
QVariant data(const QModelIndex &index, 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; int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> 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: private:
Q_DISABLE_COPY_MOVE(TimelineListModel) Q_DISABLE_COPY_MOVE(TimelineListModel)
struct Item; struct Item;
QModelIndex findIndexById(int id); QModelIndex findIndexById(int id);
void fetch(); void refill();
QList<Item *> m_items; QList<Item *> m_items;
QDate m_fromDate;
QDate m_toDate;
}; };
#endif // TIMELINELISTMODEL_H #endif // TIMELINELISTMODEL_H