Adds resource scheduling and unavailability tracking
Introduces comprehensive schedule management for resources: - Adds DateService with advanced time and date utilities - Implements ResourceScheduleService for managing work hours - Creates ScheduleRenderer to visualize unavailable time zones - Extends resource model to support default weekly schedules Enables granular tracking of resource availability and working hours
This commit is contained in:
parent
400de8c9d5
commit
a2b95515fd
17 changed files with 563 additions and 36 deletions
|
|
@ -1,6 +1,8 @@
|
|||
import { ICalendarEvent } from '../../types/CalendarTypes';
|
||||
import { EventService } from '../../storage/events/EventService';
|
||||
import { calculateEventPosition, getDateKey, formatTimeRange, GridConfig } from '../../utils/PositionUtils';
|
||||
import { DateService } from '../../core/DateService';
|
||||
import { IGridConfig } from '../../core/IGridConfig';
|
||||
import { calculateEventPosition } from '../../utils/PositionUtils';
|
||||
|
||||
/**
|
||||
* EventRenderer - Renders calendar events to the DOM
|
||||
|
|
@ -11,13 +13,11 @@ import { calculateEventPosition, getDateKey, formatTimeRange, GridConfig } from
|
|||
* - Event data retrieved via EventService when needed
|
||||
*/
|
||||
export class EventRenderer {
|
||||
private readonly gridConfig: GridConfig = {
|
||||
dayStartHour: 6,
|
||||
dayEndHour: 18,
|
||||
hourHeight: 64
|
||||
};
|
||||
|
||||
constructor(private eventService: EventService) {}
|
||||
constructor(
|
||||
private eventService: EventService,
|
||||
private dateService: DateService,
|
||||
private gridConfig: IGridConfig
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Render events for visible dates into day columns
|
||||
|
|
@ -53,7 +53,7 @@ export class EventRenderer {
|
|||
// Filter events for this column
|
||||
const columnEvents = events.filter(event => {
|
||||
// Must match date
|
||||
if (getDateKey(event.start) !== dateKey) return false;
|
||||
if (this.dateService.getDateKey(event.start) !== dateKey) return false;
|
||||
|
||||
// If column has resourceId, event must match
|
||||
if (columnResourceId && event.resourceId !== columnResourceId) return false;
|
||||
|
|
@ -110,7 +110,7 @@ export class EventRenderer {
|
|||
|
||||
// Visible content only
|
||||
element.innerHTML = `
|
||||
<swp-event-time>${formatTimeRange(event.start, event.end)}</swp-event-time>
|
||||
<swp-event-time>${this.dateService.formatTimeRange(event.start, event.end)}</swp-event-time>
|
||||
<swp-event-title>${this.escapeHtml(event.title)}</swp-event-title>
|
||||
${event.description ? `<swp-event-description>${this.escapeHtml(event.description)}</swp-event-description>` : ''}
|
||||
`;
|
||||
|
|
|
|||
106
src/v2/features/schedule/ScheduleRenderer.ts
Normal file
106
src/v2/features/schedule/ScheduleRenderer.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { ResourceScheduleService } from '../../storage/schedules/ResourceScheduleService';
|
||||
import { DateService } from '../../core/DateService';
|
||||
import { IGridConfig } from '../../core/IGridConfig';
|
||||
import { ITimeSlot } from '../../types/ScheduleTypes';
|
||||
|
||||
/**
|
||||
* ScheduleRenderer - Renders unavailable time zones in day columns
|
||||
*
|
||||
* Creates visual indicators for times outside the resource's working hours:
|
||||
* - Before work start (e.g., 06:00 - 09:00)
|
||||
* - After work end (e.g., 17:00 - 18:00)
|
||||
* - Full day if resource is off (schedule = null)
|
||||
*/
|
||||
export class ScheduleRenderer {
|
||||
constructor(
|
||||
private scheduleService: ResourceScheduleService,
|
||||
private dateService: DateService,
|
||||
private gridConfig: IGridConfig
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Render unavailable zones for visible columns
|
||||
* @param container - Calendar container element
|
||||
* @param filter - Filter with 'date' and 'resource' arrays
|
||||
*/
|
||||
async render(container: HTMLElement, filter: Record<string, string[]>): Promise<void> {
|
||||
const dates = filter['date'] || [];
|
||||
const resourceIds = filter['resource'] || [];
|
||||
|
||||
if (dates.length === 0) return;
|
||||
|
||||
// Find day columns
|
||||
const dayColumns = container.querySelector('swp-day-columns');
|
||||
if (!dayColumns) return;
|
||||
|
||||
const columns = dayColumns.querySelectorAll('swp-day-column');
|
||||
|
||||
for (const column of columns) {
|
||||
const dateKey = (column as HTMLElement).dataset.date;
|
||||
const resourceId = (column as HTMLElement).dataset.resourceId;
|
||||
|
||||
if (!dateKey || !resourceId) continue;
|
||||
|
||||
// Get or create unavailable layer
|
||||
let unavailableLayer = column.querySelector('swp-unavailable-layer');
|
||||
if (!unavailableLayer) {
|
||||
unavailableLayer = document.createElement('swp-unavailable-layer');
|
||||
column.insertBefore(unavailableLayer, column.firstChild);
|
||||
}
|
||||
|
||||
// Clear existing
|
||||
unavailableLayer.innerHTML = '';
|
||||
|
||||
// Get schedule for this resource/date
|
||||
const schedule = await this.scheduleService.getScheduleForDate(resourceId, dateKey);
|
||||
|
||||
// Render unavailable zones
|
||||
this.renderUnavailableZones(unavailableLayer as HTMLElement, schedule);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render unavailable time zones based on schedule
|
||||
*/
|
||||
private renderUnavailableZones(layer: HTMLElement, schedule: ITimeSlot | null): void {
|
||||
const dayStartMinutes = this.gridConfig.dayStartHour * 60;
|
||||
const dayEndMinutes = this.gridConfig.dayEndHour * 60;
|
||||
const minuteHeight = this.gridConfig.hourHeight / 60;
|
||||
|
||||
if (schedule === null) {
|
||||
// Full day unavailable
|
||||
const zone = this.createUnavailableZone(0, (dayEndMinutes - dayStartMinutes) * minuteHeight);
|
||||
layer.appendChild(zone);
|
||||
return;
|
||||
}
|
||||
|
||||
const workStartMinutes = this.dateService.timeToMinutes(schedule.start);
|
||||
const workEndMinutes = this.dateService.timeToMinutes(schedule.end);
|
||||
|
||||
// Before work start
|
||||
if (workStartMinutes > dayStartMinutes) {
|
||||
const top = 0;
|
||||
const height = (workStartMinutes - dayStartMinutes) * minuteHeight;
|
||||
const zone = this.createUnavailableZone(top, height);
|
||||
layer.appendChild(zone);
|
||||
}
|
||||
|
||||
// After work end
|
||||
if (workEndMinutes < dayEndMinutes) {
|
||||
const top = (workEndMinutes - dayStartMinutes) * minuteHeight;
|
||||
const height = (dayEndMinutes - workEndMinutes) * minuteHeight;
|
||||
const zone = this.createUnavailableZone(top, height);
|
||||
layer.appendChild(zone);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an unavailable zone element
|
||||
*/
|
||||
private createUnavailableZone(top: number, height: number): HTMLElement {
|
||||
const zone = document.createElement('swp-unavailable-zone');
|
||||
zone.style.top = `${top}px`;
|
||||
zone.style.height = `${height}px`;
|
||||
return zone;
|
||||
}
|
||||
}
|
||||
1
src/v2/features/schedule/index.ts
Normal file
1
src/v2/features/schedule/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { ScheduleRenderer } from './ScheduleRenderer';
|
||||
Loading…
Add table
Add a link
Reference in a new issue