Files
shopdb-flask/plugins/usb/api/routes.py
cproudlock 9c220a4194 Add USB, Notifications, Network plugins and reusable EmployeeSearch component
New Plugins:
- USB plugin: Device checkout/checkin with employee lookup, checkout history
- Notifications plugin: Announcements with types, scheduling, shopfloor display
- Network plugin: Network device management with subnets and VLANs
- Equipment and Computers plugins: Asset type separation

Frontend:
- EmployeeSearch component: Reusable employee lookup with autocomplete
- USB views: List, detail, checkout/checkin modals
- Notifications views: List, form with recognition mode
- Network views: Device list, detail, form
- Calendar view with FullCalendar integration
- Shopfloor and TV dashboard views
- Reports index page
- Map editor for asset positioning
- Light/dark mode fixes for map tooltips

Backend:
- Employee search API with external lookup service
- Collector API for PowerShell data collection
- Reports API endpoints
- Slides API for TV dashboard
- Fixed AppVersion model (removed BaseModel inheritance)
- Added checkout_name column to usbcheckouts table

Styling:
- Unified detail page styles
- Improved pagination (page numbers instead of prev/next)
- Dark/light mode theme improvements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 16:37:49 -05:00

276 lines
8.3 KiB
Python

"""USB plugin API endpoints."""
from flask import Blueprint, request
from flask_jwt_extended import jwt_required
from datetime import datetime
from shopdb.extensions import db
from shopdb.core.models import Machine, MachineType, Vendor, Model
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 USBCheckout
usb_bp = Blueprint('usb', __name__)
def get_usb_machinetype_id():
"""Get the USB Device machine type ID dynamically."""
usb_type = MachineType.query.filter(
MachineType.machinetype.ilike('%usb%')
).first()
return usb_type.machinetypeid if usb_type else None
@usb_bp.route('', methods=['GET'])
@jwt_required()
def list_usb_devices():
"""
List all USB devices with checkout status.
Query parameters:
- page, per_page: Pagination
- search: Search by serial number or alias
- available: Filter to only available (not checked out) devices
"""
page, per_page = get_pagination_params(request)
usb_type_id = get_usb_machinetype_id()
if not usb_type_id:
return success_response([]) # No USB type found
# Get USB devices from machines table
query = db.session.query(Machine).filter(
Machine.machinetypeid == usb_type_id,
Machine.isactive == True
)
# Search filter
if search := request.args.get('search'):
query = query.filter(
db.or_(
Machine.serialnumber.ilike(f'%{search}%'),
Machine.alias.ilike(f'%{search}%'),
Machine.machinenumber.ilike(f'%{search}%')
)
)
query = query.order_by(Machine.alias)
items, total = paginate_query(query, page, per_page)
# Build response with checkout status
data = []
for device in items:
# Check if currently checked out
active_checkout = USBCheckout.query.filter_by(
machineid=device.machineid,
checkin_time=None
).first()
item = {
'machineid': device.machineid,
'machinenumber': device.machinenumber,
'alias': device.alias,
'serialnumber': device.serialnumber,
'notes': device.notes,
'vendor_name': device.vendor.vendorname if device.vendor else None,
'model_name': device.model.modelnumber if device.model else None,
'is_checked_out': active_checkout is not None,
'current_checkout': active_checkout.to_dict() if active_checkout else None
}
data.append(item)
# Filter by availability if requested
if request.args.get('available', '').lower() == 'true':
data = [d for d in data if not d['is_checked_out']]
total = len(data)
return paginated_response(data, page, per_page, total)
@usb_bp.route('/<int:device_id>', methods=['GET'])
@jwt_required()
def get_usb_device(device_id: int):
"""Get a single USB device with checkout history."""
device = Machine.query.filter_by(
machineid=device_id,
machinetypeid=get_usb_machinetype_id()
).first()
if not device:
return error_response(
ErrorCodes.NOT_FOUND,
f'USB device with ID {device_id} not found',
http_code=404
)
# Get checkout history
checkouts = USBCheckout.query.filter_by(
machineid=device_id
).order_by(USBCheckout.checkout_time.desc()).limit(50).all()
# Check current checkout
active_checkout = next((c for c in checkouts if c.checkin_time is None), None)
result = {
'machineid': device.machineid,
'machinenumber': device.machinenumber,
'alias': device.alias,
'serialnumber': device.serialnumber,
'notes': device.notes,
'vendor_name': device.vendor.vendorname if device.vendor else None,
'model_name': device.model.modelnumber if device.model else None,
'is_checked_out': active_checkout is not None,
'current_checkout': active_checkout.to_dict() if active_checkout else None,
'checkout_history': [c.to_dict() for c in checkouts]
}
return success_response(result)
@usb_bp.route('/<int:device_id>/checkout', methods=['POST'])
@jwt_required()
def checkout_device(device_id: int):
"""Check out a USB device."""
device = Machine.query.filter_by(
machineid=device_id,
machinetypeid=get_usb_machinetype_id()
).first()
if not device:
return error_response(
ErrorCodes.NOT_FOUND,
f'USB device with ID {device_id} not found',
http_code=404
)
# Check if already checked out
active_checkout = USBCheckout.query.filter_by(
machineid=device_id,
checkin_time=None
).first()
if active_checkout:
return error_response(
ErrorCodes.CONFLICT,
f'Device is already checked out by {active_checkout.sso}',
http_code=409
)
data = request.get_json() or {}
if not data.get('sso'):
return error_response(ErrorCodes.VALIDATION_ERROR, 'sso is required')
checkout = USBCheckout(
machineid=device_id,
sso=data['sso'],
checkout_name=data.get('name'),
checkout_reason=data.get('reason'),
checkout_time=datetime.utcnow()
)
db.session.add(checkout)
db.session.commit()
return success_response(checkout.to_dict(), message='Device checked out', http_code=201)
@usb_bp.route('/<int:device_id>/checkin', methods=['POST'])
@jwt_required()
def checkin_device(device_id: int):
"""Check in a USB device."""
device = Machine.query.filter_by(
machineid=device_id,
machinetypeid=get_usb_machinetype_id()
).first()
if not device:
return error_response(
ErrorCodes.NOT_FOUND,
f'USB device with ID {device_id} not found',
http_code=404
)
# Find active checkout
active_checkout = USBCheckout.query.filter_by(
machineid=device_id,
checkin_time=None
).first()
if not active_checkout:
return error_response(
ErrorCodes.VALIDATION_ERROR,
'Device is not currently checked out',
http_code=400
)
data = request.get_json() or {}
active_checkout.checkin_time = datetime.utcnow()
active_checkout.was_wiped = data.get('was_wiped', False)
active_checkout.checkin_notes = data.get('notes')
db.session.commit()
return success_response(active_checkout.to_dict(), message='Device checked in')
@usb_bp.route('/<int:device_id>/history', methods=['GET'])
@jwt_required()
def get_checkout_history(device_id: int):
"""Get checkout history for a USB device."""
page, per_page = get_pagination_params(request)
query = USBCheckout.query.filter_by(
machineid=device_id
).order_by(USBCheckout.checkout_time.desc())
items, total = paginate_query(query, page, per_page)
data = [c.to_dict() for c in items]
return paginated_response(data, page, per_page, total)
@usb_bp.route('/checkouts', methods=['GET'])
@jwt_required()
def list_all_checkouts():
"""List all checkouts (active and historical)."""
page, per_page = get_pagination_params(request)
query = db.session.query(USBCheckout).join(
Machine, USBCheckout.machineid == Machine.machineid
)
# Filter by active only
if request.args.get('active', '').lower() == 'true':
query = query.filter(USBCheckout.checkin_time == None)
# Filter by user
if sso := request.args.get('sso'):
query = query.filter(USBCheckout.sso == sso)
query = query.order_by(USBCheckout.checkout_time.desc())
items, total = paginate_query(query, page, per_page)
# Include device info
data = []
for checkout in items:
device = Machine.query.get(checkout.machineid)
item = checkout.to_dict()
item['device'] = {
'machineid': device.machineid,
'alias': device.alias,
'serialnumber': device.serialnumber
} if device else None
data.append(item)
return paginated_response(data, page, per_page, total)