Moving away from Azure Devops #1

Merged
Janus007 merged 113 commits from refac into master 2026-02-03 00:04:27 +01:00
7 changed files with 81 additions and 71 deletions
Showing only changes of commit c2f7564f8e - Show all commits

View file

@ -131,12 +131,15 @@ export class EventRenderer {
const column = this.findColumn(columnKey); const column = this.findColumn(columnKey);
if (!column) return; if (!column) return;
// Parse dateKey and resourceId from columnKey // Read date and resourceId directly from column attributes (columnKey is opaque)
const { date: dateKey, resource: resourceId } = this.dateService.parseColumnKey(columnKey); const date = column.dataset.date;
const resourceId = column.dataset.resourceId;
if (!date) return;
// Get date range for this day // Get date range for this day
const startDate = new Date(dateKey); const startDate = new Date(date);
const endDate = new Date(dateKey); const endDate = new Date(date);
endDate.setHours(23, 59, 59, 999); endDate.setHours(23, 59, 59, 999);
// Fetch events from IndexedDB // Fetch events from IndexedDB
@ -144,9 +147,9 @@ export class EventRenderer {
? await this.eventService.getByResourceAndDateRange(resourceId, startDate, endDate) ? await this.eventService.getByResourceAndDateRange(resourceId, startDate, endDate)
: await this.eventService.getByDateRange(startDate, endDate); : await this.eventService.getByDateRange(startDate, endDate);
// Filter to timed events and match dateKey exactly // Filter to timed events and match date exactly
const timedEvents = events.filter(event => const timedEvents = events.filter(event =>
!event.allDay && this.dateService.getDateKey(event.start) === dateKey !event.allDay && this.dateService.getDateKey(event.start) === date
); );
// Get or create events layer // Get or create events layer
@ -260,15 +263,15 @@ export class EventRenderer {
// Render events into each column based on data attributes // Render events into each column based on data attributes
columns.forEach(column => { columns.forEach(column => {
const dateKey = (column as HTMLElement).dataset.date; const date = (column as HTMLElement).dataset.date;
const columnResourceId = (column as HTMLElement).dataset.resourceId; const columnResourceId = (column as HTMLElement).dataset.resourceId;
if (!dateKey) return; if (!date) return;
// Filter events for this column // Filter events for this column
const columnEvents = events.filter(event => { const columnEvents = events.filter(event => {
// Must match date // Must match date
if (this.dateService.getDateKey(event.start) !== dateKey) return false; if (this.dateService.getDateKey(event.start) !== date) return false;
// If column has resourceId, event must match // If column has resourceId, event must match
if (columnResourceId && event.resourceId !== columnResourceId) return false; if (columnResourceId && event.resourceId !== columnResourceId) return false;

View file

@ -16,6 +16,7 @@ import {
*/ */
interface IHeaderItemLayout { interface IHeaderItemLayout {
event: ICalendarEvent; event: ICalendarEvent;
columnKey: string; // Opaque column identifier
row: number; // 1-indexed row: number; // 1-indexed
colStart: number; // 1-indexed colStart: number; // 1-indexed
colEnd: number; // exclusive colEnd: number; // exclusive
@ -56,6 +57,10 @@ export class HeaderDrawerRenderer {
const visibleDates = filter['date'] || []; const visibleDates = filter['date'] || [];
if (visibleDates.length === 0) return; if (visibleDates.length === 0) return;
// Get column keys from DOM for correct multi-resource positioning
const visibleColumnKeys = this.getVisibleColumnKeysFromDOM();
if (visibleColumnKeys.length === 0) return;
// Fetch events for date range // Fetch events for date range
const startDate = new Date(visibleDates[0]); const startDate = new Date(visibleDates[0]);
const endDate = new Date(visibleDates[visibleDates.length - 1]); const endDate = new Date(visibleDates[visibleDates.length - 1]);
@ -71,8 +76,8 @@ export class HeaderDrawerRenderer {
if (allDayEvents.length === 0) return; if (allDayEvents.length === 0) return;
// Calculate layout with row stacking // Calculate layout with row stacking using columnKeys
const layouts = this.calculateLayout(allDayEvents, visibleDates); const layouts = this.calculateLayout(allDayEvents, visibleColumnKeys);
const rowCount = Math.max(1, ...layouts.map(l => l.row)); const rowCount = Math.max(1, ...layouts.map(l => l.row));
// Render each item with layout // Render each item with layout
@ -89,13 +94,14 @@ export class HeaderDrawerRenderer {
* Create a header item element from layout * Create a header item element from layout
*/ */
private createHeaderItem(layout: IHeaderItemLayout): HTMLElement { private createHeaderItem(layout: IHeaderItemLayout): HTMLElement {
const { event, row, colStart, colEnd } = layout; const { event, columnKey, row, colStart, colEnd } = layout;
const item = document.createElement('swp-header-item'); const item = document.createElement('swp-header-item');
item.dataset.eventId = event.id; item.dataset.eventId = event.id;
item.dataset.itemType = 'event'; item.dataset.itemType = 'event';
item.dataset.start = event.start.toISOString(); item.dataset.start = event.start.toISOString();
item.dataset.end = event.end.toISOString(); item.dataset.end = event.end.toISOString();
item.dataset.columnKey = columnKey;
item.textContent = event.title; item.textContent = event.title;
// Color class // Color class
@ -112,19 +118,22 @@ export class HeaderDrawerRenderer {
* Calculate layout for all events with row stacking * Calculate layout for all events with row stacking
* Uses track-based algorithm to find available rows for overlapping events * Uses track-based algorithm to find available rows for overlapping events
*/ */
private calculateLayout(events: ICalendarEvent[], visibleDates: string[]): IHeaderItemLayout[] { private calculateLayout(events: ICalendarEvent[], visibleColumnKeys: string[]): IHeaderItemLayout[] {
// tracks[row][col] = occupied // tracks[row][col] = occupied
const tracks: boolean[][] = [new Array(visibleDates.length).fill(false)]; const tracks: boolean[][] = [new Array(visibleColumnKeys.length).fill(false)];
const layouts: IHeaderItemLayout[] = []; const layouts: IHeaderItemLayout[] = [];
for (const event of events) { for (const event of events) {
const startCol = this.getColIndex(event.start, visibleDates); // Build columnKey from event fields (only place we need to construct it)
const endCol = this.getColIndex(event.end, visibleDates); const columnKey = this.buildColumnKeyFromEvent(event);
const startCol = visibleColumnKeys.indexOf(columnKey);
const endColumnKey = this.buildColumnKeyFromEvent(event, event.end);
const endCol = visibleColumnKeys.indexOf(endColumnKey);
if (startCol === -1 && endCol === -1) continue; if (startCol === -1 && endCol === -1) continue;
// Clamp til synlige kolonner // Clamp til synlige kolonner
const colStart = Math.max(0, startCol); const colStart = Math.max(0, startCol);
const colEnd = (endCol !== -1 ? endCol : visibleDates.length - 1) + 1; const colEnd = (endCol !== -1 ? endCol : visibleColumnKeys.length - 1) + 1;
// Find ledig række // Find ledig række
const row = this.findAvailableRow(tracks, colStart, colEnd); const row = this.findAvailableRow(tracks, colStart, colEnd);
@ -134,12 +143,23 @@ export class HeaderDrawerRenderer {
tracks[row][c] = true; tracks[row][c] = true;
} }
layouts.push({ event, row: row + 1, colStart: colStart + 1, colEnd: colEnd + 1 }); layouts.push({ event, columnKey, row: row + 1, colStart: colStart + 1, colEnd: colEnd + 1 });
} }
return layouts; return layouts;
} }
/**
* Build columnKey from event fields
* This is the only place we construct columnKey from event data
*/
private buildColumnKeyFromEvent(event: ICalendarEvent, date?: Date): string {
const dateStr = this.dateService.getDateKey(date || event.start);
const segments: Record<string, string> = { date: dateStr };
if (event.resourceId) segments.resource = event.resourceId;
return this.dateService.buildColumnKey(segments);
}
/** /**
* Find available row for event spanning columns [colStart, colEnd) * Find available row for event spanning columns [colStart, colEnd)
*/ */
@ -156,14 +176,6 @@ export class HeaderDrawerRenderer {
return tracks.length - 1; return tracks.length - 1;
} }
/**
* Get column index for a date (0-indexed, -1 if not found)
*/
private getColIndex(date: Date, visibleDates: string[]): number {
const dateKey = this.dateService.getDateKey(date);
return visibleDates.indexOf(dateKey);
}
/** /**
* Get color class based on event metadata or type * Get color class based on event metadata or type
*/ */
@ -233,15 +245,9 @@ export class HeaderDrawerRenderer {
item.dataset.eventId = payload.eventId; item.dataset.eventId = payload.eventId;
item.dataset.itemType = payload.itemType; item.dataset.itemType = payload.itemType;
item.dataset.duration = String(payload.duration); item.dataset.duration = String(payload.duration);
item.dataset.columnKey = payload.sourceColumnKey;
item.textContent = payload.title; item.textContent = payload.title;
// Set start/end as ISO dates (for recalculateDrawerLayout)
const startDate = new Date(payload.sourceDate);
const endDate = new Date(payload.sourceDate);
endDate.setDate(endDate.getDate() + payload.duration - 1);
item.dataset.start = startDate.toISOString();
item.dataset.end = endDate.toISOString();
// Apply color class if present // Apply color class if present
if (payload.colorClass) { if (payload.colorClass) {
item.classList.add(payload.colorClass); item.classList.add(payload.colorClass);
@ -276,12 +282,8 @@ export class HeaderDrawerRenderer {
this.currentItem.style.gridArea = `1 / ${col} / 2 / ${endCol}`; this.currentItem.style.gridArea = `1 / ${col} / 2 / ${endCol}`;
// Update start/end dates based on new position // Update columnKey to new position
const startDate = new Date(payload.dateKey); this.currentItem.dataset.columnKey = payload.columnKey;
const endDate = new Date(payload.dateKey);
endDate.setDate(endDate.getDate() + duration - 1);
this.currentItem.dataset.start = startDate.toISOString();
this.currentItem.dataset.end = endDate.toISOString();
} }
/** /**
@ -327,26 +329,27 @@ export class HeaderDrawerRenderer {
const items = Array.from(drawer.querySelectorAll('swp-header-item')) as HTMLElement[]; const items = Array.from(drawer.querySelectorAll('swp-header-item')) as HTMLElement[];
if (items.length === 0) return; if (items.length === 0) return;
// Get visible dates from existing items // Get visible column keys for correct multi-resource positioning
const visibleDates = this.getVisibleDatesFromDOM(); const visibleColumnKeys = this.getVisibleColumnKeysFromDOM();
if (visibleDates.length === 0) return; if (visibleColumnKeys.length === 0) return;
// Build layout data from DOM items // Build layout data from DOM items - use columnKey directly (opaque matching)
const itemData = items.map(item => ({ const itemData = items.map(item => ({
element: item, element: item,
start: new Date(item.dataset.start || ''), columnKey: item.dataset.columnKey || '',
end: new Date(item.dataset.end || '') duration: parseInt(item.dataset.duration || '1', 10)
})); }));
// Calculate new layout using track algorithm // Calculate new layout using track algorithm
const tracks: boolean[][] = [new Array(visibleDates.length).fill(false)]; const tracks: boolean[][] = [new Array(visibleColumnKeys.length).fill(false)];
for (const item of itemData) { for (const item of itemData) {
const startCol = this.getColIndex(item.start, visibleDates); // Direct columnKey matching - no parsing or construction needed
const endCol = this.getColIndex(item.end, visibleDates); const startCol = visibleColumnKeys.indexOf(item.columnKey);
if (startCol === -1) continue;
const colStart = Math.max(0, startCol); const colStart = startCol;
const colEnd = (endCol !== -1 ? endCol : visibleDates.length - 1) + 1; const colEnd = Math.min(startCol + item.duration, visibleColumnKeys.length);
const row = this.findAvailableRow(tracks, colStart, colEnd); const row = this.findAvailableRow(tracks, colStart, colEnd);
@ -364,16 +367,16 @@ export class HeaderDrawerRenderer {
} }
/** /**
* Get visible dates from header columns in DOM * Get visible column keys from DOM (preserves order for multi-resource views)
*/ */
private getVisibleDatesFromDOM(): string[] { private getVisibleColumnKeysFromDOM(): string[] {
const columns = document.querySelectorAll('swp-day-column'); const columns = document.querySelectorAll('swp-day-column');
const dates: string[] = []; const columnKeys: string[] = [];
columns.forEach(col => { columns.forEach(col => {
const date = (col as HTMLElement).dataset.date; const columnKey = (col as HTMLElement).dataset.columnKey;
if (date && !dates.includes(date)) dates.push(date); if (columnKey) columnKeys.push(columnKey);
}); });
return dates; return columnKeys;
} }
/** /**

View file

@ -36,10 +36,10 @@ export class ScheduleRenderer {
const columns = dayColumns.querySelectorAll('swp-day-column'); const columns = dayColumns.querySelectorAll('swp-day-column');
for (const column of columns) { for (const column of columns) {
const dateKey = (column as HTMLElement).dataset.date; const date = (column as HTMLElement).dataset.date;
const resourceId = (column as HTMLElement).dataset.resourceId; const resourceId = (column as HTMLElement).dataset.resourceId;
if (!dateKey || !resourceId) continue; if (!date || !resourceId) continue;
// Get or create unavailable layer // Get or create unavailable layer
let unavailableLayer = column.querySelector('swp-unavailable-layer'); let unavailableLayer = column.querySelector('swp-unavailable-layer');
@ -52,7 +52,7 @@ export class ScheduleRenderer {
unavailableLayer.innerHTML = ''; unavailableLayer.innerHTML = '';
// Get schedule for this resource/date // Get schedule for this resource/date
const schedule = await this.scheduleService.getScheduleForDate(resourceId, dateKey); const schedule = await this.scheduleService.getScheduleForDate(resourceId, date);
// Render unavailable zones // Render unavailable zones
this.renderUnavailableZones(unavailableLayer as HTMLElement, schedule); this.renderUnavailableZones(unavailableLayer as HTMLElement, schedule);

View file

@ -176,7 +176,8 @@ export class DragDropManager {
if (gridEvent) { if (gridEvent) {
const columnKey = this.dragState.currentColumn.dataset.columnKey || ''; const columnKey = this.dragState.currentColumn.dataset.columnKey || '';
const swpEvent = SwpEvent.fromElement(gridEvent, columnKey, this.gridConfig); const date = this.dragState.currentColumn.dataset.date || '';
const swpEvent = SwpEvent.fromElement(gridEvent, columnKey, date, this.gridConfig);
const payload: IDragEndPayload = { const payload: IDragEndPayload = {
swpEvent, swpEvent,
@ -203,13 +204,15 @@ export class DragDropManager {
// Remove ghost // Remove ghost
this.dragState.ghostElement?.remove(); this.dragState.ghostElement?.remove();
// Get columnKey from target column // Get columnKey and date from target column
const columnKey = this.dragState.columnElement.dataset.columnKey || ''; const columnKey = this.dragState.columnElement.dataset.columnKey || '';
const date = this.dragState.columnElement.dataset.date || '';
// Create SwpEvent from element (reads top/height/eventId from element) // Create SwpEvent from element (reads top/height/eventId from element)
const swpEvent = SwpEvent.fromElement( const swpEvent = SwpEvent.fromElement(
this.dragState.element, this.dragState.element,
columnKey, columnKey,
date,
this.gridConfig this.gridConfig
); );
@ -406,7 +409,7 @@ export class DragDropManager {
eventId: this.dragState.eventId, eventId: this.dragState.eventId,
element: this.dragState.element, element: this.dragState.element,
sourceColumnIndex: this.getColumnIndex(this.dragState.columnElement), sourceColumnIndex: this.getColumnIndex(this.dragState.columnElement),
sourceDate: this.dragState.columnElement.dataset.date || '', sourceColumnKey: this.dragState.columnElement.dataset.columnKey || '',
title: this.dragState.element.querySelector('swp-event-title')?.textContent || '', title: this.dragState.element.querySelector('swp-event-title')?.textContent || '',
colorClass: [...this.dragState.element.classList].find(c => c.startsWith('is-')), colorClass: [...this.dragState.element.classList].find(c => c.startsWith('is-')),
itemType: 'event', itemType: 'event',
@ -468,7 +471,7 @@ export class DragDropManager {
const payload: IDragMoveHeaderPayload = { const payload: IDragMoveHeaderPayload = {
eventId: this.dragState.eventId, eventId: this.dragState.eventId,
columnIndex: this.getColumnIndex(column), columnIndex: this.getColumnIndex(column),
dateKey: column.dataset.date || '' columnKey: column.dataset.columnKey || ''
}; };
this.eventBus.emit(CoreEvents.EVENT_DRAG_MOVE_HEADER, payload); this.eventBus.emit(CoreEvents.EVENT_DRAG_MOVE_HEADER, payload);

View file

@ -251,14 +251,16 @@ export class ResizeManager {
// Remove global resizing class // Remove global resizing class
document.documentElement.classList.remove('swp--resizing'); document.documentElement.classList.remove('swp--resizing');
// Get columnKey from parent column // Get columnKey and date from parent column
const column = this.resizeState.element.closest('swp-day-column') as HTMLElement; const column = this.resizeState.element.closest('swp-day-column') as HTMLElement;
const columnKey = column?.dataset.columnKey || ''; const columnKey = column?.dataset.columnKey || '';
const date = column?.dataset.date || '';
// Create SwpEvent from element (reads top/height/eventId from element) // Create SwpEvent from element (reads top/height/eventId from element)
const swpEvent = SwpEvent.fromElement( const swpEvent = SwpEvent.fromElement(
this.resizeState.element, this.resizeState.element,
columnKey, columnKey,
date,
this.gridConfig this.gridConfig
); );

View file

@ -50,7 +50,7 @@ export interface IDragEnterHeaderPayload {
eventId: string; eventId: string;
element: HTMLElement; // Original dragged element element: HTMLElement; // Original dragged element
sourceColumnIndex: number; sourceColumnIndex: number;
sourceDate: string; sourceColumnKey: string; // Opaque column identifier (for matching only)
title: string; title: string;
colorClass?: string; colorClass?: string;
itemType: 'event' | 'reminder'; itemType: 'event' | 'reminder';
@ -60,7 +60,7 @@ export interface IDragEnterHeaderPayload {
export interface IDragMoveHeaderPayload { export interface IDragMoveHeaderPayload {
eventId: string; eventId: string;
columnIndex: number; columnIndex: number;
dateKey: string; columnKey: string; // Opaque column identifier (for matching only)
} }
export interface IDragLeaveHeaderPayload { export interface IDragLeaveHeaderPayload {

View file

@ -51,24 +51,23 @@ export class SwpEvent {
/** /**
* Factory: Create SwpEvent from element + columnKey * Factory: Create SwpEvent from element + columnKey
* Reads top/height from element.style to calculate start/end * Reads top/height from element.style to calculate start/end
* @param columnKey - Uniform column identifier (e.g. "2025-12-09" or "2025-12-09:EMP001") * @param columnKey - Opaque column identifier (do NOT parse - use only for matching)
* @param date - Date string (YYYY-MM-DD) for time calculations
*/ */
static fromElement( static fromElement(
element: HTMLElement, element: HTMLElement,
columnKey: string, columnKey: string,
date: string,
gridConfig: IGridConfig gridConfig: IGridConfig
): SwpEvent { ): SwpEvent {
const topPixels = parseFloat(element.style.top) || 0; const topPixels = parseFloat(element.style.top) || 0;
const heightPixels = parseFloat(element.style.height) || 0; const heightPixels = parseFloat(element.style.height) || 0;
// Extract dateKey from columnKey (first segment)
const dateKey = columnKey.split(':')[0];
// Calculate start from top position // Calculate start from top position
const startMinutesFromGrid = (topPixels / gridConfig.hourHeight) * 60; const startMinutesFromGrid = (topPixels / gridConfig.hourHeight) * 60;
const totalMinutes = (gridConfig.dayStartHour * 60) + startMinutesFromGrid; const totalMinutes = (gridConfig.dayStartHour * 60) + startMinutesFromGrid;
const start = new Date(dateKey); const start = new Date(date);
start.setHours(Math.floor(totalMinutes / 60), totalMinutes % 60, 0, 0); start.setHours(Math.floor(totalMinutes / 60), totalMinutes % 60, 0, 0);
// Calculate end from height // Calculate end from height