Show the timeline more like a Excel-like grid
This commit is contained in:
parent
2ba3167d10
commit
ff27ea89d9
|
@ -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
|
||||
)
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import QtQuick.Controls
|
||||
|
||||
ScrollBar {
|
||||
policy: size < 1 ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<int, QByteArray> 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<int, QList<TimelineModel::Item *>> 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();
|
||||
|
|
|
@ -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<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:
|
||||
Q_DISABLE_COPY_MOVE(TimelineListModel)
|
||||
|
||||
struct Item;
|
||||
|
||||
QModelIndex findIndexById(int id);
|
||||
void fetch();
|
||||
void refill();
|
||||
|
||||
QList<Item *> m_items;
|
||||
QDate m_fromDate;
|
||||
QDate m_toDate;
|
||||
};
|
||||
|
||||
#endif // TIMELINELISTMODEL_H
|
||||
|
|
Loading…
Reference in New Issue