508 lines
15 KiB
JavaScript
508 lines
15 KiB
JavaScript
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: `
|
|
<div class="app-shell-root">
|
|
<q-layout view="hHh Lpr fFf">
|
|
<app-header></app-header>
|
|
<left-drawer></left-drawer>
|
|
<filter-drawer></filter-drawer>
|
|
<detail-drawer></detail-drawer>
|
|
<planner-page></planner-page>
|
|
</q-layout>
|
|
</div>
|
|
`
|
|
};
|