import AppHeader from '../app-header/app-header.js'; import LeftDrawer from '../left-drawer/left-drawer.js'; import FilterDrawer from '../filter-drawer/filter-drawer.js'; import DetailDrawer from '../detail-drawer/detail-drawer.js'; import PlannerPage from '../planner-page/planner-page.js'; import { HUBS, DEPARTMENTS, ROLES, SKILLS, SHIFTS, agents, loading, lockedCells, assignments, comments, notes, holidays, specialDays, loadDataFromDatabase, simulateWssLock, onlineUsers, startOnlineUsersSimulation } from '../../services/socket-service.js'; const { ref, computed, nextTick, onMounted, onUnmounted, provide } = Vue; const { useQuasar } = Quasar; export default { name: 'AppShell', components: { AppHeader, LeftDrawer, FilterDrawer, DetailDrawer, PlannerPage }, setup() { const $q = useQuasar(); const leftDrawer = ref(false); const filterDrawer = ref(false); const rightDrawer = ref(false); const editMode = ref('assignment'); const viewport = ref(null); const isCompact = ref(false); const weekendsAreWorkingDays = ref(false); const viewScope = ref(8); const pickerStartDay = ref(1); const search = ref(''); const activeDept = ref('All'); const activeHub = ref('All'); const filterRoles = ref([]); const filterSkills = ref([]); const groupingSelection = ref(['hub', 'dept']); const groupingOptions = [ { label: 'Hub > Division (Default)', value: ['hub', 'dept'] }, { label: 'Division > Role', value: ['dept', 'role'] }, { label: 'Hub > Role', value: ['hub', 'role'] }, { label: 'Division (Flat)', value: ['dept'] }, { label: 'Role (Flat)', value: ['role'] }, { label: 'No Grouping (Flat)', value: [] } ]; const hubOptions = [ { label: 'All Hubs', value: 'All' }, ...HUBS.map(h => ({ label: h.name, value: h.id })) ]; const showEodTargets = ref(false); const showAvailability = ref(false); const selectedAgent = ref(null); const selectedDate = ref(null); const pendingShift = ref(null); const dateMenu = ref(false); const proxyDate = ref(null); const highlightedRowId = ref(null); const highlightedDateStr = ref(null); const crosshairActive = ref(true); const hoveredDateStr = ref(null); const filterByAvailability = ref(false); const formatDateForId = (date) => { if (!date) return ''; const d = new Date(date); const month = '' + (d.getMonth() + 1); const day = '' + d.getDate(); const year = d.getFullYear(); return [year, month.padStart(2, '0'), day.padStart(2, '0')].join('-'); }; const parseDateId = (dateStr) => { if (!dateStr || typeof dateStr !== 'string') return null; const normalized = dateStr.replace(/\//g, '-'); const [y, m, d] = normalized.split('-').map(Number); if (!y || Number.isNaN(y) || Number.isNaN(m) || Number.isNaN(d)) return null; const parsed = new Date(y, (m || 1) - 1, d || 1); return Number.isNaN(parsed.getTime()) ? null : parsed; }; const filterDate = ref(formatDateForId(new Date())); const proxyFilterDate = ref(''); const filterShiftTypes = ref([]); const getStartOfWeek = (date) => { const d = new Date(date); const day = d.getDay(); const diff = (day < 1 ? 7 : 0) + day - 1; d.setDate(d.getDate() - diff); d.setHours(0, 0, 0, 0); return d; }; const startDate = ref(getStartOfWeek(new Date())); const isWeekend = (date) => { const day = date.getDay(); return day === 0 || day === 6; }; const dates = computed(() => { const res = []; const now = new Date(startDate.value); for (let i = 0; i < viewScope.value * 7; i++) { const d = new Date(now); d.setDate(now.getDate() + i); res.push(d); } return res; }); const notify = (payload) => $q.notify(payload); const simulateWssLockAction = () => { simulateWssLock(notify); }; const isCellLocked = (agentId, date) => { const key = `${agentId}:${formatDateForId(date)}`; return lockedCells.value.has(key); }; const getAssignment = (agentId, date) => { const dateStr = formatDateForId(date); if (assignments[agentId] && assignments[agentId][dateStr] !== undefined) { return SHIFTS.find(s => s.id === assignments[agentId][dateStr]); } return null; }; const getHoliday = (date) => holidays[formatDateForId(date)] || null; const getSpecialDay = (date) => specialDays[formatDateForId(date)] || null; const hasComment = (agentId, date) => { const dStr = formatDateForId(date); return comments[agentId] && comments[agentId][dStr]; }; const hasNote = (agentId, date) => { const dStr = formatDateForId(date); return notes[agentId] && notes[agentId][dStr]; }; const getCommentText = (agentId, date) => { const dStr = formatDateForId(date); return comments[agentId] ? comments[agentId][dStr] : ''; }; const getNoteText = (agentId, date) => { const dStr = formatDateForId(date); return notes[agentId] ? notes[agentId][dStr] : ''; }; const isFilterActive = computed(() => filterRoles.value.length > 0 || filterSkills.value.length > 0 || activeDept.value !== 'All' || activeHub.value !== 'All' || (search.value && search.value.length > 0) || filterByAvailability.value ); const activeFilterCount = computed(() => { let count = 0; if (search.value && search.value.length > 0) count++; if (activeDept.value !== 'All') count++; if (activeHub.value !== 'All') count++; if (filterRoles.value.length > 0) count++; if (filterSkills.value.length > 0) count++; if (filterByAvailability.value) count++; return count; }); const filteredAgents = computed(() => { return agents.value.filter(a => { const term = (search.value || '').toLowerCase(); const matchSearch = term === '' || a.name.toLowerCase().includes(term); const matchDept = activeDept.value === 'All' || a.dept === activeDept.value; const matchHub = activeHub.value === 'All' || a.hub === activeHub.value; const matchRoles = filterRoles.value.length === 0 || filterRoles.value.includes(a.role); const matchSkills = filterSkills.value.length === 0 || a.skills.some(s => filterSkills.value.includes(s)); let matchAvailability = true; if (filterByAvailability.value && filterDate.value) { const d = parseDateId(filterDate.value); const assignment = getAssignment(a.id, d); if (!weekendsAreWorkingDays.value && isWeekend(d)) { matchAvailability = false; } else if (!assignment) { matchAvailability = false; } else if ( filterShiftTypes.value.length > 0 && !filterShiftTypes.value.includes(assignment.id) ) { matchAvailability = false; } } return matchSearch && matchDept && matchHub && matchRoles && matchSkills && matchAvailability; }); }); const flattenedList = computed(() => { const result = []; const list = [...filteredAgents.value]; const keys = groupingSelection.value; 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++) { const key = keys[i]; if (agent[key] !== 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; const type = i === 0 ? 'header-l1' : 'header-l2'; const path = keys .slice(0, i + 1) .map((k, idx) => `${k}:${currentValues[idx]}`) .join('|'); result.push({ type: type, label: getLabel(key, val), id: `hdr-${path}` }); } } result.push({ type: 'agent', data: agent, id: agent.id }); }); return result; }); const currentAssignmentLabel = computed(() => { if (pendingShift.value) return pendingShift.value.label; if (!selectedAgent.value || !selectedDate.value) return null; const a = getAssignment(selectedAgent.value.id, selectedDate.value); return a ? a.label : null; }); const clearFilters = () => { search.value = ''; activeDept.value = 'All'; activeHub.value = 'All'; filterRoles.value = []; filterSkills.value = []; filterByAvailability.value = false; filterShiftTypes.value = []; filterDate.value = formatDateForId(new Date()); }; const applySavedFilter = (key) => { clearFilters(); if (key === 'high_potential') { filterSkills.value = ['VIP Concierge', 'Technical Training']; } else if (key === 'remote') { filterRoles.value = ['Specialist']; } }; const syncProxyDate = () => { proxyDate.value = formatDateForId(selectedDate.value || new Date()); }; const applyDateSelection = () => { if (!proxyDate.value) return; const target = parseDateId(proxyDate.value); if (!target) return; startDate.value = getStartOfWeek(target); if (rightDrawer.value && editMode.value === 'assignment' && selectedAgent.value) { selectedDate.value = target; pendingShift.value = null; } dateMenu.value = false; nextTick(() => { if (viewport.value) viewport.value.scrollLeft = 0; }); }; const resetToToday = () => { const today = new Date(); startDate.value = getStartOfWeek(today); if (rightDrawer.value && editMode.value === 'assignment') { selectedDate.value = today; pendingShift.value = null; } if (viewport.value) viewport.value.scrollLeft = 0; leftDrawer.value = false; }; const updateFilterDateProxy = () => { proxyFilterDate.value = filterDate.value; }; const applyFilterDate = () => { filterDate.value = proxyFilterDate.value; }; const openAssignment = (agent, date) => { if (isCellLocked(agent.id, date) || (!weekendsAreWorkingDays.value && isWeekend(date))) return; editMode.value = 'assignment'; selectedAgent.value = agent; selectedDate.value = date; pendingShift.value = null; rightDrawer.value = true; }; const openProfile = (agent) => { editMode.value = 'profile'; selectedAgent.value = { ...agent }; selectedDate.value = null; rightDrawer.value = true; }; const setPendingShift = (shift) => { pendingShift.value = shift; }; const saveAssignment = () => { if (!selectedAgent.value || !selectedDate.value) return; const agentId = selectedAgent.value.id; const dateStr = formatDateForId(selectedDate.value); if (!assignments[agentId]) assignments[agentId] = {}; assignments[agentId][dateStr] = pendingShift.value ? pendingShift.value.id : null; pendingShift.value = null; rightDrawer.value = false; }; const toggleRowHighlight = (agentId) => { highlightedRowId.value = highlightedRowId.value === agentId ? null : agentId; }; const toggleColHighlight = (date) => { const str = formatDateForId(date); highlightedDateStr.value = highlightedDateStr.value === str ? null : str; }; const clearHighlights = () => { highlightedRowId.value = null; highlightedDateStr.value = null; }; const hasHighlights = computed(() => Boolean(highlightedRowId.value || highlightedDateStr.value)); const toggleLeftDrawer = () => { leftDrawer.value = !leftDrawer.value; }; let stopOnlineUsersSimulation = null; onMounted(() => { loadDataFromDatabase(startDate.value, notify); stopOnlineUsersSimulation = startOnlineUsersSimulation(notify); }); onUnmounted(() => { if (stopOnlineUsersSimulation) stopOnlineUsersSimulation(); }); const appState = { leftDrawer, filterDrawer, rightDrawer, editMode, viewport, isCompact, weekendsAreWorkingDays, viewScope, pickerStartDay, search, activeDept, activeHub, filterRoles, filterSkills, onlineUsers, agents, loading, assignments, comments, notes, holidays, specialDays, lockedCells, shifts: SHIFTS, depts: DEPARTMENTS, roles: ROLES, allSkills: SKILLS, groupingSelection, groupingOptions, hubOptions, showEodTargets, showAvailability, selectedAgent, selectedDate, pendingShift, dateMenu, proxyDate, highlightedRowId, highlightedDateStr, crosshairActive, hoveredDateStr, filterByAvailability, filterDate, proxyFilterDate, filterShiftTypes, dates, formatDateForId, isWeekend, isCellLocked, getAssignment, getHoliday, getSpecialDay, hasComment, hasNote, getCommentText, getNoteText, isFilterActive, activeFilterCount, filteredAgents, flattenedList, currentAssignmentLabel, clearFilters, applySavedFilter, syncProxyDate, applyDateSelection, resetToToday, updateFilterDateProxy, applyFilterDate, openAssignment, openProfile, setPendingShift, saveAssignment, toggleRowHighlight, toggleColHighlight, clearHighlights, hasHighlights, simulateWssLockAction, toggleLeftDrawer }; provide('appState', appState); return appState; }, template: `