Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 13 additions & 51 deletions cmd/web/handlers:equipment.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,6 @@ func (app *application) getEquipment(w http.ResponseWriter, r *http.Request) *ht
return httperr.BadRequest(err)
}

cats, err := app.services.equipmentcategories.List(ctx, id)
if err != nil {
return httperr.InternalServerError(err)
}

qs := r.URL.Query()
query := qs.Get("q")
category := qs.Get("category")
Expand All @@ -105,25 +100,25 @@ func (app *application) getEquipment(w http.ResponseWriter, r *http.Request) *ht
page = 1
}

f := pagination.Filters{
Page: page,
PageSize: 25,
}

items, meta, err := app.services.equipment.List(ctx, equipment.ListParams{
OrgID: id,
Query: query,
Category: category,
Filters: f,
Filters: pagination.Filters{Page: page, PageSize: 25},
})
if err != nil {
return httperr.InternalServerError(err)
}

app.resolveEquipmentURLs(items)

data := app.html.TemplateData(r)
data.Data = equipmentData{
cats, err := app.services.equipmentcategories.List(ctx, id)
if err != nil {
return httperr.InternalServerError(err)
}

tmpl := app.html.TemplateData(r)
tmpl.Data = equipmentData{
OrgID: id,
Categories: cats,
Inventories: items,
Expand All @@ -135,46 +130,13 @@ func (app *application) getEquipment(w http.ResponseWriter, r *http.Request) *ht
PrintURL: template.URL(equipmentPrintURL(id, query, category, sort)), // #nosec G203
Pagination: meta,
}
return app.html.Render(w, r, http.StatusOK, pages.Equipment, data)
}

