Initial implementation of inventree
A small IT asset inventory system built around a nested folder tree, matching the project brief: Go + html/template + Bootstrap 5 + vanilla JS, SQLite storage (WAL, FKs, migrations), embedded templates/static assets, single compiled binary. - AJAX-driven inventory tree: lazy-loaded folder expansion with per-user persisted expand state, drag-and-drop move for folders/assets, safe delete-with-relocate-or-cascade confirmation, search with breadcrumbs. - Assets: CRUD, duplicate, move, auto-generated IT-000001-style IDs, CSV import (with optional folder auto-create) and export. - Admin-defined item types with custom fields (text/long_text/number/date/ boolean/dropdown/url/ip_address). - Configurable statuses, session-based auth (bcrypt, secure cookies), admin/user roles, user management. - CLI: serve, version, database migrate, database backup; /healthz; graceful shutdown; Dockerfile/docker-compose/systemd unit; Go test suite.
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
{{define "asset_fields_only"}}
|
||||
{{if .}}
|
||||
<h6 class="mt-3 mb-2">Type-Specific Fields</h6>
|
||||
{{range .}}
|
||||
{{$fv := .}}
|
||||
<div class="mb-2">
|
||||
<label class="form-label" for="field-{{.Def.ID}}">{{.Def.Name}}{{if .Def.Required}} <span class="text-danger">*</span>{{end}}</label>
|
||||
{{if eq .Def.FieldType "long_text"}}
|
||||
<textarea class="form-control" id="field-{{.Def.ID}}" name="field_{{.Def.ID}}" rows="3" {{if .Def.Required}}required{{end}}>{{.Value}}</textarea>
|
||||
{{else if eq .Def.FieldType "boolean"}}
|
||||
<select class="form-select" id="field-{{.Def.ID}}" name="field_{{.Def.ID}}">
|
||||
<option value="false" {{if ne .Value "true"}}selected{{end}}>No</option>
|
||||
<option value="true" {{if eq .Value "true"}}selected{{end}}>Yes</option>
|
||||
</select>
|
||||
{{else if eq .Def.FieldType "dropdown"}}
|
||||
<select class="form-select" id="field-{{.Def.ID}}" name="field_{{.Def.ID}}" {{if .Def.Required}}required{{end}}>
|
||||
<option value="">—</option>
|
||||
{{range .Def.Options}}<option value="{{.}}" {{if eq . $fv.Value}}selected{{end}}>{{.}}</option>{{end}}
|
||||
</select>
|
||||
{{else if eq .Def.FieldType "date"}}
|
||||
<input type="date" class="form-control" id="field-{{.Def.ID}}" name="field_{{.Def.ID}}" value="{{.Value}}" {{if .Def.Required}}required{{end}}>
|
||||
{{else if eq .Def.FieldType "number"}}
|
||||
<input type="number" step="any" class="form-control" id="field-{{.Def.ID}}" name="field_{{.Def.ID}}" value="{{.Value}}" {{if .Def.Required}}required{{end}}>
|
||||
{{else if eq .Def.FieldType "url"}}
|
||||
<input type="url" class="form-control" id="field-{{.Def.ID}}" name="field_{{.Def.ID}}" value="{{.Value}}" {{if .Def.Required}}required{{end}}>
|
||||
{{else if eq .Def.FieldType "ip_address"}}
|
||||
<input type="text" pattern="^[0-9a-fA-F:\.]+$" class="form-control" id="field-{{.Def.ID}}" name="field_{{.Def.ID}}" value="{{.Value}}" placeholder="192.168.1.1" {{if .Def.Required}}required{{end}}>
|
||||
{{else}}
|
||||
<input type="text" class="form-control" id="field-{{.Def.ID}}" name="field_{{.Def.ID}}" value="{{.Value}}" {{if .Def.Required}}required{{end}}>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,81 @@
|
||||
{{define "asset_form"}}
|
||||
<div class="panel-content p-3">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-2">
|
||||
<li class="breadcrumb-item"><a href="#" class="breadcrumb-link" data-type="root">Inventory</a></li>
|
||||
<li class="breadcrumb-item active">{{if .Asset}}Edit {{.Asset.Name}}{{else}}New Asset{{end}}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<form id="assetForm" data-mode="{{if .Asset}}edit{{else}}create{{end}}" {{if .Asset}}data-asset-id="{{.Asset.ID}}"{{end}}>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Asset ID</label>
|
||||
<input type="text" class="form-control" name="asset_id" value="{{if .Asset}}{{.Asset.AssetTag}}{{end}}" placeholder="Auto-generated if left blank">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="name" value="{{if .Asset}}{{.Asset.Name}}{{end}}" required autofocus>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Item Type <span class="text-danger">*</span></label>
|
||||
{{if .Asset}}
|
||||
<input type="text" class="form-control" value="{{.Asset.ItemTypeName}}" disabled>
|
||||
<div class="form-text">Item type cannot be changed after creation.</div>
|
||||
{{else}}
|
||||
<select class="form-select" name="item_type_id" id="assetFormItemType" required>
|
||||
{{range .ItemTypes}}<option value="{{.ID}}" {{if eq .ID $.SelectedIT}}selected{{end}}>{{.Name}}</option>{{end}}
|
||||
</select>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Folder</label>
|
||||
{{$selectedFolder := deref .FolderID}}
|
||||
{{if .Asset}}{{$selectedFolder = deref .Asset.FolderID}}{{end}}
|
||||
<select class="form-select" name="folder_id">
|
||||
<option value="">Inventory (top level)</option>
|
||||
{{range .FolderOptions}}
|
||||
<option value="{{.ID}}" {{if eq $selectedFolder .ID}}selected{{end}}>
|
||||
{{range seq .Depth}} {{end}}{{.Path}}
|
||||
</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Status <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="status_id" required>
|
||||
{{range .Statuses}}<option value="{{.ID}}" {{if $.Asset}}{{if eq .ID $.Asset.StatusID}}selected{{end}}{{end}}>{{.Name}}</option>{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Assigned User</label>
|
||||
{{$selectedUser := 0}}
|
||||
{{if .Asset}}{{$selectedUser = deref .Asset.AssignedUserID}}{{end}}
|
||||
<select class="form-select" name="assigned_user_id">
|
||||
<option value="">Unassigned</option>
|
||||
{{range .Users}}
|
||||
<option value="{{.ID}}" {{if eq $selectedUser .ID}}selected{{end}}>{{.Username}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label">Notes</label>
|
||||
<textarea class="form-control" name="notes" rows="3">{{if .Asset}}{{.Asset.Notes}}{{end}}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-12" id="asset-fields-container">
|
||||
{{template "asset_fields_only" .Fields}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<button type="submit" class="btn btn-primary">{{if .Asset}}Save Changes{{else}}Create Asset{{end}}</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-action="cancel-asset-form" {{if .Asset}}data-id="{{.Asset.ID}}"{{end}}>Cancel</button>
|
||||
</div>
|
||||
<div class="alert alert-danger mt-3 d-none" id="assetFormError"></div>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,15 @@
|
||||
{{define "breadcrumb"}}
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item"><a href="#" class="breadcrumb-link" data-type="root">Inventory</a></li>
|
||||
{{$last := sub (len .) 1}}
|
||||
{{range $i, $b := .}}
|
||||
{{if eq $i $last}}
|
||||
<li class="breadcrumb-item active" aria-current="page">{{$b.Name}}</li>
|
||||
{{else}}
|
||||
<li class="breadcrumb-item"><a href="#" class="breadcrumb-link" data-type="folder" data-id="{{$b.ID}}">{{$b.Name}}</a></li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</ol>
|
||||
</nav>
|
||||
{{end}}
|
||||
@@ -0,0 +1,112 @@
|
||||
{{define "modals"}}
|
||||
{{if .User.IsAdmin}}
|
||||
<!-- Create / rename folder -->
|
||||
<div class="modal fade" id="folderModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form id="folderForm">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="folderModalTitle">New Folder</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="folder_id" id="folderFormId">
|
||||
<input type="hidden" name="parent_id" id="folderFormParentId">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" class="form-control" name="name" id="folderFormName" required autofocus>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" name="description" id="folderFormDescription" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="row g-2">
|
||||
<div class="col">
|
||||
<label class="form-label">Icon <span class="text-body-secondary">(Bootstrap Icons class, optional)</span></label>
|
||||
<input type="text" class="form-control" name="icon" id="folderFormIcon" placeholder="bi-folder-fill">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label class="form-label">Sort Order</label>
|
||||
<input type="number" class="form-control" name="sort_order" id="folderFormSortOrder" value="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="invalid-feedback d-block d-none" id="folderFormError"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save Folder</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete folder confirmation -->
|
||||
<div class="modal fade" id="deleteFolderModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-exclamation-triangle-fill text-danger"></i> Delete Folder</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Delete <strong id="deleteFolderName"></strong>?</p>
|
||||
<div id="deleteFolderEmptyNotice" class="d-none">
|
||||
<p class="text-body-secondary">This folder is empty and can be deleted safely.</p>
|
||||
</div>
|
||||
<div id="deleteFolderNotEmptyNotice" class="d-none">
|
||||
<div class="alert alert-warning py-2">
|
||||
This folder contains <span id="deleteFolderCounts"></span>. Choose what to do with its contents.
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="delete_mode" id="modeRelocate" value="relocate" checked>
|
||||
<label class="form-check-label" for="modeRelocate">Move contents to…</label>
|
||||
</div>
|
||||
<select class="form-select mt-1 mb-2" id="deleteFolderDestination"></select>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="delete_mode" id="modeCascade" value="cascade">
|
||||
<label class="form-check-label text-danger" for="modeCascade">Permanently delete everything inside</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" id="confirmDeleteFolderBtn">Delete Folder</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import CSV -->
|
||||
<div class="modal fade" id="importModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form id="importForm">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Import Assets from CSV</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">CSV File</label>
|
||||
<input type="file" class="form-control" name="file" id="importFile" accept=".csv" required>
|
||||
</div>
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" name="create_missing_folders" id="importCreateFolders" value="1" checked>
|
||||
<label class="form-check-label" for="importCreateFolders">Create missing folders referenced in the Folder column</label>
|
||||
</div>
|
||||
<p class="text-body-secondary small mb-0">
|
||||
Columns: Asset ID, Name, Item Type, Folder, Status, Assigned User, Notes, plus <code>cf_<field_key></code> for custom fields.
|
||||
</p>
|
||||
<div id="importResult" class="mt-3"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="submit" class="btn btn-primary" id="importSubmitBtn">Import</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,104 @@
|
||||
{{define "panel_asset"}}
|
||||
<div class="panel-content p-3" data-asset-id="{{.Asset.ID}}">
|
||||
<div class="d-flex align-items-start mb-2">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item"><a href="#" class="breadcrumb-link" data-type="root">Inventory</a></li>
|
||||
{{range .Asset.FolderPath}}
|
||||
<li class="breadcrumb-item"><a href="#" class="breadcrumb-link" data-type="folder" data-id="{{.ID}}">{{.Name}}</a></li>
|
||||
{{end}}
|
||||
<li class="breadcrumb-item active" aria-current="page">{{.Asset.Name}}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
{{if .CanEdit}}
|
||||
<div class="ms-auto d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-secondary" data-action="edit-asset" data-id="{{.Asset.ID}}"><i class="bi bi-pencil"></i> Edit</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" data-action="move-asset" data-bs-toggle="modal" data-bs-target="#moveAssetModal-{{.Asset.ID}}"><i class="bi bi-arrows-move"></i> Move</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" data-action="duplicate-asset" data-id="{{.Asset.ID}}"><i class="bi bi-files"></i> Duplicate</button>
|
||||
<button class="btn btn-sm btn-outline-danger" data-action="delete-asset" data-id="{{.Asset.ID}}" data-name="{{.Asset.Name}}"><i class="bi bi-trash"></i> Delete</button>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-2 mb-3">
|
||||
<i class="bi {{iconOr .Asset.ItemTypeIcon "bi-box-seam"}} fs-3 text-body-secondary"></i>
|
||||
<div>
|
||||
<h4 class="mb-0">{{.Asset.Name}}</h4>
|
||||
<div class="text-body-secondary">{{.Asset.AssetTag}} · {{.Asset.ItemTypeName}}</div>
|
||||
</div>
|
||||
<span class="badge text-bg-{{.Asset.StatusColor}} ms-2 fs-6">{{.Asset.StatusName}}</span>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header py-2 fw-semibold">Details</div>
|
||||
<table class="table table-sm mb-0">
|
||||
<tbody>
|
||||
<tr><th class="w-25">Assigned User</th><td>{{if .Asset.AssignedUsername}}{{.Asset.AssignedUsername}}{{else}}<span class="text-body-secondary">Unassigned</span>{{end}}</td></tr>
|
||||
<tr><th>Created</th><td>{{fmtDate .Asset.CreatedAt}}</td></tr>
|
||||
<tr><th>Updated</th><td>{{fmtDate .Asset.UpdatedAt}}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{if .Asset.Notes}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header py-2 fw-semibold">Notes</div>
|
||||
<div class="card-body py-2" style="white-space:pre-wrap">{{.Asset.Notes}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{{if .Asset.CustomFields}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header py-2 fw-semibold">{{.Asset.ItemTypeName}} Fields</div>
|
||||
<table class="table table-sm mb-0">
|
||||
<tbody>
|
||||
{{range .Asset.CustomFields}}
|
||||
<tr>
|
||||
<th class="w-40">{{.Def.Name}}</th>
|
||||
<td>
|
||||
{{if eq .Def.FieldType "boolean"}}
|
||||
{{if eq .Value "true"}}<i class="bi bi-check-circle-fill text-success"></i>{{else}}<i class="bi bi-x-circle text-body-secondary"></i>{{end}}
|
||||
{{else if eq .Def.FieldType "url"}}
|
||||
{{if .Value}}<a href="{{.Value}}" target="_blank" rel="noopener">{{.Value}}</a>{{end}}
|
||||
{{else}}
|
||||
{{if .Value}}{{.Value}}{{else}}<span class="text-body-secondary">—</span>{{end}}
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .CanEdit}}
|
||||
<div class="modal fade" id="moveAssetModal-{{.Asset.ID}}" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Move "{{.Asset.Name}}"</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label class="form-label">Destination Folder</label>
|
||||
<select class="form-select" id="moveAssetDestination-{{.Asset.ID}}">
|
||||
<option value="">Inventory (top level)</option>
|
||||
{{range .FolderOptions}}
|
||||
<option value="{{.ID}}">{{range seq .Depth}} {{end}}{{.Path}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" data-action="confirm-move-asset" data-id="{{.Asset.ID}}" data-select="moveAssetDestination-{{.Asset.ID}}">Move</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,80 @@
|
||||
{{define "panel_folder"}}
|
||||
<div class="panel-content p-3" data-folder-id="{{.Folder.ID}}">
|
||||
<div class="d-flex align-items-start mb-2">
|
||||
{{template "breadcrumb" .Breadcrumb}}
|
||||
<div class="ms-auto d-flex gap-2 admin-only">
|
||||
<button class="btn btn-sm btn-outline-secondary" data-action="new-subfolder" data-id="{{.Folder.ID}}"><i class="bi bi-folder-plus"></i> Subfolder</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" data-action="rename-folder" data-id="{{.Folder.ID}}" data-name="{{.Folder.Name}}" data-description="{{.Folder.Description}}" data-icon="{{.Folder.Icon}}" data-sort-order="{{.Folder.SortOrder}}"><i class="bi bi-pencil"></i> Rename</button>
|
||||
<button class="btn btn-sm btn-outline-danger" data-action="delete-folder" data-id="{{.Folder.ID}}"><i class="bi bi-trash"></i> Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mb-1"><i class="bi {{iconOr .Folder.Icon "bi-folder-fill"}} text-warning-emphasis me-1"></i>{{.Folder.Name}}</h4>
|
||||
{{if .Folder.Description}}<p class="text-body-secondary">{{.Folder.Description}}</p>{{end}}
|
||||
|
||||
{{if .Subfolders}}
|
||||
<h6 class="mt-3 mb-2">Subfolders</h6>
|
||||
<div class="list-group mb-3">
|
||||
{{range .Subfolders}}
|
||||
<a href="#" class="list-group-item list-group-item-action folder-link d-flex align-items-center" data-id="{{.ID}}">
|
||||
<i class="bi {{iconOr .Icon "bi-folder-fill"}} text-warning-emphasis me-2"></i>
|
||||
{{.Name}}
|
||||
<span class="ms-auto text-body-secondary small">{{.FolderCount}} folder(s), {{.AssetCount}} asset(s)</span>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="d-flex align-items-center flex-wrap gap-2 mt-3 mb-2">
|
||||
<h6 class="mb-0 me-2">Assets</h6>
|
||||
<form class="d-flex gap-2 flex-wrap align-items-center panel-filter-form" data-folder-id="{{.Folder.ID}}">
|
||||
<select class="form-select form-select-sm" name="item_type_id" style="width:auto">
|
||||
<option value="">All Types</option>
|
||||
{{range .ItemTypes}}<option value="{{.ID}}" {{if eq .ID $.FilterType}}selected{{end}}>{{.Name}}</option>{{end}}
|
||||
</select>
|
||||
<select class="form-select form-select-sm" name="status_id" style="width:auto">
|
||||
<option value="">All Statuses</option>
|
||||
{{range .Statuses}}<option value="{{.ID}}" {{if eq .ID $.FilterStat}}selected{{end}}>{{.Name}}</option>{{end}}
|
||||
</select>
|
||||
<input type="search" class="form-control form-control-sm" name="q" value="{{.Query}}" placeholder="Filter assets…" style="width:160px">
|
||||
</form>
|
||||
<div class="ms-auto d-flex gap-2">
|
||||
<a class="btn btn-sm btn-outline-secondary" href="/export/assets.csv?folder_id={{.Folder.ID}}&recursive=1"><i class="bi bi-download"></i> Export</a>
|
||||
<button class="btn btn-sm btn-primary admin-only" data-action="new-asset" data-folder-id="{{.Folder.ID}}"><i class="bi bi-plus-lg"></i> Add Asset</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .Assets}}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover asset-table">
|
||||
<thead><tr><th>Asset ID</th><th>Name</th><th>Type</th><th>Status</th><th>Assigned</th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Assets}}
|
||||
<tr class="asset-row" data-id="{{.ID}}">
|
||||
<td>{{.AssetTag}}</td>
|
||||
<td><i class="bi {{iconOr .ItemTypeIcon "bi-box-seam"}} me-1"></i>{{.Name}}</td>
|
||||
<td>{{.ItemTypeName}}</td>
|
||||
<td><span class="badge text-bg-{{.StatusColor}}">{{.StatusName}}</span></td>
|
||||
<td>{{if .AssignedUsername}}{{.AssignedUsername}}{{else}}<span class="text-body-secondary">—</span>{{end}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{$pages := ceilDiv .Total .PageSize}}
|
||||
{{if gt $pages 1}}
|
||||
<nav>
|
||||
<ul class="pagination pagination-sm">
|
||||
{{range $i := seqFrom1 $pages}}
|
||||
<li class="page-item {{if eq $i $.Page}}active{{end}}">
|
||||
<a class="page-link panel-page-link" href="#" data-folder-id="{{$.Folder.ID}}" data-page="{{$i}}">{{$i}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</nav>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<p class="text-body-secondary">No assets in this folder yet.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,109 @@
|
||||
{{define "panel_root"}}
|
||||
<div class="panel-content p-3">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-2">
|
||||
<li class="breadcrumb-item active" aria-current="page">Inventory</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="card stat-card">
|
||||
<div class="card-body py-2">
|
||||
<div class="stat-value">{{.Stats.Total}}</div>
|
||||
<div class="stat-label">Total Assets</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{range .Stats.ByStatus}}
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="card stat-card">
|
||||
<div class="card-body py-2">
|
||||
<div class="stat-value">{{.Count}}</div>
|
||||
<div class="stat-label"><span class="badge text-bg-{{.Color}}">{{.Name}}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header py-2 fw-semibold">Assets by Type</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
{{range .Stats.ByType}}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center py-1">
|
||||
<span><i class="bi {{iconOr .Icon "bi-box-seam"}} me-2"></i>{{.Name}}</span>
|
||||
<span class="badge text-bg-secondary rounded-pill">{{.Count}}</span>
|
||||
</li>
|
||||
{{else}}
|
||||
<li class="list-group-item text-body-secondary py-2">No assets yet.</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header py-2 fw-semibold">Recently Added</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
{{range .Stats.RecentlyAdded}}
|
||||
<li class="list-group-item py-1">
|
||||
<a href="#" class="asset-link text-decoration-none" data-id="{{.ID}}">
|
||||
<i class="bi {{iconOr .ItemTypeIcon "bi-box-seam"}} me-2"></i>{{.Name}}
|
||||
<span class="text-body-secondary small">{{.AssetTag}}</span>
|
||||
</a>
|
||||
</li>
|
||||
{{else}}
|
||||
<li class="list-group-item text-body-secondary py-2">No assets yet.</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<h6 class="mb-0">Top-Level Folders</h6>
|
||||
<div class="ms-auto d-flex gap-2">
|
||||
<a class="btn btn-sm btn-outline-secondary" href="/export/assets.csv"><i class="bi bi-download"></i> Export CSV</a>
|
||||
<button class="btn btn-sm btn-outline-secondary admin-only" data-bs-toggle="modal" data-bs-target="#importModal"><i class="bi bi-upload"></i> Import CSV</button>
|
||||
<button class="btn btn-sm btn-primary admin-only" id="btn-new-folder-root"><i class="bi bi-folder-plus"></i> New Folder</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .Folders}}
|
||||
<div class="list-group mb-3">
|
||||
{{range .Folders}}
|
||||
<a href="#" class="list-group-item list-group-item-action folder-link d-flex align-items-center" data-id="{{.ID}}">
|
||||
<i class="bi {{iconOr .Icon "bi-folder-fill"}} text-warning-emphasis me-2"></i>
|
||||
{{.Name}}
|
||||
<span class="text-body-secondary small ms-2">{{if .Description}}{{.Description}}{{end}}</span>
|
||||
<span class="ms-auto text-body-secondary small">{{.FolderCount}} folder(s), {{.AssetCount}} asset(s)</span>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="text-body-secondary">No folders yet. {{if true}}<span class="admin-only">Create one to start organizing your inventory.</span>{{end}}</p>
|
||||
{{end}}
|
||||
|
||||
{{if .UnfiledAssets}}
|
||||
<h6 class="mb-2">Unfiled Assets</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover asset-table">
|
||||
<thead><tr><th>Asset ID</th><th>Name</th><th>Type</th><th>Status</th><th>Assigned</th></tr></thead>
|
||||
<tbody>
|
||||
{{range .UnfiledAssets}}
|
||||
<tr class="asset-row" data-id="{{.ID}}">
|
||||
<td>{{.AssetTag}}</td>
|
||||
<td><i class="bi {{iconOr .ItemTypeIcon "bi-box-seam"}} me-1"></i>{{.Name}}</td>
|
||||
<td>{{.ItemTypeName}}</td>
|
||||
<td><span class="badge text-bg-{{.StatusColor}}">{{.StatusName}}</span></td>
|
||||
<td>{{if .AssignedUsername}}{{.AssignedUsername}}{{else}}<span class="text-body-secondary">—</span>{{end}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,33 @@
|
||||
{{define "panel_search"}}
|
||||
<div class="panel-content p-3">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-2">
|
||||
<li class="breadcrumb-item"><a href="#" class="breadcrumb-link" data-type="root">Inventory</a></li>
|
||||
<li class="breadcrumb-item active">Search: "{{.Query}}"</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<h5 class="mb-3">{{len .Results}} result(s) for "{{.Query}}"</h5>
|
||||
|
||||
{{if .Results}}
|
||||
<div class="list-group">
|
||||
{{range .Results}}
|
||||
<a href="#" class="list-group-item list-group-item-action asset-link" data-id="{{.ID}}">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi {{iconOr .ItemTypeIcon "bi-box-seam"}} me-2 fs-5 text-body-secondary"></i>
|
||||
<div>
|
||||
<div class="fw-semibold">{{.AssetTag}} — {{.Name}}</div>
|
||||
<div class="text-body-secondary small">
|
||||
Inventory{{range .FolderPath}} / {{.Name}}{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge text-bg-{{.StatusColor}} ms-auto">{{.StatusName}}</span>
|
||||
</div>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="text-body-secondary">No matches found.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,44 @@
|
||||
{{define "tree_children"}}
|
||||
{{range .}}
|
||||
{{if eq .Type "folder"}}
|
||||
<li class="tree-node" data-type="folder" data-id="{{.ID}}" draggable="true">
|
||||
<div class="tree-row" data-type="folder" data-id="{{.ID}}" tabindex="0">
|
||||
{{if .HasChildren}}
|
||||
<button type="button" class="tree-toggle{{if .Expanded}} expanded{{end}}" data-id="{{.ID}}" aria-label="Toggle">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</button>
|
||||
{{else}}
|
||||
<span class="tree-toggle-spacer"></span>
|
||||
{{end}}
|
||||
<i class="bi {{iconOr .Icon "bi-folder-fill"}} tree-icon text-warning-emphasis"></i>
|
||||
<span class="tree-label">{{.Name}}</span>
|
||||
<span class="tree-actions admin-only">
|
||||
<button type="button" class="btn-tree-action" data-action="menu" data-id="{{.ID}}" data-type="folder" title="Actions">
|
||||
<i class="bi bi-three-dots"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<ul class="tree-children list-unstyled{{if not .Expanded}} d-none{{end}}" data-expanded="{{.Expanded}}" data-loaded="{{.Expanded}}">
|
||||
{{if .Expanded}}{{template "tree_children" .Children}}{{end}}
|
||||
</ul>
|
||||
</li>
|
||||
{{else if eq .Type "asset"}}
|
||||
<li class="tree-node" data-type="asset" data-id="{{.ID}}" draggable="true">
|
||||
<div class="tree-row" data-type="asset" data-id="{{.ID}}" tabindex="0">
|
||||
<span class="tree-toggle-spacer"></span>
|
||||
<i class="bi {{iconOr .Icon "bi-box-seam"}} tree-icon text-body-secondary"></i>
|
||||
<span class="tree-label">{{.Name}}</span>
|
||||
<span class="badge rounded-pill text-bg-{{.StatusColor}} tree-status-badge">{{.StatusName}}</span>
|
||||
</div>
|
||||
</li>
|
||||
{{else}}
|
||||
<li class="tree-node tree-node-more">
|
||||
<div class="tree-row tree-row-more text-body-secondary fst-italic">
|
||||
<span class="tree-toggle-spacer"></span>
|
||||
<i class="bi bi-three-dots"></i>
|
||||
<span class="tree-label">{{.Name}}</span>
|
||||
</div>
|
||||
</li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user