Add asset checkout, quantity/uniqueness, contacts, and QR labels
Checkout:
- assets.AssignUser + POST /app/assets/{id}/checkout assign/unassign an
asset's user independent of the full edit form. New shared
#checkoutModal, wired from a "Check Out..." context-menu action, backed
by GET /app/assets/{id} and GET /app/users/options JSON endpoints.
Quantity / uniqueness:
- Item types gain an IsUnique flag (checked by default, matching prior
behavior). Assets gain a Quantity column, fixed at 1 for unique types
and adjustable via +/- steppers (POST /app/assets/{id}/quantity) for
non-unique ones. Flipping a type back to unique clamps existing assets'
quantity back to 1. CSV import/export gained a Quantity column.
Migration 0003_quantity.sql.
Contacts:
- New models.RoleContact: an assignable person who cannot sign in
(User.CanLogin). Migration 0004_contacts.sql rebuilds the users table
(SQLite can't alter a CHECK constraint in place) to allow the role.
Users form/list updated; contact creation auto-generates an unusable
password since the login path is blocked by role regardless.
QR asset tags and printable labels:
- Each asset's QR code (via github.com/skip2/go-qrcode, pure Go) encodes
an absolute /scan/<tag> URL; GET /scan/{tag} resolves it and redirects
into /inventory?asset=<id>, which app.js picks up on load to
auto-select that asset - works from a phone camera or a USB
scanner-as-keyboard-wedge pointed at the address bar.
- GET /labels renders a print-ready sheet (right-click an asset for one
label, a folder for every asset in it) with a template switcher between
Avery 5160 (verified dimensions) and 61526 asset tag labels (best-effort
layout, flagged as such on the page).
Also refreshed README to reflect the right-click-first tree UX and all of
the above.
This commit is contained in:
@@ -13,6 +13,12 @@ type InventoryPageData struct {
|
||||
Tree []models.TreeNode
|
||||
ItemTypes []models.ItemType
|
||||
Statuses []models.Status
|
||||
|
||||
// AutoOpenAssetID and ScanError come from a redirect out of
|
||||
// /scan/<tag> (a QR/barcode label scan): either the asset to select
|
||||
// on load, or the tag that didn't match anything.
|
||||
AutoOpenAssetID string
|
||||
ScanError string
|
||||
}
|
||||
|
||||
func (a *App) handleInventoryPage(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -42,6 +48,7 @@ func (a *App) handleInventoryPage(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
a.Render(w, http.StatusOK, "inventory", InventoryPageData{
|
||||
User: u, Tree: tree, ItemTypes: itemTypes, Statuses: statusList,
|
||||
AutoOpenAssetID: r.URL.Query().Get("asset"), ScanError: r.URL.Query().Get("scan_error"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -208,14 +215,16 @@ func (a *App) handlePanelAsset(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// AssetFormData backs both the "new asset" and "edit asset" forms.
|
||||
type AssetFormData struct {
|
||||
Asset *models.Asset // nil when creating
|
||||
FolderID *int64
|
||||
ItemTypes []models.ItemType
|
||||
Statuses []models.Status
|
||||
Users []models.User
|
||||
FolderOptions []FolderOption
|
||||
SelectedIT int64
|
||||
Fields []models.AssetFieldValue
|
||||
Asset *models.Asset // nil when creating
|
||||
FolderID *int64
|
||||
ItemTypes []models.ItemType
|
||||
Statuses []models.Status
|
||||
Users []models.User
|
||||
FolderOptions []FolderOption
|
||||
SelectedIT int64
|
||||
SelectedITUnique bool
|
||||
Quantity int
|
||||
Fields []models.AssetFieldValue
|
||||
}
|
||||
|
||||
func (a *App) handlePanelAssetNew(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -247,15 +256,17 @@ func (a *App) handlePanelAssetNew(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var selectedIT int64
|
||||
var selectedITUnique bool
|
||||
var fields []models.AssetFieldValue
|
||||
if len(itemTypes) > 0 {
|
||||
selectedIT = itemTypes[0].ID
|
||||
selectedITUnique = itemTypes[0].IsUnique
|
||||
fields = fieldValuesForCreate(itemTypes[0].Fields)
|
||||
}
|
||||
|
||||
a.RenderFragment(w, http.StatusOK, "asset_form", AssetFormData{
|
||||
FolderID: folderID, ItemTypes: itemTypes, Statuses: statusList, Users: userList, FolderOptions: folderOpts,
|
||||
SelectedIT: selectedIT, Fields: fields,
|
||||
SelectedIT: selectedIT, SelectedITUnique: selectedITUnique, Quantity: 1, Fields: fields,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -294,13 +305,23 @@ func (a *App) handlePanelAssetEdit(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
a.RenderFragment(w, http.StatusOK, "asset_form", AssetFormData{
|
||||
Asset: asset, ItemTypes: itemTypes, Statuses: statusList, Users: userList, FolderOptions: folderOpts,
|
||||
SelectedIT: asset.ItemTypeID, Fields: asset.CustomFields,
|
||||
SelectedIT: asset.ItemTypeID, SelectedITUnique: asset.ItemTypeIsUnique, Quantity: asset.Quantity, Fields: asset.CustomFields,
|
||||
})
|
||||
}
|
||||
|
||||
// handleAssetFieldsFragment renders just the dynamic custom-field inputs for
|
||||
// a chosen item type, used when the item type dropdown changes on the
|
||||
// "new asset" form.
|
||||
// AssetDynamicFieldsData backs the "asset_fields_only" partial: the
|
||||
// quantity input (only relevant for non-unique item types) plus the
|
||||
// type's custom fields. It's swapped in whenever the item type dropdown
|
||||
// changes on the "new asset" form, and also used for the initial render.
|
||||
type AssetDynamicFieldsData struct {
|
||||
IsUnique bool
|
||||
Quantity int
|
||||
Fields []models.AssetFieldValue
|
||||
}
|
||||
|
||||
// handleAssetFieldsFragment renders just the dynamic quantity/custom-field
|
||||
// inputs for a chosen item type, used when the item type dropdown changes
|
||||
// on the "new asset" form.
|
||||
func (a *App) handleAssetFieldsFragment(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
itemTypeID := queryInt64(r, "item_type_id")
|
||||
@@ -309,5 +330,7 @@ func (a *App) handleAssetFieldsFragment(w http.ResponseWriter, r *http.Request)
|
||||
writeErr(w, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
a.RenderFragment(w, http.StatusOK, "asset_fields_only", fieldValuesForCreate(it.Fields))
|
||||
a.RenderFragment(w, http.StatusOK, "asset_fields_only", AssetDynamicFieldsData{
|
||||
IsUnique: it.IsUnique, Quantity: 1, Fields: fieldValuesForCreate(it.Fields),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user