[Add search function]

This commit is contained in:
xoy 2025-03-18 21:19:18 +01:00
parent d8d6820428
commit b33dc14db9
10 changed files with 288 additions and 56 deletions

View file

@ -5,30 +5,30 @@ create table locations (
description text description text
); );
insert into locations(id,parent,name,description) values(0,null,"Raum 1","Eingang / Kueche"); insert into locations(parent,name,description) values(null,"Raum 1","Eingang / Kueche");
insert into locations(id,parent,name,description) values(1,null,"Raum 2","Server / Chillout-Area"); insert into locations(parent,name,description) values(null,"Raum 2","Server / Chillout-Area");
insert into locations(id,parent,name,description) values(2,null,"Raum 3","Hauptlager / Arbeitszimmer / Gemeinschaftsraum"); insert into locations(parent,name,description) values(null,"Raum 3","Hauptlager / Arbeitszimmer / Gemeinschaftsraum");
insert into locations(id,parent,name,description) values(3,null,"Raum 4","Chemie / Nebenlager / 3D-Druck / Plotter"); insert into locations(parent,name,description) values(null,"Raum 4","Chemie / Nebenlager / 3D-Druck / Plotter");
insert into locations(id,parent,name,description) values(4,null,"Raum 5","Maschinenraum / Werkstatt"); insert into locations(parent,name,description) values(null,"Raum 5","Maschinenraum / Werkstatt");
insert into locations(id,parent,name,description) values(5,2,"A",null); insert into locations(parent,name,description) values(2,"A",null);
insert into locations(id,parent,name,description) values(6,2,"B",null); insert into locations(parent,name,description) values(2,"B",null);
insert into locations(id,parent,name,description) values(7,2,"C",null); insert into locations(parent,name,description) values(2,"C",null);
insert into locations(id,parent,name,description) values(8,2,"D",null); insert into locations(parent,name,description) values(2,"D",null);
insert into locations(id,parent,name,description) values(9,2,"E",null); insert into locations(parent,name,description) values(2,"E",null);
insert into locations(id,parent,name,description) values(10,2,"F",null); insert into locations(parent,name,description) values(2,"F",null);
insert into locations(id,parent,name,description) values(11,2,"G",null); insert into locations(parent,name,description) values(2,"G",null);
create table containers ( create table containers (
id integer primary key, id integer primary key,
name text name text
); );
insert into containers(id,name) values(0,"Grosse Kiste"); insert into containers(name) values("Grosse Kiste");
insert into containers(id,name) values(1,"Flache Kiste"); insert into containers(name) values("Flache Kiste");
insert into containers(id,name) values(2,"Komponentenschachtel"); insert into containers(name) values("Komponentenschachtel");
insert into containers(id,name) values(3,"Regal"); insert into containers(name) values("Regal");
insert into containers(id,name) values(4,"Tisch"); insert into containers(name) values("Tisch");
insert into containers(id,name) values(5,"Schublade"); insert into containers(name) values("Schublade");
create table parts ( create table parts (
id integer primary key, id integer primary key,

84
db.go
View file

@ -4,10 +4,23 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"os" "os"
"strings"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
) )
func ParseSearchString(search string) (out []any) {
for strings.Contains(search, " ") {
search = strings.ReplaceAll(search, " ", " ")
}
searchStrings := strings.Split(search, " ")
out = make([]any, len(searchStrings))
for i, s := range searchStrings {
out[i] = "%" + s + "%"
}
return out
}
type Connection struct { type Connection struct {
DB *sql.DB DB *sql.DB
Error error Error error
@ -30,6 +43,73 @@ func (conn *Connection) InitDatabase() error {
return err return err
} }
func (conn *Connection) SearchPart(search string) ([]*Part, error) {
parsed := ParseSearchString(search)
if len(parsed) == 0 || (len(parsed) == 1 && len(fmt.Sprintf("%v", parsed[0])) == 2) {
return []*Part{}, nil
}
if conn.Error != nil {
return nil, conn.Error
}
searchPattern := ""
for i := 0; i < len(parsed); i++ {
searchPattern += "tags LIKE ?"
if i < len(parsed)-1 {
searchPattern += " OR "
} else {
searchPattern += " "
}
}
rows, err := conn.DB.Query("select id, name, tags, location, container from parts where "+searchPattern, parsed...)
if err != nil {
return nil, err
}
parts := make([]*Part, 0)
for rows.Next() {
part := Part{}
var locationId sql.NullInt64
var containerId sql.NullInt64
err := rows.Scan(&part.Id, &part.Name, &part.Tags, &locationId, &containerId)
if err != nil {
return nil, err
}
var location *Location
if locationId.Valid {
location, err = conn.QueryLocation(locationId.Int64)
if err != nil {
return nil, err
}
}
var container *Container
if containerId.Valid {
container, err = conn.QueryContainer(containerId.Int64)
if err != nil {
return nil, err
}
}
part.Location = *location
part.Container = *container
part.Connection = conn
parts = append(parts, &part)
}
return parts, nil
}
func (conn *Connection) QueryLocations() ([]*Location, error) { func (conn *Connection) QueryLocations() ([]*Location, error) {
if conn.Error != nil { if conn.Error != nil {
return nil, conn.Error return nil, conn.Error
@ -49,6 +129,8 @@ func (conn *Connection) QueryLocations() ([]*Location, error) {
return nil, err return nil, err
} }
location.Connection = conn
locations = append(locations, &location) locations = append(locations, &location)
} }
@ -69,6 +151,8 @@ func (conn *Connection) QueryLocation(id int64) (*Location, error) {
return nil, err return nil, err
} }
location.Connection = conn
return &location, nil return &location, nil
} }

