Preisrechner: Rappenrundung im Backend und Rabatt-Rechner im Web

agentd task ad07191c406d4ea496b0b18db826651e
This commit is contained in:
agentd
2026-08-05 00:32:00 +00:00
parent 605a952f7a
commit 724ee92a2b
6 changed files with 236 additions and 2 deletions

View File

@@ -233,6 +233,7 @@
</defs>
</svg>
<h1>Hello, {{ title() }}</h1>
<app-discount-calculator />
<p>Congratulations! Your app is running. 🎉</p>
</div>
<div class="divider" role="separator" aria-label="Divider"></div>

View File

@@ -1,9 +1,10 @@
import { Component, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { DiscountCalculator } from './discount-calculator/discount-calculator';
@Component({
selector: 'app-root',
imports: [RouterOutlet],
imports: [RouterOutlet, DiscountCalculator],
templateUrl: './app.html',
styleUrl: './app.scss'
})

View File

@@ -0,0 +1,74 @@
import { TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { DiscountCalculator } from './discount-calculator';
describe('DiscountCalculator', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DiscountCalculator, FormsModule],
}).compileComponents();
});
it('should create the component', () => {
const fixture = TestBed.createComponent(DiscountCalculator);
const component = fixture.componentInstance;
expect(component).toBeTruthy();
});
it('should display "CHF 89.90" for price=99.90 and discount=10', async () => {
const fixture = TestBed.createComponent(DiscountCalculator);
fixture.detectChanges();
const priceInput = fixture.nativeElement.querySelector('#price') as HTMLInputElement;
const discountInput = fixture.nativeElement.querySelector('#discount') as HTMLInputElement;
priceInput.value = '99.90';
priceInput.dispatchEvent(new Event('input'));
discountInput.value = '10';
discountInput.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
const resultEl = fixture.nativeElement.querySelector('.result-value') as HTMLElement;
expect(resultEl.textContent?.trim()).toBe('CHF 89.90');
});
it('should display "CHF 100.00" for price=100 and discount=0', async () => {
const fixture = TestBed.createComponent(DiscountCalculator);
fixture.detectChanges();
const priceInput = fixture.nativeElement.querySelector('#price') as HTMLInputElement;
const discountInput = fixture.nativeElement.querySelector('#discount') as HTMLInputElement;
priceInput.value = '100';
priceInput.dispatchEvent(new Event('input'));
discountInput.value = '0';
discountInput.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
const resultEl = fixture.nativeElement.querySelector('.result-value') as HTMLElement;
expect(resultEl.textContent?.trim()).toBe('CHF 100.00');
});
it('should display "CHF 150.00" for price=200 and discount=25', async () => {
const fixture = TestBed.createComponent(DiscountCalculator);
fixture.detectChanges();
const priceInput = fixture.nativeElement.querySelector('#price') as HTMLInputElement;
const discountInput = fixture.nativeElement.querySelector('#discount') as HTMLInputElement;
priceInput.value = '200';
priceInput.dispatchEvent(new Event('input'));
discountInput.value = '25';
discountInput.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
const resultEl = fixture.nativeElement.querySelector('.result-value') as HTMLElement;
expect(resultEl.textContent?.trim()).toBe('CHF 150.00');
});
});

View File

@@ -0,0 +1,135 @@
import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-discount-calculator',
standalone: true,
imports: [FormsModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="card">
<h2>Rabatt-Rechner</h2>
<div class="field">
<label for="price">Preis (CHF)</label>
<input
id="price"
type="number"
step="0.05"
[ngModel]="priceValue()"
(ngModelChange)="priceValue.set($event)"
placeholder="0.00"
/>
</div>
<div class="field">
<label for="discount">Rabatt (%)</label>
<input
id="discount"
type="number"
step="1"
min="0"
max="100"
[ngModel]="discountValue()"
(ngModelChange)="discountValue.set($event)"
placeholder="0"
/>
</div>
<div class="result">
<span class="result-label">Endpreis</span>
<span class="result-value">{{ displayValue() }}</span>
</div>
</div>
`,
styles: `
.card {
max-width: 320px;
margin: 2rem auto;
padding: 1.5rem;
border: 1px solid #d0d0d0;
border-radius: 8px;
background: #fafafa;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
h2 {
margin: 0 0 1rem;
font-size: 1.25rem;
color: #333;
}
.field {
margin-bottom: 1rem;
display: flex;
flex-direction: column;
}
label {
font-size: 0.875rem;
color: #555;
margin-bottom: 0.25rem;
}
input {
padding: 0.5rem 0.75rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.15);
}
.result {
margin-top: 1rem;
padding: 0.75rem;
background: #e9f5e9;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
.result-label {
font-size: 0.875rem;
color: #2a6b2a;
font-weight: 600;
}
.result-value {
font-size: 1.25rem;
font-weight: 700;
color: #1a4a1a;
}
`
})
export class DiscountCalculator {
protected readonly priceValue = signal<number | null>(null);
protected readonly discountValue = signal<number | null>(null);
protected readonly displayValue = computed(() => {
const price = this.priceValue();
const discount = this.discountValue();
if (price === null || discount === null) {
return '—';
}
if (isNaN(price) || isNaN(discount)) {
return '—';
}
if (price <= 0) {
return '—';
}
if (discount < 0 || discount > 100) {
return '—';
}
const discounted = price * (1 - discount / 100);
const rounded = Math.round(discounted / 0.05) * 0.05;
return `CHF ${rounded.toFixed(2)}`;
});
}