363 lines
12 KiB
C++
363 lines
12 KiB
C++
|
|
#include "ticket_sales_report_widget.h"
|
||
|
|
|
||
|
|
#include "error_dialog.h"
|
||
|
|
#include "repository_manager.h"
|
||
|
|
#include "repository_result.h"
|
||
|
|
#include "schema_metadata.h"
|
||
|
|
#include "session_statistics_dto.h"
|
||
|
|
#include "sessions_statistics_repository.h"
|
||
|
|
#include "table_info_sqlite.h"
|
||
|
|
#include <QDateTimeAxis>
|
||
|
|
#include <QMessageBox>
|
||
|
|
#include <QScatterSeries>
|
||
|
|
#include <QValueAxis>
|
||
|
|
|
||
|
|
TicketSalesReportWidget::SalesData::SalesData()
|
||
|
|
: refundExpenses_(0), refundedTickets_(0), salesRevenue_(0),
|
||
|
|
soldTickets_(0), maximumTickets_(0) {}
|
||
|
|
|
||
|
|
TicketSalesReportWidget::SalesData::SalesData(double refundExpenses,
|
||
|
|
double salesRevenue,
|
||
|
|
qint32 refundedTickets,
|
||
|
|
qint32 soldTickets,
|
||
|
|
qint32 maxTickets)
|
||
|
|
: refundExpenses_(refundExpenses), refundedTickets_(refundedTickets),
|
||
|
|
salesRevenue_(salesRevenue), soldTickets_(soldTickets),
|
||
|
|
maximumTickets_(maxTickets) {}
|
||
|
|
|
||
|
|
TicketSalesReportWidget::SalesData
|
||
|
|
TicketSalesReportWidget::SalesData::operator+(const SalesData &other) const {
|
||
|
|
SalesData result;
|
||
|
|
result.setMaximumTickets(maximumTickets() + other.maximumTickets());
|
||
|
|
result.setSoldTickets(soldTickets() + other.soldTickets());
|
||
|
|
result.setRefundedTickets(refundedTickets() + other.refundedTickets());
|
||
|
|
result.setSalesRevenue(salesRevenue() + other.salesRevenue());
|
||
|
|
result.setRefundExpenses(refundExpenses() + other.refundExpenses());
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
TicketSalesReportWidget::SalesData &
|
||
|
|
TicketSalesReportWidget::SalesData::operator+=(const SalesData &other) {
|
||
|
|
*this = std::move(*this + other);
|
||
|
|
return *this;
|
||
|
|
}
|
||
|
|
|
||
|
|
TicketSalesReportWidget::TicketSalesReportWidget(QWidget *parent)
|
||
|
|
: QWidget(parent) {
|
||
|
|
setupUi(this);
|
||
|
|
|
||
|
|
initializeRepositories();
|
||
|
|
initializeChart();
|
||
|
|
setupChart(); // After initialize chart
|
||
|
|
setupConnections();
|
||
|
|
}
|
||
|
|
|
||
|
|
void TicketSalesReportWidget::configureReportForGenre() {
|
||
|
|
TableInfoSQLite tableInfo("genres");
|
||
|
|
columnInfoIndex_ = tableInfo.columnIndex("genre_name");
|
||
|
|
|
||
|
|
configureEntityFinder(
|
||
|
|
"genres", EntityFinderWidget::tr("Select genre"),
|
||
|
|
EntityFinderWidget::tr("Here will be information about selected genre"));
|
||
|
|
|
||
|
|
sessionsStatisticsMethod_ =
|
||
|
|
std::bind(&SessionsStatisticsRepository::getSessionsForGenre,
|
||
|
|
sessionsStatisticsRepository,
|
||
|
|
std::placeholders::_1, // entityId
|
||
|
|
std::placeholders::_2, // startDate
|
||
|
|
std::placeholders::_3, // endDate
|
||
|
|
std::placeholders::_4 // sessions
|
||
|
|
);
|
||
|
|
|
||
|
|
billetChartTitle_ = tr("Popularity of Genre: Name: \"%1\"");
|
||
|
|
seriesName_ = tr("Tickets Sold");
|
||
|
|
yAxisTitle_ = tr("Number of Tickets Sold");
|
||
|
|
}
|
||
|
|
|
||
|
|
void TicketSalesReportWidget::configureReportForHall() {
|
||
|
|
TableInfoSQLite tableInfo("halls");
|
||
|
|
columnInfoIndex_ = tableInfo.columnIndex("hall_name");
|
||
|
|
|
||
|
|
configureEntityFinder(
|
||
|
|
"halls", EntityFinderWidget::tr("Select hall"),
|
||
|
|
EntityFinderWidget::tr("Here will be information about selected hall"));
|
||
|
|
|
||
|
|
sessionsStatisticsMethod_ =
|
||
|
|
std::bind(&SessionsStatisticsRepository::getSessionsForHall,
|
||
|
|
sessionsStatisticsRepository,
|
||
|
|
std::placeholders::_1, // entityId
|
||
|
|
std::placeholders::_2, // startDate
|
||
|
|
std::placeholders::_3, // endDate
|
||
|
|
std::placeholders::_4 // sessions
|
||
|
|
);
|
||
|
|
|
||
|
|
billetChartTitle_ = tr("Attendance for Hall: Name: \"%1\"");
|
||
|
|
seriesName_ = tr("Visitors");
|
||
|
|
yAxisTitle_ = tr("Number of Visitors");
|
||
|
|
}
|
||
|
|
|
||
|
|
void TicketSalesReportWidget::configureReportForMovie() {
|
||
|
|
TableInfoSQLite tableInfo("movies");
|
||
|
|
columnInfoIndex_ = tableInfo.columnIndex("title");
|
||
|
|
|
||
|
|
configureEntityFinder(
|
||
|
|
"movies", EntityFinderWidget::tr("Select movie"),
|
||
|
|
EntityFinderWidget::tr("Here will be information about selected movie"));
|
||
|
|
|
||
|
|
sessionsStatisticsMethod_ =
|
||
|
|
std::bind(&SessionsStatisticsRepository::getSessionsForMovie,
|
||
|
|
sessionsStatisticsRepository,
|
||
|
|
std::placeholders::_1, // entityId
|
||
|
|
std::placeholders::_2, // startDate
|
||
|
|
std::placeholders::_3, // endDate
|
||
|
|
std::placeholders::_4 // sessions
|
||
|
|
);
|
||
|
|
|
||
|
|
billetChartTitle_ = tr("Tickets Sold for Movie: Title: \"%1\"");
|
||
|
|
seriesName_ = tr("Tickets Sold");
|
||
|
|
yAxisTitle_ = tr("Number of Tickets Sold");
|
||
|
|
}
|
||
|
|
|
||
|
|
void TicketSalesReportWidget::initializeRepositories() {
|
||
|
|
RepositoryManager &repositoryManager = RepositoryManager::instance();
|
||
|
|
SchemaMetadata metadata = SchemaMetadata::defaultSchema();
|
||
|
|
|
||
|
|
sessionsStatisticsRepository =
|
||
|
|
repositoryManager.getRepositoryAs<SessionsStatisticsRepository>(
|
||
|
|
metadata.repositoryTableName("sessions_statistics"));
|
||
|
|
|
||
|
|
if (!bool(sessionsStatisticsRepository)) {
|
||
|
|
qFatal() << "TicketSalesReportWidget::initializeRepositories: Failed to "
|
||
|
|
"initialize repositories";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void TicketSalesReportWidget::initializeChart() {
|
||
|
|
chartView_ = new QChartView(this);
|
||
|
|
chartView_->setRenderHint(QPainter::Antialiasing);
|
||
|
|
}
|
||
|
|
|
||
|
|
void TicketSalesReportWidget::setupChart() {
|
||
|
|
frameChart->layout()->addWidget(chartView_);
|
||
|
|
}
|
||
|
|
|
||
|
|
void TicketSalesReportWidget::setupConnections() {
|
||
|
|
connect(btnGenerate, &QPushButton::clicked, this,
|
||
|
|
&TicketSalesReportWidget::onPushButtonGenerateClicked);
|
||
|
|
|
||
|
|
connect(entityFinder, &EntityFinderWidget::fieldRetrieved, this,
|
||
|
|
&TicketSalesReportWidget::onFieldRetrieved);
|
||
|
|
|
||
|
|
connect(entityFinder, &EntityFinderWidget::reseted, this,
|
||
|
|
&TicketSalesReportWidget::onEntityFinderReseted);
|
||
|
|
|
||
|
|
connect(entityFinder, &EntityFinderWidget::recordRetrieved, this,
|
||
|
|
&TicketSalesReportWidget::onRecordRetrieved);
|
||
|
|
}
|
||
|
|
|
||
|
|
void TicketSalesReportWidget::configureEntityFinder(
|
||
|
|
const QString &databaseTableName, const QString &labelText,
|
||
|
|
const QString &placeholder) {
|
||
|
|
entityFinder->setDefaultRelation(databaseTableName);
|
||
|
|
entityFinder->setLabelText(labelText);
|
||
|
|
entityFinder->setLineEditPlaceholder(placeholder);
|
||
|
|
}
|
||
|
|
|
||
|
|
void TicketSalesReportWidget::onEntityFinderReseted() {
|
||
|
|
selectedEntityId_ = -1;
|
||
|
|
chartView_->setChart(new QChart());
|
||
|
|
}
|
||
|
|
|
||
|
|
void TicketSalesReportWidget::onPushButtonGenerateClicked() {
|
||
|
|
if (!dateRange(startRange_, endRange_)) {
|
||
|
|
QMessageBox::QMessageBox::warning(this, QMessageBox::tr("Error"),
|
||
|
|
QMessageBox::tr("Invalid date range"));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
QVector<SessionStatisticsDTO> sessions;
|
||
|
|
RepositoryResult result = retrieveSessions(sessions);
|
||
|
|
if (!result.success) {
|
||
|
|
ErrorDialog::displayError(this, result.errorMessage);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (sessions.isEmpty()) {
|
||
|
|
QMessageBox::warning(this, QMessageBox::tr("Error"),
|
||
|
|
QMessageBox::tr("No sessions found between %1 and %2.")
|
||
|
|
.arg(startRange_.toString("dd.MM.yyyy"))
|
||
|
|
.arg(endRange_.toString("dd.MM.yyyy")));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
createChart(aggregateByDateTime(sessions));
|
||
|
|
}
|
||
|
|
|
||
|
|
void TicketSalesReportWidget::onFieldRetrieved(const QSqlField &field) {
|
||
|
|
selectedEntityId_ = field.value().toInt();
|
||
|
|
chartView_->setChart(new QChart());
|
||
|
|
}
|
||
|
|
|
||
|
|
void TicketSalesReportWidget::onRecordRetrieved(const QSqlRecord &rowValues) {
|
||
|
|
QString columnInfoValue = rowValues.value(columnInfoIndex_).toString();
|
||
|
|
chartTitle_ = billetChartTitle_.arg(columnInfoValue);
|
||
|
|
}
|
||
|
|
|
||
|
|
QMap<QDateTime, TicketSalesReportWidget::SalesData>
|
||
|
|
TicketSalesReportWidget::aggregateByDateTime(
|
||
|
|
const QVector<SessionStatisticsDTO> &sessions,
|
||
|
|
std::function<bool(const SessionStatisticsDTO &)> filter) {
|
||
|
|
|
||
|
|
QMap<QDateTime, SalesData> accamulatedSalesData;
|
||
|
|
|
||
|
|
for (const SessionStatisticsDTO &session : sessions) {
|
||
|
|
if (!filter || filter(session)) {
|
||
|
|
QDateTime dateTime = session.beginAt();
|
||
|
|
|
||
|
|
SalesData buffer;
|
||
|
|
buffer.setRefundExpenses(session.refundExpenses());
|
||
|
|
buffer.setRefundedTickets(session.refundedTickets());
|
||
|
|
buffer.setSalesRevenue(session.salesRevenue());
|
||
|
|
buffer.setSoldTickets(session.soldTickets());
|
||
|
|
buffer.setMaximumTickets(session.totalSeats());
|
||
|
|
|
||
|
|
accamulatedSalesData[dateTime] += buffer;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return accamulatedSalesData;
|
||
|
|
};
|
||
|
|
|
||
|
|
QAbstractAxis *TicketSalesReportWidget::createDateTimeAxis(
|
||
|
|
int tickCount, const QString &axisTitle, const QDateTime &minDate,
|
||
|
|
const QDateTime &maxDate) const {
|
||
|
|
QDateTimeAxis *axisX = new QDateTimeAxis();
|
||
|
|
axisX->setTitleText(axisTitle);
|
||
|
|
axisX->setFormat("dd.MM.yyyy");
|
||
|
|
axisX->setTickCount(tickCount);
|
||
|
|
axisX->setRange(minDate, maxDate);
|
||
|
|
return axisX;
|
||
|
|
}
|
||
|
|
|
||
|
|
QAbstractAxis *
|
||
|
|
TicketSalesReportWidget::createValueAxis(int maxValue,
|
||
|
|
const QString &axisTitle) const {
|
||
|
|
QValueAxis *axisY = new QValueAxis();
|
||
|
|
axisY->setTitleText(axisTitle);
|
||
|
|
axisY->setLabelFormat("%d");
|
||
|
|
axisY->setRange(0, maxValue);
|
||
|
|
return axisY;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool TicketSalesReportWidget::validateData(
|
||
|
|
const QMap<QDateTime, SalesData> &data) const {
|
||
|
|
if (!data.isEmpty()) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
qWarning() << "No data for chart:" << chartTitle_;
|
||
|
|
|
||
|
|
// QMessageBox::warning(this, QMessageBox::tr("Error"),
|
||
|
|
// QMessageBox::tr("No data for chart"));
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
RepositoryResult TicketSalesReportWidget::retrieveSessions(
|
||
|
|
QVector<SessionStatisticsDTO> &sessions) {
|
||
|
|
RepositoryResult result =
|
||
|
|
sessionsStatisticsMethod_(selectedEntityId_, std::cref(startRange_),
|
||
|
|
std::cref(endRange_), std::ref(sessions));
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool TicketSalesReportWidget::dateRange(QDateTime &startOut,
|
||
|
|
QDateTime &endOut) {
|
||
|
|
startOut = dateTimeEditStartOfPeriod->dateTime();
|
||
|
|
endOut = dateTimeEditEndOfPeriod->dateTime();
|
||
|
|
return endOut > startOut;
|
||
|
|
}
|
||
|
|
|
||
|
|
void TicketSalesReportWidget::createChart(
|
||
|
|
const QMap<QDateTime, SalesData> &data) {
|
||
|
|
if (!validateData(data)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
QDateTime minDate, maxDate;
|
||
|
|
int tickCount = data.size();
|
||
|
|
if (tickCount > 10) {
|
||
|
|
tickCount = 10;
|
||
|
|
}
|
||
|
|
|
||
|
|
QChart *chart = new QChart();
|
||
|
|
chart->setTitle(chartTitle_);
|
||
|
|
|
||
|
|
QAbstractSeries *maximumTicketsSeries =
|
||
|
|
createScatterSeries(data, minDate, maxDate, &SalesData::maximumTickets,
|
||
|
|
tr("Maximum Tickets"));
|
||
|
|
QAbstractSeries *soldTicketsSeries = createScatterSeries(
|
||
|
|
data, minDate, maxDate, &SalesData::soldTickets, tr("Sold Tickets"));
|
||
|
|
QAbstractSeries *refundedTicketsSeries =
|
||
|
|
createScatterSeries(data, minDate, maxDate, &SalesData::refundedTickets,
|
||
|
|
tr("Refunded Tickets"));
|
||
|
|
QAbstractSeries *salesRevenueSeries = createScatterSeries(
|
||
|
|
data, minDate, maxDate, &SalesData::salesRevenue, tr("Sales Revenue"));
|
||
|
|
QAbstractSeries *refundExpensesSeries =
|
||
|
|
createScatterSeries(data, minDate, maxDate, &SalesData::refundExpenses,
|
||
|
|
tr("Refund Expenses"));
|
||
|
|
|
||
|
|
chart->addSeries(maximumTicketsSeries);
|
||
|
|
chart->addSeries(soldTicketsSeries);
|
||
|
|
chart->addSeries(refundedTicketsSeries);
|
||
|
|
chart->addSeries(salesRevenueSeries);
|
||
|
|
chart->addSeries(refundExpensesSeries);
|
||
|
|
|
||
|
|
QAbstractAxis *axisX =
|
||
|
|
createDateTimeAxis(tickCount, tr("Date"), minDate, maxDate);
|
||
|
|
chart->addAxis(axisX, Qt::AlignBottom);
|
||
|
|
|
||
|
|
double maxMoneyValue = std::max({maxValue(data, &SalesData::refundExpenses),
|
||
|
|
maxValue(data, &SalesData::salesRevenue)});
|
||
|
|
QAbstractAxis *axisYMoney = createValueAxis(maxMoneyValue, tr("Money"));
|
||
|
|
chart->addAxis(axisYMoney, Qt::AlignLeft);
|
||
|
|
|
||
|
|
int maxTickets = maxValue(data, &SalesData::maximumTickets);
|
||
|
|
|
||
|
|
QAbstractAxis *axisYTickets = createValueAxis(maxTickets, tr("Tickets"));
|
||
|
|
chart->addAxis(axisYTickets, Qt::AlignRight);
|
||
|
|
|
||
|
|
qDebug() << "Maximum movey Value" << maxMoneyValue << "maxTickets"
|
||
|
|
<< maxTickets;
|
||
|
|
|
||
|
|
maximumTicketsSeries->attachAxis(axisYTickets);
|
||
|
|
refundedTicketsSeries->attachAxis(axisYTickets);
|
||
|
|
soldTicketsSeries->attachAxis(axisYTickets);
|
||
|
|
|
||
|
|
refundExpensesSeries->attachAxis(axisYMoney);
|
||
|
|
salesRevenueSeries->attachAxis(axisYMoney);
|
||
|
|
|
||
|
|
maximumTicketsSeries->attachAxis(axisX);
|
||
|
|
refundExpensesSeries->attachAxis(axisX);
|
||
|
|
refundedTicketsSeries->attachAxis(axisX);
|
||
|
|
salesRevenueSeries->attachAxis(axisX);
|
||
|
|
soldTicketsSeries->attachAxis(axisX);
|
||
|
|
|
||
|
|
updateChartView(chart);
|
||
|
|
}
|
||
|
|
|
||
|
|
void TicketSalesReportWidget::setupChart(QChart *chart, QAbstractSeries *series,
|
||
|
|
QAbstractAxis *axisX,
|
||
|
|
QAbstractAxis *axisY) const {
|
||
|
|
chart->addSeries(series);
|
||
|
|
chart->addAxis(axisX, Qt::AlignBottom);
|
||
|
|
chart->addAxis(axisY, Qt::AlignLeft);
|
||
|
|
series->attachAxis(axisX);
|
||
|
|
series->attachAxis(axisY);
|
||
|
|
}
|
||
|
|
|
||
|
|
void TicketSalesReportWidget::updateChartView(QChart *chart) const {
|
||
|
|
chartView_->setChart(chart);
|
||
|
|
chartView_->setRenderHint(QPainter::Antialiasing);
|
||
|
|
}
|