From 987a99e0df8393a519d4c6a54632c812047d84db Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Wed, 17 May 2023 12:05:30 +0200 Subject: [PATCH] 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. --- pkg/dashboard.go | 80 ++++++++++++++++++++++++++++++++++- pkg/form.go | 53 +++++++++++++++++++++++ po/ca.po | 46 +++++++++++++++----- po/es.po | 46 +++++++++++++++----- web/static/numerus.css | 50 ++++++++++++++++------ web/template/dashboard.gohtml | 13 ++++++ web/template/form.gohtml | 19 ++++++++- 7 files changed, 272 insertions(+), 35 deletions(-) diff --git a/pkg/dashboard.go b/pkg/dashboard.go index f813192..982f888 100644 --- a/pkg/dashboard.go +++ b/pkg/dashboard.go @@ -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 +} diff --git a/pkg/form.go b/pkg/form.go index 191bfb6..acbdb14 100644 --- a/pkg/form.go +++ b/pkg/form.go @@ -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 diff --git a/po/ca.po b/po/ca.po index 72c0687..dff2842 100644 --- a/po/ca.po +++ b/po/ca.po @@ -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 \n" "Language-Team: Catalan \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." diff --git a/po/es.po b/po/es.po index 66b7543..591674b 100644 --- a/po/es.po +++ b/po/es.po @@ -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 \n" "Language-Team: Spanish \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" diff --git a/web/static/numerus.css b/web/static/numerus.css index 91a8026..310ca22 100644 --- a/web/static/numerus.css +++ b/web/static/numerus.css @@ -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; } diff --git a/web/template/dashboard.gohtml b/web/template/dashboard.gohtml index 270a28c..7247db3 100644 --- a/web/template/dashboard.gohtml +++ b/web/template/dashboard.gohtml @@ -9,6 +9,19 @@ {{ define "content" }} {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.DashboardPage*/ -}} +
+
+ {{ with .Filters }} + {{ template "radio-field" .Period }} + {{ end }} + +
+
{{ (pgettext "Sales" "term") }}
diff --git a/web/template/form.gohtml b/web/template/form.gohtml index 0ef7393..778f14a 100644 --- a/web/template/form.gohtml +++ b/web/template/form.gohtml @@ -33,7 +33,7 @@ {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.FileField*/ -}}
- + {{- if .Errors }}
    {{- range $error := .Errors }} @@ -125,6 +125,23 @@ {{- end }} +{{ define "radio-field" -}} + {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.RadioField*/ -}} +
    + {{ .Label }} + {{- range $option := .Options }} + + {{- end }} + {{- if .Errors }} +
      + {{- range $error := .Errors }} +
    • {{ . }}
    • + {{- end }} +
    + {{- end }} +
    +{{- end }} + {{ define "invoice-product-form" -}} {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.invoiceProductForm*/ -}}