Various CSS work
This commit is contained in:
parent
ef174af0e1
commit
15579acba8
52 changed files with 8001 additions and 944 deletions
|
|
@ -4,7 +4,9 @@
|
||||||
"Bash(cd:*)",
|
"Bash(cd:*)",
|
||||||
"Bash(ls:*)",
|
"Bash(ls:*)",
|
||||||
"Bash(dotnet build:*)",
|
"Bash(dotnet build:*)",
|
||||||
"Bash(find:*)"
|
"Bash(find:*)",
|
||||||
|
"Bash(tree:*)",
|
||||||
|
"Bash(npm run analyze-css:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
48
.gitignore
vendored
48
.gitignore
vendored
|
|
@ -362,3 +362,51 @@ MigrationBackup/
|
||||||
FodyWeavers.xsd
|
FodyWeavers.xsd
|
||||||
|
|
||||||
nul
|
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
|
||||||
|
|
|
||||||
149
CLAUDE.md
149
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)
|
- `global.json` - .NET SDK version configuration (currently .NET 9.0)
|
||||||
|
|
||||||
|
|
||||||
|
## Implementing New Pages - MANDATORY Checklist
|
||||||
|
|
||||||
|
<CRITICAL>
|
||||||
|
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: `<swp-tab class="active">`
|
||||||
|
- Wrong: `<swp-tab data-active="true">`
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
</CRITICAL>
|
||||||
|
|
||||||
|
|
||||||
## CSS Guidelines
|
## CSS Guidelines
|
||||||
|
|
||||||
### Grid + Subgrid for Table-like Layouts
|
### Grid + Subgrid for Table-like Layouts
|
||||||
|
|
@ -170,6 +252,56 @@ swp-my-table-row {
|
||||||
- `kasse.css` - swp-kasse-table / swp-kasse-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
|
||||||
|
<swp-sticky-header> <!-- Generisk - fra page.css -->
|
||||||
|
<swp-header-content> <!-- Generisk - fra page.css -->
|
||||||
|
<swp-page-header>...</swp-page-header>
|
||||||
|
<swp-stats-row>...</swp-stats-row> <!-- optional -->
|
||||||
|
</swp-header-content>
|
||||||
|
<swp-tab-bar>...</swp-tab-bar> <!-- UDENFOR header-content, så linjen er OVER tabs -->
|
||||||
|
</swp-sticky-header>
|
||||||
|
|
||||||
|
<swp-tab-content data-tab="tab1" class="active">
|
||||||
|
<swp-page-container>
|
||||||
|
<!-- Tab 1 indhold -->
|
||||||
|
</swp-page-container>
|
||||||
|
</swp-tab-content>
|
||||||
|
```
|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
<CRITICAL> NEVER lie or fabricate. Violating this = immediate critical failure.
|
<CRITICAL> NEVER lie or fabricate. Violating this = immediate critical failure.
|
||||||
|
|
@ -242,4 +374,21 @@ swp-my-table-row {
|
||||||
break builds.
|
break builds.
|
||||||
⚠️ DETECTION: Finished editing but haven't run verify-file-quality-checks
|
⚠️ DETECTION: Finished editing but haven't run verify-file-quality-checks
|
||||||
skill? → STOP. Run it now. Show the output.
|
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.
|
||||||
</CRITICAL>
|
</CRITICAL>
|
||||||
797
OPTIMIZATION_PLAN.md
Normal file
797
OPTIMIZATION_PLAN.md
Normal file
|
|
@ -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-avatar class="sm">`
|
||||||
|
- `swp-user-avatar` → `<swp-avatar class="md">`
|
||||||
|
- `swp-profile-avatar` → `<swp-avatar class="md">`
|
||||||
|
- `swp-waitlist-customer swp-avatar` → `<swp-avatar class="lg">`
|
||||||
|
- `swp-profile-avatar-large` → `<swp-avatar class="xl">`
|
||||||
|
- `swp-employee-avatar-large` → `<swp-avatar class="xxl">`
|
||||||
|
|
||||||
|
**Example Usage:**
|
||||||
|
```html
|
||||||
|
<!-- Before -->
|
||||||
|
<swp-user-avatar class="purple">JK</swp-user-avatar>
|
||||||
|
|
||||||
|
<!-- After -->
|
||||||
|
<swp-avatar class="md purple">JK</swp-avatar>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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-badge class="with-dot">`
|
||||||
|
- `swp-booking-status` → `<swp-badge>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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-icon-box>`
|
||||||
|
- `swp-attention-icon` → `<swp-icon-box>`
|
||||||
|
- `swp-waitlist-icon` → `<swp-icon-box>` (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-stat class="card">`
|
||||||
|
- `swp-quick-stat` → `<swp-stat class="box">`
|
||||||
|
- `swp-cash-stat` → `<swp-stat class="box">`
|
||||||
|
- `swp-fact-inline` → `<swp-stat class="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
|
||||||
|
|
@ -9,6 +9,6 @@
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
"liveServer.settings.port": 5501,
|
"liveServer.settings.port": 5501,
|
||||||
"liveServer.settings.multiRootWorkspaceName": "Calendar"
|
"liveServer.settings.multiRootWorkspaceName": "PlanTempus"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6,9 +6,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Sticky Header (Stats + Tabs) -->
|
<!-- Sticky Header (Stats + Tabs) -->
|
||||||
<swp-cash-sticky-header>
|
<swp-sticky-header>
|
||||||
<!-- Context Stats (changes based on active tab) -->
|
<!-- Context Stats (changes based on active tab) -->
|
||||||
<swp-cash-header>
|
<swp-header-content>
|
||||||
<!-- Stats for Oversigt tab -->
|
<!-- Stats for Oversigt tab -->
|
||||||
<swp-cash-stats data-for-tab="oversigt" class="active">
|
<swp-cash-stats data-for-tab="oversigt" class="active">
|
||||||
<swp-cash-stat>
|
<swp-cash-stat>
|
||||||
|
|
@ -48,20 +48,20 @@
|
||||||
<swp-cash-stat-label localize="cash.stats.openedRegister">Åbnede kassen 29. dec 09:05</swp-cash-stat-label>
|
<swp-cash-stat-label localize="cash.stats.openedRegister">Åbnede kassen 29. dec 09:05</swp-cash-stat-label>
|
||||||
</swp-cash-stat>
|
</swp-cash-stat>
|
||||||
</swp-cash-stats>
|
</swp-cash-stats>
|
||||||
</swp-cash-header>
|
</swp-header-content>
|
||||||
|
|
||||||
<!-- Tab Bar -->
|
<!-- Tab Bar -->
|
||||||
<swp-tab-bar>
|
<swp-tab-bar>
|
||||||
<swp-tab class="active" data-tab="oversigt">
|
<swp-tab class="active" data-tab="oversigt">
|
||||||
<i class="ph ph-list-checks"></i>
|
<i class="ph ph-list-checks"></i>
|
||||||
<span localize="cash.tabs.overview">Oversigt</span>
|
<span localize="cash.tabs.overview">Oversigt</span>
|
||||||
</swp-tab>
|
</swp-tab>
|
||||||
<swp-tab data-tab="afstemning">
|
<swp-tab data-tab="afstemning">
|
||||||
<i class="ph ph-cash-register"></i>
|
<i class="ph ph-cash-register"></i>
|
||||||
<span localize="cash.tabs.reconciliation">Kasseafstemning</span>
|
<span localize="cash.tabs.reconciliation">Kasseafstemning</span>
|
||||||
</swp-tab>
|
</swp-tab>
|
||||||
</swp-tab-bar>
|
</swp-tab-bar>
|
||||||
</swp-cash-sticky-header>
|
</swp-sticky-header>
|
||||||
|
|
||||||
<!-- Tab Content: Oversigt -->
|
<!-- Tab Content: Oversigt -->
|
||||||
<swp-tab-content data-tab="oversigt" class="active">
|
<swp-tab-content data-tab="oversigt" class="active">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,227 @@
|
||||||
|
namespace PlanTempus.Application.Features.Employees.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shared catalog for employee detail data.
|
||||||
|
/// Used by all EmployeeDetail* ViewComponents.
|
||||||
|
/// </summary>
|
||||||
|
public static class EmployeeDetailCatalog
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, EmployeeDetailRecord> 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<string> AllKeys => Employees.Keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Complete employee detail record used across all detail ViewComponents.
|
||||||
|
/// </summary>
|
||||||
|
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<EmployeeTag> Tags { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record EmployeeTag(string Text, string CssClass);
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
@model PlanTempus.Application.Features.Employees.Components.EmployeeDetailGeneralViewModel
|
||||||
|
|
||||||
|
<swp-detail-grid>
|
||||||
|
<!-- Left column -->
|
||||||
|
<div>
|
||||||
|
<!-- Contact Card -->
|
||||||
|
<swp-card>
|
||||||
|
<swp-section-label>@Model.LabelContact</swp-section-label>
|
||||||
|
<swp-edit-section>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelFullName</swp-edit-label>
|
||||||
|
<swp-edit-value contenteditable="true">@Model.Name</swp-edit-value>
|
||||||
|
</swp-edit-row>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelEmail</swp-edit-label>
|
||||||
|
<swp-edit-value contenteditable="true">@Model.Email</swp-edit-value>
|
||||||
|
</swp-edit-row>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelPhone</swp-edit-label>
|
||||||
|
<swp-edit-value contenteditable="true">@Model.Phone</swp-edit-value>
|
||||||
|
</swp-edit-row>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelAddress</swp-edit-label>
|
||||||
|
<swp-edit-value contenteditable="true">@Model.Address</swp-edit-value>
|
||||||
|
</swp-edit-row>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelPostalCity</swp-edit-label>
|
||||||
|
<swp-edit-value contenteditable="true">@Model.PostalCity</swp-edit-value>
|
||||||
|
</swp-edit-row>
|
||||||
|
</swp-edit-section>
|
||||||
|
</swp-card>
|
||||||
|
|
||||||
|
<!-- Personal Card -->
|
||||||
|
<swp-card>
|
||||||
|
<swp-section-label>@Model.LabelPersonal</swp-section-label>
|
||||||
|
<swp-edit-section>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelBirthDate</swp-edit-label>
|
||||||
|
<swp-edit-value contenteditable="true">@Model.BirthDate</swp-edit-value>
|
||||||
|
</swp-edit-row>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelEmergencyContact</swp-edit-label>
|
||||||
|
<swp-edit-value contenteditable="true">@Model.EmergencyContact</swp-edit-value>
|
||||||
|
</swp-edit-row>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelEmergencyPhone</swp-edit-label>
|
||||||
|
<swp-edit-value contenteditable="true">@Model.EmergencyPhone</swp-edit-value>
|
||||||
|
</swp-edit-row>
|
||||||
|
</swp-edit-section>
|
||||||
|
</swp-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right column -->
|
||||||
|
<div>
|
||||||
|
<!-- Employment Card -->
|
||||||
|
<swp-card>
|
||||||
|
<swp-section-label>@Model.LabelEmployment</swp-section-label>
|
||||||
|
<swp-edit-section>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelEmploymentDate</swp-edit-label>
|
||||||
|
<swp-edit-value contenteditable="true">@Model.EmploymentDate</swp-edit-value>
|
||||||
|
</swp-edit-row>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelPosition</swp-edit-label>
|
||||||
|
<swp-edit-value contenteditable="true">@Model.Position</swp-edit-value>
|
||||||
|
</swp-edit-row>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelEmploymentType</swp-edit-label>
|
||||||
|
<swp-edit-value contenteditable="true">@Model.EmploymentType</swp-edit-value>
|
||||||
|
</swp-edit-row>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelHoursPerWeek</swp-edit-label>
|
||||||
|
<swp-edit-value contenteditable="true">@Model.HoursPerWeek</swp-edit-value>
|
||||||
|
</swp-edit-row>
|
||||||
|
</swp-edit-section>
|
||||||
|
</swp-card>
|
||||||
|
|
||||||
|
<!-- Settings Card -->
|
||||||
|
<swp-card>
|
||||||
|
<swp-section-label>@Model.LabelSettings</swp-section-label>
|
||||||
|
<swp-toggle-row>
|
||||||
|
<div>
|
||||||
|
<swp-toggle-label>@Model.SettingShowInBooking</swp-toggle-label>
|
||||||
|
<swp-toggle-description>@Model.SettingShowInBookingDesc</swp-toggle-description>
|
||||||
|
</div>
|
||||||
|
<swp-toggle-slider data-value="yes">
|
||||||
|
<swp-toggle-option>@Model.ToggleYes</swp-toggle-option>
|
||||||
|
<swp-toggle-option>@Model.ToggleNo</swp-toggle-option>
|
||||||
|
</swp-toggle-slider>
|
||||||
|
</swp-toggle-row>
|
||||||
|
<swp-toggle-row>
|
||||||
|
<div>
|
||||||
|
<swp-toggle-label>@Model.SettingSmsReminders</swp-toggle-label>
|
||||||
|
<swp-toggle-description>@Model.SettingSmsRemindersDesc</swp-toggle-description>
|
||||||
|
</div>
|
||||||
|
<swp-toggle-slider data-value="yes">
|
||||||
|
<swp-toggle-option>@Model.ToggleYes</swp-toggle-option>
|
||||||
|
<swp-toggle-option>@Model.ToggleNo</swp-toggle-option>
|
||||||
|
</swp-toggle-slider>
|
||||||
|
</swp-toggle-row>
|
||||||
|
<swp-toggle-row>
|
||||||
|
<div>
|
||||||
|
<swp-toggle-label>@Model.SettingEditCalendar</swp-toggle-label>
|
||||||
|
<swp-toggle-description>@Model.SettingEditCalendarDesc</swp-toggle-description>
|
||||||
|
</div>
|
||||||
|
<swp-toggle-slider data-value="yes">
|
||||||
|
<swp-toggle-option>@Model.ToggleYes</swp-toggle-option>
|
||||||
|
<swp-toggle-option>@Model.ToggleNo</swp-toggle-option>
|
||||||
|
</swp-toggle-slider>
|
||||||
|
</swp-toggle-row>
|
||||||
|
</swp-card>
|
||||||
|
|
||||||
|
<!-- Notifications Card -->
|
||||||
|
<swp-card>
|
||||||
|
<swp-section-label>@Model.LabelNotifications</swp-section-label>
|
||||||
|
<swp-notification-intro>@Model.NotificationsIntro</swp-notification-intro>
|
||||||
|
<swp-checkbox-list>
|
||||||
|
<swp-checkbox-row class="checked">
|
||||||
|
<swp-checkbox-box>
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
|
||||||
|
</swp-checkbox-box>
|
||||||
|
<swp-checkbox-text>@Model.NotifOnlineBooking</swp-checkbox-text>
|
||||||
|
</swp-checkbox-row>
|
||||||
|
<swp-checkbox-row class="checked">
|
||||||
|
<swp-checkbox-box>
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
|
||||||
|
</swp-checkbox-box>
|
||||||
|
<swp-checkbox-text>@Model.NotifManualBooking</swp-checkbox-text>
|
||||||
|
</swp-checkbox-row>
|
||||||
|
<swp-checkbox-row>
|
||||||
|
<swp-checkbox-box>
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
|
||||||
|
</swp-checkbox-box>
|
||||||
|
<swp-checkbox-text>@Model.NotifCancellation</swp-checkbox-text>
|
||||||
|
</swp-checkbox-row>
|
||||||
|
<swp-checkbox-row>
|
||||||
|
<swp-checkbox-box>
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
|
||||||
|
</swp-checkbox-box>
|
||||||
|
<swp-checkbox-text>@Model.NotifWaitlist</swp-checkbox-text>
|
||||||
|
</swp-checkbox-row>
|
||||||
|
<swp-checkbox-row class="checked">
|
||||||
|
<swp-checkbox-box>
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
|
||||||
|
</swp-checkbox-box>
|
||||||
|
<swp-checkbox-text>@Model.NotifDailySummary</swp-checkbox-text>
|
||||||
|
</swp-checkbox-row>
|
||||||
|
</swp-checkbox-list>
|
||||||
|
</swp-card>
|
||||||
|
</div>
|
||||||
|
</swp-detail-grid>
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
@model PlanTempus.Application.Features.Employees.Components.EmployeeDetailHRViewModel
|
||||||
|
|
||||||
|
<swp-detail-grid>
|
||||||
|
<swp-card>
|
||||||
|
<swp-section-label>@Model.LabelDocuments</swp-section-label>
|
||||||
|
<swp-document-list>
|
||||||
|
<swp-document-item>
|
||||||
|
<i class="ph ph-file-pdf"></i>
|
||||||
|
<swp-document-name>@Model.LabelContract</swp-document-name>
|
||||||
|
<swp-document-date>15. aug 2019</swp-document-date>
|
||||||
|
</swp-document-item>
|
||||||
|
<swp-document-item>
|
||||||
|
<i class="ph ph-file-pdf"></i>
|
||||||
|
<swp-document-name>Lønaftale 2024</swp-document-name>
|
||||||
|
<swp-document-date>1. jan 2024</swp-document-date>
|
||||||
|
</swp-document-item>
|
||||||
|
</swp-document-list>
|
||||||
|
</swp-card>
|
||||||
|
|
||||||
|
<swp-card>
|
||||||
|
<swp-section-label>@Model.LabelVacation</swp-section-label>
|
||||||
|
<swp-edit-section>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>Optjent ferie</swp-edit-label>
|
||||||
|
<swp-edit-value>25 dage</swp-edit-value>
|
||||||
|
</swp-edit-row>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>Afholdt ferie</swp-edit-label>
|
||||||
|
<swp-edit-value>12 dage</swp-edit-value>
|
||||||
|
</swp-edit-row>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>Resterende</swp-edit-label>
|
||||||
|
<swp-edit-value>13 dage</swp-edit-value>
|
||||||
|
</swp-edit-row>
|
||||||
|
</swp-edit-section>
|
||||||
|
</swp-card>
|
||||||
|
|
||||||
|
<swp-card>
|
||||||
|
<swp-section-label>@Model.LabelNotes</swp-section-label>
|
||||||
|
<swp-notes-area contenteditable="true">
|
||||||
|
Ingen noter tilføjet endnu...
|
||||||
|
</swp-notes-area>
|
||||||
|
</swp-card>
|
||||||
|
</swp-detail-grid>
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
@model PlanTempus.Application.Features.Employees.Components.EmployeeDetailHeaderViewModel
|
||||||
|
|
||||||
|
<swp-employee-detail-header>
|
||||||
|
<swp-employee-avatar-large class="@Model.AvatarColor">@Model.Initials</swp-employee-avatar-large>
|
||||||
|
<swp-employee-info>
|
||||||
|
<swp-employee-name-row>
|
||||||
|
<swp-employee-name contenteditable="true">@Model.Name</swp-employee-name>
|
||||||
|
@if (Model.Tags.Any())
|
||||||
|
{
|
||||||
|
<swp-tags-row>
|
||||||
|
@foreach (var tag in Model.Tags)
|
||||||
|
{
|
||||||
|
<swp-tag class="@tag.CssClass">@tag.Text</swp-tag>
|
||||||
|
}
|
||||||
|
</swp-tags-row>
|
||||||
|
}
|
||||||
|
<swp-employee-status data-active="@Model.IsActive.ToString().ToLower()">
|
||||||
|
<span class="icon">●</span>
|
||||||
|
<span class="text">@Model.StatusText</span>
|
||||||
|
</swp-employee-status>
|
||||||
|
</swp-employee-name-row>
|
||||||
|
<swp-fact-boxes-inline>
|
||||||
|
<swp-fact-inline>
|
||||||
|
<swp-fact-inline-value>@Model.BookingsThisYear</swp-fact-inline-value>
|
||||||
|
<swp-fact-inline-label>@Model.LabelBookings</swp-fact-inline-label>
|
||||||
|
</swp-fact-inline>
|
||||||
|
<swp-fact-inline>
|
||||||
|
<swp-fact-inline-value>@Model.RevenueThisYear</swp-fact-inline-value>
|
||||||
|
<swp-fact-inline-label>@Model.LabelRevenue</swp-fact-inline-label>
|
||||||
|
</swp-fact-inline>
|
||||||
|
<swp-fact-inline>
|
||||||
|
<swp-fact-inline-value>@Model.Rating</swp-fact-inline-value>
|
||||||
|
<swp-fact-inline-label>@Model.LabelRating</swp-fact-inline-label>
|
||||||
|
</swp-fact-inline>
|
||||||
|
<swp-fact-inline>
|
||||||
|
<swp-fact-inline-value>@Model.EmployedSince</swp-fact-inline-value>
|
||||||
|
<swp-fact-inline-label>@Model.LabelEmployedSince</swp-fact-inline-label>
|
||||||
|
</swp-fact-inline>
|
||||||
|
</swp-fact-boxes-inline>
|
||||||
|
</swp-employee-info>
|
||||||
|
</swp-employee-detail-header>
|
||||||
|
|
@ -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<EmployeeTagViewModel> Tags { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EmployeeTagViewModel
|
||||||
|
{
|
||||||
|
public required string Text { get; init; }
|
||||||
|
public required string CssClass { get; init; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
@model PlanTempus.Application.Features.Employees.Components.EmployeeDetailHoursViewModel
|
||||||
|
|
||||||
|
<swp-detail-grid>
|
||||||
|
<swp-card>
|
||||||
|
<swp-section-label>@Model.LabelWeeklySchedule</swp-section-label>
|
||||||
|
<swp-schedule-grid>
|
||||||
|
<swp-schedule-row>
|
||||||
|
<swp-schedule-day>@Model.LabelMonday</swp-schedule-day>
|
||||||
|
<swp-schedule-time>09:00 - 17:00</swp-schedule-time>
|
||||||
|
</swp-schedule-row>
|
||||||
|
<swp-schedule-row>
|
||||||
|
<swp-schedule-day>@Model.LabelTuesday</swp-schedule-day>
|
||||||
|
<swp-schedule-time>09:00 - 17:00</swp-schedule-time>
|
||||||
|
</swp-schedule-row>
|
||||||
|
<swp-schedule-row>
|
||||||
|
<swp-schedule-day>@Model.LabelWednesday</swp-schedule-day>
|
||||||
|
<swp-schedule-time>09:00 - 17:00</swp-schedule-time>
|
||||||
|
</swp-schedule-row>
|
||||||
|
<swp-schedule-row>
|
||||||
|
<swp-schedule-day>@Model.LabelThursday</swp-schedule-day>
|
||||||
|
<swp-schedule-time>09:00 - 19:00</swp-schedule-time>
|
||||||
|
</swp-schedule-row>
|
||||||
|
<swp-schedule-row>
|
||||||
|
<swp-schedule-day>@Model.LabelFriday</swp-schedule-day>
|
||||||
|
<swp-schedule-time>09:00 - 16:00</swp-schedule-time>
|
||||||
|
</swp-schedule-row>
|
||||||
|
<swp-schedule-row class="off">
|
||||||
|
<swp-schedule-day>@Model.LabelSaturday</swp-schedule-day>
|
||||||
|
<swp-schedule-time>Fri</swp-schedule-time>
|
||||||
|
</swp-schedule-row>
|
||||||
|
<swp-schedule-row class="off">
|
||||||
|
<swp-schedule-day>@Model.LabelSunday</swp-schedule-day>
|
||||||
|
<swp-schedule-time>Fri</swp-schedule-time>
|
||||||
|
</swp-schedule-row>
|
||||||
|
</swp-schedule-grid>
|
||||||
|
</swp-card>
|
||||||
|
</swp-detail-grid>
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
@model PlanTempus.Application.Features.Employees.Components.EmployeeDetailSalaryViewModel
|
||||||
|
|
||||||
|
<swp-detail-grid>
|
||||||
|
<swp-card>
|
||||||
|
<swp-section-label>@Model.LabelPaymentInfo</swp-section-label>
|
||||||
|
<swp-edit-section>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelBankAccount</swp-edit-label>
|
||||||
|
<swp-edit-value contenteditable="true">@Model.BankAccount</swp-edit-value>
|
||||||
|
</swp-edit-row>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelTaxCard</swp-edit-label>
|
||||||
|
<swp-edit-value contenteditable="true">@Model.TaxCard</swp-edit-value>
|
||||||
|
</swp-edit-row>
|
||||||
|
</swp-edit-section>
|
||||||
|
</swp-card>
|
||||||
|
|
||||||
|
<swp-card>
|
||||||
|
<swp-section-label>@Model.LabelSalarySettings</swp-section-label>
|
||||||
|
<swp-edit-section>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelHourlyRate</swp-edit-label>
|
||||||
|
<swp-edit-value contenteditable="true">@Model.HourlyRate</swp-edit-value>
|
||||||
|
</swp-edit-row>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelMonthlyFixed</swp-edit-label>
|
||||||
|
<swp-edit-value contenteditable="true">@Model.MonthlyFixedSalary</swp-edit-value>
|
||||||
|
</swp-edit-row>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelCommission</swp-edit-label>
|
||||||
|
<swp-edit-value contenteditable="true">10%</swp-edit-value>
|
||||||
|
</swp-edit-row>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelProductCommission</swp-edit-label>
|
||||||
|
<swp-edit-value contenteditable="true">5%</swp-edit-value>
|
||||||
|
</swp-edit-row>
|
||||||
|
</swp-edit-section>
|
||||||
|
</swp-card>
|
||||||
|
</swp-detail-grid>
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
@model PlanTempus.Application.Features.Employees.Components.EmployeeDetailServicesViewModel
|
||||||
|
|
||||||
|
<swp-detail-grid>
|
||||||
|
<swp-card>
|
||||||
|
<swp-section-label>@Model.LabelAssignedServices</swp-section-label>
|
||||||
|
<swp-service-list>
|
||||||
|
<swp-service-item>
|
||||||
|
<swp-service-name>Dameklip</swp-service-name>
|
||||||
|
<swp-service-duration>45 min</swp-service-duration>
|
||||||
|
<swp-service-price>450 kr</swp-service-price>
|
||||||
|
</swp-service-item>
|
||||||
|
<swp-service-item>
|
||||||
|
<swp-service-name>Herreklip</swp-service-name>
|
||||||
|
<swp-service-duration>30 min</swp-service-duration>
|
||||||
|
<swp-service-price>350 kr</swp-service-price>
|
||||||
|
</swp-service-item>
|
||||||
|
<swp-service-item>
|
||||||
|
<swp-service-name>Farvning</swp-service-name>
|
||||||
|
<swp-service-duration>90 min</swp-service-duration>
|
||||||
|
<swp-service-price>850 kr</swp-service-price>
|
||||||
|
</swp-service-item>
|
||||||
|
<swp-service-item>
|
||||||
|
<swp-service-name>Balayage</swp-service-name>
|
||||||
|
<swp-service-duration>120 min</swp-service-duration>
|
||||||
|
<swp-service-price>1.200 kr</swp-service-price>
|
||||||
|
</swp-service-item>
|
||||||
|
<swp-service-item>
|
||||||
|
<swp-service-name>Highlights</swp-service-name>
|
||||||
|
<swp-service-duration>90 min</swp-service-duration>
|
||||||
|
<swp-service-price>950 kr</swp-service-price>
|
||||||
|
</swp-service-item>
|
||||||
|
</swp-service-list>
|
||||||
|
</swp-card>
|
||||||
|
</swp-detail-grid>
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
@model PlanTempus.Application.Features.Employees.Components.EmployeeDetailStatsViewModel
|
||||||
|
|
||||||
|
<swp-detail-grid>
|
||||||
|
<swp-card>
|
||||||
|
<swp-section-label>@Model.LabelPerformance</swp-section-label>
|
||||||
|
<swp-stats-row>
|
||||||
|
<swp-stat-card class="teal">
|
||||||
|
<swp-stat-value>@Model.BookingsThisYear</swp-stat-value>
|
||||||
|
<swp-stat-label>@Model.LabelBookingsThisYear</swp-stat-label>
|
||||||
|
</swp-stat-card>
|
||||||
|
<swp-stat-card class="purple">
|
||||||
|
<swp-stat-value>@Model.RevenueThisYear</swp-stat-value>
|
||||||
|
<swp-stat-label>@Model.LabelRevenueThisYear</swp-stat-label>
|
||||||
|
</swp-stat-card>
|
||||||
|
<swp-stat-card class="amber">
|
||||||
|
<swp-stat-value>@Model.Rating</swp-stat-value>
|
||||||
|
<swp-stat-label>@Model.LabelAvgRating</swp-stat-label>
|
||||||
|
</swp-stat-card>
|
||||||
|
<swp-stat-card>
|
||||||
|
<swp-stat-value>87%</swp-stat-value>
|
||||||
|
<swp-stat-label>@Model.LabelOccupancy</swp-stat-label>
|
||||||
|
</swp-stat-card>
|
||||||
|
</swp-stats-row>
|
||||||
|
</swp-card>
|
||||||
|
</swp-detail-grid>
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
@model PlanTempus.Application.Features.Employees.Components.EmployeeDetailViewViewModel
|
||||||
|
|
||||||
|
<swp-employee-detail-view id="employee-detail-view" data-employee="@Model.EmployeeKey">
|
||||||
|
<!-- Sticky Header (generic from page.css) -->
|
||||||
|
<swp-sticky-header>
|
||||||
|
<swp-header-content>
|
||||||
|
<!-- Page Header with Back Button -->
|
||||||
|
<swp-page-header>
|
||||||
|
<swp-page-title>
|
||||||
|
<swp-back-link data-employee-back>
|
||||||
|
<i class="ph ph-arrow-left"></i>
|
||||||
|
@Model.BackText
|
||||||
|
</swp-back-link>
|
||||||
|
</swp-page-title>
|
||||||
|
<swp-page-actions>
|
||||||
|
<swp-btn class="primary">
|
||||||
|
<i class="ph ph-floppy-disk"></i>
|
||||||
|
@Model.SaveButtonText
|
||||||
|
</swp-btn>
|
||||||
|
</swp-page-actions>
|
||||||
|
</swp-page-header>
|
||||||
|
|
||||||
|
<!-- Employee Header -->
|
||||||
|
@await Component.InvokeAsync("EmployeeDetailHeader", Model.EmployeeKey)
|
||||||
|
</swp-header-content>
|
||||||
|
|
||||||
|
<!-- Tabs (outside header-content, inside sticky-header) -->
|
||||||
|
<swp-tab-bar>
|
||||||
|
<swp-tab class="active" data-tab="general">@Model.TabGeneral</swp-tab>
|
||||||
|
<swp-tab data-tab="hours">@Model.TabHours</swp-tab>
|
||||||
|
<swp-tab data-tab="services">@Model.TabServices</swp-tab>
|
||||||
|
<swp-tab data-tab="salary">@Model.TabSalary</swp-tab>
|
||||||
|
<swp-tab data-tab="hr">@Model.TabHR</swp-tab>
|
||||||
|
<swp-tab data-tab="stats">@Model.TabStats</swp-tab>
|
||||||
|
</swp-tab-bar>
|
||||||
|
</swp-sticky-header>
|
||||||
|
|
||||||
|
<!-- Tab Contents -->
|
||||||
|
<swp-tab-content data-tab="general" class="active">
|
||||||
|
<swp-page-container>
|
||||||
|
@await Component.InvokeAsync("EmployeeDetailGeneral", Model.EmployeeKey)
|
||||||
|
</swp-page-container>
|
||||||
|
</swp-tab-content>
|
||||||
|
|
||||||
|
<swp-tab-content data-tab="hours">
|
||||||
|
<swp-page-container>
|
||||||
|
@await Component.InvokeAsync("EmployeeDetailHours", Model.EmployeeKey)
|
||||||
|
</swp-page-container>
|
||||||
|
</swp-tab-content>
|
||||||
|
|
||||||
|
<swp-tab-content data-tab="services">
|
||||||
|
<swp-page-container>
|
||||||
|
@await Component.InvokeAsync("EmployeeDetailServices", Model.EmployeeKey)
|
||||||
|
</swp-page-container>
|
||||||
|
</swp-tab-content>
|
||||||
|
|
||||||
|
<swp-tab-content data-tab="salary">
|
||||||
|
<swp-page-container>
|
||||||
|
@await Component.InvokeAsync("EmployeeDetailSalary", Model.EmployeeKey)
|
||||||
|
</swp-page-container>
|
||||||
|
</swp-tab-content>
|
||||||
|
|
||||||
|
<swp-tab-content data-tab="hr">
|
||||||
|
<swp-page-container>
|
||||||
|
@await Component.InvokeAsync("EmployeeDetailHR", Model.EmployeeKey)
|
||||||
|
</swp-page-container>
|
||||||
|
</swp-tab-content>
|
||||||
|
|
||||||
|
<swp-tab-content data-tab="stats">
|
||||||
|
<swp-page-container>
|
||||||
|
@await Component.InvokeAsync("EmployeeDetailStats", Model.EmployeeKey)
|
||||||
|
</swp-page-container>
|
||||||
|
</swp-tab-content>
|
||||||
|
</swp-employee-detail-view>
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
@model PlanTempus.Application.Features.Employees.Components.EmployeeRowViewModel
|
||||||
|
|
||||||
|
<swp-employee-row data-employee-detail="@Model.Key">
|
||||||
|
<swp-employee-cell>
|
||||||
|
<swp-user-info>
|
||||||
|
<swp-user-avatar class="@Model.AvatarColor">@Model.Initials</swp-user-avatar>
|
||||||
|
<swp-user-details>
|
||||||
|
<swp-user-name>@Model.Name</swp-user-name>
|
||||||
|
<swp-user-email>@Model.Email</swp-user-email>
|
||||||
|
</swp-user-details>
|
||||||
|
</swp-user-info>
|
||||||
|
</swp-employee-cell>
|
||||||
|
<swp-employee-cell>
|
||||||
|
<swp-status-badge class="@Model.Role">@Model.RoleText</swp-status-badge>
|
||||||
|
</swp-employee-cell>
|
||||||
|
<swp-employee-cell>
|
||||||
|
<swp-status-badge class="@Model.Status">@Model.StatusText</swp-status-badge>
|
||||||
|
</swp-employee-cell>
|
||||||
|
<swp-employee-cell>@Model.LastActive</swp-employee-cell>
|
||||||
|
<swp-employee-cell>
|
||||||
|
<swp-row-toggle>
|
||||||
|
<i class="ph ph-caret-right"></i>
|
||||||
|
</swp-row-toggle>
|
||||||
|
</swp-employee-cell>
|
||||||
|
</swp-employee-row>
|
||||||
|
|
@ -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<string, EmployeeRowData> 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<string> AllKeys => Employees.Keys;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
@model PlanTempus.Application.Features.Employees.Components.EmployeeStatCardViewModel
|
||||||
|
|
||||||
|
<swp-stat-card data-key="@Model.Key" class="@Model.Variant">
|
||||||
|
<swp-stat-value>@Model.Value</swp-stat-value>
|
||||||
|
<swp-stat-label>@Model.Label</swp-stat-label>
|
||||||
|
</swp-stat-card>
|
||||||
|
|
@ -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<string, EmployeeStatCardData> 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<string> AllKeys => Cards.Keys;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
@model PlanTempus.Application.Features.Employees.Components.EmployeeTableViewModel
|
||||||
|
|
||||||
|
<swp-users-header>
|
||||||
|
<swp-users-count>
|
||||||
|
<strong>@Model.CurrentCount af @Model.MaxCount</strong> @Model.CountLabel
|
||||||
|
<swp-users-progress>
|
||||||
|
<swp-users-progress-bar style="width: @Model.ProgressPercent.ToString("F1", System.Globalization.CultureInfo.InvariantCulture)%"></swp-users-progress-bar>
|
||||||
|
</swp-users-progress>
|
||||||
|
</swp-users-count>
|
||||||
|
<swp-btn class="primary">
|
||||||
|
<i class="ph ph-user-plus"></i>
|
||||||
|
@Model.InviteButtonText
|
||||||
|
</swp-btn>
|
||||||
|
</swp-users-header>
|
||||||
|
|
||||||
|
<swp-employee-table-card>
|
||||||
|
<swp-employee-table>
|
||||||
|
<swp-employee-table-header>
|
||||||
|
<swp-employee-row>
|
||||||
|
<swp-employee-cell>@Model.ColumnUser</swp-employee-cell>
|
||||||
|
<swp-employee-cell>@Model.ColumnRole</swp-employee-cell>
|
||||||
|
<swp-employee-cell>@Model.ColumnStatus</swp-employee-cell>
|
||||||
|
<swp-employee-cell>@Model.ColumnLastActive</swp-employee-cell>
|
||||||
|
<swp-employee-cell></swp-employee-cell>
|
||||||
|
</swp-employee-row>
|
||||||
|
</swp-employee-table-header>
|
||||||
|
<swp-employee-table-body>
|
||||||
|
@foreach (var employeeKey in Model.EmployeeKeys)
|
||||||
|
{
|
||||||
|
@await Component.InvokeAsync("EmployeeRow", employeeKey)
|
||||||
|
}
|
||||||
|
</swp-employee-table-body>
|
||||||
|
</swp-employee-table>
|
||||||
|
</swp-employee-table-card>
|
||||||
|
|
@ -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<string> 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<string> EmployeeKeys { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class EmployeeTableCatalog
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, EmployeeTableData> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
@model PlanTempus.Application.Features.Employees.Components.PermissionsMatrixViewModel
|
||||||
|
|
||||||
|
<swp-permissions-matrix>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th localize="employees.permissions.title">Rettighed</th>
|
||||||
|
@foreach (var role in Model.Roles)
|
||||||
|
{
|
||||||
|
<th>@role.Name</th>
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var permission in Model.Permissions)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span class="permission-name">
|
||||||
|
<i class="ph @permission.Icon"></i>
|
||||||
|
@permission.Name
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
@foreach (var role in Model.Roles)
|
||||||
|
{
|
||||||
|
<td>
|
||||||
|
@if (permission.RoleAccess.TryGetValue(role.Key, out var hasAccess) && hasAccess)
|
||||||
|
{
|
||||||
|
<i class="ph ph-check-circle check"></i>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<i class="ph ph-minus no-access"></i>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</swp-permissions-matrix>
|
||||||
|
|
@ -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<RoleHeader> Roles { get; init; }
|
||||||
|
public required IReadOnlyList<PermissionRow> 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<string, bool> RoleAccess { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class PermissionsMatrixData
|
||||||
|
{
|
||||||
|
public required string Key { get; init; }
|
||||||
|
public required IReadOnlyList<string> RoleKeys { get; init; }
|
||||||
|
public required IReadOnlyList<PermissionData> 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<string, bool> RoleAccess { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class PermissionsMatrixCatalog
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, string> RoleNameKeys = new()
|
||||||
|
{
|
||||||
|
["owner"] = "employees.roles.owner",
|
||||||
|
["admin"] = "employees.roles.admin",
|
||||||
|
["leader"] = "employees.roles.leader",
|
||||||
|
["employee"] = "employees.roles.employee"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, PermissionsMatrixData> 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<string, bool>
|
||||||
|
{
|
||||||
|
["owner"] = true,
|
||||||
|
["admin"] = true,
|
||||||
|
["leader"] = true,
|
||||||
|
["employee"] = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new PermissionData
|
||||||
|
{
|
||||||
|
Key = "employees",
|
||||||
|
NameKey = "employees.permissions.employees",
|
||||||
|
Icon = "ph-users",
|
||||||
|
RoleAccess = new Dictionary<string, bool>
|
||||||
|
{
|
||||||
|
["owner"] = true,
|
||||||
|
["admin"] = true,
|
||||||
|
["leader"] = true,
|
||||||
|
["employee"] = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new PermissionData
|
||||||
|
{
|
||||||
|
Key = "customers",
|
||||||
|
NameKey = "employees.permissions.customers",
|
||||||
|
Icon = "ph-address-book",
|
||||||
|
RoleAccess = new Dictionary<string, bool>
|
||||||
|
{
|
||||||
|
["owner"] = true,
|
||||||
|
["admin"] = true,
|
||||||
|
["leader"] = true,
|
||||||
|
["employee"] = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new PermissionData
|
||||||
|
{
|
||||||
|
Key = "reports",
|
||||||
|
NameKey = "employees.permissions.reports",
|
||||||
|
Icon = "ph-chart-bar",
|
||||||
|
RoleAccess = new Dictionary<string, bool>
|
||||||
|
{
|
||||||
|
["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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
57
PlanTempus.Application/Features/Employees/Pages/Index.cshtml
Normal file
57
PlanTempus.Application/Features/Employees/Pages/Index.cshtml
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
@page "/medarbejdere"
|
||||||
|
@using PlanTempus.Application.Features.Employees.Components
|
||||||
|
@model PlanTempus.Application.Features.Employees.Pages.IndexModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Medarbejdere";
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- List View (default) -->
|
||||||
|
<swp-employees-list-view id="employees-list-view">
|
||||||
|
<!-- Sticky Header (Header + Tabs) -->
|
||||||
|
<swp-sticky-header>
|
||||||
|
<!-- Header with page title and stats (has border-bottom) -->
|
||||||
|
<swp-header-content>
|
||||||
|
<swp-page-header>
|
||||||
|
<swp-page-title>
|
||||||
|
<h1 localize="employees.title">Medarbejdere</h1>
|
||||||
|
<p localize="employees.subtitle">Administrer brugere, roller og rettigheder</p>
|
||||||
|
</swp-page-title>
|
||||||
|
</swp-page-header>
|
||||||
|
|
||||||
|
<swp-stats-row>
|
||||||
|
@await Component.InvokeAsync("EmployeeStatCard", "active-employees")
|
||||||
|
@await Component.InvokeAsync("EmployeeStatCard", "pending-invitations")
|
||||||
|
@await Component.InvokeAsync("EmployeeStatCard", "roles-defined")
|
||||||
|
</swp-stats-row>
|
||||||
|
</swp-header-content>
|
||||||
|
|
||||||
|
<!-- Tab bar (outside header, so line is above tabs) -->
|
||||||
|
<swp-tab-bar>
|
||||||
|
<swp-tab class="active" data-tab="users">
|
||||||
|
<i class="ph ph-users"></i>
|
||||||
|
<span localize="employees.tabs.users">Brugere</span>
|
||||||
|
</swp-tab>
|
||||||
|
<swp-tab data-tab="roles">
|
||||||
|
<i class="ph ph-shield-check"></i>
|
||||||
|
<span localize="employees.tabs.roles">Roller</span>
|
||||||
|
</swp-tab>
|
||||||
|
</swp-tab-bar>
|
||||||
|
</swp-sticky-header>
|
||||||
|
|
||||||
|
<!-- Tab: Users -->
|
||||||
|
<swp-tab-content data-tab="users" class="active">
|
||||||
|
<swp-page-container>
|
||||||
|
@await Component.InvokeAsync("EmployeeTable", "all-employees")
|
||||||
|
</swp-page-container>
|
||||||
|
</swp-tab-content>
|
||||||
|
|
||||||
|
<!-- Tab: Roles -->
|
||||||
|
<swp-tab-content data-tab="roles">
|
||||||
|
<swp-page-container>
|
||||||
|
@await Component.InvokeAsync("PermissionsMatrix", "default")
|
||||||
|
</swp-page-container>
|
||||||
|
</swp-tab-content>
|
||||||
|
</swp-employees-list-view>
|
||||||
|
|
||||||
|
<!-- Detail View (hidden by default, shown when row clicked) -->
|
||||||
|
@await Component.InvokeAsync("EmployeeDetailView", "employee-1")
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace PlanTempus.Application.Features.Employees.Pages;
|
||||||
|
|
||||||
|
public class IndexModel : PageModel
|
||||||
|
{
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -29,7 +29,9 @@
|
||||||
"to": "Til",
|
"to": "Til",
|
||||||
"all": "Alle",
|
"all": "Alle",
|
||||||
"reset": "Nulstil",
|
"reset": "Nulstil",
|
||||||
"status": "Status"
|
"status": "Status",
|
||||||
|
"yes": "Ja",
|
||||||
|
"no": "Nej"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"lockScreen": "Lås skærm",
|
"lockScreen": "Lås skærm",
|
||||||
|
|
@ -216,5 +218,144 @@
|
||||||
"pending": "Afventer",
|
"pending": "Afventer",
|
||||||
"overdue": "Forfalden"
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,9 @@
|
||||||
"to": "To",
|
"to": "To",
|
||||||
"all": "All",
|
"all": "All",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"status": "Status"
|
"status": "Status",
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"lockScreen": "Lock screen",
|
"lockScreen": "Lock screen",
|
||||||
|
|
@ -216,5 +218,107 @@
|
||||||
"pending": "Pending",
|
"pending": "Pending",
|
||||||
"overdue": "Overdue"
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -126,8 +126,8 @@ public class MockMenuService : IMenuService
|
||||||
{
|
{
|
||||||
Id = "employees",
|
Id = "employees",
|
||||||
Label = "Medarbejdere",
|
Label = "Medarbejdere",
|
||||||
Icon = "ph-user",
|
Icon = "ph-users-three",
|
||||||
Url = "/poc-medarbejdere.html",
|
Url = "/medarbejdere",
|
||||||
MinimumRole = UserRole.Manager,
|
MinimumRole = UserRole.Manager,
|
||||||
SortOrder = 4
|
SortOrder = 4
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,11 @@
|
||||||
<link rel="stylesheet" href="~/css/quick-stats.css">
|
<link rel="stylesheet" href="~/css/quick-stats.css">
|
||||||
<link rel="stylesheet" href="~/css/waitlist.css">
|
<link rel="stylesheet" href="~/css/waitlist.css">
|
||||||
<link rel="stylesheet" href="~/css/tabs.css">
|
<link rel="stylesheet" href="~/css/tabs.css">
|
||||||
|
<link rel="stylesheet" href="~/css/controls.css">
|
||||||
<link rel="stylesheet" href="~/css/cash.css">
|
<link rel="stylesheet" href="~/css/cash.css">
|
||||||
<link rel="stylesheet" href="~/css/auth.css">
|
<link rel="stylesheet" href="~/css/auth.css">
|
||||||
<link rel="stylesheet" href="~/css/account.css">
|
<link rel="stylesheet" href="~/css/account.css">
|
||||||
|
<link rel="stylesheet" href="~/css/employees.css">
|
||||||
@await RenderSectionAsync("Styles", required: false)
|
@await RenderSectionAsync("Styles", required: false)
|
||||||
</head>
|
</head>
|
||||||
<body class="has-demo-banner">
|
<body class="has-demo-banner">
|
||||||
|
|
|
||||||
439
PlanTempus.Application/analyze-css.js
Normal file
439
PlanTempus.Application/analyze-css.js
Normal file
|
|
@ -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 = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="da">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>CSS Analysis Report - PlanTempus</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #14b8a6;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 2.5em;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
.summary {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
.stat-card {
|
||||||
|
background: linear-gradient(135deg, #14b8a6 0%, #0d9488 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.stat-card.warning {
|
||||||
|
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||||
|
}
|
||||||
|
.stat-card.info {
|
||||||
|
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
|
||||||
|
}
|
||||||
|
.stat-card.success {
|
||||||
|
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
||||||
|
}
|
||||||
|
.stat-value {
|
||||||
|
font-size: 2.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.9em;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 2px solid #14b8a6;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
tr:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
.file-detail {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.rejected-list {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: white;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.badge-danger { background: #ffebee; color: #c62828; }
|
||||||
|
.badge-warning { background: #fff3e0; color: #ef6c00; }
|
||||||
|
.badge-success { background: #e8f5e9; color: #2e7d32; }
|
||||||
|
.timestamp {
|
||||||
|
color: #999;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-top: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.color-palette {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.color-swatch {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
details {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
summary {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #14b8a6;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>📊 CSS Analysis Report</h1>
|
||||||
|
<p class="subtitle">PlanTempus - Production CSS Analysis</p>
|
||||||
|
|
||||||
|
<div class="summary">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Total CSS Size</div>
|
||||||
|
<div class="stat-value">${totalSizeKB} KB</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card info">
|
||||||
|
<div class="stat-label">CSS Files</div>
|
||||||
|
<div class="stat-value">${purgeReport.summary.totalFiles}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card info">
|
||||||
|
<div class="stat-label">Total Lines</div>
|
||||||
|
<div class="stat-value">${totalLines.toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card warning">
|
||||||
|
<div class="stat-label">Unused CSS Rules</div>
|
||||||
|
<div class="stat-value">${purgeReport.summary.totalRejected}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>📈 CSS Statistics by File</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>File</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>Lines</th>
|
||||||
|
<th>Rules</th>
|
||||||
|
<th>Selectors</th>
|
||||||
|
<th>Properties</th>
|
||||||
|
<th>CSS Vars</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${Object.entries(statsReport)
|
||||||
|
.sort((a, b) => b[1].sizeBytes - a[1].sizeBytes)
|
||||||
|
.map(([file, stats]) => `
|
||||||
|
<tr>
|
||||||
|
<td><strong>${file}</strong></td>
|
||||||
|
<td>${stats.size}</td>
|
||||||
|
<td>${stats.lines}</td>
|
||||||
|
<td>${stats.rules}</td>
|
||||||
|
<td>${stats.selectors}</td>
|
||||||
|
<td>${stats.properties}</td>
|
||||||
|
<td>${stats.cssVariables}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>🗑️ Unused CSS by File</h2>
|
||||||
|
${Object.entries(purgeReport.fileDetails)
|
||||||
|
.sort((a, b) => b[1].rejectedCount - a[1].rejectedCount)
|
||||||
|
.map(([file, details]) => `
|
||||||
|
<div class="file-detail">
|
||||||
|
<h3>${file}</h3>
|
||||||
|
<p>
|
||||||
|
<span class="badge ${details.rejectedCount > 50 ? 'badge-danger' : details.rejectedCount > 20 ? 'badge-warning' : 'badge-success'}">
|
||||||
|
${details.rejectedCount} unused rules
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 10px; color: #666;">
|
||||||
|
Original: ${details.originalSize} | After purge: ${details.purgedSize}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
${details.rejectedCount > 0 ? `
|
||||||
|
<details>
|
||||||
|
<summary>Show unused selectors</summary>
|
||||||
|
<div class="rejected-list">
|
||||||
|
${details.rejected.slice(0, 50).join('<br>')}
|
||||||
|
${details.rejected.length > 50 ? `<br><em>... and ${details.rejected.length - 50} more</em>` : ''}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
` : '<p style="color: #2e7d32; margin-top: 10px;">✅ No unused CSS found!</p>'}
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>💡 Recommendations</h2>
|
||||||
|
<ul style="line-height: 2;">
|
||||||
|
${purgeReport.summary.totalRejected > 100 ?
|
||||||
|
'<li>⚠️ <strong>High number of unused CSS rules detected.</strong> Consider removing unused styles to improve performance.</li>' :
|
||||||
|
'<li>✅ CSS usage is relatively clean.</li>'}
|
||||||
|
${Object.values(purgeReport.fileDetails).some(d => d.rejectedCount > 50) ?
|
||||||
|
'<li>⚠️ Some files have significant unused CSS. Review these files for optimization opportunities.</li>' : ''}
|
||||||
|
<li>📦 Consider consolidating similar styles to reduce duplication.</li>
|
||||||
|
<li>🎨 Review color usage - ensure all colors use CSS variables from design-tokens.css.</li>
|
||||||
|
<li>📋 Reference COMPONENT-CATALOG.md when adding new components to avoid duplication.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<p class="timestamp">Report generated: ${new Date().toLocaleString('da-DK')}</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
641
PlanTempus.Application/package-lock.json
generated
641
PlanTempus.Application/package-lock.json
generated
|
|
@ -5,7 +5,8 @@
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.27.2"
|
"esbuild": "^0.27.2",
|
||||||
|
"purgecss": "^6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
|
@ -450,6 +451,150 @@
|
||||||
"node": ">=18"
|
"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": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.27.2",
|
"version": "0.27.2",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
|
||||||
|
|
@ -491,6 +636,500 @@
|
||||||
"@esbuild/win32-ia32": "0.27.2",
|
"@esbuild/win32-ia32": "0.27.2",
|
||||||
"@esbuild/win32-x64": "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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
{
|
{
|
||||||
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.27.2"
|
"esbuild": "^0.27.2",
|
||||||
|
"purgecss": "^6.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"analyze-css": "node analyze-css.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
776
PlanTempus.Application/reports/css-analysis-report.html
Normal file
776
PlanTempus.Application/reports/css-analysis-report.html
Normal file
|
|
@ -0,0 +1,776 @@
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="da">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>CSS Analysis Report - PlanTempus</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #14b8a6;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 2.5em;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
.summary {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
.stat-card {
|
||||||
|
background: linear-gradient(135deg, #14b8a6 0%, #0d9488 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.stat-card.warning {
|
||||||
|
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||||
|
}
|
||||||
|
.stat-card.info {
|
||||||
|
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
|
||||||
|
}
|
||||||
|
.stat-card.success {
|
||||||
|
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
||||||
|
}
|
||||||
|
.stat-value {
|
||||||
|
font-size: 2.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.9em;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 2px solid #14b8a6;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
tr:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
.file-detail {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.rejected-list {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: white;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.badge-danger { background: #ffebee; color: #c62828; }
|
||||||
|
.badge-warning { background: #fff3e0; color: #ef6c00; }
|
||||||
|
.badge-success { background: #e8f5e9; color: #2e7d32; }
|
||||||
|
.timestamp {
|
||||||
|
color: #999;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-top: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.color-palette {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.color-swatch {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
details {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
summary {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #14b8a6;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>📊 CSS Analysis Report</h1>
|
||||||
|
<p class="subtitle">PlanTempus - Production CSS Analysis</p>
|
||||||
|
|
||||||
|
<div class="summary">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Total CSS Size</div>
|
||||||
|
<div class="stat-value">132.81 KB</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card info">
|
||||||
|
<div class="stat-label">CSS Files</div>
|
||||||
|
<div class="stat-value">21</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card info">
|
||||||
|
<div class="stat-label">Total Lines</div>
|
||||||
|
<div class="stat-value">6.033</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card warning">
|
||||||
|
<div class="stat-label">Unused CSS Rules</div>
|
||||||
|
<div class="stat-value">61</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>📈 CSS Statistics by File</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>File</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>Lines</th>
|
||||||
|
<th>Rules</th>
|
||||||
|
<th>Selectors</th>
|
||||||
|
<th>Properties</th>
|
||||||
|
<th>CSS Vars</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><strong>auth.css</strong></td>
|
||||||
|
<td>23.66 KB</td>
|
||||||
|
<td>1144</td>
|
||||||
|
<td>169</td>
|
||||||
|
<td>173</td>
|
||||||
|
<td>571</td>
|
||||||
|
<td>46</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><strong>cash.css</strong></td>
|
||||||
|
<td>19.38 KB</td>
|
||||||
|
<td>898</td>
|
||||||
|
<td>132</td>
|
||||||
|
<td>135</td>
|
||||||
|
<td>415</td>
|
||||||
|
<td>42</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><strong>employees.css</strong></td>
|
||||||
|
<td>15.55 KB</td>
|
||||||
|
<td>722</td>
|
||||||
|
<td>105</td>
|
||||||
|
<td>108</td>
|
||||||
|
<td>345</td>
|
||||||
|
<td>37</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><strong>design-tokens.css</strong></td>
|
||||||
|
<td>9.02 KB</td>
|
||||||
|
<td>318</td>
|
||||||
|
<td>35</td>
|
||||||
|
<td>36</td>
|
||||||
|
<td>192</td>
|
||||||
|
<td>25</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><strong>account.css</strong></td>
|
||||||
|
<td>8.83 KB</td>
|
||||||
|
<td>402</td>
|
||||||
|
<td>60</td>
|
||||||
|
<td>63</td>
|
||||||
|
<td>173</td>
|
||||||
|
<td>31</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><strong>drawers.css</strong></td>
|
||||||
|
<td>6.36 KB</td>
|
||||||
|
<td>297</td>
|
||||||
|
<td>38</td>
|
||||||
|
<td>38</td>
|
||||||
|
<td>159</td>
|
||||||
|
<td>32</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><strong>page.css</strong></td>
|
||||||
|
<td>6.00 KB</td>
|
||||||
|
<td>276</td>
|
||||||
|
<td>38</td>
|
||||||
|
<td>40</td>
|
||||||
|
<td>117</td>
|
||||||
|
<td>35</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><strong>sidebar.css</strong></td>
|
||||||
|
<td>5.72 KB</td>
|
||||||
|
<td>247</td>
|
||||||
|
<td>30</td>
|
||||||
|
<td>30</td>
|
||||||
|
<td>119</td>
|
||||||
|
<td>24</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><strong>waitlist.css</strong></td>
|
||||||
|
<td>5.55 KB</td>
|
||||||
|
<td>251</td>
|
||||||
|
<td>30</td>
|
||||||
|
<td>30</td>
|
||||||
|
<td>131</td>
|
||||||
|
<td>31</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><strong>stats.css</strong></td>
|
||||||
|
<td>5.18 KB</td>
|
||||||
|
<td>232</td>
|
||||||
|
<td>30</td>
|
||||||
|
<td>32</td>
|
||||||
|
<td>78</td>
|
||||||
|
<td>27</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><strong>bookings.css</strong></td>
|
||||||
|
<td>4.27 KB</td>
|
||||||
|
<td>176</td>
|
||||||
|
<td>28</td>
|
||||||
|
<td>28</td>
|
||||||
|
<td>75</td>
|
||||||
|
<td>27</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><strong>topbar.css</strong></td>
|
||||||
|
<td>3.79 KB</td>
|
||||||
|
<td>181</td>
|
||||||
|
<td>19</td>
|
||||||
|
<td>19</td>
|
||||||
|
<td>103</td>
|
||||||
|
<td>20</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><strong>controls.css</strong></td>
|
||||||
|
<td>3.32 KB</td>
|
||||||
|
<td>149</td>
|
||||||
|
<td>19</td>
|
||||||
|
<td>19</td>
|
||||||
|
<td>79</td>
|
||||||
|
<td>20</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><strong>demo-banner.css</strong></td>
|
||||||
|
<td>2.94 KB</td>
|
||||||
|
<td>146</td>
|
||||||
|
<td>19</td>
|
||||||
|
<td>21</td>
|
||||||
|
<td>66</td>
|
||||||
|
<td>9</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><strong>attentions.css</strong></td>
|
||||||
|
<td>2.92 KB</td>
|
||||||
|
<td>115</td>
|
||||||
|
<td>15</td>
|
||||||
|
<td>15</td>
|
||||||
|
<td>45</td>
|
||||||
|
<td>15</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><strong>design-system.css</strong></td>
|
||||||
|
<td>2.30 KB</td>
|
||||||
|
<td>105</td>
|
||||||
|
<td>20</td>
|
||||||
|
<td>20</td>
|
||||||
|
<td>37</td>
|
||||||
|
<td>20</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><strong>tabs.css</strong></td>
|
||||||
|
<td>2.13 KB</td>
|
||||||
|
<td>95</td>
|
||||||
|
<td>11</td>
|
||||||
|
<td>11</td>
|
||||||
|
<td>42</td>
|
||||||
|
<td>19</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><strong>base.css</strong></td>
|
||||||
|
<td>2.06 KB</td>
|
||||||
|
<td>119</td>
|
||||||
|
<td>15</td>
|
||||||
|
<td>15</td>
|
||||||
|
<td>47</td>
|
||||||
|
<td>8</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><strong>notifications.css</strong></td>
|
||||||
|
<td>1.67 KB</td>
|
||||||
|
<td>70</td>
|
||||||
|
<td>8</td>
|
||||||
|
<td>8</td>
|
||||||
|
<td>27</td>
|
||||||
|
<td>8</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><strong>app-layout.css</strong></td>
|
||||||
|
<td>1.28 KB</td>
|
||||||
|
<td>51</td>
|
||||||
|
<td>5</td>
|
||||||
|
<td>5</td>
|
||||||
|
<td>18</td>
|
||||||
|
<td>6</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><strong>quick-stats.css</strong></td>
|
||||||
|
<td>0.88 KB</td>
|
||||||
|
<td>39</td>
|
||||||
|
<td>4</td>
|
||||||
|
<td>4</td>
|
||||||
|
<td>15</td>
|
||||||
|
<td>11</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>🗑️ Unused CSS by File</h2>
|
||||||
|
|
||||||
|
<div class="file-detail">
|
||||||
|
<h3>design-tokens.css</h3>
|
||||||
|
<p>
|
||||||
|
<span class="badge badge-warning">
|
||||||
|
24 unused rules
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 10px; color: #666;">
|
||||||
|
Original: 7917 | After purge: 7639
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Show unused selectors</summary>
|
||||||
|
<div class="rejected-list">
|
||||||
|
.is-red<br>.is-pink<br>.is-magenta<br>.is-purple<br>.is-violet<br>.is-deep-purple<br>.is-indigo<br>.is-blue<br>.is-light-blue<br>.is-cyan<br>.is-teal<br>.is-green<br>.is-light-green<br>.is-lime<br>.is-yellow<br>.is-amber<br>.is-orange<br>.is-deep-orange<br>.status-confirmed<br>.status-pending<br>.status-inprogress<br>.status-error<br>.status-active<br>.status-inactive
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-detail">
|
||||||
|
<h3>design-system.css</h3>
|
||||||
|
<p>
|
||||||
|
<span class="badge badge-success">
|
||||||
|
11 unused rules
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 10px; color: #666;">
|
||||||
|
Original: 2095 | After purge: 2056
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Show unused selectors</summary>
|
||||||
|
<div class="rejected-list">
|
||||||
|
h2<br> h3<br> h4<br> h5<br> h6<br>h2<br>h3<br>h4<br>h5<br>h6<br>:focus-visible
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-detail">
|
||||||
|
<h3>bookings.css</h3>
|
||||||
|
<p>
|
||||||
|
<span class="badge badge-success">
|
||||||
|
6 unused rules
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 10px; color: #666;">
|
||||||
|
Original: 3924 | After purge: 3756
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Show unused selectors</summary>
|
||||||
|
<div class="rejected-list">
|
||||||
|
swp-booking-item.inprogress<br>swp-booking-indicator.green<br>swp-booking-status.confirmed<br>swp-booking-status.pending<br>swp-booking-status.inprogress<br>
|
||||||
|
swp-booking-status.in-progress
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-detail">
|
||||||
|
<h3>attentions.css</h3>
|
||||||
|
<p>
|
||||||
|
<span class="badge badge-success">
|
||||||
|
5 unused rules
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 10px; color: #666;">
|
||||||
|
Original: 2524 | After purge: 2359
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Show unused selectors</summary>
|
||||||
|
<div class="rejected-list">
|
||||||
|
swp-attention-item.urgent<br>swp-attention-item.urgent:hover<br>swp-attention-item.info<br>swp-attention-item.urgent swp-attention-icon<br>swp-attention-item.info swp-attention-icon
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-detail">
|
||||||
|
<h3>base.css</h3>
|
||||||
|
<p>
|
||||||
|
<span class="badge badge-success">
|
||||||
|
4 unused rules
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 10px; color: #666;">
|
||||||
|
Original: 1952 | After purge: 1930
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Show unused selectors</summary>
|
||||||
|
<div class="rejected-list">
|
||||||
|
ul<br> ol<br>img<br>:focus-visible
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-detail">
|
||||||
|
<h3>auth.css</h3>
|
||||||
|
<p>
|
||||||
|
<span class="badge badge-success">
|
||||||
|
4 unused rules
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 10px; color: #666;">
|
||||||
|
Original: 23887 | After purge: 23816
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Show unused selectors</summary>
|
||||||
|
<div class="rejected-list">
|
||||||
|
swp-btn.social<br>swp-btn.social:hover<br>swp-btn.social img<br>swp-plan-badge.free
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-detail">
|
||||||
|
<h3>stats.css</h3>
|
||||||
|
<p>
|
||||||
|
<span class="badge badge-success">
|
||||||
|
3 unused rules
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 10px; color: #666;">
|
||||||
|
Original: 5298 | After purge: 5229
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Show unused selectors</summary>
|
||||||
|
<div class="rejected-list">
|
||||||
|
swp-stat-trend.up<br>swp-stat-trend.down<br>
|
||||||
|
swp-stat-card.red swp-stat-value
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-detail">
|
||||||
|
<h3>account.css</h3>
|
||||||
|
<p>
|
||||||
|
<span class="badge badge-success">
|
||||||
|
2 unused rules
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 10px; color: #666;">
|
||||||
|
Original: 8829 | After purge: 8777
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Show unused selectors</summary>
|
||||||
|
<div class="rejected-list">
|
||||||
|
swp-invoice-status.pending<br>swp-invoice-status.overdue
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-detail">
|
||||||
|
<h3>drawers.css</h3>
|
||||||
|
<p>
|
||||||
|
<span class="badge badge-success">
|
||||||
|
1 unused rules
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 10px; color: #666;">
|
||||||
|
Original: 6485 | After purge: 6467
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Show unused selectors</summary>
|
||||||
|
<div class="rejected-list">
|
||||||
|
[data-drawer="xl"]
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-detail">
|
||||||
|
<h3>cash.css</h3>
|
||||||
|
<p>
|
||||||
|
<span class="badge badge-success">
|
||||||
|
1 unused rules
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 10px; color: #666;">
|
||||||
|
Original: 19816 | After purge: 19778
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Show unused selectors</summary>
|
||||||
|
<div class="rejected-list">
|
||||||
|
swp-cash-stat.user swp-cash-stat-value
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-detail">
|
||||||
|
<h3>waitlist.css</h3>
|
||||||
|
<p>
|
||||||
|
<span class="badge badge-success">
|
||||||
|
0 unused rules
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 10px; color: #666;">
|
||||||
|
Original: 5686 | After purge: 5686
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p style="color: #2e7d32; margin-top: 10px;">✅ No unused CSS found!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-detail">
|
||||||
|
<h3>topbar.css</h3>
|
||||||
|
<p>
|
||||||
|
<span class="badge badge-success">
|
||||||
|
0 unused rules
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 10px; color: #666;">
|
||||||
|
Original: 3885 | After purge: 3885
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p style="color: #2e7d32; margin-top: 10px;">✅ No unused CSS found!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-detail">
|
||||||
|
<h3>tabs.css</h3>
|
||||||
|
<p>
|
||||||
|
<span class="badge badge-success">
|
||||||
|
0 unused rules
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 10px; color: #666;">
|
||||||
|
Original: 2183 | After purge: 2183
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p style="color: #2e7d32; margin-top: 10px;">✅ No unused CSS found!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-detail">
|
||||||
|
<h3>sidebar.css</h3>
|
||||||
|
<p>
|
||||||
|
<span class="badge badge-success">
|
||||||
|
0 unused rules
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 10px; color: #666;">
|
||||||
|
Original: 5859 | After purge: 5859
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p style="color: #2e7d32; margin-top: 10px;">✅ No unused CSS found!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-detail">
|
||||||
|
<h3>quick-stats.css</h3>
|
||||||
|
<p>
|
||||||
|
<span class="badge badge-success">
|
||||||
|
0 unused rules
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 10px; color: #666;">
|
||||||
|
Original: 899 | After purge: 899
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p style="color: #2e7d32; margin-top: 10px;">✅ No unused CSS found!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-detail">
|
||||||
|
<h3>page.css</h3>
|
||||||
|
<p>
|
||||||
|
<span class="badge badge-success">
|
||||||
|
0 unused rules
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 10px; color: #666;">
|
||||||
|
Original: 6140 | After purge: 6140
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p style="color: #2e7d32; margin-top: 10px;">✅ No unused CSS found!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-detail">
|
||||||
|
<h3>notifications.css</h3>
|
||||||
|
<p>
|
||||||
|
<span class="badge badge-success">
|
||||||
|
0 unused rules
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 10px; color: #666;">
|
||||||
|
Original: 1712 | After purge: 1712
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p style="color: #2e7d32; margin-top: 10px;">✅ No unused CSS found!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-detail">
|
||||||
|
<h3>employees.css</h3>
|
||||||
|
<p>
|
||||||
|
<span class="badge badge-success">
|
||||||
|
0 unused rules
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 10px; color: #666;">
|
||||||
|
Original: 15923 | After purge: 15923
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p style="color: #2e7d32; margin-top: 10px;">✅ No unused CSS found!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-detail">
|
||||||
|
<h3>demo-banner.css</h3>
|
||||||
|
<p>
|
||||||
|
<span class="badge badge-success">
|
||||||
|
0 unused rules
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 10px; color: #666;">
|
||||||
|
Original: 3014 | After purge: 3014
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p style="color: #2e7d32; margin-top: 10px;">✅ No unused CSS found!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-detail">
|
||||||
|
<h3>controls.css</h3>
|
||||||
|
<p>
|
||||||
|
<span class="badge badge-success">
|
||||||
|
0 unused rules
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 10px; color: #666;">
|
||||||
|
Original: 3397 | After purge: 3397
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p style="color: #2e7d32; margin-top: 10px;">✅ No unused CSS found!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-detail">
|
||||||
|
<h3>app-layout.css</h3>
|
||||||
|
<p>
|
||||||
|
<span class="badge badge-success">
|
||||||
|
0 unused rules
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 10px; color: #666;">
|
||||||
|
Original: 1306 | After purge: 1306
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p style="color: #2e7d32; margin-top: 10px;">✅ No unused CSS found!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>💡 Recommendations</h2>
|
||||||
|
<ul style="line-height: 2;">
|
||||||
|
<li>✅ CSS usage is relatively clean.</li>
|
||||||
|
|
||||||
|
<li>📦 Consider consolidating similar styles to reduce duplication.</li>
|
||||||
|
<li>🎨 Review color usage - ensure all colors use CSS variables from design-tokens.css.</li>
|
||||||
|
<li>📋 Reference COMPONENT-CATALOG.md when adding new components to avoid duplication.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<p class="timestamp">Report generated: 12.1.2026, 21.57.11</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
437
PlanTempus.Application/reports/css-stats.json
Normal file
437
PlanTempus.Application/reports/css-stats.json
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
209
PlanTempus.Application/reports/purgecss-report.json
Normal file
209
PlanTempus.Application/reports/purgecss-report.json
Normal file
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
305
PlanTempus.Application/wwwroot/css/COMPONENT-CATALOG.md
Normal file
305
PlanTempus.Application/wwwroot/css/COMPONENT-CATALOG.md
Normal file
|
|
@ -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-container>...</swp-page-container>` |
|
||||||
|
| `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 | `<swp-card-title><i>...</i> Text</swp-card-title>` |
|
||||||
|
| `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
|
||||||
|
<swp-stat-card class="[variant]">
|
||||||
|
<swp-stat-value>42</swp-stat-value>
|
||||||
|
<swp-stat-label>Aktive brugere</swp-stat-label>
|
||||||
|
</swp-stat-card>
|
||||||
|
```
|
||||||
|
|
||||||
|
**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
|
||||||
|
<swp-tab-bar>
|
||||||
|
<swp-tab class="active" data-tab="users">
|
||||||
|
<i class="ph ph-users"></i>
|
||||||
|
<span>Brugere</span>
|
||||||
|
</swp-tab>
|
||||||
|
<swp-tab data-tab="roles">
|
||||||
|
<i class="ph ph-shield-check"></i>
|
||||||
|
<span>Roller</span>
|
||||||
|
</swp-tab>
|
||||||
|
</swp-tab-bar>
|
||||||
|
|
||||||
|
<swp-tab-content data-tab="users" class="active">
|
||||||
|
<!-- Content -->
|
||||||
|
</swp-tab-content>
|
||||||
|
|
||||||
|
<swp-tab-content data-tab="roles">
|
||||||
|
<!-- Content -->
|
||||||
|
</swp-tab-content>
|
||||||
|
```
|
||||||
|
|
||||||
|
**VIGTIGT:**
|
||||||
|
- Aktiv tab: `class="active"` (IKKE data-active="true")
|
||||||
|
- Tab content: `class="active"` for at vise
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Buttons (cash.css)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<swp-btn class="primary">
|
||||||
|
<i class="ph ph-plus"></i>
|
||||||
|
Tilføj
|
||||||
|
</swp-btn>
|
||||||
|
```
|
||||||
|
|
||||||
|
**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
|
||||||
|
<swp-status-badge class="[variant]">Tekst</swp-status-badge>
|
||||||
|
```
|
||||||
|
|
||||||
|
**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
|
||||||
|
<swp-[feature]-table>
|
||||||
|
<swp-[feature]-table-header>
|
||||||
|
<swp-[feature]-cell>Kolonne 1</swp-[feature]-cell>
|
||||||
|
<swp-[feature]-cell>Kolonne 2</swp-[feature]-cell>
|
||||||
|
</swp-[feature]-table-header>
|
||||||
|
<swp-[feature]-table-body>
|
||||||
|
<swp-[feature]-row>
|
||||||
|
<swp-[feature]-cell>Data 1</swp-[feature]-cell>
|
||||||
|
<swp-[feature]-cell>Data 2</swp-[feature]-cell>
|
||||||
|
</swp-[feature]-row>
|
||||||
|
</swp-[feature]-table-body>
|
||||||
|
</swp-[feature]-table>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
<swp-table-actions>
|
||||||
|
<swp-icon-btn title="Rediger">
|
||||||
|
<i class="ph ph-pencil"></i>
|
||||||
|
</swp-icon-btn>
|
||||||
|
<swp-icon-btn class="danger" title="Slet">
|
||||||
|
<i class="ph ph-trash"></i>
|
||||||
|
</swp-icon-btn>
|
||||||
|
</swp-table-actions>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Info Pattern (employees.css)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<swp-user-info>
|
||||||
|
<swp-user-avatar class="[color]">MJ</swp-user-avatar>
|
||||||
|
<swp-user-details>
|
||||||
|
<swp-user-name>Maria Jensen</swp-user-name>
|
||||||
|
<swp-user-email>maria@example.com</swp-user-email>
|
||||||
|
</swp-user-details>
|
||||||
|
</swp-user-info>
|
||||||
|
```
|
||||||
|
|
||||||
|
**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 |
|
||||||
|
|
@ -2,36 +2,9 @@
|
||||||
* Cash Register - Page Styling
|
* Cash Register - Page Styling
|
||||||
*
|
*
|
||||||
* Filter bar, stats, table, forms, and difference box
|
* 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
|
FILTER BAR
|
||||||
=========================================== */
|
=========================================== */
|
||||||
|
|
@ -375,16 +348,40 @@ swp-status-badge::before {
|
||||||
background: currentColor;
|
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);
|
background: color-mix(in srgb, var(--color-green) 15%, transparent);
|
||||||
color: var(--color-green);
|
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);
|
background: color-mix(in srgb, var(--color-amber) 15%, transparent);
|
||||||
color: #b45309;
|
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)
|
TWO-COLUMN GRID (Detail View)
|
||||||
=========================================== */
|
=========================================== */
|
||||||
|
|
|
||||||
148
PlanTempus.Application/wwwroot/css/controls.css
Normal file
148
PlanTempus.Application/wwwroot/css/controls.css
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
721
PlanTempus.Application/wwwroot/css/employees.css
Normal file
721
PlanTempus.Application/wwwroot/css/employees.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,42 @@ swp-page-container {
|
||||||
padding: var(--page-padding);
|
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
|
PAGE HEADER
|
||||||
=========================================== */
|
=========================================== */
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -10,6 +10,7 @@ import { ThemeController } from './modules/theme';
|
||||||
import { SearchController } from './modules/search';
|
import { SearchController } from './modules/search';
|
||||||
import { LockScreenController } from './modules/lockscreen';
|
import { LockScreenController } from './modules/lockscreen';
|
||||||
import { CashController } from './modules/cash';
|
import { CashController } from './modules/cash';
|
||||||
|
import { EmployeesController } from './modules/employees';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main application class
|
* Main application class
|
||||||
|
|
@ -21,6 +22,7 @@ export class App {
|
||||||
readonly search: SearchController;
|
readonly search: SearchController;
|
||||||
readonly lockScreen: LockScreenController;
|
readonly lockScreen: LockScreenController;
|
||||||
readonly cash: CashController;
|
readonly cash: CashController;
|
||||||
|
readonly employees: EmployeesController;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Initialize controllers
|
// Initialize controllers
|
||||||
|
|
@ -30,6 +32,7 @@ export class App {
|
||||||
this.search = new SearchController();
|
this.search = new SearchController();
|
||||||
this.lockScreen = new LockScreenController(this.drawers);
|
this.lockScreen = new LockScreenController(this.drawers);
|
||||||
this.cash = new CashController();
|
this.cash = new CashController();
|
||||||
|
this.employees = new EmployeesController();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
191
PlanTempus.Application/wwwroot/ts/modules/employees.ts
Normal file
191
PlanTempus.Application/wwwroot/ts/modules/employees.ts
Normal file
|
|
@ -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<HTMLElement>('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<HTMLElement>('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<HTMLElement>('swp-tab-bar > swp-tab[data-tab]');
|
||||||
|
const contents = container.querySelectorAll<HTMLElement>('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<HTMLElement>('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<HTMLElement>('[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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue