Files
hotline-planner/dev/ui-ux/Opus w images/src/components/grid-cell/grid-cell.js
2026-02-24 13:32:01 +01:00

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>
`
};