Files
vibetree/internal/models/models.go
T
Owen fb85c9edbb 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.
2026-08-17 16:28:14 -05:00

193 lines
4.4 KiB
Go

// Package models defines the core domain types shared across inventree.
package models
import "time"
type Role string
const (
RoleAdmin Role = "admin"
RoleUser Role = "user"
// RoleContact is an assignable person (end user) with no ability to
// sign in - useful for tracking who a checked-out asset belongs to
// without needing to create a login account for everyone in a
// building.
RoleContact Role = "contact"
)
type User struct {
ID int64
Username string
PasswordHash string
Role Role
Enabled bool
CreatedAt time.Time
UpdatedAt time.Time
}
func (u User) IsAdmin() bool { return u.Role == RoleAdmin }
// CanLogin reports whether this account is allowed to sign in at all;
// contacts exist only to be assigned assets.
func (u User) CanLogin() bool { return u.Role == RoleAdmin || u.Role == RoleUser }
type Folder struct {
ID int64
Name string
ParentID *int64
Description string
Icon string
SortOrder int
CreatedAt time.Time
UpdatedAt time.Time
// Populated by list/tree queries; not persisted directly on this table.
FolderCount int
AssetCount int
}
type Status struct {
ID int64
Name string
Color string
SortOrder int
CreatedAt time.Time
UpdatedAt time.Time
}
type FieldType string
const (
FieldTypeText FieldType = "text"
FieldTypeLongText FieldType = "long_text"
FieldTypeNumber FieldType = "number"
FieldTypeDate FieldType = "date"
FieldTypeBoolean FieldType = "boolean"
FieldTypeDropdown FieldType = "dropdown"
FieldTypeURL FieldType = "url"
FieldTypeIPAddress FieldType = "ip_address"
)
var AllFieldTypes = []FieldType{
FieldTypeText, FieldTypeLongText, FieldTypeNumber, FieldTypeDate,
FieldTypeBoolean, FieldTypeDropdown, FieldTypeURL, FieldTypeIPAddress,
}
func (t FieldType) Label() string {
switch t {
case FieldTypeText:
return "Text"
case FieldTypeLongText:
return "Long Text"
case FieldTypeNumber:
return "Number"
case FieldTypeDate:
return "Date"
case FieldTypeBoolean:
return "Boolean"
case FieldTypeDropdown:
return "Dropdown"
case FieldTypeURL:
return "URL"
case FieldTypeIPAddress:
return "IP Address"
}
return string(t)
}
type ItemType struct {
ID int64
Name string
Description string
Icon string
Enabled bool
SortOrder int
CreatedAt time.Time
UpdatedAt time.Time
// IsUnique means each asset of this type is its own distinct item
// (a laptop with its own serial number, tracked as one row per unit).
// When false, assets of this type are bulk/consumable line items
// (e.g. "USB-A Cable") tracked by Quantity instead of one row each.
IsUnique bool
Fields []CustomFieldDef
}
type CustomFieldDef struct {
ID int64
ItemTypeID int64
Name string
FieldKey string
FieldType FieldType
Required bool
DefaultValue string
Options []string
SortOrder int
CreatedAt time.Time
UpdatedAt time.Time
}
type CustomFieldValue struct {
ID int64
AssetID int64
FieldDefID int64
Value string
}
type Asset struct {
ID int64
AssetTag string
Name string
ItemTypeID int64
FolderID *int64
StatusID int64
AssignedUserID *int64
Notes string
// Quantity is meaningful only when the asset's item type is not
// IsUnique; unique-type assets are always kept at 1.
Quantity int
CreatedAt time.Time
UpdatedAt time.Time
// Populated by joined list/detail queries.
ItemTypeName string
ItemTypeIcon string
ItemTypeIsUnique bool
StatusName string
StatusColor string
AssignedUsername string
FolderPath []BreadcrumbItem
CustomFields []AssetFieldValue
}
type AssetFieldValue struct {
Def CustomFieldDef
Value string
}
type BreadcrumbItem struct {
ID int64
Name string
}
// TreeNode is a folder-or-asset node used to render the inventory tree.
type TreeNode struct {
Type string // "folder" or "asset"
ID int64
Name string
Icon string
ParentID *int64
HasChildren bool
AssetTag string
StatusName string
StatusColor string
// Populated only when the node is pre-expanded for server-side
// recursive rendering (see web.buildTree); empty otherwise, in which
// case the client lazy-loads children via /app/tree/children.
Expanded bool
Children []TreeNode
MoreAssets int // count of additional assets beyond the tree display cap, if any
}