531 lines
24 KiB
Plaintext
531 lines
24 KiB
Plaintext
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons" rel="stylesheet" type="text/css">
|
|
<link href="https://cdn.jsdelivr.net/npm/quasar@2.16.0/dist/quasar.prod.css" rel="stylesheet" type="text/css">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<style>
|
|
:root {
|
|
--left-col-width: 240px;
|
|
--cell-width: 100px;
|
|
--h-dates: 52px;
|
|
--border-color: #e2e8f0;
|
|
--highlight-bg: rgba(99, 102, 241, 0.08);
|
|
--highlight-border: rgba(99, 102, 241, 0.3);
|
|
--selection-color: #3b82f6;
|
|
}
|
|
|
|
body, html {
|
|
height: 100%;
|
|
overflow: hidden;
|
|
margin: 0;
|
|
padding: 0;
|
|
background-color: #f8fafc;
|
|
color: #334155;
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
}
|
|
|
|
body.is-selecting-active * {
|
|
user-select: none !important;
|
|
-webkit-user-select: none !important;
|
|
}
|
|
|
|
.grid-viewport {
|
|
height: calc(100vh - 64px) !important;
|
|
overflow: auto !important;
|
|
background: white;
|
|
position: relative;
|
|
display: block;
|
|
border-top: 1px solid var(--border-color);
|
|
}
|
|
|
|
.planner-content { display: inline-block; min-width: fit-content; vertical-align: top; }
|
|
.planner-row { display: flex; flex-direction: row; border-bottom: 1px solid #f1f5f9; }
|
|
|
|
.left-col {
|
|
width: var(--left-col-width);
|
|
min-width: var(--left-col-width);
|
|
position: sticky;
|
|
left: 0;
|
|
z-index: 20;
|
|
background: white;
|
|
border-right: 1px solid var(--border-color);
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.row-dates { position: sticky; top: 0; z-index: 38; height: var(--h-dates); background: #fdfdfd; }
|
|
.row-dates .left-col { z-index: 58; background: #fdfdfd; border-bottom: 1px solid var(--border-color); }
|
|
|
|
.cells-area { display: flex; flex-direction: row; }
|
|
|
|
.cell {
|
|
width: var(--cell-width);
|
|
min-width: var(--cell-width);
|
|
height: 58px;
|
|
border-right: 1px solid #f1f5f9;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
position: relative;
|
|
transition: background 0.05s ease;
|
|
cursor: cell;
|
|
}
|
|
.cell:hover:not(.cursor-not-allowed) { background-color: #f8fafc; }
|
|
.cell-compact { height: 42px; }
|
|
|
|
.cell-selected {
|
|
background-color: rgba(59, 130, 246, 0.12) !important;
|
|
box-shadow: inset 0 0 0 2px var(--selection-color) !important;
|
|
z-index: 10;
|
|
}
|
|
|
|
.shift-badge {
|
|
font-size: 9px;
|
|
font-weight: 800;
|
|
padding: 2px 4px;
|
|
border-radius: 4px;
|
|
width: 90%;
|
|
text-align: center;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
margin-bottom: 2px;
|
|
border: 1px solid rgba(0,0,0,0.05);
|
|
pointer-events: none;
|
|
user-select: none;
|
|
}
|
|
|
|
.group-header-l1 { background: #eef2ff; font-size: 12px; font-weight: 900; color: #3730a3; height: 36px; border-top: 1px solid #c7d2fe; }
|
|
.bg-weekend { background-color: #f1f5f9 !important; }
|
|
.col-hovered { background-color: var(--highlight-bg) !important; }
|
|
|
|
.grid-viewport::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
.grid-viewport::-webkit-scrollbar-track { background: #f1f5f9; }
|
|
.grid-viewport::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
|
|
</style>
|
|
</head>
|
|
|
|
<body @mouseup="onGlobalMouseUp" :class="{'is-selecting-active': isSelecting}">
|
|
<div id="q-app">
|
|
<q-layout view="hHh Lpr fFf" @keydown.esc="clearSelection">
|
|
|
|
<q-header class="bg-white text-grey-9 border-b" style="border-bottom: 1px solid #e2e8f0">
|
|
<q-toolbar class="q-py-sm">
|
|
<q-btn flat round dense icon="menu" color="grey-7" @click="leftDrawer = !leftDrawer"></q-btn>
|
|
<div class="row items-center q-ml-md">
|
|
<div class="bg-indigo-1 q-pa-xs rounded-borders q-mr-sm"><q-icon name="edit_calendar" color="indigo-8" size="24px"></q-icon></div>
|
|
<q-toolbar-title class="text-weight-bold text-subtitle1 text-indigo-10">Hotline Planner Pro</q-toolbar-title>
|
|
</div>
|
|
<q-space></q-space>
|
|
|
|
<div class="row items-center q-gutter-sm">
|
|
<transition appear enter-active-class="animated bounceIn" leave-active-class="animated bounceOut">
|
|
<div v-if="selectedCells.size > 0" class="row items-center bg-blue-1 text-blue-9 q-px-md q-py-xs rounded-borders border border-blue-2 q-mr-md">
|
|
<div class="text-caption text-weight-bold q-mr-sm">{{ selectedCells.size }} selected</div>
|
|
<q-separator vertical inset class="q-mx-sm"></q-separator>
|
|
<q-btn flat round dense size="sm" icon="content_copy" @click="handleCopy"><q-tooltip>Copy (Ctrl+C)</q-tooltip></q-btn>
|
|
<q-btn flat round dense size="sm" icon="content_paste" @click="handlePaste" :disable="!clipboard"><q-tooltip>Paste (Ctrl+V)</q-tooltip></q-btn>
|
|
<q-btn flat round dense size="sm" icon="close" @click="clearSelection" class="q-ml-xs"></q-btn>
|
|
</div>
|
|
</transition>
|
|
|
|
<q-btn flat round dense icon="help_outline" color="grey-6">
|
|
<q-menu class="q-pa-md" style="min-width: 250px">
|
|
<div class="text-weight-bold q-mb-md">Shortcuts</div>
|
|
<div class="column q-gutter-y-xs text-caption">
|
|
<div class="row justify-between"><span>Select Block</span><q-badge color="grey-2" text-color="grey-9">Drag Mouse</q-badge></div>
|
|
<div class="row justify-between"><span>Edit Cell</span><q-badge color="grey-2" text-color="grey-9">Double Click</q-badge></div>
|
|
<div class="row justify-between"><span>Add Selection</span><q-badge color="grey-2" text-color="grey-9">Ctrl + Drag</q-badge></div>
|
|
<div class="row justify-between"><span>Clear</span><q-badge color="grey-2" text-color="grey-9">Esc</q-badge></div>
|
|
</div>
|
|
</q-menu>
|
|
</q-btn>
|
|
<q-avatar size="32px" class="q-ml-xs"><img src="https://i.pravatar.cc/150?u=me"></q-avatar>
|
|
</div>
|
|
</q-toolbar>
|
|
</q-header>
|
|
|
|
<q-drawer v-model="leftDrawer" side="left" bordered :width="280" class="bg-white">
|
|
<div class="q-pa-lg">
|
|
<div class="text-overline text-grey-6 q-mb-md">Workspace</div>
|
|
<q-item tag="label" v-ripple class="q-px-none">
|
|
<q-item-section><q-item-label>Compact View</q-item-label></q-item-section>
|
|
<q-item-section side><q-toggle v-model="isCompact" color="indigo-8"></q-toggle></q-item-section>
|
|
</q-item>
|
|
<q-item tag="label" v-ripple class="q-px-none">
|
|
<q-item-section><q-item-label>Reading Crosshair</q-item-label><q-item-label caption>Hover highlight</q-item-label></q-item-section>
|
|
<q-item-section side><q-toggle v-model="crosshairActive" color="indigo-8"></q-toggle></q-item-section>
|
|
</q-item>
|
|
</div>
|
|
</q-drawer>
|
|
|
|
<q-drawer v-model="rightDrawer" side="right" bordered :width="350" overlay elevated>
|
|
<div class="column full-height bg-white">
|
|
<q-toolbar class="bg-grey-1 border-b">
|
|
<q-toolbar-title class="text-subtitle2 text-weight-bold">Assignment Editor</q-toolbar-title>
|
|
<q-btn flat round dense icon="close" size="sm" @click="rightDrawer = false"></q-btn>
|
|
</q-toolbar>
|
|
<q-scroll-area class="col q-pa-md">
|
|
<div v-if="selectedAgent && selectedDate" class="column q-gutter-y-md">
|
|
<div class="row items-center no-wrap">
|
|
<q-avatar size="48px"><img :src="selectedAgent.avatar"></q-avatar>
|
|
<div class="q-ml-md">
|
|
<div class="text-weight-bold">{{ selectedAgent.name }}</div>
|
|
<div class="text-caption text-grey-6">{{ formatDateForDisplay(selectedDate) }}</div>
|
|
</div>
|
|
</div>
|
|
<q-separator></q-separator>
|
|
<div class="text-overline">Shift Type</div>
|
|
<div class="row q-col-gutter-sm">
|
|
<div v-for="s in shifts" :key="s.id" class="col-6">
|
|
<q-btn :color="s.color" :outline="getAssignment(selectedAgent.id, selectedDate)?.id !== s.id" :unelevated="getAssignment(selectedAgent.id, selectedDate)?.id === s.id" :label="s.label" class="full-width" dense no-caps @click="updateAssignment(selectedAgent.id, selectedDate, s.id)"></q-btn>
|
|
</div>
|
|
<div class="col-12">
|
|
<q-btn outline color="red-4" label="Clear Shift" class="full-width q-mt-sm" dense no-caps @click="updateAssignment(selectedAgent.id, selectedDate, null)"></q-btn>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</q-scroll-area>
|
|
</div>
|
|
</q-drawer>
|
|
|
|
<q-page-container>
|
|
<q-page class="q-pa-none">
|
|
<div id="viewport-target" class="grid-viewport" ref="viewport">
|
|
<div class="planner-content">
|
|
<div class="planner-row row-dates">
|
|
<div class="left-col">
|
|
<q-input v-model="search" debounce="300" dense outlined rounded placeholder="Filter agents..." class="full-width" bg-color="white">
|
|
<template v-slot:prepend><q-icon name="search" size="xs"></q-icon></template>
|
|
</q-input>
|
|
</div>
|
|
<div class="cells-area">
|
|
<div v-for="(date, i) in dates" :key="'d'+i"
|
|
class="cell column flex-center"
|
|
:class="[isWeekend(date) ? 'bg-weekend' : '', crosshairActive && hoveredDateStr === formatDateForId(date) ? 'col-hovered' : '']"
|
|
style="height: var(--h-dates)"
|
|
@mouseenter="hoveredDateStr = formatDateForId(date)"
|
|
@mouseleave="hoveredDateStr = null">
|
|
<div class="text-[10px] uppercase text-weight-bold text-grey-5">{{ date.toLocaleDateString('en-US', {weekday: 'short'}) }}</div>
|
|
<div class="text-subtitle2 text-weight-bold text-grey-9">{{ date.getDate() }}. {{ date.toLocaleDateString('en-US', { month: 'short' }) }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<q-virtual-scroll
|
|
scroll-target="#viewport-target"
|
|
:items="flattenedList"
|
|
:item-size="isCompact ? 43 : 59"
|
|
>
|
|
<template v-slot="{ item, index }">
|
|
<div v-if="item.type === 'header-l1'" :key="item.id" class="planner-row group-header-l1">
|
|
<div class="left-col group-header-l1">{{ item.label }}</div>
|
|
<div class="cells-area"><div class="cell" style="flex: 1; border-right: none"></div></div>
|
|
</div>
|
|
|
|
<div v-else :key="item.id" class="planner-row planner-row-item">
|
|
<div class="left-col border-b" :class="[isCompact ? 'cell-compact' : '']">
|
|
<q-avatar :size="isCompact ? '24px' : '32px'"><img :src="item.data.avatar"></q-avatar>
|
|
<div class="q-ml-sm overflow-hidden col">
|
|
<div class="text-weight-bold truncate" :style="{fontSize: isCompact ? '11px' : '13px'}">{{ item.data.name }}</div>
|
|
<div v-if="!isCompact" class="text-[10px] text-grey-5 uppercase text-weight-bold truncate">{{ item.data.role }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="cells-area">
|
|
<div v-for="(date, i) in dates"
|
|
:key="'c'+item.data.id+i"
|
|
class="cell"
|
|
:class="getCellClass(item.data.id, date)"
|
|
@mouseenter="onCellMouseEnter(item.data.id, i, index)"
|
|
@mousedown.stop="onCellMouseDown(item.data.id, i, index, $event)"
|
|
@dblclick.stop="handleCellDblClick(item.data, date)">
|
|
|
|
<div v-if="getAssignment(item.data.id, date)" class="shift-badge shadow-1" :class="getAssignment(item.data.id, date).badgeClass">
|
|
{{ getAssignment(item.data.id, date).label }}
|
|
</div>
|
|
<div v-if="hasNote(item.data.id, date)" class="absolute-bottom-right q-pa-xs">
|
|
<q-icon name="info" size="8px" color="orange-4"></q-icon>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</q-virtual-scroll>
|
|
</div>
|
|
</div>
|
|
</q-page>
|
|
</q-page-container>
|
|
</q-layout>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/quasar@2.16.0/dist/quasar.umd.prod.js"></script>
|
|
|
|
<script>
|
|
const { createApp, ref, computed, reactive, onMounted, onBeforeUnmount } = Vue;
|
|
const { useQuasar } = Quasar;
|
|
|
|
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' }
|
|
];
|
|
|
|
app = createApp({
|
|
setup() {
|
|
const $q = useQuasar();
|
|
const leftDrawer = ref(false);
|
|
const rightDrawer = ref(false);
|
|
const search = ref("");
|
|
const isCompact = ref(false);
|
|
const crosshairActive = ref(false);
|
|
const hoveredDateStr = ref(null);
|
|
|
|
const agents = ref([]);
|
|
const assignments = reactive({});
|
|
const notes = reactive({});
|
|
const selectedAgent = ref(null);
|
|
const selectedDate = ref(null);
|
|
|
|
const selectedCells = ref(new Set());
|
|
const clipboard = ref(null);
|
|
|
|
const isSelecting = ref(false);
|
|
const dragStart = ref(null);
|
|
const dragCurrent = ref(null);
|
|
const selectionSnapshot = ref(new Set());
|
|
|
|
const formatDateForId = (date) => {
|
|
if (!date) return '';
|
|
const d = new Date(date);
|
|
return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
|
|
};
|
|
|
|
const formatDateForDisplay = (d) => d.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' });
|
|
|
|
const startDate = ref(new Date());
|
|
startDate.value.setDate(startDate.value.getDate() - (startDate.value.getDay() || 7) + 1);
|
|
|
|
const dates = computed(() => {
|
|
const res = [];
|
|
for (let i = 0; i < 28; i++) {
|
|
const d = new Date(startDate.value);
|
|
d.setDate(startDate.value.getDate() + i);
|
|
res.push(d);
|
|
}
|
|
return res;
|
|
});
|
|
|
|
const isWeekend = (date) => [0, 6].includes(date.getDay());
|
|
|
|
const onCellMouseDown = (agentId, hIdx, vIdx, event) => {
|
|
event.preventDefault();
|
|
if (window.getSelection) { window.getSelection().removeAllRanges(); }
|
|
isSelecting.value = true;
|
|
dragStart.value = { vIdx, hIdx };
|
|
dragCurrent.value = { vIdx, hIdx };
|
|
if (!event.ctrlKey && !event.metaKey) {
|
|
selectedCells.value.clear();
|
|
selectionSnapshot.value = new Set();
|
|
} else {
|
|
selectionSnapshot.value = new Set(selectedCells.value);
|
|
}
|
|
updateSelectionRect();
|
|
};
|
|
|
|
const onCellMouseEnter = (agentId, hIdx, vIdx) => {
|
|
hoveredDateStr.value = formatDateForId(dates.value[hIdx]);
|
|
if (!isSelecting.value) return;
|
|
dragCurrent.value = { vIdx, hIdx };
|
|
updateSelectionRect();
|
|
};
|
|
|
|
const onGlobalMouseUp = () => {
|
|
if (isSelecting.value) {
|
|
isSelecting.value = false;
|
|
dragStart.value = null;
|
|
dragCurrent.value = null;
|
|
}
|
|
};
|
|
|
|
const updateSelectionRect = () => {
|
|
if (!dragStart.value || !dragCurrent.value) return;
|
|
const minV = Math.min(dragStart.value.vIdx, dragCurrent.value.vIdx);
|
|
const maxV = Math.max(dragStart.value.vIdx, dragCurrent.value.vIdx);
|
|
const minH = Math.min(dragStart.value.hIdx, dragCurrent.value.hIdx);
|
|
const maxH = Math.max(dragStart.value.hIdx, dragCurrent.value.hIdx);
|
|
const gridItems = flattenedList.value;
|
|
const dateStrs = dates.value.map(d => formatDateForId(d));
|
|
const nextSelection = new Set(selectionSnapshot.value);
|
|
for (let v = minV; v <= maxV; v++) {
|
|
const row = gridItems[v];
|
|
if (row && row.type === 'agent') {
|
|
for (let h = minH; h <= maxH; h++) {
|
|
const dStr = dateStrs[h];
|
|
if (dStr) nextSelection.add(`${row.data.id}:${dStr}`);
|
|
}
|
|
}
|
|
}
|
|
selectedCells.value = nextSelection;
|
|
};
|
|
|
|
const handleCellDblClick = (agent, date) => {
|
|
selectedAgent.value = agent;
|
|
selectedDate.value = date;
|
|
rightDrawer.value = true;
|
|
};
|
|
|
|
const clearSelection = () => { selectedCells.value.clear(); };
|
|
|
|
const handleCopy = () => {
|
|
if (selectedCells.value.size === 0) return;
|
|
const selectedArray = Array.from(selectedCells.value);
|
|
const dateStrs = dates.value.map(d => formatDateForId(d));
|
|
const gridItems = flattenedList.value;
|
|
const items = selectedArray.map(key => {
|
|
const [aid, d] = key.split(':');
|
|
const agentId = parseInt(aid);
|
|
const vIdx = gridItems.findIndex(i => i.type === 'agent' && i.data.id === agentId);
|
|
const hIdx = dateStrs.indexOf(d);
|
|
return {
|
|
agentId, dateStr: d, vIdx, hIdx,
|
|
shift: assignments[agentId]?.[d] || null,
|
|
note: notes[agentId]?.[d] || null
|
|
};
|
|
});
|
|
const minVIdx = Math.min(...items.map(i => i.vIdx));
|
|
const minHIdx = Math.min(...items.map(i => i.hIdx));
|
|
clipboard.value = {
|
|
type: selectedCells.value.size === 1 ? 'single' : 'block',
|
|
items: items.map(i => ({
|
|
relV: i.vIdx - minVIdx, relH: i.hIdx - minHIdx,
|
|
shift: i.shift, note: i.note
|
|
}))
|
|
};
|
|
let msg = clipboard.value.type === 'single' ? 'Cell copied' : `${selectedCells.value.size} cells copied`;
|
|
$q.notify({ message: msg, color: 'indigo-8', icon: 'content_copy', position: 'bottom', timeout: 2000 });
|
|
};
|
|
|
|
const handlePaste = () => {
|
|
if (!clipboard.value || selectedCells.value.size === 0) return;
|
|
const gridItems = flattenedList.value;
|
|
const dateStrs = dates.value.map(d => formatDateForId(d));
|
|
const targetArray = Array.from(selectedCells.value);
|
|
const [firstAid, firstD] = targetArray[0].split(':');
|
|
const firstV = gridItems.findIndex(i => i.type === 'agent' && i.data.id === parseInt(firstAid));
|
|
const firstH = dateStrs.indexOf(firstD);
|
|
|
|
const nextSelection = new Set();
|
|
|
|
if (clipboard.value.type === 'single') {
|
|
const source = clipboard.value.items[0];
|
|
targetArray.forEach(key => {
|
|
const [aid, d] = key.split(':');
|
|
const agentId = parseInt(aid);
|
|
if (!assignments[agentId]) assignments[agentId] = {};
|
|
if (!notes[agentId]) notes[agentId] = {};
|
|
assignments[agentId][d] = source.shift;
|
|
notes[agentId][d] = source.note;
|
|
nextSelection.add(key);
|
|
});
|
|
} else {
|
|
clipboard.value.items.forEach(clipItem => {
|
|
const targetV = firstV + clipItem.relV;
|
|
const targetH = firstH + clipItem.relH;
|
|
const row = gridItems[targetV];
|
|
const dStr = dateStrs[targetH];
|
|
if (row && row.type === 'agent' && dStr) {
|
|
const agentId = row.data.id;
|
|
const key = `${agentId}:${dStr}`;
|
|
if (!assignments[agentId]) assignments[agentId] = {};
|
|
if (!notes[agentId]) notes[agentId] = {};
|
|
assignments[agentId][dStr] = clipItem.shift;
|
|
notes[agentId][dStr] = clipItem.note;
|
|
nextSelection.add(key);
|
|
}
|
|
});
|
|
}
|
|
selectedCells.value = nextSelection; // Select the pasted area
|
|
$q.notify({ message: 'Paste complete', color: 'positive', icon: 'check_circle', position: 'bottom', timeout: 1500 });
|
|
};
|
|
|
|
const updateAssignment = (aid, date, shiftId) => {
|
|
const dStr = formatDateForId(date);
|
|
if (!assignments[aid]) assignments[aid] = {};
|
|
assignments[aid][dStr] = shiftId;
|
|
};
|
|
|
|
const getAssignment = (aid, d) => {
|
|
const code = assignments[aid]?.[formatDateForId(d)];
|
|
return code ? SHIFTS.find(s => s.id === code) : null;
|
|
};
|
|
|
|
const hasNote = (aid, d) => notes[aid]?.[formatDateForId(d)];
|
|
|
|
const getCellClass = (aid, d) => {
|
|
const key = `${aid}:${formatDateForId(d)}`;
|
|
return {
|
|
'cell-selected': selectedCells.value.has(key),
|
|
'bg-weekend': isWeekend(d),
|
|
'cell-compact': isCompact.value,
|
|
'col-hovered': crosshairActive.value && hoveredDateStr.value === formatDateForId(d)
|
|
};
|
|
};
|
|
|
|
const flattenedList = computed(() => {
|
|
const res = [];
|
|
const filtered = agents.value.filter(a => a.name.toLowerCase().includes(search.value.toLowerCase()));
|
|
const hubs = [...new Set(filtered.map(a => a.hub))];
|
|
hubs.sort().forEach(h => {
|
|
res.push({ type: 'header-l1', label: h, id: `h-${h}` });
|
|
filtered.filter(a => a.hub === h).forEach(a => res.push({ type: 'agent', data: a, id: a.id }));
|
|
});
|
|
return res;
|
|
});
|
|
|
|
const onGlobalKeyDown = (e) => {
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'c') { e.preventDefault(); handleCopy(); }
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'v') { e.preventDefault(); handlePaste(); }
|
|
};
|
|
|
|
const onSelectStart = (e) => { if (isSelecting.value) e.preventDefault(); };
|
|
|
|
onMounted(() => {
|
|
agents.value = Array.from({ length: 800 }, (_, i) => ({
|
|
id: i + 1, name: `Agent ${i + 1}`, role: i % 3 === 0 ? 'Lead' : 'Specialist',
|
|
avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${i}`,
|
|
hub: i % 5 === 0 ? 'Central Hub' : 'Nordic Hub'
|
|
}));
|
|
window.addEventListener('keydown', onGlobalKeyDown);
|
|
window.addEventListener('mouseup', onGlobalMouseUp);
|
|
window.addEventListener('selectstart', onSelectStart);
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener('keydown', onGlobalKeyDown);
|
|
window.removeEventListener('mouseup', onGlobalMouseUp);
|
|
window.removeEventListener('selectstart', onSelectStart);
|
|
});
|
|
|
|
return {
|
|
isSelecting, leftDrawer, rightDrawer, search, isCompact, crosshairActive, hoveredDateStr,
|
|
dates, flattenedList, formatDateForId, formatDateForDisplay, isWeekend,
|
|
selectedCells, onCellMouseDown, onCellMouseEnter, onGlobalMouseUp, handleCellDblClick, clearSelection,
|
|
getAssignment, hasNote, getCellClass,
|
|
clipboard, handleCopy, handlePaste,
|
|
selectedAgent, selectedDate, shifts: SHIFTS, updateAssignment
|
|
};
|
|
}
|
|
});
|
|
|
|
app.use(Quasar);
|
|
app.mount('#q-app');
|
|
</script>
|
|
</body>
|
|
</html> |