Explorar o código

Choice model settings was added

Denis V. Dedkov %!s(int64=2) %!d(string=hai) anos
pai
achega
4a52926be9

+ 6 - 4
CMakeLists.txt

@@ -17,10 +17,14 @@ set(TS_FILES beerlog_ru_RU.ts)
 set(PROJECT_SOURCES
         main.cpp
         qml/qml.qrc
-        models/abstractmodel.h models/abstractmodel.cpp
-        models/summarymodel.h models/summarymodel.cpp
+        models/basemodel.h models/basemodel.cpp
+        models/ordersmodel.h models/ordersmodel.cpp
         models/usersmodel.h models/usersmodel.cpp
         viewmodels/usersviewmodel.h viewmodels/usersviewmodel.cpp
+        viewmodels/storesviewmodel.h viewmodels/storesviewmodel.cpp
+        viewmodels/productsviewmodel.h viewmodels/productsviewmodel.cpp
+        viewmodels/ordersviewmodel.h viewmodels/ordersviewmodel.cpp
+        viewmodels/settingsviewmodel.h viewmodels/settingsviewmodel.cpp
         services/beerservice.h services/beerservice.cpp
         services/settingsservice.h services/settingsservice.cpp
         ${TS_FILES}
@@ -55,8 +59,6 @@ else()
     qt5_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES})
 endif()
 
-qt_add_translations(beerlog TS_FILES beerlog_ru_RU.ts)
-
 target_link_libraries(beerlog
   PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Quick Qt${QT_VERSION_MAJOR}::WebSockets)
 

+ 31 - 5
beerlog_ru_RU.ts

@@ -14,6 +14,27 @@
         <translation>Не в сети</translation>
     </message>
 </context>
+<context>
+    <name>OrdersView</name>
+    <message>
+        <location filename="qml/Views/OrdersView.qml" line="56"/>
+        <source>Summary: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>ProductsView</name>
+    <message>
+        <location filename="qml/Views/ProductsView.qml" line="88"/>
+        <source>Summary: %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="qml/Views/ProductsView.qml" line="95"/>
+        <source>Order</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>SettingsView</name>
     <message>
@@ -40,24 +61,29 @@
         <translation></translation>
     </message>
     <message>
-        <location filename="qml/main.qml" line="72"/>
+        <location filename="qml/main.qml" line="73"/>
         <source>BeerLog v0.1</source>
         <oldsource>BeerLog v1.0.0</oldsource>
         <translation></translation>
     </message>
     <message>
-        <location filename="qml/main.qml" line="77"/>
+        <location filename="qml/main.qml" line="78"/>
+        <source>Orders</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="qml/main.qml" line="82"/>
         <source>Settings</source>
         <translation>Настройки</translation>
     </message>
     <message>
-        <location filename="qml/main.qml" line="81"/>
-        <location filename="qml/main.qml" line="117"/>
+        <location filename="qml/main.qml" line="86"/>
+        <location filename="qml/main.qml" line="122"/>
         <source>Quit</source>
         <translation>Выход</translation>
     </message>
     <message>
-        <location filename="qml/main.qml" line="121"/>
+        <location filename="qml/main.qml" line="126"/>
         <source>Realy quit the application?</source>
         <translation>Действительно выйти из приложения?</translation>
     </message>

+ 9 - 5
main.cpp

@@ -6,6 +6,11 @@
 #include <QQmlContext>
 
 #include "viewmodels/usersviewmodel.h"
+#include "viewmodels/productsviewmodel.h"
+#include "viewmodels/ordersviewmodel.h"
+#include "viewmodels/storesviewmodel.h"
+#include "viewmodels/settingsviewmodel.h"
+
 #include "services/beerservice.h"
 #include "services/settingsservice.h"
 
@@ -17,11 +22,6 @@ int main(int argc, char *argv[])
     QGuiApplication app(argc, argv);
 
     QTranslator translator;
