diff --git a/pkg/invoices.go b/pkg/invoices.go index 27c7744..f744458 100644 --- a/pkg/invoices.go +++ b/pkg/invoices.go @@ -29,12 +29,16 @@ type InvoiceEntry struct { } type InvoicesIndexPage struct { - Invoices []*InvoiceEntry + Invoices []*InvoiceEntry + InvoiceStatuses map[string]string } func IndexInvoices(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + conn := getConn(r) + locale := getLocale(r) page := &InvoicesIndexPage{ - Invoices: mustCollectInvoiceEntries(r.Context(), getConn(r), mustGetCompany(r), getLocale(r)), + Invoices: mustCollectInvoiceEntries(r.Context(), conn, mustGetCompany(r), locale), + InvoiceStatuses: mustCollectInvoiceStatuses(r.Context(), conn, locale), } mustRenderAppTemplate(w, r, "invoices/index.gohtml", page) } @@ -58,6 +62,25 @@ func mustCollectInvoiceEntries(ctx context.Context, conn *Conn, company *Company return entries } +func mustCollectInvoiceStatuses(ctx context.Context, conn *Conn, locale *Locale) map[string]string { + rows := conn.MustQuery(ctx, "select invoice_status.invoice_status, isi18n.name from invoice_status join invoice_status_i18n isi18n using(invoice_status) where isi18n.lang_tag = $1 order by invoice_status", locale.Language.String()) + defer rows.Close() + + statuses := map[string]string{} + for rows.Next() { + var key, name string + if err := rows.Scan(&key, &name); err != nil { + panic(err) + } + statuses[key] = name + } + if rows.Err() != nil { + panic(rows.Err()) + } + + return statuses +} + func ServeInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Params) { locale := getLocale(r) conn := getConn(r) @@ -555,3 +578,21 @@ func (form *invoiceProductForm) Validate() bool { validator.CheckAtMostOneOfEachGroup(form.Tax, gettext("You can only select a tax of each class.", form.locale)) return validator.AllOK() } + +func HandleUpdateInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + conn := getConn(r) + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if err := verifyCsrfTokenValid(r); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + invoiceStatus := r.FormValue("status") + slug := conn.MustGetText(r.Context(), "", "update invoice set invoice_status = $1 where slug = $2 returning slug", invoiceStatus, params[0].Value) + if slug == "" { + http.NotFound(w, r) + } + http.Redirect(w, r, companyURI(mustGetCompany(r), "/invoices"), http.StatusSeeOther) +} diff --git a/pkg/router.go b/pkg/router.go index 069c61f..522434a 100644 --- a/pkg/router.go +++ b/pkg/router.go @@ -27,6 +27,7 @@ func NewRouter(db *Db) http.Handler { companyRouter.GET("/invoices", IndexInvoices) companyRouter.POST("/invoices", HandleAddInvoice) companyRouter.GET("/invoices/:slug", ServeInvoice) + companyRouter.PUT("/invoices/:slug", HandleUpdateInvoice) companyRouter.POST("/invoices/new", HandleNewInvoiceAction) companyRouter.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { mustRenderAppTemplate(w, r, "dashboard.gohtml", nil) diff --git a/web/static/numerus.css b/web/static/numerus.css index 233e392..c218fda 100644 --- a/web/static/numerus.css +++ b/web/static/numerus.css @@ -146,8 +146,11 @@ --numerus--color--white: #ffffff; --numerus--color--yellow: #ffd200; --numerus--color--red: #ff7a53; + --numerus--color--rosy: #ffbaa6; --numerus--color--green: #5ae487; + --numerus--color--light-green: #9fefb9; --numerus--color--blue: #55bfff; + --numerus--color--light-blue: #cbebff; --numerus--color--hay: #ffe673; --numerus--text-color: var(--numerus--color--black); @@ -232,6 +235,10 @@ table { border-collapse: collapse; } +.no-padding td { + padding: 0; +} + tbody tr:nth-child(even) { background-color: var(--numerus--header--background-color); } @@ -439,7 +446,7 @@ fieldset { background-color: var(--numerus--background-color); } -#profile-menu[open] summary::before { +details.menu[open] summary::before { background-color: var(--numerus--header--background-color); position: fixed; top: 0; @@ -452,8 +459,12 @@ fieldset { mix-blend-mode: multiply; } -#profile-menu ul { +ul[role="menu"] { + padding: 0; list-style: none; +} + +#profile-menu ul { position: absolute; right: -1.875em; top: 100%; @@ -517,6 +528,53 @@ main > nav { text-decoration: none; } +.invoice-status { + position: relative; +} + +.invoice-status summary { + height: 3rem; + display: block; +} + +.invoice-status summary::-webkit-details-marker { + display: none; +} + +.invoice-status ul { + position: absolute; + top: 1.5em; + left: 0; + z-index: 20; +} + +.invoice-status button { + border: 0; + min-width: 15rem; +} + +[class^='invoice-status-'] { + text-align: center; + text-transform: uppercase; + cursor: pointer; +} + +.invoice-status-created { + background-color: var(--numerus--color--light-blue); +} + +.invoice-status-sent { + background-color: var(--numerus--color--hay); +} + +.invoice-status-paid { + background-color: var(--numerus--color--light-green); +} + +.invoice-status-unpaid { + background-color: var(--numerus--color--rosy); +} + /* Remix Icon */ @font-face { diff --git a/web/template/app.gohtml b/web/template/app.gohtml index c558220..7ecc425 100644 --- a/web/template/app.gohtml +++ b/web/template/app.gohtml @@ -9,7 +9,7 @@

Numerus

-
+