Add the bare-bones form for invoices
This commit is contained in:
parent
6bf51d5eeb
commit
5c15b9de20
|
@ -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),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,9 @@ func NewRouter(db *Db) http.Handler {
|
||||||
companyRouter.POST("/products", HandleAddProduct)
|
companyRouter.POST("/products", HandleAddProduct)
|
||||||
companyRouter.GET("/products/:slug", GetProductForm)
|
companyRouter.GET("/products/:slug", GetProductForm)
|
||||||
companyRouter.PUT("/products/:slug", HandleUpdateProduct)
|
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) {
|
companyRouter.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
mustRenderAppTemplate(w, r, "dashboard.gohtml", nil)
|
mustRenderAppTemplate(w, r, "dashboard.gohtml", nil)
|
||||||
})
|
})
|
||||||
|
|
|
@ -304,7 +304,7 @@ main {
|
||||||
margin-top: 2rem;
|
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);
|
background-color: var(--numerus--background-color);
|
||||||
border: 1px solid var(--numerus--color--black);
|
border: 1px solid var(--numerus--color--black);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
<nav aria-label="{{( pgettext "Main" "title" )}}">
|
<nav aria-label="{{( pgettext "Main" "title" )}}">
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{{ companyURI "/" }}">{{( pgettext "Dashboard" "nav" )}}</a></li>
|
<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 "/products" }}">{{( pgettext "Products" "nav" )}}</a></li>
|
||||||
<li><a href="{{ companyURI "/contacts" }}">{{( pgettext "Contacts" "nav" )}}</a></li>
|
<li><a href="{{ companyURI "/contacts" }}">{{( pgettext "Contacts" "nav" )}}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -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" -}}
|
{{ define "input-field" -}}
|
||||||
<div class="input {{ if .Errors }}has-errors{{ end }}">
|
<div class="input {{ if .Errors }}has-errors{{ end }}">
|
||||||
{{ if eq .Type "textarea" }}
|
{{ if eq .Type "textarea" }}
|
||||||
|
|
|
@ -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 }}
|
|
@ -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 }}
|
Loading…
Reference in New Issue