Adds employee work schedule component
Introduces a new work schedule feature for managing employee shifts and schedules Implements interactive schedule view with: - Week-based schedule grid - Shift status tracking (work, vacation, sick, off) - Editable time ranges - Repeat shift functionality Enhances employee management with dynamic scheduling capabilities
This commit is contained in:
parent
d5a803ba80
commit
3214cbdc16
11 changed files with 1669 additions and 0 deletions
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
export class EmployeesController {
|
||||
private ratesSync: RatesSyncController | null = null;
|
||||
private scheduleController: ScheduleController | null = null;
|
||||
private listView: HTMLElement | null = null;
|
||||
private detailView: HTMLElement | null = null;
|
||||
|
||||
|
|
@ -25,6 +26,7 @@ export class EmployeesController {
|
|||
this.setupHistoryNavigation();
|
||||
this.restoreStateFromUrl();
|
||||
this.ratesSync = new RatesSyncController();
|
||||
this.scheduleController = new ScheduleController();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -397,3 +399,646 @@ class RatesSyncController {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule Controller
|
||||
*
|
||||
* Handles work schedule (vagtplan) functionality:
|
||||
* - Edit mode toggle
|
||||
* - Cell selection (single, ctrl+click, shift+click)
|
||||
* - Drawer interaction
|
||||
* - Time range slider
|
||||
* - Status options
|
||||
*/
|
||||
class ScheduleController {
|
||||
private isEditMode = false;
|
||||
private selectedCells: HTMLElement[] = [];
|
||||
private anchorCell: HTMLElement | null = null;
|
||||
private drawer: HTMLElement | null = null;
|
||||
private editBtn: HTMLElement | null = null;
|
||||
private scheduleTable: HTMLElement | null = null;
|
||||
|
||||
// Time range constants
|
||||
private readonly TIME_RANGE_MAX = 60; // 15 hours (06:00-21:00) * 4 intervals
|
||||
|
||||
constructor() {
|
||||
this.drawer = document.getElementById('schedule-drawer');
|
||||
this.editBtn = document.getElementById('scheduleEditBtn');
|
||||
this.scheduleTable = document.getElementById('scheduleTable');
|
||||
|
||||
if (!this.scheduleTable) return;
|
||||
|
||||
this.setupEditModeToggle();
|
||||
this.setupCellSelection();
|
||||
this.setupStatusOptions();
|
||||
this.setupTypeToggle();
|
||||
this.setupTimeRangeSlider();
|
||||
this.setupDrawerSave();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup edit mode toggle button
|
||||
*/
|
||||
private setupEditModeToggle(): void {
|
||||
this.editBtn?.addEventListener('click', () => {
|
||||
this.isEditMode = !this.isEditMode;
|
||||
document.body.classList.toggle('schedule-edit-mode', this.isEditMode);
|
||||
|
||||
if (this.editBtn) {
|
||||
const icon = this.editBtn.querySelector('i');
|
||||
const text = this.editBtn.childNodes[this.editBtn.childNodes.length - 1];
|
||||
|
||||
if (this.isEditMode) {
|
||||
icon?.classList.replace('ph-pencil-simple', 'ph-check');
|
||||
if (text && text.nodeType === Node.TEXT_NODE) {
|
||||
text.textContent = ' Færdig';
|
||||
}
|
||||
this.openDrawer();
|
||||
this.showEmptyState();
|
||||
} else {
|
||||
icon?.classList.replace('ph-check', 'ph-pencil-simple');
|
||||
if (text && text.nodeType === Node.TEXT_NODE) {
|
||||
text.textContent = ' Rediger';
|
||||
}
|
||||
this.closeDrawer();
|
||||
this.clearSelection();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup cell click selection
|
||||
*/
|
||||
private setupCellSelection(): void {
|
||||
if (!this.scheduleTable) return;
|
||||
|
||||
const dayCells = this.scheduleTable.querySelectorAll<HTMLElement>('swp-schedule-cell.day');
|
||||
|
||||
dayCells.forEach(cell => {
|
||||
// Double-click to enter edit mode
|
||||
cell.addEventListener('dblclick', () => {
|
||||
if (!this.isEditMode) {
|
||||
this.isEditMode = true;
|
||||
document.body.classList.add('schedule-edit-mode');
|
||||
|
||||
if (this.editBtn) {
|
||||
const icon = this.editBtn.querySelector('i');
|
||||
const text = this.editBtn.childNodes[this.editBtn.childNodes.length - 1];
|
||||
icon?.classList.replace('ph-pencil-simple', 'ph-check');
|
||||
if (text && text.nodeType === Node.TEXT_NODE) {
|
||||
text.textContent = ' Færdig';
|
||||
}
|
||||
}
|
||||
|
||||
this.openDrawer();
|
||||
this.clearSelection();
|
||||
cell.classList.add('selected');
|
||||
this.selectedCells = [cell];
|
||||
this.anchorCell = cell;
|
||||
this.updateDrawerFields();
|
||||
}
|
||||
});
|
||||
|
||||
// Click selection in edit mode
|
||||
cell.addEventListener('click', (e: MouseEvent) => {
|
||||
if (!this.isEditMode) return;
|
||||
|
||||
if (e.shiftKey && this.anchorCell) {
|
||||
// Shift+click: range selection
|
||||
this.selectRange(this.anchorCell, cell);
|
||||
} else if (e.ctrlKey || e.metaKey) {
|
||||
// Ctrl/Cmd+click: toggle selection
|
||||
if (cell.classList.contains('selected')) {
|
||||
cell.classList.remove('selected');
|
||||
this.selectedCells = this.selectedCells.filter(c => c !== cell);
|
||||
} else {
|
||||
cell.classList.add('selected');
|
||||
this.selectedCells.push(cell);
|
||||
this.anchorCell = cell;
|
||||
}
|
||||
} else {
|
||||
// Single click: replace selection
|
||||
this.clearSelection();
|
||||
cell.classList.add('selected');
|
||||
this.selectedCells = [cell];
|
||||
this.anchorCell = cell;
|
||||
}
|
||||
|
||||
this.updateDrawerFields();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a range of cells between anchor and target
|
||||
*/
|
||||
private selectRange(anchor: HTMLElement, target: HTMLElement): void {
|
||||
if (!this.scheduleTable) return;
|
||||
|
||||
const allDayCells = Array.from(this.scheduleTable.querySelectorAll<HTMLElement>('swp-schedule-cell.day'));
|
||||
const anchorIdx = allDayCells.indexOf(anchor);
|
||||
const targetIdx = allDayCells.indexOf(target);
|
||||
|
||||
// Calculate grid positions (7 columns for days)
|
||||
const anchorRow = Math.floor(anchorIdx / 7);
|
||||
const anchorCol = anchorIdx % 7;
|
||||
const targetRow = Math.floor(targetIdx / 7);
|
||||
const targetCol = targetIdx % 7;
|
||||
|
||||
const minRow = Math.min(anchorRow, targetRow);
|
||||
const maxRow = Math.max(anchorRow, targetRow);
|
||||
const minCol = Math.min(anchorCol, targetCol);
|
||||
const maxCol = Math.max(anchorCol, targetCol);
|
||||
|
||||
this.clearSelection();
|
||||
|
||||
allDayCells.forEach((c, idx) => {
|
||||
const row = Math.floor(idx / 7);
|
||||
const col = idx % 7;
|
||||
if (row >= minRow && row <= maxRow && col >= minCol && col <= maxCol) {
|
||||
c.classList.add('selected');
|
||||
this.selectedCells.push(c);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all selected cells
|
||||
*/
|
||||
private clearSelection(): void {
|
||||
this.selectedCells.forEach(c => c.classList.remove('selected'));
|
||||
this.selectedCells = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get initials from name
|
||||
*/
|
||||
private getInitials(name: string): string {
|
||||
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show empty state in drawer
|
||||
*/
|
||||
private showEmptyState(): void {
|
||||
const employeeDisplay = document.getElementById('scheduleFieldEmployee');
|
||||
const avatar = document.getElementById('scheduleFieldAvatar');
|
||||
const employeeName = document.getElementById('scheduleFieldEmployeeName');
|
||||
const dateField = document.getElementById('scheduleFieldDate');
|
||||
const weekdayField = document.getElementById('scheduleFieldWeekday');
|
||||
const drawerBody = this.drawer?.querySelector('swp-drawer-body') as HTMLElement;
|
||||
const drawerFooter = this.drawer?.querySelector('swp-drawer-footer') as HTMLElement;
|
||||
|
||||
if (employeeDisplay) employeeDisplay.classList.add('empty');
|
||||
if (employeeDisplay) employeeDisplay.classList.remove('multi');
|
||||
if (avatar) avatar.textContent = '?';
|
||||
if (employeeName) employeeName.textContent = 'Vælg celle...';
|
||||
if (dateField) dateField.textContent = '—';
|
||||
if (weekdayField) weekdayField.textContent = '—';
|
||||
if (drawerBody) {
|
||||
drawerBody.style.opacity = '0.5';
|
||||
drawerBody.style.pointerEvents = 'none';
|
||||
}
|
||||
if (drawerFooter) drawerFooter.style.display = 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Show edit state in drawer
|
||||
*/
|
||||
private showEditState(): void {
|
||||
const drawerBody = this.drawer?.querySelector('swp-drawer-body') as HTMLElement;
|
||||
const drawerFooter = this.drawer?.querySelector('swp-drawer-footer') as HTMLElement;
|
||||
|
||||
if (drawerBody) {
|
||||
drawerBody.style.opacity = '1';
|
||||
drawerBody.style.pointerEvents = 'auto';
|
||||
}
|
||||
if (drawerFooter) drawerFooter.style.display = 'flex';
|
||||
}
|
||||
|
||||
/**
|
||||
* Update drawer fields based on selected cells
|
||||
*/
|
||||
private updateDrawerFields(): void {
|
||||
if (this.selectedCells.length === 0) {
|
||||
this.showEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
this.showEditState();
|
||||
|
||||
const employeeDisplay = document.getElementById('scheduleFieldEmployee');
|
||||
const avatar = document.getElementById('scheduleFieldAvatar');
|
||||
const employeeName = document.getElementById('scheduleFieldEmployeeName');
|
||||
const dateField = document.getElementById('scheduleFieldDate');
|
||||
const weekdayField = document.getElementById('scheduleFieldWeekday');
|
||||
|
||||
employeeDisplay?.classList.remove('empty', 'multi');
|
||||
|
||||
if (this.selectedCells.length === 1) {
|
||||
const cell = this.selectedCells[0];
|
||||
const name = cell.dataset.employee || '';
|
||||
const date = cell.dataset.date || '';
|
||||
const dayName = this.getDayName(cell.dataset.day || '');
|
||||
|
||||
if (employeeName) employeeName.textContent = name;
|
||||
if (avatar) avatar.textContent = this.getInitials(name);
|
||||
if (dateField) dateField.textContent = this.formatDate(date);
|
||||
if (weekdayField) weekdayField.textContent = dayName;
|
||||
|
||||
this.prefillFormFromCell(cell);
|
||||
} else {
|
||||
const employees = [...new Set(this.selectedCells.map(c => c.dataset.employee))];
|
||||
const days = [...new Set(this.selectedCells.map(c => c.dataset.day))];
|
||||
|
||||
if (employees.length === 1) {
|
||||
if (employeeName) employeeName.textContent = employees[0] || '';
|
||||
if (avatar) avatar.textContent = this.getInitials(employees[0] || '');
|
||||
if (dateField) dateField.textContent = `${this.selectedCells.length} dage valgt`;
|
||||
} else {
|
||||
if (employeeName) employeeName.textContent = `${this.selectedCells.length} valgt`;
|
||||
if (avatar) avatar.textContent = String(this.selectedCells.length);
|
||||
employeeDisplay?.classList.add('multi');
|
||||
if (dateField) dateField.textContent = `${employees.length} medarbejdere, ${days.length} dage`;
|
||||
}
|
||||
|
||||
if (days.length === 1) {
|
||||
if (weekdayField) weekdayField.textContent = this.getDayName(days[0] || '');
|
||||
} else {
|
||||
if (weekdayField) weekdayField.textContent = 'Flere dage';
|
||||
}
|
||||
|
||||
this.resetFormToDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format ISO date to display format
|
||||
*/
|
||||
private formatDate(isoDate: string): string {
|
||||
const date = new Date(isoDate);
|
||||
return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full day name from short name
|
||||
*/
|
||||
private getDayName(shortName: string): string {
|
||||
const dayMap: Record<string, string> = {
|
||||
'Man': 'Mandag', 'Tir': 'Tirsdag', 'Ons': 'Onsdag',
|
||||
'Tor': 'Torsdag', 'Fre': 'Fredag', 'Lør': 'Lørdag', 'Søn': 'Søndag'
|
||||
};
|
||||
return dayMap[shortName] || shortName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefill form from cell data
|
||||
*/
|
||||
private prefillFormFromCell(cell: HTMLElement): void {
|
||||
const timeDisplay = cell.querySelector('swp-time-display');
|
||||
if (!timeDisplay) return;
|
||||
|
||||
let status = 'work';
|
||||
if (timeDisplay.classList.contains('off')) status = 'off';
|
||||
else if (timeDisplay.classList.contains('vacation')) status = 'vacation';
|
||||
else if (timeDisplay.classList.contains('sick')) status = 'sick';
|
||||
|
||||
// Update status options
|
||||
document.querySelectorAll('#scheduleStatusOptions swp-status-option').forEach(opt => {
|
||||
opt.classList.toggle('selected', (opt as HTMLElement).dataset.status === status);
|
||||
});
|
||||
|
||||
// Show/hide time row
|
||||
const timeRow = document.getElementById('scheduleTimeRow');
|
||||
if (timeRow) timeRow.style.display = status === 'work' ? 'flex' : 'none';
|
||||
|
||||
// Parse time if work status
|
||||
if (status === 'work') {
|
||||
const timeText = timeDisplay.textContent?.trim() || '';
|
||||
const timeMatch = timeText.match(/(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/);
|
||||
if (timeMatch) {
|
||||
this.setTimeRange(timeMatch[1], timeMatch[2]);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset type to template
|
||||
document.querySelectorAll('#scheduleTypeOptions swp-toggle-option').forEach(opt => {
|
||||
opt.classList.toggle('selected', (opt as HTMLElement).dataset.value === 'template');
|
||||
});
|
||||
const repeatGroup = document.getElementById('scheduleRepeatGroup');
|
||||
if (repeatGroup) repeatGroup.style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset form to default values
|
||||
*/
|
||||
private resetFormToDefault(): void {
|
||||
document.querySelectorAll('#scheduleStatusOptions swp-status-option').forEach(opt => {
|
||||
opt.classList.toggle('selected', (opt as HTMLElement).dataset.status === 'work');
|
||||
});
|
||||
|
||||
const timeRow = document.getElementById('scheduleTimeRow');
|
||||
if (timeRow) timeRow.style.display = 'flex';
|
||||
|
||||
this.setTimeRange('09:00', '17:00');
|
||||
|
||||
document.querySelectorAll('#scheduleTypeOptions swp-toggle-option').forEach(opt => {
|
||||
opt.classList.toggle('selected', (opt as HTMLElement).dataset.value === 'template');
|
||||
});
|
||||
|
||||
const repeatGroup = document.getElementById('scheduleRepeatGroup');
|
||||
if (repeatGroup) repeatGroup.style.display = 'block';
|
||||
|
||||
const noteField = document.getElementById('scheduleFieldNote') as HTMLInputElement;
|
||||
if (noteField) noteField.value = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup status options click handlers
|
||||
*/
|
||||
private setupStatusOptions(): void {
|
||||
document.querySelectorAll('#scheduleStatusOptions swp-status-option').forEach(option => {
|
||||
option.addEventListener('click', () => {
|
||||
document.querySelectorAll('#scheduleStatusOptions swp-status-option').forEach(o =>
|
||||
o.classList.remove('selected')
|
||||
);
|
||||
option.classList.add('selected');
|
||||
|
||||
const status = (option as HTMLElement).dataset.status;
|
||||
const timeRow = document.getElementById('scheduleTimeRow');
|
||||
if (timeRow) timeRow.style.display = status === 'work' ? 'flex' : 'none';
|
||||
|
||||
this.updateSelectedCellsStatus();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup type toggle (single/repeat)
|
||||
*/
|
||||
private setupTypeToggle(): void {
|
||||
document.querySelectorAll('#scheduleTypeOptions swp-toggle-option').forEach(option => {
|
||||
option.addEventListener('click', () => {
|
||||
document.querySelectorAll('#scheduleTypeOptions swp-toggle-option').forEach(o =>
|
||||
o.classList.remove('selected')
|
||||
);
|
||||
option.classList.add('selected');
|
||||
|
||||
const isTemplate = (option as HTMLElement).dataset.value === 'template';
|
||||
const repeatGroup = document.getElementById('scheduleRepeatGroup');
|
||||
if (repeatGroup) repeatGroup.style.display = isTemplate ? 'block' : 'none';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert slider value to time string
|
||||
*/
|
||||
private valueToTime(value: number): string {
|
||||
const totalMinutes = (value * 15) + (6 * 60);
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert time string to slider value
|
||||
*/
|
||||
private timeToValue(timeStr: string): number {
|
||||
const [hours, minutes] = timeStr.split(':').map(Number);
|
||||
const totalMinutes = hours * 60 + minutes;
|
||||
return Math.round((totalMinutes - 6 * 60) / 15);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set time range slider values
|
||||
*/
|
||||
private setTimeRange(startTime: string, endTime: string): void {
|
||||
const timeRange = document.getElementById('scheduleTimeRange');
|
||||
if (!timeRange) return;
|
||||
|
||||
const startInput = timeRange.querySelector<HTMLInputElement>('.range-start');
|
||||
const endInput = timeRange.querySelector<HTMLInputElement>('.range-end');
|
||||
|
||||
if (startInput) startInput.value = String(this.timeToValue(startTime));
|
||||
if (endInput) endInput.value = String(this.timeToValue(endTime));
|
||||
|
||||
this.updateTimeRangeDisplay(timeRange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update time range display
|
||||
*/
|
||||
private updateTimeRangeDisplay(container: HTMLElement): void {
|
||||
const startInput = container.querySelector<HTMLInputElement>('.range-start');
|
||||
const endInput = container.querySelector<HTMLInputElement>('.range-end');
|
||||
const fill = container.querySelector<HTMLElement>('swp-time-range-fill');
|
||||
const timesEl = container.querySelector('swp-time-range-times');
|
||||
const durationEl = container.querySelector('swp-time-range-duration');
|
||||
|
||||
if (!startInput || !endInput) return;
|
||||
|
||||
let startVal = parseInt(startInput.value);
|
||||
let endVal = parseInt(endInput.value);
|
||||
|
||||
// Ensure start doesn't exceed end
|
||||
if (startVal > endVal) {
|
||||
if (startInput === document.activeElement) {
|
||||
startInput.value = String(endVal);
|
||||
startVal = endVal;
|
||||
} else {
|
||||
endInput.value = String(startVal);
|
||||
endVal = startVal;
|
||||
}
|
||||
}
|
||||
|
||||
// Update fill bar
|
||||
if (fill) {
|
||||
const startPercent = (startVal / this.TIME_RANGE_MAX) * 100;
|
||||
const endPercent = (endVal / this.TIME_RANGE_MAX) * 100;
|
||||
fill.style.left = startPercent + '%';
|
||||
fill.style.width = (endPercent - startPercent) + '%';
|
||||
}
|
||||
|
||||
// Calculate duration
|
||||
const durationMinutes = (endVal - startVal) * 15;
|
||||
const durationHours = durationMinutes / 60;
|
||||
const durationText = durationHours % 1 === 0
|
||||
? `${durationHours} timer`
|
||||
: `${durationHours.toFixed(1).replace('.', ',')} timer`;
|
||||
|
||||
if (timesEl) timesEl.textContent = `${this.valueToTime(startVal)} – ${this.valueToTime(endVal)}`;
|
||||
if (durationEl) durationEl.textContent = durationText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup time range slider
|
||||
*/
|
||||
private setupTimeRangeSlider(): void {
|
||||
const timeRange = document.getElementById('scheduleTimeRange');
|
||||
if (!timeRange) return;
|
||||
|
||||
const startInput = timeRange.querySelector<HTMLInputElement>('.range-start');
|
||||
const endInput = timeRange.querySelector<HTMLInputElement>('.range-end');
|
||||
const fill = timeRange.querySelector<HTMLElement>('swp-time-range-fill');
|
||||
const track = timeRange.querySelector<HTMLElement>('swp-time-range-track');
|
||||
|
||||
this.updateTimeRangeDisplay(timeRange);
|
||||
|
||||
startInput?.addEventListener('input', () => {
|
||||
this.updateTimeRangeDisplay(timeRange);
|
||||
this.updateSelectedCellsTime();
|
||||
});
|
||||
|
||||
endInput?.addEventListener('input', () => {
|
||||
this.updateTimeRangeDisplay(timeRange);
|
||||
this.updateSelectedCellsTime();
|
||||
});
|
||||
|
||||
// Drag fill bar to move entire range
|
||||
if (fill && track && startInput && endInput) {
|
||||
let isDragging = false;
|
||||
let dragStartX = 0;
|
||||
let dragStartValues = { start: 0, end: 0 };
|
||||
|
||||
fill.addEventListener('mousedown', (e: MouseEvent) => {
|
||||
isDragging = true;
|
||||
dragStartX = e.clientX;
|
||||
dragStartValues.start = parseInt(startInput.value);
|
||||
dragStartValues.end = parseInt(endInput.value);
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e: MouseEvent) => {
|
||||
if (!isDragging) return;
|
||||
|
||||
const sliderWidth = track.offsetWidth;
|
||||
const deltaX = e.clientX - dragStartX;
|
||||
const deltaValue = Math.round((deltaX / sliderWidth) * this.TIME_RANGE_MAX);
|
||||
|
||||
const duration = dragStartValues.end - dragStartValues.start;
|
||||
let newStart = dragStartValues.start + deltaValue;
|
||||
let newEnd = dragStartValues.end + deltaValue;
|
||||
|
||||
if (newStart < 0) {
|
||||
newStart = 0;
|
||||
newEnd = duration;
|
||||
}
|
||||
if (newEnd > this.TIME_RANGE_MAX) {
|
||||
newEnd = this.TIME_RANGE_MAX;
|
||||
newStart = this.TIME_RANGE_MAX - duration;
|
||||
}
|
||||
|
||||
startInput.value = String(newStart);
|
||||
endInput.value = String(newEnd);
|
||||
this.updateTimeRangeDisplay(timeRange);
|
||||
this.updateSelectedCellsTime();
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
isDragging = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update selected cells with current time
|
||||
*/
|
||||
private updateSelectedCellsTime(): void {
|
||||
const selectedStatus = document.querySelector('#scheduleStatusOptions swp-status-option.selected') as HTMLElement;
|
||||
const status = selectedStatus?.dataset.status || 'work';
|
||||
|
||||
if (status !== 'work') return;
|
||||
|
||||
const timeRange = document.getElementById('scheduleTimeRange');
|
||||
if (!timeRange) return;
|
||||
|
||||
const startInput = timeRange.querySelector<HTMLInputElement>('.range-start');
|
||||
const endInput = timeRange.querySelector<HTMLInputElement>('.range-end');
|
||||
if (!startInput || !endInput) return;
|
||||
|
||||
const startTime = this.valueToTime(parseInt(startInput.value));
|
||||
const endTime = this.valueToTime(parseInt(endInput.value));
|
||||
const formattedTime = `${startTime} - ${endTime}`;
|
||||
|
||||
this.selectedCells.forEach(cell => {
|
||||
const timeDisplay = cell.querySelector('swp-time-display');
|
||||
if (timeDisplay && !timeDisplay.classList.contains('off') &&
|
||||
!timeDisplay.classList.contains('vacation') &&
|
||||
!timeDisplay.classList.contains('sick')) {
|
||||
timeDisplay.textContent = formattedTime;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update selected cells with current status
|
||||
*/
|
||||
private updateSelectedCellsStatus(): void {
|
||||
const selectedStatus = document.querySelector('#scheduleStatusOptions swp-status-option.selected') as HTMLElement;
|
||||
const status = selectedStatus?.dataset.status || 'work';
|
||||
|
||||
const timeRange = document.getElementById('scheduleTimeRange');
|
||||
const startInput = timeRange?.querySelector<HTMLInputElement>('.range-start');
|
||||
const endInput = timeRange?.querySelector<HTMLInputElement>('.range-end');
|
||||
|
||||
const startTime = startInput ? this.valueToTime(parseInt(startInput.value)) : '09:00';
|
||||
const endTime = endInput ? this.valueToTime(parseInt(endInput.value)) : '17:00';
|
||||
|
||||
this.selectedCells.forEach(cell => {
|
||||
const timeDisplay = cell.querySelector('swp-time-display');
|
||||
if (!timeDisplay) return;
|
||||
|
||||
timeDisplay.classList.remove('off', 'off-override', 'vacation', 'sick');
|
||||
|
||||
switch (status) {
|
||||
case 'work':
|
||||
timeDisplay.textContent = `${startTime} - ${endTime}`;
|
||||
break;
|
||||
case 'off':
|
||||
timeDisplay.classList.add('off');
|
||||
timeDisplay.textContent = '—';
|
||||
break;
|
||||
case 'vacation':
|
||||
timeDisplay.classList.add('vacation');
|
||||
timeDisplay.textContent = 'Ferie';
|
||||
break;
|
||||
case 'sick':
|
||||
timeDisplay.classList.add('sick');
|
||||
timeDisplay.textContent = 'Syg';
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup drawer save button
|
||||
*/
|
||||
private setupDrawerSave(): void {
|
||||
const saveBtn = document.getElementById('scheduleDrawerSave');
|
||||
saveBtn?.addEventListener('click', () => {
|
||||
// Changes are already applied in real-time
|
||||
this.clearSelection();
|
||||
this.showEmptyState();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the schedule drawer
|
||||
*/
|
||||
private openDrawer(): void {
|
||||
this.drawer?.classList.add('open');
|
||||
document.getElementById('drawerOverlay')?.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the schedule drawer
|
||||
*/
|
||||
private closeDrawer(): void {
|
||||
this.drawer?.classList.remove('open');
|
||||
document.getElementById('drawerOverlay')?.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
PlanTempus.Application/wwwroot/ts/types/WorkSchedule.ts
Normal file
30
PlanTempus.Application/wwwroot/ts/types/WorkSchedule.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* Work Schedule Types
|
||||
*
|
||||
* Types for employee work schedule (arbejdstidsplan)
|
||||
*/
|
||||
|
||||
export type ShiftStatus = 'work' | 'off' | 'vacation' | 'sick';
|
||||
|
||||
export interface WorkScheduleShift {
|
||||
status: ShiftStatus;
|
||||
start?: string; // "09:00" - only when status=work
|
||||
end?: string; // "17:00" - only when status=work
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface EmployeeSchedule {
|
||||
employeeId: string;
|
||||
name: string;
|
||||
weeklyHours: number;
|
||||
schedule: Record<string, WorkScheduleShift>; // key = ISO date "2025-12-23"
|
||||
}
|
||||
|
||||
export interface WeekSchedule {
|
||||
weekNumber: number;
|
||||
year: number;
|
||||
startDate: string; // ISO date "2025-12-23"
|
||||
endDate: string; // ISO date "2025-12-29"
|
||||
closedDays: string[]; // ["2025-12-25"]
|
||||
employees: EmployeeSchedule[];
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue