Add the “menu” to change invoice statuses
This commit is contained in:
parent
d8997de654
commit
039bf3abbd
|
@ -29,12 +29,16 @@ 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)
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,12 +36,31 @@
|
||||||
<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"
|
||||||
title="{{( pgettext "Download invoice" "action" )}}"
|
download="{{ .Number}}.pdf"
|
||||||
aria-label="{{( pgettext "Download invoice %s" "action" )}}"><i
|
title="{{( pgettext "Download invoice" "action" )}}"
|
||||||
|
aria-label="{{( pgettext "Download invoice %s" "action" )}}"><i
|
||||||
class="ri-download-line"></i></a></td>
|
class="ri-download-line"></i></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
Loading…
Reference in New Issue