Files
2026-02-23 14:02:44 +01:00

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>