Add a period filter for the dashboard

I do not yet know whether Oriol wants a YTD or MAT period, and i went
for the easiest for me: everything is MAT.
This commit is contained in:
jordi fita mas 2023-05-17 12:05:30 +02:00
parent f68aba1387
commit 987a99e0df
7 changed files with 272 additions and 35 deletions

View File

@ -5,6 +5,13 @@ import (
"net/http"
)
const (
YearPeriod = "year"
QuarterPeriod = "quarter"
MonthPeriod = "month"
YesteryearPeriod = "yesteryear"
)
type DashboardPage struct {
Sales string
Income string
@ -12,10 +19,30 @@ type DashboardPage struct {
VAT string
IRPF string
NetIncome string
Filters *dashboardFilterForm
}
func ServeDashboard(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
company := mustGetCompany(r)
locale := getLocale(r)
filters := newDashboardFilterForm(locale, company)
if err := filters.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
periodStart := "30 DAYS"
periodEnd := "0 DAYS"
switch filters.Period.Selected {
case YearPeriod:
periodStart = "1 YEAR"
case QuarterPeriod:
periodStart = "3 MONTHS"
case YesteryearPeriod:
periodStart = "2 YEARS"
periodEnd = "1 YEAR"
case "":
filters.Period.Selected = MonthPeriod
}
conn := getConn(r)
rows := conn.MustQuery(r.Context(), `
select to_price(0, decimal_digits) as sales
@ -29,11 +56,13 @@ func ServeDashboard(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
select company_id, sum(total)::integer as total
from invoice
join invoice_amount using (invoice_id)
where invoice_date between CURRENT_DATE - $2::interval and CURRENT_DATE - $3::interval
group by company_id
) as invoice using (company_id)
left join (
select company_id, sum(amount)::integer as total
from expense
where invoice_date between CURRENT_DATE - $2::interval and CURRENT_DATE - $3::interval
group by company_id
) as expense using (company_id)
left join (
@ -44,14 +73,17 @@ func ServeDashboard(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
join invoice_tax_amount using (invoice_id)
join tax using (tax_id)
join tax_class using (tax_class_id)
where invoice_date between CURRENT_DATE - $2::interval and CURRENT_DATE - $3::interval
group by invoice.company_id
) as tax using (company_id)
join currency using (currency_code)
where company_id = $1
`, company.Id)
`, company.Id, periodStart, periodEnd)
defer rows.Close()
dashboard := &DashboardPage{}
dashboard := &DashboardPage{
Filters: filters,
}
for rows.Next() {
if err := rows.Scan(&dashboard.Sales, &dashboard.Income, &dashboard.Expenses, &dashboard.VAT, &dashboard.IRPF, &dashboard.NetIncome); err != nil {
panic(err)
@ -62,3 +94,47 @@ func ServeDashboard(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
}
mustRenderMainTemplate(w, r, "dashboard.gohtml", dashboard)
}
type dashboardFilterForm struct {
locale *Locale
company *Company
Period *RadioField
}
func newDashboardFilterForm(locale *Locale, company *Company) *dashboardFilterForm {
return &dashboardFilterForm{
locale: locale,
company: company,
Period: &RadioField{
Name: "period",
Selected: MonthPeriod,
Label: pgettext("input", "Period", locale),
Options: []*RadioOption{
{
Label: pgettext("period option", "Year", locale),
Value: YearPeriod,
},
{
Label: pgettext("period option", "Quarter", locale),
Value: QuarterPeriod,
},
{
Label: pgettext("period option", "Month", locale),
Value: MonthPeriod,
},
{
Label: pgettext("period option", "Previous Year", locale),
Value: YesteryearPeriod,
},
},
},
}
}
func (form *dashboardFilterForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
form.Period.FillValue(r)
return nil
}

View File

@ -217,6 +217,59 @@ func mustGetCountryOptions(ctx context.Context, conn *Conn, locale *Locale) []*S
return MustGetOptions(ctx, conn, "select country.country_code, coalesce(i18n.name, country.name) as l10n_name from country left join country_i18n as i18n on country.country_code = i18n.country_code and i18n.lang_tag = $1 order by l10n_name", locale.Language)
}
type RadioOption struct {
Value string
Label string
}
type RadioField struct {
Name string
Label string
Selected string
Options []*RadioOption
Attributes []template.HTMLAttr
Required bool
Errors []error
}
func (field *RadioField) Scan(value interface{}) error {
if value == nil {
field.Selected = ""
return nil
}
field.Selected = fmt.Sprintf("%v", value)
return nil
}
func (field *RadioField) Value() (driver.Value, error) {
return field.Selected, nil
}
func (field *RadioField) String() string {
return field.Selected
}
func (field *RadioField) FillValue(r *http.Request) {
field.Selected = strings.TrimSpace(r.FormValue(field.Name))
}
func (field *RadioField) IsSelected(v string) bool {
return field.Selected == v
}
func (field *RadioField) FindOption(value string) *RadioOption {
for _, option := range field.Options {
if option.Value == value {
return option
}
}
return nil
}
func (field *RadioField) isValidOption(selected string) bool {
return field.FindOption(selected) != nil
}
type FileField struct {
Name string
Label string

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-05-16 14:50+0200\n"
"POT-Creation-Date: 2023-05-17 11:59+0200\n"
"PO-Revision-Date: 2023-01-18 17:08+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\n"
@ -104,8 +104,9 @@ msgctxt "action"
msgid "Download invoices"
msgstr "Descarrega factures"
#: web/template/invoices/index.gohtml:40 web/template/contacts/index.gohtml:32
#: web/template/expenses/index.gohtml:34 web/template/products/index.gohtml:32
#: web/template/invoices/index.gohtml:40 web/template/dashboard.gohtml:22
#: web/template/contacts/index.gohtml:32 web/template/expenses/index.gohtml:34
#: web/template/products/index.gohtml:32
msgctxt "action"
msgid "Filter"
msgstr "Filtra"
@ -219,7 +220,7 @@ msgid "Edit invoice"
msgstr "Edita factura"
#: web/template/form.gohtml:36
msgctxt "label"
msgctxt "input"
msgid "(Max. %s)"
msgstr "(Màx. %s)"
@ -228,32 +229,32 @@ msgctxt "title"
msgid "Dashboard"
msgstr "Tauler"
#: web/template/dashboard.gohtml:14
#: web/template/dashboard.gohtml:27
msgctxt "term"
msgid "Sales"
msgstr "Vendes"
#: web/template/dashboard.gohtml:18
#: web/template/dashboard.gohtml:31
msgctxt "term"
msgid "Income"
msgstr "Ingressos"
#: web/template/dashboard.gohtml:22
#: web/template/dashboard.gohtml:35
msgctxt "term"
msgid "Expenses"
msgstr "Despeses"
#: web/template/dashboard.gohtml:26
#: web/template/dashboard.gohtml:39
msgctxt "term"
msgid "VAT"
msgstr "IVA"
#: web/template/dashboard.gohtml:30
#: web/template/dashboard.gohtml:43
msgctxt "term"
msgid "IRPF"
msgstr "IRPF"
#: web/template/dashboard.gohtml:34
#: web/template/dashboard.gohtml:47
msgctxt "term"
msgid "Net Income"
msgstr "Ingressos nets"
@ -738,6 +739,31 @@ msgstr "La confirmació no és igual a la contrasenya."
msgid "Selected language is not valid."
msgstr "Heu seleccionat un idioma que no és vàlid."
#: pkg/dashboard.go:112
msgctxt "input"
msgid "Period"
msgstr "Període"
#: pkg/dashboard.go:115
msgctxt "period option"
msgid "Year"
msgstr "Any"
#: pkg/dashboard.go:119
msgctxt "period option"
msgid "Quarter"
msgstr "Trimestre"
#: pkg/dashboard.go:123
msgctxt "period option"
msgid "Month"
msgstr "Mes"
#: pkg/dashboard.go:127
msgctxt "period option"
msgid "Previous Year"
msgstr "Any anterior"
#: pkg/expenses.go:129
msgid "Select a contact."
msgstr "Escolliu un contacte."

View File

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-05-16 14:50+0200\n"
"POT-Creation-Date: 2023-05-17 11:59+0200\n"
"PO-Revision-Date: 2023-01-18 17:45+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\n"
@ -104,8 +104,9 @@ msgctxt "action"
msgid "Download invoices"
msgstr "Descargar facturas"
#: web/template/invoices/index.gohtml:40 web/template/contacts/index.gohtml:32
#: web/template/expenses/index.gohtml:34 web/template/products/index.gohtml:32
#: web/template/invoices/index.gohtml:40 web/template/dashboard.gohtml:22
#: web/template/contacts/index.gohtml:32 web/template/expenses/index.gohtml:34
#: web/template/products/index.gohtml:32
msgctxt "action"
msgid "Filter"
msgstr "Filtrar"
@ -219,7 +220,7 @@ msgid "Edit invoice"
msgstr "Editar factura"
#: web/template/form.gohtml:36
msgctxt "label"
msgctxt "input"
msgid "(Max. %s)"
msgstr "(Máx. %s)"
@ -228,32 +229,32 @@ msgctxt "title"
msgid "Dashboard"
msgstr "Panel"
#: web/template/dashboard.gohtml:14
#: web/template/dashboard.gohtml:27
msgctxt "term"
msgid "Sales"
msgstr "Ventas"
#: web/template/dashboard.gohtml:18
#: web/template/dashboard.gohtml:31
msgctxt "term"
msgid "Income"
msgstr "Ingresos"
#: web/template/dashboard.gohtml:22
#: web/template/dashboard.gohtml:35
msgctxt "term"
msgid "Expenses"
msgstr "Gastos"
#: web/template/dashboard.gohtml:26
#: web/template/dashboard.gohtml:39
msgctxt "term"
msgid "VAT"
msgstr "IVA"
#: web/template/dashboard.gohtml:30
#: web/template/dashboard.gohtml:43
msgctxt "term"
msgid "IRPF"
msgstr "IRPF"
#: web/template/dashboard.gohtml:34
#: web/template/dashboard.gohtml:47
msgctxt "term"
msgid "Net Income"
msgstr "Ingresos netos"
@ -738,6 +739,31 @@ msgstr "La confirmación no corresponde con la contraseña."
msgid "Selected language is not valid."
msgstr "Habéis escogido un idioma que no es válido."
#: pkg/dashboard.go:112
msgctxt "input"
msgid "Period"
msgstr "Periodo"
#: pkg/dashboard.go:115
msgctxt "period option"
msgid "Year"
msgstr "Año"
#: pkg/dashboard.go:119
msgctxt "period option"
msgid "Quarter"
msgstr "Trimestre"
#: pkg/dashboard.go:123
msgctxt "period option"
msgid "Month"
msgstr "Mes"
#: pkg/dashboard.go:127
msgctxt "period option"
msgid "Previous Year"
msgstr "Año anterior"
#: pkg/expenses.go:129
msgid "Select a contact."
msgstr "Escoged un contacto"

View File

@ -309,9 +309,12 @@ main {
}
.input {
margin-top: 2rem;
}
.input:not(.radio) {
position: relative;
display: inline-block;
margin-top: 2rem;
}
input[type="text"], input[type="search"], input[type="password"], input[type="email"], input[type="tel"], input[type="url"], input[type="number"], input[type="date"], select, textarea {
@ -334,7 +337,7 @@ input.width-2x {
color: transparent;
}
.input label, .input input:focus ~ label {
.input:not(.radio) label, .input:not(.radio) input:focus ~ label {
position: absolute;
font-style: italic;
pointer-events: none;
@ -365,7 +368,7 @@ input.width-2x {
content: " (opcional)"
}
.input label, .input input:focus ~ label {
.input:not(.radio) label, .input:not(.radio) input:focus ~ label {
background-color: var(--numerus--background-color);
top: -.9rem;
left: 2rem;
@ -384,17 +387,23 @@ input.width-2x {
fieldset {
border: none;
padding: 2rem 0 0;
margin-top: 3rem;
border-top: 1px solid var(--numerus--color--light-gray);
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
fieldset:not(.radio) {
padding: 2rem 0 0;
margin-top: 3rem;
border-top: 1px solid var(--numerus--color--light-gray);
}
legend {
float: left;
font-style: italic;
}
fieldset:not(.radio) {
float: left;
margin-bottom: 3rem;
width: 100%;
}
@ -615,7 +624,7 @@ main > nav {
}
[is="numerus-multiselect"] .tags, [is="numerus-tags"] .tags,
[is="numerus-multiselect"] .options, [is="numerus-product-search"] .options {
[is="numerus-multiselect"] .options, [is="numerus-product-search"] .options {
font-size: 1em;
list-style: none;
color: var(--numerus--text-color);
@ -701,8 +710,8 @@ main > nav {
padding: 1rem 2rem;
}
[is="numerus-product-search"] .options li:hover,[is="numerus-product-search"] .options .highlight,
[is="numerus-multiselect"] .options li:hover,[is="numerus-multiselect"] .options .highlight {
[is="numerus-product-search"] .options li:hover, [is="numerus-product-search"] .options .highlight,
[is="numerus-multiselect"] .options li:hover, [is="numerus-multiselect"] .options .highlight {
background-color: var(--numerus--color--light-gray);
}
@ -720,7 +729,7 @@ main > nav {
}
[is="numerus-tags"] [role="menu"] {
min-width: 27em;
min-width: 27em;
}
[is="numerus-tags"] [role="menu"] li {
@ -825,6 +834,23 @@ div[x-data="snackbar"] div[role="alert"].enter.end, div[x-data="snackbar"] div[r
}
/* Dashboard */
#dashboard-filters {
display: flex;
}
#dashboard-filters .radio {
margin-top: 0;
flex: 1;
}
#dashboard-filters .radio label {
text-transform: lowercase;
}
#dashboard-filters legend {
display: none;
}
#income-statement {
display: flex;
flex-wrap: wrap;
@ -834,7 +860,7 @@ div[x-data="snackbar"] div[role="alert"].enter.end, div[x-data="snackbar"] div[r
#income-statement div {
display: block;
padding: 2rem;
width: calc(100%/3);
width: calc(100% / 3);
min-width: 33rem;
}

View File

@ -9,6 +9,19 @@
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.DashboardPage*/ -}}
<div aria-label="{{( pgettext "Filters" "title" )}}">
<form method="GET" action="{{ companyURI "/"}}"
id="dashboard-filters"
data-hx-target="main"
data-hx-boost="true"
data-hx-trigger="change,submit"
>
{{ with .Filters }}
{{ template "radio-field" .Period }}
{{ end }}
<button type="submit">{{( pgettext "Filter" "action" )}}</button>
</form>
</div>
<dl id="income-statement">
<div>
<dt>{{ (pgettext "Sales" "term") }}</dt>

View File

@ -33,7 +33,7 @@
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.FileField*/ -}}
<div class="input{{ if .Errors }} has-errors{{ end }}">
<input type="file" name="{{ .Name }}" id="{{ .Name }}-field" placeholder="{{ .Label }}">
<label for="{{ .Name }}-field">{{ .Label }}{{ if gt .MaxSize 0 }} {{printf (pgettext "(Max. %s)" "label") (.MaxSize|humanizeBytes) }}{{ end }}</label>
<label for="{{ .Name }}-field">{{ .Label }}{{ if gt .MaxSize 0 }} {{printf (pgettext "(Max. %s)" "input") (.MaxSize|humanizeBytes) }}{{ end }}</label>
{{- if .Errors }}
<ul>
{{- range $error := .Errors }}
@ -125,6 +125,23 @@
</fieldset>
{{- end }}
{{ define "radio-field" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.RadioField*/ -}}
<fieldset id="{{ .Name}}-field" class="input radio{{ if .Errors }} has-errors{{ end }}">
<legend>{{ .Label }}</legend>
{{- range $option := .Options }}
<label><input type="radio" name="{{$.Name}}" value="{{.Value}}" {{- if $.IsSelected .Value }} checked="checked"{{ end }}> {{.Label}}</label>
{{- end }}
{{- if .Errors }}
<ul>
{{- range $error := .Errors }}
<li>{{ . }}</li>
{{- end }}
</ul>
{{- end }}
</fieldset>
{{- end }}
{{ define "invoice-product-form" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.invoiceProductForm*/ -}}
<fieldset class="new-invoice-product"