Moving away from Azure Devops #1

Merged
Janus007 merged 113 commits from refac into master 2026-02-03 00:04:27 +01:00
11 changed files with 199 additions and 5 deletions
Showing only changes of commit 570c91527a - Show all commits

View file

@ -7,6 +7,7 @@ import { ITimeFormatConfig } from './core/ITimeFormatConfig';
import { IGridConfig } from './core/IGridConfig'; import { IGridConfig } from './core/IGridConfig';
import { ResourceRenderer } from './features/resource/ResourceRenderer'; import { ResourceRenderer } from './features/resource/ResourceRenderer';
import { TeamRenderer } from './features/team/TeamRenderer'; import { TeamRenderer } from './features/team/TeamRenderer';
import { DepartmentRenderer } from './features/department/DepartmentRenderer';
import { CalendarOrchestrator } from './core/CalendarOrchestrator'; import { CalendarOrchestrator } from './core/CalendarOrchestrator';
import { TimeAxisRenderer } from './features/timeaxis/TimeAxisRenderer'; import { TimeAxisRenderer } from './features/timeaxis/TimeAxisRenderer';
import { ScrollManager } from './core/ScrollManager'; import { ScrollManager } from './core/ScrollManager';
@ -16,7 +17,7 @@ import { DemoApp } from './demo/DemoApp';
// Event system // Event system
import { EventBus } from './core/EventBus'; import { EventBus } from './core/EventBus';
import { IEventBus, ICalendarEvent, ISync, IResource, IBooking, ICustomer, ITeam } from './types/CalendarTypes'; import { IEventBus, ICalendarEvent, ISync, IResource, IBooking, ICustomer, ITeam, IDepartment } from './types/CalendarTypes';
// Storage // Storage
import { IndexedDBContext } from './storage/IndexedDBContext'; import { IndexedDBContext } from './storage/IndexedDBContext';
@ -32,6 +33,8 @@ import { CustomerStore } from './storage/customers/CustomerStore';
import { CustomerService } from './storage/customers/CustomerService'; import { CustomerService } from './storage/customers/CustomerService';
import { TeamStore } from './storage/teams/TeamStore'; import { TeamStore } from './storage/teams/TeamStore';
import { TeamService } from './storage/teams/TeamService'; import { TeamService } from './storage/teams/TeamService';
import { DepartmentStore } from './storage/departments/DepartmentStore';
import { DepartmentService } from './storage/departments/DepartmentService';
// Audit // Audit
import { AuditStore } from './storage/audit/AuditStore'; import { AuditStore } from './storage/audit/AuditStore';
@ -46,6 +49,7 @@ import { MockBookingRepository } from './repositories/MockBookingRepository';
import { MockCustomerRepository } from './repositories/MockCustomerRepository'; import { MockCustomerRepository } from './repositories/MockCustomerRepository';
import { MockAuditRepository } from './repositories/MockAuditRepository'; import { MockAuditRepository } from './repositories/MockAuditRepository';
import { MockTeamRepository } from './repositories/MockTeamRepository'; import { MockTeamRepository } from './repositories/MockTeamRepository';
import { MockDepartmentRepository } from './repositories/MockDepartmentRepository';
// Workers // Workers
import { DataSeeder } from './workers/DataSeeder'; import { DataSeeder } from './workers/DataSeeder';
@ -106,6 +110,7 @@ export function createV2Container(): Container {
builder.registerType(BookingStore).as<IStore>(); builder.registerType(BookingStore).as<IStore>();
builder.registerType(CustomerStore).as<IStore>(); builder.registerType(CustomerStore).as<IStore>();
builder.registerType(TeamStore).as<IStore>(); builder.registerType(TeamStore).as<IStore>();
builder.registerType(DepartmentStore).as<IStore>();
builder.registerType(ScheduleOverrideStore).as<IStore>(); builder.registerType(ScheduleOverrideStore).as<IStore>();
builder.registerType(AuditStore).as<IStore>(); builder.registerType(AuditStore).as<IStore>();
@ -130,6 +135,10 @@ export function createV2Container(): Container {
builder.registerType(TeamService).as<IEntityService<ISync>>(); builder.registerType(TeamService).as<IEntityService<ISync>>();
builder.registerType(TeamService).as<TeamService>(); builder.registerType(TeamService).as<TeamService>();
builder.registerType(DepartmentService).as<IEntityService<IDepartment>>();
builder.registerType(DepartmentService).as<IEntityService<ISync>>();
builder.registerType(DepartmentService).as<DepartmentService>();
// Repositories (for DataSeeder polymorphic array) // Repositories (for DataSeeder polymorphic array)
builder.registerType(MockEventRepository).as<IApiRepository<ICalendarEvent>>(); builder.registerType(MockEventRepository).as<IApiRepository<ICalendarEvent>>();
builder.registerType(MockEventRepository).as<IApiRepository<ISync>>(); builder.registerType(MockEventRepository).as<IApiRepository<ISync>>();
@ -149,6 +158,9 @@ export function createV2Container(): Container {
builder.registerType(MockTeamRepository).as<IApiRepository<ITeam>>(); builder.registerType(MockTeamRepository).as<IApiRepository<ITeam>>();
builder.registerType(MockTeamRepository).as<IApiRepository<ISync>>(); builder.registerType(MockTeamRepository).as<IApiRepository<ISync>>();
builder.registerType(MockDepartmentRepository).as<IApiRepository<IDepartment>>();
builder.registerType(MockDepartmentRepository).as<IApiRepository<ISync>>();
// Audit service (listens to ENTITY_SAVED/DELETED events automatically) // Audit service (listens to ENTITY_SAVED/DELETED events automatically)
builder.registerType(AuditService).as<AuditService>(); builder.registerType(AuditService).as<AuditService>();
@ -168,6 +180,7 @@ export function createV2Container(): Container {
builder.registerType(DateRenderer).as<IRenderer>(); builder.registerType(DateRenderer).as<IRenderer>();
builder.registerType(ResourceRenderer).as<IRenderer>(); builder.registerType(ResourceRenderer).as<IRenderer>();
builder.registerType(TeamRenderer).as<IRenderer>(); builder.registerType(TeamRenderer).as<IRenderer>();
builder.registerType(DepartmentRenderer).as<IRenderer>();
// Stores - registreres som IGroupingStore // Stores - registreres som IGroupingStore
builder.registerType(MockTeamStore).as<IGroupingStore>(); builder.registerType(MockTeamStore).as<IGroupingStore>();

View file

@ -18,7 +18,7 @@ export class DemoApp {
private animator!: NavigationAnimator; private animator!: NavigationAnimator;
private container!: HTMLElement; private container!: HTMLElement;
private weekOffset = 0; private weekOffset = 0;
private currentView: 'day' | 'simple' | 'resource' | 'team' = 'simple'; private currentView: 'day' | 'simple' | 'resource' | 'team' | 'department' = 'simple';
constructor( constructor(
private orchestrator: CalendarOrchestrator, private orchestrator: CalendarOrchestrator,
@ -129,6 +129,16 @@ export class DemoApp {
{ type: 'date', values: dates, idProperty: 'date', derivedFrom: 'start' } { type: 'date', values: dates, idProperty: 'date', derivedFrom: 'start' }
] ]
}; };
case 'department':
return {
templateId: 'department',
groupings: [
{ type: 'department', values: ['dept-styling', 'dept-training'] },
{ type: 'resource', values: ['EMP001', 'EMP002', 'EMP003', 'EMP004', 'STUDENT001', 'STUDENT002'], idProperty: 'resourceId', belongsTo: 'department.resourceIds' },
{ type: 'date', values: dates, idProperty: 'date', derivedFrom: 'start' }
]
};
} }
} }
@ -164,6 +174,11 @@ export class DemoApp {
this.currentView = 'team'; this.currentView = 'team';
this.render(); this.render();
}); });
document.getElementById('btn-department')?.addEventListener('click', () => {
this.currentView = 'department';
this.render();
});
} }
private setupDrawerToggle(): void { private setupDrawerToggle(): void {

View file

@ -0,0 +1,37 @@
import { IRenderer, IRenderContext } from '../../core/IGroupingRenderer';
import { DepartmentService } from '../../storage/departments/DepartmentService';
export class DepartmentRenderer implements IRenderer {
readonly type = 'department';
constructor(private departmentService: DepartmentService) {}
async render(context: IRenderContext): Promise<void> {
const allowedIds = context.filter[this.type] || [];
if (allowedIds.length === 0) return;
// Fetch departments from IndexedDB (only for name display)
const departments = await this.departmentService.getByIds(allowedIds);
const dateCount = context.filter['date']?.length || 1;
// Get child filter values using childType from context (not hardcoded)
const childIds = context.childType ? context.filter[context.childType] || [] : [];
// Render department headers
for (const dept of departments) {
// Get children from parentChildMap (resolved from belongsTo config)
const deptChildIds = context.parentChildMap?.[dept.id] || [];
// Count children that belong to this department AND are in the filter
const childCount = deptChildIds.filter(id => childIds.includes(id)).length;
const colspan = childCount * dateCount;
const header = document.createElement('swp-department-header');
header.dataset.departmentId = dept.id;
header.textContent = dept.name;
header.style.setProperty('--department-cols', String(colspan));
context.headerContainer.appendChild(header);
}
}
}

View file

@ -0,0 +1,55 @@
import { IDepartment, EntityType } from '../types/CalendarTypes';
import { IApiRepository } from './IApiRepository';
interface RawDepartmentData {
id: string;
name: string;
resourceIds: string[];
syncStatus?: string;
[key: string]: unknown;
}
/**
* MockDepartmentRepository - Loads department data from local JSON file
*/
export class MockDepartmentRepository implements IApiRepository<IDepartment> {
public readonly entityType: EntityType = 'Department';
private readonly dataUrl = 'data/mock-departments.json';
public async fetchAll(): Promise<IDepartment[]> {
try {
const response = await fetch(this.dataUrl);
if (!response.ok) {
throw new Error(`Failed to load mock departments: ${response.status} ${response.statusText}`);
}
const rawData: RawDepartmentData[] = await response.json();
return this.processDepartmentData(rawData);
} catch (error) {
console.error('Failed to load department data:', error);
throw error;
}
}
public async sendCreate(_department: IDepartment): Promise<IDepartment> {
throw new Error('MockDepartmentRepository does not support sendCreate. Mock data is read-only.');
}
public async sendUpdate(_id: string, _updates: Partial<IDepartment>): Promise<IDepartment> {
throw new Error('MockDepartmentRepository does not support sendUpdate. Mock data is read-only.');
}
public async sendDelete(_id: string): Promise<void> {
throw new Error('MockDepartmentRepository does not support sendDelete. Mock data is read-only.');
}
private processDepartmentData(data: RawDepartmentData[]): IDepartment[] {
return data.map((dept): IDepartment => ({
id: dept.id,
name: dept.name,
resourceIds: dept.resourceIds,
syncStatus: 'synced' as const
}));
}
}

View file

@ -0,0 +1,25 @@
import { IDepartment, EntityType, IEventBus } from '../../types/CalendarTypes';
import { DepartmentStore } from './DepartmentStore';
import { BaseEntityService } from '../BaseEntityService';
import { IndexedDBContext } from '../IndexedDBContext';
/**
* DepartmentService - CRUD operations for departments in IndexedDB
*/
export class DepartmentService extends BaseEntityService<IDepartment> {
readonly storeName = DepartmentStore.STORE_NAME;
readonly entityType: EntityType = 'Department';
constructor(context: IndexedDBContext, eventBus: IEventBus) {
super(context, eventBus);
}
/**
* Get departments by IDs
*/
async getByIds(ids: string[]): Promise<IDepartment[]> {
if (ids.length === 0) return [];
const results = await Promise.all(ids.map(id => this.get(id)));
return results.filter((d): d is IDepartment => d !== null);
}
}

View file

@ -0,0 +1,13 @@
import { IStore } from '../IStore';
/**
* DepartmentStore - IndexedDB ObjectStore definition for departments
*/
export class DepartmentStore implements IStore {
static readonly STORE_NAME = 'departments';
readonly storeName = DepartmentStore.STORE_NAME;
create(db: IDBDatabase): void {
db.createObjectStore(DepartmentStore.STORE_NAME, { keyPath: 'id' });
}
}

View file

@ -6,7 +6,7 @@ import { IWeekSchedule } from './ScheduleTypes';
export type SyncStatus = 'synced' | 'pending' | 'error'; export type SyncStatus = 'synced' | 'pending' | 'error';
export type EntityType = 'Event' | 'Booking' | 'Customer' | 'Resource' | 'Team' | 'Audit'; export type EntityType = 'Event' | 'Booking' | 'Customer' | 'Resource' | 'Team' | 'Department' | 'Audit';
/** /**
* CalendarEventType - Used by ICalendarEvent.type * CalendarEventType - Used by ICalendarEvent.type
@ -125,6 +125,13 @@ export interface ITeam extends ISync {
resourceIds: string[]; resourceIds: string[];
} }
// Department types
export interface IDepartment extends ISync {
id: string;
name: string;
resourceIds: string[];
}
// Booking types // Booking types
export type BookingStatus = export type BookingStatus =
| 'created' | 'created'

File diff suppressed because one or more lines are too long

View file

@ -175,11 +175,18 @@ swp-calendar-header {
> swp-resource-header { grid-row: 2; } > swp-resource-header { grid-row: 2; }
> swp-day-header { grid-row: 3; } > swp-day-header { grid-row: 3; }
} }
&[data-levels="department resource date"] {
> swp-department-header { grid-row: 1; }
> swp-resource-header { grid-row: 2; }
> swp-day-header { grid-row: 3; }
}
} }
swp-day-header, swp-day-header,
swp-resource-header, swp-resource-header,
swp-team-header { swp-team-header,
swp-department-header {
padding: 8px; padding: 8px;
text-align: center; text-align: center;
border-right: 1px solid var(--color-border); border-right: 1px solid var(--color-border);
@ -194,6 +201,13 @@ swp-team-header {
grid-column: span var(--team-cols, 1); grid-column: span var(--team-cols, 1);
} }
swp-department-header {
background: var(--color-team-bg);
color: var(--color-team-text);
font-weight: 500;
grid-column: span var(--department-cols, 1);
}
swp-resource-header { swp-resource-header {
background: var(--color-background-alt); background: var(--color-background-alt);
font-size: 13px; font-size: 13px;

View file

@ -0,0 +1,14 @@
[
{
"id": "dept-styling",
"name": "Styling",
"resourceIds": ["EMP001", "EMP002", "EMP003"],
"syncStatus": "synced"
},
{
"id": "dept-training",
"name": "Training",
"resourceIds": ["EMP004", "STUDENT001", "STUDENT002"],
"syncStatus": "synced"
}
]

View file

@ -14,6 +14,7 @@
<swp-nav-button id="btn-simple">Datoer</swp-nav-button> <swp-nav-button id="btn-simple">Datoer</swp-nav-button>
<swp-nav-button id="btn-resource">Resources</swp-nav-button> <swp-nav-button id="btn-resource">Resources</swp-nav-button>
<swp-nav-button id="btn-team">Teams</swp-nav-button> <swp-nav-button id="btn-team">Teams</swp-nav-button>
<swp-nav-button id="btn-department">Departments</swp-nav-button>
<swp-week-info> <swp-week-info>
<swp-week-number>V2</swp-week-number> <swp-week-number>V2</swp-week-number>
<swp-date-range id="view-info"></swp-date-range> <swp-date-range id="view-info"></swp-date-range>