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:
parent
f68aba1387
commit
987a99e0df
|
@ -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
|
||||
}
|
||||
|
|
53
pkg/form.go
53
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
|
||||
|
|
46
po/ca.po
46
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 <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."
|
||||
|
|
46
po/es.po
46
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 <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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue