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:
@@ -58,7 +58,7 @@ func validFieldType(t models.FieldType) bool {
|
||||
// List returns all item types, optionally including disabled ones, each with
|
||||
// its custom field definitions loaded.
|
||||
func (s *Store) List(ctx context.Context, includeDisabled bool) ([]models.ItemType, error) {
|
||||
q := `SELECT id, name, description, icon, enabled, sort_order, created_at, updated_at FROM item_types`
|
||||
q := `SELECT id, name, description, icon, enabled, sort_order, is_unique, created_at, updated_at FROM item_types`
|
||||
if !includeDisabled {
|
||||
q += ` WHERE enabled = 1`
|
||||
}
|
||||
@@ -72,7 +72,7 @@ func (s *Store) List(ctx context.Context, includeDisabled bool) ([]models.ItemTy
|
||||
for rows.Next() {
|
||||
var it models.ItemType
|
||||
var created, updated string
|
||||
if err := rows.Scan(&it.ID, &it.Name, &it.Description, &it.Icon, &it.Enabled, &it.SortOrder, &created, &updated); err != nil {
|
||||
if err := rows.Scan(&it.ID, &it.Name, &it.Description, &it.Icon, &it.Enabled, &it.SortOrder, &it.IsUnique, &created, &updated); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
it.CreatedAt = parseTime(created)
|
||||
@@ -93,10 +93,10 @@ func (s *Store) List(ctx context.Context, includeDisabled bool) ([]models.ItemTy
|
||||
}
|
||||
|
||||
func (s *Store) Get(ctx context.Context, id int64) (*models.ItemType, error) {
|
||||
row := s.db.QueryRowContext(ctx, `SELECT id, name, description, icon, enabled, sort_order, created_at, updated_at FROM item_types WHERE id = ?`, id)
|
||||
row := s.db.QueryRowContext(ctx, `SELECT id, name, description, icon, enabled, sort_order, is_unique, created_at, updated_at FROM item_types WHERE id = ?`, id)
|
||||
var it models.ItemType
|
||||
var created, updated string
|
||||
err := row.Scan(&it.ID, &it.Name, &it.Description, &it.Icon, &it.Enabled, &it.SortOrder, &created, &updated)
|
||||
err := row.Scan(&it.ID, &it.Name, &it.Description, &it.Icon, &it.Enabled, &it.SortOrder, &it.IsUnique, &created, &updated)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
@@ -146,8 +146,8 @@ func (s *Store) Create(ctx context.Context, it *models.ItemType) error {
|
||||
it.Icon = "bi-box-seam"
|
||||
}
|
||||
res, err := s.db.ExecContext(ctx,
|
||||
`INSERT INTO item_types (name, description, icon, enabled, sort_order) VALUES (?, ?, ?, ?, ?)`,
|
||||
it.Name, it.Description, it.Icon, it.Enabled, it.SortOrder)
|
||||
`INSERT INTO item_types (name, description, icon, enabled, sort_order, is_unique) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
it.Name, it.Description, it.Icon, it.Enabled, it.SortOrder, it.IsUnique)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -164,8 +164,17 @@ func (s *Store) Update(ctx context.Context, it *models.ItemType) error {
|
||||
return ErrNameRequired
|
||||
}
|
||||
_, err := s.db.ExecContext(ctx,
|
||||
`UPDATE item_types SET name = ?, description = ?, icon = ?, enabled = ?, sort_order = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?`,
|
||||
it.Name, it.Description, it.Icon, it.Enabled, it.SortOrder, it.ID)
|
||||
`UPDATE item_types SET name = ?, description = ?, icon = ?, enabled = ?, sort_order = ?, is_unique = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?`,
|
||||
it.Name, it.Description, it.Icon, it.Enabled, it.SortOrder, it.IsUnique, it.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if it.IsUnique {
|
||||
// Item type just became (or remained) unique: bring every asset
|
||||
// of this type back to quantity 1 so the two concepts can't drift
|
||||
// out of sync.
|
||||
_, err = s.db.ExecContext(ctx, `UPDATE assets SET quantity = 1 WHERE item_type_id = ? AND quantity != 1`, it.ID)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user