diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 3b07a8b..8bc6931 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -4,7 +4,9 @@ "Bash(cd:*)", "Bash(ls:*)", "Bash(dotnet build:*)", - "Bash(find:*)" + "Bash(find:*)", + "Bash(tree:*)", + "Bash(npm run analyze-css:*)" ] } } diff --git a/.gitignore b/.gitignore index 8c05519..77b6968 100644 --- a/.gitignore +++ b/.gitignore @@ -362,3 +362,51 @@ MigrationBackup/ FodyWeavers.xsd nul + +tmpclaude-031f-cwd + +tmpclaude-0e66-cwd + +tmpclaude-0fed-cwd + +tmpclaude-1d0d-cwd + +tmpclaude-1ef3-cwd + +tmpclaude-2d7f-cwd + +tmpclaude-5104-cwd + +tmpclaude-536e-cwd + +tmpclaude-556b-cwd + +tmpclaude-690d-cwd + +tmpclaude-7e66-cwd + +tmpclaude-89bb-cwd + +tmpclaude-d74d-cwd + +tmpclaude-d8f2-cwd + +tmpclaude-eab3-cwd + +tmpclaude-ff51-cwd + +PlanTempus.Application/tmpclaude-0b72-cwd + +PlanTempus.Application/tmpclaude-0eb8-cwd + +PlanTempus.Application/tmpclaude-4109-cwd + +PlanTempus.Application/tmpclaude-6c34-cwd + +PlanTempus.Application/tmpclaude-7386-cwd + +PlanTempus.Application/tmpclaude-cbe1-cwd + +PlanTempus.Application/tmpclaude-d41e-cwd + +PlanTempus.Application/tmpclaude-ea41-cwd diff --git a/CLAUDE.md b/CLAUDE.md index 0f3cfce..0a06e64 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -126,6 +126,88 @@ The solution follows a clean architecture pattern with these main projects: - `global.json` - .NET SDK version configuration (currently .NET 9.0) +## Implementing New Pages - MANDATORY Checklist + + +When implementing a new page or feature, you MUST analyze existing patterns BEFORE writing any code. Creating duplicate components or overriding existing styles is a critical failure. + +### Step 1: Identify UI Elements in POC/Design + +List ALL UI elements the new page needs: +- Stats cards / KPI boxes +- Tables / Lists +- Tabs +- Badges / Status indicators +- Buttons +- Cards +- Forms + +### Step 2: Read the Component Catalog + +**MANDATORY:** Read `wwwroot/css/COMPONENT-CATALOG.md` before creating ANY new page. + +This file contains: +- All reusable components with examples +- Correct usage patterns (e.g., tabs use `.active` class, not data attributes) +- Design tokens reference +- Table/grid patterns with subgrid + +**Component Catalog Location:** `PlanTempus.Application/wwwroot/css/COMPONENT-CATALOG.md` + +### Step 3: Document Reusable vs New + +Before writing code, create a list: + +| Element | Existing Component | File | Action | +|---------|-------------------|------|--------| +| Stats cards | `swp-stat-card` | stats.css | REUSE | +| Tabs | `swp-tab-bar` | tabs.css | REUSE | +| Role badge | `swp-status-badge` | cash.css | REUSE (add variant if needed) | +| Status badge | `swp-status-badge` | cash.css | REUSE (add variant if needed) | + +### Step 4: NEVER Create Duplicate Components + +❌ **WRONG**: Creating `swp-role-badge` when `swp-status-badge` exists +✅ **CORRECT**: Add `.owner`, `.admin` variants to existing `swp-status-badge` + +❌ **WRONG**: Creating `swp-employee-status` for a new status type +✅ **CORRECT**: Add `.active`, `.invited` variants to existing `swp-status-badge` + +**Rule:** If a component exists, add a variant class - don't create a new element. + +### Step 5: Add Header Comment to New CSS + +Every feature CSS file MUST have a header listing reused components: + +```css +/** + * Feature Styles - Description + * + * Feature-specific styling only. + * Reuses: swp-stat-card (stats.css), swp-tab-bar (tabs.css), etc. + */ +``` + +### Common Failure Modes + +1. ❌ **Creating custom stat cards** instead of using `swp-stat-card` + - `swp-stat-card` uses `font-family: var(--font-mono)` for values + - Custom implementations lose this consistency + +2. ❌ **Using wrong tab pattern** (data attributes instead of classes) + - Correct: `` + - Wrong: `` + +3. ❌ **Creating new badge elements** instead of adding variants + - ALL badges use `swp-status-badge` with variant classes + - Add new variant to cash.css, don't create `swp-role-badge`, `swp-employee-status`, etc. + +4. ❌ **Different font sizes** in tables + - Use `var(--font-size-base)` consistently + + + + ## CSS Guidelines ### Grid + Subgrid for Table-like Layouts @@ -170,6 +252,56 @@ swp-my-table-row { - `kasse.css` - swp-kasse-table / swp-kasse-table-row +### Sticky Header + Tab Content Pattern + +For sider med tabs, brug de **GENERISKE** komponenter fra `page.css`: + +**Struktur (TO NIVEAUER ER KRITISK):** +```html + + + ... + ... + + ... + + + + + + + +``` + +**KRITISK:** +- Brug `swp-sticky-header` og `swp-header-content` fra page.css +- ALDRIG opret feature-specifikke varianter (swp-cash-sticky-header, swp-employees-header, etc.) +- Linjen (border-bottom) er på `swp-header-content`, så den er MELLEM stats og tabs +- Tab-bar er UDENFOR header-content, INDEN I sticky-header + +**Reference:** Se `page.css` for styling, `Features/CashRegister/Pages/Index.cshtml` for brug + + +### Undgå Feature-Specifikke Layout-Komponenter + +**ALDRIG** opret feature-specifikke varianter af layout-komponenter: + +❌ **FORKERT:** +```css +swp-cash-sticky-header { ... } +swp-employees-sticky-header { ... } +swp-products-sticky-header { ... } +``` + +✅ **KORREKT:** +```css +/* I page.css - én generisk komponent */ +swp-sticky-header { ... } +``` + +**Regel:** Hvis en komponent er ren layout (sticky header, grid, container), skal den være generisk og ligge i `page.css`. Feature-specifikke styles er kun til feature-specifikt indhold (swp-cash-stats, swp-employee-table, etc.) + + ## NEVER Lie or Fabricate NEVER lie or fabricate. Violating this = immediate critical failure. @@ -242,4 +374,21 @@ swp-my-table-row { break builds. ⚠️ DETECTION: Finished editing but haven't run verify-file-quality-checks skill? → STOP. Run it now. Show the output. + +10. ❌ BAD THOUGHT: "I'll create a new component, it's faster than searching for + existing ones." + ✅ REALITY: Creating duplicate components causes style conflicts, inconsistent + UX, and maintenance burden. The codebase already has reusable patterns. + Duplicating them wastes time on fixes later. + ⚠️ DETECTION: About to create a new CSS element or ViewComponent? → STOP. + Search wwwroot/css/ for existing patterns first. Document what exists vs. + what needs to be created. Show your analysis before writing code. + +11. ❌ BAD THOUGHT: "This element looks different in the POC, so I need to create + a new version." + ✅ REALITY: POC files often use slightly different markup for prototyping. + The production codebase has established patterns. Match the production + patterns, not the POC variations. + ⚠️ DETECTION: POC uses different element names or attributes than existing + code? → STOP. Use the production pattern. The POC is just a reference. \ No newline at end of file diff --git a/OPTIMIZATION_PLAN.md b/OPTIMIZATION_PLAN.md new file mode 100644 index 0000000..865cd15 --- /dev/null +++ b/OPTIMIZATION_PLAN.md @@ -0,0 +1,797 @@ +# CSS & HTML Optimization Plan for PlanTempus + +**Date:** 2026-01-12 +**Status:** Proposal - Awaiting Approval +**Target:** wwwroot CSS and HTML Components + +--- + +## 📊 Executive Summary + +Analysis of the PlanTempus wwwroot folder has identified **5 major categories of duplication** across CSS and HTML components. By consolidating these into a unified component system, we can: + +- ✅ **Reduce CSS size** by eliminating ~40% duplicate code +- ✅ **Improve maintainability** with single source of truth +- ✅ **Ensure UI consistency** across all pages +- ✅ **Simplify future development** with predictable patterns + +--- + +## 🔍 Analysis Results + +### 1. Avatar Components (6+ variations) + +**Current Implementations:** +- `swp-avatar` (waitlist.css) - 40px +- `swp-avatar-small` (bookings.css) - 24px +- `swp-user-avatar` (employees.css) - 36px +- `swp-user-avatar` (auth.css) - 44px +- `swp-employee-avatar-large` (employees.css) - 80px +- `swp-profile-avatar` (topbar.css) - 32px +- `swp-profile-avatar-large` (drawers.css) - 64px + +**Problem:** +- 7 different sizes: 24px, 32px, 36px, 40px, 44px, 64px, 80px +- Duplicated styling for circular containers +- Inconsistent naming conventions +- Color variants duplicated across multiple files + +**Impact:** ~150 lines of duplicated CSS + +--- + +### 2. Status Badge Components (3+ variations) + +**Current Implementations:** +- `swp-status-badge` (cash.css) - Full-featured with dot indicator +- `swp-booking-status` (bookings.css) - Similar styling +- Status utility classes in design-tokens.css + +**Problem:** +- Two separate implementations of the same pattern +- Inconsistent API (one uses dot, one doesn't) +- Duplicated color-mix calculations +- Status variants defined in multiple places + +**Impact:** ~80 lines of duplicated CSS + +--- + +### 3. Icon Box Components (3 variations) + +**Current Implementations:** +- `swp-notification-icon` (notifications.css) - 40px circular +- `swp-attention-icon` (attentions.css) - 40px circular +- `swp-waitlist-icon` (waitlist.css) - 40px + badge + +**Problem:** +- Nearly identical styling (40px, circular, centered) +- Same hover states and color variants +- Only difference is semantic naming + +**Impact:** ~60 lines of duplicated CSS + +--- + +### 4. Stat Display Components (4+ variations) + +**Current Implementations:** +- `swp-stat-card` (stats.css) - Card container with value + label +- `swp-quick-stat` (quick-stats.css) - Smaller variant +- `swp-cash-stat` (cash.css) - Similar to stat-card +- `swp-fact-inline` (employees.css) - Inline variant + +**Problem:** +- Same pattern: value + label in vertical layout +- Duplicated typography rules (mono font, semibold weight) +- Inconsistent sizing and naming +- Color variants repeated across files + +**Impact:** ~100 lines of duplicated CSS + +--- + +### 5. Button Components (2+ variations) + +**Current Implementations:** +- `swp-btn` (cash.css) - Full button system with variants +- Inline button styles in waitlist.css +- Icon buttons in employees.css + +**Problem:** +- Button variants defined in multiple places +- Duplicated padding, transitions, and states +- Inconsistent hover behaviors + +**Impact:** ~50 lines of duplicated CSS + +--- + +## 🎯 Proposed Solution + +### Architecture Overview + +```mermaid +graph TD + A[Current State: Scattered Components] --> B[Create components.css] + B --> C[Define Generic Components] + C --> D1[swp-avatar System] + C --> D2[swp-badge System] + C --> D3[swp-icon-box System] + C --> D4[swp-stat System] + C --> D5[swp-btn System] + + D1 --> E1[Size Modifiers: xs, sm, md, lg, xl] + D1 --> E2[Color Modifiers: teal, blue, purple, etc.] + + D2 --> F1[Semantic Variants: success, warning, error] + D2 --> F2[Role Variants: owner, admin, employee] + + D4 --> G1[Unified Value + Label Pattern] + D4 --> G2[Size Variants: inline, card, box] + + style B fill:#00897b,color:#fff + style C fill:#1976d2,color:#fff +``` + +### Component Class Diagram + +```mermaid +classDiagram + class AvatarSystem { + +swp-avatar (base) + +Size: xs, sm, md, lg, xl, xxl + +Color: teal, blue, purple, amber, green + } + + class BadgeSystem { + +swp-badge (base) + +Status: success, warning, error, pending + +Role: owner, admin, leader, employee + +Size: sm, md + } + + class IconBoxSystem { + +swp-icon-box (base) + +Size: sm, md, lg + +State: urgent, warning, info, success + } + + class StatSystem { + +swp-stat (container) + +swp-stat-value + +swp-stat-label + +Layout: inline, card, box + } + + class ButtonSystem { + +swp-btn (base) + +Variant: primary, secondary, ghost + +Size: sm, md, lg + +State: disabled, loading + } +``` + +--- + +## 📝 Implementation Details + +### 1. Avatar System + +**New Implementation:** + +```css +/* Base avatar */ +swp-avatar { + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-full); + background: var(--color-teal); + color: white; + font-weight: var(--font-weight-semibold); + flex-shrink: 0; + + /* Default size (md) */ + width: 32px; + height: 32px; + font-size: var(--font-size-sm); +} + +/* Size modifiers */ +swp-avatar.xs { width: 20px; height: 20px; font-size: 10px; } +swp-avatar.sm { width: 24px; height: 24px; font-size: 10px; } +swp-avatar.md { width: 32px; height: 32px; font-size: var(--font-size-sm); } +swp-avatar.lg { width: 40px; height: 40px; font-size: var(--font-size-base); } +swp-avatar.xl { width: 64px; height: 64px; font-size: var(--font-size-xl); } +swp-avatar.xxl { width: 80px; height: 80px; font-size: var(--font-size-2xl); } + +/* Color modifiers */ +swp-avatar.purple { background: var(--color-purple); } +swp-avatar.blue { background: var(--color-blue); } +swp-avatar.amber { background: var(--color-amber); } +swp-avatar.green { background: var(--color-green); } +``` + +**Migration Map:** +- `swp-avatar-small` → `` +- `swp-user-avatar` → `` +- `swp-profile-avatar` → `` +- `swp-waitlist-customer swp-avatar` → `` +- `swp-profile-avatar-large` → `` +- `swp-employee-avatar-large` → `` + +**Example Usage:** +```html + +JK + + +JK +``` + +--- + +### 2. Badge System + +**New Implementation:** + +```css +swp-badge { + display: inline-flex; + align-items: center; + gap: var(--spacing-2); + padding: var(--spacing-2) var(--spacing-4); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-medium); + border-radius: var(--radius-pill); +} + +/* Optional dot indicator */ +swp-badge.with-dot::before { + content: ''; + width: 6px; + height: 6px; + border-radius: var(--radius-full); + background: currentColor; +} + +/* Status variants */ +swp-badge.success, +swp-badge.confirmed, +swp-badge.active { + background: color-mix(in srgb, var(--color-green) 15%, transparent); + color: var(--color-green); +} + +swp-badge.warning, +swp-badge.pending, +swp-badge.draft { + background: color-mix(in srgb, var(--color-amber) 15%, transparent); + color: var(--color-amber); +} + +swp-badge.error, +swp-badge.urgent { + background: color-mix(in srgb, var(--color-red) 15%, transparent); + color: var(--color-red); +} + +swp-badge.info, +swp-badge.inprogress { + background: color-mix(in srgb, var(--color-blue) 15%, transparent); + color: var(--color-blue); +} + +/* Role variants */ +swp-badge.owner { + background: color-mix(in srgb, var(--color-teal) 15%, transparent); + color: var(--color-teal); +} + +swp-badge.admin { + background: color-mix(in srgb, var(--color-purple) 15%, transparent); + color: var(--color-purple); +} + +swp-badge.leader { + background: color-mix(in srgb, var(--color-blue) 15%, transparent); + color: var(--color-blue); +} + +swp-badge.employee { + background: var(--color-background-alt); + color: var(--color-text-secondary); +} +``` + +**Migration Map:** +- `swp-status-badge` → `` +- `swp-booking-status` → `` + +--- + +### 3. Icon Box System + +**New Implementation:** + +```css +swp-icon-box { + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-xl); + background: var(--color-background-hover); + color: var(--color-text-secondary); + + /* Default size */ + width: 40px; + height: 40px; + font-size: var(--font-size-xl); +} + +/* Size variants */ +swp-icon-box.sm { + width: 32px; + height: 32px; + font-size: var(--font-size-lg); +} + +swp-icon-box.lg { + width: 48px; + height: 48px; + font-size: var(--font-size-2xl); +} + +/* State modifiers */ +swp-icon-box.urgent { + background: color-mix(in srgb, var(--color-red) 15%, transparent); + color: var(--color-red); +} + +swp-icon-box.warning { + background: color-mix(in srgb, var(--color-amber) 15%, transparent); + color: var(--color-amber); +} + +swp-icon-box.info { + background: color-mix(in srgb, var(--color-blue) 15%, transparent); + color: var(--color-blue); +} + +swp-icon-box.success { + background: color-mix(in srgb, var(--color-green) 15%, transparent); + color: var(--color-green); +} + +/* Unread state for notifications */ +swp-icon-box.unread { + background: color-mix(in srgb, var(--color-teal) 15%, transparent); + color: var(--color-teal); +} +``` + +**Migration Map:** +- `swp-notification-icon` → `` +- `swp-attention-icon` → `` +- `swp-waitlist-icon` → `` (with separate badge element) + +--- + +### 4. Stat System + +**New Implementation:** + +```css +swp-stat { + display: flex; + flex-direction: column; + gap: var(--spacing-2); +} + +swp-stat-value { + display: block; + font-family: var(--font-mono); + font-weight: var(--font-weight-semibold); + color: var(--color-text); + line-height: var(--line-height-tight); + font-size: var(--font-size-xl); +} + +swp-stat-label { + display: block; + font-size: var(--font-size-sm); + color: var(--color-text-secondary); +} + +/* Layout variants */ +swp-stat.inline { + gap: var(--spacing-1); +} + +swp-stat.inline swp-stat-value { + font-size: var(--font-size-lg); +} + +swp-stat.card { + padding: var(--card-padding); + background: var(--color-surface); + border-radius: var(--radius-lg); + border: 1px solid var(--color-border); +} + +swp-stat.card swp-stat-value { + font-size: var(--font-size-3xl); +} + +swp-stat.box { + padding: var(--spacing-6) var(--spacing-8); + background: var(--color-background-alt); + border-radius: var(--radius-lg); +} + +swp-stat.box swp-stat-value { + font-size: var(--font-size-2xl); +} + +/* Color modifiers */ +swp-stat.highlight swp-stat-value, +swp-stat.teal swp-stat-value { + color: var(--color-teal); +} + +swp-stat.success swp-stat-value, +swp-stat.positive swp-stat-value { + color: var(--color-green); +} + +swp-stat.warning swp-stat-value, +swp-stat.amber swp-stat-value { + color: var(--color-amber); +} + +swp-stat.danger swp-stat-value, +swp-stat.negative swp-stat-value, +swp-stat.red swp-stat-value { + color: var(--color-red); +} + +swp-stat.purple swp-stat-value { + color: var(--color-purple); +} + +swp-stat.blue swp-stat-value, +swp-stat.user swp-stat-value { + color: var(--color-blue); +} +``` + +**Migration Map:** +- `swp-stat-card` → `` +- `swp-quick-stat` → `` +- `swp-cash-stat` → `` +- `swp-fact-inline` → `` + +--- + +### 5. Button System + +**New Implementation:** + +```css +swp-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--spacing-3); + padding: var(--spacing-5) var(--spacing-8); + font-size: var(--font-size-md); + font-weight: var(--font-weight-medium); + font-family: var(--font-family); + border-radius: var(--radius-md); + cursor: pointer; + transition: all var(--transition-fast); + border: none; +} + +swp-btn i { + font-size: var(--font-size-lg); +} + +/* Variants */ +swp-btn.primary { + background: var(--color-teal); + color: white; +} + +swp-btn.primary:hover { + opacity: 0.9; +} + +swp-btn.primary:disabled { + background: var(--color-border); + cursor: not-allowed; + opacity: 0.6; +} + +swp-btn.secondary { + background: var(--color-surface); + border: 1px solid var(--color-border); + color: var(--color-text); +} + +swp-btn.secondary:hover { + background: var(--color-background-hover); +} + +swp-btn.ghost { + background: transparent; + color: var(--color-text-secondary); +} + +swp-btn.ghost:hover { + color: var(--color-text); + background: var(--color-background-alt); +} + +/* Size variants */ +swp-btn.sm { + padding: var(--spacing-3) var(--spacing-6); + font-size: var(--font-size-sm); +} + +swp-btn.lg { + padding: var(--spacing-6) var(--spacing-10); + font-size: var(--font-size-lg); +} + +/* Icon-only button */ +swp-btn.icon-only { + padding: var(--spacing-4); + width: 36px; + height: 36px; +} +``` + +--- + +## 📁 File Structure Changes + +### Before: +``` +wwwroot/css/ +├── design-tokens.css +├── base.css +├── page.css +├── stats.css (contains stat-card) +├── quick-stats.css (contains quick-stat) +├── bookings.css (contains avatar-small, booking-status) +├── notifications.css (contains notification-icon) +├── attentions.css (contains attention-icon) +├── waitlist.css (contains avatar, icon) +├── employees.css (contains user-avatar, employee-avatar-large) +├── topbar.css (contains profile-avatar) +├── drawers.css (contains profile-avatar-large) +├── auth.css (contains user-avatar) +└── cash.css (contains status-badge, cash-stat, btn) +``` + +### After: +``` +wwwroot/css/ +├── design-tokens.css (unchanged) +├── base.css (unchanged) +├── components.css ⭐ NEW - Generic reusable components +├── page.css (keep page-level layouts) +├── stats.css ➡️ Simplified (removes duplicates) +├── quick-stats.css ➡️ Can be removed/merged +├── bookings.css ➡️ Simplified (uses swp-avatar, swp-badge) +├── notifications.css ➡️ Simplified (uses swp-icon-box) +├── attentions.css ➡️ Simplified (uses swp-icon-box) +├── waitlist.css ➡️ Simplified (uses swp-avatar, swp-icon-box, swp-btn) +├── employees.css ➡️ Simplified (uses swp-avatar, swp-stat) +├── topbar.css ➡️ Simplified (uses swp-avatar) +├── drawers.css ➡️ Simplified (uses swp-avatar) +├── auth.css ➡️ Simplified (uses swp-avatar) +└── cash.css ➡️ Simplified (uses swp-badge, swp-stat, swp-btn) +``` + +--- + +## 🔄 Implementation Strategy + +### Phase 1: Create Foundation +1. Create `components.css` file +2. Implement base components (avatar, badge, icon-box, stat, btn) +3. Test components in isolation + +### Phase 2: Pilot Migration +1. Start with Avatar system (most instances) +2. Update HTML in one feature (e.g., Dashboard) +3. Verify no visual regressions +4. Document any issues + +### Phase 3: Full Migration +1. Update remaining HTML components +2. Remove duplicated CSS from feature files +3. Test all pages for visual consistency +4. Verify responsive behavior + +### Phase 4: Cleanup +1. Remove unused CSS rules +2. Consider merging small CSS files +3. Update documentation +4. Create component usage guide + +### Implementation Flowchart + +```mermaid +graph TD + A[Start] --> B[Create components.css] + B --> C[Implement base components] + C --> D[Test components in isolation] + D --> E[Pilot: Migrate Dashboard avatars] + E --> F{Visual regression?} + F -->|Yes| G[Fix issues] + G --> E + F -->|No| H[Migrate all HTML components] + H --> I[Remove duplicated CSS] + I --> J[Test all pages] + J --> K{Issues found?} + K -->|Yes| L[Fix issues] + L --> J + K -->|No| M[Cleanup unused CSS] + M --> N[Update documentation] + N --> O[Complete] + + style B fill:#00897b,color:#fff + style I fill:#e53935,color:#fff + style O fill:#43a047,color:#fff +``` + +--- + +## 📊 Expected Benefits + +### Quantitative Benefits + +| Metric | Before | After | Improvement | +|--------|--------|-------|-------------| +| Total CSS Lines | ~2,500 | ~2,000 | -20% | +| Avatar Definitions | 7 | 1 + modifiers | -85% | +| Badge Definitions | 3 | 1 + modifiers | -67% | +| Icon Box Definitions | 3 | 1 + modifiers | -67% | +| Stat Definitions | 4 | 1 + modifiers | -75% | + +### Qualitative Benefits + +```mermaid +mindmap + root((CSS Optimization)) + Maintainability + Single source of truth + Easier updates + Consistent behavior + Less context switching + Performance + Reduced CSS size + Better caching + Faster load times + Fewer parse operations + Developer Experience + Clear naming conventions + Predictable class API + Less cognitive load + Self-documenting code + Design Consistency + Unified components + Consistent sizing + Cohesive UI + Brand alignment +``` + +--- + +## ⚠️ Potential Risks & Mitigation + +### Risk 1: Visual Regressions +**Impact:** High +**Likelihood:** Medium +**Mitigation:** +- Pilot migration with one component first +- Visual regression testing on key pages +- Screenshot comparison before/after +- Incremental rollout + +### Risk 2: Breaking Changes +**Impact:** Medium +**Likelihood:** Low +**Mitigation:** +- Keep old CSS during migration period +- Use feature flags if needed +- Gradual deprecation of old classes +- Clear migration guide for team + +### Risk 3: HTML Update Overhead +**Impact:** Medium +**Likelihood:** High +**Mitigation:** +- Create search/replace patterns +- Update one feature at a time +- Use code review process +- Document common patterns + +### Risk 4: Component Naming Conflicts +**Impact:** Low +**Likelihood:** Low +**Mitigation:** +- Choose unique, descriptive names +- Namespace with swp- prefix +- Check for conflicts before migration +- Update naming if conflicts found + +--- + +## 📋 Checklist + +### Pre-Implementation +- [ ] Review plan with team +- [ ] Get stakeholder approval +- [ ] Create backup branch +- [ ] Set up visual regression testing + +### Phase 1: Foundation +- [ ] Create components.css +- [ ] Implement swp-avatar system +- [ ] Implement swp-badge system +- [ ] Implement swp-icon-box system +- [ ] Implement swp-stat system +- [ ] Implement swp-btn system +- [ ] Test components in isolation + +### Phase 2: Pilot +- [ ] Update Dashboard avatar usage +- [ ] Visual regression test +- [ ] Document any issues +- [ ] Team review & feedback + +### Phase 3: Full Migration +- [ ] Migrate all avatar instances +- [ ] Migrate all badge instances +- [ ] Migrate all icon-box instances +- [ ] Migrate all stat instances +- [ ] Migrate all button instances +- [ ] Remove old CSS rules +- [ ] Test all pages + +### Phase 4: Cleanup +- [ ] Remove unused CSS +- [ ] Consider file consolidation +- [ ] Update component documentation +- [ ] Create usage guide +- [ ] Final testing round + +--- + +## 📚 Next Steps + +1. **Review & Approve** - Stakeholders review this plan +2. **Discuss Concerns** - Address any questions or modifications +3. **Create Timeline** - Determine priority and resources +4. **Begin Implementation** - Start with Phase 1 + +--- + +## 📞 Questions & Discussion Points + +- Is the modifier-based approach (e.g., `class="md purple"`) acceptable? +- Should we prioritize certain components over others? +- Do we need to maintain backward compatibility during migration? +- What's the preferred testing strategy? +- Are there any other duplications not covered here? + +--- + +**Document Status:** Draft +**Last Updated:** 2026-01-12 +**Next Review:** After stakeholder feedback \ No newline at end of file diff --git a/PTWork.code-workspace b/PTWork.code-workspace index fd2c04e..cf7dd46 100644 --- a/PTWork.code-workspace +++ b/PTWork.code-workspace @@ -9,6 +9,6 @@ ], "settings": { "liveServer.settings.port": 5501, - "liveServer.settings.multiRootWorkspaceName": "Calendar" + "liveServer.settings.multiRootWorkspaceName": "PlanTempus" } } \ No newline at end of file diff --git a/PlanTempus.Application/Features/CashRegister/Pages/Index.cshtml b/PlanTempus.Application/Features/CashRegister/Pages/Index.cshtml index 4a0e869..37b4bb2 100644 --- a/PlanTempus.Application/Features/CashRegister/Pages/Index.cshtml +++ b/PlanTempus.Application/Features/CashRegister/Pages/Index.cshtml @@ -6,9 +6,9 @@ } - + - + @@ -48,20 +48,20 @@ Åbnede kassen 29. dec 09:05 - + - - - Oversigt - - - - Kasseafstemning - - - + + + Oversigt + + + + Kasseafstemning + + + diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailCatalog.cs b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailCatalog.cs new file mode 100644 index 0000000..6aa1517 --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailCatalog.cs @@ -0,0 +1,227 @@ +namespace PlanTempus.Application.Features.Employees.Components; + +/// +/// Shared catalog for employee detail data. +/// Used by all EmployeeDetail* ViewComponents. +/// +public static class EmployeeDetailCatalog +{ + private static readonly Dictionary Employees = new() + { + ["employee-1"] = new EmployeeDetailRecord + { + Key = "employee-1", + Initials = "MJ", + Name = "Maria Jensen", + Email = "maria@salonbeauty.dk", + Phone = "+45 12 34 56 78", + Role = "owner", + RoleKey = "employees.roles.owner", + Status = "active", + StatusKey = "employees.status.active", + BookingsThisYear = "312", + RevenueThisYear = "245.800 kr", + Rating = "4.9", + EmployedSince = "2018", + Address = "Hovedgaden 12", + PostalCity = "2100 København Ø", + EmploymentDate = "1. januar 2018", + Position = "Ejer", + EmploymentType = "Fuldtid", + HoursPerWeek = "37", + BirthDate = "12. maj 1985", + EmergencyContact = "Peter Jensen (ægtefælle)", + EmergencyPhone = "+45 98 76 54 32", + BankAccount = "1234-5678901234", + TaxCard = "Hovedkort", + HourlyRate = "250 kr", + MonthlyFixedSalary = "45.000 kr", + Tags = new() { new("Master Stylist", "master"), new("Farvecertificeret", "cert") } + }, + ["employee-2"] = new EmployeeDetailRecord + { + Key = "employee-2", + Initials = "AS", + Name = "Anna Sørensen", + Email = "anna@salonbeauty.dk", + Phone = "+45 23 45 67 89", + Role = "admin", + RoleKey = "employees.roles.admin", + Status = "active", + StatusKey = "employees.status.active", + AvatarColor = "purple", + BookingsThisYear = "248", + RevenueThisYear = "186.450 kr", + Rating = "4.9", + EmployedSince = "2019", + Address = "Vestergade 15, 3. tv", + PostalCity = "8000 Aarhus C", + EmploymentDate = "1. august 2019", + Position = "Master Stylist", + EmploymentType = "Fuldtid", + HoursPerWeek = "37", + BirthDate = "15. marts 1992", + EmergencyContact = "Peter Sørensen (ægtefælle)", + EmergencyPhone = "+45 87 65 43 21", + BankAccount = "2345-6789012345", + TaxCard = "Hovedkort", + HourlyRate = "220 kr", + MonthlyFixedSalary = "38.000 kr", + Tags = new() { new("Master Stylist", "master"), new("Farvecertificeret", "cert"), new("Balayage", "cert") } + }, + ["employee-3"] = new EmployeeDetailRecord + { + Key = "employee-3", + Initials = "LP", + Name = "Louise Pedersen", + Email = "louise@salonbeauty.dk", + Phone = "+45 34 56 78 90", + Role = "leader", + RoleKey = "employees.roles.leader", + Status = "active", + StatusKey = "employees.status.active", + AvatarColor = "blue", + BookingsThisYear = "198", + RevenueThisYear = "156.200 kr", + Rating = "4.7", + EmployedSince = "2020", + Address = "Nørrebrogade 45", + PostalCity = "2200 København N", + EmploymentDate = "15. marts 2020", + Position = "Senior Stylist", + EmploymentType = "Fuldtid", + HoursPerWeek = "37", + BirthDate = "22. november 1988", + EmergencyContact = "Hans Pedersen (far)", + EmergencyPhone = "+45 76 54 32 10", + BankAccount = "3456-7890123456", + TaxCard = "Hovedkort", + HourlyRate = "200 kr", + MonthlyFixedSalary = "35.000 kr", + Tags = new() { new("Senior Stylist", "senior"), new("Farvecertificeret", "cert") } + }, + ["employee-4"] = new EmployeeDetailRecord + { + Key = "employee-4", + Initials = "KN", + Name = "Katrine Nielsen", + Email = "katrine@salonbeauty.dk", + Phone = "+45 45 67 89 01", + Role = "employee", + RoleKey = "employees.roles.employee", + Status = "active", + StatusKey = "employees.status.active", + AvatarColor = "amber", + BookingsThisYear = "165", + RevenueThisYear = "124.300 kr", + Rating = "4.8", + EmployedSince = "2021", + Address = "Frederiksberggade 28", + PostalCity = "1459 København K", + EmploymentDate = "1. juni 2021", + Position = "Stylist", + EmploymentType = "Fuldtid", + HoursPerWeek = "32", + BirthDate = "8. august 1995", + EmergencyContact = "Mette Nielsen (mor)", + EmergencyPhone = "+45 65 43 21 09", + BankAccount = "4567-8901234567", + TaxCard = "Hovedkort", + HourlyRate = "180 kr", + MonthlyFixedSalary = "32.000 kr", + Tags = new() { new("Stylist", "default") } + }, + ["employee-5"] = new EmployeeDetailRecord + { + Key = "employee-5", + Initials = "SH", + Name = "Sofie Hansen", + Email = "sofie@salonbeauty.dk", + Phone = "+45 56 78 90 12", + Role = "employee", + RoleKey = "employees.roles.employee", + Status = "invited", + StatusKey = "employees.status.invited", + AvatarColor = "purple", + BookingsThisYear = "0", + RevenueThisYear = "0 kr", + Rating = "-", + EmployedSince = "2025", + Address = "-", + PostalCity = "-", + EmploymentDate = "1. januar 2025", + Position = "Junior Stylist", + EmploymentType = "Fuldtid", + HoursPerWeek = "37", + BirthDate = "-", + EmergencyContact = "-", + EmergencyPhone = "-", + BankAccount = "-", + TaxCard = "-", + HourlyRate = "150 kr", + MonthlyFixedSalary = "28.000 kr", + Tags = new() { new("Junior Stylist", "junior") } + } + }; + + public static EmployeeDetailRecord Get(string key) + { + if (!Employees.TryGetValue(key, out var employee)) + throw new KeyNotFoundException($"Employee with key '{key}' not found"); + return employee; + } + + public static IEnumerable AllKeys => Employees.Keys; +} + +/// +/// Complete employee detail record used across all detail ViewComponents. +/// +public record EmployeeDetailRecord +{ + // Identity + public required string Key { get; init; } + public required string Initials { get; init; } + public required string Name { get; init; } + public string? AvatarColor { get; init; } + + // Contact + public required string Email { get; init; } + public required string Phone { get; init; } + public required string Address { get; init; } + public required string PostalCity { get; init; } + + // Role & Status + public required string Role { get; init; } + public required string RoleKey { get; init; } + public required string Status { get; init; } + public required string StatusKey { get; init; } + + // Stats + public required string BookingsThisYear { get; init; } + public required string RevenueThisYear { get; init; } + public required string Rating { get; init; } + public required string EmployedSince { get; init; } + + // Employment + public required string EmploymentDate { get; init; } + public required string Position { get; init; } + public required string EmploymentType { get; init; } + public required string HoursPerWeek { get; init; } + + // Personal + public required string BirthDate { get; init; } + public required string EmergencyContact { get; init; } + public required string EmergencyPhone { get; init; } + + // Salary + public required string BankAccount { get; init; } + public required string TaxCard { get; init; } + public required string HourlyRate { get; init; } + public required string MonthlyFixedSalary { get; init; } + + // Tags (certifications, specialties) + public List Tags { get; init; } = new(); +} + +public record EmployeeTag(string Text, string CssClass); diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailGeneral/Default.cshtml b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailGeneral/Default.cshtml new file mode 100644 index 0000000..9b4d08a --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailGeneral/Default.cshtml @@ -0,0 +1,151 @@ +@model PlanTempus.Application.Features.Employees.Components.EmployeeDetailGeneralViewModel + + + +
+ + + @Model.LabelContact + + + @Model.LabelFullName + @Model.Name + + + @Model.LabelEmail + @Model.Email + + + @Model.LabelPhone + @Model.Phone + + + @Model.LabelAddress + @Model.Address + + + @Model.LabelPostalCity + @Model.PostalCity + + + + + + + @Model.LabelPersonal + + + @Model.LabelBirthDate + @Model.BirthDate + + + @Model.LabelEmergencyContact + @Model.EmergencyContact + + + @Model.LabelEmergencyPhone + @Model.EmergencyPhone + + + +
+ + +
+ + + @Model.LabelEmployment + + + @Model.LabelEmploymentDate + @Model.EmploymentDate + + + @Model.LabelPosition + @Model.Position + + + @Model.LabelEmploymentType + @Model.EmploymentType + + + @Model.LabelHoursPerWeek + @Model.HoursPerWeek + + + + + + + @Model.LabelSettings + +
+ @Model.SettingShowInBooking + @Model.SettingShowInBookingDesc +
+ + @Model.ToggleYes + @Model.ToggleNo + +
+ +
+ @Model.SettingSmsReminders + @Model.SettingSmsRemindersDesc +
+ + @Model.ToggleYes + @Model.ToggleNo + +
+ +
+ @Model.SettingEditCalendar + @Model.SettingEditCalendarDesc +
+ + @Model.ToggleYes + @Model.ToggleNo + +
+
+ + + + @Model.LabelNotifications + @Model.NotificationsIntro + + + + + + @Model.NotifOnlineBooking + + + + + + @Model.NotifManualBooking + + + + + + @Model.NotifCancellation + + + + + + @Model.NotifWaitlist + + + + + + @Model.NotifDailySummary + + + +
+
diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailGeneral/EmployeeDetailGeneralViewComponent.cs b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailGeneral/EmployeeDetailGeneralViewComponent.cs new file mode 100644 index 0000000..4359477 --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailGeneral/EmployeeDetailGeneralViewComponent.cs @@ -0,0 +1,137 @@ +using Microsoft.AspNetCore.Mvc; +using PlanTempus.Application.Features.Localization.Services; + +namespace PlanTempus.Application.Features.Employees.Components; + +public class EmployeeDetailGeneralViewComponent : ViewComponent +{ + private readonly ILocalizationService _localization; + + public EmployeeDetailGeneralViewComponent(ILocalizationService localization) + { + _localization = localization; + } + + public IViewComponentResult Invoke(string key) + { + var employee = EmployeeDetailCatalog.Get(key); + + var model = new EmployeeDetailGeneralViewModel + { + // Contact + Name = employee.Name, + Email = employee.Email, + Phone = employee.Phone, + Address = employee.Address, + PostalCity = employee.PostalCity, + + // Personal + BirthDate = employee.BirthDate, + EmergencyContact = employee.EmergencyContact, + EmergencyPhone = employee.EmergencyPhone, + + // Employment + EmploymentDate = employee.EmploymentDate, + Position = employee.Position, + EmploymentType = employee.EmploymentType, + HoursPerWeek = employee.HoursPerWeek, + + // Labels + LabelContact = _localization.Get("employees.detail.contact"), + LabelPersonal = _localization.Get("employees.detail.personal"), + LabelEmployment = _localization.Get("employees.detail.employment"), + LabelFullName = _localization.Get("employees.detail.fullname"), + LabelEmail = _localization.Get("employees.detail.email"), + LabelPhone = _localization.Get("employees.detail.phone"), + LabelAddress = _localization.Get("employees.detail.address"), + LabelPostalCity = _localization.Get("employees.detail.postalcity"), + LabelBirthDate = _localization.Get("employees.detail.birthdate"), + LabelEmergencyContact = _localization.Get("employees.detail.emergencycontact"), + LabelEmergencyPhone = _localization.Get("employees.detail.emergencyphone"), + LabelEmploymentDate = _localization.Get("employees.detail.employmentdate"), + LabelPosition = _localization.Get("employees.detail.position"), + LabelEmploymentType = _localization.Get("employees.detail.employmenttype"), + LabelHoursPerWeek = _localization.Get("employees.detail.hoursperweek"), + + // Settings + LabelSettings = _localization.Get("employees.detail.settings.label"), + SettingShowInBooking = _localization.Get("employees.detail.settings.showinbooking.label"), + SettingShowInBookingDesc = _localization.Get("employees.detail.settings.showinbooking.desc"), + SettingSmsReminders = _localization.Get("employees.detail.settings.smsreminders.label"), + SettingSmsRemindersDesc = _localization.Get("employees.detail.settings.smsreminders.desc"), + SettingEditCalendar = _localization.Get("employees.detail.settings.editcalendar.label"), + SettingEditCalendarDesc = _localization.Get("employees.detail.settings.editcalendar.desc"), + ToggleYes = _localization.Get("common.yes"), + ToggleNo = _localization.Get("common.no"), + + // Notifications + LabelNotifications = _localization.Get("employees.detail.notifications.label"), + NotificationsIntro = _localization.Get("employees.detail.notifications.intro"), + NotifOnlineBooking = _localization.Get("employees.detail.notifications.onlinebooking"), + NotifManualBooking = _localization.Get("employees.detail.notifications.manualbooking"), + NotifCancellation = _localization.Get("employees.detail.notifications.cancellation"), + NotifWaitlist = _localization.Get("employees.detail.notifications.waitlist"), + NotifDailySummary = _localization.Get("employees.detail.notifications.dailysummary") + }; + + return View(model); + } +} + +public class EmployeeDetailGeneralViewModel +{ + // Contact + public required string Name { get; init; } + public required string Email { get; init; } + public required string Phone { get; init; } + public required string Address { get; init; } + public required string PostalCity { get; init; } + + // Personal + public required string BirthDate { get; init; } + public required string EmergencyContact { get; init; } + public required string EmergencyPhone { get; init; } + + // Employment + public required string EmploymentDate { get; init; } + public required string Position { get; init; } + public required string EmploymentType { get; init; } + public required string HoursPerWeek { get; init; } + + // Labels + public required string LabelContact { get; init; } + public required string LabelPersonal { get; init; } + public required string LabelEmployment { get; init; } + public required string LabelFullName { get; init; } + public required string LabelEmail { get; init; } + public required string LabelPhone { get; init; } + public required string LabelAddress { get; init; } + public required string LabelPostalCity { get; init; } + public required string LabelBirthDate { get; init; } + public required string LabelEmergencyContact { get; init; } + public required string LabelEmergencyPhone { get; init; } + public required string LabelEmploymentDate { get; init; } + public required string LabelPosition { get; init; } + public required string LabelEmploymentType { get; init; } + public required string LabelHoursPerWeek { get; init; } + + // Settings + public required string LabelSettings { get; init; } + public required string SettingShowInBooking { get; init; } + public required string SettingShowInBookingDesc { get; init; } + public required string SettingSmsReminders { get; init; } + public required string SettingSmsRemindersDesc { get; init; } + public required string SettingEditCalendar { get; init; } + public required string SettingEditCalendarDesc { get; init; } + public required string ToggleYes { get; init; } + public required string ToggleNo { get; init; } + + // Notifications + public required string LabelNotifications { get; init; } + public required string NotificationsIntro { get; init; } + public required string NotifOnlineBooking { get; init; } + public required string NotifManualBooking { get; init; } + public required string NotifCancellation { get; init; } + public required string NotifWaitlist { get; init; } + public required string NotifDailySummary { get; init; } +} diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHR/Default.cshtml b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHR/Default.cshtml new file mode 100644 index 0000000..819af4d --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHR/Default.cshtml @@ -0,0 +1,44 @@ +@model PlanTempus.Application.Features.Employees.Components.EmployeeDetailHRViewModel + + + + @Model.LabelDocuments + + + + @Model.LabelContract + 15. aug 2019 + + + + Lønaftale 2024 + 1. jan 2024 + + + + + + @Model.LabelVacation + + + Optjent ferie + 25 dage + + + Afholdt ferie + 12 dage + + + Resterende + 13 dage + + + + + + @Model.LabelNotes + + Ingen noter tilføjet endnu... + + + diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHR/EmployeeDetailHRViewComponent.cs b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHR/EmployeeDetailHRViewComponent.cs new file mode 100644 index 0000000..269dcc3 --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHR/EmployeeDetailHRViewComponent.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Mvc; +using PlanTempus.Application.Features.Localization.Services; + +namespace PlanTempus.Application.Features.Employees.Components; + +public class EmployeeDetailHRViewComponent : ViewComponent +{ + private readonly ILocalizationService _localization; + + public EmployeeDetailHRViewComponent(ILocalizationService localization) + { + _localization = localization; + } + + public IViewComponentResult Invoke(string key) + { + var model = new EmployeeDetailHRViewModel + { + LabelDocuments = _localization.Get("employees.detail.hr.documents"), + LabelContract = _localization.Get("employees.detail.hr.contract"), + LabelVacation = _localization.Get("employees.detail.hr.vacation"), + LabelSickLeave = _localization.Get("employees.detail.hr.sickleave"), + LabelNotes = _localization.Get("employees.detail.hr.notes") + }; + + return View(model); + } +} + +public class EmployeeDetailHRViewModel +{ + public required string LabelDocuments { get; init; } + public required string LabelContract { get; init; } + public required string LabelVacation { get; init; } + public required string LabelSickLeave { get; init; } + public required string LabelNotes { get; init; } +} diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHeader/Default.cshtml b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHeader/Default.cshtml new file mode 100644 index 0000000..1c5a36d --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHeader/Default.cshtml @@ -0,0 +1,41 @@ +@model PlanTempus.Application.Features.Employees.Components.EmployeeDetailHeaderViewModel + + + @Model.Initials + + + @Model.Name + @if (Model.Tags.Any()) + { + + @foreach (var tag in Model.Tags) + { + @tag.Text + } + + } + + + @Model.StatusText + + + + + @Model.BookingsThisYear + @Model.LabelBookings + + + @Model.RevenueThisYear + @Model.LabelRevenue + + + @Model.Rating + @Model.LabelRating + + + @Model.EmployedSince + @Model.LabelEmployedSince + + + + diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHeader/EmployeeDetailHeaderViewComponent.cs b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHeader/EmployeeDetailHeaderViewComponent.cs new file mode 100644 index 0000000..b60682f --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHeader/EmployeeDetailHeaderViewComponent.cs @@ -0,0 +1,68 @@ +using Microsoft.AspNetCore.Mvc; +using PlanTempus.Application.Features.Localization.Services; + +namespace PlanTempus.Application.Features.Employees.Components; + +public class EmployeeDetailHeaderViewComponent : ViewComponent +{ + private readonly ILocalizationService _localization; + + public EmployeeDetailHeaderViewComponent(ILocalizationService localization) + { + _localization = localization; + } + + public IViewComponentResult Invoke(string key) + { + var employee = EmployeeDetailCatalog.Get(key); + + var model = new EmployeeDetailHeaderViewModel + { + Initials = employee.Initials, + Name = employee.Name, + AvatarColor = employee.AvatarColor, + Role = employee.Role, + RoleText = _localization.Get(employee.RoleKey), + Status = employee.Status, + StatusText = _localization.Get(employee.StatusKey), + BookingsThisYear = employee.BookingsThisYear, + RevenueThisYear = employee.RevenueThisYear, + Rating = employee.Rating, + EmployedSince = employee.EmployedSince, + LabelBookings = _localization.Get("employees.detail.bookings"), + LabelRevenue = _localization.Get("employees.detail.revenue"), + LabelRating = _localization.Get("employees.detail.rating"), + LabelEmployedSince = _localization.Get("employees.detail.employedsince"), + Tags = employee.Tags.Select(t => new EmployeeTagViewModel { Text = t.Text, CssClass = t.CssClass }).ToList() + }; + + return View(model); + } +} + +public class EmployeeDetailHeaderViewModel +{ + public required string Initials { get; init; } + public required string Name { get; init; } + public string? AvatarColor { get; init; } + public required string Role { get; init; } + public required string RoleText { get; init; } + public required string Status { get; init; } + public required string StatusText { get; init; } + public bool IsActive => Status == "active"; + public required string BookingsThisYear { get; init; } + public required string RevenueThisYear { get; init; } + public required string Rating { get; init; } + public required string EmployedSince { get; init; } + public required string LabelBookings { get; init; } + public required string LabelRevenue { get; init; } + public required string LabelRating { get; init; } + public required string LabelEmployedSince { get; init; } + public List Tags { get; init; } = new(); +} + +public class EmployeeTagViewModel +{ + public required string Text { get; init; } + public required string CssClass { get; init; } +} diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHours/Default.cshtml b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHours/Default.cshtml new file mode 100644 index 0000000..64c92bd --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHours/Default.cshtml @@ -0,0 +1,37 @@ +@model PlanTempus.Application.Features.Employees.Components.EmployeeDetailHoursViewModel + + + + @Model.LabelWeeklySchedule + + + @Model.LabelMonday + 09:00 - 17:00 + + + @Model.LabelTuesday + 09:00 - 17:00 + + + @Model.LabelWednesday + 09:00 - 17:00 + + + @Model.LabelThursday + 09:00 - 19:00 + + + @Model.LabelFriday + 09:00 - 16:00 + + + @Model.LabelSaturday + Fri + + + @Model.LabelSunday + Fri + + + + diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHours/EmployeeDetailHoursViewComponent.cs b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHours/EmployeeDetailHoursViewComponent.cs new file mode 100644 index 0000000..e81e771 --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHours/EmployeeDetailHoursViewComponent.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Mvc; +using PlanTempus.Application.Features.Localization.Services; + +namespace PlanTempus.Application.Features.Employees.Components; + +public class EmployeeDetailHoursViewComponent : ViewComponent +{ + private readonly ILocalizationService _localization; + + public EmployeeDetailHoursViewComponent(ILocalizationService localization) + { + _localization = localization; + } + + public IViewComponentResult Invoke(string key) + { + var model = new EmployeeDetailHoursViewModel + { + LabelWeeklySchedule = _localization.Get("employees.detail.hours.weekly"), + LabelMonday = _localization.Get("employees.detail.hours.monday"), + LabelTuesday = _localization.Get("employees.detail.hours.tuesday"), + LabelWednesday = _localization.Get("employees.detail.hours.wednesday"), + LabelThursday = _localization.Get("employees.detail.hours.thursday"), + LabelFriday = _localization.Get("employees.detail.hours.friday"), + LabelSaturday = _localization.Get("employees.detail.hours.saturday"), + LabelSunday = _localization.Get("employees.detail.hours.sunday") + }; + + return View(model); + } +} + +public class EmployeeDetailHoursViewModel +{ + public required string LabelWeeklySchedule { get; init; } + public required string LabelMonday { get; init; } + public required string LabelTuesday { get; init; } + public required string LabelWednesday { get; init; } + public required string LabelThursday { get; init; } + public required string LabelFriday { get; init; } + public required string LabelSaturday { get; init; } + public required string LabelSunday { get; init; } +} diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailSalary/Default.cshtml b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailSalary/Default.cshtml new file mode 100644 index 0000000..1bcb33c --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailSalary/Default.cshtml @@ -0,0 +1,39 @@ +@model PlanTempus.Application.Features.Employees.Components.EmployeeDetailSalaryViewModel + + + + @Model.LabelPaymentInfo + + + @Model.LabelBankAccount + @Model.BankAccount + + + @Model.LabelTaxCard + @Model.TaxCard + + + + + + @Model.LabelSalarySettings + + + @Model.LabelHourlyRate + @Model.HourlyRate + + + @Model.LabelMonthlyFixed + @Model.MonthlyFixedSalary + + + @Model.LabelCommission + 10% + + + @Model.LabelProductCommission + 5% + + + + diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailSalary/EmployeeDetailSalaryViewComponent.cs b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailSalary/EmployeeDetailSalaryViewComponent.cs new file mode 100644 index 0000000..48349bc --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailSalary/EmployeeDetailSalaryViewComponent.cs @@ -0,0 +1,53 @@ +using Microsoft.AspNetCore.Mvc; +using PlanTempus.Application.Features.Localization.Services; + +namespace PlanTempus.Application.Features.Employees.Components; + +public class EmployeeDetailSalaryViewComponent : ViewComponent +{ + private readonly ILocalizationService _localization; + + public EmployeeDetailSalaryViewComponent(ILocalizationService localization) + { + _localization = localization; + } + + public IViewComponentResult Invoke(string key) + { + var employee = EmployeeDetailCatalog.Get(key); + + var model = new EmployeeDetailSalaryViewModel + { + BankAccount = employee.BankAccount, + TaxCard = employee.TaxCard, + HourlyRate = employee.HourlyRate, + MonthlyFixedSalary = employee.MonthlyFixedSalary, + LabelPaymentInfo = _localization.Get("employees.detail.salary.paymentinfo"), + LabelBankAccount = _localization.Get("employees.detail.salary.bankaccount"), + LabelTaxCard = _localization.Get("employees.detail.salary.taxcard"), + LabelSalarySettings = _localization.Get("employees.detail.salary.settings"), + LabelHourlyRate = _localization.Get("employees.detail.salary.hourlyrate"), + LabelMonthlyFixed = _localization.Get("employees.detail.salary.monthlyfixed"), + LabelCommission = _localization.Get("employees.detail.salary.commission"), + LabelProductCommission = _localization.Get("employees.detail.salary.productcommission") + }; + + return View(model); + } +} + +public class EmployeeDetailSalaryViewModel +{ + public required string BankAccount { get; init; } + public required string TaxCard { get; init; } + public required string HourlyRate { get; init; } + public required string MonthlyFixedSalary { get; init; } + public required string LabelPaymentInfo { get; init; } + public required string LabelBankAccount { get; init; } + public required string LabelTaxCard { get; init; } + public required string LabelSalarySettings { get; init; } + public required string LabelHourlyRate { get; init; } + public required string LabelMonthlyFixed { get; init; } + public required string LabelCommission { get; init; } + public required string LabelProductCommission { get; init; } +} diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailServices/Default.cshtml b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailServices/Default.cshtml new file mode 100644 index 0000000..f6f55f0 --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailServices/Default.cshtml @@ -0,0 +1,34 @@ +@model PlanTempus.Application.Features.Employees.Components.EmployeeDetailServicesViewModel + + + + @Model.LabelAssignedServices + + + Dameklip + 45 min + 450 kr + + + Herreklip + 30 min + 350 kr + + + Farvning + 90 min + 850 kr + + + Balayage + 120 min + 1.200 kr + + + Highlights + 90 min + 950 kr + + + + diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailServices/EmployeeDetailServicesViewComponent.cs b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailServices/EmployeeDetailServicesViewComponent.cs new file mode 100644 index 0000000..c1f4a16 --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailServices/EmployeeDetailServicesViewComponent.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Mvc; +using PlanTempus.Application.Features.Localization.Services; + +namespace PlanTempus.Application.Features.Employees.Components; + +public class EmployeeDetailServicesViewComponent : ViewComponent +{ + private readonly ILocalizationService _localization; + + public EmployeeDetailServicesViewComponent(ILocalizationService localization) + { + _localization = localization; + } + + public IViewComponentResult Invoke(string key) + { + var model = new EmployeeDetailServicesViewModel + { + LabelAssignedServices = _localization.Get("employees.detail.services.assigned") + }; + + return View(model); + } +} + +public class EmployeeDetailServicesViewModel +{ + public required string LabelAssignedServices { get; init; } +} diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailStats/Default.cshtml b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailStats/Default.cshtml new file mode 100644 index 0000000..6a4eec4 --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailStats/Default.cshtml @@ -0,0 +1,25 @@ +@model PlanTempus.Application.Features.Employees.Components.EmployeeDetailStatsViewModel + + + + @Model.LabelPerformance + + + @Model.BookingsThisYear + @Model.LabelBookingsThisYear + + + @Model.RevenueThisYear + @Model.LabelRevenueThisYear + + + @Model.Rating + @Model.LabelAvgRating + + + 87% + @Model.LabelOccupancy + + + + diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailStats/EmployeeDetailStatsViewComponent.cs b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailStats/EmployeeDetailStatsViewComponent.cs new file mode 100644 index 0000000..e53563c --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailStats/EmployeeDetailStatsViewComponent.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Mvc; +using PlanTempus.Application.Features.Localization.Services; + +namespace PlanTempus.Application.Features.Employees.Components; + +public class EmployeeDetailStatsViewComponent : ViewComponent +{ + private readonly ILocalizationService _localization; + + public EmployeeDetailStatsViewComponent(ILocalizationService localization) + { + _localization = localization; + } + + public IViewComponentResult Invoke(string key) + { + var employee = EmployeeDetailCatalog.Get(key); + + var model = new EmployeeDetailStatsViewModel + { + BookingsThisYear = employee.BookingsThisYear, + RevenueThisYear = employee.RevenueThisYear, + Rating = employee.Rating, + LabelPerformance = _localization.Get("employees.detail.stats.performance"), + LabelBookingsThisYear = _localization.Get("employees.detail.stats.bookingsyear"), + LabelRevenueThisYear = _localization.Get("employees.detail.stats.revenueyear"), + LabelAvgRating = _localization.Get("employees.detail.stats.avgrating"), + LabelOccupancy = _localization.Get("employees.detail.stats.occupancy") + }; + + return View(model); + } +} + +public class EmployeeDetailStatsViewModel +{ + public required string BookingsThisYear { get; init; } + public required string RevenueThisYear { get; init; } + public required string Rating { get; init; } + public required string LabelPerformance { get; init; } + public required string LabelBookingsThisYear { get; init; } + public required string LabelRevenueThisYear { get; init; } + public required string LabelAvgRating { get; init; } + public required string LabelOccupancy { get; init; } +} diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailView/Default.cshtml b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailView/Default.cshtml new file mode 100644 index 0000000..5208c9c --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailView/Default.cshtml @@ -0,0 +1,74 @@ +@model PlanTempus.Application.Features.Employees.Components.EmployeeDetailViewViewModel + + + + + + + + + + + @Model.BackText + + + + + + @Model.SaveButtonText + + + + + + @await Component.InvokeAsync("EmployeeDetailHeader", Model.EmployeeKey) + + + + + @Model.TabGeneral + @Model.TabHours + @Model.TabServices + @Model.TabSalary + @Model.TabHR + @Model.TabStats + + + + + + + @await Component.InvokeAsync("EmployeeDetailGeneral", Model.EmployeeKey) + + + + + + @await Component.InvokeAsync("EmployeeDetailHours", Model.EmployeeKey) + + + + + + @await Component.InvokeAsync("EmployeeDetailServices", Model.EmployeeKey) + + + + + + @await Component.InvokeAsync("EmployeeDetailSalary", Model.EmployeeKey) + + + + + + @await Component.InvokeAsync("EmployeeDetailHR", Model.EmployeeKey) + + + + + + @await Component.InvokeAsync("EmployeeDetailStats", Model.EmployeeKey) + + + diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailView/EmployeeDetailViewViewComponent.cs b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailView/EmployeeDetailViewViewComponent.cs new file mode 100644 index 0000000..9f94386 --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailView/EmployeeDetailViewViewComponent.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Mvc; +using PlanTempus.Application.Features.Localization.Services; + +namespace PlanTempus.Application.Features.Employees.Components; + +public class EmployeeDetailViewViewComponent : ViewComponent +{ + private readonly ILocalizationService _localization; + + public EmployeeDetailViewViewComponent(ILocalizationService localization) + { + _localization = localization; + } + + public IViewComponentResult Invoke(string key) + { + var employee = EmployeeDetailCatalog.Get(key); + + var model = new EmployeeDetailViewViewModel + { + EmployeeKey = employee.Key, + BackText = _localization.Get("employees.detail.back"), + SaveButtonText = _localization.Get("employees.detail.save"), + TabGeneral = _localization.Get("employees.detail.tabs.general"), + TabHours = _localization.Get("employees.detail.tabs.hours"), + TabServices = _localization.Get("employees.detail.tabs.services"), + TabSalary = _localization.Get("employees.detail.tabs.salary"), + TabHR = _localization.Get("employees.detail.tabs.hr"), + TabStats = _localization.Get("employees.detail.tabs.stats") + }; + + return View(model); + } +} + +public class EmployeeDetailViewViewModel +{ + public required string EmployeeKey { get; init; } + public required string BackText { get; init; } + public required string SaveButtonText { get; init; } + public required string TabGeneral { get; init; } + public required string TabHours { get; init; } + public required string TabServices { get; init; } + public required string TabSalary { get; init; } + public required string TabHR { get; init; } + public required string TabStats { get; init; } +} diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeRow/Default.cshtml b/PlanTempus.Application/Features/Employees/Components/EmployeeRow/Default.cshtml new file mode 100644 index 0000000..d453a45 --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeRow/Default.cshtml @@ -0,0 +1,25 @@ +@model PlanTempus.Application.Features.Employees.Components.EmployeeRowViewModel + + + + + @Model.Initials + + @Model.Name + @Model.Email + + + + + @Model.RoleText + + + @Model.StatusText + + @Model.LastActive + + + + + + diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeRow/EmployeeRowViewComponent.cs b/PlanTempus.Application/Features/Employees/Components/EmployeeRow/EmployeeRowViewComponent.cs new file mode 100644 index 0000000..9596677 --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeRow/EmployeeRowViewComponent.cs @@ -0,0 +1,145 @@ +using Microsoft.AspNetCore.Mvc; +using PlanTempus.Application.Features.Localization.Services; + +namespace PlanTempus.Application.Features.Employees.Components; + +public class EmployeeRowViewComponent : ViewComponent +{ + private readonly ILocalizationService _localization; + + public EmployeeRowViewComponent(ILocalizationService localization) + { + _localization = localization; + } + + public IViewComponentResult Invoke(string key) + { + var model = EmployeeRowCatalog.Get(key, _localization); + return View(model); + } +} + +public class EmployeeRowViewModel +{ + public required string Key { get; init; } + public required string Initials { get; init; } + public required string Name { get; init; } + public required string Email { get; init; } + public required string Role { get; init; } + public required string RoleText { get; init; } + public required string Status { get; init; } + public required string StatusText { get; init; } + public required string LastActive { get; init; } + public string? AvatarColor { get; init; } + public bool IsOwner { get; init; } + public bool IsInvited { get; init; } +} + +internal class EmployeeRowData +{ + public required string Key { get; init; } + public required string Initials { get; init; } + public required string Name { get; init; } + public required string Email { get; init; } + public required string Role { get; init; } + public required string RoleKey { get; init; } + public required string Status { get; init; } + public required string StatusKey { get; init; } + public required string LastActive { get; init; } + public string? AvatarColor { get; init; } +} + +public static class EmployeeRowCatalog +{ + private static readonly Dictionary Employees = new() + { + ["employee-1"] = new EmployeeRowData + { + Key = "employee-1", + Initials = "MJ", + Name = "Maria Jensen", + Email = "maria@salonbeauty.dk", + Role = "owner", + RoleKey = "employees.roles.owner", + Status = "active", + StatusKey = "employees.status.active", + LastActive = "I dag, 14:32" + }, + ["employee-2"] = new EmployeeRowData + { + Key = "employee-2", + Initials = "AS", + Name = "Anna Sørensen", + Email = "anna@salonbeauty.dk", + Role = "admin", + RoleKey = "employees.roles.admin", + Status = "active", + StatusKey = "employees.status.active", + LastActive = "I dag, 12:15", + AvatarColor = "purple" + }, + ["employee-3"] = new EmployeeRowData + { + Key = "employee-3", + Initials = "LP", + Name = "Louise Pedersen", + Email = "louise@salonbeauty.dk", + Role = "leader", + RoleKey = "employees.roles.leader", + Status = "active", + StatusKey = "employees.status.active", + LastActive = "I går, 17:45", + AvatarColor = "blue" + }, + ["employee-4"] = new EmployeeRowData + { + Key = "employee-4", + Initials = "KN", + Name = "Katrine Nielsen", + Email = "katrine@salonbeauty.dk", + Role = "employee", + RoleKey = "employees.roles.employee", + Status = "active", + StatusKey = "employees.status.active", + LastActive = "27. dec, 09:30", + AvatarColor = "amber" + }, + ["employee-5"] = new EmployeeRowData + { + Key = "employee-5", + Initials = "SH", + Name = "Sofie Hansen", + Email = "sofie@salonbeauty.dk", + Role = "employee", + RoleKey = "employees.roles.employee", + Status = "invited", + StatusKey = "employees.status.invited", + LastActive = "-", + AvatarColor = "purple" + } + }; + + public static EmployeeRowViewModel Get(string key, ILocalizationService localization) + { + if (!Employees.TryGetValue(key, out var employee)) + throw new KeyNotFoundException($"Employee with key '{key}' not found"); + + return new EmployeeRowViewModel + { + Key = employee.Key, + Initials = employee.Initials, + Name = employee.Name, + Email = employee.Email, + Role = employee.Role, + RoleText = localization.Get(employee.RoleKey), + Status = employee.Status, + StatusText = localization.Get(employee.StatusKey), + LastActive = employee.LastActive, + AvatarColor = employee.AvatarColor, + IsOwner = employee.Role == "owner", + IsInvited = employee.Status == "invited" + }; + } + + public static IEnumerable AllKeys => Employees.Keys; +} diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeStatCard/Default.cshtml b/PlanTempus.Application/Features/Employees/Components/EmployeeStatCard/Default.cshtml new file mode 100644 index 0000000..beaee25 --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeStatCard/Default.cshtml @@ -0,0 +1,6 @@ +@model PlanTempus.Application.Features.Employees.Components.EmployeeStatCardViewModel + + + @Model.Value + @Model.Label + diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeStatCard/EmployeeStatCardViewComponent.cs b/PlanTempus.Application/Features/Employees/Components/EmployeeStatCard/EmployeeStatCardViewComponent.cs new file mode 100644 index 0000000..fdf31e6 --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeStatCard/EmployeeStatCardViewComponent.cs @@ -0,0 +1,80 @@ +using Microsoft.AspNetCore.Mvc; +using PlanTempus.Application.Features.Localization.Services; + +namespace PlanTempus.Application.Features.Employees.Components; + +public class EmployeeStatCardViewComponent : ViewComponent +{ + private readonly ILocalizationService _localization; + + public EmployeeStatCardViewComponent(ILocalizationService localization) + { + _localization = localization; + } + + public IViewComponentResult Invoke(string key) + { + var model = EmployeeStatCardCatalog.Get(key, _localization); + return View(model); + } +} + +public class EmployeeStatCardViewModel +{ + public required string Key { get; init; } + public required string Value { get; init; } + public required string Label { get; init; } + public string? Variant { get; init; } +} + +internal class EmployeeStatCardData +{ + public required string Key { get; init; } + public required string Value { get; init; } + public required string LabelKey { get; init; } + public string? Variant { get; init; } +} + +public static class EmployeeStatCardCatalog +{ + private static readonly Dictionary Cards = new() + { + ["active-employees"] = new EmployeeStatCardData + { + Key = "active-employees", + Value = "4", + LabelKey = "employees.stats.activeEmployees", + Variant = "teal" + }, + ["pending-invitations"] = new EmployeeStatCardData + { + Key = "pending-invitations", + Value = "1", + LabelKey = "employees.stats.pendingInvitations", + Variant = "amber" + }, + ["roles-defined"] = new EmployeeStatCardData + { + Key = "roles-defined", + Value = "4", + LabelKey = "employees.stats.rolesDefined", + Variant = "purple" + } + }; + + public static EmployeeStatCardViewModel Get(string key, ILocalizationService localization) + { + if (!Cards.TryGetValue(key, out var card)) + throw new KeyNotFoundException($"EmployeeStatCard with key '{key}' not found"); + + return new EmployeeStatCardViewModel + { + Key = card.Key, + Value = card.Value, + Label = localization.Get(card.LabelKey), + Variant = card.Variant + }; + } + + public static IEnumerable AllKeys => Cards.Keys; +} diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeTable/Default.cshtml b/PlanTempus.Application/Features/Employees/Components/EmployeeTable/Default.cshtml new file mode 100644 index 0000000..b0fbdaf --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeTable/Default.cshtml @@ -0,0 +1,34 @@ +@model PlanTempus.Application.Features.Employees.Components.EmployeeTableViewModel + + + + @Model.CurrentCount af @Model.MaxCount @Model.CountLabel + + + + + + + @Model.InviteButtonText + + + + + + + + @Model.ColumnUser + @Model.ColumnRole + @Model.ColumnStatus + @Model.ColumnLastActive + + + + + @foreach (var employeeKey in Model.EmployeeKeys) + { + @await Component.InvokeAsync("EmployeeRow", employeeKey) + } + + + diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeTable/EmployeeTableViewComponent.cs b/PlanTempus.Application/Features/Employees/Components/EmployeeTable/EmployeeTableViewComponent.cs new file mode 100644 index 0000000..1cd8923 --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeTable/EmployeeTableViewComponent.cs @@ -0,0 +1,77 @@ +using Microsoft.AspNetCore.Mvc; +using PlanTempus.Application.Features.Localization.Services; + +namespace PlanTempus.Application.Features.Employees.Components; + +public class EmployeeTableViewComponent : ViewComponent +{ + private readonly ILocalizationService _localization; + + public EmployeeTableViewComponent(ILocalizationService localization) + { + _localization = localization; + } + + public IViewComponentResult Invoke(string key) + { + var model = EmployeeTableCatalog.Get(key, _localization); + return View(model); + } +} + +public class EmployeeTableViewModel +{ + public required string Key { get; init; } + public required int CurrentCount { get; init; } + public required int MaxCount { get; init; } + public required string CountLabel { get; init; } + public required string InviteButtonText { get; init; } + public required string ColumnUser { get; init; } + public required string ColumnRole { get; init; } + public required string ColumnStatus { get; init; } + public required string ColumnLastActive { get; init; } + public required IReadOnlyList EmployeeKeys { get; init; } + public double ProgressPercent => MaxCount > 0 ? (double)CurrentCount / MaxCount * 100 : 0; +} + +internal class EmployeeTableData +{ + public required string Key { get; init; } + public required int CurrentCount { get; init; } + public required int MaxCount { get; init; } + public required IReadOnlyList EmployeeKeys { get; init; } +} + +public static class EmployeeTableCatalog +{ + private static readonly Dictionary Tables = new() + { + ["all-employees"] = new EmployeeTableData + { + Key = "all-employees", + CurrentCount = 5, + MaxCount = 8, + EmployeeKeys = ["employee-1", "employee-2", "employee-3", "employee-4", "employee-5"] + } + }; + + public static EmployeeTableViewModel Get(string key, ILocalizationService localization) + { + if (!Tables.TryGetValue(key, out var table)) + throw new KeyNotFoundException($"EmployeeTable with key '{key}' not found"); + + return new EmployeeTableViewModel + { + Key = table.Key, + CurrentCount = table.CurrentCount, + MaxCount = table.MaxCount, + CountLabel = localization.Get("employees.users.count"), + InviteButtonText = localization.Get("employees.users.inviteUser"), + ColumnUser = localization.Get("employees.users.columns.user"), + ColumnRole = localization.Get("employees.users.columns.role"), + ColumnStatus = localization.Get("employees.users.columns.status"), + ColumnLastActive = localization.Get("employees.users.columns.lastActive"), + EmployeeKeys = table.EmployeeKeys + }; + } +} diff --git a/PlanTempus.Application/Features/Employees/Components/PermissionsMatrix/Default.cshtml b/PlanTempus.Application/Features/Employees/Components/PermissionsMatrix/Default.cshtml new file mode 100644 index 0000000..87cf3d4 --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/PermissionsMatrix/Default.cshtml @@ -0,0 +1,41 @@ +@model PlanTempus.Application.Features.Employees.Components.PermissionsMatrixViewModel + + + + + + + @foreach (var role in Model.Roles) + { + + } + + + + @foreach (var permission in Model.Permissions) + { + + + @foreach (var role in Model.Roles) + { + + } + + } + +
Rettighed@role.Name
+ + + @permission.Name + + + @if (permission.RoleAccess.TryGetValue(role.Key, out var hasAccess) && hasAccess) + { + + } + else + { + + } +
+
diff --git a/PlanTempus.Application/Features/Employees/Components/PermissionsMatrix/PermissionsMatrixViewComponent.cs b/PlanTempus.Application/Features/Employees/Components/PermissionsMatrix/PermissionsMatrixViewComponent.cs new file mode 100644 index 0000000..3d03718 --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Components/PermissionsMatrix/PermissionsMatrixViewComponent.cs @@ -0,0 +1,158 @@ +using Microsoft.AspNetCore.Mvc; +using PlanTempus.Application.Features.Localization.Services; + +namespace PlanTempus.Application.Features.Employees.Components; + +public class PermissionsMatrixViewComponent : ViewComponent +{ + private readonly ILocalizationService _localization; + + public PermissionsMatrixViewComponent(ILocalizationService localization) + { + _localization = localization; + } + + public IViewComponentResult Invoke(string key) + { + var model = PermissionsMatrixCatalog.Get(key, _localization); + return View(model); + } +} + +public class PermissionsMatrixViewModel +{ + public required string Key { get; init; } + public required IReadOnlyList Roles { get; init; } + public required IReadOnlyList Permissions { get; init; } +} + +public class RoleHeader +{ + public required string Key { get; init; } + public required string Name { get; init; } +} + +public class PermissionRow +{ + public required string Key { get; init; } + public required string Name { get; init; } + public required string Icon { get; init; } + public required IReadOnlyDictionary RoleAccess { get; init; } +} + +internal class PermissionsMatrixData +{ + public required string Key { get; init; } + public required IReadOnlyList RoleKeys { get; init; } + public required IReadOnlyList Permissions { get; init; } +} + +internal class PermissionData +{ + public required string Key { get; init; } + public required string NameKey { get; init; } + public required string Icon { get; init; } + public required IReadOnlyDictionary RoleAccess { get; init; } +} + +public static class PermissionsMatrixCatalog +{ + private static readonly Dictionary RoleNameKeys = new() + { + ["owner"] = "employees.roles.owner", + ["admin"] = "employees.roles.admin", + ["leader"] = "employees.roles.leader", + ["employee"] = "employees.roles.employee" + }; + + private static readonly Dictionary Matrices = new() + { + ["default"] = new PermissionsMatrixData + { + Key = "default", + RoleKeys = ["owner", "admin", "leader", "employee"], + Permissions = + [ + new PermissionData + { + Key = "calendar", + NameKey = "employees.permissions.calendar", + Icon = "ph-calendar", + RoleAccess = new Dictionary + { + ["owner"] = true, + ["admin"] = true, + ["leader"] = true, + ["employee"] = true + } + }, + new PermissionData + { + Key = "employees", + NameKey = "employees.permissions.employees", + Icon = "ph-users", + RoleAccess = new Dictionary + { + ["owner"] = true, + ["admin"] = true, + ["leader"] = true, + ["employee"] = false + } + }, + new PermissionData + { + Key = "customers", + NameKey = "employees.permissions.customers", + Icon = "ph-address-book", + RoleAccess = new Dictionary + { + ["owner"] = true, + ["admin"] = true, + ["leader"] = true, + ["employee"] = true + } + }, + new PermissionData + { + Key = "reports", + NameKey = "employees.permissions.reports", + Icon = "ph-chart-bar", + RoleAccess = new Dictionary + { + ["owner"] = true, + ["admin"] = true, + ["leader"] = false, + ["employee"] = false + } + } + ] + } + }; + + public static PermissionsMatrixViewModel Get(string key, ILocalizationService localization) + { + if (!Matrices.TryGetValue(key, out var matrix)) + throw new KeyNotFoundException($"PermissionsMatrix with key '{key}' not found"); + + var roles = matrix.RoleKeys.Select(roleKey => new RoleHeader + { + Key = roleKey, + Name = localization.Get(RoleNameKeys[roleKey]) + }).ToList(); + + var permissions = matrix.Permissions.Select(p => new PermissionRow + { + Key = p.Key, + Name = localization.Get(p.NameKey), + Icon = p.Icon, + RoleAccess = p.RoleAccess + }).ToList(); + + return new PermissionsMatrixViewModel + { + Key = matrix.Key, + Roles = roles, + Permissions = permissions + }; + } +} diff --git a/PlanTempus.Application/Features/Employees/Pages/Index.cshtml b/PlanTempus.Application/Features/Employees/Pages/Index.cshtml new file mode 100644 index 0000000..5481d20 --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Pages/Index.cshtml @@ -0,0 +1,57 @@ +@page "/medarbejdere" +@using PlanTempus.Application.Features.Employees.Components +@model PlanTempus.Application.Features.Employees.Pages.IndexModel +@{ + ViewData["Title"] = "Medarbejdere"; +} + + + + + + + + + +

Medarbejdere

+

Administrer brugere, roller og rettigheder

+
+
+ + + @await Component.InvokeAsync("EmployeeStatCard", "active-employees") + @await Component.InvokeAsync("EmployeeStatCard", "pending-invitations") + @await Component.InvokeAsync("EmployeeStatCard", "roles-defined") + +
+ + + + + + Brugere + + + + Roller + + +
+ + + + + @await Component.InvokeAsync("EmployeeTable", "all-employees") + + + + + + + @await Component.InvokeAsync("PermissionsMatrix", "default") + + +
+ + +@await Component.InvokeAsync("EmployeeDetailView", "employee-1") diff --git a/PlanTempus.Application/Features/Employees/Pages/Index.cshtml.cs b/PlanTempus.Application/Features/Employees/Pages/Index.cshtml.cs new file mode 100644 index 0000000..6c7004b --- /dev/null +++ b/PlanTempus.Application/Features/Employees/Pages/Index.cshtml.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace PlanTempus.Application.Features.Employees.Pages; + +public class IndexModel : PageModel +{ + public void OnGet() + { + } +} diff --git a/PlanTempus.Application/Features/Localization/Translations/da.json b/PlanTempus.Application/Features/Localization/Translations/da.json index 2589a9c..53bfaf5 100644 --- a/PlanTempus.Application/Features/Localization/Translations/da.json +++ b/PlanTempus.Application/Features/Localization/Translations/da.json @@ -29,7 +29,9 @@ "to": "Til", "all": "Alle", "reset": "Nulstil", - "status": "Status" + "status": "Status", + "yes": "Ja", + "no": "Nej" }, "sidebar": { "lockScreen": "Lås skærm", @@ -216,5 +218,144 @@ "pending": "Afventer", "overdue": "Forfalden" } + }, + "employees": { + "title": "Medarbejdere", + "subtitle": "Administrer brugere, roller og rettigheder", + "stats": { + "activeEmployees": "Aktive medarbejdere", + "pendingInvitations": "Afventer invitation", + "rolesDefined": "Roller defineret" + }, + "tabs": { + "users": "Brugere", + "roles": "Roller" + }, + "users": { + "count": "brugere", + "inviteUser": "Inviter bruger", + "columns": { + "user": "Bruger", + "role": "Rolle", + "status": "Status", + "lastActive": "Sidst aktiv" + } + }, + "roles": { + "owner": "Ejer", + "admin": "Admin", + "leader": "Leder", + "employee": "Medarbejder" + }, + "status": { + "active": "Aktiv", + "invited": "Invitation sendt" + }, + "permissions": { + "title": "Rettighed", + "calendar": "Kalender", + "employees": "Medarbejdere", + "customers": "Kunder", + "reports": "Rapporter & Økonomi" + }, + "actions": { + "edit": "Rediger", + "remove": "Fjern bruger", + "resend": "Send invitation igen", + "cancel": "Annuller invitation" + }, + "detail": { + "title": "Medarbejderdetaljer", + "back": "Tilbage til medarbejdere", + "save": "Gem ændringer", + "tabs": { + "general": "Generelt", + "hours": "Arbejdstid", + "services": "Services", + "salary": "Løn", + "hr": "HR", + "stats": "Statistik" + }, + "contact": "Kontaktoplysninger", + "personal": "Personlige oplysninger", + "employment": "Ansættelse", + "fullname": "Fulde navn", + "email": "E-mail", + "phone": "Telefon", + "address": "Adresse", + "postalcity": "Postnr. & By", + "birthdate": "Fødselsdato", + "emergencycontact": "Nødkontakt", + "emergencyphone": "Nødkontakt tlf.", + "employmentdate": "Ansættelsesdato", + "position": "Stilling", + "employmenttype": "Ansættelsestype", + "hoursperweek": "Timer/uge", + "bookings": "bookinger i år", + "revenue": "omsætning i år", + "rating": "rating", + "employedsince": "ansat siden", + "hours": { + "weekly": "Ugentlig arbejdstid", + "monday": "Mandag", + "tuesday": "Tirsdag", + "wednesday": "Onsdag", + "thursday": "Torsdag", + "friday": "Fredag", + "saturday": "Lørdag", + "sunday": "Søndag" + }, + "services": { + "assigned": "Tildelte services" + }, + "salary": { + "paymentinfo": "Betalingsoplysninger", + "bankaccount": "Bankkonto", + "taxcard": "Skattekort", + "settings": "Lønindstillinger", + "hourlyrate": "Timesats", + "monthlyfixed": "Fast månedsløn", + "commission": "Provision (services)", + "productcommission": "Provision (produkter)" + }, + "hr": { + "documents": "Dokumenter", + "contract": "Ansættelseskontrakt", + "vacation": "Ferie", + "sickleave": "Sygefravær", + "notes": "Noter" + }, + "stats": { + "performance": "Performance", + "bookingsyear": "Bookinger i år", + "revenueyear": "Omsætning i år", + "avgrating": "Gns. rating", + "occupancy": "Belægningsgrad" + }, + "settings": { + "label": "Indstillinger", + "showinbooking": { + "label": "Vis i online booking", + "desc": "Kunder kan vælge denne medarbejder" + }, + "smsreminders": { + "label": "Modtag SMS-påmindelser", + "desc": "Få besked om nye bookinger" + }, + "editcalendar": { + "label": "Kan redigere egen kalender", + "desc": "Tillad ændringer i egne bookinger" + } + }, + "notifications": { + "label": "Notifikationer", + "intro": "Vælg hvilke email-notifikationer medarbejderen skal modtage.", + "onlinebooking": "Modtag email ved online booking", + "manualbooking": "Modtag email ved manuel booking", + "cancellation": "Modtag email ved aflysning", + "waitlist": "Modtag email ved opskrivning til venteliste", + "dailysummary": "Modtag daglig oversigt over morgendagens bookinger" + } + } } } diff --git a/PlanTempus.Application/Features/Localization/Translations/en.json b/PlanTempus.Application/Features/Localization/Translations/en.json index 9738bcc..fa41c98 100644 --- a/PlanTempus.Application/Features/Localization/Translations/en.json +++ b/PlanTempus.Application/Features/Localization/Translations/en.json @@ -29,7 +29,9 @@ "to": "To", "all": "All", "reset": "Reset", - "status": "Status" + "status": "Status", + "yes": "Yes", + "no": "No" }, "sidebar": { "lockScreen": "Lock screen", @@ -216,5 +218,107 @@ "pending": "Pending", "overdue": "Overdue" } + }, + "employees": { + "title": "Employees", + "subtitle": "Manage users, roles and permissions", + "stats": { + "activeEmployees": "Active employees", + "pendingInvitations": "Pending invitations", + "rolesDefined": "Roles defined" + }, + "tabs": { + "users": "Users", + "roles": "Roles" + }, + "users": { + "count": "users", + "inviteUser": "Invite user", + "columns": { + "user": "User", + "role": "Role", + "status": "Status", + "lastActive": "Last active" + } + }, + "roles": { + "owner": "Owner", + "admin": "Admin", + "leader": "Manager", + "employee": "Employee" + }, + "status": { + "active": "Active", + "invited": "Invitation sent" + }, + "permissions": { + "title": "Permission", + "calendar": "Calendar", + "employees": "Employees", + "customers": "Customers", + "reports": "Reports & Finance" + }, + "actions": { + "edit": "Edit", + "remove": "Remove user", + "resend": "Resend invitation", + "cancel": "Cancel invitation" + }, + "detail": { + "title": "Employee details", + "back": "Back to employees", + "save": "Save changes", + "tabs": { + "general": "General", + "hours": "Working hours", + "services": "Services", + "salary": "Salary", + "hr": "HR", + "stats": "Statistics" + }, + "contact": "Contact information", + "personal": "Personal information", + "employment": "Employment", + "fullname": "Full name", + "email": "Email", + "phone": "Phone", + "address": "Address", + "postalcity": "Postal code & City", + "birthdate": "Date of birth", + "emergencycontact": "Emergency contact", + "emergencyphone": "Emergency phone", + "employmentdate": "Employment date", + "position": "Position", + "employmenttype": "Employment type", + "hoursperweek": "Hours/week", + "bookings": "bookings this year", + "revenue": "revenue this year", + "rating": "rating", + "employedsince": "employed since", + "settings": { + "label": "Settings", + "showinbooking": { + "label": "Show in online booking", + "desc": "Customers can select this employee" + }, + "smsreminders": { + "label": "Receive SMS reminders", + "desc": "Get notified about new bookings" + }, + "editcalendar": { + "label": "Can edit own calendar", + "desc": "Allow changes to own bookings" + } + }, + "notifications": { + "label": "Notifications", + "intro": "Choose which email notifications the employee should receive.", + "onlinebooking": "Receive email for online booking", + "manualbooking": "Receive email for manual booking", + "cancellation": "Receive email for cancellation", + "waitlist": "Receive email for waitlist signup", + "dailysummary": "Receive daily summary of tomorrow's bookings" + } + } } } diff --git a/PlanTempus.Application/Features/Menu/Services/MockMenuService.cs b/PlanTempus.Application/Features/Menu/Services/MockMenuService.cs index a728e0b..2e3d90f 100644 --- a/PlanTempus.Application/Features/Menu/Services/MockMenuService.cs +++ b/PlanTempus.Application/Features/Menu/Services/MockMenuService.cs @@ -126,8 +126,8 @@ public class MockMenuService : IMenuService { Id = "employees", Label = "Medarbejdere", - Icon = "ph-user", - Url = "/poc-medarbejdere.html", + Icon = "ph-users-three", + Url = "/medarbejdere", MinimumRole = UserRole.Manager, SortOrder = 4 } diff --git a/PlanTempus.Application/Features/_Shared/Pages/_Layout.cshtml b/PlanTempus.Application/Features/_Shared/Pages/_Layout.cshtml index 7ce5429..75a0e00 100644 --- a/PlanTempus.Application/Features/_Shared/Pages/_Layout.cshtml +++ b/PlanTempus.Application/Features/_Shared/Pages/_Layout.cshtml @@ -24,9 +24,11 @@ + + @await RenderSectionAsync("Styles", required: false) diff --git a/PlanTempus.Application/analyze-css.js b/PlanTempus.Application/analyze-css.js new file mode 100644 index 0000000..4112a25 --- /dev/null +++ b/PlanTempus.Application/analyze-css.js @@ -0,0 +1,439 @@ +import { PurgeCSS } from 'purgecss'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Create reports directory if it doesn't exist +const reportsDir = './reports'; +if (!fs.existsSync(reportsDir)) { + fs.mkdirSync(reportsDir); +} + +console.log('🔍 Starting CSS Analysis for PlanTempus...\n'); + +// 1. Run PurgeCSS to find unused CSS +console.log('📊 Running PurgeCSS analysis...'); +async function runPurgeCSS() { + const purgeCSSResults = await new PurgeCSS().purge({ + content: [ + './Features/**/*.cshtml', + './wwwroot/ts/**/*.ts' + ], + css: [ + './wwwroot/css/*.css' + ], + rejected: true, + rejectedCss: true, + safelist: { + standard: [ + /^swp-/, // All custom web components + /^ph-/, // Phosphor icons + 'active', // Tab states + 'checked', // Checkbox states + 'collapsed', + 'expanded', + 'hidden', + 'has-demo-banner', + /^owner$/, // Role badges + /^admin$/, + /^leader$/, + /^employee$/, + /^purple$/, // Avatar colors + /^blue$/, + /^amber$/, + /^teal$/, + /^master$/, // Employee tags + /^senior$/, + /^junior$/, + /^cert$/, + /^draft$/, // Status badges + /^approved$/, + /^invited$/, + /^danger$/, // Button variants + /^primary$/, + /^secondary$/ + ] + } + }); + + // Calculate statistics + let totalOriginalSize = 0; + let totalPurgedSize = 0; + let totalRejected = 0; + const rejectedByFile = {}; + + purgeCSSResults.forEach(result => { + const fileName = path.basename(result.file); + const originalSize = result.css.length + (result.rejected ? result.rejected.join('').length : 0); + const purgedSize = result.css.length; + const rejectedSize = result.rejected ? result.rejected.length : 0; + + totalOriginalSize += originalSize; + totalPurgedSize += purgedSize; + totalRejected += rejectedSize; + + rejectedByFile[fileName] = { + originalSize, + purgedSize, + rejectedCount: rejectedSize, + rejected: result.rejected || [] + }; + }); + + const report = { + summary: { + totalFiles: purgeCSSResults.length, + totalOriginalSize, + totalPurgedSize, + totalRejected, + percentageRemoved: ((totalRejected / (totalOriginalSize || 1)) * 100).toFixed(2) + '%', + potentialSavings: totalOriginalSize - totalPurgedSize + }, + fileDetails: rejectedByFile + }; + + fs.writeFileSync( + path.join(reportsDir, 'purgecss-report.json'), + JSON.stringify(report, null, 2) + ); + + console.log('✅ PurgeCSS analysis complete'); + console.log(` - Total CSS rules analyzed: ${totalOriginalSize}`); + console.log(` - Unused CSS rules found: ${totalRejected}`); + console.log(` - Potential removal: ${report.summary.percentageRemoved}`); + + return report; +} + +// 2. Analyze CSS with basic stats +console.log('\n📊 Running CSS Stats analysis...'); +function runCSSStats() { + const cssDir = './wwwroot/css'; + const cssFiles = fs.readdirSync(cssDir) + .filter(file => file.endsWith('.css')) + .map(file => path.join(cssDir, file)); + + const stats = {}; + + cssFiles.forEach(file => { + if (fs.existsSync(file)) { + const fileName = path.basename(file); + const content = fs.readFileSync(file, 'utf8'); + + // Basic statistics + const lines = content.split('\n').length; + const size = Buffer.byteLength(content, 'utf8'); + const rules = (content.match(/\{[^}]*\}/g) || []).length; + const selectors = (content.match(/[^{]+(?=\{)/g) || []).length; + const properties = (content.match(/[^:]+:[^;]+;/g) || []).length; + const colors = [...new Set(content.match(/#[0-9a-fA-F]{3,6}|rgba?\([^)]+\)|hsla?\([^)]+\)|var\(--color-[^)]+\)/g) || [])]; + const mediaQueries = (content.match(/@media[^{]+/g) || []).length; + const cssVariables = [...new Set(content.match(/var\(--[^)]+\)/g) || [])]; + + stats[fileName] = { + lines, + size: `${(size / 1024).toFixed(2)} KB`, + sizeBytes: size, + rules, + selectors, + properties, + uniqueColors: colors.length, + colors: colors.slice(0, 10), // First 10 colors + mediaQueries, + cssVariables: cssVariables.length + }; + } + }); + + fs.writeFileSync( + path.join(reportsDir, 'css-stats.json'), + JSON.stringify(stats, null, 2) + ); + + console.log('✅ CSS Stats analysis complete'); + console.log(` - Files analyzed: ${Object.keys(stats).length}`); + + return stats; +} + +// 3. Generate HTML report +function generateHTMLReport(purgeReport, statsReport) { + const totalSize = Object.values(statsReport).reduce((sum, stat) => sum + stat.sizeBytes, 0); + const totalSizeKB = (totalSize / 1024).toFixed(2); + const totalLines = Object.values(statsReport).reduce((sum, stat) => sum + stat.lines, 0); + + const html = ` + + + + + + CSS Analysis Report - PlanTempus + + + +
+

📊 CSS Analysis Report

+

PlanTempus - Production CSS Analysis

+ +
+
+
Total CSS Size
+
${totalSizeKB} KB
+
+
+
CSS Files
+
${purgeReport.summary.totalFiles}
+
+
+
Total Lines
+
${totalLines.toLocaleString()}
+
+
+
Unused CSS Rules
+
${purgeReport.summary.totalRejected}
+
+
+ +
+

📈 CSS Statistics by File

+ + + + + + + + + + + + + + ${Object.entries(statsReport) + .sort((a, b) => b[1].sizeBytes - a[1].sizeBytes) + .map(([file, stats]) => ` + + + + + + + + + + `).join('')} + +
FileSizeLinesRulesSelectorsPropertiesCSS Vars
${file}${stats.size}${stats.lines}${stats.rules}${stats.selectors}${stats.properties}${stats.cssVariables}
+
+ +
+

🗑️ Unused CSS by File

+ ${Object.entries(purgeReport.fileDetails) + .sort((a, b) => b[1].rejectedCount - a[1].rejectedCount) + .map(([file, details]) => ` +
+

${file}

+

+ + ${details.rejectedCount} unused rules + + + Original: ${details.originalSize} | After purge: ${details.purgedSize} + +

+ ${details.rejectedCount > 0 ? ` +
+ Show unused selectors +
+ ${details.rejected.slice(0, 50).join('
')} + ${details.rejected.length > 50 ? `
... and ${details.rejected.length - 50} more` : ''} +
+
+ ` : '

✅ No unused CSS found!

'} +
+ `).join('')} +
+ +
+

💡 Recommendations

+
    + ${purgeReport.summary.totalRejected > 100 ? + '
  • ⚠️ High number of unused CSS rules detected. Consider removing unused styles to improve performance.
  • ' : + '
  • ✅ CSS usage is relatively clean.
  • '} + ${Object.values(purgeReport.fileDetails).some(d => d.rejectedCount > 50) ? + '
  • ⚠️ Some files have significant unused CSS. Review these files for optimization opportunities.
  • ' : ''} +
  • 📦 Consider consolidating similar styles to reduce duplication.
  • +
  • 🎨 Review color usage - ensure all colors use CSS variables from design-tokens.css.
  • +
  • 📋 Reference COMPONENT-CATALOG.md when adding new components to avoid duplication.
  • +
+
+ +

Report generated: ${new Date().toLocaleString('da-DK')}

+
+ + + `; + + fs.writeFileSync(path.join(reportsDir, 'css-analysis-report.html'), html); + console.log('\n✅ HTML report generated: reports/css-analysis-report.html'); +} + +// Run all analyses +(async () => { + try { + const purgeReport = await runPurgeCSS(); + const statsReport = runCSSStats(); + generateHTMLReport(purgeReport, statsReport); + + console.log('\n🎉 CSS Analysis Complete!'); + console.log('📄 Reports generated in ./reports/ directory'); + console.log(' - purgecss-report.json (detailed unused CSS data)'); + console.log(' - css-stats.json (CSS statistics)'); + console.log(' - css-analysis-report.html (visual report)'); + console.log('\n💡 Open reports/css-analysis-report.html in your browser to view the full report'); + } catch (error) { + console.error('❌ Error during analysis:', error); + process.exit(1); + } +})(); diff --git a/PlanTempus.Application/package-lock.json b/PlanTempus.Application/package-lock.json index 1b57bcb..e66fc28 100644 --- a/PlanTempus.Application/package-lock.json +++ b/PlanTempus.Application/package-lock.json @@ -5,7 +5,8 @@ "packages": { "": { "devDependencies": { - "esbuild": "^0.27.2" + "esbuild": "^0.27.2", + "purgecss": "^6.0.0" } }, "node_modules/@esbuild/aix-ppc64": { @@ -450,6 +451,150 @@ "node": ">=18" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", @@ -491,6 +636,500 @@ "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/purgecss": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-6.0.0.tgz", + "integrity": "sha512-s3EBxg5RSWmpqd0KGzNqPiaBbWDz1/As+2MzoYVGMqgDqRTLBhJW6sywfTBek7OwNfoS/6pS0xdtvChNhFj2cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^12.0.0", + "glob": "^10.3.10", + "postcss": "^8.4.4", + "postcss-selector-parser": "^6.0.7" + }, + "bin": { + "purgecss": "bin/purgecss.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } } } } diff --git a/PlanTempus.Application/package.json b/PlanTempus.Application/package.json index d4bea98..20ba044 100644 --- a/PlanTempus.Application/package.json +++ b/PlanTempus.Application/package.json @@ -1,5 +1,10 @@ { + "type": "module", "devDependencies": { - "esbuild": "^0.27.2" + "esbuild": "^0.27.2", + "purgecss": "^6.0.0" + }, + "scripts": { + "analyze-css": "node analyze-css.js" } } diff --git a/PlanTempus.Application/reports/css-analysis-report.html b/PlanTempus.Application/reports/css-analysis-report.html new file mode 100644 index 0000000..c2e18ad --- /dev/null +++ b/PlanTempus.Application/reports/css-analysis-report.html @@ -0,0 +1,776 @@ + + + + + + + CSS Analysis Report - PlanTempus + + + +
+

📊 CSS Analysis Report

+

PlanTempus - Production CSS Analysis

+ +
+
+
Total CSS Size
+
132.81 KB
+
+
+
CSS Files
+
21
+
+
+
Total Lines
+
6.033
+
+
+
Unused CSS Rules
+
61
+
+
+ +
+

📈 CSS Statistics by File

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileSizeLinesRulesSelectorsPropertiesCSS Vars
auth.css23.66 KB114416917357146
cash.css19.38 KB89813213541542
employees.css15.55 KB72210510834537
design-tokens.css9.02 KB318353619225
account.css8.83 KB402606317331
drawers.css6.36 KB297383815932
page.css6.00 KB276384011735
sidebar.css5.72 KB247303011924
waitlist.css5.55 KB251303013131
stats.css5.18 KB23230327827
bookings.css4.27 KB17628287527
topbar.css3.79 KB181191910320
controls.css3.32 KB14919197920
demo-banner.css2.94 KB1461921669
attentions.css2.92 KB11515154515
design-system.css2.30 KB10520203720
tabs.css2.13 KB9511114219
base.css2.06 KB1191515478
notifications.css1.67 KB7088278
app-layout.css1.28 KB5155186
quick-stats.css0.88 KB39441511
+
+ +
+

🗑️ Unused CSS by File

+ +
+

design-tokens.css

+

+ + 24 unused rules + + + Original: 7917 | After purge: 7639 + +

+ +
+ Show unused selectors +
+ .is-red
.is-pink
.is-magenta
.is-purple
.is-violet
.is-deep-purple
.is-indigo
.is-blue
.is-light-blue
.is-cyan
.is-teal
.is-green
.is-light-green
.is-lime
.is-yellow
.is-amber
.is-orange
.is-deep-orange
.status-confirmed
.status-pending
.status-inprogress
.status-error
.status-active
.status-inactive + +
+
+ +
+ +
+

design-system.css

+

+ + 11 unused rules + + + Original: 2095 | After purge: 2056 + +

+ +
+ Show unused selectors +
+ h2
h3
h4
h5
h6
h2
h3
h4
h5
h6
:focus-visible + +
+
+ +
+ +
+

bookings.css

+

+ + 6 unused rules + + + Original: 3924 | After purge: 3756 + +

+ +
+ Show unused selectors +
+ swp-booking-item.inprogress
swp-booking-indicator.green
swp-booking-status.confirmed
swp-booking-status.pending
swp-booking-status.inprogress
+swp-booking-status.in-progress + +
+
+ +
+ +
+

attentions.css

+

+ + 5 unused rules + + + Original: 2524 | After purge: 2359 + +

+ +
+ Show unused selectors +
+ swp-attention-item.urgent
swp-attention-item.urgent:hover
swp-attention-item.info
swp-attention-item.urgent swp-attention-icon
swp-attention-item.info swp-attention-icon + +
+
+ +
+ +
+

base.css

+

+ + 4 unused rules + + + Original: 1952 | After purge: 1930 + +

+ +
+ Show unused selectors +
+ ul
ol
img
:focus-visible + +
+
+ +
+ +
+

auth.css

+

+ + 4 unused rules + + + Original: 23887 | After purge: 23816 + +

+ +
+ Show unused selectors +
+ swp-btn.social
swp-btn.social:hover
swp-btn.social img
swp-plan-badge.free + +
+
+ +
+ +
+

stats.css

+

+ + 3 unused rules + + + Original: 5298 | After purge: 5229 + +

+ +
+ Show unused selectors +
+ swp-stat-trend.up
swp-stat-trend.down
+swp-stat-card.red swp-stat-value + +
+
+ +
+ +
+

account.css

+

+ + 2 unused rules + + + Original: 8829 | After purge: 8777 + +

+ +
+ Show unused selectors +
+ swp-invoice-status.pending
swp-invoice-status.overdue + +
+
+ +
+ +
+

drawers.css

+

+ + 1 unused rules + + + Original: 6485 | After purge: 6467 + +

+ +
+ Show unused selectors +
+ [data-drawer="xl"] + +
+
+ +
+ +
+

cash.css

+

+ + 1 unused rules + + + Original: 19816 | After purge: 19778 + +

+ +
+ Show unused selectors +
+ swp-cash-stat.user swp-cash-stat-value + +
+
+ +
+ +
+

waitlist.css

+

+ + 0 unused rules + + + Original: 5686 | After purge: 5686 + +

+

✅ No unused CSS found!

+
+ +
+

topbar.css

+

+ + 0 unused rules + + + Original: 3885 | After purge: 3885 + +

+

✅ No unused CSS found!

+
+ +
+

tabs.css

+

+ + 0 unused rules + + + Original: 2183 | After purge: 2183 + +

+

✅ No unused CSS found!

+
+ +
+

sidebar.css

+

+ + 0 unused rules + + + Original: 5859 | After purge: 5859 + +

+

✅ No unused CSS found!

+
+ +
+

quick-stats.css

+

+ + 0 unused rules + + + Original: 899 | After purge: 899 + +

+

✅ No unused CSS found!

+
+ +
+

page.css

+

+ + 0 unused rules + + + Original: 6140 | After purge: 6140 + +

+

✅ No unused CSS found!

+
+ +
+

notifications.css

+

+ + 0 unused rules + + + Original: 1712 | After purge: 1712 + +

+

✅ No unused CSS found!

+
+ +
+

employees.css

+

+ + 0 unused rules + + + Original: 15923 | After purge: 15923 + +

+

✅ No unused CSS found!

+
+ +
+

demo-banner.css

+

+ + 0 unused rules + + + Original: 3014 | After purge: 3014 + +

+

✅ No unused CSS found!

+
+ +
+

controls.css

+

+ + 0 unused rules + + + Original: 3397 | After purge: 3397 + +

+

✅ No unused CSS found!

+
+ +
+

app-layout.css

+

+ + 0 unused rules + + + Original: 1306 | After purge: 1306 + +

+

✅ No unused CSS found!

+
+ +
+ +
+

💡 Recommendations

+
    +
  • ✅ CSS usage is relatively clean.
  • + +
  • 📦 Consider consolidating similar styles to reduce duplication.
  • +
  • 🎨 Review color usage - ensure all colors use CSS variables from design-tokens.css.
  • +
  • 📋 Reference COMPONENT-CATALOG.md when adding new components to avoid duplication.
  • +
+
+ +

Report generated: 12.1.2026, 21.57.11

+
+ + + \ No newline at end of file diff --git a/PlanTempus.Application/reports/css-stats.json b/PlanTempus.Application/reports/css-stats.json new file mode 100644 index 0000000..023f2b1 --- /dev/null +++ b/PlanTempus.Application/reports/css-stats.json @@ -0,0 +1,437 @@ +{ + "account.css": { + "lines": 402, + "size": "8.83 KB", + "sizeBytes": 9037, + "rules": 60, + "selectors": 63, + "properties": 173, + "uniqueColors": 13, + "colors": [ + "var(--color-text)", + "var(--color-teal)", + "var(--color-background-alt)", + "var(--color-text-secondary)", + "var(--color-surface)", + "var(--color-purple)", + "var(--color-amber)", + "rgba(0, 0, 0, 0.08)", + "var(--color-border)", + "var(--color-blue)" + ], + "mediaQueries": 3, + "cssVariables": 31 + }, + "app-layout.css": { + "lines": 51, + "size": "1.28 KB", + "sizeBytes": 1306, + "rules": 5, + "selectors": 5, + "properties": 18, + "uniqueColors": 2, + "colors": [ + "var(--color-background)", + "rgba(0, 0, 0, 0.5)" + ], + "mediaQueries": 0, + "cssVariables": 6 + }, + "attentions.css": { + "lines": 115, + "size": "2.92 KB", + "sizeBytes": 2992, + "rules": 15, + "selectors": 15, + "properties": 45, + "uniqueColors": 8, + "colors": [ + "var(--color-background-alt)", + "var(--color-border)", + "var(--color-background-hover)", + "var(--color-red)", + "var(--color-amber)", + "var(--color-blue)", + "var(--color-text-secondary)", + "var(--color-teal)" + ], + "mediaQueries": 0, + "cssVariables": 15 + }, + "auth.css": { + "lines": 1144, + "size": "23.66 KB", + "sizeBytes": 24232, + "rules": 169, + "selectors": 173, + "properties": 571, + "uniqueColors": 17, + "colors": [ + "var(--color-teal)", + "#00695c", + "rgba(255,255,255,0.1)", + "rgba(255,255,255,0.08)", + "var(--color-background)", + "var(--color-text)", + "var(--color-text-secondary)", + "var(--color-surface)", + "var(--color-border)", + "var(--color-purple)" + ], + "mediaQueries": 3, + "cssVariables": 46 + }, + "base.css": { + "lines": 119, + "size": "2.06 KB", + "sizeBytes": 2105, + "rules": 15, + "selectors": 15, + "properties": 47, + "uniqueColors": 4, + "colors": [ + "var(--color-text)", + "var(--color-background)", + "var(--color-teal)", + "var(--color-teal-light)" + ], + "mediaQueries": 0, + "cssVariables": 8 + }, + "bookings.css": { + "lines": 176, + "size": "4.27 KB", + "sizeBytes": 4369, + "rules": 28, + "selectors": 28, + "properties": 75, + "uniqueColors": 10, + "colors": [ + "var(--color-background-alt)", + "var(--color-background-hover)", + "var(--color-border)", + "var(--color-teal)", + "var(--color-text)", + "var(--color-text-secondary)", + "var(--color-blue)", + "var(--color-purple)", + "var(--color-amber)", + "var(--color-green)" + ], + "mediaQueries": 0, + "cssVariables": 27 + }, + "cash.css": { + "lines": 898, + "size": "19.38 KB", + "sizeBytes": 19850, + "rules": 132, + "selectors": 135, + "properties": 415, + "uniqueColors": 14, + "colors": [ + "var(--color-surface)", + "var(--color-border)", + "var(--color-text-secondary)", + "var(--color-teal)", + "var(--color-background-alt)", + "var(--color-text)", + "var(--color-amber)", + "var(--color-red)", + "var(--color-blue)", + "var(--color-background-hover)" + ], + "mediaQueries": 3, + "cssVariables": 42 + }, + "controls.css": { + "lines": 149, + "size": "3.32 KB", + "sizeBytes": 3397, + "rules": 19, + "selectors": 19, + "properties": 79, + "uniqueColors": 8, + "colors": [ + "var(--color-border)", + "var(--color-text)", + "var(--color-text-secondary)", + "var(--color-background)", + "var(--color-green)", + "var(--color-red)", + "var(--color-background-alt)", + "var(--color-teal)" + ], + "mediaQueries": 0, + "cssVariables": 20 + }, + "demo-banner.css": { + "lines": 146, + "size": "2.94 KB", + "sizeBytes": 3014, + "rules": 19, + "selectors": 21, + "properties": 66, + "uniqueColors": 7, + "colors": [ + "var(--color-teal)", + "#00796b", + "rgba(255, 255, 255, 0.95)", + "#00695c", + "rgba(0, 0, 0, 0.15)", + "rgba(255, 255, 255, 0.15)", + "rgba(255, 255, 255, 0.25)" + ], + "mediaQueries": 2, + "cssVariables": 9 + }, + "design-system.css": { + "lines": 105, + "size": "2.30 KB", + "sizeBytes": 2356, + "rules": 20, + "selectors": 20, + "properties": 37, + "uniqueColors": 7, + "colors": [ + "var(--color-text)", + "var(--color-background)", + "var(--color-text-secondary)", + "var(--color-teal)", + "var(--color-primary)", + "var(--color-border)", + "var(--color-text-muted)" + ], + "mediaQueries": 0, + "cssVariables": 20 + }, + "design-tokens.css": { + "lines": 318, + "size": "9.02 KB", + "sizeBytes": 9235, + "rules": 35, + "selectors": 36, + "properties": 192, + "uniqueColors": 54, + "colors": [ + "#fff", + "#f5f5f5", + "#f0f0f0", + "#fafafa", + "#e0e0e0", + "#333", + "#666", + "#999", + "#1976d2", + "#00897b" + ], + "mediaQueries": 2, + "cssVariables": 25 + }, + "drawers.css": { + "lines": 297, + "size": "6.36 KB", + "sizeBytes": 6513, + "rules": 38, + "selectors": 38, + "properties": 159, + "uniqueColors": 9, + "colors": [ + "var(--color-surface)", + "var(--color-border)", + "var(--color-text)", + "var(--color-text-secondary)", + "var(--color-background-alt)", + "var(--color-background-hover)", + "var(--color-teal)", + "var(--color-background)", + "var(--color-red)" + ], + "mediaQueries": 0, + "cssVariables": 32 + }, + "employees.css": { + "lines": 722, + "size": "15.55 KB", + "sizeBytes": 15923, + "rules": 105, + "selectors": 108, + "properties": 345, + "uniqueColors": 15, + "colors": [ + "var(--color-text-secondary)", + "var(--color-text)", + "var(--color-border)", + "var(--color-teal)", + "var(--color-surface)", + "rgba(0, 0, 0, 0.08)", + "var(--color-background-alt)", + "var(--color-background-hover)", + "var(--color-purple)", + "var(--color-blue)" + ], + "mediaQueries": 3, + "cssVariables": 37 + }, + "notifications.css": { + "lines": 70, + "size": "1.67 KB", + "sizeBytes": 1712, + "rules": 8, + "selectors": 8, + "properties": 27, + "uniqueColors": 4, + "colors": [ + "var(--color-background-alt)", + "var(--color-background-hover)", + "var(--color-teal)", + "var(--color-text-secondary)" + ], + "mediaQueries": 0, + "cssVariables": 8 + }, + "page.css": { + "lines": 276, + "size": "6.00 KB", + "sizeBytes": 6141, + "rules": 38, + "selectors": 40, + "properties": 117, + "uniqueColors": 8, + "colors": [ + "var(--color-surface)", + "var(--color-border)", + "var(--color-text)", + "var(--color-text-secondary)", + "var(--color-teal)", + "var(--color-purple)", + "var(--color-background)", + "var(--color-background-hover)" + ], + "mediaQueries": 2, + "cssVariables": 35 + }, + "quick-stats.css": { + "lines": 39, + "size": "0.88 KB", + "sizeBytes": 899, + "rules": 4, + "selectors": 4, + "properties": 15, + "uniqueColors": 3, + "colors": [ + "var(--color-background-alt)", + "var(--color-text)", + "var(--color-text-secondary)" + ], + "mediaQueries": 0, + "cssVariables": 11 + }, + "sidebar.css": { + "lines": 247, + "size": "5.72 KB", + "sizeBytes": 5859, + "rules": 30, + "selectors": 30, + "properties": 119, + "uniqueColors": 9, + "colors": [ + "var(--color-surface)", + "var(--color-border)", + "var(--color-teal)", + "var(--color-text)", + "var(--color-text-secondary)", + "var(--color-background-hover)", + "var(--color-teal-light)", + "var(--color-amber)", + "var(--color-red)" + ], + "mediaQueries": 0, + "cssVariables": 24 + }, + "stats.css": { + "lines": 232, + "size": "5.18 KB", + "sizeBytes": 5301, + "rules": 30, + "selectors": 32, + "properties": 78, + "uniqueColors": 14, + "colors": [ + "var(--color-surface)", + "var(--color-border)", + "var(--color-text)", + "var(--color-text-secondary)", + "var(--color-text-muted)", + "var(--color-green)", + "var(--color-red)", + "var(--color-teal)", + "var(--color-amber)", + "var(--color-purple)" + ], + "mediaQueries": 2, + "cssVariables": 27 + }, + "tabs.css": { + "lines": 95, + "size": "2.13 KB", + "sizeBytes": 2183, + "rules": 11, + "selectors": 11, + "properties": 42, + "uniqueColors": 6, + "colors": [ + "var(--color-surface)", + "var(--color-border)", + "var(--color-text-secondary)", + "var(--color-text)", + "var(--color-background-alt)", + "var(--color-teal)" + ], + "mediaQueries": 0, + "cssVariables": 19 + }, + "topbar.css": { + "lines": 181, + "size": "3.79 KB", + "sizeBytes": 3885, + "rules": 19, + "selectors": 19, + "properties": 103, + "uniqueColors": 8, + "colors": [ + "var(--color-surface)", + "var(--color-border)", + "var(--color-background)", + "var(--color-teal)", + "var(--color-text-secondary)", + "var(--color-text)", + "var(--color-background-hover)", + "var(--color-red)" + ], + "mediaQueries": 0, + "cssVariables": 20 + }, + "waitlist.css": { + "lines": 251, + "size": "5.55 KB", + "sizeBytes": 5686, + "rules": 30, + "selectors": 30, + "properties": 131, + "uniqueColors": 9, + "colors": [ + "var(--color-surface)", + "var(--color-border)", + "var(--color-teal)", + "var(--color-text-secondary)", + "var(--color-text)", + "var(--color-background-alt)", + "var(--color-background)", + "var(--color-amber)", + "var(--color-background-hover)" + ], + "mediaQueries": 0, + "cssVariables": 31 + } +} \ No newline at end of file diff --git a/PlanTempus.Application/reports/purgecss-report.json b/PlanTempus.Application/reports/purgecss-report.json new file mode 100644 index 0000000..b96102c --- /dev/null +++ b/PlanTempus.Application/reports/purgecss-report.json @@ -0,0 +1,209 @@ +{ + "summary": { + "totalFiles": 21, + "totalOriginalSize": 132731, + "totalPurgedSize": 131811, + "totalRejected": 61, + "percentageRemoved": "0.05%", + "potentialSavings": 920 + }, + "fileDetails": { + "waitlist.css": { + "originalSize": 5686, + "purgedSize": 5686, + "rejectedCount": 0, + "rejected": [] + }, + "topbar.css": { + "originalSize": 3885, + "purgedSize": 3885, + "rejectedCount": 0, + "rejected": [] + }, + "tabs.css": { + "originalSize": 2183, + "purgedSize": 2183, + "rejectedCount": 0, + "rejected": [] + }, + "stats.css": { + "originalSize": 5298, + "purgedSize": 5229, + "rejectedCount": 3, + "rejected": [ + "swp-stat-trend.up", + "swp-stat-trend.down", + "\nswp-stat-card.red swp-stat-value" + ] + }, + "sidebar.css": { + "originalSize": 5859, + "purgedSize": 5859, + "rejectedCount": 0, + "rejected": [] + }, + "quick-stats.css": { + "originalSize": 899, + "purgedSize": 899, + "rejectedCount": 0, + "rejected": [] + }, + "page.css": { + "originalSize": 6140, + "purgedSize": 6140, + "rejectedCount": 0, + "rejected": [] + }, + "notifications.css": { + "originalSize": 1712, + "purgedSize": 1712, + "rejectedCount": 0, + "rejected": [] + }, + "employees.css": { + "originalSize": 15923, + "purgedSize": 15923, + "rejectedCount": 0, + "rejected": [] + }, + "drawers.css": { + "originalSize": 6485, + "purgedSize": 6467, + "rejectedCount": 1, + "rejected": [ + "[data-drawer=\"xl\"]" + ] + }, + "design-tokens.css": { + "originalSize": 7917, + "purgedSize": 7639, + "rejectedCount": 24, + "rejected": [ + ".is-red", + ".is-pink", + ".is-magenta", + ".is-purple", + ".is-violet", + ".is-deep-purple", + ".is-indigo", + ".is-blue", + ".is-light-blue", + ".is-cyan", + ".is-teal", + ".is-green", + ".is-light-green", + ".is-lime", + ".is-yellow", + ".is-amber", + ".is-orange", + ".is-deep-orange", + ".status-confirmed", + ".status-pending", + ".status-inprogress", + ".status-error", + ".status-active", + ".status-inactive" + ] + }, + "design-system.css": { + "originalSize": 2095, + "purgedSize": 2056, + "rejectedCount": 11, + "rejected": [ + " h2", + " h3", + " h4", + " h5", + " h6", + "h2", + "h3", + "h4", + "h5", + "h6", + ":focus-visible" + ] + }, + "demo-banner.css": { + "originalSize": 3014, + "purgedSize": 3014, + "rejectedCount": 0, + "rejected": [] + }, + "controls.css": { + "originalSize": 3397, + "purgedSize": 3397, + "rejectedCount": 0, + "rejected": [] + }, + "cash.css": { + "originalSize": 19816, + "purgedSize": 19778, + "rejectedCount": 1, + "rejected": [ + "swp-cash-stat.user swp-cash-stat-value" + ] + }, + "bookings.css": { + "originalSize": 3924, + "purgedSize": 3756, + "rejectedCount": 6, + "rejected": [ + "swp-booking-item.inprogress", + "swp-booking-indicator.green", + "swp-booking-status.confirmed", + "swp-booking-status.pending", + "swp-booking-status.inprogress", + "\nswp-booking-status.in-progress" + ] + }, + "base.css": { + "originalSize": 1952, + "purgedSize": 1930, + "rejectedCount": 4, + "rejected": [ + "ul", + " ol", + "img", + ":focus-visible" + ] + }, + "auth.css": { + "originalSize": 23887, + "purgedSize": 23816, + "rejectedCount": 4, + "rejected": [ + "swp-btn.social", + "swp-btn.social:hover", + "swp-btn.social img", + "swp-plan-badge.free" + ] + }, + "attentions.css": { + "originalSize": 2524, + "purgedSize": 2359, + "rejectedCount": 5, + "rejected": [ + "swp-attention-item.urgent", + "swp-attention-item.urgent:hover", + "swp-attention-item.info", + "swp-attention-item.urgent swp-attention-icon", + "swp-attention-item.info swp-attention-icon" + ] + }, + "app-layout.css": { + "originalSize": 1306, + "purgedSize": 1306, + "rejectedCount": 0, + "rejected": [] + }, + "account.css": { + "originalSize": 8829, + "purgedSize": 8777, + "rejectedCount": 2, + "rejected": [ + "swp-invoice-status.pending", + "swp-invoice-status.overdue" + ] + } + } +} \ No newline at end of file diff --git a/PlanTempus.Application/wwwroot/css/COMPONENT-CATALOG.md b/PlanTempus.Application/wwwroot/css/COMPONENT-CATALOG.md new file mode 100644 index 0000000..27ee15c --- /dev/null +++ b/PlanTempus.Application/wwwroot/css/COMPONENT-CATALOG.md @@ -0,0 +1,305 @@ +# SWP Design System - Component Catalog + +Reference for alle genbrugelige komponenter. **LAV ALDRIG EN NY KOMPONENT HVIS DEN ALLEREDE EKSISTERER HER.** + +--- + +## Page Structure (page.css) + +| Element | Beskrivelse | Eksempel | +|---------|-------------|----------| +| `swp-page-container` | Hovedcontainer for side | `...` | +| `swp-page-header` | Side header med titel og actions | Flex, space-between | +| `swp-page-title` | Titel-wrapper med h1 og p | h1 + subtitle | +| `swp-page-actions` | Action buttons i header | Flex gap | +| `swp-card` | Standard card wrapper | Border, padding, rounded | +| `swp-card-header` | Card header | Flex, title + action | +| `swp-card-title` | Card titel med ikon | `... Text` | +| `swp-card-content` | Card indhold | Block | + +--- + +## Stats Components (stats.css) + +### Containers + +| Element | Kolonner | Brug | +|---------|----------|------| +| `swp-stats-bar` | 4 kolonner | Dashboard stats | +| `swp-stats-grid` | 4 kolonner | Grid layout | +| `swp-stats-row` | 3 kolonner | Feature pages (Employees, etc.) | + +### Stat Card + +```html + + 42 + Aktive brugere + +``` + +**Varianter (class):** +- `highlight` / `teal` - Teal farve +- `success` - Grøn +- `warning` / `amber` - Amber/orange +- `danger` / `negative` / `red` - Rød +- `purple` - Lilla +- `highlight filled` - Filled teal baggrund + +**VIGTIGT:** `swp-stat-value` bruger `font-family: var(--font-mono)` automatisk! + +--- + +## Tabs (tabs.css) + +```html + + + + Brugere + + + + Roller + + + + + + + + + + +``` + +**VIGTIGT:** +- Aktiv tab: `class="active"` (IKKE data-active="true") +- Tab content: `class="active"` for at vise + +--- + +## Buttons (cash.css) + +```html + + + Tilføj + +``` + +**Varianter:** +- `primary` - Teal baggrund, hvid tekst +- `secondary` - Hvid baggrund, border +- `ghost` - Transparent + +--- + +## Badges (cash.css) + +**ALLE badges bruger `swp-status-badge`** - kun farve og indhold ændres. + +```html +Tekst +``` + +**Varianter:** + +| Class | Farve | Brug | +|-------|-------|------| +| `approved` | Grøn | Godkendt status | +| `active` | Grøn | Aktiv status | +| `draft` | Amber | Kladde status | +| `invited` | Amber | Invitation sendt | +| `owner` | Teal | Ejer rolle | +| `admin` | Purple | Admin rolle | +| `leader` | Blue | Leder rolle | +| `employee` | Grå | Medarbejder rolle | + +Automatisk dot via `::before` pseudo-element. + +--- + +## Tables - Grid + Subgrid Pattern + +### Struktur (ALTID følg dette mønster) + +```html + + + Kolonne 1 + Kolonne 2 + + + + Data 1 + Data 2 + + + +``` + +### CSS Pattern + +```css +swp-[feature]-table { + display: grid; + grid-template-columns: /* definer kolonner her */; +} + +swp-[feature]-table-header, +swp-[feature]-table-body { + display: grid; + grid-column: 1 / -1; + grid-template-columns: subgrid; +} + +swp-[feature]-row { + display: grid; + grid-column: 1 / -1; + grid-template-columns: subgrid; + align-items: center; +} +``` + +### Eksisterende tabeller + +| Feature | Container | Row | CSS fil | +|---------|-----------|-----|---------| +| Cash | `swp-cash-table` | `swp-cash-table-row` | cash.css | +| Employees | `swp-employee-table` | `swp-employee-row` | employees.css | +| Bookings | `swp-booking-list` | `swp-booking-item` | bookings.css | +| Notifications | `swp-notification-list` | `swp-notification-item` | notifications.css | +| Attentions | `swp-attention-list` | `swp-attention-item` | attentions.css | + +--- + +## Table Cells - Standard Styling + +```css +/* Header cells */ +swp-[feature]-table-header swp-[feature]-cell { + font-size: var(--font-size-xs); + font-weight: var(--font-weight-semibold); + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--color-text-secondary); +} + +/* Body cells */ +swp-[feature]-cell { + padding: var(--spacing-5); + font-size: var(--font-size-base); /* ALTID base, ikke sm */ + color: var(--color-text); +} +``` + +--- + +## Icon Buttons (employees.css) + +```html + + + + + + + + +``` + +--- + +## User Info Pattern (employees.css) + +```html + + MJ + + Maria Jensen + maria@example.com + + +``` + +**Avatar farver:** (ingen class = teal), `purple`, `blue`, `amber` + +--- + +## Design Tokens (design-tokens.css) + +### Farver + +| Token | Brug | +|-------|------| +| `--color-teal` | Primary brand, success | +| `--color-green` | Success, positive | +| `--color-amber` | Warning, pending | +| `--color-red` | Error, danger | +| `--color-purple` | Special, AI | +| `--color-blue` | Info | + +### Spacing + +```css +--spacing-1: 2px; +--spacing-2: 4px; +--spacing-3: 6px; +--spacing-4: 8px; +--spacing-5: 10px; +--spacing-6: 12px; +--spacing-7: 14px; +--spacing-8: 16px; +--spacing-10: 20px; +--spacing-12: 24px; +``` + +### Font Sizes + +```css +--font-size-xs: 11px; +--font-size-sm: 12px; +--font-size-md: 13px; +--font-size-base: 14px; /* Standard body text */ +--font-size-lg: 16px; +--font-size-xl: 18px; +--font-size-2xl: 20px; +--font-size-3xl: 22px; +``` + +### Font Families + +```css +--font-family: 'Poppins', sans-serif; +--font-mono: 'JetBrains Mono', monospace; /* Til tal/værdier */ +``` + +--- + +## Checklist for Ny Side + +1. [ ] Læs denne fil +2. [ ] List UI elementer der skal bruges +3. [ ] Match hver element med eksisterende komponent +4. [ ] Dokumenter kun NYE elementer der skal oprettes +5. [ ] Opret feature CSS med header der angiver genbrugte komponenter +6. [ ] Brug `var(--font-size-base)` for body text +7. [ ] Brug `var(--font-mono)` kun for tal/værdier + +--- + +## Fil Reference + +| Fil | Indhold | +|-----|---------| +| `design-tokens.css` | Farver, spacing, fonts, shadows | +| `design-system.css` | Base resets, typography | +| `page.css` | Page structure, cards | +| `stats.css` | Stat cards, stat rows | +| `tabs.css` | Tab bar, tab content | +| `cash.css` | Buttons, status badges, tables | +| `employees.css` | User info, role badges, employee table | +| `bookings.css` | Booking list items | +| `notifications.css` | Notification items | +| `attentions.css` | Attention items | diff --git a/PlanTempus.Application/wwwroot/css/cash.css b/PlanTempus.Application/wwwroot/css/cash.css index 8506141..1dfcb14 100644 --- a/PlanTempus.Application/wwwroot/css/cash.css +++ b/PlanTempus.Application/wwwroot/css/cash.css @@ -2,36 +2,9 @@ * Cash Register - Page Styling * * Filter bar, stats, table, forms, and difference box + * Reuses: swp-sticky-header, swp-header-content (page.css) */ -/* =========================================== - STICKY HEADER CONTAINER - =========================================== */ -swp-cash-sticky-header { - display: block; - position: sticky; - top: 0; - z-index: var(--z-sticky); - background: var(--color-surface); - overflow: visible; -} - -/* Override tab-bar sticky when inside sticky header */ -swp-cash-sticky-header swp-tab-bar { - position: static; - top: auto; -} - -/* =========================================== - KASSE HEADER (Stats above tabs) - =========================================== */ -swp-cash-header { - display: block; - background: var(--color-surface); - border-bottom: 1px solid var(--color-border); - padding: var(--spacing-10) var(--spacing-12); -} - /* =========================================== FILTER BAR =========================================== */ @@ -375,16 +348,40 @@ swp-status-badge::before { background: currentColor; } -swp-status-badge.approved { +/* Status variants */ +swp-status-badge.approved, +swp-status-badge.active { background: color-mix(in srgb, var(--color-green) 15%, transparent); color: var(--color-green); } -swp-status-badge.draft { +swp-status-badge.draft, +swp-status-badge.invited { background: color-mix(in srgb, var(--color-amber) 15%, transparent); color: #b45309; } +/* Role variants */ +swp-status-badge.owner { + background: color-mix(in srgb, var(--color-teal) 15%, transparent); + color: var(--color-teal); +} + +swp-status-badge.admin { + background: color-mix(in srgb, var(--color-purple) 15%, transparent); + color: var(--color-purple); +} + +swp-status-badge.leader { + background: color-mix(in srgb, var(--color-blue) 15%, transparent); + color: var(--color-blue); +} + +swp-status-badge.employee { + background: var(--color-background-alt); + color: var(--color-text-secondary); +} + /* =========================================== TWO-COLUMN GRID (Detail View) =========================================== */ diff --git a/PlanTempus.Application/wwwroot/css/controls.css b/PlanTempus.Application/wwwroot/css/controls.css new file mode 100644 index 0000000..d516a09 --- /dev/null +++ b/PlanTempus.Application/wwwroot/css/controls.css @@ -0,0 +1,148 @@ +/** + * Form Controls - Toggles, Checkboxes, Inputs + * + * Reusable form control components used across the application. + */ + +/* =========================================== + TOGGLE SLIDER (Yes/No switch) + =========================================== */ +swp-toggle-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--spacing-3) 0; + border-bottom: 1px solid var(--color-border); +} + +swp-toggle-row:last-child { + border-bottom: none; +} + +swp-toggle-label { + font-size: var(--font-size-base); + color: var(--color-text); +} + +swp-toggle-description { + display: block; + font-size: var(--font-size-sm); + color: var(--color-text-secondary); + margin-top: var(--spacing-1); +} + +swp-toggle-slider { + display: inline-flex; + width: fit-content; + background: var(--color-background); + border-radius: var(--radius-md); + border: 1px solid var(--color-border); + overflow: hidden; + position: relative; +} + +swp-toggle-slider::before { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: calc(50% - 4px); + height: calc(100% - 4px); + background: color-mix(in srgb, var(--color-green) 18%, white); + border-radius: var(--radius-sm); + transition: transform 200ms ease, background 200ms ease; +} + +swp-toggle-slider[data-value="no"]::before { + transform: translateX(100%); + background: color-mix(in srgb, var(--color-red) 18%, white); +} + +swp-toggle-option { + position: relative; + z-index: 1; + padding: var(--spacing-2) var(--spacing-5); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); + cursor: pointer; + transition: color var(--transition-fast); + user-select: none; +} + +swp-toggle-slider[data-value="yes"] swp-toggle-option:first-child { + color: var(--color-green); + font-weight: var(--font-weight-semibold); +} + +swp-toggle-slider[data-value="no"] swp-toggle-option:last-child { + color: var(--color-red); + font-weight: var(--font-weight-semibold); +} + +/* =========================================== + CHECKBOX LIST + =========================================== */ +swp-checkbox-list { + display: flex; + flex-direction: column; + gap: var(--spacing-1); +} + +swp-checkbox-row { + display: flex; + align-items: flex-start; + gap: var(--spacing-4); + padding: var(--spacing-3) var(--spacing-4); + border-radius: var(--radius-md); + cursor: pointer; + transition: background var(--transition-fast); +} + +swp-checkbox-row:hover { + background: var(--color-background-alt); +} + +swp-checkbox-box { + width: 18px; + height: 18px; + border: 2px solid var(--color-border); + border-radius: var(--radius-sm); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-top: 1px; + transition: all var(--transition-fast); +} + +swp-checkbox-row.checked swp-checkbox-box { + background: var(--color-teal); + border-color: var(--color-teal); +} + +swp-checkbox-box svg { + width: 12px; + height: 12px; + fill: white; + opacity: 0; + transition: opacity var(--transition-fast); +} + +swp-checkbox-row.checked swp-checkbox-box svg { + opacity: 1; +} + +swp-checkbox-text { + font-size: var(--font-size-base); + color: var(--color-text); + line-height: 1.4; +} + +/* Intro text for checkbox lists (e.g. notifications) */ +swp-notification-intro { + display: block; + font-size: var(--font-size-sm); + color: var(--color-text-secondary); + margin-bottom: var(--spacing-5); +} diff --git a/PlanTempus.Application/wwwroot/css/employees.css b/PlanTempus.Application/wwwroot/css/employees.css new file mode 100644 index 0000000..a9cc30f --- /dev/null +++ b/PlanTempus.Application/wwwroot/css/employees.css @@ -0,0 +1,721 @@ +/** + * Employees Styles - User & Role Management + * + * Employees-specific styling only. + * Reuses: swp-stat-card (stats.css), swp-stats-row (stats.css), swp-tab-bar (tabs.css), + * swp-btn (cash.css), swp-status-badge (cash.css), swp-row-toggle (cash.css), + * swp-sticky-header, swp-header-content (page.css), + * swp-toggle-slider, swp-checkbox-list (controls.css) + * + * Creates: swp-employee-table, swp-employee-row, swp-user-info, + * swp-employee-avatar-large, swp-employee-detail-header, + * swp-fact-inline, swp-edit-row, swp-detail-grid + */ + +/* =========================================== + USERS HEADER + =========================================== */ +swp-users-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--section-gap); +} + +swp-users-count { + display: flex; + align-items: center; + gap: var(--spacing-4); + font-size: var(--font-size-sm); + color: var(--color-text-secondary); +} + +swp-users-count strong { + color: var(--color-text); + font-weight: var(--font-weight-semibold); +} + +swp-users-progress { + width: 120px; + height: 6px; + background: var(--color-border); + border-radius: var(--radius-sm); + overflow: hidden; +} + +swp-users-progress-bar { + display: block; + height: 100%; + background: var(--color-teal); + border-radius: var(--radius-sm); + transition: width var(--transition-normal); +} + +/* =========================================== + EMPLOYEE TABLE (Grid + Subgrid) + =========================================== */ +swp-employee-table-card { + display: block; + background: var(--color-surface); + border-radius: var(--radius-lg); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + overflow: hidden; +} + +swp-employee-table { + display: grid; + grid-template-columns: minmax(220px, 1fr) 120px 140px 120px 40px; +} + +swp-employee-table-header { + display: grid; + grid-column: 1 / -1; + grid-template-columns: subgrid; + background: var(--color-background-alt); + padding: 0 var(--spacing-10); +} + +swp-employee-table-body { + display: grid; + grid-column: 1 / -1; + grid-template-columns: subgrid; +} + +swp-employee-row { + display: grid; + grid-column: 1 / -1; + grid-template-columns: subgrid; + align-items: center; + padding: 0 var(--spacing-10); + border-bottom: 1px solid var(--color-border); + transition: background var(--transition-fast); +} + +swp-employee-table-body swp-employee-row { + cursor: pointer; +} + +swp-employee-table-body swp-employee-row:hover { + background: var(--color-background-hover); +} + +swp-employee-row:last-child { + border-bottom: none; +} + +swp-employee-cell { + padding: var(--spacing-5) 0; + font-size: var(--font-size-base); + color: var(--color-text); +} + +/* Chevron cell (last column) */ +swp-employee-cell:last-child { + display: flex; + align-items: center; + justify-content: center; +} + +/* Header cells */ +swp-employee-table-header swp-employee-cell { + font-size: var(--font-size-xs); + font-weight: var(--font-weight-semibold); + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--color-text-secondary); + padding-top: var(--spacing-5); + padding-bottom: var(--spacing-5); +} + +/* =========================================== + USER INFO (Avatar + Details) + =========================================== */ +swp-user-info { + display: flex; + align-items: center; + gap: var(--spacing-3); +} + +swp-user-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + background: var(--color-teal); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-size: var(--font-size-xs); + font-weight: var(--font-weight-semibold); + flex-shrink: 0; +} + +swp-user-avatar.purple { + background: var(--color-purple); +} + +swp-user-avatar.blue { + background: var(--color-blue); +} + +swp-user-avatar.amber { + background: var(--color-amber); +} + +swp-user-details { + min-width: 0; +} + +swp-user-name { + display: block; + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + color: var(--color-text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +swp-user-email { + display: block; + font-size: var(--font-size-sm); + color: var(--color-text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* =========================================== + TABLE ACTIONS + =========================================== */ +swp-table-actions { + display: flex; + align-items: center; + gap: var(--spacing-2); + justify-content: flex-end; +} + +swp-icon-btn { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-md); + background: transparent; + border: none; + color: var(--color-text-secondary); + cursor: pointer; + transition: all var(--transition-fast); +} + +swp-icon-btn:hover { + background: var(--color-background-alt); + color: var(--color-text); +} + +swp-icon-btn.danger:hover { + background: color-mix(in srgb, var(--color-red) 10%, transparent); + color: var(--color-red); +} + +swp-icon-btn i { + font-size: 18px; +} + +/* =========================================== + PERMISSIONS MATRIX + =========================================== */ +swp-permissions-matrix { + display: block; + background: var(--color-surface); + border-radius: var(--radius-lg); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + overflow: hidden; +} + +swp-permissions-matrix table { + width: 100%; + border-collapse: collapse; +} + +swp-permissions-matrix th, +swp-permissions-matrix td { + padding: var(--spacing-5) var(--spacing-6); + text-align: center; + border-bottom: 1px solid var(--color-border); + font-size: var(--font-size-base); +} + +swp-permissions-matrix th:first-child, +swp-permissions-matrix td:first-child { + text-align: left; + font-weight: var(--font-weight-medium); +} + +swp-permissions-matrix thead th { + background: var(--color-background-alt); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-semibold); + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--color-text-secondary); +} + +swp-permissions-matrix tbody tr:last-child td { + border-bottom: none; +} + +swp-permissions-matrix .permission-name { + display: flex; + align-items: center; + gap: var(--spacing-3); + font-size: var(--font-size-base); + color: var(--color-text); +} + +swp-permissions-matrix .permission-name i { + font-size: 18px; + color: var(--color-text-secondary); +} + +swp-permissions-matrix .check { + color: var(--color-teal); + font-size: 20px; +} + +swp-permissions-matrix .no-access { + color: var(--color-border); + font-size: 16px; +} + +/* =========================================== + EMPLOYEE DETAIL VIEW (replaces page content) + =========================================== */ + +/* Large avatar for detail view */ +swp-employee-avatar-large { + width: 80px; + height: 80px; + border-radius: 50%; + background: var(--color-teal); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-size: var(--font-size-2xl); + font-weight: var(--font-weight-semibold); + flex-shrink: 0; +} + +swp-employee-avatar-large.purple { background: var(--color-purple); } +swp-employee-avatar-large.blue { background: var(--color-blue); } +swp-employee-avatar-large.amber { background: var(--color-amber); } + +/* Detail header content (inside swp-header-content) */ +swp-employee-detail-header { + display: flex; + gap: var(--spacing-12); +} + +swp-employee-info { + flex: 1; + display: flex; + flex-direction: column; + gap: var(--spacing-2); + min-width: 0; +} + +/* Name row with name + tags + status */ +swp-employee-name-row { + display: flex; + align-items: center; + gap: var(--spacing-8); +} + +swp-employee-name { + font-size: var(--font-size-2xl); + font-weight: var(--font-weight-semibold); + color: var(--color-text); +} + +/* Tags row */ +swp-tags-row { + display: flex; + align-items: center; + gap: var(--spacing-2); + flex-wrap: wrap; +} + +swp-tag { + display: inline-flex; + align-items: center; + gap: var(--spacing-1); + padding: var(--spacing-1) var(--spacing-3); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-semibold); + text-transform: uppercase; + letter-spacing: 0.3px; + border-radius: var(--radius-sm); + background: var(--color-background); + color: var(--color-text-secondary); +} + +swp-tag.master { + background: color-mix(in srgb, var(--color-purple) 15%, white); + color: var(--color-purple); +} + +swp-tag.senior { + background: color-mix(in srgb, var(--color-blue) 15%, white); + color: var(--color-blue); +} + +swp-tag.junior { + background: color-mix(in srgb, var(--color-amber) 15%, white); + color: #b45309; +} + +swp-tag.cert { + background: color-mix(in srgb, var(--color-teal) 15%, white); + color: var(--color-teal); +} + +/* Employee status indicator */ +swp-employee-status { + display: inline-flex; + align-items: center; + gap: var(--spacing-2); + padding: var(--spacing-2) var(--spacing-4); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + border-radius: var(--radius-md); + cursor: pointer; + transition: all var(--transition-fast); + margin-left: auto; +} + +swp-employee-status[data-active="true"] { + background: color-mix(in srgb, var(--color-green) 15%, white); + color: var(--color-green); + border: 1px solid color-mix(in srgb, var(--color-green) 30%, white); +} + +swp-employee-status[data-active="false"] { + background: color-mix(in srgb, var(--color-red) 12%, white); + color: var(--color-red); + border: 1px solid color-mix(in srgb, var(--color-red) 30%, white); +} + +swp-employee-status .icon { + font-size: var(--font-size-base); +} + +/* Inline fact boxes (horizontal baseline alignment) */ +swp-fact-boxes-inline { + display: flex; + gap: var(--spacing-12); + margin-top: var(--spacing-1); + flex-wrap: wrap; +} + +swp-fact-inline { + display: flex; + align-items: baseline; + gap: var(--spacing-2); +} + +swp-fact-inline-value { + font-size: var(--font-size-lg); + font-weight: var(--font-weight-semibold); + font-family: var(--font-mono); + color: var(--color-text); +} + +swp-fact-inline-label { + font-size: var(--font-size-sm); + color: var(--color-text-secondary); +} + +/* Edit rows for contenteditable (Grid + Subgrid) */ +swp-edit-section { + display: grid; + grid-template-columns: 140px 1fr; + gap: var(--spacing-4); +} + +swp-edit-row { + display: grid; + grid-column: 1 / -1; + grid-template-columns: subgrid; + align-items: center; +} + +swp-edit-label { + font-size: var(--font-size-sm); + color: var(--color-text-secondary); +} + +swp-edit-value { + font-size: var(--font-size-base); + color: var(--color-text); + padding: var(--spacing-2) var(--spacing-4); + border-radius: var(--radius-sm); + background: var(--color-background-alt); + border: 1px solid transparent; + transition: all var(--transition-fast); + cursor: text; +} + +swp-edit-value:hover { + background: var(--color-background); +} + +swp-edit-value:focus { + outline: none; + background: var(--color-surface); + border-color: var(--color-teal); +} + +/* Section label in cards */ +swp-section-label { + display: block; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + color: var(--color-text); + margin-bottom: var(--spacing-6); + padding-bottom: var(--spacing-4); + border-bottom: 1px solid var(--color-border); +} + +/* =========================================== + VIEW CONTAINERS (List/Detail swap) + =========================================== */ +swp-employees-list-view { + display: block; +} + +swp-employee-detail-view { + display: none; + min-height: calc(100vh - 60px); +} + +/* Back link */ +swp-back-link { + display: inline-flex; + align-items: center; + gap: var(--spacing-2); + color: var(--color-text-secondary); + font-size: var(--font-size-sm); + cursor: pointer; + transition: color var(--transition-fast); +} + +swp-back-link:hover { + color: var(--color-teal); +} + +swp-back-link i { + font-size: 16px; +} + +/* Detail grid for cards (2-column layout) */ +swp-detail-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--spacing-8); +} + +swp-detail-grid > div { + display: flex; + flex-direction: column; + gap: var(--spacing-8); +} + +swp-detail-grid swp-card { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: var(--card-padding); +} + +@media (max-width: 900px) { + swp-detail-grid { + grid-template-columns: 1fr; + } +} + +/* =========================================== + SCHEDULE GRID (Hours tab) + =========================================== */ +swp-schedule-grid { + display: flex; + flex-direction: column; +} + +swp-schedule-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--spacing-4) 0; + border-bottom: 1px solid var(--color-border); +} + +swp-schedule-row:last-child { + border-bottom: none; +} + +swp-schedule-row.off { + color: var(--color-text-secondary); +} + +swp-schedule-day { + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); +} + +swp-schedule-time { + font-size: var(--font-size-base); + font-family: var(--font-mono); + color: var(--color-teal); +} + +swp-schedule-row.off swp-schedule-time { + color: var(--color-text-secondary); +} + +/* =========================================== + SERVICE LIST (Services tab) + =========================================== */ +swp-service-list { + display: flex; + flex-direction: column; +} + +swp-service-item { + display: grid; + grid-template-columns: 1fr auto auto; + gap: var(--spacing-6); + align-items: center; + padding: var(--spacing-4) 0; + border-bottom: 1px solid var(--color-border); +} + +swp-service-item:last-child { + border-bottom: none; +} + +swp-service-name { + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); +} + +swp-service-duration { + font-size: var(--font-size-sm); + color: var(--color-text-secondary); +} + +swp-service-price { + font-size: var(--font-size-base); + font-family: var(--font-mono); + color: var(--color-teal); + font-weight: var(--font-weight-medium); +} + +/* =========================================== + DOCUMENT LIST (HR tab) + =========================================== */ +swp-document-list { + display: flex; + flex-direction: column; +} + +swp-document-item { + display: flex; + align-items: center; + gap: var(--spacing-3); + padding: var(--spacing-4) 0; + border-bottom: 1px solid var(--color-border); + cursor: pointer; + transition: background var(--transition-fast); +} + +swp-document-item:last-child { + border-bottom: none; +} + +swp-document-item:hover { + background: var(--color-background-alt); + margin: 0 calc(-1 * var(--spacing-3)); + padding-left: var(--spacing-3); + padding-right: var(--spacing-3); + border-radius: var(--radius-sm); +} + +swp-document-item i { + font-size: 24px; + color: var(--color-red); +} + +swp-document-name { + flex: 1; + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); +} + +swp-document-date { + font-size: var(--font-size-sm); + color: var(--color-text-secondary); +} + +/* Notes area */ +swp-notes-area { + display: block; + min-height: 100px; + padding: var(--spacing-4); + background: var(--color-background-alt); + border-radius: var(--radius-sm); + font-size: var(--font-size-base); + color: var(--color-text-secondary); + line-height: 1.6; +} + +swp-notes-area:focus { + outline: none; + color: var(--color-text); +} + +/* =========================================== + RESPONSIVE + =========================================== */ +@media (max-width: 1024px) { + swp-employee-table { + grid-template-columns: minmax(180px, 1fr) 100px 120px 100px 40px; + } +} + +@media (max-width: 768px) { + swp-employee-table { + grid-template-columns: minmax(160px, 1fr) 90px 110px 90px 40px; + } + + swp-employee-cell { + padding: var(--spacing-3) 0; + } + + swp-employee-table-header, + swp-employee-row { + padding: 0 var(--spacing-4); + } + + swp-users-header { + flex-direction: column; + align-items: flex-start; + gap: var(--spacing-4); + } + + swp-users-header swp-btn { + width: 100%; + justify-content: center; + } +} diff --git a/PlanTempus.Application/wwwroot/css/page.css b/PlanTempus.Application/wwwroot/css/page.css index 3afe199..d04259e 100644 --- a/PlanTempus.Application/wwwroot/css/page.css +++ b/PlanTempus.Application/wwwroot/css/page.css @@ -14,6 +14,42 @@ swp-page-container { padding: var(--page-padding); } +/* =========================================== + STICKY HEADER (Generic - use for all tabbed pages) + =========================================== */ +swp-sticky-header { + display: block; + position: sticky; + top: 0; + z-index: var(--z-sticky); + background: var(--color-surface); + overflow: visible; + /* INGEN padding eller border - det er på swp-header-content */ +} + +/* Override tab-bar sticky when inside sticky header */ +swp-sticky-header swp-tab-bar { + position: static; + top: auto; +} + +/* Header content wrapper - HAR padding + border */ +swp-header-content { + display: block; + background: var(--color-surface); + border-bottom: 1px solid var(--color-border); + padding: var(--spacing-10) var(--spacing-12); +} + +swp-header-content swp-page-header { + padding: 0; + margin-bottom: 0; +} + +swp-header-content swp-stats-row { + margin-bottom: 0; +} + /* =========================================== PAGE HEADER =========================================== */ diff --git a/PlanTempus.Application/wwwroot/js/app.js b/PlanTempus.Application/wwwroot/js/app.js index 75e7310..e6abe7f 100644 --- a/PlanTempus.Application/wwwroot/js/app.js +++ b/PlanTempus.Application/wwwroot/js/app.js @@ -1,927 +1,1071 @@ -var __defProp = Object.defineProperty; -var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); +"use strict"; +(() => { + // wwwroot/ts/modules/sidebar.ts + var SidebarController = class { + constructor() { + this.menuToggle = null; + this.appLayout = null; + this.menuTooltip = null; + this.menuToggle = document.getElementById("menuToggle"); + this.appLayout = document.querySelector("swp-app-layout"); + this.menuTooltip = document.getElementById("menuTooltip"); + this.setupListeners(); + this.setupTooltips(); + this.restoreState(); + } + /** + * Check if sidebar is collapsed + */ + get isCollapsed() { + return this.appLayout?.classList.contains("menu-collapsed") ?? false; + } + /** + * Toggle sidebar collapsed state + */ + toggle() { + if (!this.appLayout) return; + this.appLayout.classList.toggle("menu-collapsed"); + localStorage.setItem("sidebar-collapsed", String(this.isCollapsed)); + } + /** + * Collapse the sidebar + */ + collapse() { + this.appLayout?.classList.add("menu-collapsed"); + localStorage.setItem("sidebar-collapsed", "true"); + } + /** + * Expand the sidebar + */ + expand() { + this.appLayout?.classList.remove("menu-collapsed"); + localStorage.setItem("sidebar-collapsed", "false"); + } + setupListeners() { + this.menuToggle?.addEventListener("click", () => this.toggle()); + } + setupTooltips() { + if (!this.menuTooltip) return; + const menuItems = document.querySelectorAll("swp-side-menu-item[data-tooltip]"); + menuItems.forEach((item) => { + item.addEventListener("mouseenter", () => this.showTooltip(item)); + item.addEventListener("mouseleave", () => this.hideTooltip()); + }); + } + showTooltip(item) { + if (!this.isCollapsed || !this.menuTooltip) return; + const rect = item.getBoundingClientRect(); + const tooltipText = item.dataset.tooltip; + if (!tooltipText) return; + this.menuTooltip.textContent = tooltipText; + this.menuTooltip.style.left = `${rect.right + 8}px`; + this.menuTooltip.style.top = `${rect.top + rect.height / 2}px`; + this.menuTooltip.style.transform = "translateY(-50%)"; + this.menuTooltip.showPopover(); + } + hideTooltip() { + this.menuTooltip?.hidePopover(); + } + restoreState() { + if (!this.appLayout) return; + if (localStorage.getItem("sidebar-collapsed") === "true") { + this.appLayout.classList.add("menu-collapsed"); + } + } + }; -// wwwroot/ts/modules/sidebar.ts -var _SidebarController = class _SidebarController { - constructor() { - this.menuToggle = null; - this.appLayout = null; - this.menuTooltip = null; - this.menuToggle = document.getElementById("menuToggle"); - this.appLayout = document.querySelector("swp-app-layout"); - this.menuTooltip = document.getElementById("menuTooltip"); - this.setupListeners(); - this.setupTooltips(); - this.restoreState(); - } - /** - * Check if sidebar is collapsed - */ - get isCollapsed() { - return this.appLayout?.classList.contains("menu-collapsed") ?? false; - } - /** - * Toggle sidebar collapsed state - */ - toggle() { - if (!this.appLayout) return; - this.appLayout.classList.toggle("menu-collapsed"); - localStorage.setItem("sidebar-collapsed", String(this.isCollapsed)); - } - /** - * Collapse the sidebar - */ - collapse() { - this.appLayout?.classList.add("menu-collapsed"); - localStorage.setItem("sidebar-collapsed", "true"); - } - /** - * Expand the sidebar - */ - expand() { - this.appLayout?.classList.remove("menu-collapsed"); - localStorage.setItem("sidebar-collapsed", "false"); - } - setupListeners() { - this.menuToggle?.addEventListener("click", () => this.toggle()); - } - setupTooltips() { - if (!this.menuTooltip) return; - const menuItems = document.querySelectorAll("swp-side-menu-item[data-tooltip]"); - menuItems.forEach((item) => { - item.addEventListener("mouseenter", () => this.showTooltip(item)); - item.addEventListener("mouseleave", () => this.hideTooltip()); - }); - } - showTooltip(item) { - if (!this.isCollapsed || !this.menuTooltip) return; - const rect = item.getBoundingClientRect(); - const tooltipText = item.dataset.tooltip; - if (!tooltipText) return; - this.menuTooltip.textContent = tooltipText; - this.menuTooltip.style.left = `${rect.right + 8}px`; - this.menuTooltip.style.top = `${rect.top + rect.height / 2}px`; - this.menuTooltip.style.transform = "translateY(-50%)"; - this.menuTooltip.showPopover(); - } - hideTooltip() { - this.menuTooltip?.hidePopover(); - } - restoreState() { - if (!this.appLayout) return; - if (localStorage.getItem("sidebar-collapsed") === "true") { - this.appLayout.classList.add("menu-collapsed"); - } - } -}; -__name(_SidebarController, "SidebarController"); -var SidebarController = _SidebarController; - -// wwwroot/ts/modules/drawers.ts -var _DrawerController = class _DrawerController { - constructor() { - this.profileDrawer = null; - this.notificationDrawer = null; - this.todoDrawer = null; - this.newTodoDrawer = null; - this.overlay = null; - this.activeDrawer = null; - this.activeGenericDrawer = null; - this.profileDrawer = document.getElementById("profileDrawer"); - this.notificationDrawer = document.getElementById("notificationDrawer"); - this.todoDrawer = document.getElementById("todoDrawer"); - this.newTodoDrawer = document.getElementById("newTodoDrawer"); - this.overlay = document.getElementById("drawerOverlay"); - this.setupListeners(); - this.setupGenericDrawers(); - } - /** - * Get currently active drawer name - */ - get active() { - return this.activeDrawer; - } - /** - * Open a drawer by name - */ - open(name) { - this.closeAll(); - const drawer = this.getDrawer(name); - if (drawer && this.overlay) { - drawer.classList.add("active"); - this.overlay.classList.add("active"); - document.body.style.overflow = "hidden"; - this.activeDrawer = name; - } - } - /** - * Close a specific drawer - */ - close(name) { - const drawer = this.getDrawer(name); - drawer?.classList.remove("active"); - if (this.overlay && !document.querySelector('.active[class*="drawer"]')) { - this.overlay.classList.remove("active"); - document.body.style.overflow = ""; - } - if (this.activeDrawer === name) { + // wwwroot/ts/modules/drawers.ts + var DrawerController = class { + constructor() { + this.profileDrawer = null; + this.notificationDrawer = null; + this.todoDrawer = null; + this.newTodoDrawer = null; + this.overlay = null; this.activeDrawer = null; + this.activeGenericDrawer = null; + this.profileDrawer = document.getElementById("profileDrawer"); + this.notificationDrawer = document.getElementById("notificationDrawer"); + this.todoDrawer = document.getElementById("todoDrawer"); + this.newTodoDrawer = document.getElementById("newTodoDrawer"); + this.overlay = document.getElementById("drawerOverlay"); + this.setupListeners(); + this.setupGenericDrawers(); } - } - /** - * Close all drawers - */ - closeAll() { - [this.profileDrawer, this.notificationDrawer, this.todoDrawer, this.newTodoDrawer].forEach((drawer) => drawer?.classList.remove("active")); - this.closeGenericDrawer(); - this.overlay?.classList.remove("active"); - document.body.style.overflow = ""; - this.activeDrawer = null; - } - /** - * Open a generic drawer by ID - */ - openGenericDrawer(drawerId) { - this.closeAll(); - const drawer = document.getElementById(drawerId); - if (drawer && this.overlay) { - drawer.classList.add("open"); - this.overlay.classList.add("active"); - document.body.style.overflow = "hidden"; - this.activeGenericDrawer = drawer; + /** + * Get currently active drawer name + */ + get active() { + return this.activeDrawer; } - } - /** - * Close the currently open generic drawer - */ - closeGenericDrawer() { - this.activeGenericDrawer?.classList.remove("open"); - this.activeGenericDrawer = null; - } - /** - * Open profile drawer - */ - openProfile() { - this.open("profile"); - } - /** - * Open notification drawer - */ - openNotification() { - this.open("notification"); - } - /** - * Open todo drawer (slides on top of profile) - */ - openTodo() { - this.todoDrawer?.classList.add("active"); - } - /** - * Close todo drawer - */ - closeTodo() { - this.todoDrawer?.classList.remove("active"); - this.closeNewTodo(); - } - /** - * Open new todo drawer - */ - openNewTodo() { - this.newTodoDrawer?.classList.add("active"); - } - /** - * Close new todo drawer - */ - closeNewTodo() { - this.newTodoDrawer?.classList.remove("active"); - } - /** - * Mark all notifications as read - */ - markAllNotificationsRead() { - if (!this.notificationDrawer) return; - const unreadItems = this.notificationDrawer.querySelectorAll( - 'swp-notification-item[data-unread="true"]' - ); - unreadItems.forEach((item) => item.removeAttribute("data-unread")); - const badge = document.querySelector("swp-notification-badge"); - if (badge) { - badge.style.display = "none"; - } - } - getDrawer(name) { - switch (name) { - case "profile": - return this.profileDrawer; - case "notification": - return this.notificationDrawer; - case "todo": - return this.todoDrawer; - case "newTodo": - return this.newTodoDrawer; - } - } - setupListeners() { - document.getElementById("profileTrigger")?.addEventListener("click", () => this.openProfile()); - document.getElementById("drawerClose")?.addEventListener("click", () => this.close("profile")); - document.getElementById("notificationsBtn")?.addEventListener("click", () => this.openNotification()); - document.getElementById("notificationDrawerClose")?.addEventListener("click", () => this.close("notification")); - document.getElementById("markAllRead")?.addEventListener("click", () => this.markAllNotificationsRead()); - document.getElementById("openTodoDrawer")?.addEventListener("click", () => this.openTodo()); - document.getElementById("todoDrawerBack")?.addEventListener("click", () => this.closeTodo()); - document.getElementById("addTodoBtn")?.addEventListener("click", () => this.openNewTodo()); - document.getElementById("newTodoDrawerBack")?.addEventListener("click", () => this.closeNewTodo()); - document.getElementById("cancelNewTodo")?.addEventListener("click", () => this.closeNewTodo()); - document.getElementById("saveNewTodo")?.addEventListener("click", () => this.closeNewTodo()); - this.overlay?.addEventListener("click", () => this.closeAll()); - document.addEventListener("keydown", (e) => { - if (e.key === "Escape") this.closeAll(); - }); - this.todoDrawer?.addEventListener("click", (e) => this.handleTodoClick(e)); - document.addEventListener("click", (e) => this.handleVisibilityClick(e)); - } - handleTodoClick(e) { - const target = e.target; - const todoItem = target.closest("swp-todo-item"); - const checkbox = target.closest("swp-todo-checkbox"); - if (checkbox && todoItem) { - const isCompleted = todoItem.dataset.completed === "true"; - if (isCompleted) { - todoItem.removeAttribute("data-completed"); - } else { - todoItem.dataset.completed = "true"; + /** + * Open a drawer by name + */ + open(name) { + this.closeAll(); + const drawer = this.getDrawer(name); + if (drawer && this.overlay) { + drawer.classList.add("active"); + this.overlay.classList.add("active"); + document.body.style.overflow = "hidden"; + this.activeDrawer = name; } } - const sectionHeader = target.closest("swp-todo-section-header"); - if (sectionHeader) { - const section = sectionHeader.closest("swp-todo-section"); - section?.classList.toggle("collapsed"); - } - } - handleVisibilityClick(e) { - const target = e.target; - const option = target.closest("swp-visibility-option"); - if (option) { - document.querySelectorAll("swp-visibility-option").forEach((o) => o.classList.remove("active")); - option.classList.add("active"); - } - } - /** - * Setup generic drawer triggers and close buttons - * Uses data-drawer-trigger="drawer-id" and data-drawer-close attributes - */ - setupGenericDrawers() { - document.addEventListener("click", (e) => { - const target = e.target; - const trigger = target.closest("[data-drawer-trigger]"); - if (trigger) { - const drawerId = trigger.dataset.drawerTrigger; - if (drawerId) { - this.openGenericDrawer(drawerId); - } - } - }); - document.addEventListener("click", (e) => { - const target = e.target; - const closeBtn = target.closest("[data-drawer-close]"); - if (closeBtn) { - this.closeGenericDrawer(); - this.overlay?.classList.remove("active"); + /** + * Close a specific drawer + */ + close(name) { + const drawer = this.getDrawer(name); + drawer?.classList.remove("active"); + if (this.overlay && !document.querySelector('.active[class*="drawer"]')) { + this.overlay.classList.remove("active"); document.body.style.overflow = ""; } - }); - } -}; -__name(_DrawerController, "DrawerController"); -var DrawerController = _DrawerController; - -// wwwroot/ts/modules/theme.ts -var _ThemeController = class _ThemeController { - constructor() { - this.root = document.documentElement; - this.themeOptions = document.querySelectorAll("swp-theme-option"); - this.applyTheme(this.current); - this.updateUI(); - this.setupListeners(); - } - /** - * Get the current theme setting - */ - get current() { - const stored = localStorage.getItem(_ThemeController.STORAGE_KEY); - if (stored === "dark" || stored === "light" || stored === "system") { - return stored; - } - return "system"; - } - /** - * Check if dark mode is currently active - */ - get isDark() { - return this.root.classList.contains(_ThemeController.DARK_CLASS) || this.systemPrefersDark && !this.root.classList.contains(_ThemeController.LIGHT_CLASS); - } - /** - * Check if system prefers dark mode - */ - get systemPrefersDark() { - return window.matchMedia("(prefers-color-scheme: dark)").matches; - } - /** - * Set theme and persist preference - */ - set(theme) { - localStorage.setItem(_ThemeController.STORAGE_KEY, theme); - this.applyTheme(theme); - this.updateUI(); - } - /** - * Toggle between light and dark themes - */ - toggle() { - this.set(this.isDark ? "light" : "dark"); - } - applyTheme(theme) { - this.root.classList.remove(_ThemeController.DARK_CLASS, _ThemeController.LIGHT_CLASS); - if (theme === "dark") { - this.root.classList.add(_ThemeController.DARK_CLASS); - } else if (theme === "light") { - this.root.classList.add(_ThemeController.LIGHT_CLASS); - } - } - updateUI() { - if (!this.themeOptions) return; - const darkActive = this.isDark; - this.themeOptions.forEach((option) => { - const theme = option.dataset.theme; - const isActive = theme === "dark" && darkActive || theme === "light" && !darkActive; - option.classList.toggle("active", isActive); - }); - } - setupListeners() { - this.themeOptions.forEach((option) => { - option.addEventListener("click", (e) => this.handleOptionClick(e)); - }); - window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => this.handleSystemChange()); - } - handleOptionClick(e) { - const target = e.target; - const option = target.closest("swp-theme-option"); - if (option) { - const theme = option.dataset.theme; - if (theme) { - this.set(theme); + if (this.activeDrawer === name) { + this.activeDrawer = null; } } - } - handleSystemChange() { - if (this.current === "system") { + /** + * Close all drawers + */ + closeAll() { + [this.profileDrawer, this.notificationDrawer, this.todoDrawer, this.newTodoDrawer].forEach((drawer) => drawer?.classList.remove("active")); + this.closeGenericDrawer(); + this.overlay?.classList.remove("active"); + document.body.style.overflow = ""; + this.activeDrawer = null; + } + /** + * Open a generic drawer by ID + */ + openGenericDrawer(drawerId) { + this.closeAll(); + const drawer = document.getElementById(drawerId); + if (drawer && this.overlay) { + drawer.classList.add("open"); + this.overlay.classList.add("active"); + document.body.style.overflow = "hidden"; + this.activeGenericDrawer = drawer; + } + } + /** + * Close the currently open generic drawer + */ + closeGenericDrawer() { + this.activeGenericDrawer?.classList.remove("open"); + this.activeGenericDrawer = null; + } + /** + * Open profile drawer + */ + openProfile() { + this.open("profile"); + } + /** + * Open notification drawer + */ + openNotification() { + this.open("notification"); + } + /** + * Open todo drawer (slides on top of profile) + */ + openTodo() { + this.todoDrawer?.classList.add("active"); + } + /** + * Close todo drawer + */ + closeTodo() { + this.todoDrawer?.classList.remove("active"); + this.closeNewTodo(); + } + /** + * Open new todo drawer + */ + openNewTodo() { + this.newTodoDrawer?.classList.add("active"); + } + /** + * Close new todo drawer + */ + closeNewTodo() { + this.newTodoDrawer?.classList.remove("active"); + } + /** + * Mark all notifications as read + */ + markAllNotificationsRead() { + if (!this.notificationDrawer) return; + const unreadItems = this.notificationDrawer.querySelectorAll( + 'swp-notification-item[data-unread="true"]' + ); + unreadItems.forEach((item) => item.removeAttribute("data-unread")); + const badge = document.querySelector("swp-notification-badge"); + if (badge) { + badge.style.display = "none"; + } + } + getDrawer(name) { + switch (name) { + case "profile": + return this.profileDrawer; + case "notification": + return this.notificationDrawer; + case "todo": + return this.todoDrawer; + case "newTodo": + return this.newTodoDrawer; + } + } + setupListeners() { + document.getElementById("profileTrigger")?.addEventListener("click", () => this.openProfile()); + document.getElementById("drawerClose")?.addEventListener("click", () => this.close("profile")); + document.getElementById("notificationsBtn")?.addEventListener("click", () => this.openNotification()); + document.getElementById("notificationDrawerClose")?.addEventListener("click", () => this.close("notification")); + document.getElementById("markAllRead")?.addEventListener("click", () => this.markAllNotificationsRead()); + document.getElementById("openTodoDrawer")?.addEventListener("click", () => this.openTodo()); + document.getElementById("todoDrawerBack")?.addEventListener("click", () => this.closeTodo()); + document.getElementById("addTodoBtn")?.addEventListener("click", () => this.openNewTodo()); + document.getElementById("newTodoDrawerBack")?.addEventListener("click", () => this.closeNewTodo()); + document.getElementById("cancelNewTodo")?.addEventListener("click", () => this.closeNewTodo()); + document.getElementById("saveNewTodo")?.addEventListener("click", () => this.closeNewTodo()); + this.overlay?.addEventListener("click", () => this.closeAll()); + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") this.closeAll(); + }); + this.todoDrawer?.addEventListener("click", (e) => this.handleTodoClick(e)); + document.addEventListener("click", (e) => this.handleVisibilityClick(e)); + } + handleTodoClick(e) { + const target = e.target; + const todoItem = target.closest("swp-todo-item"); + const checkbox = target.closest("swp-todo-checkbox"); + if (checkbox && todoItem) { + const isCompleted = todoItem.dataset.completed === "true"; + if (isCompleted) { + todoItem.removeAttribute("data-completed"); + } else { + todoItem.dataset.completed = "true"; + } + } + const sectionHeader = target.closest("swp-todo-section-header"); + if (sectionHeader) { + const section = sectionHeader.closest("swp-todo-section"); + section?.classList.toggle("collapsed"); + } + } + handleVisibilityClick(e) { + const target = e.target; + const option = target.closest("swp-visibility-option"); + if (option) { + document.querySelectorAll("swp-visibility-option").forEach((o) => o.classList.remove("active")); + option.classList.add("active"); + } + } + /** + * Setup generic drawer triggers and close buttons + * Uses data-drawer-trigger="drawer-id" and data-drawer-close attributes + */ + setupGenericDrawers() { + document.addEventListener("click", (e) => { + const target = e.target; + const trigger = target.closest("[data-drawer-trigger]"); + if (trigger) { + const drawerId = trigger.dataset.drawerTrigger; + if (drawerId) { + this.openGenericDrawer(drawerId); + } + } + }); + document.addEventListener("click", (e) => { + const target = e.target; + const closeBtn = target.closest("[data-drawer-close]"); + if (closeBtn) { + this.closeGenericDrawer(); + this.overlay?.classList.remove("active"); + document.body.style.overflow = ""; + } + }); + } + }; + + // wwwroot/ts/modules/theme.ts + var ThemeController = class _ThemeController { + static { + this.STORAGE_KEY = "theme-preference"; + } + static { + this.DARK_CLASS = "dark-mode"; + } + static { + this.LIGHT_CLASS = "light-mode"; + } + constructor() { + this.root = document.documentElement; + this.themeOptions = document.querySelectorAll("swp-theme-option"); + this.applyTheme(this.current); + this.updateUI(); + this.setupListeners(); + } + /** + * Get the current theme setting + */ + get current() { + const stored = localStorage.getItem(_ThemeController.STORAGE_KEY); + if (stored === "dark" || stored === "light" || stored === "system") { + return stored; + } + return "system"; + } + /** + * Check if dark mode is currently active + */ + get isDark() { + return this.root.classList.contains(_ThemeController.DARK_CLASS) || this.systemPrefersDark && !this.root.classList.contains(_ThemeController.LIGHT_CLASS); + } + /** + * Check if system prefers dark mode + */ + get systemPrefersDark() { + return window.matchMedia("(prefers-color-scheme: dark)").matches; + } + /** + * Set theme and persist preference + */ + set(theme) { + localStorage.setItem(_ThemeController.STORAGE_KEY, theme); + this.applyTheme(theme); this.updateUI(); } - } -}; -__name(_ThemeController, "ThemeController"); -_ThemeController.STORAGE_KEY = "theme-preference"; -_ThemeController.DARK_CLASS = "dark-mode"; -_ThemeController.LIGHT_CLASS = "light-mode"; -var ThemeController = _ThemeController; - -// wwwroot/ts/modules/search.ts -var _SearchController = class _SearchController { - constructor() { - this.input = null; - this.container = null; - this.input = document.getElementById("globalSearch"); - this.container = document.querySelector("swp-topbar-search"); - this.setupListeners(); - } - /** - * Get current search value - */ - get value() { - return this.input?.value ?? ""; - } - /** - * Set search value - */ - set value(val) { - if (this.input) { - this.input.value = val; + /** + * Toggle between light and dark themes + */ + toggle() { + this.set(this.isDark ? "light" : "dark"); } - } - /** - * Focus the search input - */ - focus() { - this.input?.focus(); - } - /** - * Blur the search input - */ - blur() { - this.input?.blur(); - } - /** - * Clear the search input - */ - clear() { - this.value = ""; - } - setupListeners() { - document.addEventListener("keydown", (e) => this.handleKeyboard(e)); - if (this.input) { - this.input.addEventListener("input", (e) => this.handleInput(e)); - const form = this.input.closest("form"); - form?.addEventListener("submit", (e) => this.handleSubmit(e)); - } - } - handleKeyboard(e) { - if ((e.metaKey || e.ctrlKey) && e.key === "k") { - e.preventDefault(); - this.focus(); - return; - } - if (e.key === "Escape" && document.activeElement === this.input) { - this.blur(); - } - } - handleInput(e) { - const target = e.target; - const query = target.value.trim(); - document.dispatchEvent(new CustomEvent("app:search", { - detail: { query }, - bubbles: true - })); - } - handleSubmit(e) { - e.preventDefault(); - const query = this.value.trim(); - if (!query) return; - document.dispatchEvent(new CustomEvent("app:search-submit", { - detail: { query }, - bubbles: true - })); - } -}; -__name(_SearchController, "SearchController"); -var SearchController = _SearchController; - -// wwwroot/ts/modules/lockscreen.ts -var _LockScreenController = class _LockScreenController { - constructor(drawers) { - // Demo PIN - this.lockScreen = null; - this.pinInput = null; - this.pinKeypad = null; - this.lockTimeEl = null; - this.pinDigits = null; - this.currentPin = ""; - this.drawers = null; - this.drawers = drawers ?? null; - this.lockScreen = document.getElementById("lockScreen"); - this.pinInput = document.getElementById("pinInput"); - this.pinKeypad = document.getElementById("pinKeypad"); - this.lockTimeEl = document.getElementById("lockTime"); - this.pinDigits = this.pinInput?.querySelectorAll("swp-pin-digit") ?? null; - this.setupListeners(); - } - /** - * Check if lock screen is active - */ - get isActive() { - return this.lockScreen?.classList.contains("active") ?? false; - } - /** - * Show the lock screen - */ - show() { - this.drawers?.closeAll(); - if (this.lockScreen) { - this.lockScreen.classList.add("active"); - document.body.style.overflow = "hidden"; - } - this.currentPin = ""; - this.updateDisplay(); - if (this.lockTimeEl) { - this.lockTimeEl.textContent = `L\xE5st kl. ${this.formatTime()}`; - } - } - /** - * Hide the lock screen - */ - hide() { - if (this.lockScreen) { - this.lockScreen.classList.remove("active"); - document.body.style.overflow = ""; - } - this.currentPin = ""; - this.updateDisplay(); - } - formatTime() { - const now = /* @__PURE__ */ new Date(); - const hours = now.getHours().toString().padStart(2, "0"); - const minutes = now.getMinutes().toString().padStart(2, "0"); - return `${hours}:${minutes}`; - } - updateDisplay() { - if (!this.pinDigits) return; - this.pinDigits.forEach((digit, index) => { - digit.classList.remove("filled", "error"); - if (index < this.currentPin.length) { - digit.textContent = "\u2022"; - digit.classList.add("filled"); - } else { - digit.textContent = ""; + applyTheme(theme) { + this.root.classList.remove(_ThemeController.DARK_CLASS, _ThemeController.LIGHT_CLASS); + if (theme === "dark") { + this.root.classList.add(_ThemeController.DARK_CLASS); + } else if (theme === "light") { + this.root.classList.add(_ThemeController.LIGHT_CLASS); + } + } + updateUI() { + if (!this.themeOptions) return; + const darkActive = this.isDark; + this.themeOptions.forEach((option) => { + const theme = option.dataset.theme; + const isActive = theme === "dark" && darkActive || theme === "light" && !darkActive; + option.classList.toggle("active", isActive); + }); + } + setupListeners() { + this.themeOptions.forEach((option) => { + option.addEventListener("click", (e) => this.handleOptionClick(e)); + }); + window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => this.handleSystemChange()); + } + handleOptionClick(e) { + const target = e.target; + const option = target.closest("swp-theme-option"); + if (option) { + const theme = option.dataset.theme; + if (theme) { + this.set(theme); + } + } + } + handleSystemChange() { + if (this.current === "system") { + this.updateUI(); + } + } + }; + + // wwwroot/ts/modules/search.ts + var SearchController = class { + constructor() { + this.input = null; + this.container = null; + this.input = document.getElementById("globalSearch"); + this.container = document.querySelector("swp-topbar-search"); + this.setupListeners(); + } + /** + * Get current search value + */ + get value() { + return this.input?.value ?? ""; + } + /** + * Set search value + */ + set value(val) { + if (this.input) { + this.input.value = val; + } + } + /** + * Focus the search input + */ + focus() { + this.input?.focus(); + } + /** + * Blur the search input + */ + blur() { + this.input?.blur(); + } + /** + * Clear the search input + */ + clear() { + this.value = ""; + } + setupListeners() { + document.addEventListener("keydown", (e) => this.handleKeyboard(e)); + if (this.input) { + this.input.addEventListener("input", (e) => this.handleInput(e)); + const form = this.input.closest("form"); + form?.addEventListener("submit", (e) => this.handleSubmit(e)); + } + } + handleKeyboard(e) { + if ((e.metaKey || e.ctrlKey) && e.key === "k") { + e.preventDefault(); + this.focus(); + return; + } + if (e.key === "Escape" && document.activeElement === this.input) { + this.blur(); + } + } + handleInput(e) { + const target = e.target; + const query = target.value.trim(); + document.dispatchEvent(new CustomEvent("app:search", { + detail: { query }, + bubbles: true + })); + } + handleSubmit(e) { + e.preventDefault(); + const query = this.value.trim(); + if (!query) return; + document.dispatchEvent(new CustomEvent("app:search-submit", { + detail: { query }, + bubbles: true + })); + } + }; + + // wwwroot/ts/modules/lockscreen.ts + var LockScreenController = class _LockScreenController { + constructor(drawers) { + // Demo PIN + this.lockScreen = null; + this.pinInput = null; + this.pinKeypad = null; + this.lockTimeEl = null; + this.pinDigits = null; + this.currentPin = ""; + this.drawers = null; + this.drawers = drawers ?? null; + this.lockScreen = document.getElementById("lockScreen"); + this.pinInput = document.getElementById("pinInput"); + this.pinKeypad = document.getElementById("pinKeypad"); + this.lockTimeEl = document.getElementById("lockTime"); + this.pinDigits = this.pinInput?.querySelectorAll("swp-pin-digit") ?? null; + this.setupListeners(); + } + static { + this.CORRECT_PIN = "1234"; + } + /** + * Check if lock screen is active + */ + get isActive() { + return this.lockScreen?.classList.contains("active") ?? false; + } + /** + * Show the lock screen + */ + show() { + this.drawers?.closeAll(); + if (this.lockScreen) { + this.lockScreen.classList.add("active"); + document.body.style.overflow = "hidden"; } - }); - } - showError() { - if (!this.pinDigits) return; - this.pinDigits.forEach((digit) => digit.classList.add("error")); - this.pinInput?.classList.add("shake"); - setTimeout(() => { this.currentPin = ""; this.updateDisplay(); - this.pinInput?.classList.remove("shake"); - }, 500); - } - verify() { - if (this.currentPin === _LockScreenController.CORRECT_PIN) { - this.hide(); - } else { - this.showError(); + if (this.lockTimeEl) { + this.lockTimeEl.textContent = `L\xE5st kl. ${this.formatTime()}`; + } } - } - addDigit(digit) { - if (this.currentPin.length >= 4) return; - this.currentPin += digit; - this.updateDisplay(); - if (this.currentPin.length === 4) { - setTimeout(() => this.verify(), 200); + /** + * Hide the lock screen + */ + hide() { + if (this.lockScreen) { + this.lockScreen.classList.remove("active"); + document.body.style.overflow = ""; + } + this.currentPin = ""; + this.updateDisplay(); } - } - removeDigit() { - if (this.currentPin.length === 0) return; - this.currentPin = this.currentPin.slice(0, -1); - this.updateDisplay(); - } - clearPin() { - this.currentPin = ""; - this.updateDisplay(); - } - setupListeners() { - this.pinKeypad?.addEventListener("click", (e) => this.handleKeypadClick(e)); - document.addEventListener("keydown", (e) => this.handleKeyboard(e)); - document.querySelector("swp-side-menu-action.lock")?.addEventListener("click", () => this.show()); - } - handleKeypadClick(e) { - const target = e.target; - const key = target.closest("swp-pin-key"); - if (!key) return; - const digit = key.dataset.digit; - const action = key.dataset.action; - if (digit) { - this.addDigit(digit); - } else if (action === "backspace") { - this.removeDigit(); - } else if (action === "clear") { - this.clearPin(); + formatTime() { + const now = /* @__PURE__ */ new Date(); + const hours = now.getHours().toString().padStart(2, "0"); + const minutes = now.getMinutes().toString().padStart(2, "0"); + return `${hours}:${minutes}`; } - } - handleKeyboard(e) { - if (!this.isActive) return; - e.preventDefault(); - if (e.key >= "0" && e.key <= "9") { - this.addDigit(e.key); - } else if (e.key === "Backspace") { - this.removeDigit(); - } else if (e.key === "Escape") { - this.clearPin(); - } - } -}; -__name(_LockScreenController, "LockScreenController"); -_LockScreenController.CORRECT_PIN = "1234"; -var LockScreenController = _LockScreenController; - -// wwwroot/ts/modules/cash.ts -var _CashController = class _CashController { - constructor() { - // Base values (from system - would come from server in real app) - this.startBalance = 2e3; - this.cashSales = 3540; - this.setupTabs(); - this.setupCashCalculation(); - this.setupCheckboxSelection(); - this.setupApprovalCheckbox(); - this.setupDateFilters(); - this.setupRowToggle(); - this.setupDraftRowClick(); - } - /** - * Setup tab switching functionality - */ - setupTabs() { - const tabs = document.querySelectorAll("swp-tab[data-tab]"); - tabs.forEach((tab) => { - tab.addEventListener("click", () => { - const targetTab = tab.dataset.tab; - if (targetTab) { - this.switchToTab(targetTab); + updateDisplay() { + if (!this.pinDigits) return; + this.pinDigits.forEach((digit, index) => { + digit.classList.remove("filled", "error"); + if (index < this.currentPin.length) { + digit.textContent = "\u2022"; + digit.classList.add("filled"); + } else { + digit.textContent = ""; } }); - }); - } - /** - * Switch to a specific tab by name - */ - switchToTab(targetTab) { - const tabs = document.querySelectorAll("swp-tab[data-tab]"); - const contents = document.querySelectorAll("swp-tab-content[data-tab]"); - const statsBars = document.querySelectorAll("swp-cash-stats[data-for-tab]"); - tabs.forEach((t) => { - if (t.dataset.tab === targetTab) { - t.classList.add("active"); - } else { - t.classList.remove("active"); - } - }); - contents.forEach((content) => { - if (content.dataset.tab === targetTab) { - content.classList.add("active"); - } else { - content.classList.remove("active"); - } - }); - statsBars.forEach((stats) => { - if (stats.dataset.forTab === targetTab) { - stats.classList.add("active"); - } else { - stats.classList.remove("active"); - } - }); - } - /** - * Setup cash calculation with real-time updates - */ - setupCashCalculation() { - const payoutsInput = document.getElementById("payouts"); - const toBankInput = document.getElementById("toBank"); - const actualCashInput = document.getElementById("actualCash"); - if (!payoutsInput || !toBankInput || !actualCashInput) return; - const calculate = /* @__PURE__ */ __name(() => this.calculateCash(payoutsInput, toBankInput, actualCashInput), "calculate"); - payoutsInput.addEventListener("input", calculate); - toBankInput.addEventListener("input", calculate); - actualCashInput.addEventListener("input", calculate); - calculate(); - } - /** - * Calculate expected cash and difference - */ - calculateCash(payoutsInput, toBankInput, actualCashInput) { - const payouts = this.parseNumber(payoutsInput.value); - const toBank = this.parseNumber(toBankInput.value); - const actual = this.parseNumber(actualCashInput.value); - const expectedCash = this.startBalance + this.cashSales - payouts - toBank; - const expectedElement = document.getElementById("expectedCash"); - if (expectedElement) { - expectedElement.textContent = this.formatNumber(expectedCash); } - this.updateDifference(actual, expectedCash, actualCashInput.value); - } - /** - * Update difference box with color coding - */ - updateDifference(actual, expected, rawValue) { - const box = document.getElementById("differenceBox"); - const value = document.getElementById("differenceValue"); - if (!box || !value) return; - const diff = actual - expected; - box.classList.remove("positive", "negative", "neutral"); - if (actual === 0 && rawValue === "") { - value.textContent = "\u2013 kr"; - box.classList.add("neutral"); - } else if (diff > 0) { - value.textContent = "+" + this.formatNumber(diff) + " kr"; - box.classList.add("positive"); - } else if (diff < 0) { - value.textContent = this.formatNumber(diff) + " kr"; - box.classList.add("negative"); - } else { - value.textContent = "0,00 kr"; - box.classList.add("neutral"); + showError() { + if (!this.pinDigits) return; + this.pinDigits.forEach((digit) => digit.classList.add("error")); + this.pinInput?.classList.add("shake"); + setTimeout(() => { + this.currentPin = ""; + this.updateDisplay(); + this.pinInput?.classList.remove("shake"); + }, 500); } - } - /** - * Setup checkbox selection for table rows - */ - setupCheckboxSelection() { - const selectAll = document.getElementById("selectAll"); - const rowCheckboxes = document.querySelectorAll(".row-select"); - const exportBtn = document.getElementById("exportBtn"); - const selectionCount = document.getElementById("selectionCount"); - if (!selectAll || !exportBtn || !selectionCount) return; - const updateSelection = /* @__PURE__ */ __name(() => { - const checked = document.querySelectorAll(".row-select:checked"); - const count = checked.length; - selectionCount.textContent = count === 0 ? "0 valgt" : `${count} valgt`; - exportBtn.disabled = count === 0; - selectAll.checked = count === rowCheckboxes.length && count > 0; - selectAll.indeterminate = count > 0 && count < rowCheckboxes.length; - }, "updateSelection"); - selectAll.addEventListener("change", () => { - rowCheckboxes.forEach((cb) => cb.checked = selectAll.checked); - updateSelection(); - }); - rowCheckboxes.forEach((cb) => { - cb.addEventListener("change", updateSelection); - cb.addEventListener("click", (e) => e.stopPropagation()); - }); - } - /** - * Setup approval checkbox to enable/disable approve button - */ - setupApprovalCheckbox() { - const checkbox = document.getElementById("confirmCheckbox"); - const approveBtn = document.getElementById("approveBtn"); - if (!checkbox || !approveBtn) return; - checkbox.addEventListener("change", () => { - approveBtn.disabled = !checkbox.checked; - }); - } - /** - * Setup date filter defaults (last 30 days) - */ - setupDateFilters() { - const dateFrom = document.getElementById("dateFrom"); - const dateTo = document.getElementById("dateTo"); - if (!dateFrom || !dateTo) return; - const today = /* @__PURE__ */ new Date(); - const thirtyDaysAgo = new Date(today); - thirtyDaysAgo.setDate(today.getDate() - 30); - dateTo.value = this.formatDateISO(today); - dateFrom.value = this.formatDateISO(thirtyDaysAgo); - } - /** - * Format number as Danish currency - */ - formatNumber(num) { - return num.toLocaleString("da-DK", { - minimumFractionDigits: 2, - maximumFractionDigits: 2 - }); - } - /** - * Parse Danish number format - */ - parseNumber(str) { - if (!str) return 0; - return parseFloat(str.replace(/\./g, "").replace(",", ".")) || 0; - } - /** - * Format date as ISO string (YYYY-MM-DD) - */ - formatDateISO(date) { - return date.toISOString().split("T")[0]; - } - /** - * Setup row toggle for expandable details - */ - setupRowToggle() { - const rows = document.querySelectorAll("swp-cash-table-row[data-id]:not(.draft-row)"); - rows.forEach((row) => { - const rowId = row.getAttribute("data-id"); - if (!rowId) return; - const detail = document.querySelector(`swp-cash-row-detail[data-for="${rowId}"]`); - if (!detail) return; - row.addEventListener("click", (e) => { - if (e.target.closest('input[type="checkbox"]')) return; - const icon = row.querySelector("swp-row-toggle i"); - const isExpanded = row.classList.contains("expanded"); - document.querySelectorAll("swp-cash-table-row.expanded").forEach((r) => { - if (r !== row) { - const otherId = r.getAttribute("data-id"); - if (otherId) { - const otherDetail = document.querySelector(`swp-cash-row-detail[data-for="${otherId}"]`); - const otherIcon = r.querySelector("swp-row-toggle i"); - if (otherDetail && otherIcon) { - this.collapseRow(r, otherDetail, otherIcon); - } - } + verify() { + if (this.currentPin === _LockScreenController.CORRECT_PIN) { + this.hide(); + } else { + this.showError(); + } + } + addDigit(digit) { + if (this.currentPin.length >= 4) return; + this.currentPin += digit; + this.updateDisplay(); + if (this.currentPin.length === 4) { + setTimeout(() => this.verify(), 200); + } + } + removeDigit() { + if (this.currentPin.length === 0) return; + this.currentPin = this.currentPin.slice(0, -1); + this.updateDisplay(); + } + clearPin() { + this.currentPin = ""; + this.updateDisplay(); + } + setupListeners() { + this.pinKeypad?.addEventListener("click", (e) => this.handleKeypadClick(e)); + document.addEventListener("keydown", (e) => this.handleKeyboard(e)); + document.querySelector("swp-side-menu-action.lock")?.addEventListener("click", () => this.show()); + } + handleKeypadClick(e) { + const target = e.target; + const key = target.closest("swp-pin-key"); + if (!key) return; + const digit = key.dataset.digit; + const action = key.dataset.action; + if (digit) { + this.addDigit(digit); + } else if (action === "backspace") { + this.removeDigit(); + } else if (action === "clear") { + this.clearPin(); + } + } + handleKeyboard(e) { + if (!this.isActive) return; + e.preventDefault(); + if (e.key >= "0" && e.key <= "9") { + this.addDigit(e.key); + } else if (e.key === "Backspace") { + this.removeDigit(); + } else if (e.key === "Escape") { + this.clearPin(); + } + } + }; + + // wwwroot/ts/modules/cash.ts + var CashController = class { + constructor() { + // Base values (from system - would come from server in real app) + this.startBalance = 2e3; + this.cashSales = 3540; + this.setupTabs(); + this.setupCashCalculation(); + this.setupCheckboxSelection(); + this.setupApprovalCheckbox(); + this.setupDateFilters(); + this.setupRowToggle(); + this.setupDraftRowClick(); + } + /** + * Setup tab switching functionality + */ + setupTabs() { + const tabs = document.querySelectorAll("swp-tab[data-tab]"); + tabs.forEach((tab) => { + tab.addEventListener("click", () => { + const targetTab = tab.dataset.tab; + if (targetTab) { + this.switchToTab(targetTab); } }); - if (isExpanded) { - this.collapseRow(row, detail, icon); - } else { - this.expandRow(row, detail, icon); - } - }); - }); - } - /** - * Expand a row with animation - */ - expandRow(row, detail, icon) { - row.classList.add("expanded"); - detail.classList.add("expanded"); - icon?.animate([ - { transform: "rotate(0deg)" }, - { transform: "rotate(90deg)" } - ], { - duration: 200, - easing: "ease-out", - fill: "forwards" - }); - const content = detail.querySelector("swp-row-detail-content"); - if (content) { - const height = content.offsetHeight; - detail.animate([ - { height: "0px", opacity: 0 }, - { height: `${height}px`, opacity: 1 } - ], { - duration: 250, - easing: "ease-out", - fill: "forwards" }); } - } - /** - * Collapse a row with animation - */ - collapseRow(row, detail, icon) { - icon?.animate([ - { transform: "rotate(90deg)" }, - { transform: "rotate(0deg)" } - ], { - duration: 200, - easing: "ease-out", - fill: "forwards" - }); - const content = detail.querySelector("swp-row-detail-content"); - if (content) { - const height = content.offsetHeight; - const animation = detail.animate([ - { height: `${height}px`, opacity: 1 }, - { height: "0px", opacity: 0 } + /** + * Switch to a specific tab by name + */ + switchToTab(targetTab) { + const tabs = document.querySelectorAll("swp-tab[data-tab]"); + const contents = document.querySelectorAll("swp-tab-content[data-tab]"); + const statsBars = document.querySelectorAll("swp-cash-stats[data-for-tab]"); + tabs.forEach((t) => { + if (t.dataset.tab === targetTab) { + t.classList.add("active"); + } else { + t.classList.remove("active"); + } + }); + contents.forEach((content) => { + if (content.dataset.tab === targetTab) { + content.classList.add("active"); + } else { + content.classList.remove("active"); + } + }); + statsBars.forEach((stats) => { + if (stats.dataset.forTab === targetTab) { + stats.classList.add("active"); + } else { + stats.classList.remove("active"); + } + }); + } + /** + * Setup cash calculation with real-time updates + */ + setupCashCalculation() { + const payoutsInput = document.getElementById("payouts"); + const toBankInput = document.getElementById("toBank"); + const actualCashInput = document.getElementById("actualCash"); + if (!payoutsInput || !toBankInput || !actualCashInput) return; + const calculate = () => this.calculateCash(payoutsInput, toBankInput, actualCashInput); + payoutsInput.addEventListener("input", calculate); + toBankInput.addEventListener("input", calculate); + actualCashInput.addEventListener("input", calculate); + calculate(); + } + /** + * Calculate expected cash and difference + */ + calculateCash(payoutsInput, toBankInput, actualCashInput) { + const payouts = this.parseNumber(payoutsInput.value); + const toBank = this.parseNumber(toBankInput.value); + const actual = this.parseNumber(actualCashInput.value); + const expectedCash = this.startBalance + this.cashSales - payouts - toBank; + const expectedElement = document.getElementById("expectedCash"); + if (expectedElement) { + expectedElement.textContent = this.formatNumber(expectedCash); + } + this.updateDifference(actual, expectedCash, actualCashInput.value); + } + /** + * Update difference box with color coding + */ + updateDifference(actual, expected, rawValue) { + const box = document.getElementById("differenceBox"); + const value = document.getElementById("differenceValue"); + if (!box || !value) return; + const diff = actual - expected; + box.classList.remove("positive", "negative", "neutral"); + if (actual === 0 && rawValue === "") { + value.textContent = "\u2013 kr"; + box.classList.add("neutral"); + } else if (diff > 0) { + value.textContent = "+" + this.formatNumber(diff) + " kr"; + box.classList.add("positive"); + } else if (diff < 0) { + value.textContent = this.formatNumber(diff) + " kr"; + box.classList.add("negative"); + } else { + value.textContent = "0,00 kr"; + box.classList.add("neutral"); + } + } + /** + * Setup checkbox selection for table rows + */ + setupCheckboxSelection() { + const selectAll = document.getElementById("selectAll"); + const rowCheckboxes = document.querySelectorAll(".row-select"); + const exportBtn = document.getElementById("exportBtn"); + const selectionCount = document.getElementById("selectionCount"); + if (!selectAll || !exportBtn || !selectionCount) return; + const updateSelection = () => { + const checked = document.querySelectorAll(".row-select:checked"); + const count = checked.length; + selectionCount.textContent = count === 0 ? "0 valgt" : `${count} valgt`; + exportBtn.disabled = count === 0; + selectAll.checked = count === rowCheckboxes.length && count > 0; + selectAll.indeterminate = count > 0 && count < rowCheckboxes.length; + }; + selectAll.addEventListener("change", () => { + rowCheckboxes.forEach((cb) => cb.checked = selectAll.checked); + updateSelection(); + }); + rowCheckboxes.forEach((cb) => { + cb.addEventListener("change", updateSelection); + cb.addEventListener("click", (e) => e.stopPropagation()); + }); + } + /** + * Setup approval checkbox to enable/disable approve button + */ + setupApprovalCheckbox() { + const checkbox = document.getElementById("confirmCheckbox"); + const approveBtn = document.getElementById("approveBtn"); + if (!checkbox || !approveBtn) return; + checkbox.addEventListener("change", () => { + approveBtn.disabled = !checkbox.checked; + }); + } + /** + * Setup date filter defaults (last 30 days) + */ + setupDateFilters() { + const dateFrom = document.getElementById("dateFrom"); + const dateTo = document.getElementById("dateTo"); + if (!dateFrom || !dateTo) return; + const today = /* @__PURE__ */ new Date(); + const thirtyDaysAgo = new Date(today); + thirtyDaysAgo.setDate(today.getDate() - 30); + dateTo.value = this.formatDateISO(today); + dateFrom.value = this.formatDateISO(thirtyDaysAgo); + } + /** + * Format number as Danish currency + */ + formatNumber(num) { + return num.toLocaleString("da-DK", { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }); + } + /** + * Parse Danish number format + */ + parseNumber(str) { + if (!str) return 0; + return parseFloat(str.replace(/\./g, "").replace(",", ".")) || 0; + } + /** + * Format date as ISO string (YYYY-MM-DD) + */ + formatDateISO(date) { + return date.toISOString().split("T")[0]; + } + /** + * Setup row toggle for expandable details + */ + setupRowToggle() { + const rows = document.querySelectorAll("swp-cash-table-row[data-id]:not(.draft-row)"); + rows.forEach((row) => { + const rowId = row.getAttribute("data-id"); + if (!rowId) return; + const detail = document.querySelector(`swp-cash-row-detail[data-for="${rowId}"]`); + if (!detail) return; + row.addEventListener("click", (e) => { + if (e.target.closest('input[type="checkbox"]')) return; + const icon = row.querySelector("swp-row-toggle i"); + const isExpanded = row.classList.contains("expanded"); + document.querySelectorAll("swp-cash-table-row.expanded").forEach((r) => { + if (r !== row) { + const otherId = r.getAttribute("data-id"); + if (otherId) { + const otherDetail = document.querySelector(`swp-cash-row-detail[data-for="${otherId}"]`); + const otherIcon = r.querySelector("swp-row-toggle i"); + if (otherDetail && otherIcon) { + this.collapseRow(r, otherDetail, otherIcon); + } + } + } + }); + if (isExpanded) { + this.collapseRow(row, detail, icon); + } else { + this.expandRow(row, detail, icon); + } + }); + }); + } + /** + * Expand a row with animation + */ + expandRow(row, detail, icon) { + row.classList.add("expanded"); + detail.classList.add("expanded"); + icon?.animate([ + { transform: "rotate(0deg)" }, + { transform: "rotate(90deg)" } ], { duration: 200, easing: "ease-out", fill: "forwards" }); - animation.onfinish = () => { + const content = detail.querySelector("swp-row-detail-content"); + if (content) { + const height = content.offsetHeight; + detail.animate([ + { height: "0px", opacity: 0 }, + { height: `${height}px`, opacity: 1 } + ], { + duration: 250, + easing: "ease-out", + fill: "forwards" + }); + } + } + /** + * Collapse a row with animation + */ + collapseRow(row, detail, icon) { + icon?.animate([ + { transform: "rotate(90deg)" }, + { transform: "rotate(0deg)" } + ], { + duration: 200, + easing: "ease-out", + fill: "forwards" + }); + const content = detail.querySelector("swp-row-detail-content"); + if (content) { + const height = content.offsetHeight; + const animation = detail.animate([ + { height: `${height}px`, opacity: 1 }, + { height: "0px", opacity: 0 } + ], { + duration: 200, + easing: "ease-out", + fill: "forwards" + }); + animation.onfinish = () => { + row.classList.remove("expanded"); + detail.classList.remove("expanded"); + }; + } else { row.classList.remove("expanded"); detail.classList.remove("expanded"); - }; - } else { - row.classList.remove("expanded"); - detail.classList.remove("expanded"); + } + } + /** + * Setup draft row click to navigate to reconciliation tab + */ + setupDraftRowClick() { + const draftRow = document.querySelector("swp-cash-table-row.draft-row"); + if (!draftRow) return; + draftRow.style.cursor = "pointer"; + draftRow.addEventListener("click", (e) => { + if (e.target.closest('input[type="checkbox"]')) return; + this.switchToTab("afstemning"); + }); + } + }; + + // wwwroot/ts/modules/employees.ts + var EmployeesController = class { + constructor() { + this.listView = null; + this.detailView = null; + this.listView = document.getElementById("employees-list-view"); + this.detailView = document.getElementById("employee-detail-view"); + if (!this.listView) return; + this.setupListTabs(); + this.setupDetailTabs(); + this.setupChevronNavigation(); + this.setupBackNavigation(); + this.setupHistoryNavigation(); + this.restoreStateFromUrl(); + } + /** + * Setup popstate listener for browser back/forward + */ + setupHistoryNavigation() { + window.addEventListener("popstate", (e) => { + if (e.state?.employeeKey) { + this.showDetailViewInternal(e.state.employeeKey); + } else { + this.showListViewInternal(); + } + }); + } + /** + * Restore view state from URL on page load + */ + restoreStateFromUrl() { + const hash = window.location.hash; + if (hash.startsWith("#employee-")) { + const employeeKey = hash.substring(1); + this.showDetailViewInternal(employeeKey); + } + } + /** + * Setup tab switching for the list view + */ + setupListTabs() { + if (!this.listView) return; + const tabs = this.listView.querySelectorAll("swp-tab-bar > swp-tab[data-tab]"); + tabs.forEach((tab) => { + tab.addEventListener("click", () => { + const targetTab = tab.dataset.tab; + if (targetTab) { + this.switchTab(this.listView, targetTab); + } + }); + }); + } + /** + * Setup tab switching for the detail view + */ + setupDetailTabs() { + if (!this.detailView) return; + const tabs = this.detailView.querySelectorAll("swp-tab-bar > swp-tab[data-tab]"); + tabs.forEach((tab) => { + tab.addEventListener("click", () => { + const targetTab = tab.dataset.tab; + if (targetTab) { + this.switchTab(this.detailView, targetTab); + } + }); + }); + } + /** + * Switch to a specific tab within a container + */ + switchTab(container, targetTab) { + const tabs = container.querySelectorAll("swp-tab-bar > swp-tab[data-tab]"); + const contents = container.querySelectorAll("swp-tab-content[data-tab]"); + tabs.forEach((t) => { + t.classList.toggle("active", t.dataset.tab === targetTab); + }); + contents.forEach((content) => { + content.classList.toggle("active", content.dataset.tab === targetTab); + }); + } + /** + * Setup row click to show detail view + * Ignores clicks on action buttons + */ + setupChevronNavigation() { + document.addEventListener("click", (e) => { + const target = e.target; + if (target.closest("swp-icon-btn") || target.closest("swp-table-actions")) { + return; + } + const row = target.closest("swp-employee-row[data-employee-detail]"); + if (row) { + const employeeKey = row.dataset.employeeDetail; + if (employeeKey) { + this.showDetailView(employeeKey); + } + } + }); + } + /** + * Setup back button to return to list view + */ + setupBackNavigation() { + document.addEventListener("click", (e) => { + const target = e.target; + const backLink = target.closest("[data-employee-back]"); + if (backLink) { + this.showListView(); + } + }); + } + /** + * Show the detail view and hide list view (with history push) + */ + showDetailView(employeeKey) { + history.pushState( + { employeeKey }, + "", + `#${employeeKey}` + ); + this.showDetailViewInternal(employeeKey); + } + /** + * Show detail view without modifying history (for popstate) + */ + showDetailViewInternal(employeeKey) { + if (this.listView && this.detailView) { + this.listView.style.display = "none"; + this.detailView.style.display = "block"; + this.detailView.dataset.employee = employeeKey; + this.switchTab(this.detailView, "general"); + } + } + /** + * Show the list view and hide detail view (with history push) + */ + showListView() { + history.pushState( + {}, + "", + window.location.pathname + ); + this.showListViewInternal(); + } + /** + * Show list view without modifying history (for popstate) + */ + showListViewInternal() { + if (this.listView && this.detailView) { + this.detailView.style.display = "none"; + this.listView.style.display = "block"; + } + } + }; + + // wwwroot/ts/app.ts + var App = class { + constructor() { + this.sidebar = new SidebarController(); + this.drawers = new DrawerController(); + this.theme = new ThemeController(); + this.search = new SearchController(); + this.lockScreen = new LockScreenController(this.drawers); + this.cash = new CashController(); + this.employees = new EmployeesController(); + } + }; + var app; + function init() { + app = new App(); + if (typeof window !== "undefined") { + window.app = app; } } - /** - * Setup draft row click to navigate to reconciliation tab - */ - setupDraftRowClick() { - const draftRow = document.querySelector("swp-cash-table-row.draft-row"); - if (!draftRow) return; - draftRow.style.cursor = "pointer"; - draftRow.addEventListener("click", (e) => { - if (e.target.closest('input[type="checkbox"]')) return; - this.switchToTab("afstemning"); - }); + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); } -}; -__name(_CashController, "CashController"); -var CashController = _CashController; - -// wwwroot/ts/app.ts -var _App = class _App { - constructor() { - this.sidebar = new SidebarController(); - this.drawers = new DrawerController(); - this.theme = new ThemeController(); - this.search = new SearchController(); - this.lockScreen = new LockScreenController(this.drawers); - this.cash = new CashController(); - } -}; -__name(_App, "App"); -var App = _App; -var app; -function init() { - app = new App(); - if (typeof window !== "undefined") { - window.app = app; - } -} -__name(init, "init"); -if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", init); -} else { - init(); -} -var app_default = App; -export { - App, - app, - app_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiLi4vdHMvbW9kdWxlcy9zaWRlYmFyLnRzIiwgIi4uL3RzL21vZHVsZXMvZHJhd2Vycy50cyIsICIuLi90cy9tb2R1bGVzL3RoZW1lLnRzIiwgIi4uL3RzL21vZHVsZXMvc2VhcmNoLnRzIiwgIi4uL3RzL21vZHVsZXMvbG9ja3NjcmVlbi50cyIsICIuLi90cy9tb2R1bGVzL2Nhc2gudHMiLCAiLi4vdHMvYXBwLnRzIl0sCiAgInNvdXJjZXNDb250ZW50IjogWyIvKipcbiAqIFNpZGViYXIgQ29udHJvbGxlclxuICpcbiAqIEhhbmRsZXMgc2lkZWJhciBjb2xsYXBzZS9leHBhbmQgYW5kIHRvb2x0aXAgZnVuY3Rpb25hbGl0eVxuICovXG5cbmV4cG9ydCBjbGFzcyBTaWRlYmFyQ29udHJvbGxlciB7XG4gIHByaXZhdGUgbWVudVRvZ2dsZTogSFRNTEVsZW1lbnQgfCBudWxsID0gbnVsbDtcbiAgcHJpdmF0ZSBhcHBMYXlvdXQ6IEhUTUxFbGVtZW50IHwgbnVsbCA9IG51bGw7XG4gIHByaXZhdGUgbWVudVRvb2x0aXA6IEhUTUxFbGVtZW50IHwgbnVsbCA9IG51bGw7XG5cbiAgY29uc3RydWN0b3IoKSB7XG4gICAgdGhpcy5tZW51VG9nZ2xlID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ21lbnVUb2dnbGUnKTtcbiAgICB0aGlzLmFwcExheW91dCA9IGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IoJ3N3cC1hcHAtbGF5b3V0Jyk7XG4gICAgdGhpcy5tZW51VG9vbHRpcCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdtZW51VG9vbHRpcCcpO1xuXG4gICAgdGhpcy5zZXR1cExpc3RlbmVycygpO1xuICAgIHRoaXMuc2V0dXBUb29sdGlwcygpO1xuICAgIHRoaXMucmVzdG9yZVN0YXRlKCk7XG4gIH1cblxuICAvKipcbiAgICogQ2hlY2sgaWYgc2lkZWJhciBpcyBjb2xsYXBzZWRcbiAgICovXG4gIGdldCBpc0NvbGxhcHNlZCgpOiBib29sZWFuIHtcbiAgICByZXR1cm4gdGhpcy5hcHBMYXlvdXQ/LmNsYXNzTGlzdC5jb250YWlucygnbWVudS1jb2xsYXBzZWQnKSA/PyBmYWxzZTtcbiAgfVxuXG4gIC8qKlxuICAgKiBUb2dnbGUgc2lkZWJhciBjb2xsYXBzZWQgc3RhdGVcbiAgICovXG4gIHRvZ2dsZSgpOiB2b2lkIHtcbiAgICBpZiAoIXRoaXMuYXBwTGF5b3V0KSByZXR1cm47XG5cbiAgICB0aGlzLmFwcExheW91dC5jbGFzc0xpc3QudG9nZ2xlKCdtZW51LWNvbGxhcHNlZCcpO1xuICAgIGxvY2FsU3RvcmFnZS5zZXRJdGVtKCdzaWRlYmFyLWNvbGxhcHNlZCcsIFN0cmluZyh0aGlzLmlzQ29sbGFwc2VkKSk7XG4gIH1cblxuICAvKipcbiAgICogQ29sbGFwc2UgdGhlIHNpZGViYXJcbiAgICovXG4gIGNvbGxhcHNlKCk6IHZvaWQge1xuICAgIHRoaXMuYXBwTGF5b3V0Py5jbGFzc0xpc3QuYWRkKCdtZW51LWNvbGxhcHNlZCcpO1xuICAgIGxvY2FsU3RvcmFnZS5zZXRJdGVtKCdzaWRlYmFyLWNvbGxhcHNlZCcsICd0cnVlJyk7XG4gIH1cblxuICAvKipcbiAgICogRXhwYW5kIHRoZSBzaWRlYmFyXG4gICAqL1xuICBleHBhbmQoKTogdm9pZCB7XG4gICAgdGhpcy5hcHBMYXlvdXQ/LmNsYXNzTGlzdC5yZW1vdmUoJ21lbnUtY29sbGFwc2VkJyk7XG4gICAgbG9jYWxTdG9yYWdlLnNldEl0ZW0oJ3NpZGViYXItY29sbGFwc2VkJywgJ2ZhbHNlJyk7XG4gIH1cblxuICBwcml2YXRlIHNldHVwTGlzdGVuZXJzKCk6IHZvaWQge1xuICAgIHRoaXMubWVudVRvZ2dsZT8uYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiB0aGlzLnRvZ2dsZSgpKTtcbiAgfVxuXG4gIHByaXZhdGUgc2V0dXBUb29sdGlwcygpOiB2b2lkIHtcbiAgICBpZiAoIXRoaXMubWVudVRvb2x0aXApIHJldHVybjtcblxuICAgIGNvbnN0IG1lbnVJdGVtcyA9IGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3JBbGw8SFRNTEVsZW1lbnQ+KCdzd3Atc2lkZS1tZW51LWl0ZW1bZGF0YS10b29sdGlwXScpO1xuXG4gICAgbWVudUl0ZW1zLmZvckVhY2goaXRlbSA9PiB7XG4gICAgICBpdGVtLmFkZEV2ZW50TGlzdGVuZXIoJ21vdXNlZW50ZXInLCAoKSA9PiB0aGlzLnNob3dUb29sdGlwKGl0ZW0pKTtcbiAgICAgIGl0ZW0uYWRkRXZlbnRMaXN0ZW5lcignbW91c2VsZWF2ZScsICgpID0+IHRoaXMuaGlkZVRvb2x0aXAoKSk7XG4gICAgfSk7XG4gIH1cblxuICBwcml2YXRlIHNob3dUb29sdGlwKGl0ZW06IEhUTUxFbGVtZW50KTogdm9pZCB7XG4gICAgaWYgKCF0aGlzLmlzQ29sbGFwc2VkIHx8ICF0aGlzLm1lbnVUb29sdGlwKSByZXR1cm47XG5cbiAgICBjb25zdCByZWN0ID0gaXRlbS5nZXRCb3VuZGluZ0NsaWVudFJlY3QoKTtcbiAgICBjb25zdCB0b29sdGlwVGV4dCA9IGl0ZW0uZGF0YXNldC50b29sdGlwO1xuXG4gICAgaWYgKCF0b29sdGlwVGV4dCkgcmV0dXJuO1xuXG4gICAgdGhpcy5tZW51VG9vbHRpcC50ZXh0Q29udGVudCA9IHRvb2x0aXBUZXh0O1xuICAgIHRoaXMubWVudVRvb2x0aXAuc3R5bGUubGVmdCA9IGAke3JlY3QucmlnaHQgKyA4fXB4YDtcbiAgICB0aGlzLm1lbnVUb29sdGlwLnN0eWxlLnRvcCA9IGAke3JlY3QudG9wICsgcmVjdC5oZWlnaHQgLyAyfXB4YDtcbiAgICB0aGlzLm1lbnVUb29sdGlwLnN0eWxlLnRyYW5zZm9ybSA9ICd0cmFuc2xhdGVZKC01MCUpJztcbiAgICB0aGlzLm1lbnVUb29sdGlwLnNob3dQb3BvdmVyKCk7XG4gIH1cblxuICBwcml2YXRlIGhpZGVUb29sdGlwKCk6IHZvaWQge1xuICAgIHRoaXMubWVudVRvb2x0aXA/LmhpZGVQb3BvdmVyKCk7XG4gIH1cblxuICBwcml2YXRlIHJlc3RvcmVTdGF0ZSgpOiB2b2lkIHtcbiAgICBpZiAoIXRoaXMuYXBwTGF5b3V0KSByZXR1cm47XG5cbiAgICBpZiAobG9jYWxTdG9yYWdlLmdldEl0ZW0oJ3NpZGViYXItY29sbGFwc2VkJykgPT09ICd0cnVlJykge1xuICAgICAgdGhpcy5hcHBMYXlvdXQuY2xhc3NMaXN0LmFkZCgnbWVudS1jb2xsYXBzZWQnKTtcbiAgICB9XG4gIH1cbn1cbiIsICIvKipcbiAqIERyYXdlciBDb250cm9sbGVyXG4gKlxuICogSGFuZGxlcyBhbGwgZHJhd2VyIGZ1bmN0aW9uYWxpdHkgaW5jbHVkaW5nIHByb2ZpbGUsIG5vdGlmaWNhdGlvbnMsIGFuZCB0b2RvIGRyYXdlcnNcbiAqL1xuXG5leHBvcnQgdHlwZSBEcmF3ZXJOYW1lID0gJ3Byb2ZpbGUnIHwgJ25vdGlmaWNhdGlvbicgfCAndG9kbycgfCAnbmV3VG9kbyc7XG5cbmV4cG9ydCBjbGFzcyBEcmF3ZXJDb250cm9sbGVyIHtcbiAgcHJpdmF0ZSBwcm9maWxlRHJhd2VyOiBIVE1MRWxlbWVudCB8IG51bGwgPSBudWxsO1xuICBwcml2YXRlIG5vdGlmaWNhdGlvbkRyYXdlcjogSFRNTEVsZW1lbnQgfCBudWxsID0gbnVsbDtcbiAgcHJpdmF0ZSB0b2RvRHJhd2VyOiBIVE1MRWxlbWVudCB8IG51bGwgPSBudWxsO1xuICBwcml2YXRlIG5ld1RvZG9EcmF3ZXI6IEhUTUxFbGVtZW50IHwgbnVsbCA9IG51bGw7XG4gIHByaXZhdGUgb3ZlcmxheTogSFRNTEVsZW1lbnQgfCBudWxsID0gbnVsbDtcbiAgcHJpdmF0ZSBhY3RpdmVEcmF3ZXI6IERyYXdlck5hbWUgfCBudWxsID0gbnVsbDtcbiAgcHJpdmF0ZSBhY3RpdmVHZW5lcmljRHJhd2VyOiBIVE1MRWxlbWVudCB8IG51bGwgPSBudWxsO1xuXG4gIGNvbnN0cnVjdG9yKCkge1xuICAgIHRoaXMucHJvZmlsZURyYXdlciA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdwcm9maWxlRHJhd2VyJyk7XG4gICAgdGhpcy5ub3RpZmljYXRpb25EcmF3ZXIgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnbm90aWZpY2F0aW9uRHJhd2VyJyk7XG4gICAgdGhpcy50b2RvRHJhd2VyID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ3RvZG9EcmF3ZXInKTtcbiAgICB0aGlzLm5ld1RvZG9EcmF3ZXIgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnbmV3VG9kb0RyYXdlcicpO1xuICAgIHRoaXMub3ZlcmxheSA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdkcmF3ZXJPdmVybGF5Jyk7XG5cbiAgICB0aGlzLnNldHVwTGlzdGVuZXJzKCk7XG4gICAgdGhpcy5zZXR1cEdlbmVyaWNEcmF3ZXJzKCk7XG4gIH1cblxuICAvKipcbiAgICogR2V0IGN1cnJlbnRseSBhY3RpdmUgZHJhd2VyIG5hbWVcbiAgICovXG4gIGdldCBhY3RpdmUoKTogRHJhd2VyTmFtZSB8IG51bGwge1xuICAgIHJldHVybiB0aGlzLmFjdGl2ZURyYXdlcjtcbiAgfVxuXG4gIC8qKlxuICAgKiBPcGVuIGEgZHJhd2VyIGJ5IG5hbWVcbiAgICovXG4gIG9wZW4obmFtZTogRHJhd2VyTmFtZSk6IHZvaWQge1xuICAgIHRoaXMuY2xvc2VBbGwoKTtcblxuICAgIGNvbnN0IGRyYXdlciA9IHRoaXMuZ2V0RHJhd2VyKG5hbWUpO1xuICAgIGlmIChkcmF3ZXIgJiYgdGhpcy5vdmVybGF5KSB7XG4gICAgICBkcmF3ZXIuY2xhc3NMaXN0LmFkZCgnYWN0aXZlJyk7XG4gICAgICB0aGlzLm92ZXJsYXkuY2xhc3NMaXN0LmFkZCgnYWN0aXZlJyk7XG4gICAgICBkb2N1bWVudC5ib2R5LnN0eWxlLm92ZXJmbG93ID0gJ2hpZGRlbic7XG4gICAgICB0aGlzLmFjdGl2ZURyYXdlciA9IG5hbWU7XG4gICAgfVxuICB9XG5cbiAgLyoqXG4gICAqIENsb3NlIGEgc3BlY2lmaWMgZHJhd2VyXG4gICAqL1xuICBjbG9zZShuYW1lOiBEcmF3ZXJOYW1lKTogdm9pZCB7XG4gICAgY29uc3QgZHJhd2VyID0gdGhpcy5nZXREcmF3ZXIobmFtZSk7XG4gICAgZHJhd2VyPy5jbGFzc0xpc3QucmVtb3ZlKCdhY3RpdmUnKTtcblxuICAgIC8vIE9ubHkgaGlkZSBvdmVybGF5IGlmIG5vIGRyYXdlcnMgYXJlIGFjdGl2ZVxuICAgIGlmICh0aGlzLm92ZXJsYXkgJiYgIWRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IoJy5hY3RpdmVbY2xhc3MqPVwiZHJhd2VyXCJdJykpIHtcbiAgICAgIHRoaXMub3ZlcmxheS5jbGFzc0xpc3QucmVtb3ZlKCdhY3RpdmUnKTtcbiAgICAgIGRvY3VtZW50LmJvZHkuc3R5bGUub3ZlcmZsb3cgPSAnJztcbiAgICB9XG5cbiAgICBpZiAodGhpcy5hY3RpdmVEcmF3ZXIgPT09IG5hbWUpIHtcbiAgICAgIHRoaXMuYWN0aXZlRHJhd2VyID0gbnVsbDtcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogQ2xvc2UgYWxsIGRyYXdlcnNcbiAgICovXG4gIGNsb3NlQWxsKCk6IHZvaWQge1xuICAgIFt0aGlzLnByb2ZpbGVEcmF3ZXIsIHRoaXMubm90aWZpY2F0aW9uRHJhd2VyLCB0aGlzLnRvZG9EcmF3ZXIsIHRoaXMubmV3VG9kb0RyYXdlcl1cbiAgICAgIC5mb3JFYWNoKGRyYXdlciA9PiBkcmF3ZXI/LmNsYXNzTGlzdC5yZW1vdmUoJ2FjdGl2ZScpKTtcblxuICAgIC8vIENsb3NlIGFueSBnZW5lcmljIGRyYXdlcnNcbiAgICB0aGlzLmNsb3NlR2VuZXJpY0RyYXdlcigpO1xuXG4gICAgdGhpcy5vdmVybGF5Py5jbGFzc0xpc3QucmVtb3ZlKCdhY3RpdmUnKTtcbiAgICBkb2N1bWVudC5ib2R5LnN0eWxlLm92ZXJmbG93ID0gJyc7XG4gICAgdGhpcy5hY3RpdmVEcmF3ZXIgPSBudWxsO1xuICB9XG5cbiAgLyoqXG4gICAqIE9wZW4gYSBnZW5lcmljIGRyYXdlciBieSBJRFxuICAgKi9cbiAgb3BlbkdlbmVyaWNEcmF3ZXIoZHJhd2VySWQ6IHN0cmluZyk6IHZvaWQge1xuICAgIHRoaXMuY2xvc2VBbGwoKTtcblxuICAgIGNvbnN0IGRyYXdlciA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKGRyYXdlcklkKTtcbiAgICBpZiAoZHJhd2VyICYmIHRoaXMub3ZlcmxheSkge1xuICAgICAgZHJhd2VyLmNsYXNzTGlzdC5hZGQoJ29wZW4nKTtcbiAgICAgIHRoaXMub3ZlcmxheS5jbGFzc0xpc3QuYWRkKCdhY3RpdmUnKTtcbiAgICAgIGRvY3VtZW50LmJvZHkuc3R5bGUub3ZlcmZsb3cgPSAnaGlkZGVuJztcbiAgICAgIHRoaXMuYWN0aXZlR2VuZXJpY0RyYXdlciA9IGRyYXdlcjtcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogQ2xvc2UgdGhlIGN1cnJlbnRseSBvcGVuIGdlbmVyaWMgZHJhd2VyXG4gICAqL1xuICBjbG9zZUdlbmVyaWNEcmF3ZXIoKTogdm9pZCB7XG4gICAgdGhpcy5hY3RpdmVHZW5lcmljRHJhd2VyPy5jbGFzc0xpc3QucmVtb3ZlKCdvcGVuJyk7XG4gICAgdGhpcy5hY3RpdmVHZW5lcmljRHJhd2VyID0gbnVsbDtcbiAgfVxuXG4gIC8qKlxuICAgKiBPcGVuIHByb2ZpbGUgZHJhd2VyXG4gICAqL1xuICBvcGVuUHJvZmlsZSgpOiB2b2lkIHtcbiAgICB0aGlzLm9wZW4oJ3Byb2ZpbGUnKTtcbiAgfVxuXG4gIC8qKlxuICAgKiBPcGVuIG5vdGlmaWNhdGlvbiBkcmF3ZXJcbiAgICovXG4gIG9wZW5Ob3RpZmljYXRpb24oKTogdm9pZCB7XG4gICAgdGhpcy5vcGVuKCdub3RpZmljYXRpb24nKTtcbiAgfVxuXG4gIC8qKlxuICAgKiBPcGVuIHRvZG8gZHJhd2VyIChzbGlkZXMgb24gdG9wIG9mIHByb2ZpbGUpXG4gICAqL1xuICBvcGVuVG9kbygpOiB2b2lkIHtcbiAgICB0aGlzLnRvZG9EcmF3ZXI/LmNsYXNzTGlzdC5hZGQoJ2FjdGl2ZScpO1xuICB9XG5cbiAgLyoqXG4gICAqIENsb3NlIHRvZG8gZHJhd2VyXG4gICAqL1xuICBjbG9zZVRvZG8oKTogdm9pZCB7XG4gICAgdGhpcy50b2RvRHJhd2VyPy5jbGFzc0xpc3QucmVtb3ZlKCdhY3RpdmUnKTtcbiAgICB0aGlzLmNsb3NlTmV3VG9kbygpO1xuICB9XG5cbiAgLyoqXG4gICAqIE9wZW4gbmV3IHRvZG8gZHJhd2VyXG4gICAqL1xuICBvcGVuTmV3VG9kbygpOiB2b2lkIHtcbiAgICB0aGlzLm5ld1RvZG9EcmF3ZXI/LmNsYXNzTGlzdC5hZGQoJ2FjdGl2ZScpO1xuICB9XG5cbiAgLyoqXG4gICAqIENsb3NlIG5ldyB0b2RvIGRyYXdlclxuICAgKi9cbiAgY2xvc2VOZXdUb2RvKCk6IHZvaWQge1xuICAgIHRoaXMubmV3VG9kb0RyYXdlcj8uY2xhc3NMaXN0LnJlbW92ZSgnYWN0aXZlJyk7XG4gIH1cblxuICAvKipcbiAgICogTWFyayBhbGwgbm90aWZpY2F0aW9ucyBhcyByZWFkXG4gICAqL1xuICBtYXJrQWxsTm90aWZpY2F0aW9uc1JlYWQoKTogdm9pZCB7XG4gICAgaWYgKCF0aGlzLm5vdGlmaWNhdGlvbkRyYXdlcikgcmV0dXJuO1xuXG4gICAgY29uc3QgdW5yZWFkSXRlbXMgPSB0aGlzLm5vdGlmaWNhdGlvbkRyYXdlci5xdWVyeVNlbGVjdG9yQWxsPEhUTUxFbGVtZW50PihcbiAgICAgICdzd3Atbm90aWZpY2F0aW9uLWl0ZW1bZGF0YS11bnJlYWQ9XCJ0cnVlXCJdJ1xuICAgICk7XG4gICAgdW5yZWFkSXRlbXMuZm9yRWFjaChpdGVtID0+IGl0ZW0ucmVtb3ZlQXR0cmlidXRlKCdkYXRhLXVucmVhZCcpKTtcblxuICAgIGNvbnN0IGJhZGdlID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvcjxIVE1MRWxlbWVudD4oJ3N3cC1ub3RpZmljYXRpb24tYmFkZ2UnKTtcbiAgICBpZiAoYmFkZ2UpIHtcbiAgICAgIGJhZGdlLnN0eWxlLmRpc3BsYXkgPSAnbm9uZSc7XG4gICAgfVxuICB9XG5cbiAgcHJpdmF0ZSBnZXREcmF3ZXIobmFtZTogRHJhd2VyTmFtZSk6IEhUTUxFbGVtZW50IHwgbnVsbCB7XG4gICAgc3dpdGNoIChuYW1lKSB7XG4gICAgICBjYXNlICdwcm9maWxlJzogcmV0dXJuIHRoaXMucHJvZmlsZURyYXdlcjtcbiAgICAgIGNhc2UgJ25vdGlmaWNhdGlvbic6IHJldHVybiB0aGlzLm5vdGlmaWNhdGlvbkRyYXdlcjtcbiAgICAgIGNhc2UgJ3RvZG8nOiByZXR1cm4gdGhpcy50b2RvRHJhd2VyO1xuICAgICAgY2FzZSAnbmV3VG9kbyc6IHJldHVybiB0aGlzLm5ld1RvZG9EcmF3ZXI7XG4gICAgfVxuICB9XG5cbiAgcHJpdmF0ZSBzZXR1cExpc3RlbmVycygpOiB2b2lkIHtcbiAgICAvLyBQcm9maWxlIGRyYXdlciB0cmlnZ2Vyc1xuICAgIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdwcm9maWxlVHJpZ2dlcicpXG4gICAgICA/LmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgKCkgPT4gdGhpcy5vcGVuUHJvZmlsZSgpKTtcbiAgICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnZHJhd2VyQ2xvc2UnKVxuICAgICAgPy5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHRoaXMuY2xvc2UoJ3Byb2ZpbGUnKSk7XG5cbiAgICAvLyBOb3RpZmljYXRpb24gZHJhd2VyIHRyaWdnZXJzXG4gICAgZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ25vdGlmaWNhdGlvbnNCdG4nKVxuICAgICAgPy5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHRoaXMub3Blbk5vdGlmaWNhdGlvbigpKTtcbiAgICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnbm90aWZpY2F0aW9uRHJhd2VyQ2xvc2UnKVxuICAgICAgPy5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHRoaXMuY2xvc2UoJ25vdGlmaWNhdGlvbicpKTtcbiAgICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnbWFya0FsbFJlYWQnKVxuICAgICAgPy5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHRoaXMubWFya0FsbE5vdGlmaWNhdGlvbnNSZWFkKCkpO1xuXG4gICAgLy8gVG9kbyBkcmF3ZXIgdHJpZ2dlcnNcbiAgICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnb3BlblRvZG9EcmF3ZXInKVxuICAgICAgPy5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHRoaXMub3BlblRvZG8oKSk7XG4gICAgZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ3RvZG9EcmF3ZXJCYWNrJylcbiAgICAgID8uYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiB0aGlzLmNsb3NlVG9kbygpKTtcblxuICAgIC8vIE5ldyB0b2RvIGRyYXdlciB0cmlnZ2Vyc1xuICAgIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdhZGRUb2RvQnRuJylcbiAgICAgID8uYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiB0aGlzLm9wZW5OZXdUb2RvKCkpO1xuICAgIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCduZXdUb2RvRHJhd2VyQmFjaycpXG4gICAgICA/LmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgKCkgPT4gdGhpcy5jbG9zZU5ld1RvZG8oKSk7XG4gICAgZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ2NhbmNlbE5ld1RvZG8nKVxuICAgICAgPy5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHRoaXMuY2xvc2VOZXdUb2RvKCkpO1xuICAgIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdzYXZlTmV3VG9kbycpXG4gICAgICA/LmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgKCkgPT4gdGhpcy5jbG9zZU5ld1RvZG8oKSk7XG5cbiAgICAvLyBPdmVybGF5IGNsaWNrIGNsb3NlcyBhbGxcbiAgICB0aGlzLm92ZXJsYXk/LmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgKCkgPT4gdGhpcy5jbG9zZUFsbCgpKTtcblxuICAgIC8vIEVzY2FwZSBrZXkgY2xvc2VzIGFsbFxuICAgIGRvY3VtZW50LmFkZEV2ZW50TGlzdGVuZXIoJ2tleWRvd24nLCAoZTogS2V5Ym9hcmRFdmVudCkgPT4ge1xuICAgICAgaWYgKGUua2V5ID09PSAnRXNjYXBlJykgdGhpcy5jbG9zZUFsbCgpO1xuICAgIH0pO1xuXG4gICAgLy8gVG9kbyBpbnRlcmFjdGlvbnNcbiAgICB0aGlzLnRvZG9EcmF3ZXI/LmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgKGUpID0+IHRoaXMuaGFuZGxlVG9kb0NsaWNrKGUpKTtcblxuICAgIC8vIFZpc2liaWxpdHkgb3B0aW9uc1xuICAgIGRvY3VtZW50LmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgKGUpID0+IHRoaXMuaGFuZGxlVmlzaWJpbGl0eUNsaWNrKGUpKTtcbiAgfVxuXG4gIHByaXZhdGUgaGFuZGxlVG9kb0NsaWNrKGU6IEV2ZW50KTogdm9pZCB7XG4gICAgY29uc3QgdGFyZ2V0ID0gZS50YXJnZXQgYXMgSFRNTEVsZW1lbnQ7XG4gICAgY29uc3QgdG9kb0l0ZW0gPSB0YXJnZXQuY2xvc2VzdDxIVE1MRWxlbWVudD4oJ3N3cC10b2RvLWl0ZW0nKTtcbiAgICBjb25zdCBjaGVja2JveCA9IHRhcmdldC5jbG9zZXN0PEhUTUxFbGVtZW50Pignc3dwLXRvZG8tY2hlY2tib3gnKTtcblxuICAgIGlmIChjaGVja2JveCAmJiB0b2RvSXRlbSkge1xuICAgICAgY29uc3QgaXNDb21wbGV0ZWQgPSB0b2RvSXRlbS5kYXRhc2V0LmNvbXBsZXRlZCA9PT0gJ3RydWUnO1xuICAgICAgaWYgKGlzQ29tcGxldGVkKSB7XG4gICAgICAgIHRvZG9JdGVtLnJlbW92ZUF0dHJpYnV0ZSgnZGF0YS1jb21wbGV0ZWQnKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHRvZG9JdGVtLmRhdGFzZXQuY29tcGxldGVkID0gJ3RydWUnO1xuICAgICAgfVxuICAgIH1cblxuICAgIC8vIFRvZ2dsZSBzZWN0aW9uIGNvbGxhcHNlXG4gICAgY29uc3Qgc2VjdGlvbkhlYWRlciA9IHRhcmdldC5jbG9zZXN0PEhUTUxFbGVtZW50Pignc3dwLXRvZG8tc2VjdGlvbi1oZWFkZXInKTtcbiAgICBpZiAoc2VjdGlvbkhlYWRlcikge1xuICAgICAgY29uc3Qgc2VjdGlvbiA9IHNlY3Rpb25IZWFkZXIuY2xvc2VzdDxIVE1MRWxlbWVudD4oJ3N3cC10b2RvLXNlY3Rpb24nKTtcbiAgICAgIHNlY3Rpb24/LmNsYXNzTGlzdC50b2dnbGUoJ2NvbGxhcHNlZCcpO1xuICAgIH1cbiAgfVxuXG4gIHByaXZhdGUgaGFuZGxlVmlzaWJpbGl0eUNsaWNrKGU6IEV2ZW50KTogdm9pZCB7XG4gICAgY29uc3QgdGFyZ2V0ID0gZS50YXJnZXQgYXMgSFRNTEVsZW1lbnQ7XG4gICAgY29uc3Qgb3B0aW9uID0gdGFyZ2V0LmNsb3Nlc3Q8SFRNTEVsZW1lbnQ+KCdzd3AtdmlzaWJpbGl0eS1vcHRpb24nKTtcblxuICAgIGlmIChvcHRpb24pIHtcbiAgICAgIGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3JBbGw8SFRNTEVsZW1lbnQ+KCdzd3AtdmlzaWJpbGl0eS1vcHRpb24nKVxuICAgICAgICAuZm9yRWFjaChvID0+IG8uY2xhc3NMaXN0LnJlbW92ZSgnYWN0aXZlJykpO1xuICAgICAgb3B0aW9uLmNsYXNzTGlzdC5hZGQoJ2FjdGl2ZScpO1xuICAgIH1cbiAgfVxuXG4gIC8qKlxuICAgKiBTZXR1cCBnZW5lcmljIGRyYXdlciB0cmlnZ2VycyBhbmQgY2xvc2UgYnV0dG9uc1xuICAgKiBVc2VzIGRhdGEtZHJhd2VyLXRyaWdnZXI9XCJkcmF3ZXItaWRcIiBhbmQgZGF0YS1kcmF3ZXItY2xvc2UgYXR0cmlidXRlc1xuICAgKi9cbiAgcHJpdmF0ZSBzZXR1cEdlbmVyaWNEcmF3ZXJzKCk6IHZvaWQge1xuICAgIC8vIEhhbmRsZSBkcmF3ZXIgdHJpZ2dlcnNcbiAgICBkb2N1bWVudC5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsIChlOiBFdmVudCkgPT4ge1xuICAgICAgY29uc3QgdGFyZ2V0ID0gZS50YXJnZXQgYXMgSFRNTEVsZW1lbnQ7XG4gICAgICBjb25zdCB0cmlnZ2VyID0gdGFyZ2V0LmNsb3Nlc3Q8SFRNTEVsZW1lbnQ+KCdbZGF0YS1kcmF3ZXItdHJpZ2dlcl0nKTtcblxuICAgICAgaWYgKHRyaWdnZXIpIHtcbiAgICAgICAgY29uc3QgZHJhd2VySWQgPSB0cmlnZ2VyLmRhdGFzZXQuZHJhd2VyVHJpZ2dlcjtcbiAgICAgICAgaWYgKGRyYXdlcklkKSB7XG4gICAgICAgICAgdGhpcy5vcGVuR2VuZXJpY0RyYXdlcihkcmF3ZXJJZCk7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9KTtcblxuICAgIC8vIEhhbmRsZSBkcmF3ZXIgY2xvc2UgYnV0dG9uc1xuICAgIGRvY3VtZW50LmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgKGU6IEV2ZW50KSA9PiB7XG4gICAgICBjb25zdCB0YXJnZXQgPSBlLnRhcmdldCBhcyBIVE1MRWxlbWVudDtcbiAgICAgIGNvbnN0IGNsb3NlQnRuID0gdGFyZ2V0LmNsb3Nlc3Q8SFRNTEVsZW1lbnQ+KCdbZGF0YS1kcmF3ZXItY2xvc2VdJyk7XG5cbiAgICAgIGlmIChjbG9zZUJ0bikge1xuICAgICAgICB0aGlzLmNsb3NlR2VuZXJpY0RyYXdlcigpO1xuICAgICAgICB0aGlzLm92ZXJsYXk/LmNsYXNzTGlzdC5yZW1vdmUoJ2FjdGl2ZScpO1xuICAgICAgICBkb2N1bWVudC5ib2R5LnN0eWxlLm92ZXJmbG93ID0gJyc7XG4gICAgICB9XG4gICAgfSk7XG4gIH1cbn1cbiIsICIvKipcbiAqIFRoZW1lIENvbnRyb2xsZXJcbiAqXG4gKiBIYW5kbGVzIGRhcmsvbGlnaHQgbW9kZSBzd2l0Y2hpbmcgYW5kIHN5c3RlbSBwcmVmZXJlbmNlIGRldGVjdGlvblxuICovXG5cbmV4cG9ydCB0eXBlIFRoZW1lID0gJ2xpZ2h0JyB8ICdkYXJrJyB8ICdzeXN0ZW0nO1xuXG5leHBvcnQgY2xhc3MgVGhlbWVDb250cm9sbGVyIHtcbiAgcHJpdmF0ZSBzdGF0aWMgcmVhZG9ubHkgU1RPUkFHRV9LRVkgPSAndGhlbWUtcHJlZmVyZW5jZSc7XG4gIHByaXZhdGUgc3RhdGljIHJlYWRvbmx5IERBUktfQ0xBU1MgPSAnZGFyay1tb2RlJztcbiAgcHJpdmF0ZSBzdGF0aWMgcmVhZG9ubHkgTElHSFRfQ0xBU1MgPSAnbGlnaHQtbW9kZSc7XG5cbiAgcHJpdmF0ZSByb290OiBIVE1MRWxlbWVudDtcbiAgcHJpdmF0ZSB0aGVtZU9wdGlvbnM6IE5vZGVMaXN0T2Y8SFRNTEVsZW1lbnQ+O1xuXG4gIGNvbnN0cnVjdG9yKCkge1xuICAgIHRoaXMucm9vdCA9IGRvY3VtZW50LmRvY3VtZW50RWxlbWVudDtcbiAgICB0aGlzLnRoZW1lT3B0aW9ucyA9IGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3JBbGw8SFRNTEVsZW1lbnQ+KCdzd3AtdGhlbWUtb3B0aW9uJyk7XG5cbiAgICB0aGlzLmFwcGx5VGhlbWUodGhpcy5jdXJyZW50KTtcbiAgICB0aGlzLnVwZGF0ZVVJKCk7XG4gICAgdGhpcy5zZXR1cExpc3RlbmVycygpO1xuICB9XG5cbiAgLyoqXG4gICAqIEdldCB0aGUgY3VycmVudCB0aGVtZSBzZXR0aW5nXG4gICAqL1xuICBnZXQgY3VycmVudCgpOiBUaGVtZSB7XG4gICAgY29uc3Qgc3RvcmVkID0gbG9jYWxTdG9yYWdlLmdldEl0ZW0oVGhlbWVDb250cm9sbGVyLlNUT1JBR0VfS0VZKSBhcyBUaGVtZSB8IG51bGw7XG4gICAgaWYgKHN0b3JlZCA9PT0gJ2RhcmsnIHx8IHN0b3JlZCA9PT0gJ2xpZ2h0JyB8fCBzdG9yZWQgPT09ICdzeXN0ZW0nKSB7XG4gICAgICByZXR1cm4gc3RvcmVkO1xuICAgIH1cbiAgICByZXR1cm4gJ3N5c3RlbSc7XG4gIH1cblxuICAvKipcbiAgICogQ2hlY2sgaWYgZGFyayBtb2RlIGlzIGN1cnJlbnRseSBhY3RpdmVcbiAgICovXG4gIGdldCBpc0RhcmsoKTogYm9vbGVhbiB7XG4gICAgcmV0dXJuIHRoaXMucm9vdC5jbGFzc0xpc3QuY29udGFpbnMoVGhlbWVDb250cm9sbGVyLkRBUktfQ0xBU1MpIHx8XG4gICAgICAodGhpcy5zeXN0ZW1QcmVmZXJzRGFyayAmJiAhdGhpcy5yb290LmNsYXNzTGlzdC5jb250YWlucyhUaGVtZUNvbnRyb2xsZXIuTElHSFRfQ0xBU1MpKTtcbiAgfVxuXG4gIC8qKlxuICAgKiBDaGVjayBpZiBzeXN0ZW0gcHJlZmVycyBkYXJrIG1vZGVcbiAgICovXG4gIGdldCBzeXN0ZW1QcmVmZXJzRGFyaygpOiBib29sZWFuIHtcbiAgICByZXR1cm4gd2luZG93Lm1hdGNoTWVkaWEoJyhwcmVmZXJzLWNvbG9yLXNjaGVtZTogZGFyayknKS5tYXRjaGVzO1xuICB9XG5cbiAgLyoqXG4gICAqIFNldCB0aGVtZSBhbmQgcGVyc2lzdCBwcmVmZXJlbmNlXG4gICAqL1xuICBzZXQodGhlbWU6IFRoZW1lKTogdm9pZCB7XG4gICAgbG9jYWxTdG9yYWdlLnNldEl0ZW0oVGhlbWVDb250cm9sbGVyLlNUT1JBR0VfS0VZLCB0aGVtZSk7XG4gICAgdGhpcy5hcHBseVRoZW1lKHRoZW1lKTtcbiAgICB0aGlzLnVwZGF0ZVVJKCk7XG4gIH1cblxuICAvKipcbiAgICogVG9nZ2xlIGJldHdlZW4gbGlnaHQgYW5kIGRhcmsgdGhlbWVzXG4gICAqL1xuICB0b2dnbGUoKTogdm9pZCB7XG4gICAgdGhpcy5zZXQodGhpcy5pc0RhcmsgPyAnbGlnaHQnIDogJ2RhcmsnKTtcbiAgfVxuXG4gIHByaXZhdGUgYXBwbHlUaGVtZSh0aGVtZTogVGhlbWUpOiB2b2lkIHtcbiAgICB0aGlzLnJvb3QuY2xhc3NMaXN0LnJlbW92ZShUaGVtZUNvbnRyb2xsZXIuREFSS19DTEFTUywgVGhlbWVDb250cm9sbGVyLkxJR0hUX0NMQVNTKTtcblxuICAgIGlmICh0aGVtZSA9PT0gJ2RhcmsnKSB7XG4gICAgICB0aGlzLnJvb3QuY2xhc3NMaXN0LmFkZChUaGVtZUNvbnRyb2xsZXIuREFSS19DTEFTUyk7XG4gICAgfSBlbHNlIGlmICh0aGVtZSA9PT0gJ2xpZ2h0Jykge1xuICAgICAgdGhpcy5yb290LmNsYXNzTGlzdC5hZGQoVGhlbWVDb250cm9sbGVyLkxJR0hUX0NMQVNTKTtcbiAgICB9XG4gICAgLy8gJ3N5c3RlbScgbGVhdmVzIGJvdGggY2xhc3NlcyBvZmYsIGxldHRpbmcgQ1NTIG1lZGlhIHF1ZXJ5IGhhbmRsZSBpdFxuICB9XG5cbiAgcHJpdmF0ZSB1cGRhdGVVSSgpOiB2b2lkIHtcbiAgICBpZiAoIXRoaXMudGhlbWVPcHRpb25zKSByZXR1cm47XG5cbiAgICBjb25zdCBkYXJrQWN0aXZlID0gdGhpcy5pc0Rhcms7XG5cbiAgICB0aGlzLnRoZW1lT3B0aW9ucy5mb3JFYWNoKG9wdGlvbiA9PiB7XG4gICAgICBjb25zdCB0aGVtZSA9IG9wdGlvbi5kYXRhc2V0LnRoZW1lIGFzIFRoZW1lO1xuICAgICAgY29uc3QgaXNBY3RpdmUgPSAodGhlbWUgPT09ICdkYXJrJyAmJiBkYXJrQWN0aXZlKSB8fCAodGhlbWUgPT09ICdsaWdodCcgJiYgIWRhcmtBY3RpdmUpO1xuICAgICAgb3B0aW9uLmNsYXNzTGlzdC50b2dnbGUoJ2FjdGl2ZScsIGlzQWN0aXZlKTtcbiAgICB9KTtcbiAgfVxuXG4gIHByaXZhdGUgc2V0dXBMaXN0ZW5lcnMoKTogdm9pZCB7XG4gICAgLy8gVGhlbWUgb3B0aW9uIGNsaWNrc1xuICAgIHRoaXMudGhlbWVPcHRpb25zLmZvckVhY2gob3B0aW9uID0+IHtcbiAgICAgIG9wdGlvbi5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsIChlKSA9PiB0aGlzLmhhbmRsZU9wdGlvbkNsaWNrKGUpKTtcbiAgICB9KTtcblxuICAgIC8vIFN5c3RlbSB0aGVtZSBjaGFuZ2VzXG4gICAgd2luZG93Lm1hdGNoTWVkaWEoJyhwcmVmZXJzLWNvbG9yLXNjaGVtZTogZGFyayknKVxuICAgICAgLmFkZEV2ZW50TGlzdGVuZXIoJ2NoYW5nZScsICgpID0+IHRoaXMuaGFuZGxlU3lzdGVtQ2hhbmdlKCkpO1xuICB9XG5cbiAgcHJpdmF0ZSBoYW5kbGVPcHRpb25DbGljayhlOiBFdmVudCk6IHZvaWQge1xuICAgIGNvbnN0IHRhcmdldCA9IGUudGFyZ2V0IGFzIEhUTUxFbGVtZW50O1xuICAgIGNvbnN0IG9wdGlvbiA9IHRhcmdldC5jbG9zZXN0PEhUTUxFbGVtZW50Pignc3dwLXRoZW1lLW9wdGlvbicpO1xuXG4gICAgaWYgKG9wdGlvbikge1xuICAgICAgY29uc3QgdGhlbWUgPSBvcHRpb24uZGF0YXNldC50aGVtZSBhcyBUaGVtZTtcbiAgICAgIGlmICh0aGVtZSkge1xuICAgICAgICB0aGlzLnNldCh0aGVtZSk7XG4gICAgICB9XG4gICAgfVxuICB9XG5cbiAgcHJpdmF0ZSBoYW5kbGVTeXN0ZW1DaGFuZ2UoKTogdm9pZCB7XG4gICAgLy8gT25seSByZWFjdCB0byBzeXN0ZW0gY2hhbmdlcyBpZiB3ZSdyZSB1c2luZyBzeXN0ZW0gcHJlZmVyZW5jZVxuICAgIGlmICh0aGlzLmN1cnJlbnQgPT09ICdzeXN0ZW0nKSB7XG4gICAgICB0aGlzLnVwZGF0ZVVJKCk7XG4gICAgfVxuICB9XG59XG4iLCAiLyoqXG4gKiBTZWFyY2ggQ29udHJvbGxlclxuICpcbiAqIEhhbmRsZXMgZ2xvYmFsIHNlYXJjaCBmdW5jdGlvbmFsaXR5IGFuZCBrZXlib2FyZCBzaG9ydGN1dHNcbiAqL1xuXG5leHBvcnQgY2xhc3MgU2VhcmNoQ29udHJvbGxlciB7XG4gIHByaXZhdGUgaW5wdXQ6IEhUTUxJbnB1dEVsZW1lbnQgfCBudWxsID0gbnVsbDtcbiAgcHJpdmF0ZSBjb250YWluZXI6IEhUTUxFbGVtZW50IHwgbnVsbCA9IG51bGw7XG5cbiAgY29uc3RydWN0b3IoKSB7XG4gICAgdGhpcy5pbnB1dCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdnbG9iYWxTZWFyY2gnKSBhcyBIVE1MSW5wdXRFbGVtZW50IHwgbnVsbDtcbiAgICB0aGlzLmNvbnRhaW5lciA9IGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3I8SFRNTEVsZW1lbnQ+KCdzd3AtdG9wYmFyLXNlYXJjaCcpO1xuXG4gICAgdGhpcy5zZXR1cExpc3RlbmVycygpO1xuICB9XG5cbiAgLyoqXG4gICAqIEdldCBjdXJyZW50IHNlYXJjaCB2YWx1ZVxuICAgKi9cbiAgZ2V0IHZhbHVlKCk6IHN0cmluZyB7XG4gICAgcmV0dXJuIHRoaXMuaW5wdXQ/LnZhbHVlID8/ICcnO1xuICB9XG5cbiAgLyoqXG4gICAqIFNldCBzZWFyY2ggdmFsdWVcbiAgICovXG4gIHNldCB2YWx1ZSh2YWw6IHN0cmluZykge1xuICAgIGlmICh0aGlzLmlucHV0KSB7XG4gICAgICB0aGlzLmlucHV0LnZhbHVlID0gdmFsO1xuICAgIH1cbiAgfVxuXG4gIC8qKlxuICAgKiBGb2N1cyB0aGUgc2VhcmNoIGlucHV0XG4gICAqL1xuICBmb2N1cygpOiB2b2lkIHtcbiAgICB0aGlzLmlucHV0Py5mb2N1cygpO1xuICB9XG5cbiAgLyoqXG4gICAqIEJsdXIgdGhlIHNlYXJjaCBpbnB1dFxuICAgKi9cbiAgYmx1cigpOiB2b2lkIHtcbiAgICB0aGlzLmlucHV0Py5ibHVyKCk7XG4gIH1cblxuICAvKipcbiAgICogQ2xlYXIgdGhlIHNlYXJjaCBpbnB1dFxuICAgKi9cbiAgY2xlYXIoKTogdm9pZCB7XG4gICAgdGhpcy52YWx1ZSA9ICcnO1xuICB9XG5cbiAgcHJpdmF0ZSBzZXR1cExpc3RlbmVycygpOiB2b2lkIHtcbiAgICAvLyBLZXlib2FyZCBzaG9ydGN1dHNcbiAgICBkb2N1bWVudC5hZGRFdmVudExpc3RlbmVyKCdrZXlkb3duJywgKGUpID0+IHRoaXMuaGFuZGxlS2V5Ym9hcmQoZSkpO1xuXG4gICAgLy8gSW5wdXQgaGFuZGxlcnNcbiAgICBpZiAodGhpcy5pbnB1dCkge1xuICAgICAgdGhpcy5pbnB1dC5hZGRFdmVudExpc3RlbmVyKCdpbnB1dCcsIChlKSA9PiB0aGlzLmhhbmRsZUlucHV0KGUpKTtcblxuICAgICAgLy8gUHJldmVudCBmb3JtIHN1Ym1pc3Npb24gaWYgd3JhcHBlZCBpbiBmb3JtXG4gICAgICBjb25zdCBmb3JtID0gdGhpcy5pbnB1dC5jbG9zZXN0KCdmb3JtJyk7XG4gICAgICBmb3JtPy5hZGRFdmVudExpc3RlbmVyKCdzdWJtaXQnLCAoZSkgPT4gdGhpcy5oYW5kbGVTdWJtaXQoZSkpO1xuICAgIH1cbiAgfVxuXG4gIHByaXZhdGUgaGFuZGxlS2V5Ym9hcmQoZTogS2V5Ym9hcmRFdmVudCk6IHZvaWQge1xuICAgIC8vIENtZC9DdHJsICsgSyB0byBmb2N1cyBzZWFyY2hcbiAgICBpZiAoKGUubWV0YUtleSB8fCBlLmN0cmxLZXkpICYmIGUua2V5ID09PSAnaycpIHtcbiAgICAgIGUucHJldmVudERlZmF1bHQoKTtcbiAgICAgIHRoaXMuZm9jdXMoKTtcbiAgICAgIHJldHVybjtcbiAgICB9XG5cbiAgICAvLyBFc2NhcGUgdG8gYmx1ciBzZWFyY2ggd2hlbiBmb2N1c2VkXG4gICAgaWYgKGUua2V5ID09PSAnRXNjYXBlJyAmJiBkb2N1bWVudC5hY3RpdmVFbGVtZW50ID09PSB0aGlzLmlucHV0KSB7XG4gICAgICB0aGlzLmJsdXIoKTtcbiAgICB9XG4gIH1cblxuICBwcml2YXRlIGhhbmRsZUlucHV0KGU6IEV2ZW50KTogdm9pZCB7XG4gICAgY29uc3QgdGFyZ2V0ID0gZS50YXJnZXQgYXMgSFRNTElucHV0RWxlbWVudDtcbiAgICBjb25zdCBxdWVyeSA9IHRhcmdldC52YWx1ZS50cmltKCk7XG5cbiAgICAvLyBFbWl0IGN1c3RvbSBldmVudCBmb3Igc2VhcmNoXG4gICAgZG9jdW1lbnQuZGlzcGF0Y2hFdmVudChuZXcgQ3VzdG9tRXZlbnQoJ2FwcDpzZWFyY2gnLCB7XG4gICAgICBkZXRhaWw6IHsgcXVlcnkgfSxcbiAgICAgIGJ1YmJsZXM6IHRydWVcbiAgICB9KSk7XG4gIH1cblxuICBwcml2YXRlIGhhbmRsZVN1Ym1pdChlOiBFdmVudCk6IHZvaWQge1xuICAgIGUucHJldmVudERlZmF1bHQoKTtcblxuICAgIGNvbnN0IHF1ZXJ5ID0gdGhpcy52YWx1ZS50cmltKCk7XG4gICAgaWYgKCFxdWVyeSkgcmV0dXJuO1xuXG4gICAgLy8gRW1pdCBjdXN0b20gZXZlbnQgZm9yIHNlYXJjaCBzdWJtaXRcbiAgICBkb2N1bWVudC5kaXNwYXRjaEV2ZW50KG5ldyBDdXN0b21FdmVudCgnYXBwOnNlYXJjaC1zdWJtaXQnLCB7XG4gICAgICBkZXRhaWw6IHsgcXVlcnkgfSxcbiAgICAgIGJ1YmJsZXM6IHRydWVcbiAgICB9KSk7XG4gIH1cbn1cbiIsICIvKipcbiAqIExvY2sgU2NyZWVuIENvbnRyb2xsZXJcbiAqXG4gKiBIYW5kbGVzIFBJTi1iYXNlZCBsb2NrIHNjcmVlbiBmdW5jdGlvbmFsaXR5XG4gKi9cblxuaW1wb3J0IHsgRHJhd2VyQ29udHJvbGxlciB9IGZyb20gJy4vZHJhd2Vycyc7XG5cbmV4cG9ydCBjbGFzcyBMb2NrU2NyZWVuQ29udHJvbGxlciB7XG4gIHByaXZhdGUgc3RhdGljIHJlYWRvbmx5IENPUlJFQ1RfUElOID0gJzEyMzQnOyAvLyBEZW1vIFBJTlxuXG4gIHByaXZhdGUgbG9ja1NjcmVlbjogSFRNTEVsZW1lbnQgfCBudWxsID0gbnVsbDtcbiAgcHJpdmF0ZSBwaW5JbnB1dDogSFRNTEVsZW1lbnQgfCBudWxsID0gbnVsbDtcbiAgcHJpdmF0ZSBwaW5LZXlwYWQ6IEhUTUxFbGVtZW50IHwgbnVsbCA9IG51bGw7XG4gIHByaXZhdGUgbG9ja1RpbWVFbDogSFRNTEVsZW1lbnQgfCBudWxsID0gbnVsbDtcbiAgcHJpdmF0ZSBwaW5EaWdpdHM6IE5vZGVMaXN0T2Y8SFRNTEVsZW1lbnQ+IHwgbnVsbCA9IG51bGw7XG4gIHByaXZhdGUgY3VycmVudFBpbiA9ICcnO1xuICBwcml2YXRlIGRyYXdlcnM6IERyYXdlckNvbnRyb2xsZXIgfCBudWxsID0gbnVsbDtcblxuICBjb25zdHJ1Y3RvcihkcmF3ZXJzPzogRHJhd2VyQ29udHJvbGxlcikge1xuICAgIHRoaXMuZHJhd2VycyA9IGRyYXdlcnMgPz8gbnVsbDtcbiAgICB0aGlzLmxvY2tTY3JlZW4gPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnbG9ja1NjcmVlbicpO1xuICAgIHRoaXMucGluSW5wdXQgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgncGluSW5wdXQnKTtcbiAgICB0aGlzLnBpbktleXBhZCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdwaW5LZXlwYWQnKTtcbiAgICB0aGlzLmxvY2tUaW1lRWwgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnbG9ja1RpbWUnKTtcbiAgICB0aGlzLnBpbkRpZ2l0cyA9IHRoaXMucGluSW5wdXQ/LnF1ZXJ5U2VsZWN0b3JBbGw8SFRNTEVsZW1lbnQ+KCdzd3AtcGluLWRpZ2l0JykgPz8gbnVsbDtcblxuICAgIHRoaXMuc2V0dXBMaXN0ZW5lcnMoKTtcbiAgfVxuXG4gIC8qKlxuICAgKiBDaGVjayBpZiBsb2NrIHNjcmVlbiBpcyBhY3RpdmVcbiAgICovXG4gIGdldCBpc0FjdGl2ZSgpOiBib29sZWFuIHtcbiAgICByZXR1cm4gdGhpcy5sb2NrU2NyZWVuPy5jbGFzc0xpc3QuY29udGFpbnMoJ2FjdGl2ZScpID8/IGZhbHNlO1xuICB9XG5cbiAgLyoqXG4gICAqIFNob3cgdGhlIGxvY2sgc2NyZWVuXG4gICAqL1xuICBzaG93KCk6IHZvaWQge1xuICAgIHRoaXMuZHJhd2Vycz8uY2xvc2VBbGwoKTtcblxuICAgIGlmICh0aGlzLmxvY2tTY3JlZW4pIHtcbiAgICAgIHRoaXMubG9ja1NjcmVlbi5jbGFzc0xpc3QuYWRkKCdhY3RpdmUnKTtcbiAgICAgIGRvY3VtZW50LmJvZHkuc3R5bGUub3ZlcmZsb3cgPSAnaGlkZGVuJztcbiAgICB9XG5cbiAgICB0aGlzLmN1cnJlbnRQaW4gPSAnJztcbiAgICB0aGlzLnVwZGF0ZURpc3BsYXkoKTtcblxuICAgIC8vIFVwZGF0ZSBsb2NrIHRpbWVcbiAgICBpZiAodGhpcy5sb2NrVGltZUVsKSB7XG4gICAgICB0aGlzLmxvY2tUaW1lRWwudGV4dENvbnRlbnQgPSBgTFx1MDBFNXN0IGtsLiAke3RoaXMuZm9ybWF0VGltZSgpfWA7XG4gICAgfVxuICB9XG5cbiAgLyoqXG4gICAqIEhpZGUgdGhlIGxvY2sgc2NyZWVuXG4gICAqL1xuICBoaWRlKCk6IHZvaWQge1xuICAgIGlmICh0aGlzLmxvY2tTY3JlZW4pIHtcbiAgICAgIHRoaXMubG9ja1NjcmVlbi5jbGFzc0xpc3QucmVtb3ZlKCdhY3RpdmUnKTtcbiAgICAgIGRvY3VtZW50LmJvZHkuc3R5bGUub3ZlcmZsb3cgPSAnJztcbiAgICB9XG5cbiAgICB0aGlzLmN1cnJlbnRQaW4gPSAnJztcbiAgICB0aGlzLnVwZGF0ZURpc3BsYXkoKTtcbiAgfVxuXG4gIHByaXZhdGUgZm9ybWF0VGltZSgpOiBzdHJpbmcge1xuICAgIGNvbnN0IG5vdyA9IG5ldyBEYXRlKCk7XG4gICAgY29uc3QgaG91cnMgPSBub3cuZ2V0SG91cnMoKS50b1N0cmluZygpLnBhZFN0YXJ0KDIsICcwJyk7XG4gICAgY29uc3QgbWludXRlcyA9IG5vdy5nZXRNaW51dGVzKCkudG9TdHJpbmcoKS5wYWRTdGFydCgyLCAnMCcpO1xuICAgIHJldHVybiBgJHtob3Vyc306JHttaW51dGVzfWA7XG4gIH1cblxuICBwcml2YXRlIHVwZGF0ZURpc3BsYXkoKTogdm9pZCB7XG4gICAgaWYgKCF0aGlzLnBpbkRpZ2l0cykgcmV0dXJuO1xuXG4gICAgdGhpcy5waW5EaWdpdHMuZm9yRWFjaCgoZGlnaXQsIGluZGV4KSA9PiB7XG4gICAgICBkaWdpdC5jbGFzc0xpc3QucmVtb3ZlKCdmaWxsZWQnLCAnZXJyb3InKTtcbiAgICAgIGlmIChpbmRleCA8IHRoaXMuY3VycmVudFBpbi5sZW5ndGgpIHtcbiAgICAgICAgZGlnaXQudGV4dENvbnRlbnQgPSAnXHUyMDIyJztcbiAgICAgICAgZGlnaXQuY2xhc3NMaXN0LmFkZCgnZmlsbGVkJyk7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICBkaWdpdC50ZXh0Q29udGVudCA9ICcnO1xuICAgICAgfVxuICAgIH0pO1xuICB9XG5cbiAgcHJpdmF0ZSBzaG93RXJyb3IoKTogdm9pZCB7XG4gICAgaWYgKCF0aGlzLnBpbkRpZ2l0cykgcmV0dXJuO1xuXG4gICAgdGhpcy5waW5EaWdpdHMuZm9yRWFjaChkaWdpdCA9PiBkaWdpdC5jbGFzc0xpc3QuYWRkKCdlcnJvcicpKTtcblxuICAgIC8vIFNoYWtlIGFuaW1hdGlvblxuICAgIHRoaXMucGluSW5wdXQ/LmNsYXNzTGlzdC5hZGQoJ3NoYWtlJyk7XG5cbiAgICBzZXRUaW1lb3V0KCgpID0+IHtcbiAgICAgIHRoaXMuY3VycmVudFBpbiA9ICcnO1xuICAgICAgdGhpcy51cGRhdGVEaXNwbGF5KCk7XG4gICAgICB0aGlzLnBpbklucHV0Py5jbGFzc0xpc3QucmVtb3ZlKCdzaGFrZScpO1xuICAgIH0sIDUwMCk7XG4gIH1cblxuICBwcml2YXRlIHZlcmlmeSgpOiB2b2lkIHtcbiAgICBpZiAodGhpcy5jdXJyZW50UGluID09PSBMb2NrU2NyZWVuQ29udHJvbGxlci5DT1JSRUNUX1BJTikge1xuICAgICAgdGhpcy5oaWRlKCk7XG4gICAgfSBlbHNlIHtcbiAgICAgIHRoaXMuc2hvd0Vycm9yKCk7XG4gICAgfVxuICB9XG5cbiAgcHJpdmF0ZSBhZGREaWdpdChkaWdpdDogc3RyaW5nKTogdm9pZCB7XG4gICAgaWYgKHRoaXMuY3VycmVudFBpbi5sZW5ndGggPj0gNCkgcmV0dXJuO1xuXG4gICAgdGhpcy5jdXJyZW50UGluICs9IGRpZ2l0O1xuICAgIHRoaXMudXBkYXRlRGlzcGxheSgpO1xuXG4gICAgLy8gQXV0by12ZXJpZnkgd2hlbiA0IGRpZ2l0cyBlbnRlcmVkXG4gICAgaWYgKHRoaXMuY3VycmVudFBpbi5sZW5ndGggPT09IDQpIHtcbiAgICAgIHNldFRpbWVvdXQoKCkgPT4gdGhpcy52ZXJpZnkoKSwgMjAwKTtcbiAgICB9XG4gIH1cblxuICBwcml2YXRlIHJlbW92ZURpZ2l0KCk6IHZvaWQge1xuICAgIGlmICh0aGlzLmN1cnJlbnRQaW4ubGVuZ3RoID09PSAwKSByZXR1cm47XG4gICAgdGhpcy5jdXJyZW50UGluID0gdGhpcy5jdXJyZW50UGluLnNsaWNlKDAsIC0xKTtcbiAgICB0aGlzLnVwZGF0ZURpc3BsYXkoKTtcbiAgfVxuXG4gIHByaXZhdGUgY2xlYXJQaW4oKTogdm9pZCB7XG4gICAgdGhpcy5jdXJyZW50UGluID0gJyc7XG4gICAgdGhpcy51cGRhdGVEaXNwbGF5KCk7XG4gIH1cblxuICBwcml2YXRlIHNldHVwTGlzdGVuZXJzKCk6IHZvaWQge1xuICAgIC8vIEtleXBhZCBjbGljayBoYW5kbGVyXG4gICAgdGhpcy5waW5LZXlwYWQ/LmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgKGUpID0+IHRoaXMuaGFuZGxlS2V5cGFkQ2xpY2soZSkpO1xuXG4gICAgLy8gS2V5Ym9hcmQgaW5wdXRcbiAgICBkb2N1bWVudC5hZGRFdmVudExpc3RlbmVyKCdrZXlkb3duJywgKGUpID0+IHRoaXMuaGFuZGxlS2V5Ym9hcmQoZSkpO1xuXG4gICAgLy8gTG9jayBidXR0b24gaW4gc2lkZWJhclxuICAgIGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3I8SFRNTEVsZW1lbnQ+KCdzd3Atc2lkZS1tZW51LWFjdGlvbi5sb2NrJylcbiAgICAgID8uYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiB0aGlzLnNob3coKSk7XG4gIH1cblxuICBwcml2YXRlIGhhbmRsZUtleXBhZENsaWNrKGU6IEV2ZW50KTogdm9pZCB7XG4gICAgY29uc3QgdGFyZ2V0ID0gZS50YXJnZXQgYXMgSFRNTEVsZW1lbnQ7XG4gICAgY29uc3Qga2V5ID0gdGFyZ2V0LmNsb3Nlc3Q8SFRNTEVsZW1lbnQ+KCdzd3AtcGluLWtleScpO1xuXG4gICAgaWYgKCFrZXkpIHJldHVybjtcblxuICAgIGNvbnN0IGRpZ2l0ID0ga2V5LmRhdGFzZXQuZGlnaXQ7XG4gICAgY29uc3QgYWN0aW9uID0ga2V5LmRhdGFzZXQuYWN0aW9uO1xuXG4gICAgaWYgKGRpZ2l0KSB7XG4gICAgICB0aGlzLmFkZERpZ2l0KGRpZ2l0KTtcbiAgICB9IGVsc2UgaWYgKGFjdGlvbiA9PT0gJ2JhY2tzcGFjZScpIHtcbiAgICAgIHRoaXMucmVtb3ZlRGlnaXQoKTtcbiAgICB9IGVsc2UgaWYgKGFjdGlvbiA9PT0gJ2NsZWFyJykge1xuICAgICAgdGhpcy5jbGVhclBpbigpO1xuICAgIH1cbiAgfVxuXG4gIHByaXZhdGUgaGFuZGxlS2V5Ym9hcmQoZTogS2V5Ym9hcmRFdmVudCk6IHZvaWQge1xuICAgIGlmICghdGhpcy5pc0FjdGl2ZSkgcmV0dXJuO1xuXG4gICAgLy8gUHJldmVudCBkZWZhdWx0IHRvIGF2b2lkIG90aGVyIGludGVyYWN0aW9uc1xuICAgIGUucHJldmVudERlZmF1bHQoKTtcblxuICAgIGlmIChlLmtleSA+PSAnMCcgJiYgZS5rZXkgPD0gJzknKSB7XG4gICAgICB0aGlzLmFkZERpZ2l0KGUua2V5KTtcbiAgICB9IGVsc2UgaWYgKGUua2V5ID09PSAnQmFja3NwYWNlJykge1xuICAgICAgdGhpcy5yZW1vdmVEaWdpdCgpO1xuICAgIH0gZWxzZSBpZiAoZS5rZXkgPT09ICdFc2NhcGUnKSB7XG4gICAgICB0aGlzLmNsZWFyUGluKCk7XG4gICAgfVxuICB9XG59XG4iLCAiLyoqXG4gKiBDYXNoIENvbnRyb2xsZXJcbiAqXG4gKiBIYW5kbGVzIHRhYiBzd2l0Y2hpbmcsIGNhc2ggY2FsY3VsYXRpb25zLCBhbmQgZm9ybSBpbnRlcmFjdGlvbnNcbiAqIGZvciB0aGUgQ2FzaCBSZWdpc3RlciBwYWdlLlxuICovXG5cbmV4cG9ydCBjbGFzcyBDYXNoQ29udHJvbGxlciB7XG4gIC8vIEJhc2UgdmFsdWVzIChmcm9tIHN5c3RlbSAtIHdvdWxkIGNvbWUgZnJvbSBzZXJ2ZXIgaW4gcmVhbCBhcHApXG4gIHByaXZhdGUgcmVhZG9ubHkgc3RhcnRCYWxhbmNlID0gMjAwMDtcbiAgcHJpdmF0ZSByZWFkb25seSBjYXNoU2FsZXMgPSAzNTQwO1xuXG4gIGNvbnN0cnVjdG9yKCkge1xuICAgIHRoaXMuc2V0dXBUYWJzKCk7XG4gICAgdGhpcy5zZXR1cENhc2hDYWxjdWxhdGlvbigpO1xuICAgIHRoaXMuc2V0dXBDaGVja2JveFNlbGVjdGlvbigpO1xuICAgIHRoaXMuc2V0dXBBcHByb3ZhbENoZWNrYm94KCk7XG4gICAgdGhpcy5zZXR1cERhdGVGaWx0ZXJzKCk7XG4gICAgdGhpcy5zZXR1cFJvd1RvZ2dsZSgpO1xuICAgIHRoaXMuc2V0dXBEcmFmdFJvd0NsaWNrKCk7XG4gIH1cblxuICAvKipcbiAgICogU2V0dXAgdGFiIHN3aXRjaGluZyBmdW5jdGlvbmFsaXR5XG4gICAqL1xuICBwcml2YXRlIHNldHVwVGFicygpOiB2b2lkIHtcbiAgICBjb25zdCB0YWJzID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvckFsbDxIVE1MRWxlbWVudD4oJ3N3cC10YWJbZGF0YS10YWJdJyk7XG5cbiAgICB0YWJzLmZvckVhY2godGFiID0+IHtcbiAgICAgIHRhYi5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHtcbiAgICAgICAgY29uc3QgdGFyZ2V0VGFiID0gdGFiLmRhdGFzZXQudGFiO1xuICAgICAgICBpZiAodGFyZ2V0VGFiKSB7XG4gICAgICAgICAgdGhpcy5zd2l0Y2hUb1RhYih0YXJnZXRUYWIpO1xuICAgICAgICB9XG4gICAgICB9KTtcbiAgICB9KTtcbiAgfVxuXG4gIC8qKlxuICAgKiBTd2l0Y2ggdG8gYSBzcGVjaWZpYyB0YWIgYnkgbmFtZVxuICAgKi9cbiAgcHJpdmF0ZSBzd2l0Y2hUb1RhYih0YXJnZXRUYWI6IHN0cmluZyk6IHZvaWQge1xuICAgIGNvbnN0IHRhYnMgPSBkb2N1bWVudC5xdWVyeVNlbGVjdG9yQWxsPEhUTUxFbGVtZW50Pignc3dwLXRhYltkYXRhLXRhYl0nKTtcbiAgICBjb25zdCBjb250ZW50cyA9IGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3JBbGw8SFRNTEVsZW1lbnQ+KCdzd3AtdGFiLWNvbnRlbnRbZGF0YS10YWJdJyk7XG4gICAgY29uc3Qgc3RhdHNCYXJzID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvckFsbDxIVE1MRWxlbWVudD4oJ3N3cC1jYXNoLXN0YXRzW2RhdGEtZm9yLXRhYl0nKTtcblxuICAgIC8vIFVwZGF0ZSB0YWIgc3RhdGVzXG4gICAgdGFicy5mb3JFYWNoKHQgPT4ge1xuICAgICAgaWYgKHQuZGF0YXNldC50YWIgPT09IHRhcmdldFRhYikge1xuICAgICAgICB0LmNsYXNzTGlzdC5hZGQoJ2FjdGl2ZScpO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgdC5jbGFzc0xpc3QucmVtb3ZlKCdhY3RpdmUnKTtcbiAgICAgIH1cbiAgICB9KTtcblxuICAgIC8vIFVwZGF0ZSBjb250ZW50IHZpc2liaWxpdHlcbiAgICBjb250ZW50cy5mb3JFYWNoKGNvbnRlbnQgPT4ge1xuICAgICAgaWYgKGNvbnRlbnQuZGF0YXNldC50YWIgPT09IHRhcmdldFRhYikge1xuICAgICAgICBjb250ZW50LmNsYXNzTGlzdC5hZGQoJ2FjdGl2ZScpO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgY29udGVudC5jbGFzc0xpc3QucmVtb3ZlKCdhY3RpdmUnKTtcbiAgICAgIH1cbiAgICB9KTtcblxuICAgIC8vIFVwZGF0ZSBzdGF0cyBiYXIgdmlzaWJpbGl0eVxuICAgIHN0YXRzQmFycy5mb3JFYWNoKHN0YXRzID0+IHtcbiAgICAgIGlmIChzdGF0cy5kYXRhc2V0LmZvclRhYiA9PT0gdGFyZ2V0VGFiKSB7XG4gICAgICAgIHN0YXRzLmNsYXNzTGlzdC5hZGQoJ2FjdGl2ZScpO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgc3RhdHMuY2xhc3NMaXN0LnJlbW92ZSgnYWN0aXZlJyk7XG4gICAgICB9XG4gICAgfSk7XG4gIH1cblxuICAvKipcbiAgICogU2V0dXAgY2FzaCBjYWxjdWxhdGlvbiB3aXRoIHJlYWwtdGltZSB1cGRhdGVzXG4gICAqL1xuICBwcml2YXRlIHNldHVwQ2FzaENhbGN1bGF0aW9uKCk6IHZvaWQge1xuICAgIGNvbnN0IHBheW91dHNJbnB1dCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdwYXlvdXRzJykgYXMgSFRNTElucHV0RWxlbWVudDtcbiAgICBjb25zdCB0b0JhbmtJbnB1dCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCd0b0JhbmsnKSBhcyBIVE1MSW5wdXRFbGVtZW50O1xuICAgIGNvbnN0IGFjdHVhbENhc2hJbnB1dCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdhY3R1YWxDYXNoJykgYXMgSFRNTElucHV0RWxlbWVudDtcblxuICAgIGlmICghcGF5b3V0c0lucHV0IHx8ICF0b0JhbmtJbnB1dCB8fCAhYWN0dWFsQ2FzaElucHV0KSByZXR1cm47XG5cbiAgICBjb25zdCBjYWxjdWxhdGUgPSAoKSA9PiB0aGlzLmNhbGN1bGF0ZUNhc2gocGF5b3V0c0lucHV0LCB0b0JhbmtJbnB1dCwgYWN0dWFsQ2FzaElucHV0KTtcblxuICAgIHBheW91dHNJbnB1dC5hZGRFdmVudExpc3RlbmVyKCdpbnB1dCcsIGNhbGN1bGF0ZSk7XG4gICAgdG9CYW5rSW5wdXQuYWRkRXZlbnRMaXN0ZW5lcignaW5wdXQnLCBjYWxjdWxhdGUpO1xuICAgIGFjdHVhbENhc2hJbnB1dC5hZGRFdmVudExpc3RlbmVyKCdpbnB1dCcsIGNhbGN1bGF0ZSk7XG5cbiAgICAvLyBJbml0aWFsIGNhbGN1bGF0aW9uXG4gICAgY2FsY3VsYXRlKCk7XG4gIH1cblxuICAvKipcbiAgICogQ2FsY3VsYXRlIGV4cGVjdGVkIGNhc2ggYW5kIGRpZmZlcmVuY2VcbiAgICovXG4gIHByaXZhdGUgY2FsY3VsYXRlQ2FzaChcbiAgICBwYXlvdXRzSW5wdXQ6IEhUTUxJbnB1dEVsZW1lbnQsXG4gICAgdG9CYW5rSW5wdXQ6IEhUTUxJbnB1dEVsZW1lbnQsXG4gICAgYWN0dWFsQ2FzaElucHV0OiBIVE1MSW5wdXRFbGVtZW50XG4gICk6IHZvaWQge1xuICAgIGNvbnN0IHBheW91dHMgPSB0aGlzLnBhcnNlTnVtYmVyKHBheW91dHNJbnB1dC52YWx1ZSk7XG4gICAgY29uc3QgdG9CYW5rID0gdGhpcy5wYXJzZU51bWJlcih0b0JhbmtJbnB1dC52YWx1ZSk7XG4gICAgY29uc3QgYWN0dWFsID0gdGhpcy5wYXJzZU51bWJlcihhY3R1YWxDYXNoSW5wdXQudmFsdWUpO1xuXG4gICAgLy8gRXhwZWN0ZWQgPSBzdGFydCArIHNhbGVzIC0gcGF5b3V0cyAtIHRvIGJhbmtcbiAgICBjb25zdCBleHBlY3RlZENhc2ggPSB0aGlzLnN0YXJ0QmFsYW5jZSArIHRoaXMuY2FzaFNhbGVzIC0gcGF5b3V0cyAtIHRvQmFuaztcblxuICAgIGNvbnN0IGV4cGVjdGVkRWxlbWVudCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdleHBlY3RlZENhc2gnKTtcbiAgICBpZiAoZXhwZWN0ZWRFbGVtZW50KSB7XG4gICAgICBleHBlY3RlZEVsZW1lbnQudGV4dENvbnRlbnQgPSB0aGlzLmZvcm1hdE51bWJlcihleHBlY3RlZENhc2gpO1xuICAgIH1cblxuICAgIC8vIENhbGN1bGF0ZSBhbmQgZGlzcGxheSBkaWZmZXJlbmNlXG4gICAgdGhpcy51cGRhdGVEaWZmZXJlbmNlKGFjdHVhbCwgZXhwZWN0ZWRDYXNoLCBhY3R1YWxDYXNoSW5wdXQudmFsdWUpO1xuICB9XG5cbiAgLyoqXG4gICAqIFVwZGF0ZSBkaWZmZXJlbmNlIGJveCB3aXRoIGNvbG9yIGNvZGluZ1xuICAgKi9cbiAgcHJpdmF0ZSB1cGRhdGVEaWZmZXJlbmNlKGFjdHVhbDogbnVtYmVyLCBleHBlY3RlZDogbnVtYmVyLCByYXdWYWx1ZTogc3RyaW5nKTogdm9pZCB7XG4gICAgY29uc3QgYm94ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ2RpZmZlcmVuY2VCb3gnKTtcbiAgICBjb25zdCB2YWx1ZSA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdkaWZmZXJlbmNlVmFsdWUnKTtcbiAgICBpZiAoIWJveCB8fCAhdmFsdWUpIHJldHVybjtcblxuICAgIGNvbnN0IGRpZmYgPSBhY3R1YWwgLSBleHBlY3RlZDtcblxuICAgIC8vIFJlbW92ZSBhbGwgc3RhdGUgY2xhc3Nlc1xuICAgIGJveC5jbGFzc0xpc3QucmVtb3ZlKCdwb3NpdGl2ZScsICduZWdhdGl2ZScsICduZXV0cmFsJyk7XG5cbiAgICBpZiAoYWN0dWFsID09PSAwICYmIHJhd1ZhbHVlID09PSAnJykge1xuICAgICAgLy8gTm8gaW5wdXQgeWV0XG4gICAgICB2YWx1ZS50ZXh0Q29udGVudCA9ICdcdTIwMTMga3InO1xuICAgICAgYm94LmNsYXNzTGlzdC5hZGQoJ25ldXRyYWwnKTtcbiAgICB9IGVsc2UgaWYgKGRpZmYgPiAwKSB7XG4gICAgICAvLyBNb3JlIGNhc2ggdGhhbiBleHBlY3RlZFxuICAgICAgdmFsdWUudGV4dENvbnRlbnQgPSAnKycgKyB0aGlzLmZvcm1hdE51bWJlcihkaWZmKSArICcga3InO1xuICAgICAgYm94LmNsYXNzTGlzdC5hZGQoJ3Bvc2l0aXZlJyk7XG4gICAgfSBlbHNlIGlmIChkaWZmIDwgMCkge1xuICAgICAgLy8gTGVzcyBjYXNoIHRoYW4gZXhwZWN0ZWRcbiAgICAgIHZhbHVlLnRleHRDb250ZW50ID0gdGhpcy5mb3JtYXROdW1iZXIoZGlmZikgKyAnIGtyJztcbiAgICAgIGJveC5jbGFzc0xpc3QuYWRkKCduZWdhdGl2ZScpO1xuICAgIH0gZWxzZSB7XG4gICAgICAvLyBFeGFjdCBtYXRjaFxuICAgICAgdmFsdWUudGV4dENvbnRlbnQgPSAnMCwwMCBrcic7XG4gICAgICBib3guY2xhc3NMaXN0LmFkZCgnbmV1dHJhbCcpO1xuICAgIH1cbiAgfVxuXG4gIC8qKlxuICAgKiBTZXR1cCBjaGVja2JveCBzZWxlY3Rpb24gZm9yIHRhYmxlIHJvd3NcbiAgICovXG4gIHByaXZhdGUgc2V0dXBDaGVja2JveFNlbGVjdGlvbigpOiB2b2lkIHtcbiAgICBjb25zdCBzZWxlY3RBbGwgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnc2VsZWN0QWxsJykgYXMgSFRNTElucHV0RWxlbWVudDtcbiAgICBjb25zdCByb3dDaGVja2JveGVzID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvckFsbDxIVE1MSW5wdXRFbGVtZW50PignLnJvdy1zZWxlY3QnKTtcbiAgICBjb25zdCBleHBvcnRCdG4gPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnZXhwb3J0QnRuJykgYXMgSFRNTEJ1dHRvbkVsZW1lbnQ7XG4gICAgY29uc3Qgc2VsZWN0aW9uQ291bnQgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnc2VsZWN0aW9uQ291bnQnKTtcblxuICAgIGlmICghc2VsZWN0QWxsIHx8ICFleHBvcnRCdG4gfHwgIXNlbGVjdGlvbkNvdW50KSByZXR1cm47XG5cbiAgICBjb25zdCB1cGRhdGVTZWxlY3Rpb24gPSAoKSA9PiB7XG4gICAgICBjb25zdCBjaGVja2VkID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvckFsbDxIVE1MSW5wdXRFbGVtZW50PignLnJvdy1zZWxlY3Q6Y2hlY2tlZCcpO1xuICAgICAgY29uc3QgY291bnQgPSBjaGVja2VkLmxlbmd0aDtcblxuICAgICAgc2VsZWN0aW9uQ291bnQudGV4dENvbnRlbnQgPSBjb3VudCA9PT0gMCA/ICcwIHZhbGd0JyA6IGAke2NvdW50fSB2YWxndGA7XG4gICAgICBleHBvcnRCdG4uZGlzYWJsZWQgPSBjb3VudCA9PT0gMDtcblxuICAgICAgLy8gVXBkYXRlIHNlbGVjdCBhbGwgc3RhdGVcbiAgICAgIHNlbGVjdEFsbC5jaGVja2VkID0gY291bnQgPT09IHJvd0NoZWNrYm94ZXMubGVuZ3RoICYmIGNvdW50ID4gMDtcbiAgICAgIHNlbGVjdEFsbC5pbmRldGVybWluYXRlID0gY291bnQgPiAwICYmIGNvdW50IDwgcm93Q2hlY2tib3hlcy5sZW5ndGg7XG4gICAgfTtcblxuICAgIHNlbGVjdEFsbC5hZGRFdmVudExpc3RlbmVyKCdjaGFuZ2UnLCAoKSA9PiB7XG4gICAgICByb3dDaGVja2JveGVzLmZvckVhY2goY2IgPT4gY2IuY2hlY2tlZCA9IHNlbGVjdEFsbC5jaGVja2VkKTtcbiAgICAgIHVwZGF0ZVNlbGVjdGlvbigpO1xuICAgIH0pO1xuXG4gICAgcm93Q2hlY2tib3hlcy5mb3JFYWNoKGNiID0+IHtcbiAgICAgIGNiLmFkZEV2ZW50TGlzdGVuZXIoJ2NoYW5nZScsIHVwZGF0ZVNlbGVjdGlvbik7XG4gICAgICAvLyBTdG9wIGNsaWNrIGZyb20gYnViYmxpbmcgdG8gcm93XG4gICAgICBjYi5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsIGUgPT4gZS5zdG9wUHJvcGFnYXRpb24oKSk7XG4gICAgfSk7XG4gIH1cblxuICAvKipcbiAgICogU2V0dXAgYXBwcm92YWwgY2hlY2tib3ggdG8gZW5hYmxlL2Rpc2FibGUgYXBwcm92ZSBidXR0b25cbiAgICovXG4gIHByaXZhdGUgc2V0dXBBcHByb3ZhbENoZWNrYm94KCk6IHZvaWQge1xuICAgIGNvbnN0IGNoZWNrYm94ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ2NvbmZpcm1DaGVja2JveCcpIGFzIEhUTUxJbnB1dEVsZW1lbnQ7XG4gICAgY29uc3QgYXBwcm92ZUJ0biA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdhcHByb3ZlQnRuJykgYXMgSFRNTEJ1dHRvbkVsZW1lbnQ7XG5cbiAgICBpZiAoIWNoZWNrYm94IHx8ICFhcHByb3ZlQnRuKSByZXR1cm47XG5cbiAgICBjaGVja2JveC5hZGRFdmVudExpc3RlbmVyKCdjaGFuZ2UnLCAoKSA9PiB7XG4gICAgICBhcHByb3ZlQnRuLmRpc2FibGVkID0gIWNoZWNrYm94LmNoZWNrZWQ7XG4gICAgfSk7XG4gIH1cblxuICAvKipcbiAgICogU2V0dXAgZGF0ZSBmaWx0ZXIgZGVmYXVsdHMgKGxhc3QgMzAgZGF5cylcbiAgICovXG4gIHByaXZhdGUgc2V0dXBEYXRlRmlsdGVycygpOiB2b2lkIHtcbiAgICBjb25zdCBkYXRlRnJvbSA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdkYXRlRnJvbScpIGFzIEhUTUxJbnB1dEVsZW1lbnQ7XG4gICAgY29uc3QgZGF0ZVRvID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ2RhdGVUbycpIGFzIEhUTUxJbnB1dEVsZW1lbnQ7XG5cbiAgICBpZiAoIWRhdGVGcm9tIHx8ICFkYXRlVG8pIHJldHVybjtcblxuICAgIGNvbnN0IHRvZGF5ID0gbmV3IERhdGUoKTtcbiAgICBjb25zdCB0aGlydHlEYXlzQWdvID0gbmV3IERhdGUodG9kYXkpO1xuICAgIHRoaXJ0eURheXNBZ28uc2V0RGF0ZSh0b2RheS5nZXREYXRlKCkgLSAzMCk7XG5cbiAgICBkYXRlVG8udmFsdWUgPSB0aGlzLmZvcm1hdERhdGVJU08odG9kYXkpO1xuICAgIGRhdGVGcm9tLnZhbHVlID0gdGhpcy5mb3JtYXREYXRlSVNPKHRoaXJ0eURheXNBZ28pO1xuICB9XG5cbiAgLyoqXG4gICAqIEZvcm1hdCBudW1iZXIgYXMgRGFuaXNoIGN1cnJlbmN5XG4gICAqL1xuICBwcml2YXRlIGZvcm1hdE51bWJlcihudW06IG51bWJlcik6IHN0cmluZyB7XG4gICAgcmV0dXJuIG51bS50b0xvY2FsZVN0cmluZygnZGEtREsnLCB7XG4gICAgICBtaW5pbXVtRnJhY3Rpb25EaWdpdHM6IDIsXG4gICAgICBtYXhpbXVtRnJhY3Rpb25EaWdpdHM6IDJcbiAgICB9KTtcbiAgfVxuXG4gIC8qKlxuICAgKiBQYXJzZSBEYW5pc2ggbnVtYmVyIGZvcm1hdFxuICAgKi9cbiAgcHJpdmF0ZSBwYXJzZU51bWJlcihzdHI6IHN0cmluZyk6IG51bWJlciB7XG4gICAgaWYgKCFzdHIpIHJldHVybiAwO1xuICAgIHJldHVybiBwYXJzZUZsb2F0KHN0ci5yZXBsYWNlKC9cXC4vZywgJycpLnJlcGxhY2UoJywnLCAnLicpKSB8fCAwO1xuICB9XG5cbiAgLyoqXG4gICAqIEZvcm1hdCBkYXRlIGFzIElTTyBzdHJpbmcgKFlZWVktTU0tREQpXG4gICAqL1xuICBwcml2YXRlIGZvcm1hdERhdGVJU08oZGF0ZTogRGF0ZSk6IHN0cmluZyB7XG4gICAgcmV0dXJuIGRhdGUudG9JU09TdHJpbmcoKS5zcGxpdCgnVCcpWzBdO1xuICB9XG5cbiAgLyoqXG4gICAqIFNldHVwIHJvdyB0b2dnbGUgZm9yIGV4cGFuZGFibGUgZGV0YWlsc1xuICAgKi9cbiAgcHJpdmF0ZSBzZXR1cFJvd1RvZ2dsZSgpOiB2b2lkIHtcbiAgICBjb25zdCByb3dzID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvckFsbDxIVE1MRWxlbWVudD4oJ3N3cC1jYXNoLXRhYmxlLXJvd1tkYXRhLWlkXTpub3QoLmRyYWZ0LXJvdyknKTtcblxuICAgIHJvd3MuZm9yRWFjaChyb3cgPT4ge1xuICAgICAgY29uc3Qgcm93SWQgPSByb3cuZ2V0QXR0cmlidXRlKCdkYXRhLWlkJyk7XG4gICAgICBpZiAoIXJvd0lkKSByZXR1cm47XG5cbiAgICAgIGNvbnN0IGRldGFpbCA9IGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3I8SFRNTEVsZW1lbnQ+KGBzd3AtY2FzaC1yb3ctZGV0YWlsW2RhdGEtZm9yPVwiJHtyb3dJZH1cIl1gKTtcbiAgICAgIGlmICghZGV0YWlsKSByZXR1cm47XG5cbiAgICAgIHJvdy5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsIChlKSA9PiB7XG4gICAgICAgIC8vIERvbid0IHRvZ2dsZSBpZiBjbGlja2luZyBvbiBjaGVja2JveFxuICAgICAgICBpZiAoKGUudGFyZ2V0IGFzIEhUTUxFbGVtZW50KS5jbG9zZXN0KCdpbnB1dFt0eXBlPVwiY2hlY2tib3hcIl0nKSkgcmV0dXJuO1xuXG4gICAgICAgIGNvbnN0IGljb24gPSByb3cucXVlcnlTZWxlY3Rvcignc3dwLXJvdy10b2dnbGUgaScpO1xuICAgICAgICBjb25zdCBpc0V4cGFuZGVkID0gcm93LmNsYXNzTGlzdC5jb250YWlucygnZXhwYW5kZWQnKTtcblxuICAgICAgICAvLyBDbG9zZSBvdGhlciBleHBhbmRlZCByb3dzXG4gICAgICAgIGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3JBbGwoJ3N3cC1jYXNoLXRhYmxlLXJvdy5leHBhbmRlZCcpLmZvckVhY2gociA9PiB7XG4gICAgICAgICAgaWYgKHIgIT09IHJvdykge1xuICAgICAgICAgICAgY29uc3Qgb3RoZXJJZCA9IHIuZ2V0QXR0cmlidXRlKCdkYXRhLWlkJyk7XG4gICAgICAgICAgICBpZiAob3RoZXJJZCkge1xuICAgICAgICAgICAgICBjb25zdCBvdGhlckRldGFpbCA9IGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3I8SFRNTEVsZW1lbnQ+KGBzd3AtY2FzaC1yb3ctZGV0YWlsW2RhdGEtZm9yPVwiJHtvdGhlcklkfVwiXWApO1xuICAgICAgICAgICAgICBjb25zdCBvdGhlckljb24gPSByLnF1ZXJ5U2VsZWN0b3IoJ3N3cC1yb3ctdG9nZ2xlIGknKTtcbiAgICAgICAgICAgICAgaWYgKG90aGVyRGV0YWlsICYmIG90aGVySWNvbikge1xuICAgICAgICAgICAgICAgIHRoaXMuY29sbGFwc2VSb3cociwgb3RoZXJEZXRhaWwsIG90aGVySWNvbiBhcyBIVE1MRWxlbWVudCk7XG4gICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH0pO1xuXG4gICAgICAgIC8vIFRvZ2dsZSBjdXJyZW50IHJvd1xuICAgICAgICBpZiAoaXNFeHBhbmRlZCkge1xuICAgICAgICAgIHRoaXMuY29sbGFwc2VSb3cocm93LCBkZXRhaWwsIGljb24pO1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgIHRoaXMuZXhwYW5kUm93KHJvdywgZGV0YWlsLCBpY29uKTtcbiAgICAgICAgfVxuICAgICAgfSk7XG4gICAgfSk7XG4gIH1cblxuICAvKipcbiAgICogRXhwYW5kIGEgcm93IHdpdGggYW5pbWF0aW9uXG4gICAqL1xuICBwcml2YXRlIGV4cGFuZFJvdyhyb3c6IEVsZW1lbnQsIGRldGFpbDogSFRNTEVsZW1lbnQsIGljb246IEVsZW1lbnQgfCBudWxsKTogdm9pZCB7XG4gICAgcm93LmNsYXNzTGlzdC5hZGQoJ2V4cGFuZGVkJyk7XG4gICAgZGV0YWlsLmNsYXNzTGlzdC5hZGQoJ2V4cGFuZGVkJyk7XG5cbiAgICAvLyBBbmltYXRlIGljb24gcm90YXRpb25cbiAgICBpY29uPy5hbmltYXRlKFtcbiAgICAgIHsgdHJhbnNmb3JtOiAncm90YXRlKDBkZWcpJyB9LFxuICAgICAgeyB0cmFuc2Zvcm06ICdyb3RhdGUoOTBkZWcpJyB9XG4gICAgXSwge1xuICAgICAgZHVyYXRpb246IDIwMCxcbiAgICAgIGVhc2luZzogJ2Vhc2Utb3V0JyxcbiAgICAgIGZpbGw6ICdmb3J3YXJkcydcbiAgICB9KTtcblxuICAgIC8vIEFuaW1hdGUgZGV0YWlsIGV4cGFuc2lvblxuICAgIGNvbnN0IGNvbnRlbnQgPSBkZXRhaWwucXVlcnlTZWxlY3Rvcignc3dwLXJvdy1kZXRhaWwtY29udGVudCcpIGFzIEhUTUxFbGVtZW50O1xuICAgIGlmIChjb250ZW50KSB7XG4gICAgICBjb25zdCBoZWlnaHQgPSBjb250ZW50Lm9mZnNldEhlaWdodDtcbiAgICAgIGRldGFpbC5hbmltYXRlKFtcbiAgICAgICAgeyBoZWlnaHQ6ICcwcHgnLCBvcGFjaXR5OiAwIH0sXG4gICAgICAgIHsgaGVpZ2h0OiBgJHtoZWlnaHR9cHhgLCBvcGFjaXR5OiAxIH1cbiAgICAgIF0sIHtcbiAgICAgICAgZHVyYXRpb246IDI1MCxcbiAgICAgICAgZWFzaW5nOiAnZWFzZS1vdXQnLFxuICAgICAgICBmaWxsOiAnZm9yd2FyZHMnXG4gICAgICB9KTtcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogQ29sbGFwc2UgYSByb3cgd2l0aCBhbmltYXRpb25cbiAgICovXG4gIHByaXZhdGUgY29sbGFwc2VSb3cocm93OiBFbGVtZW50LCBkZXRhaWw6IEhUTUxFbGVtZW50LCBpY29uOiBFbGVtZW50IHwgbnVsbCk6IHZvaWQge1xuICAgIC8vIEFuaW1hdGUgaWNvbiByb3RhdGlvblxuICAgIGljb24/LmFuaW1hdGUoW1xuICAgICAgeyB0cmFuc2Zvcm06ICdyb3RhdGUoOTBkZWcpJyB9LFxuICAgICAgeyB0cmFuc2Zvcm06ICdyb3RhdGUoMGRlZyknIH1cbiAgICBdLCB7XG4gICAgICBkdXJhdGlvbjogMjAwLFxuICAgICAgZWFzaW5nOiAnZWFzZS1vdXQnLFxuICAgICAgZmlsbDogJ2ZvcndhcmRzJ1xuICAgIH0pO1xuXG4gICAgLy8gQW5pbWF0ZSBkZXRhaWwgY29sbGFwc2VcbiAgICBjb25zdCBjb250ZW50ID0gZGV0YWlsLnF1ZXJ5U2VsZWN0b3IoJ3N3cC1yb3ctZGV0YWlsLWNvbnRlbnQnKSBhcyBIVE1MRWxlbWVudDtcbiAgICBpZiAoY29udGVudCkge1xuICAgICAgY29uc3QgaGVpZ2h0ID0gY29udGVudC5vZmZzZXRIZWlnaHQ7XG4gICAgICBjb25zdCBhbmltYXRpb24gPSBkZXRhaWwuYW5pbWF0ZShbXG4gICAgICAgIHsgaGVpZ2h0OiBgJHtoZWlnaHR9cHhgLCBvcGFjaXR5OiAxIH0sXG4gICAgICAgIHsgaGVpZ2h0OiAnMHB4Jywgb3BhY2l0eTogMCB9XG4gICAgICBdLCB7XG4gICAgICAgIGR1cmF0aW9uOiAyMDAsXG4gICAgICAgIGVhc2luZzogJ2Vhc2Utb3V0JyxcbiAgICAgICAgZmlsbDogJ2ZvcndhcmRzJ1xuICAgICAgfSk7XG5cbiAgICAgIGFuaW1hdGlvbi5vbmZpbmlzaCA9ICgpID0+IHtcbiAgICAgICAgcm93LmNsYXNzTGlzdC5yZW1vdmUoJ2V4cGFuZGVkJyk7XG4gICAgICAgIGRldGFpbC5jbGFzc0xpc3QucmVtb3ZlKCdleHBhbmRlZCcpO1xuICAgICAgfTtcbiAgICB9IGVsc2Uge1xuICAgICAgcm93LmNsYXNzTGlzdC5yZW1vdmUoJ2V4cGFuZGVkJyk7XG4gICAgICBkZXRhaWwuY2xhc3NMaXN0LnJlbW92ZSgnZXhwYW5kZWQnKTtcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogU2V0dXAgZHJhZnQgcm93IGNsaWNrIHRvIG5hdmlnYXRlIHRvIHJlY29uY2lsaWF0aW9uIHRhYlxuICAgKi9cbiAgcHJpdmF0ZSBzZXR1cERyYWZ0Um93Q2xpY2soKTogdm9pZCB7XG4gICAgY29uc3QgZHJhZnRSb3cgPSBkb2N1bWVudC5xdWVyeVNlbGVjdG9yPEhUTUxFbGVtZW50Pignc3dwLWNhc2gtdGFibGUtcm93LmRyYWZ0LXJvdycpO1xuICAgIGlmICghZHJhZnRSb3cpIHJldHVybjtcblxuICAgIGRyYWZ0Um93LnN0eWxlLmN1cnNvciA9ICdwb2ludGVyJztcbiAgICBkcmFmdFJvdy5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsIChlKSA9PiB7XG4gICAgICAvLyBEb24ndCBuYXZpZ2F0ZSBpZiBjbGlja2luZyBvbiBjaGVja2JveFxuICAgICAgaWYgKChlLnRhcmdldCBhcyBIVE1MRWxlbWVudCkuY2xvc2VzdCgnaW5wdXRbdHlwZT1cImNoZWNrYm94XCJdJykpIHJldHVybjtcblxuICAgICAgdGhpcy5zd2l0Y2hUb1RhYignYWZzdGVtbmluZycpO1xuICAgIH0pO1xuICB9XG59XG4iLCAiLyoqXG4gKiBTYWxvbiBPUyBBcHBcbiAqXG4gKiBNYWluIGFwcGxpY2F0aW9uIGNsYXNzIHRoYXQgb3JjaGVzdHJhdGVzIGFsbCBVSSBjb250cm9sbGVyc1xuICovXG5cbmltcG9ydCB7IFNpZGViYXJDb250cm9sbGVyIH0gZnJvbSAnLi9tb2R1bGVzL3NpZGViYXInO1xuaW1wb3J0IHsgRHJhd2VyQ29udHJvbGxlciB9IGZyb20gJy4vbW9kdWxlcy9kcmF3ZXJzJztcbmltcG9ydCB7IFRoZW1lQ29udHJvbGxlciB9IGZyb20gJy4vbW9kdWxlcy90aGVtZSc7XG5pbXBvcnQgeyBTZWFyY2hDb250cm9sbGVyIH0gZnJvbSAnLi9tb2R1bGVzL3NlYXJjaCc7XG5pbXBvcnQgeyBMb2NrU2NyZWVuQ29udHJvbGxlciB9IGZyb20gJy4vbW9kdWxlcy9sb2Nrc2NyZWVuJztcbmltcG9ydCB7IENhc2hDb250cm9sbGVyIH0gZnJvbSAnLi9tb2R1bGVzL2Nhc2gnO1xuXG4vKipcbiAqIE1haW4gYXBwbGljYXRpb24gY2xhc3NcbiAqL1xuZXhwb3J0IGNsYXNzIEFwcCB7XG4gIHJlYWRvbmx5IHNpZGViYXI6IFNpZGViYXJDb250cm9sbGVyO1xuICByZWFkb25seSBkcmF3ZXJzOiBEcmF3ZXJDb250cm9sbGVyO1xuICByZWFkb25seSB0aGVtZTogVGhlbWVDb250cm9sbGVyO1xuICByZWFkb25seSBzZWFyY2g6IFNlYXJjaENvbnRyb2xsZXI7XG4gIHJlYWRvbmx5IGxvY2tTY3JlZW46IExvY2tTY3JlZW5Db250cm9sbGVyO1xuICByZWFkb25seSBjYXNoOiBDYXNoQ29udHJvbGxlcjtcblxuICBjb25zdHJ1Y3RvcigpIHtcbiAgICAvLyBJbml0aWFsaXplIGNvbnRyb2xsZXJzXG4gICAgdGhpcy5zaWRlYmFyID0gbmV3IFNpZGViYXJDb250cm9sbGVyKCk7XG4gICAgdGhpcy5kcmF3ZXJzID0gbmV3IERyYXdlckNvbnRyb2xsZXIoKTtcbiAgICB0aGlzLnRoZW1lID0gbmV3IFRoZW1lQ29udHJvbGxlcigpO1xuICAgIHRoaXMuc2VhcmNoID0gbmV3IFNlYXJjaENvbnRyb2xsZXIoKTtcbiAgICB0aGlzLmxvY2tTY3JlZW4gPSBuZXcgTG9ja1NjcmVlbkNvbnRyb2xsZXIodGhpcy5kcmF3ZXJzKTtcbiAgICB0aGlzLmNhc2ggPSBuZXcgQ2FzaENvbnRyb2xsZXIoKTtcbiAgfVxufVxuXG4vKipcbiAqIEdsb2JhbCBhcHAgaW5zdGFuY2VcbiAqL1xubGV0IGFwcDogQXBwO1xuXG4vKipcbiAqIEluaXRpYWxpemUgdGhlIGFwcGxpY2F0aW9uXG4gKi9cbmZ1bmN0aW9uIGluaXQoKTogdm9pZCB7XG4gIGFwcCA9IG5ldyBBcHAoKTtcblxuICAvLyBFeHBvc2UgdG8gd2luZG93IGZvciBkZWJ1Z2dpbmdcbiAgaWYgKHR5cGVvZiB3aW5kb3cgIT09ICd1bmRlZmluZWQnKSB7XG4gICAgKHdpbmRvdyBhcyB1bmtub3duIGFzIHsgYXBwOiBBcHAgfSkuYXBwID0gYXBwO1xuICB9XG59XG5cbi8vIFdhaXQgZm9yIERPTSByZWFkeVxuaWYgKGRvY3VtZW50LnJlYWR5U3RhdGUgPT09ICdsb2FkaW5nJykge1xuICBkb2N1bWVudC5hZGRFdmVudExpc3RlbmVyKCdET01Db250ZW50TG9hZGVkJywgaW5pdCk7XG59IGVsc2Uge1xuICBpbml0KCk7XG59XG5cbmV4cG9ydCB7IGFwcCB9O1xuZXhwb3J0IGRlZmF1bHQgQXBwO1xuIl0sCiAgIm1hcHBpbmdzIjogIjs7OztBQU1PLElBQU0scUJBQU4sTUFBTSxtQkFBa0I7QUFBQSxFQUs3QixjQUFjO0FBSmQsU0FBUSxhQUFpQztBQUN6QyxTQUFRLFlBQWdDO0FBQ3hDLFNBQVEsY0FBa0M7QUFHeEMsU0FBSyxhQUFhLFNBQVMsZUFB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 + var app_default = App; +})(); +//# sourceMappingURL=app.js.map diff --git a/PlanTempus.Application/wwwroot/js/app.js.map b/PlanTempus.Application/wwwroot/js/app.js.map index eeba422..344ec30 100644 --- a/PlanTempus.Application/wwwroot/js/app.js.map +++ b/PlanTempus.Application/wwwroot/js/app.js.map @@ -1,7 +1,7 @@ { "version": 3, - "sources": ["../ts/modules/sidebar.ts", "../ts/modules/drawers.ts", "../ts/modules/theme.ts", "../ts/modules/search.ts", "../ts/modules/lockscreen.ts", "../ts/app.ts"], - "sourcesContent": ["/**\n * Sidebar Controller\n *\n * Handles sidebar collapse/expand and tooltip functionality\n */\n\nexport class SidebarController {\n private menuToggle: HTMLElement | null = null;\n private appLayout: HTMLElement | null = null;\n private menuTooltip: HTMLElement | null = null;\n\n constructor() {\n this.menuToggle = document.getElementById('menuToggle');\n this.appLayout = document.querySelector('swp-app-layout');\n this.menuTooltip = document.getElementById('menuTooltip');\n\n this.setupListeners();\n this.setupTooltips();\n this.restoreState();\n }\n\n /**\n * Check if sidebar is collapsed\n */\n get isCollapsed(): boolean {\n return this.appLayout?.classList.contains('menu-collapsed') ?? false;\n }\n\n /**\n * Toggle sidebar collapsed state\n */\n toggle(): void {\n if (!this.appLayout) return;\n\n this.appLayout.classList.toggle('menu-collapsed');\n localStorage.setItem('sidebar-collapsed', String(this.isCollapsed));\n }\n\n /**\n * Collapse the sidebar\n */\n collapse(): void {\n this.appLayout?.classList.add('menu-collapsed');\n localStorage.setItem('sidebar-collapsed', 'true');\n }\n\n /**\n * Expand the sidebar\n */\n expand(): void {\n this.appLayout?.classList.remove('menu-collapsed');\n localStorage.setItem('sidebar-collapsed', 'false');\n }\n\n private setupListeners(): void {\n this.menuToggle?.addEventListener('click', () => this.toggle());\n }\n\n private setupTooltips(): void {\n if (!this.menuTooltip) return;\n\n const menuItems = document.querySelectorAll('swp-side-menu-item[data-tooltip]');\n\n menuItems.forEach(item => {\n item.addEventListener('mouseenter', () => this.showTooltip(item));\n item.addEventListener('mouseleave', () => this.hideTooltip());\n });\n }\n\n private showTooltip(item: HTMLElement): void {\n if (!this.isCollapsed || !this.menuTooltip) return;\n\n const rect = item.getBoundingClientRect();\n const tooltipText = item.dataset.tooltip;\n\n if (!tooltipText) return;\n\n this.menuTooltip.textContent = tooltipText;\n this.menuTooltip.style.left = `${rect.right + 8}px`;\n this.menuTooltip.style.top = `${rect.top + rect.height / 2}px`;\n this.menuTooltip.style.transform = 'translateY(-50%)';\n this.menuTooltip.showPopover();\n }\n\n private hideTooltip(): void {\n this.menuTooltip?.hidePopover();\n }\n\n private restoreState(): void {\n if (!this.appLayout) return;\n\n if (localStorage.getItem('sidebar-collapsed') === 'true') {\n this.appLayout.classList.add('menu-collapsed');\n }\n }\n}\n", "/**\n * Drawer Controller\n *\n * Handles all drawer functionality including profile, notifications, and todo drawers\n */\n\nexport type DrawerName = 'profile' | 'notification' | 'todo' | 'newTodo';\n\nexport class DrawerController {\n private profileDrawer: HTMLElement | null = null;\n private notificationDrawer: HTMLElement | null = null;\n private todoDrawer: HTMLElement | null = null;\n private newTodoDrawer: HTMLElement | null = null;\n private overlay: HTMLElement | null = null;\n private activeDrawer: DrawerName | null = null;\n private activeGenericDrawer: HTMLElement | null = null;\n\n constructor() {\n this.profileDrawer = document.getElementById('profileDrawer');\n this.notificationDrawer = document.getElementById('notificationDrawer');\n this.todoDrawer = document.getElementById('todoDrawer');\n this.newTodoDrawer = document.getElementById('newTodoDrawer');\n this.overlay = document.getElementById('drawerOverlay');\n\n this.setupListeners();\n this.setupGenericDrawers();\n }\n\n /**\n * Get currently active drawer name\n */\n get active(): DrawerName | null {\n return this.activeDrawer;\n }\n\n /**\n * Open a drawer by name\n */\n open(name: DrawerName): void {\n this.closeAll();\n\n const drawer = this.getDrawer(name);\n if (drawer && this.overlay) {\n drawer.classList.add('active');\n this.overlay.classList.add('active');\n document.body.style.overflow = 'hidden';\n this.activeDrawer = name;\n }\n }\n\n /**\n * Close a specific drawer\n */\n close(name: DrawerName): void {\n const drawer = this.getDrawer(name);\n drawer?.classList.remove('active');\n\n // Only hide overlay if no drawers are active\n if (this.overlay && !document.querySelector('.active[class*=\"drawer\"]')) {\n this.overlay.classList.remove('active');\n document.body.style.overflow = '';\n }\n\n if (this.activeDrawer === name) {\n this.activeDrawer = null;\n }\n }\n\n /**\n * Close all drawers\n */\n closeAll(): void {\n [this.profileDrawer, this.notificationDrawer, this.todoDrawer, this.newTodoDrawer]\n .forEach(drawer => drawer?.classList.remove('active'));\n\n // Close any generic drawers\n this.closeGenericDrawer();\n\n this.overlay?.classList.remove('active');\n document.body.style.overflow = '';\n this.activeDrawer = null;\n }\n\n /**\n * Open a generic drawer by ID\n */\n openGenericDrawer(drawerId: string): void {\n this.closeAll();\n\n const drawer = document.getElementById(drawerId);\n if (drawer && this.overlay) {\n drawer.classList.add('open');\n this.overlay.classList.add('active');\n document.body.style.overflow = 'hidden';\n this.activeGenericDrawer = drawer;\n }\n }\n\n /**\n * Close the currently open generic drawer\n */\n closeGenericDrawer(): void {\n this.activeGenericDrawer?.classList.remove('open');\n this.activeGenericDrawer = null;\n }\n\n /**\n * Open profile drawer\n */\n openProfile(): void {\n this.open('profile');\n }\n\n /**\n * Open notification drawer\n */\n openNotification(): void {\n this.open('notification');\n }\n\n /**\n * Open todo drawer (slides on top of profile)\n */\n openTodo(): void {\n this.todoDrawer?.classList.add('active');\n }\n\n /**\n * Close todo drawer\n */\n closeTodo(): void {\n this.todoDrawer?.classList.remove('active');\n this.closeNewTodo();\n }\n\n /**\n * Open new todo drawer\n */\n openNewTodo(): void {\n this.newTodoDrawer?.classList.add('active');\n }\n\n /**\n * Close new todo drawer\n */\n closeNewTodo(): void {\n this.newTodoDrawer?.classList.remove('active');\n }\n\n /**\n * Mark all notifications as read\n */\n markAllNotificationsRead(): void {\n if (!this.notificationDrawer) return;\n\n const unreadItems = this.notificationDrawer.querySelectorAll(\n 'swp-notification-item[data-unread=\"true\"]'\n );\n unreadItems.forEach(item => item.removeAttribute('data-unread'));\n\n const badge = document.querySelector('swp-notification-badge');\n if (badge) {\n badge.style.display = 'none';\n }\n }\n\n private getDrawer(name: DrawerName): HTMLElement | null {\n switch (name) {\n case 'profile': return this.profileDrawer;\n case 'notification': return this.notificationDrawer;\n case 'todo': return this.todoDrawer;\n case 'newTodo': return this.newTodoDrawer;\n }\n }\n\n private setupListeners(): void {\n // Profile drawer triggers\n document.getElementById('profileTrigger')\n ?.addEventListener('click', () => this.openProfile());\n document.getElementById('drawerClose')\n ?.addEventListener('click', () => this.close('profile'));\n\n // Notification drawer triggers\n document.getElementById('notificationsBtn')\n ?.addEventListener('click', () => this.openNotification());\n document.getElementById('notificationDrawerClose')\n ?.addEventListener('click', () => this.close('notification'));\n document.getElementById('markAllRead')\n ?.addEventListener('click', () => this.markAllNotificationsRead());\n\n // Todo drawer triggers\n document.getElementById('openTodoDrawer')\n ?.addEventListener('click', () => this.openTodo());\n document.getElementById('todoDrawerBack')\n ?.addEventListener('click', () => this.closeTodo());\n\n // New todo drawer triggers\n document.getElementById('addTodoBtn')\n ?.addEventListener('click', () => this.openNewTodo());\n document.getElementById('newTodoDrawerBack')\n ?.addEventListener('click', () => this.closeNewTodo());\n document.getElementById('cancelNewTodo')\n ?.addEventListener('click', () => this.closeNewTodo());\n document.getElementById('saveNewTodo')\n ?.addEventListener('click', () => this.closeNewTodo());\n\n // Overlay click closes all\n this.overlay?.addEventListener('click', () => this.closeAll());\n\n // Escape key closes all\n document.addEventListener('keydown', (e: KeyboardEvent) => {\n if (e.key === 'Escape') this.closeAll();\n });\n\n // Todo interactions\n this.todoDrawer?.addEventListener('click', (e) => this.handleTodoClick(e));\n\n // Visibility options\n document.addEventListener('click', (e) => this.handleVisibilityClick(e));\n }\n\n private handleTodoClick(e: Event): void {\n const target = e.target as HTMLElement;\n const todoItem = target.closest('swp-todo-item');\n const checkbox = target.closest('swp-todo-checkbox');\n\n if (checkbox && todoItem) {\n const isCompleted = todoItem.dataset.completed === 'true';\n if (isCompleted) {\n todoItem.removeAttribute('data-completed');\n } else {\n todoItem.dataset.completed = 'true';\n }\n }\n\n // Toggle section collapse\n const sectionHeader = target.closest('swp-todo-section-header');\n if (sectionHeader) {\n const section = sectionHeader.closest('swp-todo-section');\n section?.classList.toggle('collapsed');\n }\n }\n\n private handleVisibilityClick(e: Event): void {\n const target = e.target as HTMLElement;\n const option = target.closest('swp-visibility-option');\n\n if (option) {\n document.querySelectorAll('swp-visibility-option')\n .forEach(o => o.classList.remove('active'));\n option.classList.add('active');\n }\n }\n\n /**\n * Setup generic drawer triggers and close buttons\n * Uses data-drawer-trigger=\"drawer-id\" and data-drawer-close attributes\n */\n private setupGenericDrawers(): void {\n // Handle drawer triggers\n document.addEventListener('click', (e: Event) => {\n const target = e.target as HTMLElement;\n const trigger = target.closest('[data-drawer-trigger]');\n\n if (trigger) {\n const drawerId = trigger.dataset.drawerTrigger;\n if (drawerId) {\n this.openGenericDrawer(drawerId);\n }\n }\n });\n\n // Handle drawer close buttons\n document.addEventListener('click', (e: Event) => {\n const target = e.target as HTMLElement;\n const closeBtn = target.closest('[data-drawer-close]');\n\n if (closeBtn) {\n this.closeGenericDrawer();\n this.overlay?.classList.remove('active');\n document.body.style.overflow = '';\n }\n });\n }\n}\n", "/**\n * Theme Controller\n *\n * Handles dark/light mode switching and system preference detection\n */\n\nexport type Theme = 'light' | 'dark' | 'system';\n\nexport class ThemeController {\n private static readonly STORAGE_KEY = 'theme-preference';\n private static readonly DARK_CLASS = 'dark-mode';\n private static readonly LIGHT_CLASS = 'light-mode';\n\n private root: HTMLElement;\n private themeOptions: NodeListOf;\n\n constructor() {\n this.root = document.documentElement;\n this.themeOptions = document.querySelectorAll('swp-theme-option');\n\n this.applyTheme(this.current);\n this.updateUI();\n this.setupListeners();\n }\n\n /**\n * Get the current theme setting\n */\n get current(): Theme {\n const stored = localStorage.getItem(ThemeController.STORAGE_KEY) as Theme | null;\n if (stored === 'dark' || stored === 'light' || stored === 'system') {\n return stored;\n }\n return 'system';\n }\n\n /**\n * Check if dark mode is currently active\n */\n get isDark(): boolean {\n return this.root.classList.contains(ThemeController.DARK_CLASS) ||\n (this.systemPrefersDark && !this.root.classList.contains(ThemeController.LIGHT_CLASS));\n }\n\n /**\n * Check if system prefers dark mode\n */\n get systemPrefersDark(): boolean {\n return window.matchMedia('(prefers-color-scheme: dark)').matches;\n }\n\n /**\n * Set theme and persist preference\n */\n set(theme: Theme): void {\n localStorage.setItem(ThemeController.STORAGE_KEY, theme);\n this.applyTheme(theme);\n this.updateUI();\n }\n\n /**\n * Toggle between light and dark themes\n */\n toggle(): void {\n this.set(this.isDark ? 'light' : 'dark');\n }\n\n private applyTheme(theme: Theme): void {\n this.root.classList.remove(ThemeController.DARK_CLASS, ThemeController.LIGHT_CLASS);\n\n if (theme === 'dark') {\n this.root.classList.add(ThemeController.DARK_CLASS);\n } else if (theme === 'light') {\n this.root.classList.add(ThemeController.LIGHT_CLASS);\n }\n // 'system' leaves both classes off, letting CSS media query handle it\n }\n\n private updateUI(): void {\n if (!this.themeOptions) return;\n\n const darkActive = this.isDark;\n\n this.themeOptions.forEach(option => {\n const theme = option.dataset.theme as Theme;\n const isActive = (theme === 'dark' && darkActive) || (theme === 'light' && !darkActive);\n option.classList.toggle('active', isActive);\n });\n }\n\n private setupListeners(): void {\n // Theme option clicks\n this.themeOptions.forEach(option => {\n option.addEventListener('click', (e) => this.handleOptionClick(e));\n });\n\n // System theme changes\n window.matchMedia('(prefers-color-scheme: dark)')\n .addEventListener('change', () => this.handleSystemChange());\n }\n\n private handleOptionClick(e: Event): void {\n const target = e.target as HTMLElement;\n const option = target.closest('swp-theme-option');\n\n if (option) {\n const theme = option.dataset.theme as Theme;\n if (theme) {\n this.set(theme);\n }\n }\n }\n\n private handleSystemChange(): void {\n // Only react to system changes if we're using system preference\n if (this.current === 'system') {\n this.updateUI();\n }\n }\n}\n", "/**\n * Search Controller\n *\n * Handles global search functionality and keyboard shortcuts\n */\n\nexport class SearchController {\n private input: HTMLInputElement | null = null;\n private container: HTMLElement | null = null;\n\n constructor() {\n this.input = document.getElementById('globalSearch') as HTMLInputElement | null;\n this.container = document.querySelector('swp-topbar-search');\n\n this.setupListeners();\n }\n\n /**\n * Get current search value\n */\n get value(): string {\n return this.input?.value ?? '';\n }\n\n /**\n * Set search value\n */\n set value(val: string) {\n if (this.input) {\n this.input.value = val;\n }\n }\n\n /**\n * Focus the search input\n */\n focus(): void {\n this.input?.focus();\n }\n\n /**\n * Blur the search input\n */\n blur(): void {\n this.input?.blur();\n }\n\n /**\n * Clear the search input\n */\n clear(): void {\n this.value = '';\n }\n\n private setupListeners(): void {\n // Keyboard shortcuts\n document.addEventListener('keydown', (e) => this.handleKeyboard(e));\n\n // Input handlers\n if (this.input) {\n this.input.addEventListener('input', (e) => this.handleInput(e));\n\n // Prevent form submission if wrapped in form\n const form = this.input.closest('form');\n form?.addEventListener('submit', (e) => this.handleSubmit(e));\n }\n }\n\n private handleKeyboard(e: KeyboardEvent): void {\n // Cmd/Ctrl + K to focus search\n if ((e.metaKey || e.ctrlKey) && e.key === 'k') {\n e.preventDefault();\n this.focus();\n return;\n }\n\n // Escape to blur search when focused\n if (e.key === 'Escape' && document.activeElement === this.input) {\n this.blur();\n }\n }\n\n private handleInput(e: Event): void {\n const target = e.target as HTMLInputElement;\n const query = target.value.trim();\n\n // Emit custom event for search\n document.dispatchEvent(new CustomEvent('app:search', {\n detail: { query },\n bubbles: true\n }));\n }\n\n private handleSubmit(e: Event): void {\n e.preventDefault();\n\n const query = this.value.trim();\n if (!query) return;\n\n // Emit custom event for search submit\n document.dispatchEvent(new CustomEvent('app:search-submit', {\n detail: { query },\n bubbles: true\n }));\n }\n}\n", "/**\n * Lock Screen Controller\n *\n * Handles PIN-based lock screen functionality\n */\n\nimport { DrawerController } from './drawers';\n\nexport class LockScreenController {\n private static readonly CORRECT_PIN = '1234'; // Demo PIN\n\n private lockScreen: HTMLElement | null = null;\n private pinInput: HTMLElement | null = null;\n private pinKeypad: HTMLElement | null = null;\n private lockTimeEl: HTMLElement | null = null;\n private pinDigits: NodeListOf | null = null;\n private currentPin = '';\n private drawers: DrawerController | null = null;\n\n constructor(drawers?: DrawerController) {\n this.drawers = drawers ?? null;\n this.lockScreen = document.getElementById('lockScreen');\n this.pinInput = document.getElementById('pinInput');\n this.pinKeypad = document.getElementById('pinKeypad');\n this.lockTimeEl = document.getElementById('lockTime');\n this.pinDigits = this.pinInput?.querySelectorAll('swp-pin-digit') ?? null;\n\n this.setupListeners();\n }\n\n /**\n * Check if lock screen is active\n */\n get isActive(): boolean {\n return this.lockScreen?.classList.contains('active') ?? false;\n }\n\n /**\n * Show the lock screen\n */\n show(): void {\n this.drawers?.closeAll();\n\n if (this.lockScreen) {\n this.lockScreen.classList.add('active');\n document.body.style.overflow = 'hidden';\n }\n\n this.currentPin = '';\n this.updateDisplay();\n\n // Update lock time\n if (this.lockTimeEl) {\n this.lockTimeEl.textContent = `L\u00E5st kl. ${this.formatTime()}`;\n }\n }\n\n /**\n * Hide the lock screen\n */\n hide(): void {\n if (this.lockScreen) {\n this.lockScreen.classList.remove('active');\n document.body.style.overflow = '';\n }\n\n this.currentPin = '';\n this.updateDisplay();\n }\n\n private formatTime(): string {\n const now = new Date();\n const hours = now.getHours().toString().padStart(2, '0');\n const minutes = now.getMinutes().toString().padStart(2, '0');\n return `${hours}:${minutes}`;\n }\n\n private updateDisplay(): void {\n if (!this.pinDigits) return;\n\n this.pinDigits.forEach((digit, index) => {\n digit.classList.remove('filled', 'error');\n if (index < this.currentPin.length) {\n digit.textContent = '\u2022';\n digit.classList.add('filled');\n } else {\n digit.textContent = '';\n }\n });\n }\n\n private showError(): void {\n if (!this.pinDigits) return;\n\n this.pinDigits.forEach(digit => digit.classList.add('error'));\n\n // Shake animation\n this.pinInput?.classList.add('shake');\n\n setTimeout(() => {\n this.currentPin = '';\n this.updateDisplay();\n this.pinInput?.classList.remove('shake');\n }, 500);\n }\n\n private verify(): void {\n if (this.currentPin === LockScreenController.CORRECT_PIN) {\n this.hide();\n } else {\n this.showError();\n }\n }\n\n private addDigit(digit: string): void {\n if (this.currentPin.length >= 4) return;\n\n this.currentPin += digit;\n this.updateDisplay();\n\n // Auto-verify when 4 digits entered\n if (this.currentPin.length === 4) {\n setTimeout(() => this.verify(), 200);\n }\n }\n\n private removeDigit(): void {\n if (this.currentPin.length === 0) return;\n this.currentPin = this.currentPin.slice(0, -1);\n this.updateDisplay();\n }\n\n private clearPin(): void {\n this.currentPin = '';\n this.updateDisplay();\n }\n\n private setupListeners(): void {\n // Keypad click handler\n this.pinKeypad?.addEventListener('click', (e) => this.handleKeypadClick(e));\n\n // Keyboard input\n document.addEventListener('keydown', (e) => this.handleKeyboard(e));\n\n // Lock button in sidebar\n document.querySelector('swp-side-menu-action.lock')\n ?.addEventListener('click', () => this.show());\n }\n\n private handleKeypadClick(e: Event): void {\n const target = e.target as HTMLElement;\n const key = target.closest('swp-pin-key');\n\n if (!key) return;\n\n const digit = key.dataset.digit;\n const action = key.dataset.action;\n\n if (digit) {\n this.addDigit(digit);\n } else if (action === 'backspace') {\n this.removeDigit();\n } else if (action === 'clear') {\n this.clearPin();\n }\n }\n\n private handleKeyboard(e: KeyboardEvent): void {\n if (!this.isActive) return;\n\n // Prevent default to avoid other interactions\n e.preventDefault();\n\n if (e.key >= '0' && e.key <= '9') {\n this.addDigit(e.key);\n } else if (e.key === 'Backspace') {\n this.removeDigit();\n } else if (e.key === 'Escape') {\n this.clearPin();\n }\n }\n}\n", "/**\n * Salon OS App\n *\n * Main application class that orchestrates all UI controllers\n */\n\nimport { SidebarController } from './modules/sidebar';\nimport { DrawerController } from './modules/drawers';\nimport { ThemeController } from './modules/theme';\nimport { SearchController } from './modules/search';\nimport { LockScreenController } from './modules/lockscreen';\n\n/**\n * Main application class\n */\nexport class App {\n readonly sidebar: SidebarController;\n readonly drawers: DrawerController;\n readonly theme: ThemeController;\n readonly search: SearchController;\n readonly lockScreen: LockScreenController;\n\n constructor() {\n // Initialize controllers\n this.sidebar = new SidebarController();\n this.drawers = new DrawerController();\n this.theme = new ThemeController();\n this.search = new SearchController();\n this.lockScreen = new LockScreenController(this.drawers);\n }\n}\n\n/**\n * Global app instance\n */\nlet app: App;\n\n/**\n * Initialize the application\n */\nfunction init(): void {\n app = new App();\n\n // Expose to window for debugging\n if (typeof window !== 'undefined') {\n (window as unknown as { app: App }).app = app;\n }\n}\n\n// Wait for DOM ready\nif (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', init);\n} else {\n init();\n}\n\nexport { app };\nexport default App;\n"], - "mappings": "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", + "sources": ["../ts/modules/sidebar.ts", "../ts/modules/drawers.ts", "../ts/modules/theme.ts", "../ts/modules/search.ts", "../ts/modules/lockscreen.ts", "../ts/modules/cash.ts", "../ts/modules/employees.ts", "../ts/app.ts"], + "sourcesContent": ["/**\n * Sidebar Controller\n *\n * Handles sidebar collapse/expand and tooltip functionality\n */\n\nexport class SidebarController {\n private menuToggle: HTMLElement | null = null;\n private appLayout: HTMLElement | null = null;\n private menuTooltip: HTMLElement | null = null;\n\n constructor() {\n this.menuToggle = document.getElementById('menuToggle');\n this.appLayout = document.querySelector('swp-app-layout');\n this.menuTooltip = document.getElementById('menuTooltip');\n\n this.setupListeners();\n this.setupTooltips();\n this.restoreState();\n }\n\n /**\n * Check if sidebar is collapsed\n */\n get isCollapsed(): boolean {\n return this.appLayout?.classList.contains('menu-collapsed') ?? false;\n }\n\n /**\n * Toggle sidebar collapsed state\n */\n toggle(): void {\n if (!this.appLayout) return;\n\n this.appLayout.classList.toggle('menu-collapsed');\n localStorage.setItem('sidebar-collapsed', String(this.isCollapsed));\n }\n\n /**\n * Collapse the sidebar\n */\n collapse(): void {\n this.appLayout?.classList.add('menu-collapsed');\n localStorage.setItem('sidebar-collapsed', 'true');\n }\n\n /**\n * Expand the sidebar\n */\n expand(): void {\n this.appLayout?.classList.remove('menu-collapsed');\n localStorage.setItem('sidebar-collapsed', 'false');\n }\n\n private setupListeners(): void {\n this.menuToggle?.addEventListener('click', () => this.toggle());\n }\n\n private setupTooltips(): void {\n if (!this.menuTooltip) return;\n\n const menuItems = document.querySelectorAll('swp-side-menu-item[data-tooltip]');\n\n menuItems.forEach(item => {\n item.addEventListener('mouseenter', () => this.showTooltip(item));\n item.addEventListener('mouseleave', () => this.hideTooltip());\n });\n }\n\n private showTooltip(item: HTMLElement): void {\n if (!this.isCollapsed || !this.menuTooltip) return;\n\n const rect = item.getBoundingClientRect();\n const tooltipText = item.dataset.tooltip;\n\n if (!tooltipText) return;\n\n this.menuTooltip.textContent = tooltipText;\n this.menuTooltip.style.left = `${rect.right + 8}px`;\n this.menuTooltip.style.top = `${rect.top + rect.height / 2}px`;\n this.menuTooltip.style.transform = 'translateY(-50%)';\n this.menuTooltip.showPopover();\n }\n\n private hideTooltip(): void {\n this.menuTooltip?.hidePopover();\n }\n\n private restoreState(): void {\n if (!this.appLayout) return;\n\n if (localStorage.getItem('sidebar-collapsed') === 'true') {\n this.appLayout.classList.add('menu-collapsed');\n }\n }\n}\n", "/**\n * Drawer Controller\n *\n * Handles all drawer functionality including profile, notifications, and todo drawers\n */\n\nexport type DrawerName = 'profile' | 'notification' | 'todo' | 'newTodo';\n\nexport class DrawerController {\n private profileDrawer: HTMLElement | null = null;\n private notificationDrawer: HTMLElement | null = null;\n private todoDrawer: HTMLElement | null = null;\n private newTodoDrawer: HTMLElement | null = null;\n private overlay: HTMLElement | null = null;\n private activeDrawer: DrawerName | null = null;\n private activeGenericDrawer: HTMLElement | null = null;\n\n constructor() {\n this.profileDrawer = document.getElementById('profileDrawer');\n this.notificationDrawer = document.getElementById('notificationDrawer');\n this.todoDrawer = document.getElementById('todoDrawer');\n this.newTodoDrawer = document.getElementById('newTodoDrawer');\n this.overlay = document.getElementById('drawerOverlay');\n\n this.setupListeners();\n this.setupGenericDrawers();\n }\n\n /**\n * Get currently active drawer name\n */\n get active(): DrawerName | null {\n return this.activeDrawer;\n }\n\n /**\n * Open a drawer by name\n */\n open(name: DrawerName): void {\n this.closeAll();\n\n const drawer = this.getDrawer(name);\n if (drawer && this.overlay) {\n drawer.classList.add('active');\n this.overlay.classList.add('active');\n document.body.style.overflow = 'hidden';\n this.activeDrawer = name;\n }\n }\n\n /**\n * Close a specific drawer\n */\n close(name: DrawerName): void {\n const drawer = this.getDrawer(name);\n drawer?.classList.remove('active');\n\n // Only hide overlay if no drawers are active\n if (this.overlay && !document.querySelector('.active[class*=\"drawer\"]')) {\n this.overlay.classList.remove('active');\n document.body.style.overflow = '';\n }\n\n if (this.activeDrawer === name) {\n this.activeDrawer = null;\n }\n }\n\n /**\n * Close all drawers\n */\n closeAll(): void {\n [this.profileDrawer, this.notificationDrawer, this.todoDrawer, this.newTodoDrawer]\n .forEach(drawer => drawer?.classList.remove('active'));\n\n // Close any generic drawers\n this.closeGenericDrawer();\n\n this.overlay?.classList.remove('active');\n document.body.style.overflow = '';\n this.activeDrawer = null;\n }\n\n /**\n * Open a generic drawer by ID\n */\n openGenericDrawer(drawerId: string): void {\n this.closeAll();\n\n const drawer = document.getElementById(drawerId);\n if (drawer && this.overlay) {\n drawer.classList.add('open');\n this.overlay.classList.add('active');\n document.body.style.overflow = 'hidden';\n this.activeGenericDrawer = drawer;\n }\n }\n\n /**\n * Close the currently open generic drawer\n */\n closeGenericDrawer(): void {\n this.activeGenericDrawer?.classList.remove('open');\n this.activeGenericDrawer = null;\n }\n\n /**\n * Open profile drawer\n */\n openProfile(): void {\n this.open('profile');\n }\n\n /**\n * Open notification drawer\n */\n openNotification(): void {\n this.open('notification');\n }\n\n /**\n * Open todo drawer (slides on top of profile)\n */\n openTodo(): void {\n this.todoDrawer?.classList.add('active');\n }\n\n /**\n * Close todo drawer\n */\n closeTodo(): void {\n this.todoDrawer?.classList.remove('active');\n this.closeNewTodo();\n }\n\n /**\n * Open new todo drawer\n */\n openNewTodo(): void {\n this.newTodoDrawer?.classList.add('active');\n }\n\n /**\n * Close new todo drawer\n */\n closeNewTodo(): void {\n this.newTodoDrawer?.classList.remove('active');\n }\n\n /**\n * Mark all notifications as read\n */\n markAllNotificationsRead(): void {\n if (!this.notificationDrawer) return;\n\n const unreadItems = this.notificationDrawer.querySelectorAll(\n 'swp-notification-item[data-unread=\"true\"]'\n );\n unreadItems.forEach(item => item.removeAttribute('data-unread'));\n\n const badge = document.querySelector('swp-notification-badge');\n if (badge) {\n badge.style.display = 'none';\n }\n }\n\n private getDrawer(name: DrawerName): HTMLElement | null {\n switch (name) {\n case 'profile': return this.profileDrawer;\n case 'notification': return this.notificationDrawer;\n case 'todo': return this.todoDrawer;\n case 'newTodo': return this.newTodoDrawer;\n }\n }\n\n private setupListeners(): void {\n // Profile drawer triggers\n document.getElementById('profileTrigger')\n ?.addEventListener('click', () => this.openProfile());\n document.getElementById('drawerClose')\n ?.addEventListener('click', () => this.close('profile'));\n\n // Notification drawer triggers\n document.getElementById('notificationsBtn')\n ?.addEventListener('click', () => this.openNotification());\n document.getElementById('notificationDrawerClose')\n ?.addEventListener('click', () => this.close('notification'));\n document.getElementById('markAllRead')\n ?.addEventListener('click', () => this.markAllNotificationsRead());\n\n // Todo drawer triggers\n document.getElementById('openTodoDrawer')\n ?.addEventListener('click', () => this.openTodo());\n document.getElementById('todoDrawerBack')\n ?.addEventListener('click', () => this.closeTodo());\n\n // New todo drawer triggers\n document.getElementById('addTodoBtn')\n ?.addEventListener('click', () => this.openNewTodo());\n document.getElementById('newTodoDrawerBack')\n ?.addEventListener('click', () => this.closeNewTodo());\n document.getElementById('cancelNewTodo')\n ?.addEventListener('click', () => this.closeNewTodo());\n document.getElementById('saveNewTodo')\n ?.addEventListener('click', () => this.closeNewTodo());\n\n // Overlay click closes all\n this.overlay?.addEventListener('click', () => this.closeAll());\n\n // Escape key closes all\n document.addEventListener('keydown', (e: KeyboardEvent) => {\n if (e.key === 'Escape') this.closeAll();\n });\n\n // Todo interactions\n this.todoDrawer?.addEventListener('click', (e) => this.handleTodoClick(e));\n\n // Visibility options\n document.addEventListener('click', (e) => this.handleVisibilityClick(e));\n }\n\n private handleTodoClick(e: Event): void {\n const target = e.target as HTMLElement;\n const todoItem = target.closest('swp-todo-item');\n const checkbox = target.closest('swp-todo-checkbox');\n\n if (checkbox && todoItem) {\n const isCompleted = todoItem.dataset.completed === 'true';\n if (isCompleted) {\n todoItem.removeAttribute('data-completed');\n } else {\n todoItem.dataset.completed = 'true';\n }\n }\n\n // Toggle section collapse\n const sectionHeader = target.closest('swp-todo-section-header');\n if (sectionHeader) {\n const section = sectionHeader.closest('swp-todo-section');\n section?.classList.toggle('collapsed');\n }\n }\n\n private handleVisibilityClick(e: Event): void {\n const target = e.target as HTMLElement;\n const option = target.closest('swp-visibility-option');\n\n if (option) {\n document.querySelectorAll('swp-visibility-option')\n .forEach(o => o.classList.remove('active'));\n option.classList.add('active');\n }\n }\n\n /**\n * Setup generic drawer triggers and close buttons\n * Uses data-drawer-trigger=\"drawer-id\" and data-drawer-close attributes\n */\n private setupGenericDrawers(): void {\n // Handle drawer triggers\n document.addEventListener('click', (e: Event) => {\n const target = e.target as HTMLElement;\n const trigger = target.closest('[data-drawer-trigger]');\n\n if (trigger) {\n const drawerId = trigger.dataset.drawerTrigger;\n if (drawerId) {\n this.openGenericDrawer(drawerId);\n }\n }\n });\n\n // Handle drawer close buttons\n document.addEventListener('click', (e: Event) => {\n const target = e.target as HTMLElement;\n const closeBtn = target.closest('[data-drawer-close]');\n\n if (closeBtn) {\n this.closeGenericDrawer();\n this.overlay?.classList.remove('active');\n document.body.style.overflow = '';\n }\n });\n }\n}\n", "/**\n * Theme Controller\n *\n * Handles dark/light mode switching and system preference detection\n */\n\nexport type Theme = 'light' | 'dark' | 'system';\n\nexport class ThemeController {\n private static readonly STORAGE_KEY = 'theme-preference';\n private static readonly DARK_CLASS = 'dark-mode';\n private static readonly LIGHT_CLASS = 'light-mode';\n\n private root: HTMLElement;\n private themeOptions: NodeListOf;\n\n constructor() {\n this.root = document.documentElement;\n this.themeOptions = document.querySelectorAll('swp-theme-option');\n\n this.applyTheme(this.current);\n this.updateUI();\n this.setupListeners();\n }\n\n /**\n * Get the current theme setting\n */\n get current(): Theme {\n const stored = localStorage.getItem(ThemeController.STORAGE_KEY) as Theme | null;\n if (stored === 'dark' || stored === 'light' || stored === 'system') {\n return stored;\n }\n return 'system';\n }\n\n /**\n * Check if dark mode is currently active\n */\n get isDark(): boolean {\n return this.root.classList.contains(ThemeController.DARK_CLASS) ||\n (this.systemPrefersDark && !this.root.classList.contains(ThemeController.LIGHT_CLASS));\n }\n\n /**\n * Check if system prefers dark mode\n */\n get systemPrefersDark(): boolean {\n return window.matchMedia('(prefers-color-scheme: dark)').matches;\n }\n\n /**\n * Set theme and persist preference\n */\n set(theme: Theme): void {\n localStorage.setItem(ThemeController.STORAGE_KEY, theme);\n this.applyTheme(theme);\n this.updateUI();\n }\n\n /**\n * Toggle between light and dark themes\n */\n toggle(): void {\n this.set(this.isDark ? 'light' : 'dark');\n }\n\n private applyTheme(theme: Theme): void {\n this.root.classList.remove(ThemeController.DARK_CLASS, ThemeController.LIGHT_CLASS);\n\n if (theme === 'dark') {\n this.root.classList.add(ThemeController.DARK_CLASS);\n } else if (theme === 'light') {\n this.root.classList.add(ThemeController.LIGHT_CLASS);\n }\n // 'system' leaves both classes off, letting CSS media query handle it\n }\n\n private updateUI(): void {\n if (!this.themeOptions) return;\n\n const darkActive = this.isDark;\n\n this.themeOptions.forEach(option => {\n const theme = option.dataset.theme as Theme;\n const isActive = (theme === 'dark' && darkActive) || (theme === 'light' && !darkActive);\n option.classList.toggle('active', isActive);\n });\n }\n\n private setupListeners(): void {\n // Theme option clicks\n this.themeOptions.forEach(option => {\n option.addEventListener('click', (e) => this.handleOptionClick(e));\n });\n\n // System theme changes\n window.matchMedia('(prefers-color-scheme: dark)')\n .addEventListener('change', () => this.handleSystemChange());\n }\n\n private handleOptionClick(e: Event): void {\n const target = e.target as HTMLElement;\n const option = target.closest('swp-theme-option');\n\n if (option) {\n const theme = option.dataset.theme as Theme;\n if (theme) {\n this.set(theme);\n }\n }\n }\n\n private handleSystemChange(): void {\n // Only react to system changes if we're using system preference\n if (this.current === 'system') {\n this.updateUI();\n }\n }\n}\n", "/**\n * Search Controller\n *\n * Handles global search functionality and keyboard shortcuts\n */\n\nexport class SearchController {\n private input: HTMLInputElement | null = null;\n private container: HTMLElement | null = null;\n\n constructor() {\n this.input = document.getElementById('globalSearch') as HTMLInputElement | null;\n this.container = document.querySelector('swp-topbar-search');\n\n this.setupListeners();\n }\n\n /**\n * Get current search value\n */\n get value(): string {\n return this.input?.value ?? '';\n }\n\n /**\n * Set search value\n */\n set value(val: string) {\n if (this.input) {\n this.input.value = val;\n }\n }\n\n /**\n * Focus the search input\n */\n focus(): void {\n this.input?.focus();\n }\n\n /**\n * Blur the search input\n */\n blur(): void {\n this.input?.blur();\n }\n\n /**\n * Clear the search input\n */\n clear(): void {\n this.value = '';\n }\n\n private setupListeners(): void {\n // Keyboard shortcuts\n document.addEventListener('keydown', (e) => this.handleKeyboard(e));\n\n // Input handlers\n if (this.input) {\n this.input.addEventListener('input', (e) => this.handleInput(e));\n\n // Prevent form submission if wrapped in form\n const form = this.input.closest('form');\n form?.addEventListener('submit', (e) => this.handleSubmit(e));\n }\n }\n\n private handleKeyboard(e: KeyboardEvent): void {\n // Cmd/Ctrl + K to focus search\n if ((e.metaKey || e.ctrlKey) && e.key === 'k') {\n e.preventDefault();\n this.focus();\n return;\n }\n\n // Escape to blur search when focused\n if (e.key === 'Escape' && document.activeElement === this.input) {\n this.blur();\n }\n }\n\n private handleInput(e: Event): void {\n const target = e.target as HTMLInputElement;\n const query = target.value.trim();\n\n // Emit custom event for search\n document.dispatchEvent(new CustomEvent('app:search', {\n detail: { query },\n bubbles: true\n }));\n }\n\n private handleSubmit(e: Event): void {\n e.preventDefault();\n\n const query = this.value.trim();\n if (!query) return;\n\n // Emit custom event for search submit\n document.dispatchEvent(new CustomEvent('app:search-submit', {\n detail: { query },\n bubbles: true\n }));\n }\n}\n", "/**\n * Lock Screen Controller\n *\n * Handles PIN-based lock screen functionality\n */\n\nimport { DrawerController } from './drawers';\n\nexport class LockScreenController {\n private static readonly CORRECT_PIN = '1234'; // Demo PIN\n\n private lockScreen: HTMLElement | null = null;\n private pinInput: HTMLElement | null = null;\n private pinKeypad: HTMLElement | null = null;\n private lockTimeEl: HTMLElement | null = null;\n private pinDigits: NodeListOf | null = null;\n private currentPin = '';\n private drawers: DrawerController | null = null;\n\n constructor(drawers?: DrawerController) {\n this.drawers = drawers ?? null;\n this.lockScreen = document.getElementById('lockScreen');\n this.pinInput = document.getElementById('pinInput');\n this.pinKeypad = document.getElementById('pinKeypad');\n this.lockTimeEl = document.getElementById('lockTime');\n this.pinDigits = this.pinInput?.querySelectorAll('swp-pin-digit') ?? null;\n\n this.setupListeners();\n }\n\n /**\n * Check if lock screen is active\n */\n get isActive(): boolean {\n return this.lockScreen?.classList.contains('active') ?? false;\n }\n\n /**\n * Show the lock screen\n */\n show(): void {\n this.drawers?.closeAll();\n\n if (this.lockScreen) {\n this.lockScreen.classList.add('active');\n document.body.style.overflow = 'hidden';\n }\n\n this.currentPin = '';\n this.updateDisplay();\n\n // Update lock time\n if (this.lockTimeEl) {\n this.lockTimeEl.textContent = `L\u00E5st kl. ${this.formatTime()}`;\n }\n }\n\n /**\n * Hide the lock screen\n */\n hide(): void {\n if (this.lockScreen) {\n this.lockScreen.classList.remove('active');\n document.body.style.overflow = '';\n }\n\n this.currentPin = '';\n this.updateDisplay();\n }\n\n private formatTime(): string {\n const now = new Date();\n const hours = now.getHours().toString().padStart(2, '0');\n const minutes = now.getMinutes().toString().padStart(2, '0');\n return `${hours}:${minutes}`;\n }\n\n private updateDisplay(): void {\n if (!this.pinDigits) return;\n\n this.pinDigits.forEach((digit, index) => {\n digit.classList.remove('filled', 'error');\n if (index < this.currentPin.length) {\n digit.textContent = '\u2022';\n digit.classList.add('filled');\n } else {\n digit.textContent = '';\n }\n });\n }\n\n private showError(): void {\n if (!this.pinDigits) return;\n\n this.pinDigits.forEach(digit => digit.classList.add('error'));\n\n // Shake animation\n this.pinInput?.classList.add('shake');\n\n setTimeout(() => {\n this.currentPin = '';\n this.updateDisplay();\n this.pinInput?.classList.remove('shake');\n }, 500);\n }\n\n private verify(): void {\n if (this.currentPin === LockScreenController.CORRECT_PIN) {\n this.hide();\n } else {\n this.showError();\n }\n }\n\n private addDigit(digit: string): void {\n if (this.currentPin.length >= 4) return;\n\n this.currentPin += digit;\n this.updateDisplay();\n\n // Auto-verify when 4 digits entered\n if (this.currentPin.length === 4) {\n setTimeout(() => this.verify(), 200);\n }\n }\n\n private removeDigit(): void {\n if (this.currentPin.length === 0) return;\n this.currentPin = this.currentPin.slice(0, -1);\n this.updateDisplay();\n }\n\n private clearPin(): void {\n this.currentPin = '';\n this.updateDisplay();\n }\n\n private setupListeners(): void {\n // Keypad click handler\n this.pinKeypad?.addEventListener('click', (e) => this.handleKeypadClick(e));\n\n // Keyboard input\n document.addEventListener('keydown', (e) => this.handleKeyboard(e));\n\n // Lock button in sidebar\n document.querySelector('swp-side-menu-action.lock')\n ?.addEventListener('click', () => this.show());\n }\n\n private handleKeypadClick(e: Event): void {\n const target = e.target as HTMLElement;\n const key = target.closest('swp-pin-key');\n\n if (!key) return;\n\n const digit = key.dataset.digit;\n const action = key.dataset.action;\n\n if (digit) {\n this.addDigit(digit);\n } else if (action === 'backspace') {\n this.removeDigit();\n } else if (action === 'clear') {\n this.clearPin();\n }\n }\n\n private handleKeyboard(e: KeyboardEvent): void {\n if (!this.isActive) return;\n\n // Prevent default to avoid other interactions\n e.preventDefault();\n\n if (e.key >= '0' && e.key <= '9') {\n this.addDigit(e.key);\n } else if (e.key === 'Backspace') {\n this.removeDigit();\n } else if (e.key === 'Escape') {\n this.clearPin();\n }\n }\n}\n", "/**\n * Cash Controller\n *\n * Handles tab switching, cash calculations, and form interactions\n * for the Cash Register page.\n */\n\nexport class CashController {\n // Base values (from system - would come from server in real app)\n private readonly startBalance = 2000;\n private readonly cashSales = 3540;\n\n constructor() {\n this.setupTabs();\n this.setupCashCalculation();\n this.setupCheckboxSelection();\n this.setupApprovalCheckbox();\n this.setupDateFilters();\n this.setupRowToggle();\n this.setupDraftRowClick();\n }\n\n /**\n * Setup tab switching functionality\n */\n private setupTabs(): void {\n const tabs = document.querySelectorAll('swp-tab[data-tab]');\n\n tabs.forEach(tab => {\n tab.addEventListener('click', () => {\n const targetTab = tab.dataset.tab;\n if (targetTab) {\n this.switchToTab(targetTab);\n }\n });\n });\n }\n\n /**\n * Switch to a specific tab by name\n */\n private switchToTab(targetTab: string): void {\n const tabs = document.querySelectorAll('swp-tab[data-tab]');\n const contents = document.querySelectorAll('swp-tab-content[data-tab]');\n const statsBars = document.querySelectorAll('swp-cash-stats[data-for-tab]');\n\n // Update tab states\n tabs.forEach(t => {\n if (t.dataset.tab === targetTab) {\n t.classList.add('active');\n } else {\n t.classList.remove('active');\n }\n });\n\n // Update content visibility\n contents.forEach(content => {\n if (content.dataset.tab === targetTab) {\n content.classList.add('active');\n } else {\n content.classList.remove('active');\n }\n });\n\n // Update stats bar visibility\n statsBars.forEach(stats => {\n if (stats.dataset.forTab === targetTab) {\n stats.classList.add('active');\n } else {\n stats.classList.remove('active');\n }\n });\n }\n\n /**\n * Setup cash calculation with real-time updates\n */\n private setupCashCalculation(): void {\n const payoutsInput = document.getElementById('payouts') as HTMLInputElement;\n const toBankInput = document.getElementById('toBank') as HTMLInputElement;\n const actualCashInput = document.getElementById('actualCash') as HTMLInputElement;\n\n if (!payoutsInput || !toBankInput || !actualCashInput) return;\n\n const calculate = () => this.calculateCash(payoutsInput, toBankInput, actualCashInput);\n\n payoutsInput.addEventListener('input', calculate);\n toBankInput.addEventListener('input', calculate);\n actualCashInput.addEventListener('input', calculate);\n\n // Initial calculation\n calculate();\n }\n\n /**\n * Calculate expected cash and difference\n */\n private calculateCash(\n payoutsInput: HTMLInputElement,\n toBankInput: HTMLInputElement,\n actualCashInput: HTMLInputElement\n ): void {\n const payouts = this.parseNumber(payoutsInput.value);\n const toBank = this.parseNumber(toBankInput.value);\n const actual = this.parseNumber(actualCashInput.value);\n\n // Expected = start + sales - payouts - to bank\n const expectedCash = this.startBalance + this.cashSales - payouts - toBank;\n\n const expectedElement = document.getElementById('expectedCash');\n if (expectedElement) {\n expectedElement.textContent = this.formatNumber(expectedCash);\n }\n\n // Calculate and display difference\n this.updateDifference(actual, expectedCash, actualCashInput.value);\n }\n\n /**\n * Update difference box with color coding\n */\n private updateDifference(actual: number, expected: number, rawValue: string): void {\n const box = document.getElementById('differenceBox');\n const value = document.getElementById('differenceValue');\n if (!box || !value) return;\n\n const diff = actual - expected;\n\n // Remove all state classes\n box.classList.remove('positive', 'negative', 'neutral');\n\n if (actual === 0 && rawValue === '') {\n // No input yet\n value.textContent = '\u2013 kr';\n box.classList.add('neutral');\n } else if (diff > 0) {\n // More cash than expected\n value.textContent = '+' + this.formatNumber(diff) + ' kr';\n box.classList.add('positive');\n } else if (diff < 0) {\n // Less cash than expected\n value.textContent = this.formatNumber(diff) + ' kr';\n box.classList.add('negative');\n } else {\n // Exact match\n value.textContent = '0,00 kr';\n box.classList.add('neutral');\n }\n }\n\n /**\n * Setup checkbox selection for table rows\n */\n private setupCheckboxSelection(): void {\n const selectAll = document.getElementById('selectAll') as HTMLInputElement;\n const rowCheckboxes = document.querySelectorAll('.row-select');\n const exportBtn = document.getElementById('exportBtn') as HTMLButtonElement;\n const selectionCount = document.getElementById('selectionCount');\n\n if (!selectAll || !exportBtn || !selectionCount) return;\n\n const updateSelection = () => {\n const checked = document.querySelectorAll('.row-select:checked');\n const count = checked.length;\n\n selectionCount.textContent = count === 0 ? '0 valgt' : `${count} valgt`;\n exportBtn.disabled = count === 0;\n\n // Update select all state\n selectAll.checked = count === rowCheckboxes.length && count > 0;\n selectAll.indeterminate = count > 0 && count < rowCheckboxes.length;\n };\n\n selectAll.addEventListener('change', () => {\n rowCheckboxes.forEach(cb => cb.checked = selectAll.checked);\n updateSelection();\n });\n\n rowCheckboxes.forEach(cb => {\n cb.addEventListener('change', updateSelection);\n // Stop click from bubbling to row\n cb.addEventListener('click', e => e.stopPropagation());\n });\n }\n\n /**\n * Setup approval checkbox to enable/disable approve button\n */\n private setupApprovalCheckbox(): void {\n const checkbox = document.getElementById('confirmCheckbox') as HTMLInputElement;\n const approveBtn = document.getElementById('approveBtn') as HTMLButtonElement;\n\n if (!checkbox || !approveBtn) return;\n\n checkbox.addEventListener('change', () => {\n approveBtn.disabled = !checkbox.checked;\n });\n }\n\n /**\n * Setup date filter defaults (last 30 days)\n */\n private setupDateFilters(): void {\n const dateFrom = document.getElementById('dateFrom') as HTMLInputElement;\n const dateTo = document.getElementById('dateTo') as HTMLInputElement;\n\n if (!dateFrom || !dateTo) return;\n\n const today = new Date();\n const thirtyDaysAgo = new Date(today);\n thirtyDaysAgo.setDate(today.getDate() - 30);\n\n dateTo.value = this.formatDateISO(today);\n dateFrom.value = this.formatDateISO(thirtyDaysAgo);\n }\n\n /**\n * Format number as Danish currency\n */\n private formatNumber(num: number): string {\n return num.toLocaleString('da-DK', {\n minimumFractionDigits: 2,\n maximumFractionDigits: 2\n });\n }\n\n /**\n * Parse Danish number format\n */\n private parseNumber(str: string): number {\n if (!str) return 0;\n return parseFloat(str.replace(/\\./g, '').replace(',', '.')) || 0;\n }\n\n /**\n * Format date as ISO string (YYYY-MM-DD)\n */\n private formatDateISO(date: Date): string {\n return date.toISOString().split('T')[0];\n }\n\n /**\n * Setup row toggle for expandable details\n */\n private setupRowToggle(): void {\n const rows = document.querySelectorAll('swp-cash-table-row[data-id]:not(.draft-row)');\n\n rows.forEach(row => {\n const rowId = row.getAttribute('data-id');\n if (!rowId) return;\n\n const detail = document.querySelector(`swp-cash-row-detail[data-for=\"${rowId}\"]`);\n if (!detail) return;\n\n row.addEventListener('click', (e) => {\n // Don't toggle if clicking on checkbox\n if ((e.target as HTMLElement).closest('input[type=\"checkbox\"]')) return;\n\n const icon = row.querySelector('swp-row-toggle i');\n const isExpanded = row.classList.contains('expanded');\n\n // Close other expanded rows\n document.querySelectorAll('swp-cash-table-row.expanded').forEach(r => {\n if (r !== row) {\n const otherId = r.getAttribute('data-id');\n if (otherId) {\n const otherDetail = document.querySelector(`swp-cash-row-detail[data-for=\"${otherId}\"]`);\n const otherIcon = r.querySelector('swp-row-toggle i');\n if (otherDetail && otherIcon) {\n this.collapseRow(r, otherDetail, otherIcon as HTMLElement);\n }\n }\n }\n });\n\n // Toggle current row\n if (isExpanded) {\n this.collapseRow(row, detail, icon);\n } else {\n this.expandRow(row, detail, icon);\n }\n });\n });\n }\n\n /**\n * Expand a row with animation\n */\n private expandRow(row: Element, detail: HTMLElement, icon: Element | null): void {\n row.classList.add('expanded');\n detail.classList.add('expanded');\n\n // Animate icon rotation\n icon?.animate([\n { transform: 'rotate(0deg)' },\n { transform: 'rotate(90deg)' }\n ], {\n duration: 200,\n easing: 'ease-out',\n fill: 'forwards'\n });\n\n // Animate detail expansion\n const content = detail.querySelector('swp-row-detail-content') as HTMLElement;\n if (content) {\n const height = content.offsetHeight;\n detail.animate([\n { height: '0px', opacity: 0 },\n { height: `${height}px`, opacity: 1 }\n ], {\n duration: 250,\n easing: 'ease-out',\n fill: 'forwards'\n });\n }\n }\n\n /**\n * Collapse a row with animation\n */\n private collapseRow(row: Element, detail: HTMLElement, icon: Element | null): void {\n // Animate icon rotation\n icon?.animate([\n { transform: 'rotate(90deg)' },\n { transform: 'rotate(0deg)' }\n ], {\n duration: 200,\n easing: 'ease-out',\n fill: 'forwards'\n });\n\n // Animate detail collapse\n const content = detail.querySelector('swp-row-detail-content') as HTMLElement;\n if (content) {\n const height = content.offsetHeight;\n const animation = detail.animate([\n { height: `${height}px`, opacity: 1 },\n { height: '0px', opacity: 0 }\n ], {\n duration: 200,\n easing: 'ease-out',\n fill: 'forwards'\n });\n\n animation.onfinish = () => {\n row.classList.remove('expanded');\n detail.classList.remove('expanded');\n };\n } else {\n row.classList.remove('expanded');\n detail.classList.remove('expanded');\n }\n }\n\n /**\n * Setup draft row click to navigate to reconciliation tab\n */\n private setupDraftRowClick(): void {\n const draftRow = document.querySelector('swp-cash-table-row.draft-row');\n if (!draftRow) return;\n\n draftRow.style.cursor = 'pointer';\n draftRow.addEventListener('click', (e) => {\n // Don't navigate if clicking on checkbox\n if ((e.target as HTMLElement).closest('input[type=\"checkbox\"]')) return;\n\n this.switchToTab('afstemning');\n });\n }\n}\n", "/**\n * Employees Controller\n *\n * Handles content swap between list view and detail view,\n * plus tab switching within each view.\n * Uses History API for browser back/forward navigation.\n */\n\nexport class EmployeesController {\n private listView: HTMLElement | null = null;\n private detailView: HTMLElement | null = null;\n\n constructor() {\n this.listView = document.getElementById('employees-list-view');\n this.detailView = document.getElementById('employee-detail-view');\n\n // Only initialize if we're on the employees page\n if (!this.listView) return;\n\n this.setupListTabs();\n this.setupDetailTabs();\n this.setupChevronNavigation();\n this.setupBackNavigation();\n this.setupHistoryNavigation();\n this.restoreStateFromUrl();\n }\n\n /**\n * Setup popstate listener for browser back/forward\n */\n private setupHistoryNavigation(): void {\n window.addEventListener('popstate', (e: PopStateEvent) => {\n if (e.state?.employeeKey) {\n this.showDetailViewInternal(e.state.employeeKey);\n } else {\n this.showListViewInternal();\n }\n });\n }\n\n /**\n * Restore view state from URL on page load\n */\n private restoreStateFromUrl(): void {\n const hash = window.location.hash;\n if (hash.startsWith('#employee-')) {\n const employeeKey = hash.substring(1); // Remove #\n this.showDetailViewInternal(employeeKey);\n }\n }\n\n /**\n * Setup tab switching for the list view\n */\n private setupListTabs(): void {\n if (!this.listView) return;\n\n const tabs = this.listView.querySelectorAll('swp-tab-bar > swp-tab[data-tab]');\n\n tabs.forEach(tab => {\n tab.addEventListener('click', () => {\n const targetTab = tab.dataset.tab;\n if (targetTab) {\n this.switchTab(this.listView!, targetTab);\n }\n });\n });\n }\n\n /**\n * Setup tab switching for the detail view\n */\n private setupDetailTabs(): void {\n if (!this.detailView) return;\n\n const tabs = this.detailView.querySelectorAll('swp-tab-bar > swp-tab[data-tab]');\n\n tabs.forEach(tab => {\n tab.addEventListener('click', () => {\n const targetTab = tab.dataset.tab;\n if (targetTab) {\n this.switchTab(this.detailView!, targetTab);\n }\n });\n });\n }\n\n /**\n * Switch to a specific tab within a container\n */\n private switchTab(container: HTMLElement, targetTab: string): void {\n const tabs = container.querySelectorAll('swp-tab-bar > swp-tab[data-tab]');\n const contents = container.querySelectorAll('swp-tab-content[data-tab]');\n\n tabs.forEach(t => {\n t.classList.toggle('active', t.dataset.tab === targetTab);\n });\n\n contents.forEach(content => {\n content.classList.toggle('active', content.dataset.tab === targetTab);\n });\n }\n\n /**\n * Setup row click to show detail view\n * Ignores clicks on action buttons\n */\n private setupChevronNavigation(): void {\n document.addEventListener('click', (e: Event) => {\n const target = e.target as HTMLElement;\n\n // Ignore clicks on action buttons\n if (target.closest('swp-icon-btn') || target.closest('swp-table-actions')) {\n return;\n }\n\n const row = target.closest('swp-employee-row[data-employee-detail]');\n\n if (row) {\n const employeeKey = row.dataset.employeeDetail;\n if (employeeKey) {\n this.showDetailView(employeeKey);\n }\n }\n });\n }\n\n /**\n * Setup back button to return to list view\n */\n private setupBackNavigation(): void {\n document.addEventListener('click', (e: Event) => {\n const target = e.target as HTMLElement;\n const backLink = target.closest('[data-employee-back]');\n\n if (backLink) {\n this.showListView();\n }\n });\n }\n\n /**\n * Show the detail view and hide list view (with history push)\n */\n private showDetailView(employeeKey: string): void {\n // Push state to history\n history.pushState(\n { employeeKey },\n '',\n `#${employeeKey}`\n );\n this.showDetailViewInternal(employeeKey);\n }\n\n /**\n * Show detail view without modifying history (for popstate)\n */\n private showDetailViewInternal(employeeKey: string): void {\n if (this.listView && this.detailView) {\n this.listView.style.display = 'none';\n this.detailView.style.display = 'block';\n this.detailView.dataset.employee = employeeKey;\n\n // Reset to first tab\n this.switchTab(this.detailView, 'general');\n }\n }\n\n /**\n * Show the list view and hide detail view (with history push)\n */\n private showListView(): void {\n // Push state to history (clear hash)\n history.pushState(\n {},\n '',\n window.location.pathname\n );\n this.showListViewInternal();\n }\n\n /**\n * Show list view without modifying history (for popstate)\n */\n private showListViewInternal(): void {\n if (this.listView && this.detailView) {\n this.detailView.style.display = 'none';\n this.listView.style.display = 'block';\n }\n }\n}\n", "/**\n * Salon OS App\n *\n * Main application class that orchestrates all UI controllers\n */\n\nimport { SidebarController } from './modules/sidebar';\nimport { DrawerController } from './modules/drawers';\nimport { ThemeController } from './modules/theme';\nimport { SearchController } from './modules/search';\nimport { LockScreenController } from './modules/lockscreen';\nimport { CashController } from './modules/cash';\nimport { EmployeesController } from './modules/employees';\n\n/**\n * Main application class\n */\nexport class App {\n readonly sidebar: SidebarController;\n readonly drawers: DrawerController;\n readonly theme: ThemeController;\n readonly search: SearchController;\n readonly lockScreen: LockScreenController;\n readonly cash: CashController;\n readonly employees: EmployeesController;\n\n constructor() {\n // Initialize controllers\n this.sidebar = new SidebarController();\n this.drawers = new DrawerController();\n this.theme = new ThemeController();\n this.search = new SearchController();\n this.lockScreen = new LockScreenController(this.drawers);\n this.cash = new CashController();\n this.employees = new EmployeesController();\n }\n}\n\n/**\n * Global app instance\n */\nlet app: App;\n\n/**\n * Initialize the application\n */\nfunction init(): void {\n app = new App();\n\n // Expose to window for debugging\n if (typeof window !== 'undefined') {\n (window as unknown as { app: App }).app = app;\n }\n}\n\n// Wait for DOM ready\nif (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', init);\n} else {\n init();\n}\n\nexport { app };\nexport default App;\n"], + "mappings": "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", "names": [] } diff --git a/PlanTempus.Application/wwwroot/ts/app.ts b/PlanTempus.Application/wwwroot/ts/app.ts index 572c427..f8067d2 100644 --- a/PlanTempus.Application/wwwroot/ts/app.ts +++ b/PlanTempus.Application/wwwroot/ts/app.ts @@ -10,6 +10,7 @@ import { ThemeController } from './modules/theme'; import { SearchController } from './modules/search'; import { LockScreenController } from './modules/lockscreen'; import { CashController } from './modules/cash'; +import { EmployeesController } from './modules/employees'; /** * Main application class @@ -21,6 +22,7 @@ export class App { readonly search: SearchController; readonly lockScreen: LockScreenController; readonly cash: CashController; + readonly employees: EmployeesController; constructor() { // Initialize controllers @@ -30,6 +32,7 @@ export class App { this.search = new SearchController(); this.lockScreen = new LockScreenController(this.drawers); this.cash = new CashController(); + this.employees = new EmployeesController(); } } diff --git a/PlanTempus.Application/wwwroot/ts/modules/employees.ts b/PlanTempus.Application/wwwroot/ts/modules/employees.ts new file mode 100644 index 0000000..9c78e02 --- /dev/null +++ b/PlanTempus.Application/wwwroot/ts/modules/employees.ts @@ -0,0 +1,191 @@ +/** + * Employees Controller + * + * Handles content swap between list view and detail view, + * plus tab switching within each view. + * Uses History API for browser back/forward navigation. + */ + +export class EmployeesController { + private listView: HTMLElement | null = null; + private detailView: HTMLElement | null = null; + + constructor() { + this.listView = document.getElementById('employees-list-view'); + this.detailView = document.getElementById('employee-detail-view'); + + // Only initialize if we're on the employees page + if (!this.listView) return; + + this.setupListTabs(); + this.setupDetailTabs(); + this.setupChevronNavigation(); + this.setupBackNavigation(); + this.setupHistoryNavigation(); + this.restoreStateFromUrl(); + } + + /** + * Setup popstate listener for browser back/forward + */ + private setupHistoryNavigation(): void { + window.addEventListener('popstate', (e: PopStateEvent) => { + if (e.state?.employeeKey) { + this.showDetailViewInternal(e.state.employeeKey); + } else { + this.showListViewInternal(); + } + }); + } + + /** + * Restore view state from URL on page load + */ + private restoreStateFromUrl(): void { + const hash = window.location.hash; + if (hash.startsWith('#employee-')) { + const employeeKey = hash.substring(1); // Remove # + this.showDetailViewInternal(employeeKey); + } + } + + /** + * Setup tab switching for the list view + */ + private setupListTabs(): void { + if (!this.listView) return; + + const tabs = this.listView.querySelectorAll('swp-tab-bar > swp-tab[data-tab]'); + + tabs.forEach(tab => { + tab.addEventListener('click', () => { + const targetTab = tab.dataset.tab; + if (targetTab) { + this.switchTab(this.listView!, targetTab); + } + }); + }); + } + + /** + * Setup tab switching for the detail view + */ + private setupDetailTabs(): void { + if (!this.detailView) return; + + const tabs = this.detailView.querySelectorAll('swp-tab-bar > swp-tab[data-tab]'); + + tabs.forEach(tab => { + tab.addEventListener('click', () => { + const targetTab = tab.dataset.tab; + if (targetTab) { + this.switchTab(this.detailView!, targetTab); + } + }); + }); + } + + /** + * Switch to a specific tab within a container + */ + private switchTab(container: HTMLElement, targetTab: string): void { + const tabs = container.querySelectorAll('swp-tab-bar > swp-tab[data-tab]'); + const contents = container.querySelectorAll('swp-tab-content[data-tab]'); + + tabs.forEach(t => { + t.classList.toggle('active', t.dataset.tab === targetTab); + }); + + contents.forEach(content => { + content.classList.toggle('active', content.dataset.tab === targetTab); + }); + } + + /** + * Setup row click to show detail view + * Ignores clicks on action buttons + */ + private setupChevronNavigation(): void { + document.addEventListener('click', (e: Event) => { + const target = e.target as HTMLElement; + + // Ignore clicks on action buttons + if (target.closest('swp-icon-btn') || target.closest('swp-table-actions')) { + return; + } + + const row = target.closest('swp-employee-row[data-employee-detail]'); + + if (row) { + const employeeKey = row.dataset.employeeDetail; + if (employeeKey) { + this.showDetailView(employeeKey); + } + } + }); + } + + /** + * Setup back button to return to list view + */ + private setupBackNavigation(): void { + document.addEventListener('click', (e: Event) => { + const target = e.target as HTMLElement; + const backLink = target.closest('[data-employee-back]'); + + if (backLink) { + this.showListView(); + } + }); + } + + /** + * Show the detail view and hide list view (with history push) + */ + private showDetailView(employeeKey: string): void { + // Push state to history + history.pushState( + { employeeKey }, + '', + `#${employeeKey}` + ); + this.showDetailViewInternal(employeeKey); + } + + /** + * Show detail view without modifying history (for popstate) + */ + private showDetailViewInternal(employeeKey: string): void { + if (this.listView && this.detailView) { + this.listView.style.display = 'none'; + this.detailView.style.display = 'block'; + this.detailView.dataset.employee = employeeKey; + + // Reset to first tab + this.switchTab(this.detailView, 'general'); + } + } + + /** + * Show the list view and hide detail view (with history push) + */ + private showListView(): void { + // Push state to history (clear hash) + history.pushState( + {}, + '', + window.location.pathname + ); + this.showListViewInternal(); + } + + /** + * Show list view without modifying history (for popstate) + */ + private showListViewInternal(): void { + if (this.listView && this.detailView) { + this.detailView.style.display = 'none'; + this.listView.style.display = 'block'; + } + } +}