Add the bare-bones form for invoices

This commit is contained in:
jordi fita mas 2023-02-11 22:16:48 +01:00
parent 6bf51d5eeb
commit 5c15b9de20
7 changed files with 241 additions and 1 deletions

152
pkg/invoices.go Normal file
View File

@ -0,0 +1,152 @@
package pkg
import (
"context"
"fmt"
"github.com/julienschmidt/httprouter"
"html/template"
"net/http"
"time"
)
type InvoicesIndexPage struct {
}
func IndexInvoices(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
page := &InvoicesIndexPage{}
mustRenderAppTemplate(w, r, "invoices/index.gohtml", page)
}
func GetInvoiceForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
locale := getLocale(r)
conn := getConn(r)
company := mustGetCompany(r)
form := newInvoiceForm(r.Context(), conn, locale, company)
slug := params[0].Value
if slug == "new" {
form.Customer.EmptyLabel = gettext("Select a customer to bill.", locale)
form.Date.Val = time.Now().Format("2006-01-02")
w.WriteHeader(http.StatusOK)
mustRenderNewInvoiceForm(w, r, form)
return
}
}
func mustRenderNewInvoiceForm(w http.ResponseWriter, r *http.Request, form *invoiceForm) {
mustRenderAppTemplate(w, r, "invoices/new.gohtml", form)
}
func HandleAddInvoice(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
r.ParseForm()
w.Write([]byte("OK"))
}
type invoiceProductForm struct {
ProductId *InputField
Name *InputField
Description *InputField
Price *InputField
Quantity *InputField
Discount *InputField
Tax *SelectField
}
type invoiceForm struct {
locale *Locale
company *Company
Customer *SelectField
Number *InputField
Date *InputField
Notes *InputField
Products []*invoiceProductForm
}
func newInvoiceForm(ctx context.Context, conn *Conn, locale *Locale, company *Company) *invoiceForm {
return &invoiceForm{
locale: locale,
company: company,
Customer: &SelectField{
Name: "customer",
Label: pgettext("input", "Customer", locale),
Required: true,
Options: MustGetOptions(ctx, conn, "select contact_id::text, business_name from contact where company_id = $1 order by business_name", company.Id),
},
Number: &InputField{
Name: "number",
Label: pgettext("input", "Number", locale),
Type: "text",
Required: false,
},
Date: &InputField{
Name: "date",
Label: pgettext("input", "Invoice Date", locale),
Type: "date",
Required: true,
},
Notes: &InputField{
Name: "description",
Label: pgettext("input", "Notes", locale),
Type: "textarea",
},
Products: []*invoiceProductForm{
newInvoiceProductForm(ctx, conn, company, locale),
},
}
}
func newInvoiceProductForm(ctx context.Context, conn *Conn, company *Company, locale *Locale) *invoiceProductForm {
return &invoiceProductForm{
ProductId: &InputField{
Name: "product[][id]",
Label: pgettext("input", "Id", locale),
Type: "hidden",
Required: true,
},
Name: &InputField{
Name: "product[][name]",
Label: pgettext("input", "Name", locale),
Type: "text",
Required: true,
},
Description: &InputField{
Name: "product[][description]",
Label: pgettext("input", "Description", locale),
Type: "textarea",
},
Price: &InputField{
Name: "product[][price]",
Label: pgettext("input", "Price", locale),
Type: "number",
Required: true,
Attributes: []template.HTMLAttr{
`min="0"`,
template.HTMLAttr(fmt.Sprintf(`step="%v"`, company.MinCents())),
},
},
Quantity: &InputField{
Name: "product[][quantity]",
Label: pgettext("input", "Quantity", locale),
Type: "number",
Required: true,
Attributes: []template.HTMLAttr{
`min="0"`,
},
},
Discount: &InputField{
Name: "product[][discount]",
Label: pgettext("input", "Discount (%)", locale),
Type: "number",
Required: true,
Attributes: []template.HTMLAttr{
`min="0"`,
`max="100"`,
},
},
Tax: &SelectField{
Name: "product[][tax]",
Label: pgettext("input", "Taxes", locale),
Multiple: true,
Options: MustGetOptions(ctx, conn, "select tax_id::text, name from tax where company_id = $1 order by name", company.Id),
},
}
}

View File