65
main.go
View file

@ -2,11 +2,14 @@ package main
import ( import (
"log" "log"
"math/rand/v2"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/template/html/v2" "github.com/gofiber/template/html/v2"
) )
const figchProbability = 5
func main() { func main() {
engine := html.New("views", ".html") engine := html.New("views", ".html")
@ -24,7 +27,10 @@ func main() {
conn := Connect() conn := Connect()
app.Get("/", func(c *fiber.Ctx) error { app.Get("/", func(c *fiber.Ctx) error {
figch := rand.IntN(100) <= figchProbability
return c.Render("search", fiber.Map{ return c.Render("search", fiber.Map{
"Figch": figch,
"Title": "Suche", "Title": "Suche",
"Stylenames": NewStyleItemList("colors", "main", "search"), "Stylenames": NewStyleItemList("colors", "main", "search"),
"NavItems": navItems, "NavItems": navItems,
@ -34,7 +40,10 @@ func main() {
}) })
app.Get("/search", func(c *fiber.Ctx) error { app.Get("/search", func(c *fiber.Ctx) error {
figch := rand.IntN(100) <= figchProbability
return c.Render("search", fiber.Map{ return c.Render("search", fiber.Map{
"Figch": figch,
"Title": "Suche", "Title": "Suche",
"Stylenames": NewStyleItemList("colors", "main", "search"), "Stylenames": NewStyleItemList("colors", "main", "search"),
"NavItems": navItems, "NavItems": navItems,
@ -44,17 +53,59 @@ func main() {
}) })
app.Post("/search", func(c *fiber.Ctx) error { app.Post("/search", func(c *fiber.Ctx) error {
figch := rand.IntN(100) <= figchProbability
search := c.FormValue("search", "")
var parts []*Part
var err error
var table Table
var notification string
var resultCount int = -1
if IsEmpty(search) {
notification = "Sucheingabe darf nicht leer sein!"
search = ""
} else if len(search) < 3 {
notification = "Sucheingabe muss mehr als 2 Zeichen haben!"
search = ""
} else if !IsAlphaNumeric(search) {
notification = "Sucheingabe darf keine Sonderzeichen enthalten (außer Leerzeichen)"
search = ""
} else {
parts, err = conn.SearchPart(search)
if err != nil {
return err
}
table = ToTable[*Part](parts, TableColumns{
"ID",
"Name",
"Tags",
"Ort",
"Behälter",
})
resultCount = len(table.Rows)
}
return c.Render("search", fiber.Map{ return c.Render("search", fiber.Map{
"Figch": figch,
"Title": "Suche", "Title": "Suche",
"Stylenames": NewStyleItemList("colors", "main", "search"), "Stylenames": NewStyleItemList("colors", "main", "search"),
"NavItems": navItems, "NavItems": navItems,
"ActivePage": "/search", "ActivePage": "/search",
"SearchResultCount": 0, "SearchResultCount": resultCount,
"Columns": table.Collumns,
"Rows": table.Rows,
"Notification": notification,
"Search": search,
}) })
}) })
app.Get("/admin", func(c *fiber.Ctx) error { app.Get("/admin", func(c *fiber.Ctx) error {
figch := rand.IntN(100) <= figchProbability
return c.Render("admin/tables", fiber.Map{ return c.Render("admin/tables", fiber.Map{
"Figch": figch,
"Title": "Verwaltung", "Title": "Verwaltung",
"Stylenames": NewStyleItemList("colors", "main", "tables"), "Stylenames": NewStyleItemList("colors", "main", "tables"),
"NavItems": navItems, "NavItems": navItems,
@ -68,18 +119,20 @@ func main() {
}) })
app.Get("/admin/locations/overview", func(c *fiber.Ctx) error { app.Get("/admin/locations/overview", func(c *fiber.Ctx) error {
figch := rand.IntN(100) <= figchProbability
locations, err := conn.QueryLocations() locations, err := conn.QueryLocations()
if err != nil { if err != nil {
return err return err
} }
table := ToTable[*Location](locations, TableColumns{ table := ToTable[*Location](locations, TableColumns{
"ID", "ID",
"Übergeordneter Ort", "Ort",
"Name",
"Beschreibung", "Beschreibung",
}) })
return c.Render("admin/overview", fiber.Map{ return c.Render("admin/overview", fiber.Map{
"Figch": figch,
"Title": "Verwaltung", "Title": "Verwaltung",
"Stylenames": NewStyleItemList("colors", "main", "overview"), "Stylenames": NewStyleItemList("colors", "main", "overview"),
"NavItems": navItems, "NavItems": navItems,
@ -91,6 +144,8 @@ func main() {
}) })
app.Get("/admin/containers/overview", func(c *fiber.Ctx) error { app.Get("/admin/containers/overview", func(c *fiber.Ctx) error {
figch := rand.IntN(100) <= figchProbability
containers, err := conn.QueryContainers() containers, err := conn.QueryContainers()
if err != nil { if err != nil {
return err return err
@ -101,6 +156,7 @@ func main() {
}) })
return c.Render("admin/overview", fiber.Map{ return c.Render("admin/overview", fiber.Map{
"Figch": figch,
"Title": "Verwaltung", "Title": "Verwaltung",
"Stylenames": NewStyleItemList("colors", "main", "overview"), "Stylenames": NewStyleItemList("colors", "main", "overview"),
"NavItems": navItems, "NavItems": navItems,
@ -112,6 +168,8 @@ func main() {
}) })
app.Get("/admin/parts/overview", func(c *fiber.Ctx) error { app.Get("/admin/parts/overview", func(c *fiber.Ctx) error {
figch := rand.IntN(100) <= figchProbability
parts, err := conn.QueryParts() parts, err := conn.QueryParts()
if err != nil { if err != nil {
return err return err
@ -125,6 +183,7 @@ func main() {
}) })
return c.Render("admin/overview", fiber.Map{ return c.Render("admin/overview", fiber.Map{
"Figch": figch,
"Title": "Verwaltung", "Title": "Verwaltung",
"Stylenames": NewStyleItemList("colors", "main", "overview"), "Stylenames": NewStyleItemList("colors", "main", "overview"),
"NavItems": navItems, "NavItems": navItems,

25
misc.go
View file

@ -1,5 +1,9 @@
package main package main
import (
"unicode"
)
func ToTable[T DatabaseType](rows []T, columns TableColumns) Table { func ToTable[T DatabaseType](rows []T, columns TableColumns) Table {
tableRows := make([]TableRow, len(rows)) tableRows := make([]TableRow, len(rows))
@ -12,3 +16,24 @@ func ToTable[T DatabaseType](rows []T, columns TableColumns) Table {
tableRows, tableRows,
} }
} }
func IsAlphaNumeric(s string) bool {
for _, r := range s {
if !(unicode.IsLetter(r) || unicode.IsNumber(r) || unicode.IsSpace(r)) {
return false
}
}
return true
}
func IsEmpty(s string) bool {
if len(s) == 0 {
return true
}
for _, r := range s {
if !unicode.IsSpace(r) {
return false
}
}
return true
}

View file

@ -1,26 +1,62 @@
main { main {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: start;
padding-top: 2em; padding-top: 2em;
padding-bottom: 2em; padding-bottom: 2em;
} }
a.table-header-btn, a.table-header-btn:visited {
color: var(--link);
border: 2px solid var(--link);
padding: .5em;
border-radius: 4px;
}
table { table {
border-collapse: collapse; border-collapse: collapse;
width: 1200px; width: 1200px;
max-width: 100%; max-width: 100%;
} }
table :is(th, td) { table tr:first-child {
border: none;
background-color: transparent !important;
}
table tr:nth-child(2n + 1) {
background-color: var(--bg-border);
}
table tr {
border: 2px solid var(--bg-border); border: 2px solid var(--bg-border);
padding: 1em; border-top: none;
border-bottom: none;
}
table tr:nth-child(2) {
background-color: var(--fg);
color: var(--bg-light);
border: 2px solid var(--fg);
border-bottom: none;
font-size: 1.2em;
}
table tr:last-child {
border: 2px solid var(--bg-border);
border-top: none;
}
table tr :is(th, td) {
padding: 10px;
text-align: left;
} }
table tr td:last-child > div.action-container { table tr td:last-child > div.action-container {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
justify-content: space-around; justify-content: start;
gap: .1em;
} }
table tr td:last-child form { table tr td:last-child form {
@ -35,4 +71,5 @@ table tr td:last-child form button {
border: none; border: none;
cursor: pointer; cursor: pointer;
color: var(--fg); color: var(--fg);
font-size: 1em;
} }

View file

@ -6,12 +6,12 @@ main {
gap: 50px; gap: 50px;
} }
main > form { main form {
display: flex; display: flex;
margin-bottom: 50px; margin-bottom: 50px;
} }
main > form > input { main form > input {
width: 420px; width: 420px;
height: 50px; height: 50px;
border-radius: 0px; border-radius: 0px;
@ -25,11 +25,11 @@ main > form > input {
border-right: 2px solid var(--bg-border); border-right: 2px solid var(--bg-border);
} }
main > form > input:focus { main form > input:focus {
outline: none; outline: none;
} }
main > form > button { main form > button {
width: 50px; width: 50px;
height: 50px; height: 50px;
border: none; border: none;
@ -51,4 +51,14 @@ main > table tr :is(td, th) {
border: 2px solid var(--bg-border); border: 2px solid var(--bg-border);
padding: 5px; padding: 5px;
text-align: center; text-align: center;
}
main div.form-with-notification {
width: 472px;
}
main div.form-with-notification > p.notification {
text-align: center;
color: var(--accent);
font-weight: bold;
} }

View file

@ -41,35 +41,30 @@ type Location struct {
Parent sql.NullInt64 Parent sql.NullInt64
Name sql.NullString Name sql.NullString
Description sql.NullString Description sql.NullString
Connection *Connection
} }
func (l *Location) ToTableRow() TableRow { func (l *Location) ToTableRow() TableRow {
columns := make(TableColumns, 4) columns := make(TableColumns, 3)
tr := TableRow{} tr := TableRow{}
var path string
if l.Id.Valid { if l.Id.Valid {
columns[0] = fmt.Sprintf("%d", l.Id.Int64) columns[0] = fmt.Sprintf("%d", l.Id.Int64)
tr.Id = int(l.Id.Int64) tr.Id = int(l.Id.Int64)
path, _ = l.Connection.QueryLocationFullPath(l.Id.Int64)
} else { } else {
columns[0] = "NULL" columns[0] = "NULL"
tr.Id = -1 tr.Id = -1
} }
if l.Parent.Valid { columns[1] = path
columns[1] = fmt.Sprintf("%d", l.Parent.Int64) if l.Description.Valid {
} else { columns[2] = l.Description.String
columns[1] = "NULL"
}
if l.Name.Valid {
columns[2] = l.Name.String
} else { } else {
columns[2] = "NULL" columns[2] = "NULL"
} }
if l.Description.Valid {
columns[3] = l.Description.String
} else {
columns[3] = "NULL"
}
tr.Columns = columns tr.Columns = columns

View file

@ -1,13 +1,19 @@
{{template "partials/base-top" .}} {{template "partials/base-top" .}}
{{ $Table := .Table }}
<table> <table>
<tr>
<th>
<a class="table-header-btn" href="/admin/{{ $Table }}/add">Hinzufügen</a>
</th>
</tr>
<tr> <tr>
{{ range .Columns }} {{ range .Columns }}
<th>{{ . }}</th> <th>{{ . }}</th>
{{ end }} {{ end }}
<th>Aktionen</th> <th>Aktionen</th>
</tr> </tr>
{{ $Table := .Table }}
{{ range .Rows }} {{ range .Rows }}
<tr> <tr>
{{ range .Columns }} {{ range .Columns }}

View file

@ -3,14 +3,14 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>fisch - {{ .Title }}</title> <title>{{ if .Figch }}figch{{ else }}fisch{{ end }} - {{ .Title }}</title>
{{ range .Stylenames }} {{ range .Stylenames }}
<link rel="stylesheet" href="/css/{{ . }}.css"> <link rel="stylesheet" href="/css/{{ . }}.css">
{{ end }} {{ end }}
</head> </head>
<body> <body>
<header> <header>
<h1>fisch</h1> <h1>{{ if .Figch }}figch{{ else }}fisch{{ end }}</h1>
<nav> <nav>
{{ range .NavItems }} {{ range .NavItems }}
<a href="{{ .Destination }}" {{ if eq .Destination $.ActivePage }} class="active" {{ end }}>{{ .Caption }}</a> <a href="{{ .Destination }}" {{ if eq .Destination $.ActivePage }} class="active" {{ end }}>{{ .Caption }}</a>

View file

@ -1,10 +1,24 @@
{{template "partials/base-top" .}} {{template "partials/base-top" .}}
{{ if .Notification }}
<div class="form-with-notification">
<p class="notification">{{ .Notification }}</p>
{{ end }}
<form action="/search" method="post"> <form action="/search" method="post">
<input type="text" name="search" placeholder="Suche . . ."> <input type="text" name="search" placeholder="Suche . . ." {{ if .Search }} value="{{ .Search }}" {{ end }}>
<button type="submit">🔍</button> <button type="submit">🔍</button>
</form> </form>
{{ if .Notification }}
</divdiv>
{{ end }}
{{ if eq .SearchResultCount -1 }} {{ if eq .SearchResultCount -1 }}
@ -13,16 +27,18 @@
{{ if gt .SearchResultCount 0 }} {{ if gt .SearchResultCount 0 }}
<table> <table>
<tr> <tr>
<th>Ort</th> {{ range .Columns }}
<th>Kistenbezeichnung</th> <th>{{ . }}</th>
<th>Möglicher Inhalt</th> {{ end }}
</tr> </tr>
<tr> {{ range .Rows }}
<td>A</td> <tr>
<td>A</td> {{ range .Columns }}
<td>A</td> <td>{{ . }}</td>
</tr> {{ end }}
</tr>
{{ end }}
</table> </table>
{{ else }} {{ else }}