shopfloor dashboard: show site time, and align the state badge
Every time on the board was wrong, from two faults stacked. The shopfloor feed serialised starttime/endtime with a bare isoformat(). Those columns are stored NAIVE but hold UTC, so an untagged string is read by the browser as LOCAL and every card shifted by the tz offset. The model's to_dict already learned this - its _utc_iso helper documents the exact symptom, a 14:34 notification showing 18:34 - but the feed had not, so the feed now uses it too. The dashboard then formatted with toLocaleString, i.e. the VIEWER's zone. A board hangs on a wall in the plant: it has to read plant time whatever the machine driving it is set to, and a kiosk with a wrong system timezone would otherwise show wrong times to the floor with nothing to reveal it. It now loads site_timezone and formats through formatInZone, the wall clock included - a header disagreeing with the cards beneath it is worse than either being wrong alone. startsWhen was worse still: it decided TODAY/TOMORROW from browser-local calendar days, so the wording itself could differ between the board and a remote admin looking at the same card. That arithmetic now runs on the site's calendar day. Separately, the type chip carried a margin-bottom while the state chip beside it did not. .chip-row centres each item's MARGIN box, so that margin lifted the type chip about 4px and left "Starts Thu, Aug 13 8:00 PM" looking low. The row already provides the spacing, so the chip's own margin is gone.
This commit is contained in:
@@ -237,7 +237,8 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { notificationsApi, businessUnitsApi, dashboardDefaultsApi } from '@/api'
|
||||
import { notificationsApi, businessUnitsApi, dashboardDefaultsApi, settingsApi } from '@/api'
|
||||
import { formatInZone, zonedInputFromUtc, DEFAULT_TZ } from '@/utils/datetime'
|
||||
import { getFacilityName, getSiteLogo, getServicenowUrls } from '@/utils/siteSettings'
|
||||
import { withBase } from '@/utils/basePath'
|
||||
|
||||
@@ -386,17 +387,30 @@ const upcomingNotifications = computed(() =>
|
||||
)
|
||||
|
||||
// Clock
|
||||
// Site timezone drives every time on the board. Defaults until the setting
|
||||
// loads, so a slow settings call shows plausible time rather than blank.
|
||||
const siteTimezone = ref(DEFAULT_TZ)
|
||||
|
||||
const currentTime = ref('')
|
||||
function updateClock() {
|
||||
const now = new Date()
|
||||
currentTime.value = now.toLocaleTimeString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
// The wall clock too: a board showing a different time to the cards beneath
|
||||
// it is worse than either being wrong on its own.
|
||||
currentTime.value = formatInZone(new Date(), siteTimezone.value, {
|
||||
year: undefined, month: undefined, day: undefined,
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// Before the first clock tick, so the board never renders viewer-zone time.
|
||||
try {
|
||||
const response = await settingsApi.get('site_timezone')
|
||||
const value = response?.data?.data?.value
|
||||
if (value) siteTimezone.value = value
|
||||
} catch (e) {
|
||||
// Keep the default zone; the board must still come up.
|
||||
}
|
||||
|
||||
updateClock()
|
||||
setInterval(updateClock, 1000)
|
||||
|
||||
@@ -528,15 +542,24 @@ function stateKind(item) {
|
||||
// timestamp alone makes a reader do the arithmetic.
|
||||
function startsWhen(dateStr) {
|
||||
if (!dateStr) return 'SOON'
|
||||
const start = new Date(dateStr)
|
||||
const time = start.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })
|
||||
const startday = new Date(start.getFullYear(), start.getMonth(), start.getDate())
|
||||
const now = new Date()
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
const days = Math.round((startday - today) / 86400000)
|
||||
const time = formatInZone(dateStr, siteTimezone.value, {
|
||||
year: undefined, month: undefined, day: undefined,
|
||||
hour: 'numeric', minute: '2-digit'
|
||||
})
|
||||
// TODAY/TOMORROW has to be decided on the SITE's calendar day. Doing the
|
||||
// arithmetic on browser-local days makes the wording itself wrong for a
|
||||
// viewer in another zone - a card can read TOMORROW to a remote admin and
|
||||
// TODAY on the board, from the same data.
|
||||
const startday = zonedInputFromUtc(dateStr, siteTimezone.value).slice(0, 10)
|
||||
const todayday = zonedInputFromUtc(new Date(), siteTimezone.value).slice(0, 10)
|
||||
const days = Math.round(
|
||||
(new Date(`${startday}T00:00:00`) - new Date(`${todayday}T00:00:00`)) / 86400000)
|
||||
if (days <= 0) return `TODAY ${time}`
|
||||
if (days === 1) return `TOMORROW ${time}`
|
||||
const when = start.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })
|
||||
const when = formatInZone(dateStr, siteTimezone.value, {
|
||||
weekday: 'short', month: 'short', day: 'numeric',
|
||||
hour: undefined, minute: undefined
|
||||
})
|
||||
return `${when} ${time}`
|
||||
}
|
||||
|
||||
@@ -604,17 +627,20 @@ function textOn(backgroundcolor) {
|
||||
return (0.2126 * r + 0.7152 * g + 0.0722 * b) > 150 ? '#231b00' : '#fff'
|
||||
}
|
||||
|
||||
// SITE zone, not the viewer's. A board hangs on a wall in the plant: it must
|
||||
// read plant time whatever the machine driving it is set to, and a kiosk with a
|
||||
// wrong system timezone would otherwise silently show wrong times to the floor.
|
||||
function formatTime(dateStr) {
|
||||
if (!dateStr) return ''
|
||||
return new Date(dateStr).toLocaleTimeString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
return formatInZone(dateStr, siteTimezone.value, {
|
||||
year: undefined, month: undefined, day: undefined,
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
function formatDateTime(dateStr) {
|
||||
if (!dateStr) return ''
|
||||
return new Date(dateStr).toLocaleString('en-US', {
|
||||
return formatInZone(dateStr, siteTimezone.value, {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
@@ -935,7 +961,9 @@ function handlePhotoError(e) {
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 8px;
|
||||
/* No margin-bottom. .chip-row centres each item's MARGIN box, so a bottom
|
||||
margin here lifted this chip ~4px and left the state chip beside it
|
||||
looking low. The row already provides the spacing underneath. */
|
||||
}
|
||||
|
||||
/* Type colour as a bar down the left edge, the way the legacy board marked a
|
||||
|
||||
@@ -11,6 +11,7 @@ from flask_jwt_extended import jwt_required
|
||||
from shopdb.api import db, success_response, error_response, paginated_response, ErrorCodes, get_pagination_params, paginate_query
|
||||
|
||||
from ..models import Notification, NotificationType
|
||||
from ..models.notification import _utc_iso
|
||||
|
||||
from shopdb.api import require_permission, Setting
|
||||
|
||||
@@ -792,8 +793,12 @@ def get_shopfloor_notifications():
|
||||
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,
|
||||
# _utc_iso, not a bare isoformat: these are stored NAIVE but hold
|
||||
# UTC, and an untagged string is read by the browser as LOCAL time,
|
||||
# shifting every card on the board by the tz offset. The model's
|
||||
# to_dict already learned this; the shopfloor feed had not.
|
||||
'starttime': _utc_iso(n.starttime),
|
||||
'endtime': _utc_iso(n.endtime),
|
||||
'ticketnumber': n.ticketnumber,
|
||||
'link': n.link,
|
||||
'isactive': n.isactive,
|
||||
|
||||
Reference in New Issue
Block a user