diff --git a/apps/backend/src/renovation/furniture.service.ts b/apps/backend/src/renovation/furniture.service.ts index 309469c..07f20cd 100644 --- a/apps/backend/src/renovation/furniture.service.ts +++ b/apps/backend/src/renovation/furniture.service.ts @@ -1120,8 +1120,8 @@ export class FurnitureService { } private optionValues(dto: CreateFurnitureOptionDto) { const text = (value?: string) => value?.trim() || null; - const decimal = (value?: number) => - value === undefined ? null : value.toFixed(2); + const decimal = (value?: number | null) => + value === undefined || value === null ? null : value.toFixed(2); return { name: dto.name?.trim() || 'Unbenannter Möbelvorschlag', manufacturer: text(dto.manufacturer), @@ -1130,13 +1130,13 @@ export class FurnitureService { retailer: text(dto.retailer), productUrl: text(dto.productUrl), articleNumber: text(dto.articleNumber), - unitPrice: dto.unitPrice.toFixed(2), + unitPrice: (dto.unitPrice ?? 0).toFixed(2), originalPrice: decimal(dto.originalPrice), - shippingCost: dto.shippingCost.toFixed(2), - additionalCost: dto.additionalCost.toFixed(2), - discount: dto.discount.toFixed(2), + shippingCost: (dto.shippingCost ?? 0).toFixed(2), + additionalCost: (dto.additionalCost ?? 0).toFixed(2), + discount: (dto.discount ?? 0).toFixed(2), currency: dto.currency.toUpperCase(), - quantity: dto.quantity, + quantity: dto.quantity ?? 1, width: decimal(dto.width), height: decimal(dto.height), depth: decimal(dto.depth), @@ -1154,15 +1154,19 @@ export class FurnitureService { budgetCategoryId: dto.budgetCategoryId ?? null, existingItem: dto.existingItem, estimatedCurrentValue: decimal(dto.estimatedCurrentValue), - movingCost: dto.movingCost.toFixed(2), - refurbishmentCost: dto.refurbishmentCost.toFixed(2), + movingCost: (dto.movingCost ?? 0).toFixed(2), + refurbishmentCost: (dto.refurbishmentCost ?? 0).toFixed(2), currentLocation: text(dto.currentLocation), condition: dto.condition ?? null, }; } private total(dto: CreateFurnitureOptionDto) { try { - return calculateFurnitureTotal(dto); + return calculateFurnitureTotal({ + ...dto, + unitPrice: dto.unitPrice ?? 0, + quantity: dto.quantity ?? 1, + }); } catch { this.validation( 'Der Rabatt darf die berechneten Gesamtkosten nicht überschreiten.', diff --git a/apps/frontend/src/app/features/projects/furniture-planning.component.spec.ts b/apps/frontend/src/app/features/projects/furniture-planning.component.spec.ts index 970e573..c930906 100644 --- a/apps/frontend/src/app/features/projects/furniture-planning.component.spec.ts +++ b/apps/frontend/src/app/features/projects/furniture-planning.component.spec.ts @@ -11,10 +11,44 @@ import type { } from './hauspilot-api.service'; import { HauspilotApiService } from './hauspilot-api.service'; import { FurniturePlanningComponent } from './furniture-planning.component'; +import { ToastService } from '../../shared/ui'; registerLocaleData(localeDe); describe('FurniturePlanningComponent', () => { + it('groups optional furniture details in a collapsed advanced section', async () => { + await TestBed.configureTestingModule({ + imports: [FurniturePlanningComponent], + providers: [provideHttpClient()], + }).compileComponents(); + const fixture = TestBed.createComponent(FurniturePlanningComponent); + fixture.componentInstance.showOptionForm.set(true); + fixture.detectChanges(); + const host: unknown = fixture.nativeElement; + if (!(host instanceof HTMLElement)) throw new Error('Test-Hostelement fehlt.'); + + const details = host.querySelector('details.advanced-fields'); + expect(details?.querySelector('summary')?.textContent).toContain('Erweiterte Informationen'); + expect(details?.textContent).toContain('Versand'); + expect(details?.textContent).toContain('Erwartete Lieferung'); + expect(details?.hasAttribute('open')).toBe(false); + }); + + it('shows a toast naming a missing required furniture selection', async () => { + const show = vi.fn(); + await TestBed.configureTestingModule({ + imports: [FurniturePlanningComponent], + providers: [provideHttpClient(), { provide: ToastService, useValue: { show } }], + }).compileComponents(); + const component = TestBed.createComponent(FurniturePlanningComponent).componentInstance; + + component.chooseOptionRequirement(); + + expect(show).toHaveBeenCalledWith( + expect.objectContaining({ message: 'Bitte einen Möbelbedarf auswählen.' }), + ); + }); + it('allows optional furniture and delivery fields to stay empty', async () => { await TestBed.configureTestingModule({ imports: [FurniturePlanningComponent], diff --git a/apps/frontend/src/app/features/projects/furniture-planning.component.ts b/apps/frontend/src/app/features/projects/furniture-planning.component.ts index fc2af0b..aff26a7 100644 --- a/apps/frontend/src/app/features/projects/furniture-planning.component.ts +++ b/apps/frontend/src/app/features/projects/furniture-planning.component.ts @@ -13,6 +13,7 @@ import type { import { HauspilotApiService } from './hauspilot-api.service'; import { conflictMessage } from './project-workspace.helpers'; import { FurnitureGridComponent } from './furniture-grid.component'; +import { ToastService } from '../../shared/ui'; const categoryLabels: Record = { seating: 'Sitzmöbel', @@ -521,40 +522,13 @@ const statusLabels: Record = { min="0" step="0.01" formControlName="unitPrice" /> + > Gesamtpreis: {{ calculatedTotal() | currency: 'EUR' : 'symbol' : '1.2-2' : 'de' }} - - @if (optionForm.controls.existingItem.value) { - - } - +
+ Erweiterte Informationen +

Diese Angaben sind vollständig optional.

+
+ + + @if (optionForm.controls.existingItem.value) { + + } + +
+