Files
hotline-planner/component split/not good/hlp q gemini code assist split/planner-grid.js
2026-02-23 12:27:26 +01:00

146 lines
15 KiB
JavaScript

import { ref, computed, nextTick } from 'https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js';
import { useQuasar } from 'https://cdn.jsdelivr.net/npm/quasar@2.16.0/dist/quasar.umd.prod.js';
import { usePlannerState } from '../../services/planner-state-service.js';
import { HUBS } from '../../services/data-constants.js';
export default {
name: 'PlannerGrid',
emits: ['toggle-filter-drawer'],
setup(props, { emit }) {
const { state, methods, getters } = usePlannerState();
const $q = useQuasar();
const viewport = ref(null);
const dateMenu = ref(false);
const proxyDate = ref(null);
const gridStyles = computed(() => ({
'--h-eod': state.showEodTargets ? '42px' : '0px',
'--h-status': state.showAvailability ? '34px' : '0px'
}));
const isFilterActive = computed(() => state.filterRoles.length > 0 || state.filterSkills.length > 0 || state.activeDept !== 'All' || state.activeHub !== 'All' || (state.search && state.search.length > 0) || state.filterByAvailability);
const activeFilterCount = computed(() => {
let count = 0;
if (state.search && state.search.length > 0) count++;
if (state.activeDept !== 'All') count++;
if (state.activeHub !== 'All') count++;
if (state.filterRoles.length > 0) count++;
if (state.filterSkills.length > 0) count++;
if (state.filterByAvailability) count++;
return count;
});
const flattenedList = computed(() => {
const result = [];
const list = [...getters.filteredAgents.value];
const keys = state.groupingSelection;
const getLabel = (key, value) => {
if (key === 'hub') return HUBS.find(h => h.id === value)?.name || value;
if (key === 'dept') return value.toUpperCase() + ' DIVISION';
if (key === 'role') return value + 's';
return value;
};
if (keys.length === 0) return list.map(a => ({ type: 'agent', data: a, id: a.id }));
list.sort((a, b) => {
for (const key of keys) {
if (a[key] < b[key]) return -1;
if (a[key] > b[key]) return 1;
}
return 0;
});
const currentValues = keys.map(() => null);
list.forEach(agent => {
let changedLevel = -1;
for (let i = 0; i < keys.length; i++) {
if (agent[keys[i]] !== currentValues[i]) { changedLevel = i; break; }
}
if (changedLevel !== -1) {
for (let i = changedLevel; i < keys.length; i++) {
const key = keys[i];
const val = agent[key];
currentValues[i] = val;
result.push({ type: i === 0 ? 'header-l1' : 'header-l2', label: getLabel(key, val), id: `hdr-${key}-${val}-${Math.random()}` });
}
}
result.push({ type: 'agent', data: agent, id: agent.id });
});
return result;
});
const getCellClass = (agentId, date) => {
const isRow = state.highlightedRowId === agentId;
const isCol = state.highlightedDateStr === methods.formatDateForId(date);
const isHoverCol = state.crosshairActive && state.hoveredDateStr === methods.formatDateForId(date);
const isWknd = methods.isWeekend(date) && !state.weekendsAreWorkingDays;
const isLocked = methods.isCellLocked(agentId, date);
const classes = [];
if (state.isCompact) classes.push('planner-grid-cell-compact');
if (isWknd) classes.push('planner-grid-bg-weekend');
if (isWknd || isLocked) classes.push('cursor-not-allowed');
else classes.push('cursor-pointer');
if (isRow && isCol) classes.push('planner-grid-bg-reading-mode-intersection');
else if (isRow || isCol) classes.push('planner-grid-bg-reading-mode');
if (isHoverCol) classes.push('planner-grid-col-hovered');
return classes.join(' ');
};
const applyDateSelection = () => {
methods.applyDateSelection(proxyDate.value);
dateMenu.value = false;
nextTick(() => { if (viewport.value) viewport.value.scrollLeft = 0; });
};
return {
state,
methods,
getters,
$q,
viewport,
gridStyles,
isFilterActive,
activeFilterCount,
flattenedList,
getCellClass,
dateMenu,
proxyDate,
syncProxyDate: () => { proxyDate.value = methods.formatDateForId(state.selectedDate || new Date()); },
applyDateSelection,
toggleFilterDrawer: () => emit('toggle-filter-drawer'),
};
},
template: `
<q-page class="q-pa-none">
<q-inner-loading :showing="state.loading"><q-spinner-gears size="50px" color="indigo-8" /><div class="text-indigo-8 q-mt-sm">Loading Agents...</div></q-inner-loading>
<div id="viewport-target" class="planner-grid-viewport" ref="viewport" :style="gridStyles">
<div class="planner-grid-content">
<div v-if="state.showEodTargets" class="planner-grid-row planner-grid-row-eod"><div class="planner-grid-left-col text-overline text-weight-bold">EOD Targets</div><div class="planner-grid-cells-area"><div v-for="(d, i) in getters.dates.value" :key="'e'+i" class="planner-grid-cell" :class="{'planner-grid-col-hovered': state.crosshairActive && state.hoveredDateStr === methods.formatDateForId(d)}" :style="{height: 'var(--h-eod)'}"><div class="text-weight-bold text-[10px] text-pink-7">94%</div><q-linear-progress :value="0.94" color="pink-3" size="3px" rounded style="width: 60%"></q-linear-progress></div></div></div>
<div v-if="state.showAvailability" class="planner-grid-row planner-grid-row-status"><div class="planner-grid-left-col text-overline text-grey-5">Availability</div><div class="planner-grid-cells-area"><div v-for="(d, i) in getters.dates.value" :key="'s'+i" class="planner-grid-cell" :class="{'planner-grid-col-hovered': state.crosshairActive && state.hoveredDateStr === methods.formatDateForId(d)}" :style="{height: 'var(--h-status)'}"><q-icon name="circle" :color="i % 10 === 0 ? 'red-3' : (i % 5 === 0 ? 'orange-3' : 'green-3')" size="8px"></q-icon></div></div></div>
<div class="planner-grid-row planner-grid-row-dates">
<div class="planner-grid-left-col"><div class="planner-grid-search-container"><q-input v-model="state.search" debounce="300" dense outlined rounded :placeholder="$q.screen.lt.sm ? 'Search' : 'Quick search...'" class="full-width" bg-color="white" clearable><template v-slot:prepend><q-icon name="search" size="xs" color="grey-5"></q-icon></template></q-input><q-btn flat round dense icon="filter_alt" :color="isFilterActive ? 'indigo-10' : 'grey-6'" :class="isFilterActive ? 'planner-filter-drawer-btn-active' : ''" size="sm" @click="toggleFilterDrawer"><div v-if="isFilterActive" class="planner-filter-drawer-status-dot"></div><q-tooltip>{{ isFilterActive ? activeFilterCount + ' filters active' : 'Advanced Filters' }}</q-tooltip></q-btn><q-btn flat round dense icon="calendar_today" color="indigo-7" size="sm"><q-menu v-model="dateMenu" @show="syncProxyDate"><q-card class="planner-edit-drawer-dense-date-card shadow-12 border"><q-card-section class="q-pa-none"><q-date v-model="proxyDate" minimal flat color="indigo-8" :first-day-of-week="state.pickerStartDay"></q-date></q-card-section><q-card-actions align="right" class="q-pt-none q-pb-sm q-px-md"><q-btn flat dense rounded label="Cancel" color="grey-7" @click="dateMenu = false"></q-btn><q-btn unelevated dense rounded label="OK" color="indigo-8" class="q-px-md" @click="applyDateSelection"></q-btn></q-card-actions></q-card></q-menu></q-btn></div></div>
<div class="planner-grid-cells-area"><div v-for="(date, i) in getters.dates.value" :key="'d'+i" class="planner-grid-cell column flex-center relative-position planner-grid-date-header-trigger" :class="[methods.isWeekend(date) && !state.weekendsAreWorkingDays ? 'planner-grid-bg-weekend' : '', state.highlightedDateStr === methods.formatDateForId(date) ? 'planner-grid-reading-active-header' : '', state.crosshairActive && state.hoveredDateStr === methods.formatDateForId(date) ? 'planner-grid-col-hovered' : '']" :style="{height: 'var(--h-dates)'}" @mouseenter="state.hoveredDateStr = methods.formatDateForId(date)" @mouseleave="state.hoveredDateStr = null"><div class="absolute-top-right q-pa-xs"><q-icon v-if="methods.getHoliday(date)" name="celebration" size="10px" color="red-5" class="cursor-help"><q-tooltip class="bg-red-9 text-white shadow-4 q-pa-sm">Holiday: {{ methods.getHoliday(date) }}</q-tooltip></q-icon><q-icon v-else-if="methods.getSpecialDay(date)" name="bookmark" size="10px" color="indigo-4" class="cursor-help"><q-tooltip class="bg-indigo-9 text-white shadow-4 q-pa-sm">Event: {{ methods.getSpecialDay(date) }}</q-tooltip></q-icon></div><div class="text-[10px] uppercase text-weight-bold" :class="[methods.isWeekend(date) && !state.weekendsAreWorkingDays ? (date.getDay() === 6 ? 'text-sat' : 'text-sun') : 'text-grey-5']">{{ date.toLocaleDateString('en-US', {weekday: 'short'}) }}</div><div class="text-subtitle2 text-weight-bold leading-none" :class="[methods.isWeekend(date) && !state.weekendsAreWorkingDays ? (date.getDay() === 6 ? 'text-sat' : 'text-sun') : 'text-grey-9']">{{ date.getDate() }}. {{ date.toLocaleDateString('en-US', { month: 'short' }) }}</div><q-btn round flat dense :icon="state.highlightedDateStr === methods.formatDateForId(date) ? 'visibility_off' : 'visibility'" :color="state.highlightedDateStr === methods.formatDateForId(date) ? 'amber-9' : 'grey-5'" size="sm" class="planner-grid-header-highlight-btn absolute-bottom-right q-ma-xs" :class="{ 'is-active': state.highlightedDateStr === methods.formatDateForId(date) }" @click.stop="methods.toggleColHighlight(date)"><q-tooltip>{{ state.highlightedDateStr === methods.formatDateForId(date) ? 'Turn off column mode' : 'Highlight Column' }}</q-tooltip></q-btn><div v-if="state.highlightedDateStr === methods.formatDateForId(date)" class="absolute-bottom full-width bg-amber-8" style="height: 3px;"></div></div></div>
</div>
<div v-if="state.loading" class="q-pa-none"><div v-for="n in 18" :key="n" class="planner-grid-row"><div class="planner-grid-left-col border-b" style="height: 59px"><div class="row items-center full-width"><q-skeleton type='QAvatar' size='32px' class='q-mr-sm' /><div class='col'><q-skeleton type='text' width='60%' /><q-skeleton type='text' width='40%' height='10px' /></div></div></div><div class="planner-grid-cells-area"><div v-for="c in 8" :key="c" class="planner-grid-cell"><q-skeleton v-if="Math.random() > 0.6" type="rect" height="20px" width="80%" class="rounded-borders" style="opacity: 0.3" /></div></div></div></div>
<q-virtual-scroll v-else scroll-target="#viewport-target" :items="flattenedList" :item-size="state.isCompact ? 43 : 59">
<template v-slot="{ item }">
<div v-if="item.type === 'header-l1'" :key="item.id" class="planner-grid-row planner-grid-group-header-l1"><div class="planner-grid-left-col">{{ item.label }}</div><div class="planner-grid-cells-area"><div class="planner-grid-cell" style="flex: 1; height: 36px; border-right: none"></div></div></div>
<div v-else-if="item.type === 'header-l2'" :key="item.id" class="planner-grid-row planner-grid-group-header-l2"><div class="planner-grid-left-col">{{ item.label }}</div><div class="planner-grid-cells-area"><div class="planner-grid-cell" style="flex: 1; height: 32px; border-right: none"></div></div></div>
<div v-else :key="item.id" class="planner-grid-row planner-grid-row-item" :class="{'planner-grid-row-highlighted': state.highlightedRowId === item.data.id, 'crosshair-enabled': state.crosshairActive}">
<div class="planner-grid-left-col border-b cursor-pointer planner-grid-agent-row-trigger relative-position" :class="[state.isCompact ? 'planner-grid-cell-compact' : '']" @click="methods.openProfile(item.data)"><q-avatar :size="state.isCompact ? '24px' : '32px'" class="shadow-1"><img :src="item.data.avatar"></q-avatar><div class="q-ml-sm overflow-hidden col"><div class="text-weight-bold truncate" :style="{fontSize: state.isCompact ? '11px' : '13px'}">{{ item.data.name }}</div><div v-if="!state.isCompact" class="text-[10px] text-grey-5 uppercase text-weight-bold truncate planner-grid-agent-role">{{ item.data.role }}</div></div><q-btn round flat dense :icon="state.highlightedRowId === item.data.id ? 'visibility_off' : 'visibility'" :color="state.highlightedRowId === item.data.id ? 'amber-9' : 'grey-4'" size="sm" class="planner-grid-agent-highlight-btn" :class="{ 'is-active': state.highlightedRowId === item.data.id }" @click.stop="methods.toggleRowHighlight(item.data.id)"><q-tooltip>{{ state.highlightedRowId === item.data.id ? 'Turn off reading mode' : 'Highlight Row' }}</q-tooltip></q-btn></div>
<div class="planner-grid-cells-area"><div v-for="(date, i) in getters.dates.value" :key="'c'+item.data.id+i" class="planner-grid-cell" :class="getCellClass(item.data.id, date)" @mouseenter="state.hoveredDateStr = methods.formatDateForId(date)" @mouseleave="state.hoveredDateStr = null" @click="methods.openAssignment(item.data, date)"><template v-if="state.weekendsAreWorkingDays || !methods.isWeekend(date)"><div v-if="methods.getAssignment(item.data.id, date)" class="planner-grid-shift-badge shadow-1" :class="methods.getAssignment(item.data.id, date).badgeClass">{{ methods.getAssignment(item.data.id, date).label }}</div></template><div v-if="methods.isCellLocked(item.data.id, date)" class="planner-grid-locked-overlay"><q-icon name="lock" color="blue-grey-3" size="14px"><q-tooltip class="bg-grey-9 text-white shadow-4 q-pa-sm">This cell is currently being edited by another user.</q-tooltip></q-icon></div><div class="absolute-bottom-right q-pa-xs row no-wrap" style="gap: 2px"><q-icon v-if="methods.hasComment(item.data.id, date)" name="chat_bubble" size="8px" color="blue-grey-3" class="cursor-help opacity-60"><q-tooltip class="bg-white text-grey-9 border shadow-4 planner-edit-drawer-shift-tooltip" anchor="top middle" self="bottom middle"><div class="text-weight-bold text-caption text-indigo-9 q-mb-xs">User Comment</div><div class="text-caption text-grey-8">{{ methods.getCommentText(item.data.id, date) }}</div></q-tooltip></q-icon><q-icon v-if="methods.hasNote(item.data.id, date)" name="info" size="8px" color="orange-4" class="cursor-help opacity-70"><q-tooltip class="bg-white text-grey-9 border shadow-4 planner-edit-drawer-shift-tooltip" anchor="top middle" self="bottom middle"><div class="text-weight-bold text-caption text-orange-9 q-mb-xs">Technical Note</div><div class="text-caption text-grey-8">{{ methods.getNoteText(item.data.id, date) }}</div></q-tooltip></q-icon></div></div></div>
</div>
</template>
</q-virtual-scroll>
</div>
</div>
</q-page>`
};