365 lines
15 KiB
HTML
365 lines
15 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">
|
|
|
|
<!-- Component CSS Files (with proper namespacing) -->
|
|
<link rel="stylesheet" href="./src/components/app-header/app-header.css">
|
|
<link rel="stylesheet" href="./src/components/grid-container/grid-container.css">
|
|
<link rel="stylesheet" href="./src/components/grid-cell/grid-cell.css">
|
|
<link rel="stylesheet" href="./src/components/agent-row/agent-row.css">
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<style>
|
|
:root {
|
|
--left-col-width: 240px;
|
|
--cell-width: 100px;
|
|
/* Dynamic offsets controlled by Vue */
|
|
--h-eod: 42px;
|
|
--h-status: 34px;
|
|
|
|
--h-dates: 52px;
|
|
--primary-soft: #f8fafc;
|
|
--border-color: #e2e8f0;
|
|
|
|
/* Crosshair Colors */
|
|
--highlight-bg: rgba(99, 102, 241, 0.12);
|
|
--highlight-border: rgba(99, 102, 241, 0.5);
|
|
}
|
|
|
|
/* MOBILE OPTIMIZATION */
|
|
@media (max-width: 599px) {
|
|
:root {
|
|
--left-col-width: 160px;
|
|
}
|
|
}
|
|
|
|
body, html {
|
|
height: 100%;
|
|
overflow: hidden;
|
|
margin: 0;
|
|
padding: 0;
|
|
background-color: #f8fafc;
|
|
color: #334155;
|
|
}
|
|
|
|
/* Minimal global styles - most moved to component CSS files */
|
|
.q-field--outlined.q-field--rounded .q-field__control { border-radius: 12px; }
|
|
.q-btn--rounded { border-radius: 10px; }
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div id="q-app">
|
|
<q-layout view="hHh Lpr fFf">
|
|
|
|
<!-- App Header Component -->
|
|
<app-header @toggle-left-drawer="leftDrawer = !leftDrawer"></app-header>
|
|
|
|
<!-- Left Drawer - Workspace Settings -->
|
|
<q-drawer v-model="leftDrawer" side="left" bordered :width="300" class="bg-white">
|
|
<q-scroll-area class="fit">
|
|
<div class="q-pa-lg">
|
|
<div class="text-h6 text-weight-bold q-mb-xs text-indigo-10">Workspace</div>
|
|
<div class="text-caption text-grey-6 q-mb-xl">Manage grid and picker preferences.</div>
|
|
<div class="column q-gutter-y-lg">
|
|
<div>
|
|
<div class="text-overline text-grey-6 q-mb-sm">Timeline Range</div>
|
|
<q-select v-model="viewScope" :options="[4, 8, 12]" outlined dense rounded emit-value map-options suffix=" Weeks" bg-color="white"></q-select>
|
|
</div>
|
|
<div>
|
|
<div class="text-overline text-grey-6 q-mb-sm">Date Picker Week Start</div>
|
|
<q-select v-model="pickerStartDay" :options="[{label: 'Sunday', value: 0}, {label: 'Monday', value: 1}]" outlined dense rounded emit-value map-options bg-color="white"></q-select>
|
|
</div>
|
|
|
|
<q-separator></q-separator>
|
|
|
|
<div>
|
|
<div class="text-overline text-grey-6 q-mb-sm">Dev Tools</div>
|
|
<q-btn
|
|
outline
|
|
rounded
|
|
color="purple-7"
|
|
label="Simulate WSS Lock"
|
|
class="full-width"
|
|
icon="lock"
|
|
@click="simulateWssLock"
|
|
>
|
|
<q-tooltip>Simulate receiving a "Lock Cell" message from server</q-tooltip>
|
|
</q-btn>
|
|
</div>
|
|
|
|
<q-separator></q-separator>
|
|
|
|
<div>
|
|
<div class="text-overline text-grey-6 q-mb-sm">Grid Visibility</div>
|
|
<q-item tag="label" v-ripple class="q-px-none">
|
|
<q-item-section><q-item-label class="text-weight-medium">Reading Crosshair</q-item-label><q-item-label caption>Dynamic highlight on hover</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>
|
|
<q-item tag="label" v-ripple class="q-px-none">
|
|
<q-item-section><q-item-label class="text-weight-medium">Show EOD Targets</q-item-label><q-item-label caption>Top row KPI</q-item-label></q-item-section>
|
|
<q-item-section side><q-toggle v-model="showEodTargets" color="pink-6"></q-toggle></q-item-section>
|
|
</q-item>
|
|
<q-item tag="label" v-ripple class="q-px-none">
|
|
<q-item-section><q-item-label class="text-weight-medium">Show Availability</q-item-label><q-item-label caption>Traffic light indicators</q-item-label></q-item-section>
|
|
<q-item-section side><q-toggle v-model="showAvailability" color="orange-6"></q-toggle></q-item-section>
|
|
</q-item>
|
|
</div>
|
|
|
|
<q-separator></q-separator>
|
|
|
|
<q-item tag="label" v-ripple class="q-px-none">
|
|
<q-item-section><q-item-label class="text-weight-medium">Compact Grid</q-item-label><q-item-label caption>High density 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 class="text-weight-medium">Working Weekends</q-item-label><q-item-label caption>Enable Sat/Sun shifts</q-item-label></q-item-section>
|
|
<q-item-section side><q-toggle v-model="weekendsAreWorkingDays" color="indigo-8"></q-toggle></q-item-section>
|
|
</q-item>
|
|
<q-btn outline rounded color="indigo-8" label="Reset to Today" icon="today" class="full-width q-mt-md" @click="resetToToday"></q-btn>
|
|
</div>
|
|
</div>
|
|
</q-scroll-area>
|
|
</q-drawer>
|
|
|
|
<!-- Right Assignment Drawer -->
|
|
<q-drawer v-model="rightDrawer" side="right" bordered :width="380" overlay elevated>
|
|
<div class="column full-height bg-white">
|
|
<q-toolbar class="bg-white border-b q-px-md" style="height: 64px">
|
|
<q-toolbar-title class="text-subtitle1 text-weight-bold">{{ editMode === 'assignment' ? 'Edit Assignment' : 'Agent Details' }}</q-toolbar-title>
|
|
<q-btn flat round dense icon="close" color="grey-5" @click="rightDrawer = false"></q-btn>
|
|
</q-toolbar>
|
|
<q-scroll-area class="col q-pa-lg">
|
|
<div v-if="selectedAgent" class="row items-center q-mb-xl">
|
|
<q-avatar size="64px" class="shadow-1"><img :src="selectedAgent.avatar"></q-avatar>
|
|
<div class="q-ml-md">
|
|
<div class="text-h6 text-weight-bold">{{ selectedAgent.name }}</div>
|
|
<div class="text-caption text-grey-7">{{ selectedAgent.dept }} • {{ selectedAgent.role }}</div>
|
|
<div class="text-caption text-indigo-8 text-weight-bold q-mt-xs">
|
|
<q-icon name="public" size="xs" class="q-mr-xs"></q-icon> {{ selectedAgent.hubName }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<template v-if="editMode === 'assignment' && selectedDate">
|
|
<div class="q-mb-lg q-pa-md bg-blue-grey-1 rounded-borders border">
|
|
<div class="row items-center justify-between no-wrap">
|
|
<div>
|
|
<div class="text-overline text-blue-grey-4">Shift Date</div>
|
|
<div class="text-subtitle1 text-weight-bold text-blue-grey-9">{{ selectedDate.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' }) }}</div>
|
|
</div>
|
|
<q-icon name="event" color="blue-grey-2" size="sm"></q-icon>
|
|
</div>
|
|
</div>
|
|
<div class="text-weight-bold q-mb-md">Standard Shifts</div>
|
|
<div class="row q-col-gutter-sm q-mb-xl">
|
|
<div v-for="s in shifts" :key="s.id" class="col-6"><q-btn :color="s.color" :unelevated="currentAssignmentLabel === s.label" :outline="currentAssignmentLabel !== s.label" rounded dense no-caps :label="s.label" class="full-width q-py-sm" @click="setPendingShift(s)"></q-btn></div>
|
|
<div class="col-12"><q-btn outline rounded dense color="red-4" label="Clear Assignment" class="full-width q-mt-sm" icon="delete_outline" @click="setPendingShift(null)"></q-btn></div>
|
|
</div>
|
|
</template>
|
|
</q-scroll-area>
|
|
<div class="q-pa-lg border-t row q-gutter-md bg-grey-1">
|
|
<q-btn flat rounded label="Cancel" class="col" @click="rightDrawer = false"></q-btn>
|
|
<q-btn unelevated rounded color="indigo-10" label="Save Assignment" class="col" @click="saveAssignment"></q-btn>
|
|
</div>
|
|
</div>
|
|
</q-drawer>
|
|
|
|
<q-page-container>
|
|
<q-page class="q-pa-none">
|
|
<!-- LOADING OVERLAY -->
|
|
<q-inner-loading :showing="loading">
|
|
<q-spinner-gears size="50px" color="indigo-8"></q-spinner-gears>
|
|
<div class="text-indigo-8 q-mt-sm">Loading Agents...</div>
|
|
</q-inner-loading>
|
|
|
|
<!-- Grid Container Component -->
|
|
<grid-container
|
|
:grouping-selection="['hub', 'dept']"
|
|
@open-assignment="openAssignment"
|
|
@open-profile="openProfile"
|
|
>
|
|
<!-- EOD Targets Slot -->
|
|
<template #eod-targets>
|
|
<div v-if="showEodTargets" class="planner-row row-eod">
|
|
<div class="left-col text-overline text-weight-bold">EOD Targets</div>
|
|
<div class="cells-area">
|
|
<div v-for="(d, i) in dates" :key="'e'+i" class="cell" style="height: var(--h-eod)">
|
|
<div class="text-weight-bold text-[10px] text-pink-7">94%</div>
|
|
<q-linear-progress :value="0.94" color="pink-3" size="3px" rounded style="width: 60%"></q-linear-progress>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Availability Slot -->
|
|
<template #availability>
|
|
<div v-if="showAvailability" class="planner-row row-status">
|
|
<div class="left-col text-overline text-grey-5">Availability</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 % 10 === 0 ? 'red-3' : (i % 5 === 0 ? 'orange-3' : 'green-3')" size="8px"></q-icon>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Agent Row Slot -->
|
|
<template #agent-row="{ agent, openProfile }">
|
|
<agent-row :agent="agent" @click="openProfile"></agent-row>
|
|
</template>
|
|
|
|
<!-- Agent Cells Slot -->
|
|
<template #agent-cells="{ agent, dates, openAssignment }">
|
|
<div class="cells-area">
|
|
<grid-cell
|
|
v-for="(date, i) in dates"
|
|
:key="`c${agent.id}${i}`"
|
|
:agent="agent"
|
|
:date="date"
|
|
@click="openAssignment"
|
|
></grid-cell>
|
|
</div>
|
|
</template>
|
|
</grid-container>
|
|
|
|
</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 type="module">
|
|
// Import services
|
|
import { loadDataFromDatabase, getAssignment, setAssignment, SHIFTS, agents, loading } from './src/services/data-service.js';
|
|
import { simulateWssLock } from './src/services/socket-service.js';
|
|
import { formatDateForId } from './src/services/date-helper-service.js';
|
|
import {
|
|
isCompact,
|
|
weekendsAreWorkingDays,
|
|
viewScope,
|
|
pickerStartDay,
|
|
showEodTargets,
|
|
showAvailability,
|
|
crosshairActive,
|
|
dates,
|
|
resetToToday
|
|
} from './src/services/grid-state-service.js';
|
|
|
|
// Import components
|
|
import AppHeader from './src/components/app-header/app-header.js';
|
|
import GridContainer from './src/components/grid-container/grid-container.js';
|
|
import GridCell from './src/components/grid-cell/grid-cell.js';
|
|
import AgentRow from './src/components/agent-row/agent-row.js';
|
|
|
|
const { createApp, ref, computed, nextTick, onMounted } = Vue;
|
|
const { useQuasar } = Quasar;
|
|
|
|
const app = createApp({
|
|
components: {
|
|
AppHeader,
|
|
GridContainer,
|
|
GridCell,
|
|
AgentRow
|
|
},
|
|
setup() {
|
|
const $q = useQuasar();
|
|
|
|
// UI State
|
|
const leftDrawer = ref(false);
|
|
const rightDrawer = ref(false);
|
|
const editMode = ref('assignment');
|
|
const selectedAgent = ref(null);
|
|
const selectedDate = ref(null);
|
|
const pendingShift = ref(null);
|
|
|
|
// Current assignment label
|
|
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;
|
|
});
|
|
|
|
// Methods
|
|
const openAssignment = (agent, date) => {
|
|
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 shiftId = pendingShift.value ? pendingShift.value.id : null;
|
|
setAssignment(agentId, selectedDate.value, shiftId);
|
|
pendingShift.value = null;
|
|
rightDrawer.value = false;
|
|
};
|
|
|
|
const handleSimulateWssLock = () => {
|
|
simulateWssLock(agents.value);
|
|
};
|
|
|
|
// Lifecycle
|
|
onMounted(() => {
|
|
loadDataFromDatabase(new Date());
|
|
});
|
|
|
|
return {
|
|
// UI State
|
|
leftDrawer,
|
|
rightDrawer,
|
|
editMode,
|
|
selectedAgent,
|
|
selectedDate,
|
|
pendingShift,
|
|
currentAssignmentLabel,
|
|
|
|
// Grid State (imported from services)
|
|
loading,
|
|
agents,
|
|
dates,
|
|
isCompact,
|
|
weekendsAreWorkingDays,
|
|
viewScope,
|
|
pickerStartDay,
|
|
showEodTargets,
|
|
showAvailability,
|
|
crosshairActive,
|
|
|
|
// Constants
|
|
shifts: SHIFTS,
|
|
|
|
// Methods
|
|
openAssignment,
|
|
openProfile,
|
|
setPendingShift,
|
|
saveAssignment,
|
|
resetToToday,
|
|
simulateWssLock: handleSimulateWssLock
|
|
};
|
|
}
|
|
});
|
|
|
|
app.use(Quasar, { config: { brand: { primary: '#334155' } } });
|
|
app.mount('#q-app');
|
|
</script>
|
|
</body>
|
|
</html> |