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>
This commit is contained in:
5
plugins/usb/api/__init__.py
Normal file
5
plugins/usb/api/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""USB plugin API."""
|
||||
|
||||
from .routes import usb_bp
|
||||
|
||||
__all__ = ['usb_bp']
|
||||
275
plugins/usb/api/routes.py
Normal file
275
plugins/usb/api/routes.py
Normal file
@@ -0,0 +1,275 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user