1
0
mirror of https://github.com/muety/wakapi.git synced 2023-08-10 21:12:56 +03:00

feat: custom time intervals (resolve #115)

This commit is contained in:
Ferdinand Mütsch 2021-02-13 12:59:59 +01:00
parent daf67b844a
commit 30510591eb
14 changed files with 118 additions and 54 deletions

View File

@ -27,7 +27,7 @@ db:
security:
password_salt: # CHANGE !
insecure_cookies: false
insecure_cookies: false # You need to set this to 'true' when on localhost
cookie_max_age: 172800
allow_signup: true
expose_metrics: false

View File

@ -47,6 +47,7 @@ type SummaryItemContainer struct {
type SummaryViewModel struct {
*Summary
User *User
LanguageColors map[string]string
EditorColors map[string]string
OSColors map[string]string

View File

@ -13,6 +13,7 @@ type User struct {
ShareOSs bool `json:"-" gorm:"default:false; type:bool; column:share_oss"`
ShareMachines bool `json:"-" gorm:"default:false; type:bool"`
IsAdmin bool `json:"-" gorm:"default:false; type:bool"`
HasData bool `json:"-" gorm:"default:false; type:bool"`
WakatimeApiKey string `json:"-"`
}

View File

@ -117,6 +117,7 @@ func (r *UserRepository) Update(user *models.User) (*models.User, error) {
"share_projects": user.ShareProjects,
"share_machines": user.ShareMachines,
"wakatime_api_key": user.WakatimeApiKey,
"has_data": user.HasData,
}
result := r.db.Model(user).Updates(updateMap)

View File

@ -73,7 +73,7 @@ func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) {
if !hb.Valid() {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Invalid heartbeat object."))
w.Write([]byte("invalid heartbeat object"))
return
}
@ -82,10 +82,21 @@ func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) {
if err := h.heartbeatSrvc.InsertBatch(heartbeats); err != nil {
w.WriteHeader(http.StatusInternalServerError)
logbuch.Error(err.Error())
w.Write([]byte(conf.ErrInternalServerError))
logbuch.Error("failed to batch-insert heartbeats %v", err)
return
}
if !user.HasData {
user.HasData = true
if _, err := h.userSrvc.Update(user); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(conf.ErrInternalServerError))
logbuch.Error("failed to update user %v", err)
return
}
}
utils.RespondJSON(w, http.StatusCreated, constructSuccessResponse(len(heartbeats)))
}

View File

@ -218,7 +218,7 @@ func (h *MetricsHandler) getAdminMetrics(user *models.User) (*mm.Metrics, error)
activeUsers, err := h.userSrvc.GetActive()
if err != nil {
logbuch.Error("failed to retrieve active users for metric", err)
logbuch.Error("failed to retrieve active users for metric %v", err)
return nil, err
}

View File

