backups plugin with NTLARS revision history; shopfloor dashboard upcoming cards and state chips
This commit is contained in:
@@ -37,8 +37,10 @@
|
||||
v-for="n in group.items"
|
||||
:key="n.notificationid"
|
||||
class="banner-strip"
|
||||
:class="{ pending: n.upcoming || n.resolved }"
|
||||
:style="{ backgroundColor: getTypeColor(n.typecolor) }"
|
||||
>
|
||||
<span v-if="stateLabel(n)" class="state-chip">{{ stateLabel(n) }}</span>
|
||||
{{ n.notification }}
|
||||
</div>
|
||||
</section>
|
||||
@@ -47,14 +49,15 @@
|
||||
<section v-else-if="group.displaystyle === 'carousel'" class="recognition-section">
|
||||
<div
|
||||
class="section-title recognition"
|
||||
:style="{ backgroundColor: getTypeColor(group.typecolor), color: textOn(group.typecolor) }"
|
||||
:class="{ mixed: isMixed(group) }"
|
||||
:style="headingStyle(group)"
|
||||
>{{ group.typename }}</div>
|
||||
<div class="recognition-carousel">
|
||||
<div
|
||||
v-for="(rec, idx) in group.items"
|
||||
:key="`${rec.notificationid}-${rec.employeesso}`"
|
||||
class="recognition-card"
|
||||
:class="{ active: idx === carouselIndex(group) }"
|
||||
:class="{ active: idx === carouselIndex(group), pending: rec.upcoming || rec.resolved }"
|
||||
:style="{ '--accent': getTypeColor(rec.typecolor) }"
|
||||
>
|
||||
<!-- A type that names no employee is a message, not a person:
|
||||
@@ -78,17 +81,41 @@
|
||||
<div v-if="hasEmployee(rec)" class="recognition-header">
|
||||
<div class="recognition-name">{{ rec.employeename }}</div>
|
||||
</div>
|
||||
<!-- On a shared category row the heading names the category, so
|
||||
the card has to say which type it is. Named, not just
|
||||
coloured: colour alone is no use to a colourblind reader
|
||||
across a 1920 board. -->
|
||||
<div class="chip-row">
|
||||
<span
|
||||
v-if="isMixed(group)"
|
||||
class="type-chip"
|
||||
:style="{ backgroundColor: getTypeColor(rec.typecolor), color: textOn(rec.typecolor) }"
|
||||
>{{ rec.typename }}</span>
|
||||
<span v-if="stateLabel(rec)" class="state-chip">{{ stateLabel(rec) }}</span>
|
||||
</div>
|
||||
<div class="recognition-message">{{ rec.notification }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- One pip per card, so a reader knows how many are in the rotation
|
||||
and how long until theirs comes back round. -->
|
||||
<div v-if="group.items.length > 1" class="carousel-pips">
|
||||
<span
|
||||
v-for="(rec, idx) in group.items"
|
||||
:key="`pip-${rec.notificationid}-${rec.employeesso}`"
|
||||
class="pip"
|
||||
:class="{ on: idx === carouselIndex(group) }"
|
||||
:style="{ backgroundColor: getTypeColor(rec.typecolor) }"
|
||||
></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Grid - a row of tiles, cycling a page at a time -->
|
||||
<section v-else class="recert-section">
|
||||
<div
|
||||
class="section-title recert-title"
|
||||
:style="{ backgroundColor: getTypeColor(group.typecolor), color: textOn(group.typecolor) }"
|
||||
:class="{ mixed: isMixed(group) }"
|
||||
:style="headingStyle(group)"
|
||||
>
|
||||
<span>{{ group.typename }} ({{ group.items.length }})</span>
|
||||
<span v-if="gridRangeLabel(group)" class="recert-range">{{ gridRangeLabel(group) }}</span>
|
||||
@@ -105,7 +132,7 @@
|
||||
v-for="rec in gridPageItems(group)"
|
||||
:key="`grid-${rec.notificationid}-${rec.employeesso}`"
|
||||
class="recert-tile"
|
||||
:class="{ 'message-tile': !hasEmployee(rec) }"
|
||||
:class="{ 'message-tile': !hasEmployee(rec), pending: rec.upcoming || rec.resolved }"
|
||||
:style="{ '--accent': getTypeColor(rec.typecolor) }"
|
||||
>
|
||||
<template v-if="hasEmployee(rec)">
|
||||
@@ -127,6 +154,7 @@
|
||||
<!-- No employee: the message IS the tile, so a grid type like
|
||||
Awareness does not render rows of blank placeholder faces. -->
|
||||
<div v-else class="recert-message">{{ rec.notification }}</div>
|
||||
<span v-if="stateLabel(rec)" class="state-chip tile-state">{{ stateLabel(rec) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -264,7 +292,14 @@ const STYLE_ORDER = { banner: 0, carousel: 1, grid: 2 }
|
||||
// row, so same-category types of different styles still render separately.
|
||||
const styledGroups = computed(() => {
|
||||
const groups = new Map()
|
||||
for (const n of notifications.value.current) {
|
||||
// Upcoming cards belong here too. They used to be filtered into the standard
|
||||
// Upcoming list, which drops anything with a special style - so a scheduled
|
||||
// banner or carousel card rendered nowhere at all until it went live.
|
||||
const tagged = [
|
||||
...notifications.value.current.map(n => ({ ...n, upcoming: false })),
|
||||
...notifications.value.upcoming.map(n => ({ ...n, upcoming: true }))
|
||||
]
|
||||
for (const n of tagged) {
|
||||
if (!SPECIAL_STYLES.includes(n.displaystyle)) continue
|
||||
const heading = (n.boardcategory || '').trim() || n.typename || n.displaystyle
|
||||
const key = `${n.displaystyle}|${heading}`
|
||||
@@ -282,6 +317,10 @@ const styledGroups = computed(() => {
|
||||
}
|
||||
groups.get(key).items.push(n)
|
||||
}
|
||||
// Live cards lead each row; what has not started yet follows.
|
||||
for (const group of groups.values()) {
|
||||
group.items.sort((a, b) => Number(a.upcoming) - Number(b.upcoming))
|
||||
}
|
||||
return [...groups.values()].sort((a, b) =>
|
||||
(STYLE_ORDER[a.displaystyle] - STYLE_ORDER[b.displaystyle]) ||
|
||||
a.typename.localeCompare(b.typename)
|
||||
@@ -465,6 +504,29 @@ function getTypeColor(typecolor) {
|
||||
return aliases[typecolor] || typecolor || '#14abef'
|
||||
}
|
||||
|
||||
// What a card's state chip says, or '' while it is simply live. Resolved is the
|
||||
// tail a type's grace window buys it; upcoming is scheduled and not yet showing.
|
||||
function stateLabel(item) {
|
||||
if (item.upcoming) return `UPCOMING ${formatDateTime(item.starttime)}`
|
||||
return item.resolved ? 'RESOLVED' : ''
|
||||
}
|
||||
|
||||
// A row holding more than one type: the heading names a category, so it cannot
|
||||
// speak for any single type's colour or name.
|
||||
function isMixed(group) {
|
||||
return new Set(group.items.map(item => item.typename)).size > 1
|
||||
}
|
||||
|
||||
// A single-type row wears that type's colour. A mixed one keeps the neutral
|
||||
// heading, since picking the first type's colour just mislabels the rest.
|
||||
function headingStyle(group) {
|
||||
if (isMixed(group)) return {}
|
||||
return {
|
||||
backgroundColor: getTypeColor(group.typecolor),
|
||||
color: textOn(group.typecolor)
|
||||
}
|
||||
}
|
||||
|
||||
// Text colour for a heading painted an arbitrary type colour. Gold needs dark
|
||||
// text, navy needs white, and a site picks its own hexes - so this is computed
|
||||
// rather than a per-colour rule.
|
||||
@@ -701,6 +763,14 @@ function handlePhotoError(e) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* A row of several types speaks for none of them, so it keeps the neutral
|
||||
heading rather than wearing the first type's colour (or, via the per-style
|
||||
fallbacks above, Recognition's gold and Recertification's blue). */
|
||||
.section-title.mixed {
|
||||
background: #4181ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.section-title.danger {
|
||||
background: #f5365c;
|
||||
}
|
||||
@@ -732,9 +802,75 @@ function handlePhotoError(e) {
|
||||
|
||||
.recognition-carousel {
|
||||
position: relative;
|
||||
/* Enough for a photo card; a message-only card is shorter and leaves the
|
||||
rest as breathing room rather than resizing the board every 8 seconds. */
|
||||
min-height: 170px;
|
||||
}
|
||||
|
||||
.carousel-pips {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.pip {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
opacity: 0.35;
|
||||
transition: opacity 0.4s ease, transform 0.4s ease;
|
||||
}
|
||||
.pip.on {
|
||||
opacity: 1;
|
||||
transform: scale(1.25);
|
||||
}
|
||||
|
||||
.chip-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Scheduled or just-ended cards stay legible but visibly quieter than live
|
||||
ones, so a glance at the board never mistakes one for the other. Carousel
|
||||
cards are dimmed only while ACTIVE: the inactive ones are held at opacity 0
|
||||
by .recognition-card, and a plain .pending rule outranks it, which brought
|
||||
the whole stack back into view on top of each other. */
|
||||
.recognition-card.active.pending,
|
||||
.recert-tile.pending,
|
||||
.banner-strip.pending {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.state-chip {
|
||||
padding: 4px 14px;
|
||||
border-radius: 999px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
border: 2px solid rgba(255, 255, 255, 0.45);
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.state-chip.tile-state {
|
||||
font-size: 14px;
|
||||
padding: 2px 10px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.type-chip {
|
||||
padding: 4px 14px;
|
||||
border-radius: 999px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.recognition-card {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -774,6 +910,8 @@ function handlePhotoError(e) {
|
||||
}
|
||||
|
||||
.recognition-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user