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/notifications/api/__init__.py
Normal file
5
plugins/notifications/api/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Notifications plugin API."""
|
||||
|
||||
from .routes import notifications_bp
|
||||
|
||||
__all__ = ['notifications_bp']
|
||||
617
plugins/notifications/api/routes.py
Normal file
617
plugins/notifications/api/routes.py
Normal file
@@ -0,0 +1,617 @@
|
||||
"""Notifications plugin API endpoints - adapted to existing schema."""
|
||||
|
||||
from datetime import datetime
|
||||
from flask import Blueprint, request
|
||||
from flask_jwt_extended import jwt_required
|
||||
|
||||
from shopdb.extensions import db
|
||||
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 Notification, NotificationType
|
||||
|
||||
notifications_bp = Blueprint('notifications', __name__)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Notification Types
|
||||
# =============================================================================
|
||||
|
||||
@notifications_bp.route('/types', methods=['GET'])
|
||||
def list_notification_types():
|
||||
"""List all notification types."""
|
||||
page, per_page = get_pagination_params(request)
|
||||
|
||||
query = NotificationType.query
|
||||
|
||||
if request.args.get('active', 'true').lower() != 'false':
|
||||
query = query.filter(NotificationType.isactive == True)
|
||||
|
||||
query = query.order_by(NotificationType.typename)
|
||||
|
||||
items, total = paginate_query(query, page, per_page)
|
||||
data = [t.to_dict() for t in items]
|
||||
|
||||
return paginated_response(data, page, per_page, total)
|
||||
|
||||
|
||||
@notifications_bp.route('/types', methods=['POST'])
|
||||
@jwt_required()
|
||||
def create_notification_type():
|
||||
"""Create a new notification type."""
|
||||
data = request.get_json()
|
||||
|
||||
if not data or not data.get('typename'):
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'typename is required')
|
||||
|
||||
if NotificationType.query.filter_by(typename=data['typename']).first():
|
||||
return error_response(
|
||||
ErrorCodes.CONFLICT,
|
||||
f"Notification type '{data['typename']}' already exists",
|
||||
http_code=409
|
||||
)
|
||||
|
||||
t = NotificationType(
|
||||
typename=data['typename'],
|
||||
typedescription=data.get('typedescription') or data.get('description'),
|
||||
typecolor=data.get('typecolor') or data.get('color', '#17a2b8')
|
||||
)
|
||||
|
||||
db.session.add(t)
|
||||
db.session.commit()
|
||||
|
||||
return success_response(t.to_dict(), message='Notification type created', http_code=201)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Notifications CRUD
|
||||
# =============================================================================
|
||||
|
||||
@notifications_bp.route('', methods=['GET'])
|
||||
def list_notifications():
|
||||
"""
|
||||
List all notifications with filtering and pagination.
|
||||
|
||||
Query parameters:
|
||||
- page, per_page: Pagination
|
||||
- active: Filter by active status (default: true)
|
||||
- type_id: Filter by notification type ID
|
||||
- current: Filter to currently active notifications only
|
||||
- search: Search in notification text
|
||||
"""
|
||||
page, per_page = get_pagination_params(request)
|
||||
|
||||
query = Notification.query
|
||||
|
||||
# Active filter
|
||||
if request.args.get('active', 'true').lower() != 'false':
|
||||
query = query.filter(Notification.isactive == True)
|
||||
|
||||
# Type filter
|
||||
if type_id := request.args.get('type_id'):
|
||||
query = query.filter(Notification.notificationtypeid == int(type_id))
|
||||
|
||||
# Current filter (active based on dates)
|
||||
if request.args.get('current', 'false').lower() == 'true':
|
||||
now = datetime.utcnow()
|
||||
query = query.filter(
|
||||
Notification.starttime <= now,
|
||||
db.or_(
|
||||
Notification.endtime.is_(None),
|
||||
Notification.endtime >= now
|
||||
)
|
||||
)
|
||||
|
||||
# Search filter
|
||||
if search := request.args.get('search'):
|
||||
query = query.filter(
|
||||
Notification.notification.ilike(f'%{search}%')
|
||||
)
|
||||
|
||||
# Sorting by start time (newest first)
|
||||
query = query.order_by(Notification.starttime.desc())
|
||||
|
||||
items, total = paginate_query(query, page, per_page)
|
||||
data = [n.to_dict() for n in items]
|
||||
|
||||
return paginated_response(data, page, per_page, total)
|
||||
|
||||
|
||||
@notifications_bp.route('/<int:notification_id>', methods=['GET'])
|
||||
def get_notification(notification_id: int):
|
||||
"""Get a single notification."""
|
||||
n = Notification.query.get(notification_id)
|
||||
|
||||
if not n:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Notification with ID {notification_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
return success_response(n.to_dict())
|
||||
|
||||
|
||||
@notifications_bp.route('', methods=['POST'])
|
||||
@jwt_required()
|
||||
def create_notification():
|
||||
"""Create a new notification."""
|
||||
data = request.get_json()
|
||||
|
||||
if not data:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided')
|
||||
|
||||
# Validate required fields
|
||||
notification_text = data.get('notification') or data.get('message')
|
||||
if not notification_text:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'notification/message is required')
|
||||
|
||||
# Parse dates
|
||||
starttime = datetime.utcnow()
|
||||
if data.get('starttime') or data.get('startdate'):
|
||||
try:
|
||||
date_str = data.get('starttime') or data.get('startdate')
|
||||
starttime = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
||||
except ValueError:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'Invalid starttime format')
|
||||
|
||||
endtime = None
|
||||
if data.get('endtime') or data.get('enddate'):
|
||||
try:
|
||||
date_str = data.get('endtime') or data.get('enddate')
|
||||
endtime = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
||||
except ValueError:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'Invalid endtime format')
|
||||
|
||||
n = Notification(
|
||||
notification=notification_text,
|
||||
notificationtypeid=data.get('notificationtypeid'),
|
||||
businessunitid=data.get('businessunitid'),
|
||||
appid=data.get('appid'),
|
||||
starttime=starttime,
|
||||
endtime=endtime,
|
||||
ticketnumber=data.get('ticketnumber'),
|
||||
link=data.get('link') or data.get('linkurl'),
|
||||
isactive=True,
|
||||
isshopfloor=data.get('isshopfloor', False),
|
||||
employeesso=data.get('employeesso'),
|
||||
employeename=data.get('employeename')
|
||||
)
|
||||
|
||||
db.session.add(n)
|
||||
db.session.commit()
|
||||
|
||||
return success_response(n.to_dict(), message='Notification created', http_code=201)
|
||||
|
||||
|
||||
@notifications_bp.route('/<int:notification_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
def update_notification(notification_id: int):
|
||||
"""Update a notification."""
|
||||
n = Notification.query.get(notification_id)
|
||||
|
||||
if not n:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Notification with ID {notification_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided')
|
||||
|
||||
# Update text content
|
||||
if 'notification' in data or 'message' in data:
|
||||
n.notification = data.get('notification') or data.get('message')
|
||||
|
||||
# Update simple fields
|
||||
if 'notificationtypeid' in data:
|
||||
n.notificationtypeid = data['notificationtypeid']
|
||||
if 'businessunitid' in data:
|
||||
n.businessunitid = data['businessunitid']
|
||||
if 'appid' in data:
|
||||
n.appid = data['appid']
|
||||
if 'ticketnumber' in data:
|
||||
n.ticketnumber = data['ticketnumber']
|
||||
if 'link' in data or 'linkurl' in data:
|
||||
n.link = data.get('link') or data.get('linkurl')
|
||||
if 'isactive' in data:
|
||||
n.isactive = data['isactive']
|
||||
if 'isshopfloor' in data:
|
||||
n.isshopfloor = data['isshopfloor']
|
||||
if 'employeesso' in data:
|
||||
n.employeesso = data['employeesso']
|
||||
if 'employeename' in data:
|
||||
n.employeename = data['employeename']
|
||||
|
||||
# Parse and update dates
|
||||
if 'starttime' in data or 'startdate' in data:
|
||||
date_str = data.get('starttime') or data.get('startdate')
|
||||
if date_str:
|
||||
try:
|
||||
n.starttime = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
||||
except ValueError:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'Invalid starttime format')
|
||||
else:
|
||||
n.starttime = datetime.utcnow()
|
||||
|
||||
if 'endtime' in data or 'enddate' in data:
|
||||
date_str = data.get('endtime') or data.get('enddate')
|
||||
if date_str:
|
||||
try:
|
||||
n.endtime = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
||||
except ValueError:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'Invalid endtime format')
|
||||
else:
|
||||
n.endtime = None
|
||||
|
||||
db.session.commit()
|
||||
return success_response(n.to_dict(), message='Notification updated')
|
||||
|
||||
|
||||
@notifications_bp.route('/<int:notification_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
def delete_notification(notification_id: int):
|
||||
"""Delete (soft delete) a notification."""
|
||||
n = Notification.query.get(notification_id)
|
||||
|
||||
if not n:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Notification with ID {notification_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
n.isactive = False
|
||||
db.session.commit()
|
||||
|
||||
return success_response(message='Notification deleted')
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Special Endpoints
|
||||
# =============================================================================
|
||||
|
||||
@notifications_bp.route('/active', methods=['GET'])
|
||||
def get_active_notifications():
|
||||
"""
|
||||
Get currently active notifications for display.
|
||||
"""
|
||||
now = datetime.utcnow()
|
||||
|
||||
notifications = Notification.query.filter(
|
||||
Notification.isactive == True,
|
||||
db.or_(
|
||||
Notification.starttime.is_(None),
|
||||
Notification.starttime <= now
|
||||
),
|
||||
db.or_(
|
||||
Notification.endtime.is_(None),
|
||||
Notification.endtime >= now
|
||||
)
|
||||
).order_by(Notification.starttime.desc()).all()
|
||||
|
||||
data = [n.to_dict() for n in notifications]
|
||||
|
||||
return success_response({
|
||||
'notifications': data,
|
||||
'total': len(data)
|
||||
})
|
||||
|
||||
|
||||
@notifications_bp.route('/calendar', methods=['GET'])
|
||||
def get_calendar_events():
|
||||
"""
|
||||
Get notifications in FullCalendar event format.
|
||||
|
||||
Query parameters:
|
||||
- start: Start date (ISO format)
|
||||
- end: End date (ISO format)
|
||||
"""
|
||||
query = Notification.query.filter(Notification.isactive == True)
|
||||
|
||||
# Date range filter
|
||||
if start := request.args.get('start'):
|
||||
try:
|
||||
start_date = datetime.fromisoformat(start.replace('Z', '+00:00'))
|
||||
query = query.filter(
|
||||
db.or_(
|
||||
Notification.endtime >= start_date,
|
||||
Notification.endtime.is_(None)
|
||||
)
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if end := request.args.get('end'):
|
||||
try:
|
||||
end_date = datetime.fromisoformat(end.replace('Z', '+00:00'))
|
||||
query = query.filter(Notification.starttime <= end_date)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
notifications = query.order_by(Notification.starttime).all()
|
||||
|
||||
events = [n.to_calendar_event() for n in notifications]
|
||||
|
||||
return success_response(events)
|
||||
|
||||
|
||||
@notifications_bp.route('/dashboard/summary', methods=['GET'])
|
||||
def dashboard_summary():
|
||||
"""Get notifications dashboard summary."""
|
||||
now = datetime.utcnow()
|
||||
|
||||
# Total active notifications
|
||||
total_active = Notification.query.filter(
|
||||
Notification.isactive == True,
|
||||
db.or_(
|
||||
Notification.starttime.is_(None),
|
||||
Notification.starttime <= now
|
||||
),
|
||||
db.or_(
|
||||
Notification.endtime.is_(None),
|
||||
Notification.endtime >= now
|
||||
)
|
||||
).count()
|
||||
|
||||
# By type
|
||||
by_type = db.session.query(
|
||||
NotificationType.typename,
|
||||
NotificationType.typecolor,
|
||||
db.func.count(Notification.notificationid)
|
||||
).join(Notification
|
||||
).filter(
|
||||
Notification.isactive == True
|
||||
).group_by(NotificationType.typename, NotificationType.typecolor
|
||||
).all()
|
||||
|
||||
return success_response({
|
||||
'active': total_active,
|
||||
'by_type': [{'type': t, 'color': c, 'count': n} for t, c, n in by_type]
|
||||
})
|
||||
|
||||
|
||||
@notifications_bp.route('/employee/<sso>', methods=['GET'])
|
||||
def get_employee_recognitions(sso):
|
||||
"""
|
||||
Get recognitions for a specific employee by SSO.
|
||||
|
||||
Returns all recognition-type notifications where the employee is mentioned.
|
||||
"""
|
||||
if not sso or not sso.isdigit():
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'Valid SSO required')
|
||||
|
||||
# Find recognition type(s)
|
||||
recognition_types = NotificationType.query.filter(
|
||||
db.or_(
|
||||
NotificationType.typecolor == 'recognition',
|
||||
NotificationType.typename.ilike('%recognition%')
|
||||
)
|
||||
).all()
|
||||
|
||||
recognition_type_ids = [rt.notificationtypeid for rt in recognition_types]
|
||||
|
||||
# Find notifications where this employee is mentioned
|
||||
# Check both exact match and comma-separated list
|
||||
query = Notification.query.filter(
|
||||
Notification.isactive == True,
|
||||
db.or_(
|
||||
Notification.employeesso == sso,
|
||||
Notification.employeesso.like(f'{sso},%'),
|
||||
Notification.employeesso.like(f'%,{sso}'),
|
||||
Notification.employeesso.like(f'%,{sso},%')
|
||||
)
|
||||
)
|
||||
|
||||
# Optionally filter to recognition types only
|
||||
if recognition_type_ids:
|
||||
query = query.filter(Notification.notificationtypeid.in_(recognition_type_ids))
|
||||
|
||||
query = query.order_by(Notification.starttime.desc())
|
||||
|
||||
notifications = query.all()
|
||||
data = [n.to_dict() for n in notifications]
|
||||
|
||||
return success_response({
|
||||
'recognitions': data,
|
||||
'total': len(data)
|
||||
})
|
||||
|
||||
|
||||
@notifications_bp.route('/shopfloor', methods=['GET'])
|
||||
def get_shopfloor_notifications():
|
||||
"""
|
||||
Get notifications for shopfloor TV dashboard.
|
||||
|
||||
Returns current and upcoming notifications with isshopfloor=1.
|
||||
Splits multi-employee recognition into separate entries.
|
||||
|
||||
Query parameters:
|
||||
- businessunit: Filter by business unit ID (null = all units)
|
||||
"""
|
||||
from datetime import timedelta
|
||||
|
||||
now = datetime.utcnow()
|
||||
business_unit = request.args.get('businessunit')
|
||||
|
||||
# Base query for shopfloor notifications
|
||||
base_query = Notification.query.filter(Notification.isshopfloor == True)
|
||||
|
||||
# Business unit filter
|
||||
if business_unit and business_unit.isdigit():
|
||||
# Specific BU: show that BU's notifications AND null (all units)
|
||||
base_query = base_query.filter(
|
||||
db.or_(
|
||||
Notification.businessunitid == int(business_unit),
|
||||
Notification.businessunitid.is_(None)
|
||||
)
|
||||
)
|
||||
else:
|
||||
# All units: only show notifications with NULL businessunitid
|
||||
base_query = base_query.filter(Notification.businessunitid.is_(None))
|
||||
|
||||
# Current notifications (active now or ended within 30 minutes)
|
||||
thirty_min_ago = now - timedelta(minutes=30)
|
||||
current_query = base_query.filter(
|
||||
db.or_(
|
||||
# Active and currently showing
|
||||
db.and_(
|
||||
Notification.isactive == True,
|
||||
db.or_(Notification.starttime.is_(None), Notification.starttime <= now),
|
||||
db.or_(Notification.endtime.is_(None), Notification.endtime >= now)
|
||||
),
|
||||
# Recently ended (within 30 min) - show as resolved
|
||||
db.and_(
|
||||
Notification.endtime.isnot(None),
|
||||
Notification.endtime >= thirty_min_ago,
|
||||
Notification.endtime < now
|
||||
)
|
||||
)
|
||||
).order_by(Notification.notificationid.desc())
|
||||
|
||||
current_notifications = current_query.all()
|
||||
|
||||
# Upcoming notifications (starts within next 5 days)
|
||||
five_days = now + timedelta(days=5)
|
||||
upcoming_query = base_query.filter(
|
||||
Notification.isactive == True,
|
||||
Notification.starttime > now,
|
||||
Notification.starttime <= five_days
|
||||
).order_by(Notification.starttime)
|
||||
|
||||
upcoming_notifications = upcoming_query.all()
|
||||
|
||||
def notification_to_shopfloor(n, employee_override=None):
|
||||
"""Convert notification to shopfloor format."""
|
||||
is_resolved = n.endtime and n.endtime < now
|
||||
|
||||
result = {
|
||||
'notificationid': n.notificationid,
|
||||
'notification': n.notification,
|
||||
'starttime': n.starttime.isoformat() if n.starttime else None,
|
||||
'endtime': n.endtime.isoformat() if n.endtime else None,
|
||||
'ticketnumber': n.ticketnumber,
|
||||
'link': n.link,
|
||||
'isactive': n.isactive,
|
||||
'isshopfloor': True,
|
||||
'resolved': is_resolved,
|
||||
'typename': n.notificationtype.typename if n.notificationtype else None,
|
||||
'typecolor': n.notificationtype.typecolor if n.notificationtype else None,
|
||||
}
|
||||
|
||||
# Employee info
|
||||
if employee_override:
|
||||
result['employeesso'] = employee_override.get('sso')
|
||||
result['employeename'] = employee_override.get('name')
|
||||
result['employeepicture'] = employee_override.get('picture')
|
||||
else:
|
||||
result['employeesso'] = n.employeesso
|
||||
result['employeename'] = n.employeename
|
||||
result['employeepicture'] = None
|
||||
|
||||
# Try to get picture from wjf_employees
|
||||
if n.employeesso and n.employeesso.isdigit():
|
||||
try:
|
||||
import pymysql
|
||||
conn = pymysql.connect(
|
||||
host='localhost', user='root', password='rootpassword',
|
||||
database='wjf_employees', cursorclass=pymysql.cursors.DictCursor
|
||||
)
|
||||
with conn.cursor() as cur:
|
||||
cur.execute('SELECT Picture FROM employees WHERE SSO = %s', (int(n.employeesso),))
|
||||
emp = cur.fetchone()
|
||||
if emp and emp.get('Picture'):
|
||||
result['employeepicture'] = emp['Picture']
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
# Process current notifications (split multi-employee recognition)
|
||||
current_data = []
|
||||
for n in current_notifications:
|
||||
is_recognition = n.notificationtype and n.notificationtype.typecolor == 'recognition'
|
||||
|
||||
if is_recognition and n.employeesso and ',' in n.employeesso:
|
||||
# Split into individual cards for each employee
|
||||
ssos = [s.strip() for s in n.employeesso.split(',')]
|
||||
names = n.employeename.split(', ') if n.employeename else []
|
||||
|
||||
for i, sso in enumerate(ssos):
|
||||
name = names[i] if i < len(names) else sso
|
||||
|
||||
# Look up picture
|
||||
picture = None
|
||||
if sso.isdigit():
|
||||
try:
|
||||
import pymysql
|
||||
conn = pymysql.connect(
|
||||
host='localhost', user='root', password='rootpassword',
|
||||
database='wjf_employees', cursorclass=pymysql.cursors.DictCursor
|
||||
)
|
||||
with conn.cursor() as cur:
|
||||
cur.execute('SELECT Picture FROM employees WHERE SSO = %s', (int(sso),))
|
||||
emp = cur.fetchone()
|
||||
if emp:
|
||||
picture = emp.get('Picture')
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
current_data.append(notification_to_shopfloor(n, {
|
||||
'sso': sso,
|
||||
'name': name,
|
||||
'picture': picture
|
||||
}))
|
||||
else:
|
||||
current_data.append(notification_to_shopfloor(n))
|
||||
|
||||
# Process upcoming notifications
|
||||
upcoming_data = []
|
||||
for n in upcoming_notifications:
|
||||
is_recognition = n.notificationtype and n.notificationtype.typecolor == 'recognition'
|
||||
|
||||
if is_recognition and n.employeesso and ',' in n.employeesso:
|
||||
ssos = [s.strip() for s in n.employeesso.split(',')]
|
||||
names = n.employeename.split(', ') if n.employeename else []
|
||||
|
||||
for i, sso in enumerate(ssos):
|
||||
name = names[i] if i < len(names) else sso
|
||||
picture = None
|
||||
if sso.isdigit():
|
||||
try:
|
||||
import pymysql
|
||||
conn = pymysql.connect(
|
||||
host='localhost', user='root', password='rootpassword',
|
||||
database='wjf_employees', cursorclass=pymysql.cursors.DictCursor
|
||||
)
|
||||
with conn.cursor() as cur:
|
||||
cur.execute('SELECT Picture FROM employees WHERE SSO = %s', (int(sso),))
|
||||
emp = cur.fetchone()
|
||||
if emp:
|
||||
picture = emp.get('Picture')
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
upcoming_data.append(notification_to_shopfloor(n, {
|
||||
'sso': sso,
|
||||
'name': name,
|
||||
'picture': picture
|
||||
}))
|
||||
else:
|
||||
upcoming_data.append(notification_to_shopfloor(n))
|
||||
|
||||
return success_response({
|
||||
'timestamp': now.isoformat(),
|
||||
'current': current_data,
|
||||
'upcoming': upcoming_data
|
||||
})
|
||||
Reference in New Issue
Block a user