"""Equipment plugin API endpoints.""" from flask import Blueprint, request from flask_jwt_extended import jwt_required from shopdb.extensions import db from shopdb.core.models import Asset, AssetType, Vendor, Model, AuditLog from shopdb.utils.responses import ( success_response, error_response, paginated_response, ErrorCodes ) from shopdb.utils.pagination import get_pagination_params, paginate_query from ..models import Equipment, EquipmentType equipment_bp = Blueprint('equipment', __name__) # ============================================================================= # Equipment Types # ============================================================================= @equipment_bp.route('/types', methods=['GET']) @jwt_required(optional=True) def list_equipment_types(): """List all equipment types.""" page, per_page = get_pagination_params(request) query = EquipmentType.query if request.args.get('active', 'true').lower() != 'false': query = query.filter(EquipmentType.isactive == True) if search := request.args.get('search'): query = query.filter(EquipmentType.equipmenttype.ilike(f'%{search}%')) query = query.order_by(EquipmentType.equipmenttype) items, total = paginate_query(query, page, per_page) data = [t.to_dict() for t in items] return paginated_response(data, page, per_page, total) @equipment_bp.route('/types/', methods=['GET']) @jwt_required(optional=True) def get_equipment_type(type_id: int): """Get a single equipment type.""" t = EquipmentType.query.get(type_id) if not t: return error_response( ErrorCodes.NOT_FOUND, f'Equipment type with ID {type_id} not found', http_code=404 ) return success_response(t.to_dict()) @equipment_bp.route('/types', methods=['POST']) @jwt_required() def create_equipment_type(): """Create a new equipment type.""" data = request.get_json() if not data or not data.get('equipmenttype'): return error_response(ErrorCodes.VALIDATION_ERROR, 'equipmenttype is required') if EquipmentType.query.filter_by(equipmenttype=data['equipmenttype']).first(): return error_response( ErrorCodes.CONFLICT, f"Equipment type '{data['equipmenttype']}' already exists", http_code=409 ) t = EquipmentType( equipmenttype=data['equipmenttype'], description=data.get('description'), icon=data.get('icon') ) db.session.add(t) db.session.commit() return success_response(t.to_dict(), message='Equipment type created', http_code=201) @equipment_bp.route('/types/', methods=['PUT']) @jwt_required() def update_equipment_type(type_id: int): """Update an equipment type.""" t = EquipmentType.query.get(type_id) if not t: return error_response( ErrorCodes.NOT_FOUND, f'Equipment type with ID {type_id} not found', http_code=404 ) data = request.get_json() if not data: return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided') if 'equipmenttype' in data and data['equipmenttype'] != t.equipmenttype: if EquipmentType.query.filter_by(equipmenttype=data['equipmenttype']).first(): return error_response( ErrorCodes.CONFLICT, f"Equipment type '{data['equipmenttype']}' already exists", http_code=409 ) for key in ['equipmenttype', 'description', 'icon', 'isactive']: if key in data: setattr(t, key, data[key]) db.session.commit() return success_response(t.to_dict(), message='Equipment type updated') # ============================================================================= # Equipment CRUD # ============================================================================= @equipment_bp.route('', methods=['GET']) @jwt_required(optional=True) def list_equipment(): """ List all equipment with filtering and pagination. Query parameters: - page, per_page: Pagination - active: Filter by active status - search: Search by asset number or name - type_id: Filter by equipment type ID - vendor_id: Filter by vendor ID - location_id: Filter by location ID - businessunit_id: Filter by business unit ID """ page, per_page = get_pagination_params(request) # Join Equipment with Asset query = db.session.query(Equipment).join(Asset) # Active filter if request.args.get('active', 'true').lower() != 'false': query = query.filter(Asset.isactive == True) # Search filter if search := request.args.get('search'): query = query.filter( db.or_( Asset.assetnumber.ilike(f'%{search}%'), Asset.name.ilike(f'%{search}%'), Asset.serialnumber.ilike(f'%{search}%') ) ) # Equipment type filter if type_id := request.args.get('type_id'): query = query.filter(Equipment.equipmenttypeid == int(type_id)) # Vendor filter if vendor_id := request.args.get('vendor_id'): query = query.filter(Equipment.vendorid == int(vendor_id)) # Location filter if location_id := request.args.get('location_id'): query = query.filter(Asset.locationid == int(location_id)) # Business unit filter if bu_id := request.args.get('businessunit_id'): query = query.filter(Asset.businessunitid == int(bu_id)) # Sorting sort_by = request.args.get('sort', 'assetnumber') sort_dir = request.args.get('dir', 'asc') if sort_by == 'assetnumber': col = Asset.assetnumber elif sort_by == 'name': col = Asset.name else: col = Asset.assetnumber query = query.order_by(col.desc() if sort_dir == 'desc' else col) items, total = paginate_query(query, page, per_page) # Build response with both asset and equipment data data = [] for equip in items: item = equip.asset.to_dict() if equip.asset else {} item['equipment'] = equip.to_dict() data.append(item) return paginated_response(data, page, per_page, total) @equipment_bp.route('/', methods=['GET']) @jwt_required(optional=True) def get_equipment(equipment_id: int): """Get a single equipment item with full details.""" equip = Equipment.query.get(equipment_id) if not equip: return error_response( ErrorCodes.NOT_FOUND, f'Equipment with ID {equipment_id} not found', http_code=404 ) result = equip.asset.to_dict() if equip.asset else {} result['equipment'] = equip.to_dict() return success_response(result) @equipment_bp.route('/by-asset/', methods=['GET']) @jwt_required(optional=True) def get_equipment_by_asset(asset_id: int): """Get equipment data by asset ID.""" equip = Equipment.query.filter_by(assetid=asset_id).first() if not equip: return error_response( ErrorCodes.NOT_FOUND, f'Equipment for asset {asset_id} not found', http_code=404 ) result = equip.asset.to_dict() if equip.asset else {} result['equipment'] = equip.to_dict() return success_response(result) @equipment_bp.route('', methods=['POST']) @jwt_required() def create_equipment(): """ Create new equipment (creates both Asset and Equipment records). Required fields: - assetnumber: Business identifier Optional fields: - name, serialnumber, statusid, locationid, businessunitid - equipmenttypeid, vendorid, modelnumberid - requiresmanualconfig, islocationonly - mapleft, maptop, notes """ data = request.get_json() if not data: return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided') if not data.get('assetnumber'): return error_response(ErrorCodes.VALIDATION_ERROR, 'assetnumber is required') # Check for duplicate assetnumber if Asset.query.filter_by(assetnumber=data['assetnumber']).first(): return error_response( ErrorCodes.CONFLICT, f"Asset with number '{data['assetnumber']}' already exists", http_code=409 ) # Get equipment asset type equipment_type = AssetType.query.filter_by(assettype='equipment').first() if not equipment_type: return error_response( ErrorCodes.INTERNAL_ERROR, 'Equipment asset type not found. Plugin may not be properly installed.', http_code=500 ) # Create the core asset asset = Asset( assetnumber=data['assetnumber'], name=data.get('name'), serialnumber=data.get('serialnumber'), assettypeid=equipment_type.assettypeid, statusid=data.get('statusid', 1), locationid=data.get('locationid'), businessunitid=data.get('businessunitid'), mapleft=data.get('mapleft'), maptop=data.get('maptop'), notes=data.get('notes') ) db.session.add(asset) db.session.flush() # Get the assetid # Create the equipment extension equip = Equipment( assetid=asset.assetid, equipmenttypeid=data.get('equipmenttypeid'), vendorid=data.get('vendorid'), modelnumberid=data.get('modelnumberid'), requiresmanualconfig=data.get('requiresmanualconfig', False), islocationonly=data.get('islocationonly', False), lastmaintenancedate=data.get('lastmaintenancedate'), nextmaintenancedate=data.get('nextmaintenancedate'), maintenanceintervaldays=data.get('maintenanceintervaldays'), controllervendorid=data.get('controllervendorid'), controllermodelid=data.get('controllermodelid') ) db.session.add(equip) db.session.flush() # Audit log AuditLog.log('created', 'Equipment', entityid=equip.equipmentid, entityname=data['assetnumber']) db.session.commit() result = asset.to_dict() result['equipment'] = equip.to_dict() return success_response(result, message='Equipment created', http_code=201) @equipment_bp.route('/', methods=['PUT']) @jwt_required() def update_equipment(equipment_id: int): """Update equipment (both Asset and Equipment records).""" equip = Equipment.query.get(equipment_id) if not equip: return error_response( ErrorCodes.NOT_FOUND, f'Equipment with ID {equipment_id} not found', http_code=404 ) data = request.get_json() if not data: return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided') asset = equip.asset # Check for conflicting assetnumber if 'assetnumber' in data and data['assetnumber'] != asset.assetnumber: if Asset.query.filter_by(assetnumber=data['assetnumber']).first(): return error_response( ErrorCodes.CONFLICT, f"Asset with number '{data['assetnumber']}' already exists", http_code=409 ) # Track changes for audit log changes = {} # Update asset fields asset_fields = ['assetnumber', 'name', 'serialnumber', 'statusid', 'locationid', 'businessunitid', 'mapleft', 'maptop', 'notes', 'isactive'] for key in asset_fields: if key in data: old_val = getattr(asset, key) new_val = data[key] if old_val != new_val: changes[key] = {'old': old_val, 'new': new_val} setattr(asset, key, data[key]) # Update equipment fields equipment_fields = ['equipmenttypeid', 'vendorid', 'modelnumberid', 'requiresmanualconfig', 'islocationonly', 'lastmaintenancedate', 'nextmaintenancedate', 'maintenanceintervaldays', 'controllervendorid', 'controllermodelid'] for key in equipment_fields: if key in data: old_val = getattr(equip, key) new_val = data[key] if old_val != new_val: changes[key] = {'old': old_val, 'new': new_val} setattr(equip, key, data[key]) # Audit log if there were changes if changes: AuditLog.log('updated', 'Equipment', entityid=equip.equipmentid, entityname=asset.assetnumber, changes=changes) db.session.commit() result = asset.to_dict() result['equipment'] = equip.to_dict() return success_response(result, message='Equipment updated') @equipment_bp.route('/', methods=['DELETE']) @jwt_required() def delete_equipment(equipment_id: int): """Delete (soft delete) equipment.""" equip = Equipment.query.get(equipment_id) if not equip: return error_response( ErrorCodes.NOT_FOUND, f'Equipment with ID {equipment_id} not found', http_code=404 ) # Soft delete the asset (equipment extension will stay linked) equip.asset.isactive = False # Audit log AuditLog.log('deleted', 'Equipment', entityid=equip.equipmentid, entityname=equip.asset.assetnumber) db.session.commit() return success_response(message='Equipment deleted') # ============================================================================= # Dashboard # ============================================================================= @equipment_bp.route('/dashboard/summary', methods=['GET']) @jwt_required(optional=True) def dashboard_summary(): """Get equipment dashboard summary data.""" # Total active equipment count total = db.session.query(Equipment).join(Asset).filter( Asset.isactive == True ).count() # Count by equipment type by_type = db.session.query( EquipmentType.equipmenttype, db.func.count(Equipment.equipmentid) ).join(Equipment, Equipment.equipmenttypeid == EquipmentType.equipmenttypeid ).join(Asset, Asset.assetid == Equipment.assetid ).filter(Asset.isactive == True ).group_by(EquipmentType.equipmenttype ).all() # Count by status from shopdb.core.models import AssetStatus by_status = db.session.query( AssetStatus.status, db.func.count(Equipment.equipmentid) ).join(Asset, Asset.assetid == Equipment.assetid ).join(AssetStatus, AssetStatus.statusid == Asset.statusid ).filter(Asset.isactive == True ).group_by(AssetStatus.status ).all() return success_response({ 'total': total, 'bytype': [{'type': t, 'count': c} for t, c in by_type], 'bystatus': [{'status': s, 'count': c} for s, c in by_status] })