func (app *application) getEquipmentSearchFragment(w http.ResponseWriter, r *http.Request) *httperr.Error {
ctx := r.Context()
id, err := uid.Parse(r.PathValue("org_id"))
if err != nil {
return httperr.BadRequest(err)
}

qs := r.URL.Query()
query := qs.Get("q")
category := qs.Get("category")

f := pagination.Filters{
Page: 1,
PageSize: 25,
}

items, _, err := app.services.equipment.List(ctx, equipment.ListParams{
OrgID: id,
Query: query,
Category: category,
Filters: f,
})
if err != nil {
return httperr.InternalServerError(err)
}

app.resolveEquipmentURLs(items)

data := app.html.TemplateData(r)
data.Data = equipmentData{
OrgID: id,
Inventories: items,
Filtered: query != "" || category != "",
Query: query,
Category: category,
// HTMX live-search: return only the results fragment so the page URL
// update (hx-push-url) reflects the current filters without a full reload.
if htmx.IsRequest(r) {
return app.html.RenderFragment(w, r, http.StatusOK, fragments.EquipmentSearch, tmpl)
}
return app.html.RenderFragment(w, r, http.StatusOK, fragments.EquipmentSearch, data)
return app.html.Render(w, r, http.StatusOK, pages.Equipment, tmpl)
}

type equipmentItemData struct {
Expand Down
2 changes: 1 addition & 1 deletion cmd/web/handlers:login.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func (app *application) redirectAfterLogin(ctx context.Context, w http.ResponseW
http.Redirect(w, r, "/orgs/"+orgs[0].ID+"/equipment", http.StatusSeeOther) //nolint:gosec
return
}
http.Redirect(w, r, "/settings/organizations", http.StatusSeeOther)
http.Redirect(w, r, "/orgs/pick", http.StatusSeeOther)
}

func (app *application) getSignIn(w http.ResponseWriter, r *http.Request) *httperr.Error {
Expand Down
18 changes: 18 additions & 0 deletions cmd/web/handlers:orgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,24 @@ func (app *application) getOrgs(w http.ResponseWriter, r *http.Request) *httperr
return app.html.Render(w, r, http.StatusOK, pages.SettingsOrganizations, data)
}

func (app *application) getOrgPicker(w http.ResponseWriter, r *http.Request) *httperr.Error {
ctx := r.Context()
session := sessions.MustFromRequest(r)

allOrgs, err := app.services.orgs.List(ctx, session.AccountID)
if err != nil {
return httperr.InternalServerError(err)
}

data := app.html.TemplateData(r)
data.Data = orgsData{
Orgs: allOrgs,
Max: app.options.Limits.MaxOrgs,
}

return app.html.Render(w, r, http.StatusOK, pages.OrgPicker, data)
}

func (app *application) getOrgsNew(w http.ResponseWriter, r *http.Request) *httperr.Error {
data := app.html.TemplateData(r)
data.Form = orgs.NewForm()
Expand Down
50 changes: 50 additions & 0 deletions cmd/web/handlers:settings:account.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ package main

import (
"context"
"fmt"
"net/http"
"strings"
"time"

"github.com/bit8bytes/gearberg/internal/accounts"
"github.com/bit8bytes/gearberg/internal/httperr"
"github.com/bit8bytes/gearberg/internal/sessions"
"github.com/bit8bytes/gearberg/internal/templates/fragments"
"github.com/bit8bytes/gearberg/internal/templates/pages"
"github.com/bit8bytes/gearberg/pkg/htmx"
)
Expand Down Expand Up @@ -50,6 +53,53 @@ func (app *application) getAccount(w http.ResponseWriter, r *http.Request) *http
return app.html.Render(w, r, http.StatusOK, pages.SettingsAccount, tmplData)
}

type accountHeaderData struct {
Email string
Initials string
OrgName string
}

func accountInitials(email string) string {
local, _, _ := strings.Cut(email, "@")
runes := []rune(local)
switch len(runes) {
case 0:
return "?"
case 1:
return strings.ToUpper(string(runes[0]))
default:
return strings.ToUpper(string(runes[0]) + string(runes[1]))
}
}

func (app *application) getAccountHeaderFragment(w http.ResponseWriter, r *http.Request) *httperr.Error {
if !htmx.IsRequest(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return nil
}

session := sessions.MustFromRequest(r)
record, err := app.services.accounts.Get(r.Context(), session.AccountID)
if err != nil {
return httperr.InternalServerError(fmt.Errorf("getAccountHeaderFragment: %w", err))
}

data := accountHeaderData{
Email: record.Email,
Initials: accountInitials(record.Email),
}

if orgID := r.URL.Query().Get("org_id"); orgID != "" {
if org, err := app.services.orgs.Get(r.Context(), orgID); err == nil {
data.OrgName = org.DisplayName
}
}

tmplData := app.html.TemplateData(r)
tmplData.Data = data
return app.html.RenderFragment(w, r, http.StatusOK, fragments.AccountHeader, tmplData)
}

func (app *application) deleteAccount(w http.ResponseWriter, r *http.Request) *httperr.Error {
ctx := r.Context()

Expand Down
7 changes: 5 additions & 2 deletions cmd/web/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func (app *application) routes() (http.Handler, error) {
mux.HandleFunc("/", app.html.Handle(app.getNotFound))
mux.Handle("GET /forbidden", app.html.Handle(app.getForbidden))

mux.Handle("GET /{$}", http.RedirectHandler("/settings/organizations", http.StatusSeeOther))
mux.Handle("GET /{$}", http.RedirectHandler("/orgs/pick", http.StatusSeeOther))
mux.Handle("GET /dist/", assets.ServeStaticFiles())
mux.Handle("GET /favicon.ico", http.RedirectHandler("/dist/images/favicon.ico", http.StatusMovedPermanently))

Expand Down Expand Up @@ -71,6 +71,7 @@ func (app *application) routes() (http.Handler, error) {
mux.Handle("GET /orgs", app.withLogin(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/settings/organizations", http.StatusSeeOther)
})))
mux.Handle("GET /orgs/pick", app.withLogin(app.html.Handle(app.getOrgPicker)))
mux.Handle("GET /orgs/new", app.withLogin(app.html.Handle(app.getOrgsNew)))
mux.Handle("POST /orgs/new", app.withLogin(app.html.Handle(app.postOrgsNew)))

Expand Down Expand Up @@ -106,13 +107,15 @@ func (app *application) routes() (http.Handler, error) {
mux.Handle("GET /orgs/{org_id}/equipment/{id}/content", app.withLogin(app.withPermission(app.html.Handle(app.getEquipmentContent))))
mux.Handle("POST /orgs/{org_id}/equipment/{id}/content", app.withLogin(app.withPermission(app.html.Handle(app.postEquipmentAssignContent))))
mux.Handle("POST /orgs/{org_id}/equipment/{id}/content/{content_id}/delete", app.withLogin(app.withPermission(app.html.Handle(app.postEquipmentRemoveContent))))
mux.Handle("GET /orgs/{org_id}/equipment/search", app.withLogin(app.withPermission(app.html.Handle(app.getEquipmentSearchFragment))))
mux.Handle("GET /orgs/{org_id}/equipment/{id}/part-of", app.withLogin(app.withPermission(app.html.Handle(app.getEquipmentPartOfFragment))))
mux.Handle("GET /orgs/{org_id}/currency", app.withLogin(app.withPermission(app.html.Handle(app.getOrgCurrencyFragment))))
mux.Handle("GET /orgs/{org_id}/equipment-categories", app.withLogin(app.withPermission(app.html.Handle(app.getEquipmentCategoriesFragment))))
mux.Handle("GET /orgs/{org_id}/equipment-manufacturers", app.withLogin(app.withPermission(app.html.Handle(app.getEquipmentManufacturersFragment))))
mux.Handle("GET /orgs/{org_id}/warehouse-locations", app.withLogin(app.withPermission(app.html.Handle(app.getEquipmentLocationsFragment))))

// Account fragments
mux.Handle("GET /account/header", app.withLogin(app.html.Handle(app.getAccountHeaderFragment)))

// Account Settings
mux.Handle("GET /settings/account", app.withLogin(app.html.Handle(app.getAccount)))
mux.Handle("DELETE /settings/account", app.withLogin(app.html.Handle(app.deleteAccount)))
Expand Down
39 changes: 29 additions & 10 deletions internal/assets/dist/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
--color-white: #fff;
--spacing: 0.25rem;
--container-xs: 20rem;
--container-sm: 24rem;
--container-md: 28rem;
--container-lg: 32rem;
--container-xl: 36rem;
Expand Down Expand Up @@ -5681,9 +5682,6 @@
gap: calc(0.25rem * 2);
}
}
.mt-0\.5 {
margin-top: calc(var(--spacing) * 0.5);
}
.mt-1 {
margin-top: calc(var(--spacing) * 1);
}
Expand Down Expand Up @@ -5817,6 +5815,9 @@
.ml-2 {
margin-left: calc(var(--spacing) * 2);
}
.ml-auto {
margin-left: auto;
}
.carousel-item {
@layer daisyui.l1.l2.l3 {
box-sizing: content-box;
Expand Down Expand Up @@ -6513,6 +6514,10 @@
width: calc(var(--spacing) * 4);
height: calc(var(--spacing) * 4);
}
.size-5 {
width: calc(var(--spacing) * 5);
height: calc(var(--spacing) * 5);
}
.size-6 {
width: calc(var(--spacing) * 6);
height: calc(var(--spacing) * 6);
Expand Down Expand Up @@ -6747,6 +6752,9 @@
.w-6 {
width: calc(var(--spacing) * 6);
}
.w-10 {
width: calc(var(--spacing) * 10);
}
.w-14 {
width: calc(var(--spacing) * 14);
}
Expand All @@ -6759,6 +6767,12 @@
.w-28 {
width: calc(var(--spacing) * 28);
}
.w-32 {
width: calc(var(--spacing) * 32);
}
.w-36 {
width: calc(var(--spacing) * 36);
}
.w-40 {
width: calc(var(--spacing) * 40);
}
Expand All @@ -6780,6 +6794,9 @@
.w-full {
width: 100%;
}
.w-sm {
width: var(--container-sm);
}
.w-xs {
width: var(--container-xs);
}
Expand Down Expand Up @@ -7097,9 +7114,6 @@
.gap-1 {
gap: calc(var(--spacing) * 1);
}
.gap-1\.5 {
gap: calc(var(--spacing) * 1.5);
}
.gap-2 {
gap: calc(var(--spacing) * 2);
}
Expand Down Expand Up @@ -10077,14 +10091,14 @@
line-height: var(--tw-leading, var(--text-3xl--line-height));
}
}
.sm\:hidden {
.sm\:flex {
@media (width >= 40rem) {
display: none;
display: flex;
}
}
.sm\:inline {
.sm\:hidden {
@media (width >= 40rem) {
display: inline;
display: none;
}
}
.sm\:w-64 {
Expand Down Expand Up @@ -10126,6 +10140,11 @@
flex-direction: row;
}
}
.sm\:items-center {
@media (width >= 40rem) {
align-items: center;
}
}
.sm\:items-start {
@media (width >= 40rem) {
align-items: flex-start;
Expand Down
19 changes: 19 additions & 0 deletions internal/assets/dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,25 @@ document.addEventListener("click", (e) => {
});
})();

// Reposition dropdown-content as position:fixed when opened so it escapes overflow:auto ancestors.
// Setting .style.* via JS is not subject to CSP style-src restrictions.
document.addEventListener("focusin", (e) => {
const btn = e.target.closest(".dropdown [tabindex][role='button']");
if (!btn) return;
const menu = btn.parentElement?.querySelector(".dropdown-content");
if (!menu) return;
const rect = btn.getBoundingClientRect();
menu.style.position = "fixed";
menu.style.top = rect.bottom + 4 + "px";
if (btn.parentElement.classList.contains("dropdown-end")) {
menu.style.left = "auto";
menu.style.right = window.innerWidth - rect.right + "px";
} else {
menu.style.right = "auto";
menu.style.left = rect.left + "px";
}
});

// Password show/hide toggle
// Trigger: <button data-pw-toggle="<input-id>">
document.addEventListener("click", (e) => {
Expand Down
2 changes: 1 addition & 1 deletion internal/database/queries/gen/equipment/equipment.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion internal/database/queries/sqlite/equipment.sql
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ WHERE e.org_id = sqlc.arg(org_id)
AND (sqlc.arg(name_query) = '' OR e.name LIKE '%' || sqlc.arg(name_query) || '%' OR EXISTS (SELECT 1 FROM equipment_serialized_items esi WHERE esi.equipment_id = e.id AND esi.serial_number LIKE '%' || sqlc.arg(name_query) || '%'))
AND (sqlc.arg(category) = '' OR ec.name = sqlc.arg(category))
AND (sqlc.arg(is_archived) = -1 OR e.is_archived = sqlc.arg(is_archived))
ORDER BY e.name ASC
ORDER BY category_name ASC, e.name ASC
LIMIT sqlc.arg(page_limit) OFFSET sqlc.arg(page_offset);

-- name: ListBySerialNumber :many
Expand Down
Loading
Loading