Files
hotline-planner/dev/ui-ux/Codex : deepseek (not good)/src/components/app-shell/app-shell.js
2026-02-24 13:32:01 +01:00

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>
`
};