@ -22,6 +22,9 @@ func NewRouter(db *Db) http.Handler {
companyRouter.POST("/products", HandleAddProduct)
companyRouter.GET("/products/:slug", GetProductForm)
companyRouter.PUT("/products/:slug", HandleUpdateProduct)
companyRouter.GET("/invoices", IndexInvoices)
companyRouter.POST("/invoices", HandleAddInvoice)
companyRouter.GET("/invoices/:slug", GetInvoiceForm)
companyRouter.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
mustRenderAppTemplate(w, r, "dashboard.gohtml", nil)
})

View File

@ -304,7 +304,7 @@ main {
margin-top: 2rem;
}
input[type="text"], input[type="password"], input[type="email"], input[type="tel"], input[type="url"], input[type="number"], select, textarea {
input[type="text"], input[type="password"], input[type="email"], input[type="tel"], input[type="url"], input[type="number"], input[type="date"], select, textarea {
background-color: var(--numerus--background-color);
border: 1px solid var(--numerus--color--black);
border-radius: 0;

View File

@ -41,6 +41,7 @@
<nav aria-label="{{( pgettext "Main" "title" )}}">
<ul>
<li><a href="{{ companyURI "/" }}">{{( pgettext "Dashboard" "nav" )}}</a></li>
<li><a href="{{ companyURI "/invoices" }}">{{( pgettext "Invoices" "nav" )}}</a></li>
<li><a href="{{ companyURI "/products" }}">{{( pgettext "Products" "nav" )}}</a></li>
<li><a href="{{ companyURI "/contacts" }}">{{( pgettext "Contacts" "nav" )}}</a></li>
</ul>

View File

@ -1,3 +1,9 @@
{{ define "hidden-field" -}}
<input type="{{ .Type }}" name="{{ .Name }}" id="{{ .Name }}-field"
{{- range $attribute := .Attributes }} {{$attribute}} {{ end }}
{{ if .Required }}required="required"{{ end }} value="{{ .Val }}">
{{- end }}
{{ define "input-field" -}}
<div class="input {{ if .Errors }}has-errors{{ end }}">
{{ if eq .Type "textarea" }}

View File

@ -0,0 +1,36 @@
{{ define "title" -}}
{{( pgettext "Invoices" "title" )}}
{{- end }}
{{ define "content" }}
<nav>
<p>
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
<a>{{( pgettext "Invoices" "title" )}}</a>
</p>
<p>
<a class="primary button"
href="{{ companyURI "/invoices/new" }}">{{( pgettext "New invoice" "action" )}}</a>
</p>
</nav>
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.InvoicesIndexPage*/ -}}
<table>
<thead>
<tr>
<th>{{( pgettext "All" "invoice" )}}</th>
<th>{{( pgettext "Date" "title" )}}</th>
<th>{{( pgettext "Invoice Num." "title" )}}</th>
<th>{{( pgettext "Customer" "title" )}}</th>
<th>{{( pgettext "Status" "title" )}}</th>
<th>{{( pgettext "Label" "title" )}}</th>
<th>{{( pgettext "Download" "title" )}}</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="7">{{( gettext "No invoices added yet." )}}</td>
</tr>
</tbody>
</table>
{{- end }}

View File

@ -0,0 +1,42 @@
{{ define "title" -}}
{{( pgettext "New Invoice" "title" )}}
{{- end }}
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.invoiceForm*/ -}}
<nav>
<p>
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
<a href="{{ companyURI "/invoices"}}">{{( pgettext "Invoices" "title" )}}</a> /
<a>{{( pgettext "New Invoice" "title" )}}</a>
</p>
</nav>
<section class="dialog-content">
<h2>{{(pgettext "New Invoice" "title")}}</h2>
<form method="POST" action="{{ companyURI "/invoices" }}">
{{ csrfToken }}
{{ template "select-field" .Customer }}
{{ template "input-field" .Number }}
{{ template "input-field" .Date }}
{{ template "input-field" .Notes }}
{{- range $product := .Products }}
<fieldset>
{{ template "hidden-field" .ProductId }}
{{ template "input-field" .Name }}
{{ template "input-field" .Description }}
{{ template "input-field" .Price }}
{{ template "input-field" .Quantity }}
{{ template "input-field" .Discount }}
{{ template "select-field" .Tax }}
</fieldset>
{{- end }}
<fieldset>
<button class="primary" name="action" value="add" type="submit">{{( pgettext "New invoice" "action" )}}</button>
</fieldset>
</form>
</section>
{{- end }}