Moving away from Azure Devops #1
11 changed files with 199 additions and 5 deletions
|
|
@ -7,6 +7,7 @@ import { ITimeFormatConfig } from './core/ITimeFormatConfig';
|
|||
import { IGridConfig } from './core/IGridConfig';
|
||||
import { ResourceRenderer } from './features/resource/ResourceRenderer';
|
||||
import { TeamRenderer } from './features/team/TeamRenderer';
|
||||
import { DepartmentRenderer } from './features/department/DepartmentRenderer';
|
||||
import { CalendarOrchestrator } from './core/CalendarOrchestrator';
|
||||
import { TimeAxisRenderer } from './features/timeaxis/TimeAxisRenderer';
|
||||
import { ScrollManager } from './core/ScrollManager';
|
||||
|
|
@ -16,7 +17,7 @@ import { DemoApp } from './demo/DemoApp';
|
|||
|
||||
// Event system
|
||||
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
|
||||
import { IndexedDBContext } from './storage/IndexedDBContext';
|
||||
|
|
@ -32,6 +33,8 @@ import { CustomerStore } from './storage/customers/CustomerStore';
|
|||
import { CustomerService } from './storage/customers/CustomerService';
|
||||
import { TeamStore } from './storage/teams/TeamStore';
|
||||
import { TeamService } from './storage/teams/TeamService';
|
||||
import { DepartmentStore } from './storage/departments/DepartmentStore';
|
||||
import { DepartmentService } from './storage/departments/DepartmentService';
|
||||
|
||||
// Audit
|
||||
import { AuditStore } from './storage/audit/AuditStore';
|
||||
|
|
@ -46,6 +49,7 @@ import { MockBookingRepository } from './repositories/MockBookingRepository';
|
|||
import { MockCustomerRepository } from './repositories/MockCustomerRepository';
|
||||
import { MockAuditRepository } from './repositories/MockAuditRepository';
|
||||
import { MockTeamRepository } from './repositories/MockTeamRepository';
|
||||
import { MockDepartmentRepository } from './repositories/MockDepartmentRepository';
|
||||
|
||||
// Workers
|
||||
import { DataSeeder } from './workers/DataSeeder';
|
||||
|
|
@ -106,6 +110,7 @@ export function createV2Container(): Container {
|
|||
builder.registerType(BookingStore).as<IStore>();
|
||||
builder.registerType(CustomerStore).as<IStore>();
|
||||
builder.registerType(TeamStore).as<IStore>();
|
||||
builder.registerType(DepartmentStore).as<IStore>();
|
||||
builder.registerType(ScheduleOverrideStore).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<TeamService>();
|
||||
|
||||
builder.registerType(DepartmentService).as<IEntityService<IDepartment>>();
|
||||
builder.registerType(DepartmentService).as<IEntityService<ISync>>();
|
||||
builder.registerType(DepartmentService).as<DepartmentService>();
|
||||
|
||||
// Repositories (for DataSeeder polymorphic array)
|
||||
builder.registerType(MockEventRepository).as<IApiRepository<ICalendarEvent>>();
|
||||
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<ISync>>();
|
||||
|
||||
builder.registerType(MockDepartmentRepository).as<IApiRepository<IDepartment>>();
|
||||
builder.registerType(MockDepartmentRepository).as<IApiRepository<ISync>>();
|
||||
|
||||
// Audit service (listens to ENTITY_SAVED/DELETED events automatically)
|
||||
builder.registerType(AuditService).as<AuditService>();
|
||||
|
||||
|
|
@ -168,6 +180,7 @@ export function createV2Container(): Container {
|
|||
builder.registerType(DateRenderer).as<IRenderer>();
|
||||
builder.registerType(ResourceRenderer).as<IRenderer>();
|
||||
builder.registerType(TeamRenderer).as<IRenderer>();
|
||||
builder.registerType(DepartmentRenderer).as<IRenderer>();
|
||||
|
||||
// Stores - registreres som IGroupingStore
|
||||
builder.registerType(MockTeamStore).as<IGroupingStore>();
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export class DemoApp {
|
|||
private animator!: NavigationAnimator;
|
||||
private container!: HTMLElement;
|
||||
private weekOffset = 0;
|
||||
private currentView: 'day' | 'simple' | 'resource' | 'team' = 'simple';
|
||||
private currentView: 'day' | 'simple' | 'resource' | 'team' | 'department' = 'simple';
|
||||
|
||||
constructor(
|
||||
private orchestrator: CalendarOrchestrator,
|
||||
|
|
@ -129,6 +129,16 @@ export class DemoApp {
|
|||
{ 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.render();
|
||||
});
|
||||
|
||||
document.getElementById('btn-department')?.addEventListener('click', () => {
|
||||
this.currentView = 'department';
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
private setupDrawerToggle(): void {
|
||||
|
|
|
|||
37
src/v2/features/department/DepartmentRenderer.ts
Normal file
37
src/v2/features/department/DepartmentRenderer.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/v2/repositories/MockDepartmentRepository.ts
Normal file
55
src/v2/repositories/MockDepartmentRepository.ts
Normal 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
|
||||
}));
|
||||
}
|
||||
}
|
||||
25
src/v2/storage/departments/DepartmentService.ts
Normal file
25
src/v2/storage/departments/DepartmentService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
13
src/v2/storage/departments/DepartmentStore.ts
Normal file
13
src/v2/storage/departments/DepartmentStore.ts
Normal 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' });
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import { IWeekSchedule } from './ScheduleTypes';
|
|||
|
||||
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
|
||||
|
|
@ -125,6 +125,13 @@ export interface ITeam extends ISync {
|
|||
resourceIds: string[];
|
||||
}
|
||||
|
||||
// Department types
|
||||
export interface IDepartment extends ISync {
|
||||
id: string;
|
||||
name: string;
|
||||
resourceIds: string[];
|
||||
}
|
||||
|
||||
// Booking types
|
||||
export type BookingStatus =
|
||||
| 'created'
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -175,11 +175,18 @@ swp-calendar-header {
|
|||
> swp-resource-header { grid-row: 2; }
|
||||
> 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-resource-header,
|
||||
swp-team-header {
|
||||
swp-team-header,
|
||||
swp-department-header {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
border-right: 1px solid var(--color-border);
|
||||
|
|
@ -194,6 +201,13 @@ swp-team-header {
|
|||
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 {
|
||||
background: var(--color-background-alt);
|
||||
font-size: 13px;
|
||||
|
|
|
|||
14
wwwroot/data/mock-departments.json
Normal file
14
wwwroot/data/mock-departments.json
Normal 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"
|
||||
}
|
||||
]
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
<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-team">Teams</swp-nav-button>
|
||||
<swp-nav-button id="btn-department">Departments</swp-nav-button>
|
||||
<swp-week-info>
|
||||
<swp-week-number>V2</swp-week-number>
|
||||
<swp-date-range id="view-info"></swp-date-range>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue