Added new modules and updated existing logic

This commit is contained in:
Dieter Neumann
2026-02-24 13:32:01 +01:00
parent 2a4b4ed5fe
commit ad734273ce
694 changed files with 27935 additions and 610 deletions

View File

@@ -0,0 +1,246 @@
/**
* data-service.js
* ================
* Domain constants, mock-data generation, and reactive data stores
* for agents, assignments, comments, notes, holidays, and special days.
*
* In production, replace generateMockAgents / loadDataFromDatabase
* with real ASP.NET Core API calls.
*/
const { ref, computed, reactive } = Vue;
import {
formatDateForId,
startDate,
weekendsAreWorkingDays,
isWeekend,
filterByAvailability,
filterDate,
filterShiftTypes,
search,
activeDept,
activeHub,
filterRoles,
filterSkills
} from './planner-state.js';
/* ===== CONSTANTS ===== */
export const HUBS = [
{ id: 'DE', name: 'GERMANY HUB (DE)' },
{ id: 'IT', name: 'ITALY HUB (IT)' },
{ id: 'FR', name: 'FRANCE HUB (FR)' },
{ id: 'GB', name: 'UNITED KINGDOM (GB)' },
{ id: 'ES', name: 'SPAIN HUB (ES)' },
{ id: 'AE', name: 'UAE HUB (AE)' },
{ id: 'PL', name: 'POLAND HUB (PL)' }
];
export const DEPARTMENTS = ['Support', 'Technical', 'Sales', 'VIP', 'Billing'];
export const ROLES = ['Senior Lead', 'Specialist', 'Agent'];
export const SKILLS = ['English', 'German', 'French', 'Molecular App', 'Hardware', 'Billing Specialist', 'VIP Concierge', 'Technical Training'];
export const SHIFTS = [
{ id: 'm', label: 'MORNING', color: 'green-7', badgeClass: 'bg-green-1 text-green-9 border-green-2' },
{ id: 'a', label: 'AFTERNOON', color: 'blue-7', badgeClass: 'bg-blue-1 text-blue-9 border-blue-2' },
{ id: 'h', label: 'HOTLINE', color: 'indigo-7', badgeClass: 'bg-indigo-1 text-indigo-9 border-indigo-2' },
{ id: 'e', label: 'EOD ONLY', color: 'pink-7', badgeClass: 'bg-pink-1 text-pink-9 border-pink-2' }
];
export const hubOptions = [
{ label: 'All Hubs', value: 'All' },
...HUBS.map(h => ({ label: h.name, value: h.id }))
];
const MOCK_COMMENTS_TEXT = ['Late arrival expected.', 'Dental appointment.', 'Swapped shift.', 'Priority focus.', 'Remote session.'];
const MOCK_NOTES_TEXT = ['Headset check.', 'VPN slow.', 'Training session.', 'Backup Billing.', 'Overtime pending.'];
/* ===== REACTIVE DATA STORES ===== */
export const agents = ref([]);
export const loading = ref(false);
export const assignments = reactive({});
export const comments = reactive({});
export const notes = reactive({});
export const holidays = reactive({});
export const specialDays = reactive({});
/* ===== MOCK GENERATOR ===== */
const generateMockAgents = (count) => {
return Array.from({ length: count }, (_, i) => {
const hub = HUBS[Math.floor(Math.random() * HUBS.length)];
return {
id: i + 1,
name: `Agent ${i + 1}`,
dept: DEPARTMENTS[i % DEPARTMENTS.length],
role: ROLES[i % ROLES.length],
hub: hub.id,
hubName: hub.name,
skills: [SKILLS[i % SKILLS.length], SKILLS[(i + 2) % SKILLS.length]],
avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=Agent${i}`
};
});
};
/* ===== DATABASE LOADER (mock) ===== */
export const loadDataFromDatabase = ($q) => {
loading.value = true;
setTimeout(() => {
const fetchedAgents = generateMockAgents(800);
agents.value = fetchedAgents;
const mockStartDate = new Date(startDate.value);
fetchedAgents.forEach(agent => {
if (!assignments[agent.id]) assignments[agent.id] = {};
if (!comments[agent.id]) comments[agent.id] = {};
if (!notes[agent.id]) notes[agent.id] = {};
for (let i = 0; i < 60; i++) {
const d = new Date(mockStartDate);
d.setDate(d.getDate() + i);
const dStr = formatDateForId(d);
if ((agent.id + i) % 7 === 0) assignments[agent.id][dStr] = SHIFTS[0].id;
else if ((agent.id + i) % 5 === 0) assignments[agent.id][dStr] = SHIFTS[1].id;
if ((agent.id + i) % 20 === 0) comments[agent.id][dStr] = MOCK_COMMENTS_TEXT[(agent.id + i) % MOCK_COMMENTS_TEXT.length];
if ((agent.id + i) % 25 === 0) notes[agent.id][dStr] = MOCK_NOTES_TEXT[(agent.id + i) % MOCK_NOTES_TEXT.length];
}
});
// Dynamic holidays / special days relative to today
for (const key in holidays) delete holidays[key];
for (const key in specialDays) delete specialDays[key];
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
holidays[formatDateForId(tomorrow)] = 'Regional Holiday';
const eventDay = new Date(today);
eventDay.setDate(today.getDate() + 5);
specialDays[formatDateForId(eventDay)] = 'Quarterly Planning';
loading.value = false;
$q.notify({ message: 'Data Loaded: 800 Agents', color: 'positive', position: 'top', timeout: 1000 });
}, 2000);
};
/* ===== ACCESSORS ===== */
export 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;
};
export const getHoliday = (date) => holidays[formatDateForId(date)] || null;
export const getSpecialDay = (date) => specialDays[formatDateForId(date)] || null;
export const hasComment = (agentId, date) => { const d = formatDateForId(date); return comments[agentId] && comments[agentId][d]; };
export const hasNote = (agentId, date) => { const d = formatDateForId(date); return notes[agentId] && notes[agentId][d]; };
export const getCommentText = (agentId, date) => { const d = formatDateForId(date); return comments[agentId] ? comments[agentId][d] : ''; };
export const getNoteText = (agentId, date) => { const d = formatDateForId(date); return notes[agentId] ? notes[agentId][d] : ''; };
/* ===== FILTERING ===== */
export 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 = new Date(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;
});
});
/* ===== GROUPING ===== */
export const flattenedList = computed(() => {
const result = [];
const list = [...filteredAgents.value];
const keys = ['hub', 'dept']; // default grouping
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;
});
/* ===== ASSIGNMENT ACTIONS ===== */
export const saveAssignment = (selectedAgentRef, selectedDateRef, pendingShiftRef, rightDrawerRef) => {
if (!selectedAgentRef.value || !selectedDateRef.value) return;
const agentId = selectedAgentRef.value.id;
const dateStr = formatDateForId(selectedDateRef.value);
if (!assignments[agentId]) assignments[agentId] = {};
assignments[agentId][dateStr] = pendingShiftRef.value ? pendingShiftRef.value.id : null;
pendingShiftRef.value = null;
rightDrawerRef.value = false;
};

View File

@@ -0,0 +1,178 @@
/**
* planner-state.js
* ================
* Central reactive store consumed by every component.
* No UI — pure state + derived computations.
*/
const { ref, computed, reactive } = Vue;
/* ---- helpers ---- */
export 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('-');
};
export 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;
};
export const isWeekend = (date) => {
const day = date.getDay();
return day === 0 || day === 6;
};
/* ---- drawer / panel toggles ---- */
export const leftDrawer = ref(false);
export const filterDrawer = ref(false);
export const rightDrawer = ref(false);
export const editMode = ref('assignment');
export const dateMenu = ref(false);
/* ---- grid display settings ---- */
export const isCompact = ref(false);
export const weekendsAreWorkingDays = ref(false);
export const viewScope = ref(8);
export const pickerStartDay = ref(1);
export const showEodTargets = ref(false);
export const showAvailability = ref(false);
/* ---- crosshair / highlighting ---- */
export const crosshairActive = ref(true);
export const hoveredDateStr = ref(null);
export const highlightedRowId = ref(null);
export const highlightedDateStr = ref(null);
export const toggleRowHighlight = (agentId) => {
highlightedRowId.value = highlightedRowId.value === agentId ? null : agentId;
};
export const toggleColHighlight = (date) => {
const str = formatDateForId(date);
highlightedDateStr.value = highlightedDateStr.value === str ? null : str;
};
export const clearHighlights = () => {
highlightedRowId.value = null;
highlightedDateStr.value = null;
};
/* ---- search / filter state ---- */
export const search = ref('');
export const activeDept = ref('All');
export const activeHub = ref('All');
export const filterRoles = ref([]);
export const filterSkills = ref([]);
export const filterByAvailability = ref(false);
export const filterDate = ref(formatDateForId(new Date()));
export const proxyFilterDate = ref('');
export const filterShiftTypes = ref([]);
/* ---- date navigation ---- */
export const startDate = ref(getStartOfWeek(new Date()));
export const proxyDate = ref(null);
export 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;
});
/* ---- dynamic CSS-variable overrides ---- */
export const gridStyles = computed(() => ({
'--h-eod': showEodTargets.value ? '42px' : '0px',
'--h-status': showAvailability.value ? '34px' : '0px'
}));
/* ---- selected cell / assignment editing ---- */
export const selectedAgent = ref(null);
export const selectedDate = ref(null);
export const pendingShift = ref(null);
/* ---- filter helpers ---- */
export 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
);
export 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;
});
export const clearFilters = () => {
search.value = '';
activeDept.value = 'All';
activeHub.value = 'All';
filterRoles.value = [];
filterSkills.value = [];
filterByAvailability.value = false;
filterShiftTypes.value = [];
filterDate.value = formatDateForId(new Date());
};
export const applySavedFilter = (key) => {
clearFilters();
if (key === 'high_potential') {
filterSkills.value = ['VIP Concierge', 'Technical Training'];
} else if (key === 'remote') {
filterRoles.value = ['Specialist'];
}
};
/* ---- date picker actions ---- */
export const syncProxyDate = () => {
proxyDate.value = formatDateForId(selectedDate.value || new Date());
};
export const updateFilterDateProxy = () => {
proxyFilterDate.value = filterDate.value;
};
export const applyFilterDate = () => {
filterDate.value = proxyFilterDate.value;
};
/* ---- online users (reactive, driven by socket-service) ---- */
const MAX_VISIBLE_AVATARS = 5;
export const onlineUsers = ref([]);
export const visibleOnlineUsers = computed(() =>
onlineUsers.value.slice(0, MAX_VISIBLE_AVATARS)
);
export const remainingOnlineCount = computed(() =>
Math.max(0, onlineUsers.value.length - MAX_VISIBLE_AVATARS)
);
export const addOnlineUser = (user) => {
if (!onlineUsers.value.find(u => u.id === user.id)) {
onlineUsers.value = [...onlineUsers.value, user];
}
};
export const removeOnlineUser = (userId) => {
onlineUsers.value = onlineUsers.value.filter(u => u.id !== userId);
};

View File

@@ -0,0 +1,204 @@
/**
* socket-service.js
* ==================
* WebSocket simulation for cell-locking and real-time collaboration.
* Simulates:
* - LOCK_CELL — cell locking events
* - USER_ONLINE — a user comes online
* - USER_OFFLINE — a user goes offline
*
* In production, replace the simulation loops with a real
* SignalR / native WebSocket connection.
*/
const { ref } = Vue;
import { formatDateForId, addOnlineUser, removeOnlineUser, onlineUsers } from './planner-state.js';
import { agents } from './data-service.js';
/* ================================================================
USER POOL — potential collaborators who may come and go
================================================================ */
const userPool = [
{ id: 'u01', name: 'Anna Schneider', role: 'Team Lead', img: 'https://i.pravatar.cc/150?u=anna01' },
{ id: 'u02', name: 'Markus Weber', role: 'Planner', img: 'https://i.pravatar.cc/150?u=markus02' },
{ id: 'u03', name: 'Lisa Hoffmann', role: 'Planner', img: 'https://i.pravatar.cc/150?u=lisa03' },
{ id: 'u04', name: 'Thomas Müller', role: 'Supervisor', img: 'https://i.pravatar.cc/150?u=thomas04' },
{ id: 'u05', name: 'Julia Fischer', role: 'Planner', img: 'https://i.pravatar.cc/150?u=julia05' },
{ id: 'u06', name: 'Stefan Becker', role: 'Specialist', img: 'https://i.pravatar.cc/150?u=stefan06' },
{ id: 'u07', name: 'Katharina Wolf', role: 'Planner', img: 'https://i.pravatar.cc/150?u=kath07' },
{ id: 'u08', name: 'Daniel Braun', role: 'Team Lead', img: 'https://i.pravatar.cc/150?u=daniel08' },
{ id: 'u09', name: 'Sandra Koch', role: 'Quality Manager', img: 'https://i.pravatar.cc/150?u=sandra09' },
{ id: 'u10', name: 'Michael Richter', role: 'Planner', img: 'https://i.pravatar.cc/150?u=michael10' },
{ id: 'u11', name: 'Christina Lang', role: 'Supervisor', img: 'https://i.pravatar.cc/150?u=chris11' },
{ id: 'u12', name: 'Patrick Schäfer', role: 'Specialist', img: 'https://i.pravatar.cc/150?u=patrick12' },
{ id: 'u13', name: 'Nicole Baumann', role: 'Planner', img: 'https://i.pravatar.cc/150?u=nicole13' },
{ id: 'u14', name: 'Andreas Klein', role: 'Team Lead', img: 'https://i.pravatar.cc/150?u=andreas14' },
{ id: 'u15', name: 'Melanie Kraus', role: 'Planner', img: 'https://i.pravatar.cc/150?u=melanie15' },
{ id: 'u16', name: 'Tobias Neumann', role: 'Specialist', img: 'https://i.pravatar.cc/150?u=tobias16' },
{ id: 'u17', name: 'Laura Hartmann', role: 'Planner', img: 'https://i.pravatar.cc/150?u=laura17' },
{ id: 'u18', name: 'Florian Maier', role: 'Supervisor', img: 'https://i.pravatar.cc/150?u=florian18' },
{ id: 'u19', name: 'Sabine Engel', role: 'Quality Manager', img: 'https://i.pravatar.cc/150?u=sabine19' },
{ id: 'u20', name: 'Jens Zimmermann', role: 'Planner', img: 'https://i.pravatar.cc/150?u=jens20' },
];
/* ================================================================
CELL LOCKING (existing)
================================================================ */
export const lockedCells = ref(new Set()); // "agentId:YYYY-MM-DD"
/* ================================================================
INCOMING MESSAGE HANDLER
================================================================ */
export const handleWssMessage = (type, payload, $q) => {
/* ---- cell lock ---- */
if (type === 'LOCK_CELL') {
const key = `${payload.agentId}:${payload.date}`;
lockedCells.value.add(key);
const agent = agents.value.find(a => a.id === payload.agentId);
const name = agent ? agent.name : 'Unknown Agent';
$q.notify({
message: `WSS: Cell Locked for ${name} on ${payload.date}`,
color: 'negative',
position: 'top',
icon: 'lock'
});
}
/* ---- user comes online ---- */
if (type === 'USER_ONLINE') {
addOnlineUser(payload);
$q.notify({
message: `${payload.name} joined`,
color: 'white',
textColor: 'grey-8',
position: 'bottom-right',
icon: 'circle',
iconColor: 'teal',
iconSize: '10px',
timeout: 1800,
classes: 'text-caption'
});
}
/* ---- user goes offline ---- */
if (type === 'USER_OFFLINE') {
removeOnlineUser(payload.id);
$q.notify({
message: `${payload.name} left`,
color: 'white',
textColor: 'grey-6',
position: 'bottom-right',
icon: 'circle',
iconColor: 'grey-4',
iconSize: '10px',
timeout: 1800,
classes: 'text-caption'
});
}
};
/* ================================================================
CELL-LOCK SIMULATION (existing)
================================================================ */
export const simulateWssLock = ($q) => {
if (agents.value.length === 0) return;
const randomAgent = agents.value[Math.floor(Math.random() * agents.value.length)];
let targetDate = new Date();
const day = targetDate.getDay();
if (day === 0) targetDate.setDate(targetDate.getDate() + 1);
else if (day === 6) targetDate.setDate(targetDate.getDate() + 2);
const dateStr = formatDateForId(targetDate);
handleWssMessage('LOCK_CELL', { agentId: randomAgent.id, date: dateStr }, $q);
};
/* ---- cell-lock check ---- */
export const isCellLocked = (agentId, date) => {
const key = `${agentId}:${formatDateForId(date)}`;
return lockedCells.value.has(key);
};
/* ================================================================
ONLINE-USERS SIMULATION
Continuously adds/removes users at random intervals (310 s).
Starts with an initial batch of 35 users so the UI isn't empty.
================================================================ */
let onlineSimTimer = null;
/** Random int between min and max (inclusive). */
const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
/** Get users from the pool that are NOT currently online. */
const offlinePoolUsers = () =>
userPool.filter(u => !onlineUsers.value.find(o => o.id === u.id));
/** Pick a random element from an array. */
const pickRandom = (arr) => arr[Math.floor(Math.random() * arr.length)];
/** Schedule the next online-users event after a random delay. */
const scheduleNext = ($q) => {
const delay = randInt(3000, 10000);
onlineSimTimer = setTimeout(() => simulateOnlineEvent($q), delay);
};
/** One simulation tick: either bring someone online or take someone offline. */
const simulateOnlineEvent = ($q) => {
const currentCount = onlineUsers.value.length;
const offline = offlinePoolUsers();
// Decide whether to add or remove.
// Bias towards adding when few users are online, towards removing when many.
let shouldAdd;
if (currentCount === 0) shouldAdd = true;
else if (offline.length === 0) shouldAdd = false;
else if (currentCount <= 2) shouldAdd = Math.random() < 0.85;
else if (currentCount >= userPool.length - 2) shouldAdd = Math.random() < 0.15;
else shouldAdd = Math.random() < 0.55;
if (shouldAdd && offline.length > 0) {
const user = pickRandom(offline);
handleWssMessage('USER_ONLINE', user, $q);
} else if (currentCount > 0) {
const user = pickRandom(onlineUsers.value);
handleWssMessage('USER_OFFLINE', user, $q);
}
// Schedule next event
scheduleNext($q);
};
/**
* Start the online-users simulation.
* Seeds 35 users immediately, then continues at random intervals.
*/
export const startOnlineUsersSimulation = ($q) => {
// Stop any existing simulation
stopOnlineUsersSimulation();
// Seed initial users (35) without notifications
const shuffled = [...userPool].sort(() => Math.random() - 0.5);
const seedCount = randInt(3, 5);
for (let i = 0; i < seedCount; i++) {
addOnlineUser(shuffled[i]);
}
// Start the continuous loop
scheduleNext($q);
};
/**
* Stop the online-users simulation and clear all online users.
*/
export const stopOnlineUsersSimulation = () => {
if (onlineSimTimer) {
clearTimeout(onlineSimTimer);
onlineSimTimer = null;
}
};