Added new modules and updated existing logic
This commit is contained in:
246
dev/ui-ux/Opus 4.6/src/services/data-service.js
Normal file
246
dev/ui-ux/Opus 4.6/src/services/data-service.js
Normal 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;
|
||||
};
|
||||
178
dev/ui-ux/Opus 4.6/src/services/planner-state.js
Normal file
178
dev/ui-ux/Opus 4.6/src/services/planner-state.js
Normal 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);
|
||||
};
|
||||
204
dev/ui-ux/Opus 4.6/src/services/socket-service.js
Normal file
204
dev/ui-ux/Opus 4.6/src/services/socket-service.js
Normal 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 (3–10 s).
|
||||
Starts with an initial batch of 3–5 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 3–5 users immediately, then continues at random intervals.
|
||||
*/
|
||||
export const startOnlineUsersSimulation = ($q) => {
|
||||
// Stop any existing simulation
|
||||
stopOnlineUsersSimulation();
|
||||
|
||||
// Seed initial users (3–5) 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;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user