Add custom fields + warranty plugin, rework settings into two-pane shell
Feature work from the 2026-07 session: Settings IA - Replace the flat 27-card settings hub with a persistent two-pane shell (SettingsLayout.vue): grouped, searchable left rail + content pane. - Nest all settings/* routes under the shell via router post-processing; shared nav catalog in settingsNav.js. Group by asset class (PCs, Printers, Equipment, Network) so per-type settings stop scattering. Custom fields (core) - customfields + customfieldvalues tables (migration 7d14), CRUD API at /api/customfields, per-asset value get/save. - Settings management page + reusable CustomFieldsSection (detail) and CustomFieldsInputs (form) wired into all four asset types. Warranty (new plugin) - plugins/warranty: warranties + warrantyassets (migration 7d15), derived coverage status, provider abstraction (manual now; Dell/Lenovo/HP stubs). - API CRUD + per-asset panel + report buckets; WarrantyPanel on all four detail pages; Warranties management page; Warranty report + Reports card. - Seed warranty.* permissions. Printer drivers - printerdrivers table (migration 7d13) linked to printer models; drivers now surface on the matching printer's detail page. Other - PCDetail rebalanced (Network + Status + Warranty + custom fields on the right). - Rename PCs list "Features" column to "Remote Access"; fix badge hover underline. - Drop equipment islocationonly field. - Centralize asset-type label/route maps into utils/assetTypes.js. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,25 @@ class NotificationType(db.Model):
|
||||
typecolor = db.Column(db.String(20), default='#17a2b8')
|
||||
isactive = db.Column(db.Boolean, default=True)
|
||||
|
||||
# Auto-expiry rule: when a notification of this type has no explicit end time,
|
||||
# how long it stays up on the shopfloor board.
|
||||
# 'none' -> indefinite (never auto-expires)
|
||||
# 'duration' -> starttime + expirydays days
|
||||
# 'dailytime' -> next expiryhour:expiryminute Eastern (daily reset)
|
||||
expirymode = db.Column(db.String(20), default='none')
|
||||
expirydays = db.Column(db.Integer, nullable=True)
|
||||
expiryhour = db.Column(db.SmallInteger, nullable=True)
|
||||
expiryminute = db.Column(db.SmallInteger, nullable=True, default=0)
|
||||
|
||||
# Shopfloor display behavior (data-driven; replaces hardcoded per-type logic).
|
||||
# splitperemployee -> one card per listed employee SSO
|
||||
# showemployeephoto -> resolve + show each employee's photo + name
|
||||
# displaystyle -> 'standard' (rows) | 'carousel' (rotating photo card)
|
||||
# | 'grid' (cycling row of tiles) | 'banner'
|
||||
splitperemployee = db.Column(db.Boolean, default=False)
|
||||
showemployeephoto = db.Column(db.Boolean, default=False)
|
||||
displaystyle = db.Column(db.String(20), default='standard')
|
||||
|
||||
def __repr__(self):
|
||||
return f"<NotificationType {self.typename}>"
|
||||
|
||||
@@ -26,7 +45,14 @@ class NotificationType(db.Model):
|
||||
'typename': self.typename,
|
||||
'typedescription': self.typedescription,
|
||||
'typecolor': self.typecolor,
|
||||
'isactive': self.isactive
|
||||
'isactive': self.isactive,
|
||||
'expirymode': self.expirymode or 'none',
|
||||
'expirydays': self.expirydays,
|
||||
'expiryhour': self.expiryhour,
|
||||
'expiryminute': self.expiryminute if self.expiryminute is not None else 0,
|
||||
'splitperemployee': bool(self.splitperemployee),
|
||||
'showemployeephoto': bool(self.showemployeephoto),
|
||||
'displaystyle': self.displaystyle or 'standard'
|
||||
}
|
||||
|
||||
|
||||
@@ -52,8 +78,11 @@ class Notification(db.Model):
|
||||
link = db.Column(db.String(500), nullable=True)
|
||||
isactive = db.Column(db.Boolean, default=True)
|
||||
isshopfloor = db.Column(db.Boolean, default=False)
|
||||
employeesso = db.Column(db.String(100), nullable=True)
|
||||
employeename = db.Column(db.String(100), nullable=True)
|
||||
# TEXT (not VARCHAR): recognition/recertification notifications comma-join
|
||||
# every employee's SSO/name into one field, which overflows 100 chars once
|
||||
# ~11 people are listed.
|
||||
employeesso = db.Column(db.Text, nullable=True)
|
||||
employeename = db.Column(db.Text, nullable=True)
|
||||
|
||||
# Relationships
|
||||
notificationtype = db.relationship('NotificationType', backref='notifications')
|
||||
@@ -114,24 +143,25 @@ class Notification(db.Model):
|
||||
|
||||
def to_calendar_event(self):
|
||||
"""Convert to FullCalendar event format."""
|
||||
# Map Bootstrap color names to hex colors
|
||||
color_map = {
|
||||
# Color is data-driven: types store a hex typecolor. Only the legacy
|
||||
# Bootstrap color-name aliases still need translating; hex passes through.
|
||||
color_aliases = {
|
||||
'success': '#04b962',
|
||||
'warning': '#ff8800',
|
||||
'danger': '#f5365c',
|
||||
'info': '#14abef',
|
||||
'primary': '#7934f3',
|
||||
'secondary': '#94614f',
|
||||
'recognition': '#14abef', # Blue for recognition
|
||||
}
|
||||
|
||||
raw_color = self.notificationtype.typecolor if self.notificationtype else 'info'
|
||||
# Use mapped color if it's a Bootstrap name, otherwise use as-is (hex)
|
||||
color = color_map.get(raw_color, raw_color if raw_color.startswith('#') else '#14abef')
|
||||
ntype = self.notificationtype
|
||||
raw_color = ntype.typecolor if ntype else '#14abef'
|
||||
color = color_aliases.get(raw_color, raw_color if raw_color.startswith('#') else '#14abef')
|
||||
show_photo = bool(ntype and getattr(ntype, 'showemployeephoto', False))
|
||||
|
||||
# For recognition notifications, include employee name (or SSO as fallback) in title
|
||||
# Employee-photo types prefix the card with the person's name/SSO.
|
||||
title = self.title
|
||||
if raw_color == 'recognition':
|
||||
if show_photo:
|
||||
employee_display = self.employeename or self.employeesso
|
||||
if employee_display:
|
||||
title = f"{employee_display}: {title}"
|
||||
@@ -147,8 +177,10 @@ class Notification(db.Model):
|
||||
'extendedProps': {
|
||||
'notificationid': self.notificationid,
|
||||
'message': self.notification,
|
||||
'typename': self.notificationtype.typename if self.notificationtype else None,
|
||||
'typename': ntype.typename if ntype else None,
|
||||
'typecolor': raw_color,
|
||||
'showemployeephoto': show_photo,
|
||||
'displaystyle': (ntype.displaystyle or 'standard') if ntype else 'standard',
|
||||
'linkurl': self.link,
|
||||
'ticketnumber': self.ticketnumber,
|
||||
'employeename': self.employeename,
|
||||
|
||||
Reference in New Issue
Block a user