Add the “menu” to change invoice statuses

This commit is contained in:
jordi fita mas 2023-03-07 11:52:09 +01:00
parent d8997de654
commit 039bf3abbd
5 changed files with 129 additions and 10 deletions

View File

@ -30,11 +30,15 @@ type InvoiceEntry struct {
type InvoicesIndexPage struct { type InvoicesIndexPage struct {
Invoices []*InvoiceEntry Invoices []*InvoiceEntry
InvoiceStatuses map[string]string
} }
func IndexInvoices(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { func IndexInvoices(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
conn := getConn(r)
locale := getLocale(r)
page := &InvoicesIndexPage{ 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) mustRenderAppTemplate(w, r, "invoices/index.gohtml", page)
} }
@ -58,6 +62,25 @@ func mustCollectInvoiceEntries(ctx context.Context, conn *Conn, company *Company
return entries 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) { func ServeInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
locale := getLocale(r) locale := getLocale(r)
conn := getConn(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)) validator.CheckAtMostOneOfEachGroup(form.Tax, gettext("You can only select a tax of each class.", form.locale))
return validator.AllOK() 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)
}

View File

@ -27,6 +27,7 @@ func NewRouter(db *Db) http.Handler {
companyRouter.GET("/invoices", IndexInvoices) companyRouter.GET("/invoices", IndexInvoices)
companyRouter.POST("/invoices", HandleAddInvoice) companyRouter.POST("/invoices", HandleAddInvoice)
companyRouter.GET("/invoices/:slug", ServeInvoice) companyRouter.GET("/invoices/:slug", ServeInvoice)
companyRouter.PUT("/invoices/:slug", HandleUpdateInvoice)
companyRouter.POST("/invoices/new", HandleNewInvoiceAction) companyRouter.POST("/invoices/new", HandleNewInvoiceAction)
companyRouter.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { companyRouter.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
mustRenderAppTemplate(w, r, "dashboard.gohtml", nil) mustRenderAppTemplate(w, r, "dashboard.gohtml", nil)

View File

@ -146,8 +146,11 @@
--numerus--color--white: #ffffff; --numerus--color--white: #ffffff;
--numerus--color--yellow: #ffd200; --numerus--color--yellow: #ffd200;
--numerus--color--red: #ff7a53; --numerus--color--red: #ff7a53;
--numerus--color--rosy: #ffbaa6;
--numerus--color--green: #5ae487; --numerus--color--green: #5ae487;
--numerus--color--light-green: #9fefb9;
--numerus--color--blue: #55bfff; --numerus--color--blue: #55bfff;
--numerus--color--light-blue: #cbebff;
--numerus--color--hay: #ffe673; --numerus--color--hay: #ffe673;
--numerus--text-color: var(--numerus--color--black); --numerus--text-color: var(--numerus--color--black);
@ -232,6 +235,10 @@ table {
border-collapse: collapse; border-collapse: collapse;
} }
.no-padding td {
padding: 0;
}
tbody tr:nth-child(even) { tbody tr:nth-child(even) {
background-color: var(--numerus--header--background-color); background-color: var(--numerus--header--background-color);
} }
@ -439,7 +446,7 @@ fieldset {
background-color: var(--numerus--background-color); background-color: var(--numerus--background-color);
} }
#profile-menu[open] summary::before { details.menu[open] summary::before {
background-color: var(--numerus--header--background-color); background-color: var(--numerus--header--background-color);
position: fixed; position: fixed;
top: 0; top: 0;
@ -452,8 +459,12 @@ fieldset {
mix-blend-mode: multiply; mix-blend-mode: multiply;
} }
#profile-menu ul { ul[role="menu"] {
padding: 0;
list-style: none; list-style: none;
}
#profile-menu ul {
position: absolute; position: absolute;
right: -1.875em; right: -1.875em;
top: 100%; top: 100%;
@ -517,6 +528,53 @@ main > nav {
text-decoration: none; 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 */ /* Remix Icon */
@font-face { @font-face {

View File

@ -9,7 +9,7 @@
<body> <body>
<header> <header>
<h1><img src="/static/numerus.svg" alt="Numerus" width="261" height="33"></h1> <h1><img src="/static/numerus.svg" alt="Numerus" width="261" height="33"></h1>
<details id="profile-menu"> <details id="profile-menu" class="menu">
<summary> <summary>
<i class="ri-eye-close-line ri-3x"></i> <i class="ri-eye-close-line ri-3x"></i>
</summary> </summary>

View File

@ -15,7 +15,7 @@
</nav> </nav>
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.InvoicesIndexPage*/ -}} {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.InvoicesIndexPage*/ -}}
<table> <table class="no-padding">
<thead> <thead>
<tr> <tr>
<th>{{( pgettext "All" "invoice" )}}</th> <th>{{( pgettext "All" "invoice" )}}</th>
@ -36,10 +36,29 @@
<td>{{ .Date|formatDate }}</td> <td>{{ .Date|formatDate }}</td>
<td><a href="{{ companyURI "/invoices/"}}{{ .Slug }}">{{ .Number }}</a></td> <td><a href="{{ companyURI "/invoices/"}}{{ .Slug }}">{{ .Number }}</a></td>
<td><a href="{{ companyURI "/contacts/"}}{{ .CustomerSlug }}">{{ .CustomerName }}</a></td> <td><a href="{{ companyURI "/contacts/"}}{{ .CustomerSlug }}">{{ .CustomerName }}</a></td>
<td class="invoice-status-{{ .Status }}">{{ .StatusLabel }}</td> <td>
<details class="invoice-status menu">
<summary class="invoice-status-{{ .Status }}">{{ .StatusLabel }}</summary>
<form action="{{companyURI "/invoices/"}}{{ .Slug }}" method="POST">
{{ csrfToken }}
{{ putMethod }}
<ul role="menu">
{{- range $status, $name := $.InvoiceStatuses }}
{{- if ne $status $invoice.Status }}
<li>
<button type="submit" name="status" class="invoice-status-{{ $status }}"
value="{{ $status }}">{{ $name }}</button>
</li>
{{- end }}
{{- end }}
</ul>
</form>
</details>
</td>
<td></td> <td></td>
<td class="numeric">{{ .Total|formatPrice }}</td> <td class="numeric">{{ .Total|formatPrice }}</td>
<td class="invoice-download"><a href="{{ companyURI "/invoices/"}}{{ .Slug }}.pdf" download="{{ .Number}}.pdf" <td class="invoice-download"><a href="{{ companyURI "/invoices/"}}{{ .Slug }}.pdf"
download="{{ .Number}}.pdf"
title="{{( pgettext "Download invoice" "action" )}}" title="{{( pgettext "Download invoice" "action" )}}"
aria-label="{{( pgettext "Download invoice %s" "action" )}}"><i aria-label="{{( pgettext "Download invoice %s" "action" )}}"><i
class="ri-download-line"></i></a></td> class="ri-download-line"></i></a></td>