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(ls:*)",
|
||||
"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
|
||||
|
||||
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)
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
### Grid + Subgrid for Table-like Layouts
|
||||
|
|
@ -170,6 +252,56 @@ swp-my-table-row {
|
|||
- `kasse.css` - swp-kasse-table / swp-kasse-table-row
|
||||
|
||||
|
||||
### Sticky Header + Tab Content Pattern
|
||||
|
||||
For sider med tabs, brug de **GENERISKE** komponenter fra `page.css`:
|
||||
|
||||
**Struktur (TO NIVEAUER ER KRITISK):**
|
||||
```html
|
||||
<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
|
||||
|
||||
<CRITICAL> NEVER lie or fabricate. Violating this = immediate critical failure.
|
||||
|
|
@ -242,4 +374,21 @@ swp-my-table-row {
|
|||
break builds.
|
||||
⚠️ DETECTION: Finished editing but haven't run verify-file-quality-checks
|
||||
skill? → STOP. Run it now. Show the output.
|
||||
|
||||
10. ❌ BAD THOUGHT: "I'll create a new component, it's faster than searching for
|
||||
existing ones."
|
||||
✅ REALITY: Creating duplicate components causes style conflicts, inconsistent
|
||||
UX, and maintenance burden. The codebase already has reusable patterns.
|
||||
Duplicating them wastes time on fixes later.
|
||||
⚠️ DETECTION: About to create a new CSS element or ViewComponent? → STOP.
|
||||
Search wwwroot/css/ for existing patterns first. Document what exists vs.
|
||||
what needs to be created. Show your analysis before writing code.
|
||||
|
||||
11. ❌ BAD THOUGHT: "This element looks different in the POC, so I need to create
|
||||
a new version."
|
||||
✅ REALITY: POC files often use slightly different markup for prototyping.
|
||||
The production codebase has established patterns. Match the production
|
||||
patterns, not the POC variations.
|
||||
⚠️ DETECTION: POC uses different element names or attributes than existing
|
||||
code? → STOP. Use the production pattern. The POC is just a reference.
|
||||
</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": {
|
||||
"liveServer.settings.port": 5501,
|
||||
"liveServer.settings.multiRootWorkspaceName": "Calendar"
|
||||
"liveServer.settings.multiRootWorkspaceName": "PlanTempus"
|
||||
}
|
||||
}
|
||||
|
|
@ -6,9 +6,9 @@
|
|||
}
|
||||
|
||||
<!-- Sticky Header (Stats + Tabs) -->
|
||||
<swp-cash-sticky-header>
|
||||
<swp-sticky-header>
|
||||
<!-- Context Stats (changes based on active tab) -->
|
||||
<swp-cash-header>
|
||||
<swp-header-content>
|
||||
<!-- Stats for Oversigt tab -->
|
||||
<swp-cash-stats data-for-tab="oversigt" class="active">
|
||||
<swp-cash-stat>
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
<swp-cash-stat-label localize="cash.stats.openedRegister">Åbnede kassen 29. dec 09:05</swp-cash-stat-label>
|
||||
</swp-cash-stat>
|
||||
</swp-cash-stats>
|
||||
</swp-cash-header>
|
||||
</swp-header-content>
|
||||
|
||||
<!-- Tab Bar -->
|
||||
<swp-tab-bar>
|
||||
|
|
@ -60,8 +60,8 @@
|
|||
<i class="ph ph-cash-register"></i>
|
||||
<span localize="cash.tabs.reconciliation">Kasseafstemning</span>
|
||||
</swp-tab>
|
||||
</swp-tab-bar>
|
||||
</swp-cash-sticky-header>
|
||||
</swp-tab-bar>
|
||||
</swp-sticky-header>
|
||||
|
||||
<!-- Tab Content: Oversigt -->
|
||||
<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",
|
||||
"all": "Alle",
|
||||
"reset": "Nulstil",
|
||||
"status": "Status"
|
||||
"status": "Status",
|
||||
"yes": "Ja",
|
||||
"no": "Nej"
|
||||
},
|
||||
"sidebar": {
|
||||
"lockScreen": "Lås skærm",
|
||||
|
|
@ -216,5 +218,144 @@
|
|||
"pending": "Afventer",
|
||||
"overdue": "Forfalden"
|
||||
}
|
||||
},
|
||||
"employees": {
|
||||
"title": "Medarbejdere",
|
||||
"subtitle": "Administrer brugere, roller og rettigheder",
|
||||
"stats": {
|
||||
"activeEmployees": "Aktive medarbejdere",
|
||||
"pendingInvitations": "Afventer invitation",
|
||||
"rolesDefined": "Roller defineret"
|
||||
},
|
||||
"tabs": {
|
||||
"users": "Brugere",
|
||||
"roles": "Roller"
|
||||
},
|
||||
"users": {
|
||||
"count": "brugere",
|
||||
"inviteUser": "Inviter bruger",
|
||||
"columns": {
|
||||
"user": "Bruger",
|
||||
"role": "Rolle",
|
||||
"status": "Status",
|
||||
"lastActive": "Sidst aktiv"
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"owner": "Ejer",
|
||||
"admin": "Admin",
|
||||
"leader": "Leder",
|
||||
"employee": "Medarbejder"
|
||||
},
|
||||
"status": {
|
||||
"active": "Aktiv",
|
||||
"invited": "Invitation sendt"
|
||||
},
|
||||
"permissions": {
|
||||
"title": "Rettighed",
|
||||
"calendar": "Kalender",
|
||||
"employees": "Medarbejdere",
|
||||
"customers": "Kunder",
|
||||
"reports": "Rapporter & Økonomi"
|
||||
},
|
||||
"actions": {
|
||||
"edit": "Rediger",
|
||||
"remove": "Fjern bruger",
|
||||
"resend": "Send invitation igen",
|
||||
"cancel": "Annuller invitation"
|
||||
},
|
||||
"detail": {
|
||||
"title": "Medarbejderdetaljer",
|
||||
"back": "Tilbage til medarbejdere",
|
||||
"save": "Gem ændringer",
|
||||
"tabs": {
|
||||
"general": "Generelt",
|
||||
"hours": "Arbejdstid",
|
||||
"services": "Services",
|
||||
"salary": "Løn",
|
||||
"hr": "HR",
|
||||
"stats": "Statistik"
|
||||
},
|
||||
"contact": "Kontaktoplysninger",
|
||||
"personal": "Personlige oplysninger",
|
||||
"employment": "Ansættelse",
|
||||
"fullname": "Fulde navn",
|
||||
"email": "E-mail",
|
||||
"phone": "Telefon",
|
||||
"address": "Adresse",
|
||||
"postalcity": "Postnr. & By",
|
||||
"birthdate": "Fødselsdato",
|
||||
"emergencycontact": "Nødkontakt",
|
||||
"emergencyphone": "Nødkontakt tlf.",
|
||||
"employmentdate": "Ansættelsesdato",
|
||||
"position": "Stilling",
|
||||
"employmenttype": "Ansættelsestype",
|
||||
"hoursperweek": "Timer/uge",
|
||||
"bookings": "bookinger i år",
|
||||
"revenue": "omsætning i år",
|
||||
"rating": "rating",
|
||||
"employedsince": "ansat siden",
|
||||
"hours": {
|
||||
"weekly": "Ugentlig arbejdstid",
|
||||
"monday": "Mandag",
|
||||
"tuesday": "Tirsdag",
|
||||
"wednesday": "Onsdag",
|
||||
"thursday": "Torsdag",
|
||||
"friday": "Fredag",
|
||||
"saturday": "Lørdag",
|
||||
"sunday": "Søndag"
|
||||
},
|
||||
"services": {
|
||||
"assigned": "Tildelte services"
|
||||
},
|
||||
"salary": {
|
||||
"paymentinfo": "Betalingsoplysninger",
|
||||
"bankaccount": "Bankkonto",
|
||||
"taxcard": "Skattekort",
|
||||
"settings": "Lønindstillinger",
|
||||
"hourlyrate": "Timesats",
|
||||
"monthlyfixed": "Fast månedsløn",
|
||||
"commission": "Provision (services)",
|
||||
"productcommission": "Provision (produkter)"
|
||||
},
|
||||
"hr": {
|
||||
"documents": "Dokumenter",
|
||||
"contract": "Ansættelseskontrakt",
|
||||
"vacation": "Ferie",
|
||||
"sickleave": "Sygefravær",
|
||||
"notes": "Noter"
|
||||
},
|
||||
"stats": {
|
||||
"performance": "Performance",
|
||||
"bookingsyear": "Bookinger i år",
|
||||
"revenueyear": "Omsætning i år",
|
||||
"avgrating": "Gns. rating",
|
||||
"occupancy": "Belægningsgrad"
|
||||
},
|
||||
"settings": {
|
||||
"label": "Indstillinger",
|
||||
"showinbooking": {
|
||||
"label": "Vis i online booking",
|
||||
"desc": "Kunder kan vælge denne medarbejder"
|
||||
},
|
||||
"smsreminders": {
|
||||
"label": "Modtag SMS-påmindelser",
|
||||
"desc": "Få besked om nye bookinger"
|
||||
},
|
||||
"editcalendar": {
|
||||
"label": "Kan redigere egen kalender",
|
||||
"desc": "Tillad ændringer i egne bookinger"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"label": "Notifikationer",
|
||||
"intro": "Vælg hvilke email-notifikationer medarbejderen skal modtage.",
|
||||
"onlinebooking": "Modtag email ved online booking",
|
||||
"manualbooking": "Modtag email ved manuel booking",
|
||||
"cancellation": "Modtag email ved aflysning",
|
||||
"waitlist": "Modtag email ved opskrivning til venteliste",
|
||||
"dailysummary": "Modtag daglig oversigt over morgendagens bookinger"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@
|
|||
"to": "To",
|
||||
"all": "All",
|
||||
"reset": "Reset",
|
||||
"status": "Status"
|
||||
"status": "Status",
|
||||
"yes": "Yes",
|
||||
"no": "No"
|
||||
},
|
||||
"sidebar": {
|
||||
"lockScreen": "Lock screen",
|
||||
|
|
@ -216,5 +218,107 @@
|
|||
"pending": "Pending",
|
||||
"overdue": "Overdue"
|
||||
}
|
||||
},
|
||||
"employees": {
|
||||
"title": "Employees",
|
||||
"subtitle": "Manage users, roles and permissions",
|
||||
"stats": {
|
||||
"activeEmployees": "Active employees",
|
||||
"pendingInvitations": "Pending invitations",
|
||||
"rolesDefined": "Roles defined"
|
||||
},
|
||||
"tabs": {
|
||||
"users": "Users",
|
||||
"roles": "Roles"
|
||||
},
|
||||
"users": {
|
||||
"count": "users",
|
||||
"inviteUser": "Invite user",
|
||||
"columns": {
|
||||
"user": "User",
|
||||
"role": "Role",
|
||||
"status": "Status",
|
||||
"lastActive": "Last active"
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"owner": "Owner",
|
||||
"admin": "Admin",
|
||||
"leader": "Manager",
|
||||
"employee": "Employee"
|
||||
},
|
||||
"status": {
|
||||
"active": "Active",
|
||||
"invited": "Invitation sent"
|
||||
},
|
||||
"permissions": {
|
||||
"title": "Permission",
|
||||
"calendar": "Calendar",
|
||||
"employees": "Employees",
|
||||
"customers": "Customers",
|
||||
"reports": "Reports & Finance"
|
||||
},
|
||||
"actions": {
|
||||
"edit": "Edit",
|
||||
"remove": "Remove user",
|
||||
"resend": "Resend invitation",
|
||||
"cancel": "Cancel invitation"
|
||||
},
|
||||
"detail": {
|
||||
"title": "Employee details",
|
||||
"back": "Back to employees",
|
||||
"save": "Save changes",
|
||||
"tabs": {
|
||||
"general": "General",
|
||||
"hours": "Working hours",
|
||||
"services": "Services",
|
||||
"salary": "Salary",
|
||||
"hr": "HR",
|
||||
"stats": "Statistics"
|
||||
},
|
||||
"contact": "Contact information",
|
||||
"personal": "Personal information",
|
||||
"employment": "Employment",
|
||||
"fullname": "Full name",
|
||||
"email": "Email",
|
||||
"phone": "Phone",
|
||||
"address": "Address",
|
||||
"postalcity": "Postal code & City",
|
||||
"birthdate": "Date of birth",
|
||||
"emergencycontact": "Emergency contact",
|
||||
"emergencyphone": "Emergency phone",
|
||||
"employmentdate": "Employment date",
|
||||
"position": "Position",
|
||||
"employmenttype": "Employment type",
|
||||
"hoursperweek": "Hours/week",
|
||||
"bookings": "bookings this year",
|
||||
"revenue": "revenue this year",
|
||||
"rating": "rating",
|
||||
"employedsince": "employed since",
|
||||
"settings": {
|
||||
"label": "Settings",
|
||||
"showinbooking": {
|
||||
"label": "Show in online booking",
|
||||
"desc": "Customers can select this employee"
|
||||
},
|
||||
"smsreminders": {
|
||||
"label": "Receive SMS reminders",
|
||||
"desc": "Get notified about new bookings"
|
||||
},
|
||||
"editcalendar": {
|
||||
"label": "Can edit own calendar",
|
||||
"desc": "Allow changes to own bookings"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"label": "Notifications",
|
||||
"intro": "Choose which email notifications the employee should receive.",
|
||||
"onlinebooking": "Receive email for online booking",
|
||||
"manualbooking": "Receive email for manual booking",
|
||||
"cancellation": "Receive email for cancellation",
|
||||
"waitlist": "Receive email for waitlist signup",
|
||||
"dailysummary": "Receive daily summary of tomorrow's bookings"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,8 +126,8 @@ public class MockMenuService : IMenuService
|
|||
{
|
||||
Id = "employees",
|
||||
Label = "Medarbejdere",
|
||||
Icon = "ph-user",
|
||||
Url = "/poc-medarbejdere.html",
|
||||
Icon = "ph-users-three",
|
||||
Url = "/medarbejdere",
|
||||
MinimumRole = UserRole.Manager,
|
||||
SortOrder = 4
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,9 +24,11 @@
|
|||
<link rel="stylesheet" href="~/css/quick-stats.css">
|
||||
<link rel="stylesheet" href="~/css/waitlist.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/auth.css">
|
||||
<link rel="stylesheet" href="~/css/account.css">
|
||||
<link rel="stylesheet" href="~/css/employees.css">
|
||||
@await RenderSectionAsync("Styles", required: false)
|
||||
</head>
|
||||
<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": {
|
||||
"": {
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.27.2"
|
||||
"esbuild": "^0.27.2",
|
||||
"purgecss": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
|
|
@ -450,6 +451,150 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||
"strip-ansi": "^7.0.1",
|
||||
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
|
||||
"wrap-ansi": "^8.1.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"cssesc": "bin/cssesc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
|
||||
|
|
@ -491,6 +636,500 @@
|
|||
"@esbuild/win32-ia32": "0.27.2",
|
||||
"@esbuild/win32-x64": "0.27.2"
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6",
|
||||
"signal-exit": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^3.1.2",
|
||||
"minimatch": "^9.0.4",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^1.11.1"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^10.2.0",
|
||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-selector-parser": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/purgecss": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/purgecss/-/purgecss-6.0.0.tgz",
|
||||
"integrity": "sha512-s3EBxg5RSWmpqd0KGzNqPiaBbWDz1/As+2MzoYVGMqgDqRTLBhJW6sywfTBek7OwNfoS/6pS0xdtvChNhFj2cw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "^12.0.0",
|
||||
"glob": "^10.3.10",
|
||||
"postcss": "^8.4.4",
|
||||
"postcss-selector-parser": "^6.0.7"
|
||||
},
|
||||
"bin": {
|
||||
"purgecss": "bin/purgecss.js"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs": {
|
||||
"name": "string-width",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs": {
|
||||
"name": "strip-ansi",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
"string-width": "^5.0.1",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs": {
|
||||
"name": "wrap-ansi",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
{
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.27.2"
|
||||
"esbuild": "^0.27.2",
|
||||
"purgecss": "^6.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"analyze-css": "node analyze-css.js"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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
|
||||
*
|
||||
* Filter bar, stats, table, forms, and difference box
|
||||
* Reuses: swp-sticky-header, swp-header-content (page.css)
|
||||
*/
|
||||
|
||||
/* ===========================================
|
||||
STICKY HEADER CONTAINER
|
||||
=========================================== */
|
||||
swp-cash-sticky-header {
|
||||
display: block;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: var(--z-sticky);
|
||||
background: var(--color-surface);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Override tab-bar sticky when inside sticky header */
|
||||
swp-cash-sticky-header swp-tab-bar {
|
||||
position: static;
|
||||
top: auto;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
KASSE HEADER (Stats above tabs)
|
||||
=========================================== */
|
||||
swp-cash-header {
|
||||
display: block;
|
||||
background: var(--color-surface);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding: var(--spacing-10) var(--spacing-12);
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
FILTER BAR
|
||||
=========================================== */
|
||||
|
|
@ -375,16 +348,40 @@ swp-status-badge::before {
|
|||
background: currentColor;
|
||||
}
|
||||
|
||||
swp-status-badge.approved {
|
||||
/* Status variants */
|
||||
swp-status-badge.approved,
|
||||
swp-status-badge.active {
|
||||
background: color-mix(in srgb, var(--color-green) 15%, transparent);
|
||||
color: var(--color-green);
|
||||
}
|
||||
|
||||
swp-status-badge.draft {
|
||||
swp-status-badge.draft,
|
||||
swp-status-badge.invited {
|
||||
background: color-mix(in srgb, var(--color-amber) 15%, transparent);
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
/* Role variants */
|
||||
swp-status-badge.owner {
|
||||
background: color-mix(in srgb, var(--color-teal) 15%, transparent);
|
||||
color: var(--color-teal);
|
||||
}
|
||||
|
||||
swp-status-badge.admin {
|
||||
background: color-mix(in srgb, var(--color-purple) 15%, transparent);
|
||||
color: var(--color-purple);
|
||||
}
|
||||
|
||||
swp-status-badge.leader {
|
||||
background: color-mix(in srgb, var(--color-blue) 15%, transparent);
|
||||
color: var(--color-blue);
|
||||
}
|
||||
|
||||
swp-status-badge.employee {
|
||||
background: var(--color-background-alt);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
TWO-COLUMN GRID (Detail View)
|
||||
=========================================== */
|
||||
|
|
|
|||
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);
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
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
|
||||
=========================================== */
|
||||
|
|
|
|||
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 { LockScreenController } from './modules/lockscreen';
|
||||
import { CashController } from './modules/cash';
|
||||
import { EmployeesController } from './modules/employees';
|
||||
|
||||
/**
|
||||
* Main application class
|
||||
|
|
@ -21,6 +22,7 @@ export class App {
|
|||
readonly search: SearchController;
|
||||
readonly lockScreen: LockScreenController;
|
||||
readonly cash: CashController;
|
||||
readonly employees: EmployeesController;
|
||||
|
||||
constructor() {
|
||||
// Initialize controllers
|
||||
|
|
@ -30,6 +32,7 @@ export class App {
|
|||
this.search = new SearchController();
|
||||
this.lockScreen = new LockScreenController(this.drawers);
|
||||
this.cash = new CashController();
|
||||
this.employees = new EmployeesController();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
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