Skip to content

Commit

Permalink
Merge pull request #231 from vsimakhin/feature/totals-by-months
Browse files Browse the repository at this point in the history
Add new stats - totals by month
  • Loading branch information
vsimakhin authored May 31, 2024
2 parents 282042d + 74391a7 commit 2fa6c51
Show file tree
Hide file tree
Showing 10 changed files with 260 additions and 27 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## [Unreleased]

- New: New stats page - Totals by Month

## [2.35.2] - 20.05.2024

- Fix: Proper password field validation on Save
Expand Down
19 changes: 5 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ You also can easily export all flight records into EASA style pdf format, print

# Changelog

## [Unreleased]

- New: New stats page - Totals by Month

## [2.35.2] - 20.05.2024

- Fix: Proper password field validation on Save
Expand All @@ -39,20 +43,6 @@ You also can easily export all flight records into EASA style pdf format, print
- Added simple brutforce protection for login page.
- Optimize internal helper function, so need for additional error check.

## [2.34.0] - 03.04.2024

- New: Add support for MySQL database. Check [readme](https://github.com/vsimakhin/web-logbook?tab=readme-ov-file#mysql-database) for more details.
- Update: Refactored PDF export package. No UI changes.
- New: Add support for a custom title page for PDF A4/A5 exports.
- Update: Update openlayers lib from 7.3.0 to 9.0.0. No UI changes.
- Update: Update golang from 1.20.3 to 1.21.8. No UI changes.
- Fix: Finally fixed the unit tests. No UI changes.

## [2.33.0] - 08.03.2024

- New: License Number and Address fields on the `Settings` page. They are printed (if not empty) on the title page for PDF exported logbook.
- Fix: Try to fix a `Map` page layout for Mac OS

The full changelog is [here](https://github.com/vsimakhin/web-logbook/blob/main/CHANGELOG.md)

# Usage
Expand Down Expand Up @@ -121,6 +111,7 @@ Since it's written in Golang, it can run on any system after compiling the sourc
* Statistics
* Totals
* By Year
* By Month
* By Aircraft
* By Aircraft group/class
* Limits (EASA flight time limitations)
Expand Down
56 changes: 56 additions & 0 deletions app/handlers_stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ package main
import (
"fmt"
"net/http"
"strconv"
"time"

"github.com/go-chi/chi/v5"
"github.com/vsimakhin/web-logbook/internal/models"
"golang.org/x/exp/slices"
)

const farFuture = "30000101"
Expand Down Expand Up @@ -312,6 +316,58 @@ func (app *application) HandlerStatsTotalsByYearPage(w http.ResponseWriter, r *h
}
}

// HandlerStatsTotalsByMonthPage is a handler for Stats Totals by Month page
func (app *application) HandlerStatsTotalsByMonthPage(w http.ResponseWriter, r *http.Request) {
// get all years in the logbook
years, err := app.db.GetYears()
if err != nil {
app.errorLog.Println(err)
}

// year parameter
year := chi.URLParam(r, "year")
_, err = strconv.Atoi(year)
if err != nil || !slices.Contains(years, year) {
year = years[0]
}

// start and end dates for the selected year
startDate, err := time.Parse("20060102", fmt.Sprintf("%s0101", year))
if err != nil {
app.errorLog.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

endDate, err := time.Parse("20060102", fmt.Sprintf("%s1231", year))
if err != nil {
app.errorLog.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// get totals by month
totalsByMonth, err := app.db.GetDetailedTotals(
startDate.Format("20060102"), endDate.Format("20060102"), true,
app.db.GenerateFlightRecordMap(startDate, endDate, true),
)
if err != nil {
app.errorLog.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

data := make(map[string]interface{})
data["activePage"] = "stats"
data["activeSubPage"] = "totalsByMonth"
data["totalsByMonth"] = totalsByMonth
data["years"] = years
data["year"] = year
if err := app.renderTemplate(w, r, "stats-totals-month", &templateData{Data: data}); err != nil {
app.errorLog.Println(err)
}
}

// HandlerStatsTotalsByTypePage is a handler for Stats Totals by Type page
func (app *application) HandlerStatsTotalsByTypePage(w http.ResponseWriter, r *http.Request) {
data := make(map[string]interface{})
Expand Down
10 changes: 8 additions & 2 deletions app/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,18 @@ func (app *application) getDetailedLimitsStats() (map[string]map[string]string,
getData := func(key string, startDate time.Time, groupByMonth bool) error {
total, err := app.db.GetDetailedTotals(
startDate.Format("20060102"), farFuture,
groupByMonth, app.db.GenerateFlightRecordMap(startDate, groupByMonth),
groupByMonth, app.db.GenerateFlightRecordMap(startDate, now, groupByMonth),
)
if err != nil {
return err
}
totals[key] = total

totalTimeMap := make(map[string]string)
for key, record := range total {
totalTimeMap[key] = record.Time.Total
}

totals[key] = totalTimeMap
return nil
}

Expand Down
6 changes: 6 additions & 0 deletions app/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ const (
APIStatsLimits = "/stats/data/limits"
APIStatsTotalsPage = "/stats-totals"
APIStatsTotalsByYearPage = "/stats-totals-by-year"
APIStatsTotalsByMonthPage = "/stats-totals-by-month"
APIStatsTotalsByMonthYearPage = "/stats-totals-by-month/{year}"
APIStatsTotalsByTypePage = "/stats-totals-by-type"
APIStatsTotalsByClassPage = "/stats-totals-by-class"
APIStatsLimitsPage = "/stats-limits"
Expand Down Expand Up @@ -111,6 +113,8 @@ var apiMap = map[string]string{
"StatsLimits": APIStatsLimits,
"StatsTotalsPage": APIStatsTotalsPage,
"StatsTotalsByYearPage": APIStatsTotalsByYearPage,
"StatsTotalsByMonthPage": APIStatsTotalsByMonthPage,
"StatsTotalsByMonthYearPage": APIStatsTotalsByMonthYearPage,
"StatsTotalsByTypePage": APIStatsTotalsByTypePage,
"StatsTotalsByClassPage": APIStatsTotalsByClassPage,
"StatsLimitsPage": APIStatsLimitsPage,
Expand Down Expand Up @@ -205,6 +209,8 @@ func (app *application) routes() *chi.Mux {

server.Get(APIStatsTotalsPage, app.HandlerStatsTotalsPage)
server.Get(APIStatsTotalsByYearPage, app.HandlerStatsTotalsByYearPage)
server.Get(APIStatsTotalsByMonthPage, app.HandlerStatsTotalsByMonthPage)
server.Get(APIStatsTotalsByMonthYearPage, app.HandlerStatsTotalsByMonthPage)
server.Get(APIStatsTotalsByTypePage, app.HandlerStatsTotalsByTypePage)
server.Get(APIStatsTotalsByClassPage, app.HandlerStatsTotalsByClassPage)
server.Get(APIStatsLimitsPage, app.HandlerStatsLimitsPage)
Expand Down
28 changes: 28 additions & 0 deletions app/static/js/wlb-stats-totals-month.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use strict";

const statsTotalsByMonth = function () {
const yearChange = async () => {
const api = await commonUtils.getApi('StatsTotalsByMonthYearPage');
year = commonUtils.getElementValue("year");
window.location.href = api.replace('{year}', year);
}

const initPage = async () => {
const tableTotals = $("#totals_by_month").DataTable({
ordering: false,
info: false,
scrollX: true,
columnDefs: [
{ targets: "_all", width: "5%" },
],
paging: false,
searching: false,
initComplete: function () { statsUtils.loadChart('totals_by_month'); } // Show Totals for chart
});

document.getElementById("year").addEventListener("change", () => { yearChange(); });
document.getElementById("year").focus();
}

document.addEventListener("DOMContentLoaded", initPage);
}();
1 change: 1 addition & 0 deletions app/templates/base.gohtml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
<ul id="stats" class="sidebar-dropdown list-unstyled collapse {{if eq $activePage "stats"}}show{{end}}" data-bs-parent="#sidebar">
<li class="sidebar-item {{if eq $activeSubPage "totals"}}active{{end}}"><a class="sidebar-link" href="{{$api.StatsTotalsPage}}">Totals</a></li>
<li class="sidebar-item {{if eq $activeSubPage "totalsByYear"}}active{{end}}"><a class="sidebar-link" href="{{$api.StatsTotalsByYearPage}}">Totals by Year</a></li>
<li class="sidebar-item {{if eq $activeSubPage "totalsByMonth"}}active{{end}}"><a class="sidebar-link" href="{{$api.StatsTotalsByMonthPage}}">Totals by Month</a></li>
<li class="sidebar-item {{if eq $activeSubPage "totalsByType"}}active{{end}}"><a class="sidebar-link" href="{{$api.StatsTotalsByTypePage}}">Totals by Aircraft Type</a></li>
<li class="sidebar-item {{if eq $activeSubPage "totalsByClass"}}active{{end}}"><a class="sidebar-link" href="{{$api.StatsTotalsByClassPage}}">Totals by Aircraft Class</a></li>
<li class="sidebar-item {{if eq $activeSubPage "limits"}}active{{end}}"><a class="sidebar-link" href="{{$api.StatsLimitsPage}}">Limits</a></li>
Expand Down
102 changes: 102 additions & 0 deletions app/templates/stats-totals-month.page.gohtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
{{ template "base" .}}
{{ define "title" }}Stats - Totals By Month{{ end }}
{{ define "header" }}Stats - Totals By Month{{ end }}
{{ define "css" }}{{ end }}
{{ define "content" }}
{{$settings := index .Data "settings"}}
{{$totalsByMonth := index .Data "totalsByMonth"}}
{{$years := index .Data "years"}}
{{$year := index .Data "year"}}
<div class="row">
<div class="col-12 col-lg-12">
<div class="card">
<div class="card-header p-2">
<h5 class="card-title">Totals by Month</h5>
</div>
<div class="card-body pt-1" id="table_stats">
<div class="row mb-2">
<div class="col-md-4 mb-2">
<div class="input-group">
<div class="form-floating">
<select class="form-control" id="year" name="year" value="{{$year}}">
{{range $key, $value := $years}}
<option value="{{$value}}" {{if eq $value $year}}selected{{end}}>{{$value}}</option>
{{end}}
</select>
<label>Select a year for the detailed stats</label>
</div>
</div>
</div>
</div>
<div class="row mb-2">
<table id="totals_by_month" class="table table-sm table-striped table-bordered nowrap" width="100%" {{if $settings.StatsFontSize}}style="font-size:{{$settings.StatsFontSize}}%"{{end}}>
<thead>
<tr class="align-top text-center">
<th>Month</th>
{{if not $settings.HideStatsFields.SE}}<th onclick="statsUtils.onStatsClick('totals_by_month', this.cellIndex);"><a class="nav-link th-hover" href=#>SE</a></th>{{end}}
{{if not $settings.HideStatsFields.ME}}<th onclick="statsUtils.onStatsClick('totals_by_month', this.cellIndex);"><a class="nav-link th-hover" href=#>ME</a></th>{{end}}
{{if not $settings.HideStatsFields.MCC}}<th onclick="statsUtils.onStatsClick('totals_by_month', this.cellIndex);"><a class="nav-link th-hover" href=#>MCC</a></th>{{end}}
{{if not $settings.HideStatsFields.Night}}<th onclick="statsUtils.onStatsClick('totals_by_month', this.cellIndex);"><a class="nav-link th-hover" href=#>Night</a></th>{{end}}
{{if not $settings.HideStatsFields.IFR}}<th onclick="statsUtils.onStatsClick('totals_by_month', this.cellIndex);"><a class="nav-link th-hover" href=#>IFR</a></th>{{end}}
{{if not $settings.HideStatsFields.PIC}}<th onclick="statsUtils.onStatsClick('totals_by_month', this.cellIndex);"><a class="nav-link th-hover" href=#>PIC</a></th>{{end}}
{{if not $settings.HideStatsFields.CoPilot}}<th onclick="statsUtils.onStatsClick('totals_by_month', this.cellIndex);"><a class="nav-link th-hover" href=#>CoP</a></th>{{end}}
{{if not $settings.HideStatsFields.Dual}}<th onclick="statsUtils.onStatsClick('totals_by_month', this.cellIndex);"><a class="nav-link th-hover" href=#>Dual</a></th>{{end}}
{{if not $settings.HideStatsFields.Instructor}}<th onclick="statsUtils.onStatsClick('totals_by_month', this.cellIndex);"><a class="nav-link th-hover" href=#>Instr</a></th>{{end}}
{{if not $settings.HideStatsFields.Sim}}<th onclick="statsUtils.onStatsClick('totals_by_month', this.cellIndex);"><a class="nav-link th-hover" href=#>Sim</a></th>{{end}}
{{if not $settings.HideStatsFields.CrossCountry}}<th onclick="statsUtils.onStatsClick('totals_by_month', this.cellIndex);"><a class="nav-link th-hover" href=#>CC</a></th>{{end}}
{{if not $settings.HideStatsFields.Landings}}<th>Landings</th>{{end}}
{{if not $settings.HideStatsFields.Distance}}<th onclick="statsUtils.onStatsClick('totals_by_month', this.cellIndex);"><a class="nav-link th-hover" href=#>Distance</a></th>{{end}}
<th onclick="statsUtils.onStatsClick('totals_by_month', this.cellIndex);"><a class="nav-link th-hover" href=#>Total</a></th>
</tr>
</thead>
<tbody>
{{range $key, $value := $totalsByMonth}}
<tr>
<td class="fw-bold">{{$key}}</td>
{{if not $settings.HideStatsFields.SE}}<td>{{$value.Time.SE}}</td>{{end}}
{{if not $settings.HideStatsFields.ME}}<td>{{$value.Time.ME}}</td>{{end}}
{{if not $settings.HideStatsFields.MCC}}<td>{{$value.Time.MCC}}</td>{{end}}
{{if not $settings.HideStatsFields.Night}}<td>{{$value.Time.Night}}</td>{{end}}
{{if not $settings.HideStatsFields.IFR}}<td>{{$value.Time.IFR}}</td>{{end}}
{{if not $settings.HideStatsFields.PIC}}<td>{{$value.Time.PIC}}</td>{{end}}
{{if not $settings.HideStatsFields.CoPilot}}<td>{{$value.Time.CoPilot}}</td>{{end}}
{{if not $settings.HideStatsFields.Dual}}<td>{{$value.Time.Dual}}</td>{{end}}
{{if not $settings.HideStatsFields.Instructor}}<td>{{$value.Time.Instructor}}</td>{{end}}
{{if not $settings.HideStatsFields.Sim}}<td>{{$value.SIM.Time}}</td>{{end}}
{{if not $settings.HideStatsFields.CrossCountry}}<td>{{$value.Time.CrossCountry}}</td>{{end}}
{{if not $settings.HideStatsFields.Landings}}<td>{{$value.Landings.Day}}/{{$value.Landings.Night}}</td>{{end}}
{{if not $settings.HideStatsFields.Distance}}<td>{{formatNumber $value.Distance}}</td>{{end}}
<td><strong>{{$value.Time.Total}}</strong></td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
</div>
</div>

<div class="col-12 col-lg-12">
<div class="card">
<div class="card-header p-2">
<h5 class="card-title">Chart</h5>
<h6 class="card-subtitle text-muted mb-0">Click on the table column header to show the chart</h6>
</div>
<div class="card-body pt-1 text-center" id="chart_stats">
<div class="row">
<div class="col-md-12">
<canvas id="myChart" height="100%"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
{{ end }}

{{ define "js" }}
<script type="text/javascript" src="/static/js/datatables.min.js"></script>
<script type="text/javascript" src="/static/js/chart.min.js"></script>
<script type="text/javascript" src="/static/js/wlb-stats.js"></script>
<script type="text/javascript" src="/static/js/wlb-stats-totals-month.js"></script>
{{ end }}
2 changes: 1 addition & 1 deletion app/templates/stats-totals-year.page.gohtml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<div class="col-12 col-lg-12">
<div class="card">
<div class="card-header p-2">
<h5 class="card-title">Totals</h5>
<h5 class="card-title">Totals by Year</h5>
</div>
<div class="card-body pt-1" id="table_stats">
<div class="row mb-2">
Expand Down
Loading

0 comments on commit 2fa6c51

Please sign in to comment.