-
-    if (!translator.load("qt_ru.qm", "qrc:/")) {
-        qWarning() << "Cant load";
-    }
-
     const QStringList uiLanguages = QLocale::system().uiLanguages();
     for (const QString &locale : uiLanguages) {
         const QString baseName = "beerlog_" + QLocale(locale).name();
@@ -45,6 +45,10 @@ int main(int argc, char *argv[])
     engine.rootContext()->setContextProperty("settingsService", SettingsService::instance());
 
     qmlRegisterType<UsersViewModel>("ru.ded.beerlog", 1, 0, "UsersViewModel");
+    qmlRegisterType<ProductsViewModel>("ru.ded.beerlog", 1, 0, "ProductsViewModel");
+    qmlRegisterType<OrdersViewModel>("ru.ded.beerlog", 1, 0, "OrdersViewModel");
+    qmlRegisterType<StoresViewModel>("ru.ded.beerlog", 1, 0, "StoresViewModel");
+    qmlRegisterType<SettingsViewModel>("ru.ded.beerlog", 1, 0, "SettingsViewModel");
 
     engine.load(url);
 

+ 0 - 41
models/abstractmodel.cpp

@@ -1,41 +0,0 @@
-#include "abstractmodel.h"
-
-#include "services/beerservice.h"
-
-AbstractModel::AbstractModel(QObject *parent)
-    : QObject{parent}
-{
-}
-
-void AbstractModel::created(const QVariant &data)
-{
-    modified(data);
-}
-
-void AbstractModel::modified(const QVariant &data)
-{
-    QVariantMap d = data.toMap();
-    m_data[d.value("id").toString()] = d;
-
-    emit dataChanged();
-}
-
-void AbstractModel::deleted(const QVariant &data)
-{
-    QString id = data.toString();
-    m_data.remove(id);
-
-    emit dataChanged();
-}
-
-void AbstractModel::received(const QVariant &data)
-{
-    m_data = data.toMap();
-
-    emit dataChanged();
-}
-
-BeerService *AbstractModel::service() const
-{
-    return BeerService::instance();
-}

+ 0 - 34
models/abstractmodel.h

@@ -1,34 +0,0 @@
-#ifndef ABSTRACTMODEL_H
-#define ABSTRACTMODEL_H
-
-#include <QObject>
-#include <QVariantMap>
-
-class BeerService;
-class AbstractModel : public QObject
-{
-    Q_OBJECT
-
-    Q_PROPERTY(QString entity READ entity CONSTANT)
-
-public:
-    explicit AbstractModel(QObject *parent = nullptr);
-
-    virtual QString entity() const = 0;
-
-public slots:
-    void created(const QVariant &data);
-    void modified(const QVariant &data);
-    void deleted(const QVariant &data);
-    void received(const QVariant &data);
-
-signals:
-    void dataChanged();
-
-protected:
-    BeerService *service() const;
-
-    QVariantMap m_data;
-};
-
-#endif // ABSTRACTMODEL_H

+ 90 - 0
models/basemodel.cpp

@@ -0,0 +1,90 @@
+#include "basemodel.h"
+
+#include "services/beerservice.h"
+
+BaseModel::BaseModel(const QString &entity, QObject *parent) : QObject{parent},
+    m_entity(entity)
+{
+    Q_ASSERT(!m_entity.isEmpty());
+
+    service()->connectListener(this);
+    service()->sendCommand(m_entity, BeerService::ActionGet);
+}
+
+BaseModel::~BaseModel()
+{
+    service()->removeListener(this);
+}
+
+QString BaseModel::entity() const
+{
+    return m_entity;
+}
+
+QVariantList BaseModel::items() const
+{
+    return m_data.values();
+}
+
+QVariantMap BaseModel::item(const QString &itemId) const
+{
+    return m_data.value(itemId).toMap();
+}
+
+QVariant BaseModel::itemProperty(const QString &itemId, const QString &propertyName, const QVariant &def) const
+{
+    return item(itemId).value(propertyName, def);
+}
+
+void BaseModel::addItem(const QVariantMap &item) const
+{
+    service()->sendCommand(entity(), BeerService::ActionAdd, item);
+}
+
+void BaseModel::deleteItem(const QString &itemId) const
+{
+    service()->sendCommand(entity(), BeerService::ActionDelete, QVariantMap { { "id", itemId } });
+}
+
+void BaseModel::modifyItem(const QString &itemId, const QVariantMap &properties) const
+{
+    QVariantMap item = this->item(itemId);
+    for (auto it = properties.constBegin(); it != properties.constEnd(); ++it) {
+        item[it.key()] = it.value();
+    }
+
+    service()->sendCommand(entity(), BeerService::ActionModify, item);
+}
+
+void BaseModel::created(const QVariant &data)
+{
+    modified(data);
+}
+
+void BaseModel::modified(const QVariant &data)
+{
+    QVariantMap d = data.toMap();
+    m_data[d.value("id").toString()] = d;
+
+    emit dataChanged();
+}
+
+void BaseModel::deleted(const QVariant &data)
+{
+    QString id = data.toString();
+    m_data.remove(id);
+
+    emit dataChanged();
+}
+
+void BaseModel::received(const QVariant &data)
+{
+    m_data = data.toMap();
+
+    emit dataChanged();
+}
+
+BeerService *BaseModel::service() const
+{
+    return BeerService::instance();
+}

+ 44 - 0
models/basemodel.h

@@ -0,0 +1,44 @@
+#ifndef BASEMODEL_H
+#define BASEMODEL_H
+
+#include <QObject>
+#include <QVariantMap>
+
+class BeerService;
+class BaseModel : public QObject
+{
+    Q_OBJECT
+
+    Q_PROPERTY(QString entity READ entity CONSTANT)
+
+public:
+    explicit BaseModel(const QString &entity, QObject *parent = nullptr);
+    virtual ~BaseModel();
+
+    QString entity() const;
+
+    QVariantList items() const;
+    QVariantMap item(const QString &itemId) const;
+    QVariant itemProperty(const QString &itemId, const QString &propertyName, const QVariant &def = QVariant{}) const;
+
+    void addItem(const QVariantMap &item) const;
+    void deleteItem(const QString &itemId) const;
+    void modifyItem(const QString &itemId, const QVariantMap &properties) const;
+
+public slots:
+    void created(const QVariant &data);
+    void modified(const QVariant &data);
+    void deleted(const QVariant &data);
+    void received(const QVariant &data);
+
+signals:
+    void dataChanged();
+
+private:
+    BeerService *service() const;
+
+    QVariantMap m_data;
+    QString m_entity;
+};
+
+#endif // BASEMODEL_H

+ 15 - 0
models/ordersmodel.cpp

@@ -0,0 +1,15 @@
+#include "ordersmodel.h"
+
+OrdersModel::OrdersModel(QObject *parent) : BaseModel { "orders", parent }
+{
+}
+
+QVariantList OrdersModel::orders() const
+{
+    return items();
+}
+
+void OrdersModel::submitOrder(const QVariantMap &order) const
+{
+    addItem(order);
+}

+ 17 - 0
models/ordersmodel.h

@@ -0,0 +1,17 @@
+#ifndef ORDERSMODEL_H
+#define ORDERSMODEL_H
+
+#include "basemodel.h"
+
+class OrdersModel : public BaseModel
+{
+    Q_OBJECT
+
+public:
+    explicit OrdersModel(QObject *parent = nullptr);
+
+    QVariantList orders() const;
+    void submitOrder(const QVariantMap &order) const;
+};
+
+#endif // ORDERSMODEL_H

+ 0 - 39
models/summarymodel.cpp

@@ -1,39 +0,0 @@
-#include "summarymodel.h"
-
-QVariantList SummaryModel::items() const
-{
-    return m_items.values();
-}
-
-float SummaryModel::sum() const
-{
-    float res = 0.0;
-    for (auto it = m_items.constBegin(); it != m_items.constEnd(); ++it) {
-        QVariantMap item = it.value().toMap();
-        int count = item.value("count", 0).toInt();
-        float price = item.value("cost", 0.0).toFloat();
-        res += count * price;
-    }
-    return res;
-}
-
-void SummaryModel::setItemCount(QVariantMap item, int count)
-{
-    QString id = item.value("id", QString()).toString();
-
-    if (count) {
-        item["count"] = count;
-        m_items[id] = item;
-    } else {
-        m_items.remove(id);
-    }
-
-    emit itemsChanged();
-}
-
-void SummaryModel::clear()
-{
-    m_items.clear();
-
-    emit itemsChanged();
-}

+ 0 - 28
models/summarymodel.h

@@ -1,28 +0,0 @@
-#ifndef SUMMARYMODEL_H
-#define SUMMARYMODEL_H
-
-#include <QObject>
-#include <QVariantMap>
-
-class SummaryModel : public QObject
-{
-    Q_OBJECT
-
-    Q_PROPERTY(QVariantList items READ items NOTIFY itemsChanged)
-    Q_PROPERTY(float sum READ sum NOTIFY itemsChanged)
-
-public:
-    QVariantList items() const;
-    float sum() const;
-
-    Q_INVOKABLE void setItemCount(QVariantMap item, int count);
-    Q_INVOKABLE void clear();
-
-signals:
-    void itemsChanged();
-
-private:
-    QVariantMap m_items;
-};
-
-#endif // SUMMARYMODEL_H

+ 2 - 18
models/usersmodel.cpp

@@ -1,24 +1,13 @@
 #include "usersmodel.h"
 
-#include "services/beerservice.h"
-
 namespace Keys {
 
-constexpr auto Users = "users";
 constexpr auto Name = "name";
 
 }
 
-UsersModel::UsersModel(QObject *parent)
-    : AbstractModel{parent}
-{
-    service()->connectListener(this);
-    service()->sendCommand(Keys::Users, "get");
-}
-
-QString UsersModel::entity() const
+UsersModel::UsersModel(QObject *parent) : BaseModel{ "users", parent }
 {
-    return Keys::Users;
 }
 
 void UsersModel::connected(const QVariant &data)
@@ -33,10 +22,5 @@ void UsersModel::disconnected(const QVariant &data)
 
 QVariantList UsersModel::users() const
 {
-    return m_data.values();
-}
-
-QString UsersModel::userName(const QString &userId) const
-{
-    return m_data.value(userId).toMap().value(Keys::Name).toString();
+    return items();
 }

+ 2 - 5
models/usersmodel.h

@@ -4,19 +4,16 @@
 #include <QObject>
 #include <QVariantMap>
 
-#include "models/abstractmodel.h"
+#include "models/basemodel.h"
 
-class UsersModel : public AbstractModel
+class UsersModel : public BaseModel
 {
     Q_OBJECT
 
 public:
     explicit UsersModel(QObject *parent = nullptr);
 
-    QString entity() const override;
-
     QVariantList users() const;
-    QString userName(const QString &userId) const;
 
 public slots:
     void connected(const QVariant &data);

+ 54 - 3
qml/Views/OrdersView.qml

@@ -1,12 +1,63 @@
 import QtQuick 2.15
 import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+import ru.ded.beerlog 1.0
 
 Page {
 
-    Label {
-        anchors.centerIn: parent
+    title: qsTr("Orders")
 
-        text: "Orders"
+    OrdersViewModel {
+        id: ordersModel
     }
 
+    ListView {
+        id: ordersList
+
+        anchors.fill: parent
+        anchors.margins: 10
+
+        model: ordersModel
+
+        section.criteria: ViewSection.FullString
+        section.property: "date"
+        section.delegate: Label {
+            text: section
+            font.bold: true
+        }
+
+        delegate: Column {
+            width: ordersList.width
+            height: productsList.heigt
+
+            ListView {
+                id: productsList
+
+                width: ordersList.width
+                height: contentHeight
+
+                model: products
+
+                header: Label {
+                    padding: 10
+                    font.bold: true
+                    text: "%1 (%2), %3".arg(userName).arg(storeName).arg(time.toLocaleTimeString(Qt.locale(), Locale.ShortFormat))
+                }
+
+                delegate: Label {
+                    width: ordersList.width
+                    leftPadding: 20
+
+                    text: "%1 x%2, %3".arg(modelData.product).arg(modelData.quantity.toLocaleString(Qt.locale())).arg(modelData.price.toLocaleCurrencyString(Qt.locale()))
+                }
+
+                footer: Label {
+                    padding: 10
+                    font.bold: true
+                    text: qsTr("Summary: %1").arg(amount.toLocaleCurrencyString(Qt.locale()))
+                }
+            }
+        }
+    }
 }

+ 106 - 0
qml/Views/ProductsView.qml

@@ -0,0 +1,106 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+import ru.ded.beerlog 1.0
+
+Page {
+
+    ProductsViewModel {
+        id: productsModel
+    }
+
+    RowLayout {
+        anchors.fill: parent
+
+        ListView {
+            id: productsList
+
+            Layout.fillWidth: true
+            Layout.preferredHeight: parent.height
+
+            model: productsModel.products
+
+            delegate: ItemDelegate {
+                width: productsList.width
+
+                text: modelData.name
+                height: spinbox.height
+
+                SpinBox {
+                    id: spinbox
+
+                    width: 150
+                    from: 0
+                    to: 100 * 100
+                    stepSize: 50
+                    anchors.right: parent.right
+                    editable: true
+
+                    property int decimals: 1
+                    property real realValue: value / 100
+
+                    validator: DoubleValidator {
+                        bottom: Math.min(spinbox.from, spinbox.to)
+                        top:  Math.max(spinbox.from, spinbox.to)
+                    }
+
+                    textFromValue: function(value, locale) {
+                        return value === 0 ? "" : Number(value / 100).toLocaleString(locale, 'f', spinbox.decimals)
+                    }
+
+                    valueFromText: function(text, locale) {
+                        return Number.fromLocaleString(locale, text) * 100
+                    }
+
+                    onRealValueChanged: productsModel.setOrderValue(modelData.id, realValue)
+                }
+            }
+
+            function reload() {
+                model = undefined
+                model = productsModel.products
+            }
+        }
+
+        ListView {
+            id: orderList
+
+            Layout.minimumWidth: parent.width * 0.3
+            Layout.preferredHeight: parent.height
+
+            model: productsModel.order
+
+            delegate: ItemDelegate {
+                width: orderList.width
+
+                text: "%1 x%2".arg(modelData.name).arg(modelData.count.toLocaleString(Qt.locale()))
+            }
+
+            footer: Column {
+                width: orderList.width
+
+                Label {
+                    anchors.right: parent.right
+                    anchors.margins: 10
+
+                    text: qsTr("Summary: %1").arg(productsModel.orderSum.toLocaleCurrencyString(Qt.locale()))
+                }
+
+                Button {
+                    anchors.right: parent.right
+                    anchors.margins: 10
+
+                    text: qsTr("Order")
+
+                    enabled: productsModel.orderSum > 0
+
+                    onClicked: {
+                        productsModel.submitOrder()
+                        productsList.reload()
+                    }
+                }
+            }
+        }
+    }
+}

+ 54 - 22
qml/Views/SettingsView.qml

@@ -1,24 +1,20 @@
 import QtQuick 2.15
 import QtQuick.Controls 2.15
 import Components 1.0
+import ru.ded.beerlog 1.0
 
 Page {
     id: root
 
     title: qsTr("Settings")
 
-    ListModel {
-        id: settingsModel
-
-        ListElement {
-            title: qsTr("BeerLog service address")
-            name: "serverAddress"
-        }
+    property var controls: {
+        "text": textComponent,
+        "choice": choiceComponent
+    }
 
-        ListElement {
-            title: qsTr("Selected user id")
-            name: "selectedUserId"
-        }
+    SettingsViewModel {
+        id: settingsModel
     }
 
     ListView {
@@ -35,6 +31,8 @@ Page {
             onClicked: {
                 inputDialog.title = model.title
                 inputDialog.name = model.name
+                inputDialog.control = controls[model.control]
+                inputDialog.choiceModel = model.choiceModel
                 inputDialog.open()
             }
         }
@@ -44,6 +42,8 @@ Page {
         id: inputDialog
 
         property string name: ""
+        property var choiceModel: undefined
+        property alias control: editor.sourceComponent
 
         width: root.width * 0.8
 
@@ -53,22 +53,54 @@ Page {
         modal: true
         standardButtons: Dialog.Ok | Dialog.Cancel
 
-        Column {
-            spacing: 20
-            anchors.fill: parent
+        Loader {
+            id: editor
 
-            TextField {
-                id: textField
+            property var value: item && item.value
 
-                width: parent.width
-                inputMethodHints: Qt.ImhNoAutoUppercase
-                placeholderText: inputDialog.title
-                text: settingsService[inputDialog.name] || ""
-            }
+            anchors.fill: parent
         }
 
         onAccepted: {
-            settingsService[inputDialog.name] = textField.text
+            settingsService[inputDialog.name] = editor.value
+        }
+    }
+
+    Component {
+        id: textComponent
+
+        TextField {
+            id: textField
+
+            property alias value: textField.text
+
+            inputMethodHints: Qt.ImhNoAutoUppercase
+            text: settingsService[inputDialog.name] || ""
+        }
+    }
+
+    ButtonGroup {
+        id: group
+    }
+
+    Component {
+        id: choiceComponent
+
+        Column {
+
+            property var value: group.checkedButton && group.checkedButton.valueId
+
+            Repeater {
+                model: inputDialog.choiceModel
+
+                delegate: RadioDelegate {
+                    property var valueId: modelData.id
+                    text: modelData.name
+                    width: parent.width
+                    checked: settingsService[inputDialog.name] === valueId
+                    ButtonGroup.group: group
+                }
+            }
         }
     }
 }

+ 27 - 4
qml/main.qml

@@ -26,6 +26,7 @@ ApplicationWindow {
             Label {
                 text: stackView.currentItem.title || usersModel.selectedUserName
                 Layout.fillWidth: true
+                Layout.minimumWidth: 100
                 horizontalAlignment: Qt.AlignCenter
 
                 MouseArea {
@@ -55,24 +56,46 @@ ApplicationWindow {
                 }
             }
         }
+
+        Menu {
+            id: storesMenu
+
+            StoresViewModel {
+                id: storesModel
+            }
+
+            Repeater {
+                model: storesModel.stores
+
+                MenuItem {
+                    text: modelData.name
+
+                    onClicked: {
+                        storesModel.selectedStore = modelData.id
+                    }
+                }
+            }
+        }
     }
 
     MainMenu {
         id: mainMenu
 
         readonly property var actions: {
+            "orders": () => { stackView.openPage("Views/OrdersView.qml") },
             "settings": () => { stackView.openPage("Views/SettingsView.qml") },
             "quit": () => { Qt.quit() }
         }
 
-        width: parent.width * 0.66
-        height: parent.height
-
         logo: "qrc:/logo.png"
         appName: qsTr("BeerLog v0.1")
         connected: beerService.connected
 
         model: ListModel {
+            ListElement {
+                title: qsTr("Orders")
+                action: "orders"
+            }
             ListElement {
                 title: qsTr("Settings")
                 action: "settings"
@@ -88,7 +111,7 @@ ApplicationWindow {
 
     StackView {
         id: stackView
-        initialItem: "Views/OrdersView.qml"
+        initialItem: "Views/ProductsView.qml"
         anchors.fill: parent
 
         function openPage(page) {

+ 1 - 0
qml/qml.qrc

@@ -11,5 +11,6 @@
         <file>Components/SubtitledItemDelegate.qml</file>
         <file>Components/qmldir</file>
         <file>qt_ru.qm</file>
+        <file>Views/ProductsView.qml</file>
     </qresource>
 </RCC>

+ 19 - 3
services/beerservice.cpp

@@ -12,6 +12,13 @@
 BeerService::BeerService()
     : QObject{nullptr}
 {
+    m_actions = {
+        { ActionGet, "get" },
+        { ActionAdd, "add" },
+        { ActionDelete, "del" },
+        { ActionModify, "mod" }
+    };
+
     restoreStash();
 
     connect(&m_socket, &QWebSocket::textMessageReceived, this, [this](QString message) {
@@ -31,7 +38,7 @@ BeerService::BeerService()
         }
     });
 
-    connect(&m_socket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error), this, [this](QAbstractSocket::SocketError error) {
+    connect(&m_socket, &QWebSocket::errorOccurred, this, [this](QAbstractSocket::SocketError error) {
         qInfo() << error << m_socket.errorString();
     });
 
@@ -60,11 +67,13 @@ SettingsService *BeerService::settings() const
     return SettingsService::instance();
 }
 
-void BeerService::sendCommand(const QString &entity, const QString &action, const QVariantMap &data)
+void BeerService::sendCommand(const QString &entity, Action action, const QVariantMap &data)
 {
+    Q_ASSERT(action != ActionUndefined);
+
     sendCommand(QVariantMap {
                     { "entity", entity },
-                    { "action", action },
+                    { "action", m_actions[action] },
                     { "data", data }
                 });
 }
@@ -75,6 +84,12 @@ void BeerService::connectListener(QObject *listener)
     m_listeners.insert(entity, listener);
 }
 
+void BeerService::removeListener(QObject *listener)
+{
+    QString entity = listener->property("entity").toString();
+    m_listeners.remove(entity, listener);
+}
+
 QString BeerService::stashFileName() const
 {
     return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/command.stash";
@@ -102,6 +117,7 @@ void BeerService::restoreStash()
         QJsonDocument doc = QJsonDocument::fromJson(stash.readAll());
         m_commandStash = doc.array().toVariantList();
         stash.close();
+        stash.remove();
     } else {
         qWarning() << stash.errorString();
     }

+ 12 - 1
services/beerservice.h

@@ -18,8 +18,18 @@ public:
         return &i;
     }
 
-    void sendCommand(const QString &entity, const QString &action, const QVariantMap &data = QVariantMap());
+    enum Action
+    {
+        ActionUndefined = 0,
+        ActionGet,
+        ActionAdd,
+        ActionDelete,
+        ActionModify
+    };
+
+    void sendCommand(const QString &entity, Action action, const QVariantMap &data = QVariantMap());
     void connectListener(QObject *listener);
+    void removeListener(QObject *listener);
 
 signals:
     void connectedChanged();
@@ -42,6 +52,7 @@ private:
 
     QWebSocket m_socket;
     QVariantList m_commandStash;
+    QMap<Action, QString> m_actions;
 };
 
 #endif // BEERSERVICE_H

+ 28 - 4
services/settingsservice.cpp

@@ -11,6 +11,7 @@ namespace Keys {
 
 constexpr auto ServerAddress = "server_address";
 constexpr auto SelectedUser = "selected_user";
+constexpr auto SelectedStore = "selected_store";
 
 }
 
@@ -26,22 +27,45 @@ void SettingsService::setValue(const QString &key, const QVariant &value)
 
 QString SettingsService::serverAddress() const
 {
-    return m_settings.value(Keys::ServerAddress, Defaults::ServerAddress).toString();
+    return value(Keys::ServerAddress, Defaults::ServerAddress).toString();
 }
 
 void SettingsService::setServerAddress(const QString &address)
 {
-    m_settings.setValue(Keys::ServerAddress, address);
+    if (serverAddress() == address) {
+        return;
+    }
+
+    setValue(Keys::ServerAddress, address);
     emit serverAddressChanged();
 }
 
 QString SettingsService::selectedUserId() const
 {
-    return m_settings.value(Keys::SelectedUser, Defaults::GuestUserId).toString();
+    return value(Keys::SelectedUser, Defaults::GuestUserId).toString();
 }
 
 void SettingsService::setSelectedUserId(const QString &userId)
 {
-    m_settings.setValue(Keys::SelectedUser, userId);
+    if (selectedUserId() == userId) {
+        return;
+    }
+
+    setValue(Keys::SelectedUser, userId);
     emit selectedUserIdChanged();
 }
+
+QString SettingsService::selectedStoreId() const
+{
+    return value(Keys::SelectedStore).toString();
+}
+
+void SettingsService::setSelectedStoreId(const QString &storeId)
+{
+    if (selectedStoreId() == storeId) {
+        return;
+    }
+
+    setValue(Keys::SelectedStore, storeId);
+    emit selectedStoreIdChanged();
+}

+ 8 - 3
services/settingsservice.h

@@ -10,6 +10,7 @@ class SettingsService : public QObject
 
     Q_PROPERTY(QString serverAddress READ serverAddress WRITE setServerAddress NOTIFY serverAddressChanged)
     Q_PROPERTY(QString selectedUserId READ selectedUserId WRITE setSelectedUserId NOTIFY selectedUserIdChanged)
+    Q_PROPERTY(QString selectedStoreId READ selectedStoreId WRITE setSelectedStoreId NOTIFY selectedStoreIdChanged)
 
 public:
     static SettingsService *instance()
@@ -18,20 +19,24 @@ public:
         return &i;
     }
 
-    QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const;
-    void setValue(const QString &key, const QVariant &value);
-
     QString serverAddress() const;
     void setServerAddress(const QString &address);
 
     QString selectedUserId() const;
     void setSelectedUserId(const QString &userId);
 
+    QString selectedStoreId() const;
+    void setSelectedStoreId(const QString &storeId);
+
 signals:
     void serverAddressChanged();
     void selectedUserIdChanged();
+    void selectedStoreIdChanged();
 
 private:
+    QVariant value(const QString &key, const QVariant &defaultValue = QVariant{}) const;
+    void setValue(const QString &key, const QVariant &value);
+
     SettingsService() = default;
     ~SettingsService() = default;
 

+ 72 - 0
viewmodels/ordersviewmodel.cpp

@@ -0,0 +1,72 @@
+#include "ordersviewmodel.h"
+
+#include <QDateTime>
+
+OrdersViewModel::OrdersViewModel(QObject *parent)
+    : QAbstractListModel{parent}
+{
+    connect(&m_ordersModel, &BaseModel::dataChanged, this, &OrdersViewModel::reload);
+    connect(&m_usersModel, &BaseModel::dataChanged, this, &OrdersViewModel::reload);
+    connect(&m_productsModel, &BaseModel::dataChanged, this, &OrdersViewModel::reload);
+    connect(&m_storesModel, &BaseModel::dataChanged, this, &OrdersViewModel::reload);
+
+    reload();
+}
+
+int OrdersViewModel::rowCount(const QModelIndex &) const
+{
+    return m_model.count();
+}
+
+QVariant OrdersViewModel::data(const QModelIndex &index, int role) const
+{
+    if (!index.isValid()) {
+        return {};
+    }
+
+    return m_model.at(index.row()).toMap()[roleNames().value(role)];
+}
+
+QHash<int, QByteArray> OrdersViewModel::roleNames() const
+{
+    return QHash<int, QByteArray> {
+        { Roles::UserName, "userName" },
+        { Roles::StoreName, "storeName" },
+        { Roles::Date, "date" },
+        { Roles::Time, "time"},
+        { Roles::Amount, "amount" },
+        { Roles::Products, "products" }
+    };
+}
+
+void OrdersViewModel::reload()
+{
+    beginResetModel();
+
+    m_model.clear();
+
+    for (const QVariant &vOrder : m_ordersModel.orders()) {
+        QVariantMap order = vOrder.toMap();
+        QDateTime orderTime = QDateTime::fromSecsSinceEpoch(order.value("ts", 0).toDouble());
+        order["date"] = orderTime.date();
+        order["time"] = orderTime.time();
+        order["userName"] = m_usersModel.itemProperty(order["userId"].toString(), "name").toString();
+        order["storeName"] = m_storesModel.itemProperty(order["storeId"].toString(), "name").toString();
+
+        QVariantList prodModel;
+        for (const QVariant &prod : order["products"].toList()) {
+            QVariantMap product = prod.toMap();
+            product["product"] = m_productsModel.itemProperty(product.value("productId").toString(), "name");
+            prodModel << product;
+        }
+        order["products"] = prodModel;
+
+        m_model << order;
+    }
+
+    std::sort(m_model.begin(), m_model.end(), [](const QVariant &l, const QVariant &r) {
+        return l.toMap().value("ts").toDouble() < r.toMap().value("ts").toDouble();
+    });
+
+    endResetModel();
+}

+ 43 - 0
viewmodels/ordersviewmodel.h

@@ -0,0 +1,43 @@
+#ifndef ORDERSVIEWMODEL_H
+#define ORDERSVIEWMODEL_H
+
+#include <QAbstractListModel>
+
+#include "models/ordersmodel.h"
+#include "models/usersmodel.h"
+
+class OrdersViewModel : public QAbstractListModel
+{
+    Q_OBJECT
+
+public:
+    explicit OrdersViewModel(QObject *parent = nullptr);
+
+    int rowCount(const QModelIndex &parent) const override;
+    QVariant data(const QModelIndex &index, int role) const override;
+    QHash<int, QByteArray> roleNames() const override;
+
+signals:
+    void ordersChanged();
+
+private:
+    enum Roles {
+        UserName = Qt::UserRole + 1,
+        StoreName,
+        Date,
+        Time,
+        Amount,
+        Products
+    };
+
+    void reload();
+
+    OrdersModel m_ordersModel;
+    UsersModel m_usersModel;
+    BaseModel m_productsModel = BaseModel("products", this);
+    BaseModel m_storesModel = BaseModel("stores", this);
+
+    QVariantList m_model;
+};
+
+#endif // ORDERSVIEWMODEL_H

+ 78 - 0
viewmodels/productsviewmodel.cpp

@@ -0,0 +1,78 @@
+#include "productsviewmodel.h"
+
+#include "services/settingsservice.h"
+#include "models/ordersmodel.h"
+
+ProductsViewModel::ProductsViewModel(QObject *parent)
+    : QObject{parent}
+{
+    connect(&m_productsModel, &BaseModel::dataChanged, this, &ProductsViewModel::productsChanged);
+}
+
+QVariantList ProductsViewModel::products() const
+{
+    return m_productsModel.items();
+}
+
+QVariantList ProductsViewModel::order() const
+{
+    QVariantList res;
+
+    for (auto it = m_order.constBegin(); it != m_order.constEnd(); ++it) {
+        QVariantMap product = m_productsModel.item(it.key());
+        product["count"] = it.value().toMap().value("quantity").toDouble();
+        res << product;
+    }
+
+    return res;
+}
+
+float ProductsViewModel::orderSum() const
+{
+    float res = 0.0;
+
+    for (auto it = m_order.constBegin(); it != m_order.constEnd(); ++it) {
+        QVariantMap product = m_productsModel.item(it.key());
+        float price = product.value("price", 0.0).toFloat();
+        float quantity = it.value().toMap().value("quantity").toDouble();
+        res += quantity * price;
+    }
+
+    return res;
+}
+
+void ProductsViewModel::setOrderValue(const QString &productId, float value)
+{
+    if (value) {
+        float price = m_productsModel.itemProperty(productId, "price", 0.0).toFloat();
+        m_order[productId] = QVariantMap {
+            { "productId", productId },
+            { "quantity", value},
+            { "price", price }
+        };
+    } else {
+        m_order.remove(productId);
+    }
+
+    emit orderChanged();
+}
+
+void ProductsViewModel::submitOrder()
+{
+    OrdersModel model;
+    model.submitOrder(QVariantMap {
+        { "userId", settings()->selectedUserId() },
+        { "storeId", settings()->selectedStoreId() },
+        { "products", m_order.values() },
+        { "amount", orderSum() }
+    });
+
+    m_order.clear();
+
+    emit orderChanged();
+}
+
+SettingsService *ProductsViewModel::settings() const
+{
+    return SettingsService::instance();
+}

+ 38 - 0
viewmodels/productsviewmodel.h

@@ -0,0 +1,38 @@
+#ifndef PRODUCTSVIEWMODEL_H
+#define PRODUCTSVIEWMODEL_H
+
+#include <QObject>
+
+#include "models/basemodel.h"
+
+class SettingsService;
+class ProductsViewModel : public QObject
+{
+    Q_OBJECT
+
+    Q_PROPERTY(QVariantList products READ products NOTIFY productsChanged)
+    Q_PROPERTY(QVariantList order READ order NOTIFY orderChanged)
+    Q_PROPERTY(float orderSum READ orderSum NOTIFY orderChanged)
+
+public:
+    explicit ProductsViewModel(QObject *parent = nullptr);
+
+    QVariantList products() const;
+    QVariantList order() const;
+    float orderSum() const;
+
+    Q_INVOKABLE void setOrderValue(const QString &productId, float value);
+    Q_INVOKABLE void submitOrder();
+
+signals:
+    void productsChanged();
+    void orderChanged();
+
+private:
+    SettingsService *settings() const;
+
+    BaseModel m_productsModel = BaseModel("products", this);
+    QVariantMap m_order;
+};
+
+#endif // PRODUCTSVIEWMODEL_H

+ 57 - 0
viewmodels/settingsviewmodel.cpp

@@ -0,0 +1,57 @@
+#include "settingsviewmodel.h"
+
+SettingsViewModel::SettingsViewModel(QObject *parent)
+    : QAbstractListModel{parent}
+{
+    m_items << SettingItem { tr("BeerLog service address"), "serverAddress", "text" }
+            << SettingItem { tr("Selected user id"), "selectedUserId", "choice", "users" }
+            << SettingItem { tr("Selected store"), "selectedStoreId", "choice", "stores" };
+
+    m_models["users"] = new BaseModel("users", this);
+    m_models["stores"] = new BaseModel("stores", this);
+}
+
+
+int SettingsViewModel::rowCount(const QModelIndex &) const
+{
+    return m_items.count();
+}
+
+QVariant SettingsViewModel::data(const QModelIndex &index, int role) const
+{
+    if (!index.isValid()) {
+        return {};
+    }
+
+    SettingItem item = m_items.at(index.row());
+
+    switch (role) {
+    case Roles::Title: return item.title;
+    case Roles::PropertyName: return item.propertyName;
+    case Roles::ControlType: return item.controlType;
+    case Roles::Model: return model(item.modelName);
+    default:
+        break;
+    }
+
+    return {};
+}
+
+QHash<int, QByteArray> SettingsViewModel::roleNames() const
+{
+    return QHash<int, QByteArray> {
+        { Roles::Title, "title" },
+        { Roles::PropertyName, "name" },
+        { Roles::ControlType, "control" },
+        { Roles::Model, "choiceModel" }
+    };
+}
+
+QVariant SettingsViewModel::model(const QString &modelName) const
+{
+    if (!m_models.contains(modelName)) {
+        return {};
+    }
+
+    return m_models[modelName]->items();
+}

+ 47 - 0
viewmodels/settingsviewmodel.h

@@ -0,0 +1,47 @@
+#ifndef SETTINGSVIEWMODEL_H
+#define SETTINGSVIEWMODEL_H
+
+#include <QAbstractListModel>
+
+#include "models/basemodel.h"
+
+class SettingsViewModel : public QAbstractListModel
+{
+    Q_OBJECT
+
+public:
+    explicit SettingsViewModel(QObject *parent = nullptr);
+
+    int rowCount(const QModelIndex &parent) const override;
+    QVariant data(const QModelIndex &index, int role) const override;
+    QHash<int, QByteArray> roleNames() const override;
+
+private:
+    enum Roles {
+        Title = Qt::UserRole + 1,
+        PropertyName,
+        ControlType,
+        Model
+    };
+
+    struct SettingItem
+    {
+        QString title;
+        QString propertyName;
+        QString controlType;
+        QString modelName;
+
+        SettingItem(const QString &title, const QString &propertyName, const QString &controlType, const QString &modelName = {}) :
+            title(title),
+            propertyName(propertyName),
+            controlType(controlType),
+            modelName(modelName) {}
+    };
+
+    QVariant model(const QString &modelName) const;
+
+    QList<SettingItem> m_items;
+    QMap<QString, BaseModel *> m_models;
+};
+
+#endif // SETTINGSVIEWMODEL_H

+ 38 - 0
viewmodels/storesviewmodel.cpp

@@ -0,0 +1,38 @@
+#include "storesviewmodel.h"
+
+#include "services/settingsservice.h"
+
+StoresViewModel::StoresViewModel(QObject *parent)
+    : QObject{parent}
+{
+    connect(&m_storesModel, &BaseModel::dataChanged, this, &StoresViewModel::storesChanged);
+    connect(&m_storesModel, &BaseModel::dataChanged, this, &StoresViewModel::selectedStoreNameChanged);
+
+    connect(settings(), &SettingsService::selectedStoreIdChanged, this, &StoresViewModel::selectedStoreChanged);
+    connect(settings(), &SettingsService::selectedStoreIdChanged, this, &StoresViewModel::selectedStoreNameChanged);
+}
+
+QVariantList StoresViewModel::stores() const
+{
+    return m_storesModel.items();
+}
+
+QString StoresViewModel::selectedStore() const
+{
+    return settings()->selectedStoreId();
+}
+
+void StoresViewModel::setSelectedStore(const QString &newSelectedStore)
+{
+    settings()->setSelectedStoreId(newSelectedStore);
+}
+
+QString StoresViewModel::selectedStoreName() const
+{
+    return m_storesModel.itemProperty(selectedStore(), "name").toString();
+}
+
+SettingsService *StoresViewModel::settings() const
+{
+    return SettingsService::instance();
+}

+ 36 - 0
viewmodels/storesviewmodel.h

@@ -0,0 +1,36 @@
+#ifndef STORESVIEWMODEL_H
+#define STORESVIEWMODEL_H
+
+#include <QObject>
+
+#include "models/basemodel.h"
+
+class SettingsService;
+class StoresViewModel : public QObject
+{
+    Q_OBJECT
+
+    Q_PROPERTY(QVariantList stores READ stores NOTIFY storesChanged)
+    Q_PROPERTY(QString selectedStore READ selectedStore WRITE setSelectedStore NOTIFY selectedStoreChanged)
+    Q_PROPERTY(QString selectedStoreName READ selectedStoreName NOTIFY selectedStoreNameChanged)
+
+public:
+    explicit StoresViewModel(QObject *parent = nullptr);
+
+    QVariantList stores() const;
+    QString selectedStore() const;
+    void setSelectedStore(const QString &newSelectedStore);
+    QString selectedStoreName() const;
+
+signals:
+    void storesChanged();
+    void selectedStoreChanged();
+    void selectedStoreNameChanged();
+
+private:
+    SettingsService *settings() const;
+
+    BaseModel m_storesModel = BaseModel("stores", this);
+};
+
+#endif // STORESVIEWMODEL_H

+ 3 - 7
viewmodels/usersviewmodel.cpp

@@ -5,8 +5,8 @@
 UsersViewModel::UsersViewModel(QObject *parent)
     : QObject{parent}
 {
-    connect(&m_usersModel, &AbstractModel::dataChanged, this, &UsersViewModel::usersChanged);
-    connect(&m_usersModel, &AbstractModel::dataChanged, this, &UsersViewModel::selectedUserNameChanged);
+    connect(&m_usersModel, &BaseModel::dataChanged, this, &UsersViewModel::usersChanged);
+    connect(&m_usersModel, &BaseModel::dataChanged, this, &UsersViewModel::selectedUserNameChanged);
 
     connect(settings(), &SettingsService::selectedUserIdChanged, this, &UsersViewModel::selectedUserChanged);
     connect(settings(), &SettingsService::selectedUserIdChanged, this, &UsersViewModel::selectedUserNameChanged);
@@ -24,16 +24,12 @@ QString UsersViewModel::selectedUser() const
 
 void UsersViewModel::setSelectedUser(const QString &newSelectedUser)
 {
-    if (selectedUser() == newSelectedUser) {
-        return;
-    }
-
     settings()->setSelectedUserId(newSelectedUser);
 }
 
 QString UsersViewModel::selectedUserName() const
 {
-    return m_usersModel.userName(selectedUser());
+    return m_usersModel.itemProperty(selectedUser(), "name").toString();
 }
 
 SettingsService *UsersViewModel::settings() const