@ -24,15 +24,17 @@ var templates map[string]*template.Template
func loadTemplates() {
const tplPath = "/views"
tpls := template.New("").Funcs(template.FuncMap{
"json": utils.Json,
"date": utils.FormatDateHuman,
"title": strings.Title,
"join": strings.Join,
"add": utils.Add,
"capitalize": utils.Capitalize,
"toRunes": utils.ToRunes,
"entityTypes": models.SummaryTypes,
"typeName": typeName,
"json": utils.Json,
"date": utils.FormatDateHuman,
"simpledate": utils.FormatDate,
"simpledatetime": utils.FormatDateTime,
"title": strings.Title,
"join": strings.Join,
"add": utils.Add,
"capitalize": utils.Capitalize,
"toRunes": utils.ToRunes,
"entityTypes": models.SummaryTypes,
"typeName": typeName,
"getBasePath": func() string {
return config.Get().Server.BasePath
},

View File

@ -62,6 +62,7 @@ func (h *SummaryHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
vm := models.SummaryViewModel{
Summary: summary,
User: user,
LanguageColors: utils.FilterColors(h.config.App.GetLanguageColors(), summary.Languages),
EditorColors: utils.FilterColors(h.config.App.GetEditorColors(), summary.Editors),
OSColors: utils.FilterColors(h.config.App.GetOSColors(), summary.OperatingSystems),

View File

@ -4,4 +4,9 @@ body {
.bg-gray-850 {
background-color: #242b3a;
}
::-webkit-calendar-picker-indicator {
filter: invert(1);
cursor: pointer;
}

View File

@ -49,6 +49,20 @@ String.prototype.toHHMMSS = function () {
return hours + ':' + minutes + ':' + seconds
}
String.prototype.toHHMM = function () {
var sec_num = parseInt(this, 10)
var hours = Math.floor(sec_num / 3600)
var minutes = Math.floor((sec_num - (hours * 3600)) / 60)
if (hours < 10 && hours > 0) {
hours = '0' + hours
}
if (minutes < 10 && minutes > 0) {
minutes = '0' + minutes
}
return hours + ':' + minutes
}
function draw(subselection) {
function getTooltipOptions(key) {
return {
@ -319,8 +333,9 @@ function equalizeHeights() {
function getTotal(items) {
const el = document.getElementById('total-span')
if (!el) return
let total = items.reduce((acc, d) => acc + d.total, 0)
el.innerText = total.toString().toHHMMSS()
const total = items.reduce((acc, d) => acc + d.total, 0)
const formatted = total.toString().toHHMM()
el.innerText = `${formatted.split(':')[0]} hours, ${formatted.split(':')[1]} minutes`
}
function getRandomColor(seed) {

View File

@ -8,10 +8,18 @@ import (
)
func ParseDate(date string) (time.Time, error) {
return time.Parse(config.SimpleDateFormat, date)
}
func ParseDateTime(date string) (time.Time, error) {
return time.Parse(config.SimpleDateTimeFormat, date)
}
func FormatDate(date time.Time) string {
return date.Format(config.SimpleDateFormat)
}
func FormatDateTime(date time.Time) string {
return date.Format(config.SimpleDateTimeFormat)
}

View File

@ -82,14 +82,20 @@ func ParseSummaryParams(r *http.Request) (*models.SummaryParams, error) {
} else if start := params.Get("start"); start != "" {
err, from, to = ResolveIntervalRaw(start)
} else {
from, err = ParseDate(params.Get("from"))
from, err = ParseDateTime(params.Get("from"))
if err != nil {
return nil, errors.New("missing 'from' parameter")
from, err = ParseDate(params.Get("from"))
if err != nil {
return nil, errors.New("missing 'from' parameter")
}
}
to, err = ParseDate(params.Get("to"))
to, err = ParseDateTime(params.Get("to"))
if err != nil {
return nil, errors.New("missing 'to' parameter")
to, err = ParseDate(params.Get("to"))
if err != nil {
return nil, errors.New("missing 'to' parameter")
}
}
}

View File

@ -22,7 +22,7 @@
{{ template "header.tpl.html" . }}
<div class="w-full flex justify-center">
<div class="flex items-center justify-between max-w-xl flex-grow">
<div class="flex items-center justify-between max-w-2xl flex-grow">
<div><a href="" class="text-gray-500 text-sm">&larr; Go back</a></div>
<div><h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Settings</h1></div>
<div>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</div>
@ -32,7 +32,7 @@
{{ template "alerts.tpl.html" . }}
<main class="mt-4 flex-grow flex justify-center w-full">
<div class="flex flex-col flex-grow max-w-xl mt-8">
<div class="flex flex-col flex-grow max-w-2xl mt-8">
<details class="my-8 pb-8 border-b border-gray-700">
<summary class="cursor-pointer">

View File

@ -36,43 +36,56 @@
</div>
<div class="flex items-center justify-center">
<h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Your Coding Statistics 🤓</h1>
<h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Summary</h1>
</div>
<div class="text-white text-sm flex items-center justify-center mt-4 self-center max-w-lg flex-wrap">
<a href="summary?interval=today" class="mx-2 my-1 border-b border-green-700">Today</a>
<a href="summary?interval=yesterday" class="mx-2 my-1 border-b border-green-700">Yesterday</a>
<a href="summary?interval=week" class="mx-2 my-1 border-b border-green-700">This Week</a>
<a href="summary?interval=month" class="mx-2 my-1 border-b border-green-700">This Month</a>
<a href="summary?interval=year" class="mx-2 my-1 border-b border-green-700">This Year</a>
<a href="summary?interval=last_7_days" class="mx-2 my-1 border-b border-green-700">Past 7 Days</a>
<a href="summary?interval=last_30_days" class="mx-2 my-1 border-b border-green-700">Past 30 Days</a>
<a href="summary?interval=last_12_months" class="mx-2 my-1 border-b border-green-700">Past 12 Months</a>
<a href="summary?interval=any" class="mx-2 my-1 border-b border-green-700">All Time</a>
{{ if .User.HasData }}
<div class="self-center border border-gray-700 shadow mt-8 rounded-md p-4 bg-gray-900">
<form class="text-white flex flex-nowrap items-center justify-center self-center max-w-xl flex-wrap space-x-8">
<div class="flex space-x-1">
<label for="from-date-picker" class="text-gray-300 pl-1">▶️ Start:</label>
<input id="from-date-picker" type="date" name="from" class="text-sm text-gray-300 bg-gray-800 rounded-md text-center cursor-pointer"
value="{{ .FromTime.T | simpledate }}" required>
</div>
<div class="flex space-x-1">
<label for="to-date-picker" class="text-gray-300 pl-1">⏹️ End:</label>
<input id="to-date-picker" type="date" name="to" class="text-sm text-gray-300 bg-gray-800 rounded-md text-center cursor-pointer"
value="{{ .ToTime.T | simpledate }}" required>
</div>
<div>
<button type="submit" class="py-1 px-3 rounded bg-green-700 hover:bg-green-800 text-white text-sm">Show</button>
</div>
</form>
<div class="text-gray-300 text-sm flex items-center justify-center mt-4 self-center max-w-lg flex-wrap">
<a href="summary?interval=today" class="px-1 my-1 mx-1 border-b hover:border-b-2 border-gray-700 hover:bg-green-700 rounded hover:border-none">Today</a>
<a href="summary?interval=yesterday" class="px-1 my-1 mx-1 border-b hover:border-b-2 border-gray-700 hover:bg-green-700 rounded hover:border-none">Yesterday</a>
<a href="summary?interval=week" class="px-1 my-1 mx-1 border-b hover:border-b-2 border-gray-700 hover:bg-green-700 rounded hover:border-none">This Week</a>
<a href="summary?interval=month" class="px-1 my-1 mx-1 border-b hover:border-b-2 border-gray-700 hover:bg-green-700 rounded hover:border-none">This Month</a>
<a href="summary?interval=year" class="px-1 my-1 mx-1 border-b hover:border-b-2 border-gray-700 hover:bg-green-700 rounded hover:border-none">This Year</a>
<a href="summary?interval=last_7_days" class="px-1 my-1 mx-1 border-b hover:border-b-2 border-gray-700 hover:bg-green-700 rounded hover:border-none">Past 7 Days</a>
<a href="summary?interval=last_30_days" class="px-1 my-1 mx-1 border-b hover:border-b-2 border-gray-700 hover:bg-green-700 rounded hover:border-none">Past 30 Days</a>
<a href="summary?interval=last_12_months" class="px-1 my-1 mx-1 border-b hover:border-b-2 border-gray-700 hover:bg-green-700 rounded hover:border-none">Past 12 Months</a>
<a href="summary?interval=any" class="px-1 my-1 mx-1 border-b hover:border-b-2 border-gray-700 hover:bg-green-700 rounded hover:border-none">All Time</a>
</div>
</div>
{{ end }}
{{ template "alerts.tpl.html" . }}
<main class="flex flex-col items-center mt-10 flex-grow">
<div class="flex justify-center">
<div class="p-1">
<div class="flex justify-center p-4 bg-gray-900 border border-gray-700 text-gray-300 rounded-md shadow">
<p class="mx-2"><strong>▶️</strong> <span title="Start Time">{{ .FromTime.T | date }}</span></p>
<p class="mx-2"><strong>⏹️</strong> <span title="End Time">{{ .ToTime.T | date }}</span></p>
<p class="mx-2">
<strong>⏱️</strong>
{{ if gt .Summary.TotalTime 0 }}
<span id="total-span" title="Total Hours"></span>
{{ else }}
<span title="Total Hours">No Data</span>
{{ end }}
</p>
</div>
</div>
</div>
{{ if .User.HasData }}
{{ if or (gt .Summary.TotalTime 0) (ne .RawQuery "") }}
<span class="text-white text-lg text-gray-300 text-center mb-4">
<span class="text-xl">⏱️&nbsp;</span>
Showing a total of <span id="total-span" title="Total Hours" class="text-white text-xl font-semibold border-b-2 border-green-700"></span>
<span class="text-sm my-2">
(from <span title="Start Time" class="border-b border-gray-700">{{ .FromTime.T | date }}</span> to <span title="End Time" class="border-b border-gray-700">{{ .ToTime.T | date }}</span>)
</span>
</span>
<div class="flex flex-wrap justify-center">
<div class="w-full lg:w-1/2 p-1">
@ -87,7 +100,7 @@
</div>
<canvas id="chart-projects" class="mt-2"></canvas>
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
<span class="text-md font-semibold text-gray-500 mt-4">No data ...</span>
<span class="text-md font-semibold text-gray-500 mt-4">No data</span>
</div>
</div>
</div>
@ -103,7 +116,7 @@
</div>
<canvas id="chart-os"></canvas>
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
<span class="text-md font-semibold text-gray-500 mt-4">No data ...</span>
<span class="text-md font-semibold text-gray-500 mt-4">No data</span>
</div>
</div>
</div>
@ -119,7 +132,7 @@
</div>
<canvas id="chart-language"></canvas>
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
<span class="text-md font-semibold text-gray-500 mt-4">No data ...</span>
<span class="text-md font-semibold text-gray-500 mt-4">No data</span>
</div>
</div>
</div>
@ -135,7 +148,7 @@
</div>
<canvas id="chart-editor"></canvas>
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
<span class="text-md font-semibold text-gray-500 mt-4">No data ...</span>
<span class="text-md font-semibold text-gray-500 mt-4">No data</span>
</div>
</div>
</div>
@ -151,7 +164,7 @@
</div>
<canvas id="chart-machine"></canvas>
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
<span class="text-md font-semibold text-gray-500 mt-4">No data ...</span>
<span class="text-md font-semibold text-gray-500 mt-4">No data</span>
</div>
</div>
</div>