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

84
db.go
View file

@ -4,10 +4,23 @@ import (
"database/sql"
"fmt"
"os"
"strings"
_ "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 {
DB *sql.DB
Error error
@ -30,6 +43,73 @@ func (conn *Connection) InitDatabase() error {
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) {
if conn.Error != nil {
return nil, conn.Error
@ -49,6 +129,8 @@ func (conn *Connection) QueryLocations() ([]*Location, error) {
return nil, err
}
location.Connection = conn
locations = append(locations, &location)
}
@ -69,6 +151,8 @@ func (conn *Connection) QueryLocation(id int64) (*Location, error) {
return nil, err
}
location.Connection = conn
return &location, nil
}

65
main.go
View file

@ -2,11 +2,14 @@ package main
import (
"log"
"math/rand/v2"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/template/html/v2"
)
const figchProbability = 5
func main() {
engine := html.New("views", ".html")
@ -24,7 +27,10 @@ func main() {
conn := Connect()
app.Get("/", func(c *fiber.Ctx) error {
figch := rand.IntN(100) <= figchProbability
return c.Render("search", fiber.Map{
"Figch": figch,
"Title": "Suche",
"Stylenames": NewStyleItemList("colors", "main", "search"),
"NavItems": navItems,
@ -34,7 +40,10 @@ func main() {
})
app.Get("/search", func(c *fiber.Ctx) error {
figch := rand.IntN(100) <= figchProbability
return c.Render("search", fiber.Map{
"Figch": figch,
"Title": "Suche",
"Stylenames": NewStyleItemList("colors", "main", "search"),
"NavItems": navItems,
@ -44,17 +53,59 @@ func main() {
})
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{
"Figch": figch,
"Title": "Suche",
"Stylenames": NewStyleItemList("colors", "main", "search"),
"NavItems": navItems,
"ActivePage": "/search",
"SearchResultCount": 0,
"SearchResultCount": resultCount,
"Columns": table.Collumns,
"Rows": table.Rows,
"Notification": notification,
"Search": search,
})
})
app.Get("/admin", func(c *fiber.Ctx) error {
figch := rand.IntN(100) <= figchProbability
return c.Render("admin/tables", fiber.Map{
"Figch": figch,
"Title": "Verwaltung",
"Stylenames": NewStyleItemList("colors", "main", "tables"),
"NavItems": navItems,
@ -68,18 +119,20 @@ func main() {
})
app.Get("/admin/locations/overview", func(c *fiber.Ctx) error {
figch := rand.IntN(100) <= figchProbability
locations, err := conn.QueryLocations()
if err != nil {
return err
}
table := ToTable[*Location](locations, TableColumns{
"ID",
"Übergeordneter Ort",
"Name",
"Ort",
"Beschreibung",
})
return c.Render("admin/overview", fiber.Map{
"Figch": figch,
"Title": "Verwaltung",
"Stylenames": NewStyleItemList("colors", "main", "overview"),
"NavItems": navItems,
@ -91,6 +144,8 @@ func main() {
})
app.Get("/admin/containers/overview", func(c *fiber.Ctx) error {
figch := rand.IntN(100) <= figchProbability
containers, err := conn.QueryContainers()
if err != nil {
return err
@ -101,6 +156,7 @@ func main() {
})
return c.Render("admin/overview", fiber.Map{
"Figch": figch,
"Title": "Verwaltung",
"Stylenames": NewStyleItemList("colors", "main", "overview"),
"NavItems": navItems,
@ -112,6 +168,8 @@ func main() {
})
app.Get("/admin/parts/overview", func(c *fiber.Ctx) error {
figch := rand.IntN(100) <= figchProbability
parts, err := conn.QueryParts()
if err != nil {
return err
@ -125,6 +183,7 @@ func main() {
})
return c.Render("admin/overview", fiber.Map{
"Figch": figch,
"Title": "Verwaltung",
"Stylenames": NewStyleItemList("colors", "main", "overview"),
"NavItems": navItems,

25
misc.go
View file

@ -1,5 +1,9 @@
package main
import (
"unicode"
)
func ToTable[T DatabaseType](rows []T, columns TableColumns) Table {
tableRows := make([]TableRow, len(rows))
@ -12,3 +16,24 @@ func ToTable[T DatabaseType](rows []T, columns TableColumns) Table {
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 {
display: flex;
justify-content: center;
align-items: center;
align-items: start;
padding-top: 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 {
border-collapse: collapse;
width: 1200px;
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);
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 {
display: flex;
align-items: baseline;
justify-content: space-around;
justify-content: start;
gap: .1em;
}
table tr td:last-child form {
@ -35,4 +71,5 @@ table tr td:last-child form button {
border: none;
cursor: pointer;
color: var(--fg);
font-size: 1em;
}

View file

@ -6,12 +6,12 @@ main {
gap: 50px;
}
main > form {
main form {
display: flex;
margin-bottom: 50px;
}
main > form > input {
main form > input {
width: 420px;
height: 50px;
border-radius: 0px;
@ -25,11 +25,11 @@ main > form > input {
border-right: 2px solid var(--bg-border);
}
main > form > input:focus {
main form > input:focus {
outline: none;
}
main > form > button {
main form > button {
width: 50px;
height: 50px;
border: none;
@ -51,4 +51,14 @@ main > table tr :is(td, th) {
border: 2px solid var(--bg-border);
padding: 5px;
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
Name sql.NullString
Description sql.NullString
Connection *Connection
}
func (l *Location) ToTableRow() TableRow {
columns := make(TableColumns, 4)
columns := make(TableColumns, 3)
tr := TableRow{}
var path string
if l.Id.Valid {
columns[0] = fmt.Sprintf("%d", l.Id.Int64)
tr.Id = int(l.Id.Int64)
path, _ = l.Connection.QueryLocationFullPath(l.Id.Int64)
} else {
columns[0] = "NULL"
tr.Id = -1
}
if l.Parent.Valid {
columns[1] = fmt.Sprintf("%d", l.Parent.Int64)
} else {
columns[1] = "NULL"
}
if l.Name.Valid {
columns[2] = l.Name.String
columns[1] = path
if l.Description.Valid {
columns[2] = l.Description.String
} else {
columns[2] = "NULL"
}
if l.Description.Valid {
columns[3] = l.Description.String
} else {
columns[3] = "NULL"
}
tr.Columns = columns

View file

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

View file

@ -3,14 +3,14 @@
<head>
<meta charset="UTF-8">
<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 }}
<link rel="stylesheet" href="/css/{{ . }}.css">
{{ end }}
</head>
<body>
<header>
<h1>fisch</h1>
<h1>{{ if .Figch }}figch{{ else }}fisch{{ end }}</h1>
<nav>
{{ range .NavItems }}
<a href="{{ .Destination }}" {{ if eq .Destination $.ActivePage }} class="active" {{ end }}>{{ .Caption }}</a>

View file

@ -1,10 +1,24 @@
{{template "partials/base-top" .}}
{{ if .Notification }}
<div class="form-with-notification">
<p class="notification">{{ .Notification }}</p>
{{ end }}
<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>
</form>
{{ if .Notification }}
</divdiv>
{{ end }}
{{ if eq .SearchResultCount -1 }}
@ -13,16 +27,18 @@
{{ if gt .SearchResultCount 0 }}
<table>
<tr>
<th>Ort</th>
<th>Kistenbezeichnung</th>
<th>Möglicher Inhalt</th>
</tr>
<tr>
<td>A</td>
<td>A</td>
<td>A</td>
</tr>
<tr>
{{ range .Columns }}
<th>{{ . }}</th>
{{ end }}
</tr>
{{ range .Rows }}
<tr>
{{ range .Columns }}
<td>{{ . }}</td>
{{ end }}
</tr>
{{ end }}
</table>
{{ else }}