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>
276 lines
8.3 KiB
Python
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)
|