Adds double-click to edit support for rates

Enables quick editing of salary rates by double-clicking card inputs

Introduces functionality to:
- Open rates drawer on double-click
- Automatically focus and select corresponding input
- Add temporary highlight to edited row
This commit is contained in:
Janus C. H. Knudsen 2026-01-14 18:34:05 +01:00
parent 8b2a630861
commit 5fab58ff6f
5 changed files with 152 additions and 1304 deletions

2
.gitignore vendored
View file

@ -366,3 +366,5 @@ nul
tmpclaude* tmpclaude*
PlanTempus.Application/tmpclaude* PlanTempus.Application/tmpclaude*
PlanTempus.Application/wwwroot/js/app.js

View file

@ -8,45 +8,40 @@ Reference for alle genbrugelige komponenter. **LAV ALDRIG EN NY KOMPONENT HVIS D
**VIGTIGT:** Disse base patterns skal ALTID bruges som foundation for nye features. **VIGTIGT:** Disse base patterns skal ALTID bruges som foundation for nye features.
### Grid + Subgrid Table Pattern ### Data Table Pattern (ANBEFALET)
Alle tabeller skal bruge dette pattern: Alle nye tabeller skal bruge `swp-data-table` fra components.css:
```html ```html
<swp-feature-table> <swp-card class="feature-context">
<swp-feature-table-header> <!-- extends swp-table-header-base --> <swp-data-table>
<swp-feature-cell>Kolonne 1</swp-feature-cell> <swp-data-table-header>
</swp-feature-table-header> <swp-data-table-cell>Kolonne 1</swp-data-table-cell>
<swp-feature-table-body> <!-- extends swp-table-body-base --> <swp-data-table-cell>Kolonne 2</swp-data-table-cell>
<swp-feature-row> <!-- extends swp-table-row-base --> </swp-data-table-header>
<swp-feature-cell>Data</swp-feature-cell> <swp-data-table-row>
</swp-feature-row> <swp-data-table-cell>Data 1</swp-data-table-cell>
</swp-feature-table-body> <swp-data-table-cell>Data 2</swp-data-table-cell>
</swp-feature-table> </swp-data-table-row>
</swp-data-table>
</swp-card>
``` ```
**CSS Pattern:** **CSS Pattern:**
```css ```css
swp-feature-table { /* I feature CSS - definer kun kolonner via context class */
display: grid; swp-card.feature-context swp-data-table {
grid-template-columns: /* feature-specific */; grid-template-columns: 1fr 120px 80px;
} }
swp-feature-table-header, /* Kolonne-specifik styling med nth-child */
swp-feature-table-body { swp-card.feature-context swp-data-table-cell:nth-child(2) {
display: grid; font-family: var(--font-mono);
grid-column: 1 / -1;
grid-template-columns: subgrid;
}
swp-feature-row {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
align-items: center;
} }
``` ```
**VIGTIGT:** Context class skal være på `swp-card`, IKKE en wrapper div!
### List Item Pattern ### List Item Pattern
Alle lister (notifikationer, bookinger, attentions) bruger: Alle lister (notifikationer, bookinger, attentions) bruger:
@ -211,57 +206,56 @@ Automatisk dot via `::before` pseudo-element.
--- ---
## Tables - Grid + Subgrid Pattern ## Tables - swp-data-table Pattern
### Struktur (ALTID følg dette mønster) **BRUG ALTID `swp-data-table`** for nye tabeller. Den generiske komponent er defineret i components.css.
### Struktur
```html ```html
<swp-[feature]-table> <swp-card class="context-class">
<swp-[feature]-table-header> <swp-data-table>
<swp-[feature]-cell>Kolonne 1</swp-[feature]-cell> <swp-data-table-header>
<swp-[feature]-cell>Kolonne 2</swp-[feature]-cell> <swp-data-table-cell>Kolonne 1</swp-data-table-cell>
</swp-[feature]-table-header> <swp-data-table-cell>Kolonne 2</swp-data-table-cell>
<swp-[feature]-table-body> </swp-data-table-header>
<swp-[feature]-row> <swp-data-table-row>
<swp-[feature]-cell>Data 1</swp-[feature]-cell> <swp-data-table-cell>Data 1</swp-data-table-cell>
<swp-[feature]-cell>Data 2</swp-[feature]-cell> <swp-data-table-cell>Data 2</swp-data-table-cell>
</swp-[feature]-row> </swp-data-table-row>
</swp-[feature]-table-body> </swp-data-table>
</swp-[feature]-table> </swp-card>
``` ```
### CSS Pattern ### CSS Pattern
```css ```css
swp-[feature]-table { /* Definer kolonner via context class på swp-card */
display: grid; swp-card.context-class swp-data-table {
grid-template-columns: /* definer kolonner her */; grid-template-columns: 1fr 120px 80px;
} }
swp-[feature]-table-header, /* Kolonne-specifik styling med nth-child */
swp-[feature]-table-body { swp-card.context-class swp-data-table-cell:nth-child(2) {
display: grid; font-family: var(--font-mono);
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 ### Eksisterende tabeller
| Feature | Container | Row | CSS fil | | Feature | Card Class | CSS fil |
|---------|-----------|-----|---------| |---------|------------|---------|
| Cash | `swp-cash-table` | `swp-cash-table-row` | cash.css | | Employees list | `swp-card.employees-list` | employees.css |
| Employees | `swp-employee-table` | `swp-employee-row` | employees.css | | Salary history | `swp-card.salary-history` | employees.css |
| Salary | `swp-salary-table` | `swp-salary-table-row` | employees.css | | Invoice history | `swp-card.invoice-history` | account.css |
| **Data (generisk)** | `swp-data-table` | `swp-data-table-row` | components.css | | Stats bookings | `swp-card.stats-bookings` | employees.css |
| Bookings (dashboard) | `swp-booking-list` | `swp-booking-item` | bookings.css | | Cash | `swp-cash-table` (kompleks) | cash.css |
**Lister (ikke tabeller):**
| Feature | Container | Item | CSS fil |
|---------|-----------|------|---------|
| Bookings | `swp-booking-list` | `swp-booking-item` | bookings.css |
| Notifications | `swp-notification-list` | `swp-notification-item` | notifications.css | | Notifications | `swp-notification-list` | `swp-notification-item` | notifications.css |
| Attentions | `swp-attention-list` | `swp-attention-item` | attentions.css | | Attentions | `swp-attention-list` | `swp-attention-item` | attentions.css |
@ -269,22 +263,28 @@ swp-[feature]-row {
## Table Cells - Standard Styling ## Table Cells - Standard Styling
Base styling er i components.css. Tilpas kun via context class:
```css ```css
/* Header cells */ /* Header cells (automatisk fra components.css) */
swp-[feature]-table-header swp-[feature]-cell { swp-data-table-header swp-data-table-cell {
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-semibold);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
/* Body cells */ /* Body cells (automatisk fra components.css) */
swp-[feature]-cell { swp-data-table-cell {
padding: var(--spacing-5); padding: var(--spacing-4);
font-size: var(--font-size-base); /* ALTID base, ikke sm */ font-size: var(--font-size-base);
color: var(--color-text); color: var(--color-text);
} }
/* Feature-specifik tilpasning via context */
swp-card.my-feature swp-data-table-cell:nth-child(3) {
text-align: right;
}
``` ```
--- ---
@ -428,73 +428,29 @@ Simpel liste med tekst + badge (f.eks. planlagt fravær).
--- ---
## Salary Table (employees.css) ## Salary History Table (employees.css)
Bruger Grid + Subgrid mønsteret. Bruger `swp-data-table` med `.salary-history` context class.
```html ```html
<swp-salary-table> <swp-card class="salary-history">
<swp-salary-table-header> <swp-section-label>Lønhistorik</swp-section-label>
<swp-salary-table-cell>Periode</swp-salary-table-cell>
<swp-salary-table-cell>Bruttoløn</swp-salary-table-cell>
<swp-salary-table-cell></swp-salary-table-cell>
</swp-salary-table-header>
<swp-salary-table-body>
<swp-salary-table-row>
<swp-salary-table-cell>Januar 2026</swp-salary-table-cell>
<swp-salary-table-cell class="mono">34.063,50 kr</swp-salary-table-cell>
<swp-salary-table-cell><i class="ph ph-caret-right"></i></swp-salary-table-cell>
</swp-salary-table-row>
</swp-salary-table-body>
</swp-salary-table>
```
Rækker har hover-effekt og chevron bliver teal ved hover.
---
## Data Table - Generisk (components.css)
Generisk tabel med Grid + Subgrid.
**Struktur:**
- `swp-data-table` = grid (kolonner i feature CSS)
- `swp-data-table-header` = subgrid (celler direkte i)
- `swp-data-table-row` = subgrid
- `swp-data-table-cell` = celler
**Brug:** Wrap i container med klasse der definerer kolonner.
```css
/* I feature CSS (f.eks. employees.css) */
.stats-bookings swp-data-table {
grid-template-columns: 90px 60px 1fr 1fr 80px 100px 100px;
}
/* Kolonne-specifik styling med nth-child */
.stats-bookings swp-data-table-row swp-data-table-cell:nth-child(1),
.stats-bookings swp-data-table-row swp-data-table-cell:nth-child(2) {
font-family: var(--font-mono);
color: var(--color-text-secondary);
}
```
```html
<swp-card class="stats-bookings">
<swp-data-table> <swp-data-table>
<swp-data-table-header> <swp-data-table-header>
<swp-data-table-cell>Kolonne 1</swp-data-table-cell> <swp-data-table-cell>Periode</swp-data-table-cell>
<swp-data-table-cell>Kolonne 2</swp-data-table-cell> <swp-data-table-cell>Bruttoløn</swp-data-table-cell>
<swp-data-table-cell></swp-data-table-cell>
</swp-data-table-header> </swp-data-table-header>
<swp-data-table-row> <swp-data-table-row>
<swp-data-table-cell>Data 1</swp-data-table-cell> <swp-data-table-cell>Januar 2026</swp-data-table-cell>
<swp-data-table-cell>Data 2</swp-data-table-cell> <swp-data-table-cell class="mono">34.063,50 kr</swp-data-table-cell>
<swp-data-table-cell><i class="ph ph-caret-right"></i></swp-data-table-cell>
</swp-data-table-row> </swp-data-table-row>
</swp-data-table> </swp-data-table>
</swp-card> </swp-card>
``` ```
**Kolonne-styling:** Brug `nth-child()` i feature CSS frem for klasser på celler. Rækker har hover-effekt og chevron bliver teal ved hover.
--- ---
@ -579,11 +535,13 @@ Dashed border knap til tilføjelse af elementer.
|-----|---------| |-----|---------|
| `design-tokens.css` | Farver, spacing, fonts, shadows | | `design-tokens.css` | Farver, spacing, fonts, shadows |
| `design-system.css` | Base resets, typography | | `design-system.css` | Base resets, typography |
| `page.css` | Page structure | | `page.css` | Page structure, sticky-header |
| `components.css` | Buttons, badges, cards, section-label, add-button, avatars, icon-btn, data-table | | `components.css` | Buttons, badges, cards, section-label, add-button, avatars, icon-btn, **swp-data-table** |
| `stats.css` | Stat cards, stat rows | | `stats.css` | Stat cards, stat rows |
| `tabs.css` | Tab bar, tab content | | `tabs.css` | Tab bar, tab content |
| `employees.css` | Employee table, user info, edit forms, document lists, salary table | | `employees.css` | User info, edit forms, document lists, context styles (.employees-list, .salary-history, .stats-bookings) |
| `account.css` | Account/billing styles, context styles (.invoice-history) |
| `bookings.css` | Booking list items | | `bookings.css` | Booking list items |
| `notifications.css` | Notification items | | `notifications.css` | Notification items |
| `attentions.css` | Attention items | | `attentions.css` | Attention items |
| `cash.css` | Cash register (swp-cash-table - kompleks, ikke migreret) |

View file

@ -814,6 +814,23 @@ swp-simple-item {
} }
} }
/* ===========================================
FOCUS HIGHLIGHT (double-click to edit)
=========================================== */
@keyframes focus-blink {
0%, 100% { background: transparent; }
25%, 75% { background: var(--bg-teal-medium); }
}
swp-data-row.focus-highlight {
animation: focus-blink 1s ease-out;
& input {
outline: 2px solid var(--color-teal);
outline-offset: 2px;
}
}
/* =========================================== /* ===========================================
STATS BOOKINGS TABLE STATS BOOKINGS TABLE
=========================================== */ =========================================== */

File diff suppressed because one or more lines are too long

View file

@ -211,6 +211,7 @@ class RatesSyncController {
this.setupCheckboxListeners(); this.setupCheckboxListeners();
this.setupInputListeners(); this.setupInputListeners();
this.setupDoubleClickToEdit();
} }
/** /**
@ -319,4 +320,50 @@ class RatesSyncController {
const formattedValue = this.formatNumber(value); const formattedValue = this.formatNumber(value);
cardInput.value = `${formattedValue} ${unit}`; cardInput.value = `${formattedValue} ${unit}`;
} }
/**
* Setup double-click on salary card inputs to open drawer and focus field
*/
private setupDoubleClickToEdit(): void {
document.addEventListener('dblclick', (e: Event) => {
const target = e.target as HTMLElement;
const input = target.closest<HTMLInputElement>('input[id^="value-"]');
if (!input || !input.id) return;
// Extract key from value-{key}
const match = input.id.match(/^value-(.+)$/);
if (!match) return;
const rateKey = match[1];
this.openDrawerAndFocus(rateKey);
});
}
/**
* Open drawer and focus the corresponding field with highlight
*/
private openDrawerAndFocus(rateKey: string): void {
// Open the drawer
const trigger = document.querySelector<HTMLElement>('[data-drawer-trigger="rates-drawer"]');
trigger?.click();
// Wait for drawer to open, then focus field
requestAnimationFrame(() => {
const drawerInput = document.getElementById(`rate-${rateKey}`) as HTMLInputElement | null;
if (!drawerInput) return;
// Focus the input
drawerInput.focus();
drawerInput.select();
// Add highlight to row
const row = drawerInput.closest<HTMLElement>('swp-data-row');
if (row) {
row.classList.add('focus-highlight');
// Remove class after animation
setTimeout(() => row.classList.remove('focus-highlight'), 1000);
}
});
}
} }