154 lines
6.0 KiB
JavaScript
154 lines
6.0 KiB
JavaScript
/**
|
|
* grid-cell.js
|
|
* =============
|
|
* A single shift cell inside an agent row.
|
|
* Shows time ranges + activity labels, special statuses.
|
|
*/
|
|
|
|
import {
|
|
formatDateForId,
|
|
isWeekend,
|
|
isToday,
|
|
weekendsAreWorkingDays,
|
|
isCompact,
|
|
crosshairActive,
|
|
hoveredDateStr,
|
|
highlightedRowId,
|
|
highlightedDateStr
|
|
} from '../../services/planner-state.js';
|
|
|
|
import {
|
|
getShifts,
|
|
hasComment,
|
|
hasNote,
|
|
getCommentText,
|
|
getNoteText,
|
|
SPECIAL_STATUSES
|
|
} from '../../services/data-service.js';
|
|
|
|
import { isCellLocked } from '../../services/socket-service.js';
|
|
|
|
export default {
|
|
name: 'GridCell',
|
|
props: {
|
|
agentId: { type: Number, required: true },
|
|
date: { type: Date, required: true }
|
|
},
|
|
emits: ['open-assignment'],
|
|
setup(props, { emit }) {
|
|
const today = Vue.computed(() => isToday(props.date));
|
|
|
|
const cellClass = Vue.computed(() => {
|
|
const agentId = props.agentId;
|
|
const date = props.date;
|
|
const isRow = highlightedRowId.value === agentId;
|
|
const isCol = highlightedDateStr.value === formatDateForId(date);
|
|
const isHoverCol = crosshairActive.value && hoveredDateStr.value === formatDateForId(date);
|
|
const isWknd = isWeekend(date) && !weekendsAreWorkingDays.value;
|
|
const locked = isCellLocked(agentId, date);
|
|
|
|
const classes = ['grid-cell-root'];
|
|
if (isCompact.value) classes.push('grid-cell-compact');
|
|
if (isWknd) classes.push('grid-cell-bg-weekend');
|
|
if (today.value) classes.push('grid-cell-today');
|
|
if (isWknd && !weekendsAreWorkingDays.value) classes.push('cursor-not-allowed');
|
|
else if (locked) classes.push('cursor-not-allowed');
|
|
else classes.push('cursor-pointer');
|
|
|
|
if (isRow && isCol) classes.push('grid-cell-bg-reading-mode-intersection');
|
|
else if (isRow || isCol) classes.push('grid-cell-bg-reading-mode');
|
|
|
|
if (isHoverCol) classes.push('grid-cell-col-hovered');
|
|
|
|
return classes.join(' ');
|
|
});
|
|
|
|
const shifts = Vue.computed(() => getShifts(props.agentId, props.date));
|
|
const locked = Vue.computed(() => isCellLocked(props.agentId, props.date));
|
|
const showShift = Vue.computed(() => weekendsAreWorkingDays.value || !isWeekend(props.date));
|
|
const comment = Vue.computed(() => hasComment(props.agentId, props.date));
|
|
const note = Vue.computed(() => hasNote(props.agentId, props.date));
|
|
const commentTxt = Vue.computed(() => getCommentText(props.agentId, props.date));
|
|
const noteTxt = Vue.computed(() => getNoteText(props.agentId, props.date));
|
|
|
|
// Check if this is a special status (sick leave, off, etc.)
|
|
const specialStatus = Vue.computed(() => {
|
|
if (!shifts.value || shifts.value.length === 0) return null;
|
|
const first = shifts.value[0];
|
|
if (first.type === 'special') {
|
|
return SPECIAL_STATUSES.find(s => s.id === first.statusId) || null;
|
|
}
|
|
return null;
|
|
});
|
|
|
|
// Get regular shift entries only
|
|
const regularShifts = Vue.computed(() => {
|
|
if (!shifts.value) return [];
|
|
return shifts.value.filter(s => s.type === 'shift');
|
|
});
|
|
|
|
const onEnter = () => { hoveredDateStr.value = formatDateForId(props.date); };
|
|
const onLeave = () => { hoveredDateStr.value = null; };
|
|
const onClick = () => { emit('open-assignment', props.date); };
|
|
|
|
return {
|
|
cellClass, shifts, regularShifts, specialStatus, locked, showShift,
|
|
comment, note, commentTxt, noteTxt, today,
|
|
onEnter, onLeave, onClick
|
|
};
|
|
},
|
|
template: `
|
|
<div :class="cellClass"
|
|
@mouseenter="onEnter"
|
|
@mouseleave="onLeave"
|
|
@click="onClick">
|
|
<template v-if="showShift">
|
|
|
|
<!-- Special Status (Sick Leave, Off, etc.) -->
|
|
<div v-if="specialStatus" class="grid-cell-special"
|
|
:style="{ backgroundColor: specialStatus.bgColor, borderColor: specialStatus.borderColor }">
|
|
<div class="grid-cell-special-emoji" v-if="specialStatus.emoji">{{ specialStatus.emoji }}</div>
|
|
<div class="grid-cell-special-label" :style="{ color: specialStatus.textColor }">{{ specialStatus.label }}</div>
|
|
<div class="grid-cell-special-sub" :style="{ color: specialStatus.textColor }" v-if="specialStatus.id === 'sick_leave'">Full Day</div>
|
|
</div>
|
|
|
|
<!-- Regular Shifts -->
|
|
<template v-else-if="regularShifts.length > 0">
|
|
<div v-for="(shift, idx) in regularShifts" :key="idx" class="grid-cell-shift"
|
|
:class="{ 'grid-cell-shift-second': idx > 0 }">
|
|
<div class="grid-cell-time">{{ shift.timeLabel }}</div>
|
|
<div class="grid-cell-activity-badge"
|
|
:style="{ backgroundColor: shift.activityBgColor, color: shift.activityTextColor }">
|
|
{{ shift.activityLabel }}
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
</template>
|
|
|
|
<!-- Lock overlay -->
|
|
<div v-if="locked" class="grid-cell-locked-overlay">
|
|
<q-icon name="lock" color="blue-grey-3" size="14px">
|
|
<q-tooltip class="bg-grey-9 text-white shadow-4 q-pa-sm">This cell is currently being edited by another user.</q-tooltip>
|
|
</q-icon>
|
|
</div>
|
|
|
|
<!-- Comment / Note indicators -->
|
|
<div class="grid-cell-indicators">
|
|
<q-icon v-if="comment" name="chat_bubble" size="8px" color="blue-grey-3" class="cursor-help">
|
|
<q-tooltip class="bg-white text-grey-9 border shadow-4 grid-cell-tooltip" anchor="top middle" self="bottom middle">
|
|
<div class="text-weight-bold text-caption text-indigo-9 q-mb-xs">User Comment</div>
|
|
<div class="text-caption text-grey-8">{{ commentTxt }}</div>
|
|
</q-tooltip>
|
|
</q-icon>
|
|
<q-icon v-if="note" name="info" size="8px" color="orange-4" class="cursor-help">
|
|
<q-tooltip class="bg-white text-grey-9 border shadow-4 grid-cell-tooltip" anchor="top middle" self="bottom middle">
|
|
<div class="text-weight-bold text-caption text-orange-9 q-mb-xs">Technical Note</div>
|
|
<div class="text-caption text-grey-8">{{ noteTxt }}</div>
|
|
</q-tooltip>
|
|
</q-icon>
|
|
</div>
|
|
</div>
|
|
`
|
|
};
|