sklad: preliminary web interface
Only exposing most read operations thus far.
This commit is contained in:
parent
7eb84cd937
commit
e003427f9f
|
@ -1,19 +1,46 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>{{ template "Title" }} - sklad</title>
|
<title>{{ template "Title" . }} - sklad</title>
|
||||||
<meta http-equiv=Content-Type content="text/html; charset=utf-8">
|
<meta http-equiv=Content-Type content="text/html; charset=utf-8">
|
||||||
|
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||||
<style>
|
<style>
|
||||||
html, body { min-height: 100vh; }
|
html, body { min-height: 100vh; }
|
||||||
body { padding: 1em; box-sizing: border-box;
|
body { padding: 1rem; box-sizing: border-box;
|
||||||
margin: 0 auto; max-width: 50em;
|
margin: 0 auto; max-width: 50rem;
|
||||||
border-left: 1px solid #ccc; border-right: 1px solid #ccc;
|
border-left: 1px solid #ccc; border-right: 1px solid #ccc;
|
||||||
font-family: sans-serif; }
|
font-family: sans-serif; }
|
||||||
header { display: flex; justify-content: space-between; align-items: center;
|
|
||||||
flex-wrap: wrap; margin: -1em -1em 0 -1em; padding: 0 1em;
|
section { border: 1px outset #ccc; padding: 0 .5rem; margin: 1rem 0; }
|
||||||
background: linear-gradient(0deg, #fff, #eee); }
|
section > p { margin: 0 0 .5rem 0; }
|
||||||
header * { display: inline-block; }
|
|
||||||
|
header, footer { display: flex; justify-content: space-between;
|
||||||
|
align-items: center; flex-wrap: wrap; padding-top: .5em; }
|
||||||
|
header { margin: 0 -.5rem; padding: .5rem .5rem 0 .5rem;
|
||||||
|
background: linear-gradient(0deg, transparent, #f8f8f8); }
|
||||||
|
body > header { margin: -1rem -1rem 0 -1rem; padding: 1rem 1rem 0 1rem;
|
||||||
|
background: linear-gradient(0deg, transparent, #eeeeee); }
|
||||||
|
|
||||||
|
header *,
|
||||||
|
footer * { display: inline-block; }
|
||||||
|
header > *,
|
||||||
|
footer > * { margin: 0 0 .5rem 0; }
|
||||||
|
header > *:not(:last-child),
|
||||||
|
footer > *:not(:last-child) { margin-right: .5rem; }
|
||||||
|
|
||||||
|
header > h2,
|
||||||
|
header > h3 { flex-grow: 1; }
|
||||||
|
|
||||||
|
/* Don't ask me why this is an improvement on mobile browsers. */
|
||||||
|
input[type=submit], input[type=text], input[type=password],
|
||||||
|
select, textarea { border: 1px inset #ccc; padding: .25rem; }
|
||||||
|
input[type=submit] { border-style: outset; }
|
||||||
|
select { border-style: solid; }
|
||||||
|
|
||||||
a { color: inherit; }
|
a { color: inherit; }
|
||||||
|
textarea { padding: .5rem; box-sizing: border-box; width: 100%;
|
||||||
|
font-family: inherit; resize: vertical; }
|
||||||
|
select { max-width: 15rem; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -21,7 +48,7 @@
|
||||||
<header>
|
<header>
|
||||||
<h1>sklad</h1>
|
<h1>sklad</h1>
|
||||||
|
|
||||||
{{ if .LoggedIn }}
|
{{ block "HeaderControls" . }}
|
||||||
<a href=/>Obaly</a>
|
<a href=/>Obaly</a>
|
||||||
<a href=/series>Řady</a>
|
<a href=/series>Řady</a>
|
||||||
|
|
||||||
|
@ -33,6 +60,7 @@
|
||||||
<input type=submit value="Odhlásit">
|
<input type=submit value="Odhlásit">
|
||||||
</form>
|
</form>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{{ template "Content" . }}
|
{{ template "Content" . }}
|
||||||
|
|
|
@ -1,25 +1,91 @@
|
||||||
{{ define "Title" }}Přehled{{ end }}
|
{{ define "Title" }}{{ or .Id "Obaly" }}{{ end }}
|
||||||
{{ define "Content" }}
|
{{ define "Content" }}
|
||||||
|
|
||||||
{{ if .Id }}
|
{{ if .Id }}
|
||||||
<h2>{{ .Id }}</h2>
|
|
||||||
|
<section>
|
||||||
|
<header>
|
||||||
|
<h2>{{ .Id }}</h2>
|
||||||
|
<form method=post action="/label?id={{ .Id }}">
|
||||||
|
<input type=submit value="Vytisknout štítek">
|
||||||
|
</form>
|
||||||
|
<form method=post action="/?id={{ .Id }}&remove">
|
||||||
|
<input type=submit value="Odstranit">
|
||||||
|
</form>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form method=post action="/?id={{ .Id }}">
|
||||||
|
<textarea name=description rows=5>
|
||||||
|
{{ .Description }}
|
||||||
|
</textarea>
|
||||||
|
<footer>
|
||||||
|
<div>
|
||||||
|
<label for=series>Řada:</label>
|
||||||
|
<select name=series id=series>
|
||||||
|
{{ range $prefix, $desc := .AllSeries }}
|
||||||
|
<option value="{{ $prefix }}"
|
||||||
|
{{ if eq $prefix $.Series }}selected{{ end }}
|
||||||
|
>{{ $prefix }} — {{ $desc }}</option>
|
||||||
|
{{ end }}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for=parent>Nadobal:</label>
|
||||||
|
<input type=text name=parent id=parent value="{{ .Parent }}">
|
||||||
|
</div>
|
||||||
|
<input type=submit value="Uložit">
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<h2>Podobaly</h3>
|
||||||
|
|
||||||
{{ else }}
|
{{ else }}
|
||||||
|
<section>
|
||||||
|
<header>
|
||||||
|
<h2>Nový obal</h2>
|
||||||
|
</header>
|
||||||
|
<form method=post action="/">
|
||||||
|
<textarea name=description rows=5
|
||||||
|
placeholder="Popis obalu nebo jeho obsahu"></textarea>
|
||||||
|
<footer>
|
||||||
|
<div>
|
||||||
|
<label for=series>Řada:</label>
|
||||||
|
<select name=series id=series>
|
||||||
|
{{ range $prefix, $desc := .AllSeries }}
|
||||||
|
<option value="{{ $prefix }}"
|
||||||
|
{{ if eq $prefix $.Series }}selected{{ end }}
|
||||||
|
>{{ $prefix }} — {{ $desc }}</option>
|
||||||
|
{{ end }}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for=parent>Nadobal:</label>
|
||||||
|
<input type=text name=parent id=parent value="">
|
||||||
|
</div>
|
||||||
|
<input type=submit value="Uložit">
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
<h2>Obaly nejvyšší úrovně</h2>
|
<h2>Obaly nejvyšší úrovně</h2>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ if .Description }}
|
|
||||||
<p>{{ .Description }}
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ if .Children }}
|
|
||||||
{{ range .Children }}
|
{{ range .Children }}
|
||||||
<fieldset>
|
<section>
|
||||||
<h3><a href="/container?id={{ .Id }}">{{ .Id }}</a></h3>
|
<header>
|
||||||
|
<h3><a href="/container?id={{ .Id }}">{{ .Id }}</a></h3>
|
||||||
|
<form method=post action="/label?id={{ .Id }}">
|
||||||
|
<input type=submit value="Vytisknout štítek">
|
||||||
|
</form>
|
||||||
|
<form method=post action="/?id={{ .Id }}&remove">
|
||||||
|
<input type=submit value="Odstranit">
|
||||||
|
</form>
|
||||||
|
</header>
|
||||||
{{ if .Description }}
|
{{ if .Description }}
|
||||||
<p>{{ .Description }}
|
<p>{{ .Description }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</fieldset>
|
</section>
|
||||||
{{ end }}
|
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<p>Obal je prázdný.
|
<p>Obal je prázdný.
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
11
sklad/db.go
11
sklad/db.go
|
@ -123,12 +123,11 @@ func loadDatabase() error {
|
||||||
|
|
||||||
// Construct an index that goes from parent containers to their children.
|
// Construct an index that goes from parent containers to their children.
|
||||||
for _, pv := range db.Containers {
|
for _, pv := range db.Containers {
|
||||||
if pv.Parent == "" {
|
if pv.Parent != "" {
|
||||||
continue
|
if _, ok := indexContainer[pv.Parent]; !ok {
|
||||||
}
|
return fmt.Errorf("container %s has a nonexistent parent %s",
|
||||||
if _, ok := indexContainer[pv.Parent]; !ok {
|
pv.Id(), pv.Parent)
|
||||||
return fmt.Errorf("container %s has a nonexistent parent %s",
|
}
|
||||||
pv.Id(), pv.Parent)
|
|
||||||
}
|
}
|
||||||
indexChildren[pv.Parent] = append(indexChildren[pv.Parent], pv)
|
indexChildren[pv.Parent] = append(indexChildren[pv.Parent], pv)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
{{ define "Title" }}Přihlášení{{ end }}
|
{{ define "Title" }}Přihlášení{{ end }}
|
||||||
|
{{ define "HeaderControls" }}<!-- text/template requires content -->{{ end }}
|
||||||
{{ define "Content" }}
|
{{ define "Content" }}
|
||||||
|
|
||||||
<h2>Přihlášení</h2>
|
<h2>Přihlášení</h2>
|
||||||
|
|
||||||
<form method=post>
|
<form method=post>
|
||||||
<label for=password>Heslo:</label>
|
<label for=password>Heslo:</label>
|
||||||
<input type=password name=password id=password>
|
<input type=password name=password id=password
|
||||||
<input type=submit value="Přihlásit">
|
><input type=submit value="Přihlásit">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{{ if .IncorrectPassword }}
|
{{ if .IncorrectPassword }}
|
||||||
|
|
110
sklad/main.go
110
sklad/main.go
|
@ -13,8 +13,6 @@ import (
|
||||||
|
|
||||||
var templates = map[string]*template.Template{}
|
var templates = map[string]*template.Template{}
|
||||||
|
|
||||||
// TODO: Consider wrapping the data object in something that always contains
|
|
||||||
// a LoggedIn member, so that we don't need to duplicate it.
|
|
||||||
func executeTemplate(name string, w io.Writer, data interface{}) {
|
func executeTemplate(name string, w io.Writer, data interface{}) {
|
||||||
if err := templates[name].Execute(w, data); err != nil {
|
if err := templates[name].Execute(w, data); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -48,7 +46,6 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
params := struct {
|
params := struct {
|
||||||
LoggedIn bool
|
|
||||||
IncorrectPassword bool
|
IncorrectPassword bool
|
||||||
}{}
|
}{}
|
||||||
|
|
||||||
|
@ -82,39 +79,119 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleContainer(w http.ResponseWriter, r *http.Request) {
|
func handleContainer(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allSeries := map[string]string{}
|
||||||
|
for _, s := range indexSeries {
|
||||||
|
allSeries[s.Prefix] = s.Description
|
||||||
|
}
|
||||||
|
|
||||||
children := []*Container{}
|
children := []*Container{}
|
||||||
id := ContainerId(r.FormValue("id"))
|
id := ContainerId(r.FormValue("id"))
|
||||||
description := ""
|
description := ""
|
||||||
|
series := ""
|
||||||
|
parent := ContainerId("")
|
||||||
|
|
||||||
if id == "" {
|
if id == "" {
|
||||||
children = db.Containers
|
children = indexChildren[id]
|
||||||
} else if container, ok := indexContainer[id]; ok {
|
} else if container, ok := indexContainer[id]; ok {
|
||||||
children = indexChildren[id]
|
children = indexChildren[id]
|
||||||
description = container.Description
|
description = container.Description
|
||||||
|
series = container.Series
|
||||||
|
parent = container.Parent
|
||||||
}
|
}
|
||||||
|
|
||||||
params := struct {
|
params := struct {
|
||||||
LoggedIn bool
|
|
||||||
Id ContainerId
|
Id ContainerId
|
||||||
Description string
|
Description string
|
||||||
Children []*Container
|
Children []*Container
|
||||||
|
Series string
|
||||||
|
Parent ContainerId
|
||||||
|
AllSeries map[string]string
|
||||||
}{
|
}{
|
||||||
LoggedIn: true,
|
|
||||||
Id: id,
|
Id: id,
|
||||||
Description: description,
|
Description: description,
|
||||||
Children: children,
|
Children: children,
|
||||||
|
Series: series,
|
||||||
|
Parent: parent,
|
||||||
|
AllSeries: allSeries,
|
||||||
}
|
}
|
||||||
|
|
||||||
executeTemplate("container.tmpl", w, ¶ms)
|
executeTemplate("container.tmpl", w, ¶ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Consider a wrapper function that automatically calls ParseForm
|
func handleSeries(w http.ResponseWriter, r *http.Request) {
|
||||||
// and disables client-side caching.
|
if r.Method == http.MethodPost {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
allSeries := map[string]string{}
|
||||||
|
for _, s := range indexSeries {
|
||||||
|
allSeries[s.Prefix] = s.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := r.FormValue("prefix")
|
||||||
|
description := ""
|
||||||
|
|
||||||
|
if prefix == "" {
|
||||||
|
} else if series, ok := indexSeries[prefix]; ok {
|
||||||
|
description = series.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
params := struct {
|
||||||
|
Prefix string
|
||||||
|
Description string
|
||||||
|
AllSeries map[string]string
|
||||||
|
}{
|
||||||
|
Prefix: prefix,
|
||||||
|
Description: description,
|
||||||
|
AllSeries: allSeries,
|
||||||
|
}
|
||||||
|
|
||||||
|
executeTemplate("series.tmpl", w, ¶ms)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := r.FormValue("q")
|
||||||
|
_ = query
|
||||||
|
|
||||||
|
// TODO: Query the database for exact matches and fulltext.
|
||||||
|
|
||||||
|
params := struct{}{}
|
||||||
|
|
||||||
|
executeTemplate("search.tmpl", w, ¶ms)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleLabel(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id := r.FormValue("id")
|
||||||
|
_ = id
|
||||||
|
|
||||||
|
// TODO: See if such a container exists, print a label on the printer.
|
||||||
|
|
||||||
|
params := struct{}{}
|
||||||
|
|
||||||
|
executeTemplate("label.tmpl", w, ¶ms)
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Randomize the RNG for session string generation.
|
// Randomize the RNG for session string generation.
|
||||||
|
@ -144,20 +221,13 @@ func main() {
|
||||||
// TODO: Eventually we will need to load a font file for label printing.
|
// TODO: Eventually we will need to load a font file for label printing.
|
||||||
// - The path might be part of configuration, or implicit by filename.
|
// - The path might be part of configuration, or implicit by filename.
|
||||||
|
|
||||||
// TODO: Some routing and pages.
|
|
||||||
//
|
|
||||||
// - GET /container?id=UA1
|
|
||||||
// - GET /series?id=A
|
|
||||||
// - GET /search?q=bottle
|
|
||||||
//
|
|
||||||
// - https://stackoverflow.com/a/33880971/76313
|
|
||||||
// - POST /label?id=UA1
|
|
||||||
|
|
||||||
http.HandleFunc("/", sessionWrap(wrap(handleContainer)))
|
|
||||||
http.HandleFunc("/container", sessionWrap(wrap(handleContainer)))
|
|
||||||
|
|
||||||
http.HandleFunc("/login", wrap(handleLogin))
|
http.HandleFunc("/login", wrap(handleLogin))
|
||||||
http.HandleFunc("/logout", sessionWrap(wrap(handleLogout)))
|
http.HandleFunc("/logout", sessionWrap(wrap(handleLogout)))
|
||||||
|
|
||||||
|
http.HandleFunc("/", sessionWrap(wrap(handleContainer)))
|
||||||
|
http.HandleFunc("/series", sessionWrap(wrap(handleSeries)))
|
||||||
|
http.HandleFunc("/search", sessionWrap(wrap(handleSearch)))
|
||||||
|
http.HandleFunc("/label", sessionWrap(wrap(handleLabel)))
|
||||||
|
|
||||||
log.Fatalln(http.ListenAndServe(address, nil))
|
log.Fatalln(http.ListenAndServe(address, nil))
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
{{ define "Title" }}{{ or .Prefix "Řady" }}{{ end }}
|
||||||
|
{{ define "Content" }}
|
||||||
|
|
||||||
|
{{ if .Prefix }}
|
||||||
|
<h2>{{ .Prefix }}</h2>
|
||||||
|
|
||||||
|
{{ if .Description }}
|
||||||
|
<p>{{ .Description }}
|
||||||
|
{{ end }}
|
||||||
|
{{ else }}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<form method=post action="/series">
|
||||||
|
<header>
|
||||||
|
<h3>Nová řada</h3>
|
||||||
|
<input type=text name=prefix placeholder="Prefix řady">
|
||||||
|
<input type=text name=description placeholder="Popis řady"
|
||||||
|
><input type=submit value="Uložit">
|
||||||
|
</form>
|
||||||
|
</header>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{{ range $prefix, $desc := .AllSeries }}
|
||||||
|
<section>
|
||||||
|
<header>
|
||||||
|
<h3><a href="/series?prefix={{ $prefix }}">{{ $prefix }}</a></h3>
|
||||||
|
<form method=post action="/series?prefix={{ $prefix }}">
|
||||||
|
<input type=text name=description value="{{ $desc }}"
|
||||||
|
><input type=submit value="Uložit">
|
||||||
|
</form>
|
||||||
|
<form method=post action="/series?prefix={{ $prefix }}&remove">
|
||||||
|
<input type=submit value="Odstranit">
|
||||||
|
</form>
|
||||||
|
</header>
|
||||||
|
</section>
|
||||||
|
{{ else }}
|
||||||
|
<p>Nejsou žádné řady.
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ end }}
|
Loading…
Reference in New Issue