Show the kiosk label prefix, and let a plugin declare the settings it owns
This commit is contained in:
@@ -1,384 +1,410 @@
|
||||
<template>
|
||||
<div class="page-header">
|
||||
<h1>Calendar</h1>
|
||||
</div>
|
||||
|
||||
<div class="calendar-container card">
|
||||
<FullCalendar :options="calendarOptions" />
|
||||
</div>
|
||||
|
||||
<!-- More events tooltip -->
|
||||
<div
|
||||
v-if="moreTooltipData.length"
|
||||
ref="moreTooltip"
|
||||
class="fc-more-tooltip"
|
||||
:style="{ left: tooltipPosition.left + 'px', top: tooltipPosition.top + 'px' }"
|
||||
@mouseleave="hideMoreTooltip"
|
||||
>
|
||||
<div
|
||||
v-for="(evt, idx) in moreTooltipData"
|
||||
:key="idx"
|
||||
class="fc-more-tooltip-event"
|
||||
:style="{ borderLeftColor: evt.color }"
|
||||
@click="openEventFromTooltip(evt)"
|
||||
>
|
||||
{{ evt.title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event details modal -->
|
||||
<div v-if="selectedEvent" class="modal-overlay" @click.self="closeEventModal">
|
||||
<div class="modal">
|
||||
<!-- Employee-photo event (recognition/recertification) with highlight -->
|
||||
<div v-if="selectedEvent.extendedProps?.showemployeephoto" class="recognition-header">
|
||||
<div class="recognition-badge">
|
||||
<span class="recognition-icon"><Trophy :size="24" /></span>
|
||||
</div>
|
||||
<div class="recognition-info">
|
||||
<div class="recognition-label">{{ selectedEvent.extendedProps?.typename || 'Recognition' }}</div>
|
||||
<h2 class="recognition-title">{{ selectedEvent.extendedProps?.message || selectedEvent.title }}</h2>
|
||||
<div v-if="selectedEvent.extendedProps?.employeename || selectedEvent.extendedProps?.employeesso" class="recognition-employee">
|
||||
<span class="employee-icon"><User :size="16" /></span>
|
||||
<span class="employee-name">{{ selectedEvent.extendedProps.employeename || selectedEvent.extendedProps.employeesso }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Regular event header -->
|
||||
<h2 v-else>{{ selectedEvent.title }}</h2>
|
||||
|
||||
<div class="event-details">
|
||||
<p v-if="selectedEvent.extendedProps?.typename && !selectedEvent.extendedProps?.showemployeephoto">
|
||||
<strong>Type:</strong> {{ selectedEvent.extendedProps.typename }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Start:</strong> {{ formatDate(selectedEvent.start) }}
|
||||
</p>
|
||||
<p v-if="selectedEvent.end">
|
||||
<strong>End:</strong> {{ formatDate(selectedEvent.end) }}
|
||||
</p>
|
||||
<p v-if="selectedEvent.extendedProps?.message && !selectedEvent.extendedProps?.showemployeephoto" class="message-block">
|
||||
<strong>Details:</strong>
|
||||
<span class="message-text">{{ selectedEvent.extendedProps.message }}</span>
|
||||
</p>
|
||||
<p v-if="selectedEvent.extendedProps?.ticketnumber">
|
||||
<strong>Ticket:</strong> {{ selectedEvent.extendedProps.ticketnumber }}
|
||||
</p>
|
||||
<p v-if="selectedEvent.extendedProps?.linkurl">
|
||||
<a :href="selectedEvent.extendedProps.linkurl" target="_blank" class="btn btn-link">
|
||||
More Info
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<router-link
|
||||
v-if="selectedEvent.extendedProps?.notificationid"
|
||||
:to="`/notifications/${selectedEvent.extendedProps.notificationid}`"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
View Notification
|
||||
</router-link>
|
||||
<button class="btn btn-primary" @click="closeEventModal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import FullCalendar from '@fullcalendar/vue3'
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||
import { Trophy, User } from 'lucide-vue-next'
|
||||
import { notificationsApi } from '@/api'
|
||||
|
||||
const events = ref([])
|
||||
const selectedEvent = ref(null)
|
||||
const calendarRef = ref(null)
|
||||
const moreTooltip = ref(null)
|
||||
const moreTooltipData = ref([])
|
||||
const tooltipPosition = ref({ left: 0, top: 0 })
|
||||
|
||||
// Store events by date for hover lookup
|
||||
const eventsByDate = ref({})
|
||||
|
||||
const calendarOptions = ref({
|
||||
plugins: [dayGridPlugin],
|
||||
initialView: 'dayGridMonth',
|
||||
events: [],
|
||||
eventClick: (info) => {
|
||||
selectedEvent.value = info.event
|
||||
},
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'dayGridMonth,dayGridWeek'
|
||||
},
|
||||
height: 'auto',
|
||||
dayMaxEvents: 3,
|
||||
moreLinkClick: () => 'none' // Disable click, we use hover
|
||||
})
|
||||
|
||||
function hideMoreTooltip() {
|
||||
moreTooltipData.value = []
|
||||
}
|
||||
|
||||
function handleMoreLinkHover(e) {
|
||||
const moreLink = e.target.closest('.fc-daygrid-more-link')
|
||||
if (!moreLink) {
|
||||
return
|
||||
}
|
||||
|
||||
// Find the day cell and get its date
|
||||
const dayCell = moreLink.closest('.fc-daygrid-day')
|
||||
if (!dayCell) return
|
||||
|
||||
const dateStr = dayCell.getAttribute('data-date')
|
||||
if (!dateStr || !eventsByDate.value[dateStr]) return
|
||||
|
||||
// Get events for this date that would be hidden (after first 3)
|
||||
const dayEvents = eventsByDate.value[dateStr]
|
||||
const hiddenEvents = dayEvents.slice(3).map(evt => ({
|
||||
title: evt.title,
|
||||
color: evt.backgroundColor || '#14abef',
|
||||
// Include full event data for modal
|
||||
start: evt.start,
|
||||
end: evt.end,
|
||||
extendedProps: evt.extendedProps || {}
|
||||
}))
|
||||
|
||||
if (hiddenEvents.length === 0) return
|
||||
|
||||
moreTooltipData.value = hiddenEvents
|
||||
|
||||
// Position tooltip
|
||||
const rect = moreLink.getBoundingClientRect()
|
||||
tooltipPosition.value = {
|
||||
left: rect.left,
|
||||
top: rect.bottom + 5
|
||||
}
|
||||
}
|
||||
|
||||
function handleMoreLinkLeave(e) {
|
||||
const related = e.relatedTarget
|
||||
// Don't hide if moving to the tooltip itself
|
||||
if (related && (related.closest('.fc-more-tooltip') || related.closest('.fc-daygrid-more-link'))) {
|
||||
return
|
||||
}
|
||||
hideMoreTooltip()
|
||||
}
|
||||
|
||||
function buildEventsByDate() {
|
||||
const byDate = {}
|
||||
for (const evt of events.value) {
|
||||
const dateStr = evt.start ? evt.start.split('T')[0] : null
|
||||
if (dateStr) {
|
||||
if (!byDate[dateStr]) byDate[dateStr] = []
|
||||
byDate[dateStr].push(evt)
|
||||
}
|
||||
}
|
||||
eventsByDate.value = byDate
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadEvents()
|
||||
|
||||
// Add event delegation for more links
|
||||
await nextTick()
|
||||
const container = document.querySelector('.calendar-container')
|
||||
if (container) {
|
||||
container.addEventListener('mouseenter', handleMoreLinkHover, true)
|
||||
container.addEventListener('mouseleave', handleMoreLinkLeave, true)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
const container = document.querySelector('.calendar-container')
|
||||
if (container) {
|
||||
container.removeEventListener('mouseenter', handleMoreLinkHover, true)
|
||||
container.removeEventListener('mouseleave', handleMoreLinkLeave, true)
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await loadEvents()
|
||||
})
|
||||
|
||||
async function loadEvents() {
|
||||
try {
|
||||
const response = await notificationsApi.getCalendar()
|
||||
events.value = response.data.data
|
||||
calendarOptions.value.events = events.value
|
||||
buildEventsByDate()
|
||||
} catch (error) {
|
||||
console.error('Error loading calendar events:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function closeEventModal() {
|
||||
selectedEvent.value = null
|
||||
}
|
||||
|
||||
function openEventFromTooltip(evt) {
|
||||
// Create an event-like object that matches FullCalendar's event structure
|
||||
selectedEvent.value = {
|
||||
title: evt.title,
|
||||
start: evt.start,
|
||||
end: evt.end,
|
||||
extendedProps: evt.extendedProps
|
||||
}
|
||||
hideMoreTooltip()
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return ''
|
||||
// allDay events carry a site-local date-only value (YYYY-MM-DD). Build the
|
||||
// Date from local parts so it is not shifted a day by UTC-midnight parsing.
|
||||
const parts = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateStr)
|
||||
const date = parts
|
||||
? new Date(Number(parts[1]), Number(parts[2]) - 1, Number(parts[3]))
|
||||
: new Date(dateStr)
|
||||
return date.toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.calendar-container {
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.modal h2 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 18px;
|
||||
color: var(--text);
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.event-details {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.event-details p {
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.event-details p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.event-details strong {
|
||||
color: var(--text-light);
|
||||
display: inline-block;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.message-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.message-block strong {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
display: block;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--bg);
|
||||
border-radius: 0.25rem;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1.25rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Recognition event styling */
|
||||
.recognition-header {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.recognition-badge {
|
||||
flex-shrink: 0;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: linear-gradient(135deg, #ffd700 0%, #ffaa00 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 12px rgba(255, 215, 0, 0.3);
|
||||
}
|
||||
|
||||
.recognition-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.recognition-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.recognition-label {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.recognition-title {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
font-size: 16px !important;
|
||||
line-height: 1.4;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
.recognition-employee {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: rgba(65, 129, 255, 0.1);
|
||||
border-radius: 0.25rem;
|
||||
border-left: 3px solid var(--primary);
|
||||
}
|
||||
|
||||
.employee-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.employee-name {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
font-size: 15px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="page-header">
|
||||
<h1>Calendar</h1>
|
||||
</div>
|
||||
|
||||
<div class="calendar-container card">
|
||||
<FullCalendar :options="calendarOptions" />
|
||||
</div>
|
||||
|
||||
<!-- More events tooltip -->
|
||||
<div
|
||||
v-if="moreTooltipData.length"
|
||||
ref="moreTooltip"
|
||||
class="fc-more-tooltip"
|
||||
:style="{ left: tooltipPosition.left + 'px', top: tooltipPosition.top + 'px' }"
|
||||
@mouseleave="hideMoreTooltip"
|
||||
>
|
||||
<div
|
||||
v-for="(evt, idx) in moreTooltipData"
|
||||
:key="idx"
|
||||
class="fc-more-tooltip-event"
|
||||
:style="{ borderLeftColor: evt.color }"
|
||||
@click="openEventFromTooltip(evt)"
|
||||
>
|
||||
{{ evt.title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event details modal -->
|
||||
<div v-if="selectedEvent" class="modal-overlay" @click.self="closeEventModal">
|
||||
<div class="modal">
|
||||
<!-- Employee-photo event (recognition/recertification) with highlight -->
|
||||
<div v-if="selectedEvent.extendedProps?.showemployeephoto" class="recognition-header">
|
||||
<div class="recognition-badge">
|
||||
<span class="recognition-icon"><Trophy :size="24" /></span>
|
||||
</div>
|
||||
<div class="recognition-info">
|
||||
<div class="recognition-label">{{ selectedEvent.extendedProps?.typename || 'Recognition' }}</div>
|
||||
<h2 class="recognition-title">{{ selectedEvent.extendedProps?.message || selectedEvent.title }}</h2>
|
||||
<div v-if="selectedEvent.extendedProps?.employeename || selectedEvent.extendedProps?.employeesso" class="recognition-employee">
|
||||
<span class="employee-icon"><User :size="16" /></span>
|
||||
<span class="employee-name">{{ selectedEvent.extendedProps.employeename || selectedEvent.extendedProps.employeesso }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Regular event header -->
|
||||
<h2 v-else>{{ selectedEvent.title }}</h2>
|
||||
|
||||
<div class="event-details">
|
||||
<p v-if="selectedEvent.extendedProps?.typename && !selectedEvent.extendedProps?.showemployeephoto">
|
||||
<strong>Type:</strong> {{ selectedEvent.extendedProps.typename }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Start:</strong> {{ formatDate(selectedEvent.start) }}
|
||||
</p>
|
||||
<p v-if="selectedEvent.end">
|
||||
<strong>End:</strong> {{ formatDate(selectedEvent.end) }}
|
||||
</p>
|
||||
<p v-if="selectedEvent.extendedProps?.message && !selectedEvent.extendedProps?.showemployeephoto" class="message-block">
|
||||
<strong>Details:</strong>
|
||||
<span class="message-text">{{ selectedEvent.extendedProps.message }}</span>
|
||||
</p>
|
||||
<p v-if="selectedEvent.extendedProps?.ticketnumber">
|
||||
<strong>Ticket:</strong> {{ selectedEvent.extendedProps.ticketnumber }}
|
||||
</p>
|
||||
<p v-if="selectedEvent.extendedProps?.linkurl">
|
||||
<a :href="selectedEvent.extendedProps.linkurl" target="_blank" class="btn btn-link">
|
||||
More Info
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<router-link
|
||||
v-if="selectedEvent.extendedProps?.notificationid"
|
||||
:to="`/notifications/${selectedEvent.extendedProps.notificationid}`"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
View Notification
|
||||
</router-link>
|
||||
<button class="btn btn-primary" @click="closeEventModal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import FullCalendar from '@fullcalendar/vue3'
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||
import { Trophy, User } from 'lucide-vue-next'
|
||||
import { notificationsApi } from '@/api'
|
||||
|
||||
const events = ref([])
|
||||
const selectedEvent = ref(null)
|
||||
const calendarRef = ref(null)
|
||||
const moreTooltip = ref(null)
|
||||
const moreTooltipData = ref([])
|
||||
const tooltipPosition = ref({ left: 0, top: 0 })
|
||||
|
||||
// Store events by date for hover lookup
|
||||
const eventsByDate = ref({})
|
||||
|
||||
const calendarOptions = ref({
|
||||
plugins: [dayGridPlugin],
|
||||
initialView: 'dayGridMonth',
|
||||
events: [],
|
||||
eventClick: (info) => {
|
||||
selectedEvent.value = info.event
|
||||
},
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'dayGridMonth,dayGridWeek'
|
||||
},
|
||||
// 'auto' sized every row to its own content, so a month of mostly empty days
|
||||
// collapsed into thin strips and the grid looked squashed. A viewport-relative
|
||||
// height makes the calendar fill the space it has been given, and expandRows
|
||||
// shares that height evenly across the weeks rather than leaving a gap under
|
||||
// the last one.
|
||||
height: 'calc(100vh - 230px)',
|
||||
expandRows: true,
|
||||
dayMaxEvents: 3,
|
||||
moreLinkClick: () => 'none' // Disable click, we use hover
|
||||
})
|
||||
|
||||
function hideMoreTooltip() {
|
||||
moreTooltipData.value = []
|
||||
}
|
||||
|
||||
function handleMoreLinkHover(e) {
|
||||
const moreLink = e.target.closest('.fc-daygrid-more-link')
|
||||
if (!moreLink) {
|
||||
return
|
||||
}
|
||||
|
||||
// Find the day cell and get its date
|
||||
const dayCell = moreLink.closest('.fc-daygrid-day')
|
||||
if (!dayCell) return
|
||||
|
||||
const dateStr = dayCell.getAttribute('data-date')
|
||||
if (!dateStr || !eventsByDate.value[dateStr]) return
|
||||
|
||||
// Get events for this date that would be hidden (after first 3)
|
||||
const dayEvents = eventsByDate.value[dateStr]
|
||||
const hiddenEvents = dayEvents.slice(3).map(evt => ({
|
||||
title: evt.title,
|
||||
color: evt.backgroundColor || '#14abef',
|
||||
// Include full event data for modal
|
||||
start: evt.start,
|
||||
end: evt.end,
|
||||
extendedProps: evt.extendedProps || {}
|
||||
}))
|
||||
|
||||
if (hiddenEvents.length === 0) return
|
||||
|
||||
moreTooltipData.value = hiddenEvents
|
||||
|
||||
// Position tooltip
|
||||
const rect = moreLink.getBoundingClientRect()
|
||||
tooltipPosition.value = {
|
||||
left: rect.left,
|
||||
top: rect.bottom + 5
|
||||
}
|
||||
}
|
||||
|
||||
function handleMoreLinkLeave(e) {
|
||||
const related = e.relatedTarget
|
||||
// Don't hide if moving to the tooltip itself
|
||||
if (related && (related.closest('.fc-more-tooltip') || related.closest('.fc-daygrid-more-link'))) {
|
||||
return
|
||||
}
|
||||
hideMoreTooltip()
|
||||
}
|
||||
|
||||
function buildEventsByDate() {
|
||||
const byDate = {}
|
||||
for (const evt of events.value) {
|
||||
const dateStr = evt.start ? evt.start.split('T')[0] : null
|
||||
if (dateStr) {
|
||||
if (!byDate[dateStr]) byDate[dateStr] = []
|
||||
byDate[dateStr].push(evt)
|
||||
}
|
||||
}
|
||||
eventsByDate.value = byDate
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadEvents()
|
||||
|
||||
// Add event delegation for more links
|
||||
await nextTick()
|
||||
const container = document.querySelector('.calendar-container')
|
||||
if (container) {
|
||||
container.addEventListener('mouseenter', handleMoreLinkHover, true)
|
||||
container.addEventListener('mouseleave', handleMoreLinkLeave, true)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
const container = document.querySelector('.calendar-container')
|
||||
if (container) {
|
||||
container.removeEventListener('mouseenter', handleMoreLinkHover, true)
|
||||
container.removeEventListener('mouseleave', handleMoreLinkLeave, true)
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await loadEvents()
|
||||
})
|
||||
|
||||
async function loadEvents() {
|
||||
try {
|
||||
const response = await notificationsApi.getCalendar()
|
||||
events.value = response.data.data
|
||||
calendarOptions.value.events = events.value
|
||||
buildEventsByDate()
|
||||
} catch (error) {
|
||||
console.error('Error loading calendar events:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function closeEventModal() {
|
||||
selectedEvent.value = null
|
||||
}
|
||||
|
||||
function openEventFromTooltip(evt) {
|
||||
// Create an event-like object that matches FullCalendar's event structure
|
||||
selectedEvent.value = {
|
||||
title: evt.title,
|
||||
start: evt.start,
|
||||
end: evt.end,
|
||||
extendedProps: evt.extendedProps
|
||||
}
|
||||
hideMoreTooltip()
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return ''
|
||||
// allDay events carry a site-local date-only value (YYYY-MM-DD). Build the
|
||||
// Date from local parts so it is not shifted a day by UTC-midnight parsing.
|
||||
const parts = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateStr)
|
||||
const date = parts
|
||||
? new Date(Number(parts[1]), Number(parts[2]) - 1, Number(parts[3]))
|
||||
: new Date(dateStr)
|
||||
return date.toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.calendar-container {
|
||||
min-height: 640px;
|
||||
}
|
||||
|
||||
/* :deep, because these are FullCalendar's own elements and scoped styles do not
|
||||
reach them. A floor under each day cell keeps the grid readable on a short
|
||||
window, where the viewport-relative height above would otherwise squeeze the
|
||||
rows back down. */
|
||||
:deep(.fc-daygrid-day-frame) {
|
||||
min-height: 110px;
|
||||
}
|
||||
|
||||
/* The calc() height above is viewport-relative, so on a short window - a laptop
|
||||
with a docked browser, or a shopfloor display in portrait - it can resolve
|
||||
smaller than the grid needs. This stops it undercutting the day cells. */
|
||||
:deep(.fc) {
|
||||
min-height: 620px;
|
||||
}
|
||||
|
||||
/* The day number was tight against the top edge once the cells grew. */
|
||||
:deep(.fc-daygrid-day-top) {
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.modal h2 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 18px;
|
||||
color: var(--text);
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.event-details {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.event-details p {
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.event-details p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.event-details strong {
|
||||
color: var(--text-light);
|
||||
display: inline-block;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.message-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.message-block strong {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
display: block;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--bg);
|
||||
border-radius: 0.25rem;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1.25rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Recognition event styling */
|
||||
.recognition-header {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.recognition-badge {
|
||||
flex-shrink: 0;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: linear-gradient(135deg, #ffd700 0%, #ffaa00 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 12px rgba(255, 215, 0, 0.3);
|
||||
}
|
||||
|
||||
.recognition-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.recognition-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.recognition-label {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.recognition-title {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
font-size: 16px !important;
|
||||
line-height: 1.4;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
.recognition-employee {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: rgba(65, 129, 255, 0.1);
|
||||
border-radius: 0.25rem;
|
||||
border-left: 3px solid var(--primary);
|
||||
}
|
||||
|
||||
.employee-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.employee-name {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
font-size: 15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -275,7 +275,12 @@ const form = ref({
|
||||
starttime: '',
|
||||
endtime: '',
|
||||
isactive: true,
|
||||
isshopfloor: false,
|
||||
// Ticked for a NEW notification: the shopfloor board is where these are
|
||||
// meant to be seen, and leaving it off by default meant most were written
|
||||
// and then never appeared there. Editing an EXISTING notification still
|
||||
// loads its own stored value (see loadNotification), so this never flips a
|
||||
// setting somebody deliberately turned off.
|
||||
isshopfloor: true,
|
||||
employeesso: ''
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user