diff --git a/pkg/dashboard.go b/pkg/dashboard.go index 825b9a1..8b9173b 100644 --- a/pkg/dashboard.go +++ b/pkg/dashboard.go @@ -1,9 +1,13 @@ package pkg import ( + "context" "fmt" "github.com/julienschmidt/httprouter" + "html/template" + "math" "net/http" + "strings" ) const ( @@ -22,6 +26,7 @@ type DashboardPage struct { VAT string IRPF string NetIncome string + Chart template.HTML Filters *dashboardFilterForm } @@ -53,6 +58,10 @@ func ServeDashboard(w http.ResponseWriter, r *http.Request, _ httprouter.Params) filters.Period.Selected = MonthPeriod } conn := getConn(r) + dashboard := &DashboardPage{ + Filters: filters, + Chart: buildDashboardChart(r.Context(), conn, company, periodStart, periodEnd, filters.Period.Selected), + } rows := conn.MustQuery(r.Context(), fmt.Sprintf(` select to_price(0, decimal_digits) as sales , to_price(coalesce(invoice.total, 0), decimal_digits) as income @@ -101,9 +110,6 @@ func ServeDashboard(w http.ResponseWriter, r *http.Request, _ httprouter.Params) `, periodStart, periodEnd), company.Id) defer rows.Close() - 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) @@ -166,3 +172,101 @@ func (form *dashboardFilterForm) Parse(r *http.Request) error { form.Period.FillValue(r) return nil } + +func buildDashboardChart(ctx context.Context, conn *Conn, company *Company, periodStart string, periodEnd string, selectedPeriod string) template.HTML { + group := "yyyymmdd" + switch selectedPeriod { + case YearPeriod, YesteryearPeriod: + group = "yyyymm" + } + rows := conn.MustQuery(ctx, fmt.Sprintf(` + select date + , 0 as sales + , coalesce(invoice.total, 0) as income + , coalesce(expense.total, 0) as expenses + from ( + select to_char(date.invoice_date, '%[3]s')::integer as date + , sum(total)::integer as total + from generate_series(%[1]s, %[2]s, interval '1 day') as date(invoice_date) + left join invoice on invoice.invoice_date = date.invoice_date and company_id = $1 + left join invoice_amount using (invoice_id) + group by date + ) as invoice + left join ( + select to_char(date.invoice_date, '%[3]s')::integer as date + , sum(amount)::integer as total + from generate_series(%[1]s, %[2]s, interval '1 day') as date(invoice_date) + left join expense on expense.invoice_date = date.invoice_date and company_id = 1 + group by date + ) as expense using (date) + order by date + `, periodStart, periodEnd, group), company.Id) + defer rows.Close() + + type value struct { + date int + sales float64 + income float64 + expenses float64 + } + var values []value + var max = 0. + for rows.Next() { + var v value + var sales int + var income int + var expenses int + if err := rows.Scan(&v.date, &sales, &income, &expenses); err != nil { + panic(err) + } + v.sales = float64(sales) + v.income = float64(income) + v.expenses = float64(expenses) + values = append(values, v) + max = math.Max(v.sales, max) + max = math.Max(v.income, max) + max = math.Max(v.expenses, max) + } + if rows.Err() != nil { + panic(rows.Err()) + } + + width := 1024. + height := 300. + dataPoints := float64(len(values)) + var sb strings.Builder + sb.WriteString(fmt.Sprintf("", int(width)+20, int(height)+20)) + sb.WriteString("") + sb.WriteString("") + sb.WriteString("") + for i, v := range values { + sb.WriteString(fmt.Sprintf("", float64(i)/dataPoints*width, height-v.sales/max*height)) + sb.WriteString(fmt.Sprintf("", float64(i)/dataPoints*width, height-v.income/max*height)) + sb.WriteString(fmt.Sprintf("", float64(i)/dataPoints*width, height-v.expenses/max*height)) + } + sb.WriteString("") + return template.HTML(sb.String()) + return template.HTML(` + + + + + + + + + +`) +} diff --git a/web/static/numerus.css b/web/static/numerus.css index 93a9453..6ac828d 100644 --- a/web/static/numerus.css +++ b/web/static/numerus.css @@ -900,6 +900,29 @@ div[x-data="snackbar"] div[role="alert"].enter.end, div[x-data="snackbar"] div[r font-size: .66666em; } +#income-chart polyline { + fill: none; + stroke-width: 5; + vector-effect: non-scaling-stroke; +} + +#income-chart polyline:nth-of-type(1) { + stroke: var(--numerus--color--yellow); +} + +#income-chart polyline:nth-of-type(2) { + stroke: var(--numerus--color--green); +} + +#income-chart polyline:nth-of-type(3) { + stroke: var(--numerus--color--red); +} + +#income-chart circle { + fill: var(--numerus--color--dark-gray); + vector-effect: non-scaling-stroke; +} + /* Remix Icon */ @font-face { diff --git a/web/template/dashboard.gohtml b/web/template/dashboard.gohtml index 7247db3..103f24c 100644 --- a/web/template/dashboard.gohtml +++ b/web/template/dashboard.gohtml @@ -48,4 +48,5 @@
{{ formatPriceSpan .NetIncome }}
+ {{ .Chart }} {{- end }}