741 lines
29 KiB
HTML
741 lines
29 KiB
HTML
<!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;
|
|
/* Fixed heights for sticky header stacking */
|
|
--h-eod: 40px;
|
|
--h-status: 32px;
|
|
--h-dates: 48px;
|
|
}
|
|
|
|
body, html {
|
|
height: 100%;
|
|
overflow: hidden;
|
|
margin: 0;
|
|
padding: 0;
|
|
background-color: #f1f5f9;
|
|
}
|
|
|
|
/* Main Grid Viewport */
|
|
.grid-viewport {
|
|
height: calc(100vh - 50px) !important;
|
|
overflow: auto !important;
|
|
background: white;
|
|
position: relative;
|
|
display: block;
|
|
border-top: 1px solid #e2e8f0;
|
|
scroll-behavior: smooth;
|
|
}
|
|
|
|
.grid-viewport::-webkit-scrollbar {
|
|
width: 14px;
|
|
height: 14px;
|
|
display: block;
|
|
}
|
|
.grid-viewport::-webkit-scrollbar-track {
|
|
background: #f1f5f9;
|
|
}
|
|
.grid-viewport::-webkit-scrollbar-thumb {
|
|
background-color: #94a3b8;
|
|
border-radius: 10px;
|
|
border: 3px solid #f1f5f9;
|
|
}
|
|
|
|
.planner-content {
|
|
display: inline-block;
|
|
min-width: fit-content;
|
|
vertical-align: top;
|
|
}
|
|
|
|
.planner-row {
|
|
display: flex;
|
|
flex-direction: row;
|
|
border-bottom: 1px solid #edf2f7;
|
|
}
|
|
|
|
.left-col {
|
|
width: var(--left-col-width);
|
|
min-width: var(--left-col-width);
|
|
position: sticky;
|
|
left: 0;
|
|
z-index: 20;
|
|
background: white;
|
|
border-right: 2px solid #e2e8f0;
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0 12px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.row-eod {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 40;
|
|
height: var(--h-eod);
|
|
background: #fff1f2;
|
|
}
|
|
.row-status {
|
|
position: sticky;
|
|
top: var(--h-eod);
|
|
z-index: 39;
|
|
height: var(--h-status);
|
|
background: white;
|
|
}
|
|
.row-dates {
|
|
position: sticky;
|
|
top: calc(var(--h-eod) + var(--h-status));
|
|
z-index: 38;
|
|
height: var(--h-dates);
|
|
background: #f8fafc;
|
|
}
|
|
|
|
.row-eod .left-col { z-index: 60; background: #fff1f2; color: #be185d; }
|
|
.row-status .left-col { z-index: 59; background: white; color: #64748b; }
|
|
.row-dates .left-col { z-index: 58; background: #f8fafc; padding-right: 8px; }
|
|
|
|
.cells-area {
|
|
display: flex;
|
|
flex-direction: row;
|
|
}
|
|
|
|
.cell {
|
|
width: var(--cell-width);
|
|
min-width: var(--cell-width);
|
|
height: 54px;
|
|
border-right: 1px solid #f1f5f9;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
position: relative;
|
|
}
|
|
|
|
.cell-compact { height: 38px; }
|
|
|
|
.shift-badge {
|
|
font-size: 9px;
|
|
font-weight: 800;
|
|
padding: 1px 4px;
|
|
border-radius: 3px;
|
|
width: 92%;
|
|
text-align: center;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.dept-divider {
|
|
background: #f1f5f9;
|
|
font-size: 10px;
|
|
font-weight: 900;
|
|
color: #64748b;
|
|
letter-spacing: 0.05em;
|
|
height: 28px;
|
|
}
|
|
|
|
.locked-overlay {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: rgba(255, 255, 255, 0.4);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 5;
|
|
}
|
|
|
|
.grid-search-container {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
width: 100%;
|
|
}
|
|
|
|
.grid-search-input {
|
|
flex: 1;
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* Weekend Helper Classes */
|
|
.bg-weekend { background-color: #f8fafc !important; }
|
|
.text-sat { color: #2e7d32 !important; }
|
|
.text-sun { color: #c62828 !important; }
|
|
|
|
/* Avatar Group Overlap - Global Item Style */
|
|
.avatar-stack-item {
|
|
margin-left: -10px;
|
|
border: 2px solid #303f9f; /* indigo-9 toolbar border color */
|
|
transition: transform 0.2s ease;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
}
|
|
/* Scaling restricted to user faces only */
|
|
.avatar-stack-img:hover {
|
|
transform: scale(1.15);
|
|
z-index: 10;
|
|
}
|
|
.avatar-stack-item:first-child {
|
|
margin-left: 0;
|
|
}
|
|
|
|
/* Fixed Count Circle Style - Strict Dimensions */
|
|
.avatar-stack-count-circle {
|
|
width: 30px;
|
|
height: 30px;
|
|
min-width: 30px;
|
|
min-height: 30px;
|
|
border-radius: 50%;
|
|
background-color: #1a237e; /* indigo-10 */
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 10px;
|
|
font-weight: bold;
|
|
z-index: 5;
|
|
}
|
|
|
|
/* Google Sheets Style Profile Card in Tooltip */
|
|
.profile-card {
|
|
min-width: 240px;
|
|
padding: 12px;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div id="q-app">
|
|
<q-layout view="hHh Lpr fFf">
|
|
|
|
<!-- Main Header -->
|
|
<q-header elevated class="bg-indigo-9 text-white">
|
|
<q-toolbar class="q-py-xs">
|
|
<q-btn flat round dense icon="menu" @click="leftDrawer = !leftDrawer"></q-btn>
|
|
|
|
<div class="row items-center q-ml-md q-mr-sm">
|
|
<q-toolbar-title class="text-weight-bolder">
|
|
Hotline Planner
|
|
</q-toolbar-title>
|
|
</div>
|
|
|
|
<q-space></q-space>
|
|
|
|
<!-- Right Utility Section -->
|
|
<div class="row items-center q-gutter-sm">
|
|
|
|
<q-btn flat round dense icon="notifications">
|
|
<q-tooltip>Notifications</q-tooltip>
|
|
</q-btn>
|
|
|
|
<!-- USERS ONLINE SECTION -->
|
|
<div class="row items-center gt-sm q-mx-md">
|
|
<div class="row items-center no-wrap">
|
|
<!-- User Avatars -->
|
|
<q-avatar
|
|
v-for="user in visibleOnlineUsers"
|
|
:key="user.id"
|
|
size="30px"
|
|
class="avatar-stack-item avatar-stack-img"
|
|
>
|
|
<img :src="user.img">
|
|
<q-tooltip
|
|
anchor="bottom middle"
|
|
self="top middle"
|
|
:offset="[0, 10]"
|
|
class="bg-white text-grey-9 shadow-10 q-pa-none rounded-borders border"
|
|
>
|
|
<div class="profile-card row no-wrap items-start">
|
|
<q-avatar size="48px" class="q-mr-md border">
|
|
<img :src="user.img">
|
|
</q-avatar>
|
|
<div class="column flex-1 min-w-0">
|
|
<div class="text-weight-bold text-subtitle2 flex items-center no-wrap">
|
|
{{ user.name }}
|
|
<q-icon name="verified" color="blue-6" size="14px" class="q-ml-xs"></q-icon>
|
|
</div>
|
|
<div class="text-caption text-grey-7 text-weight-medium">{{ user.role }}</div>
|
|
<div class="row items-center text-grey-5 q-mt-xs" style="font-size: 10px;">
|
|
<q-icon name="mail" size="12px" class="q-mr-xs"></q-icon>
|
|
<span class="truncate">{{ user.email }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</q-tooltip>
|
|
</q-avatar>
|
|
|
|
<!-- FIXED COUNT CIRCLE (Using div for absolute layout stability) -->
|
|
<div
|
|
v-if="remainingOnlineCount > 0"
|
|
class="avatar-stack-item avatar-stack-count-circle"
|
|
>
|
|
+{{ remainingOnlineCount }}
|
|
|
|
<q-tooltip>Click to see all users online</q-tooltip>
|
|
|
|
<q-menu class="bg-white shadow-15 border" anchor="bottom right" self="top right">
|
|
<q-list dense style="min-width: 220px">
|
|
<q-item-label header class="text-weight-bold text-overline text-indigo-9 bg-grey-1">
|
|
ALL USERS ONLINE ({{ onlineUsers.length }})
|
|
</q-item-label>
|
|
<q-separator></q-separator>
|
|
<q-scroll-area style="height: 250px;">
|
|
<q-item v-for="user in onlineUsers" :key="user.id" clickable v-ripple class="q-py-sm">
|
|
<q-item-section avatar>
|
|
<q-avatar size="28px">
|
|
<img :src="user.img">
|
|
</q-avatar>
|
|
</q-item-section>
|
|
<q-item-section>
|
|
<q-item-label class="text-weight-bold text-caption text-grey-8">{{ user.name }}</q-item-label>
|
|
<q-item-label caption lines="1">{{ user.role }}</q-item-label>
|
|
</q-item-section>
|
|
</q-item>
|
|
</q-scroll-area>
|
|
</q-list>
|
|
</q-menu>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<q-btn flat round dense icon="account_circle">
|
|
<q-tooltip>My Profile</q-tooltip>
|
|
</q-btn>
|
|
</div>
|
|
</toolbar>
|
|
</q-header>
|
|
|
|
<!-- Settings Drawer -->
|
|
<q-drawer v-model="leftDrawer" side="left" bordered :width="280">
|
|
<q-scroll-area class="fit">
|
|
<q-list padding>
|
|
<q-item-label header class="text-weight-bold text-uppercase text-caption text-indigo-9">Planner Configurations</q-item-label>
|
|
|
|
<q-item>
|
|
<q-item-section>
|
|
<q-item-label caption class="q-mb-xs">Viewing Scope</q-item-label>
|
|
<q-select
|
|
v-model="viewScope"
|
|
:options="[4, 8]"
|
|
dense
|
|
outlined
|
|
emit-value
|
|
map-options
|
|
suffix=" Weeks"
|
|
></q-select>
|
|
</q-item-section>
|
|
</q-item>
|
|
|
|
<q-item>
|
|
<q-item-section>
|
|
<q-item-label caption class="q-mb-xs">Filter by Team</q-item-label>
|
|
<q-select
|
|
v-model="activeDept"
|
|
:options="['All', ...depts]"
|
|
dense
|
|
outlined
|
|
></q-select>
|
|
</q-item-section>
|
|
</q-item>
|
|
|
|
<q-separator q-my-md></q-separator>
|
|
|
|
<q-item tag="label" v-ripple>
|
|
<q-item-section>
|
|
<q-item-label>Compact View</q-item-label>
|
|
<q-item-label caption>Reduce row height for high density</q-item-label>
|
|
</q-item-section>
|
|
<q-item-section side>
|
|
<q-toggle v-model="isCompact" color="indigo"></q-toggle>
|
|
</q-item-section>
|
|
</q-item>
|
|
|
|
<q-item tag="label" v-ripple>
|
|
<q-item-section>
|
|
<q-item-label>Weekends are Working Days</q-item-label>
|
|
<q-item-label caption>Allow shift entry on Sat/Sun</q-item-label>
|
|
</q-item-section>
|
|
<q-item-section side>
|
|
<q-toggle v-model="weekendsAreWorkingDays" color="indigo"></q-toggle>
|
|
</q-item-section>
|
|
</q-item>
|
|
|
|
<q-item clickable v-ripple @click="resetToToday">
|
|
<q-item-section avatar>
|
|
<q-icon name="today"></q-icon>
|
|
</q-item-section>
|
|
<q-item-section>Reset to Today</q-item-section>
|
|
</q-item>
|
|
</q-list>
|
|
</q-scroll-area>
|
|
</q-drawer>
|
|
|
|
<!-- Multi-Purpose Sidebar -->
|
|
<q-drawer v-model="rightDrawer" side="right" bordered :width="360" overlay elevated>
|
|
<div class="column full-height bg-white">
|
|
<q-toolbar class="bg-grey-2">
|
|
<q-toolbar-title class="text-subtitle2 text-weight-bold">
|
|
{{ editMode === 'assignment' ? 'Assignment Entry' : 'Agent Profile' }}
|
|
</q-toolbar-title>
|
|
<q-btn flat round dense icon="close" @click="rightDrawer = false"></q-btn>
|
|
</q-toolbar>
|
|
|
|
<q-scroll-area class="col q-pa-md">
|
|
<div v-if="selectedAgent" class="row items-center q-mb-md q-pa-sm bg-indigo-1 rounded-borders border-indigo-2 border">
|
|
<q-avatar size="40px">
|
|
<img :src="selectedAgent.avatar">
|
|
</q-avatar>
|
|
<div class="q-ml-sm">
|
|
<div class="text-weight-bold text-indigo-10">{{ selectedAgent.name }}</div>
|
|
<div class="text-caption text-indigo-7">{{ selectedAgent.dept }} Division • {{ selectedAgent.role }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="editMode === 'assignment' && selectedDate" class="q-mb-lg q-pa-md bg-white border rounded-borders shadow-1">
|
|
<div class="row items-center justify-between">
|
|
<div class="column">
|
|
<div class="text-overline text-grey-6 leading-none">Selected Date</div>
|
|
<div class="text-h6 text-weight-bolder text-slate-8">
|
|
{{ selectedDate.toLocaleDateString('en-US', { weekday: 'long' }) }}
|
|
</div>
|
|
<div class="text-subtitle2 text-grey-7">
|
|
{{ selectedDate.toLocaleDateString('en-US', { day: 'numeric', month: 'long', year: 'numeric' }) }}
|
|
</div>
|
|
</div>
|
|
<q-icon name="event" size="md" color="indigo-3"></q-icon>
|
|
</div>
|
|
</div>
|
|
|
|
<template v-if="editMode === 'assignment'">
|
|
<div class="text-overline q-mb-sm text-grey-6 text-weight-bold">Shift Selection</div>
|
|
<div class="row q-col-gutter-sm q-mb-lg">
|
|
<div v-for="s in shifts" :key="s.id" class="col-6">
|
|
<q-btn :color="s.color" outline dense no-caps :label="s.label" class="full-width"></q-btn>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="text-overline q-mb-sm text-grey-6 text-weight-bold">Custom Activity</div>
|
|
<div class="row q-col-gutter-sm q-mb-md">
|
|
<div class="col-6"><q-input filled dense label="Start" mask="time"></q-input></div>
|
|
<div class="col-6"><q-input filled dense label="End" mask="time"></q-input></div>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-else-if="editMode === 'profile'">
|
|
<div class="text-overline q-mb-sm text-grey-6 text-weight-bold">Basic Information</div>
|
|
<div class="column q-gutter-y-md q-mb-lg">
|
|
<q-input filled dense v-model="selectedAgent.name" label="Full Name"></q-input>
|
|
<q-select filled dense v-model="selectedAgent.role" :options="roles" label="Primary Role"></q-select>
|
|
<q-select filled dense v-model="selectedAgent.dept" :options="depts" label="Division / Department"></q-select>
|
|
</div>
|
|
|
|
<div class="text-overline q-mb-sm text-grey-6 text-weight-bold">Skills & Expertise</div>
|
|
<div class="column q-gutter-y-sm">
|
|
<q-select
|
|
filled
|
|
dense
|
|
v-model="selectedAgent.skills"
|
|
multiple
|
|
use-chips
|
|
:options="allSkills"
|
|
label="Select Skills"
|
|
></q-select>
|
|
</div>
|
|
</template>
|
|
</q-scroll-area>
|
|
|
|
<div class="q-pa-md border-t row q-gutter-sm">
|
|
<q-btn outline label="Discard" class="col" @click="rightDrawer = false"></q-btn>
|
|
<q-btn unelevated color="indigo-9" label="Save Changes" class="col" @click="rightDrawer = false"></q-btn>
|
|
</div>
|
|
</div>
|
|
</q-drawer>
|
|
|
|
<!-- Main Page -->
|
|
<q-page-container>
|
|
<q-page class="q-pa-none">
|
|
<div class="grid-viewport shadow-1" ref="viewport">
|
|
<div class="planner-content">
|
|
|
|
<!-- 1. EOD TARGETS -->
|
|
<div class="planner-row row-eod">
|
|
<div class="left-col pink text-overline text-weight-bolder">EOD TARGETS</div>
|
|
<div class="cells-area">
|
|
<div v-for="i in dates.length" :key="'e'+i" class="cell" style="height: var(--h-eod)">
|
|
<div class="text-weight-bold text-caption">94%</div>
|
|
<q-linear-progress :value="0.94" color="pink-7" size="3px" rounded style="width: 70%"></q-linear-progress>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 2. COVERAGE STATUS -->
|
|
<div class="planner-row row-status">
|
|
<div class="left-col white text-overline text-grey-6">COVERAGE STATUS</div>
|
|
<div class="cells-area">
|
|
<div v-for="(d, i) in dates" :key="'s'+i" class="cell" style="height: var(--h-status)">
|
|
<q-icon name="circle" :color="i % 7 === 0 ? 'red' : (i % 5 === 0 ? 'orange' : 'green')" size="10px"></q-icon>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 3. DATE LABELS + INTEGRATED SEARCH AND CALENDAR -->
|
|
<div class="planner-row row-dates">
|
|
<div class="left-col grey">
|
|
<div class="grid-search-container">
|
|
<q-input
|
|
v-model="search"
|
|
dense
|
|
filled
|
|
placeholder="Find agent..."
|
|
class="grid-search-input"
|
|
bg-color="white"
|
|
>
|
|
<template v-slot:prepend>
|
|
<q-icon name="search" size="xs"></q-icon>
|
|
</template>
|
|
</q-input>
|
|
|
|
<q-btn flat round dense icon="calendar_today" color="indigo-9" size="sm">
|
|
<q-menu>
|
|
<q-date
|
|
v-model="pickedDate"
|
|
mask="YYYY-MM-DD"
|
|
minimal
|
|
color="indigo-9"
|
|
@update:model-value="handleDateSelection"
|
|
></q-date>
|
|
</q-menu>
|
|
<q-tooltip>Go to Date</q-tooltip>
|
|
</q-btn>
|
|
</div>
|
|
</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' : '']"
|
|
style="height: var(--h-dates)">
|
|
<div class="text-[9px] uppercase text-weight-black"
|
|
:class="[isWeekend(date) && !weekendsAreWorkingDays ? (date.getDay() === 6 ? 'text-sat' : 'text-sun') : 'text-grey-500']">
|
|
{{ date.toLocaleDateString('en-US', {weekday: 'short'}) }}
|
|
</div>
|
|
<div class="text-subtitle2 text-weight-bold leading-none"
|
|
:class="[isWeekend(date) && !weekendsAreWorkingDays ? (date.getDay() === 6 ? 'text-sat' : 'text-sun') : 'text-grey-9']">
|
|
{{ date.getDate() }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- DATA ROWS -->
|
|
<div v-for="dept in depts" :key="dept">
|
|
<div v-if="activeDept === 'All' || activeDept === dept" class="planner-row dept-divider">
|
|
<div class="left-col dept-divider border-r">{{ dept }} DIVISION</div>
|
|
<div class="cells-area"><div class="cell" style="flex: 1; height: 28px"></div></div>
|
|
</div>
|
|
|
|
<template v-if="activeDept === 'All' || activeDept === dept">
|
|
<div v-for="agent in filteredAgentsByDept(dept)" :key="agent.id" class="planner-row hover:bg-blue-1">
|
|
<div class="left-col border-b cursor-pointer" :class="[isCompact ? 'cell-compact' : '']" @click="openProfile(agent)">
|
|
<q-avatar :size="isCompact ? '22px' : '30px'">
|
|
<img :src="agent.avatar">
|
|
</q-avatar>
|
|
<div class="q-ml-sm overflow-hidden">
|
|
<div class="text-weight-bold truncate" :style="{fontSize: isCompact ? '11px' : '13px'}">{{ agent.name }}</div>
|
|
<div v-if="!isCompact" class="text-[9px] text-grey-6 uppercase font-bold truncate">{{ agent.role }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="cells-area">
|
|
<div v-for="(date, i) in dates" :key="'c'+agent.id+i"
|
|
class="cell"
|
|
:class="[
|
|
isCompact ? 'cell-compact' : '',
|
|
(agent.id + i) === 42 ? 'bg-amber-1' : '',
|
|
isWeekend(date) && !weekendsAreWorkingDays ? 'bg-weekend cursor-not-allowed' : 'cursor-pointer'
|
|
]"
|
|
@click="openAssignment(agent, date)">
|
|
|
|
<div v-if="(agent.id + i) === 42" class="locked-overlay">
|
|
<q-icon name="lock" color="amber-10" size="14px"></q-icon>
|
|
</div>
|
|
|
|
<template v-if="weekendsAreWorkingDays || !isWeekend(date)">
|
|
<div v-if="(agent.id + i) % 6 === 0" class="shift-badge bg-green-1 text-green-9 border-green-2 border">
|
|
MORNING
|
|
</div>
|
|
<div v-if="(agent.id + i) % 8 === 0" class="shift-badge bg-blue-1 text-blue-9 border-blue-2 border">
|
|
HOTLINE
|
|
</div>
|
|
</template>
|
|
|
|
<div class="absolute-bottom-right q-pa-xs row no-wrap" style="gap: 2px">
|
|
<q-icon v-if="(agent.id + i) % 11 === 0" name="chat_bubble_outline" size="8px" color="grey-4"></q-icon>
|
|
<q-icon v-if="(agent.id + i) % 13 === 0" name="info_outline" size="8px" color="orange-3"></q-icon>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
</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, nextTick } = Vue;
|
|
|
|
const DEPARTMENTS = ["Support", "Technical", "Sales", "VIP", "Billing"];
|
|
const ROLES = ["Senior Lead", "Specialist", "Agent"];
|
|
const SKILLS = ["English", "German", "Technical Support", "VIP Handling", "Molecular App", "Hardware", "Billing Specialist"];
|
|
|
|
const CORE_USERS = [
|
|
{ id: 1, name: "Alice Freeman", email: "alice.f@company.com", role: "Product Manager", img: "https://i.pravatar.cc/150?u=1" },
|
|
{ id: 2, name: "Bob Smith", email: "bob.smith@company.com", role: "Senior Developer", img: "https://i.pravatar.cc/150?u=2" },
|
|
{ id: 3, name: "Charlie Kim", email: "charlie.k@company.com", role: "UX Designer", img: "https://i.pravatar.cc/150?u=3" },
|
|
{ id: 4, name: "Diana Prince", email: "diana.p@company.com", role: "QA Lead", img: "https://i.pravatar.cc/150?u=4" },
|
|
{ id: 5, name: "Evan Wright", email: "evan.w@company.com", role: "Frontend Dev", img: "https://i.pravatar.cc/150?u=5" },
|
|
];
|
|
const generateMoreUsers = (startId, count) => {
|
|
return Array.from({ length: count }, (_, i) => ({
|
|
id: startId + i,
|
|
name: `Colleague ${startId + i}`,
|
|
email: `colleague.${startId + i}@company.com`,
|
|
role: "Contributor",
|
|
img: `https://i.pravatar.cc/150?u=${startId + i}`
|
|
}));
|
|
};
|
|
const ALL_ONLINE_USERS = [...CORE_USERS, ...generateMoreUsers(6, 27)];
|
|
|
|
const AGENTS = Array.from({ length: 100 }, (_, i) => ({
|
|
id: i + 1,
|
|
name: `Agent ${i + 1}`,
|
|
dept: DEPARTMENTS[i % DEPARTMENTS.length],
|
|
role: ROLES[i % ROLES.length],
|
|
skills: [SKILLS[i % SKILLS.length]],
|
|
avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=Agent${i}`
|
|
}));
|
|
|
|
const app = createApp({
|
|
setup() {
|
|
const leftDrawer = 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(4);
|
|
const search = ref("");
|
|
const activeDept = ref("All");
|
|
const selectedAgent = ref(null);
|
|
const selectedDate = ref(null);
|
|
|
|
const onlineUsers = ref(ALL_ONLINE_USERS);
|
|
const maxVisibleUsers = 5;
|
|
const visibleOnlineUsers = computed(() => onlineUsers.value.slice(0, maxVisibleUsers));
|
|
const remainingOnlineCount = computed(() => Math.max(0, onlineUsers.value.length - maxVisibleUsers));
|
|
|
|
const startDate = ref(new Date());
|
|
const pickedDate = ref(null);
|
|
|
|
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 handleDateSelection = (val) => {
|
|
if (!val) return;
|
|
const target = new Date(val);
|
|
const targetStr = target.toDateString();
|
|
const index = dates.value.findIndex(d => d.toDateString() === targetStr);
|
|
|
|
if (index === -1) {
|
|
startDate.value = target;
|
|
nextTick(() => {
|
|
if (viewport.value) viewport.value.scrollLeft = 0;
|
|
});
|
|
} else {
|
|
scrollToIndex(index);
|
|
}
|
|
};
|
|
|
|
const scrollToIndex = (index) => {
|
|
if (!viewport.value) return;
|
|
const cellWidth = 100;
|
|
viewport.value.scrollLeft = index * cellWidth;
|
|
};
|
|
|
|
const resetToToday = () => {
|
|
startDate.value = new Date();
|
|
handleDateSelection(new Date().toISOString().split('T')[0]);
|
|
leftDrawer.value = false;
|
|
};
|
|
|
|
const filteredAgents = computed(() => {
|
|
return AGENTS.filter(a => a.name.toLowerCase().includes(search.value.toLowerCase()));
|
|
});
|
|
|
|
const filteredAgentsByDept = (dept) => {
|
|
return filteredAgents.value.filter(a => a.dept === dept);
|
|
};
|
|
|
|
const openAssignment = (agent, date) => {
|
|
if (!weekendsAreWorkingDays.value && isWeekend(date)) return;
|
|
editMode.value = 'assignment';
|
|
selectedAgent.value = agent;
|
|
selectedDate.value = date;
|
|
rightDrawer.value = true;
|
|
};
|
|
|
|
const openProfile = (agent) => {
|
|
editMode.value = 'profile';
|
|
selectedAgent.value = { ...agent };
|
|
selectedDate.value = null;
|
|
rightDrawer.value = true;
|
|
};
|
|
|
|
return {
|
|
leftDrawer, rightDrawer, editMode, isCompact, weekendsAreWorkingDays,
|
|
viewScope, search, activeDept, isWeekend, viewport,
|
|
onlineUsers, visibleOnlineUsers, remainingOnlineCount,
|
|
dates, depts: DEPARTMENTS, roles: ROLES, allSkills: SKILLS,
|
|
filteredAgents, filteredAgentsByDept,
|
|
openAssignment, openProfile, selectedAgent, selectedDate,
|
|
pickedDate, handleDateSelection, resetToToday,
|
|
shifts: [
|
|
{ id: 'm', label: 'Morning', color: 'green-7' },
|
|
{ id: 'a', label: 'Afternoon', color: 'blue-7' },
|
|
{ id: 'e', label: 'EOD Only', color: 'pink-7' }
|
|
]
|
|
};
|
|
}
|
|
});
|
|
|
|
app.use(Quasar);
|
|
app.mount('#q-app');
|
|
</script>
|
|
</body>
|
|
</html> |