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:
parent
8b2a630861
commit
5fab58ff6f
5 changed files with 152 additions and 1304 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -366,3 +366,5 @@ nul
|
||||||
tmpclaude*
|
tmpclaude*
|
||||||
PlanTempus.Application/tmpclaude*
|
PlanTempus.Application/tmpclaude*
|
||||||
|
|
||||||
|
|
||||||
|
PlanTempus.Application/wwwroot/js/app.js
|
||||||
|
|
|
||||||
|
|
@ -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) |
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue