Add custom fields + warranty plugin, rework settings into two-pane shell
Feature work from the 2026-07 session: Settings IA - Replace the flat 27-card settings hub with a persistent two-pane shell (SettingsLayout.vue): grouped, searchable left rail + content pane. - Nest all settings/* routes under the shell via router post-processing; shared nav catalog in settingsNav.js. Group by asset class (PCs, Printers, Equipment, Network) so per-type settings stop scattering. Custom fields (core) - customfields + customfieldvalues tables (migration 7d14), CRUD API at /api/customfields, per-asset value get/save. - Settings management page + reusable CustomFieldsSection (detail) and CustomFieldsInputs (form) wired into all four asset types. Warranty (new plugin) - plugins/warranty: warranties + warrantyassets (migration 7d15), derived coverage status, provider abstraction (manual now; Dell/Lenovo/HP stubs). - API CRUD + per-asset panel + report buckets; WarrantyPanel on all four detail pages; Warranties management page; Warranty report + Reports card. - Seed warranty.* permissions. Printer drivers - printerdrivers table (migration 7d13) linked to printer models; drivers now surface on the matching printer's detail page. Other - PCDetail rebalanced (Network + Status + Warranty + custom fields on the right). - Rename PCs list "Features" column to "Remote Access"; fix badge hover underline. - Drop equipment islocationonly field. - Centralize asset-type label/route maps into utils/assetTypes.js. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,8 @@ from shopdb.utils.responses import (
|
||||
)
|
||||
from shopdb.utils.pagination import get_pagination_params, paginate_query
|
||||
|
||||
from shopdb.utils.authz import require_permission, require_role
|
||||
|
||||
assets_bp = Blueprint('assets', __name__)
|
||||
|
||||
|
||||
@@ -58,6 +60,7 @@ def get_asset_type(type_id: int):
|
||||
|
||||
@assets_bp.route('/types', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.create')
|
||||
def create_asset_type():
|
||||
"""Create a new asset type."""
|
||||
data = request.get_json()
|
||||
@@ -77,7 +80,8 @@ def create_asset_type():
|
||||
pluginname=data.get('pluginname'),
|
||||
tablename=data.get('tablename'),
|
||||
description=data.get('description'),
|
||||
icon=data.get('icon')
|
||||
icon=data.get('icon'),
|
||||
color=data.get('color')
|
||||
)
|
||||
|
||||
db.session.add(t)
|
||||
@@ -86,6 +90,23 @@ def create_asset_type():
|
||||
return success_response(t.to_dict(), message='Asset type created', http_code=201)
|
||||
|
||||
|
||||
@assets_bp.route('/types/<int:type_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.edit')
|
||||
def update_asset_type(type_id: int):
|
||||
"""Update an asset type's display fields (color/icon/description). The name,
|
||||
plugin, and table are structural and not editable here."""
|
||||
t = AssetType.query.get(type_id)
|
||||
if not t:
|
||||
return error_response(ErrorCodes.NOT_FOUND, 'Asset type not found', http_code=404)
|
||||
data = request.get_json() or {}
|
||||
for key in ('description', 'icon', 'color', 'isactive'):
|
||||
if key in data:
|
||||
setattr(t, key, data[key])
|
||||
db.session.commit()
|
||||
return success_response(t.to_dict(), message='Asset type updated')
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Asset Statuses
|
||||
# =============================================================================
|
||||
@@ -127,6 +148,7 @@ def get_asset_status(status_id: int):
|
||||
|
||||
@assets_bp.route('/statuses', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.create')
|
||||
def create_asset_status():
|
||||
"""Create a new asset status."""
|
||||
data = request.get_json()
|
||||
@@ -155,6 +177,7 @@ def create_asset_status():
|
||||
|
||||
@assets_bp.route('/statuses/<int:status_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.edit')
|
||||
def update_asset_status(status_id: int):
|
||||
"""Update an asset status."""
|
||||
s = AssetStatus.query.get(status_id)
|
||||
@@ -183,6 +206,7 @@ def update_asset_status(status_id: int):
|
||||
|
||||
@assets_bp.route('/statuses/<int:status_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.delete')
|
||||
def delete_asset_status(status_id: int):
|
||||
"""Delete an asset status. Refused if any asset still uses it."""
|
||||
s = AssetStatus.query.get(status_id)
|
||||
@@ -215,12 +239,14 @@ def list_relationship_types():
|
||||
return success_response([{
|
||||
'relationshiptypeid': t.relationshiptypeid,
|
||||
'relationshiptype': t.relationshiptype,
|
||||
'description': t.description
|
||||
'description': t.description,
|
||||
'color': t.color
|
||||
} for t in types])
|
||||
|
||||
|
||||
@assets_bp.route('/relationshiptypes', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.create')
|
||||
def create_relationship_type():
|
||||
"""Create a new asset relationship type."""
|
||||
data = request.get_json()
|
||||
@@ -236,16 +262,59 @@ def create_relationship_type():
|
||||
|
||||
rel_type = RelationshipType(
|
||||
relationshiptype=data['relationshiptype'],
|
||||
description=data.get('description')
|
||||
description=data.get('description'),
|
||||
color=data.get('color')
|
||||
)
|
||||
db.session.add(rel_type)
|
||||
db.session.commit()
|
||||
|
||||
return success_response({
|
||||
'relationshiptypeid': rel_type.relationshiptypeid,
|
||||
'relationshiptype': rel_type.relationshiptype,
|
||||
'description': rel_type.description
|
||||
}, message='Relationship type created', http_code=201)
|
||||
return success_response(_rel_type_dict(rel_type), message='Relationship type created', http_code=201)
|
||||
|
||||
|
||||
def _rel_type_dict(t):
|
||||
return {
|
||||
'relationshiptypeid': t.relationshiptypeid,
|
||||
'relationshiptype': t.relationshiptype,
|
||||
'description': t.description,
|
||||
'color': t.color,
|
||||
}
|
||||
|
||||
|
||||
@assets_bp.route('/relationshiptypes/<int:type_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.edit')
|
||||
def update_relationship_type(type_id: int):
|
||||
"""Update a relationship type."""
|
||||
t = RelationshipType.query.get(type_id)
|
||||
if not t:
|
||||
return error_response(ErrorCodes.NOT_FOUND, 'Relationship type not found', http_code=404)
|
||||
data = request.get_json() or {}
|
||||
if 'relationshiptype' in data and data['relationshiptype'] != t.relationshiptype:
|
||||
if RelationshipType.query.filter_by(relationshiptype=data['relationshiptype']).first():
|
||||
return error_response(ErrorCodes.CONFLICT,
|
||||
f"Relationship type '{data['relationshiptype']}' already exists", http_code=409)
|
||||
for key in ('relationshiptype', 'description', 'color'):
|
||||
if key in data:
|
||||
setattr(t, key, data[key])
|
||||
db.session.commit()
|
||||
return success_response(_rel_type_dict(t), message='Relationship type updated')
|
||||
|
||||
|
||||
@assets_bp.route('/relationshiptypes/<int:type_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.delete')
|
||||
def delete_relationship_type(type_id: int):
|
||||
"""Delete a relationship type. Refused if any relationship still uses it."""
|
||||
t = RelationshipType.query.get(type_id)
|
||||
if not t:
|
||||
return error_response(ErrorCodes.NOT_FOUND, 'Relationship type not found', http_code=404)
|
||||
inuse = AssetRelationship.query.filter_by(relationshiptypeid=type_id).count()
|
||||
if inuse:
|
||||
return error_response(ErrorCodes.CONFLICT,
|
||||
f"Cannot delete: {inuse} relationship(s) still use this type", http_code=409)
|
||||
db.session.delete(t)
|
||||
db.session.commit()
|
||||
return success_response(message='Relationship type deleted')
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -358,6 +427,7 @@ def get_asset(asset_id: int):
|
||||
|
||||
@assets_bp.route('', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.create')
|
||||
def create_asset():
|
||||
"""Create a new asset."""
|
||||
data = request.get_json()
|
||||
@@ -407,6 +477,7 @@ def create_asset():
|
||||
|
||||
@assets_bp.route('/<int:asset_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.edit')
|
||||
def update_asset(asset_id: int):
|
||||
"""Update an asset."""
|
||||
asset = Asset.query.get(asset_id)
|
||||
@@ -447,6 +518,7 @@ def update_asset(asset_id: int):
|
||||
|
||||
@assets_bp.route('/<int:asset_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.delete')
|
||||
def delete_asset(asset_id: int):
|
||||
"""Delete (soft delete) an asset."""
|
||||
asset = Asset.query.get(asset_id)
|
||||
@@ -537,6 +609,7 @@ def get_asset_relationships(asset_id: int):
|
||||
|
||||
@assets_bp.route('/relationships', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.create')
|
||||
def create_asset_relationship():
|
||||
"""Create a relationship between two assets."""
|
||||
data = request.get_json()
|
||||
@@ -591,6 +664,7 @@ def create_asset_relationship():
|
||||
|
||||
@assets_bp.route('/relationships/<int:rel_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.delete')
|
||||
def delete_asset_relationship(rel_id: int):
|
||||
"""Delete an asset relationship."""
|
||||
rel = AssetRelationship.query.get(rel_id)
|
||||
@@ -837,7 +911,7 @@ def get_assets_map():
|
||||
|
||||
# Get filter options - these are small reference tables, no N+1 concern
|
||||
asset_types = AssetType.query.filter(AssetType.isactive == True).all()
|
||||
types_data = [{'assettypeid': t.assettypeid, 'assettype': t.assettype, 'icon': t.icon} for t in asset_types]
|
||||
types_data = [{'assettypeid': t.assettypeid, 'assettype': t.assettype, 'icon': t.icon, 'color': t.color} for t in asset_types]
|
||||
|
||||
statuses = AssetStatus.query.filter(AssetStatus.isactive == True).all()
|
||||
status_data = [{'statusid': s.statusid, 'status': s.status, 'color': s.color} for s in statuses]
|
||||
@@ -854,28 +928,28 @@ def get_assets_map():
|
||||
try:
|
||||
from plugins.equipment.models import EquipmentType
|
||||
equipment_types = EquipmentType.query.filter(EquipmentType.isactive == True).order_by(EquipmentType.equipmenttype).all()
|
||||
subtypes['Equipment'] = [{'id': et.equipmenttypeid, 'name': et.equipmenttype} for et in equipment_types]
|
||||
subtypes['Equipment'] = [{'id': et.equipmenttypeid, 'name': et.equipmenttype, 'color': et.color} for et in equipment_types]
|
||||
except ImportError:
|
||||
subtypes['Equipment'] = []
|
||||
|
||||
try:
|
||||
from plugins.computers.models import ComputerType
|
||||
computer_types = ComputerType.query.filter(ComputerType.isactive == True).order_by(ComputerType.computertype).all()
|
||||
subtypes['Computer'] = [{'id': ct.computertypeid, 'name': ct.computertype} for ct in computer_types]
|
||||
subtypes['Computer'] = [{'id': ct.computertypeid, 'name': ct.computertype, 'color': ct.color} for ct in computer_types]
|
||||
except ImportError:
|
||||
subtypes['Computer'] = []
|
||||
|
||||
try:
|
||||
from plugins.network.models import NetworkDeviceType
|
||||
net_types = NetworkDeviceType.query.filter(NetworkDeviceType.isactive == True).order_by(NetworkDeviceType.networkdevicetype).all()
|
||||
subtypes['Network Device'] = [{'id': nt.networkdevicetypeid, 'name': nt.networkdevicetype} for nt in net_types]
|
||||
subtypes['Network Device'] = [{'id': nt.networkdevicetypeid, 'name': nt.networkdevicetype, 'color': nt.color} for nt in net_types]
|
||||
except ImportError:
|
||||
subtypes['Network Device'] = []
|
||||
|
||||
try:
|
||||
from plugins.printers.models import PrinterType
|
||||
printer_types = PrinterType.query.filter(PrinterType.isactive == True).order_by(PrinterType.printertype).all()
|
||||
subtypes['Printer'] = [{'id': pt.printertypeid, 'name': pt.printertype} for pt in printer_types]
|
||||
subtypes['Printer'] = [{'id': pt.printertypeid, 'name': pt.printertype, 'color': pt.color} for pt in printer_types]
|
||||
except ImportError:
|
||||
subtypes['Printer'] = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user