From 2f84a109e8e847a163d454b5b347b442230b7cfc Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Mon, 20 Jul 2026 11:20:41 +0200 Subject: [PATCH] bedarfe --- .../9a9850e2-1386-49b0-b169-a0786f12bd90.pdf | Bin 0 -> 80444 bytes .../projects/furniture-grid.component.spec.ts | 97 +-- .../projects/furniture-grid.component.ts | 97 +-- .../furniture-planning.component.spec.ts | 188 +++++ .../projects/furniture-planning.component.ts | 775 +++++++++++++++--- .../projects/project-workspace.page.ts | 45 +- .../frontend/src/app/layout/app-shell.spec.ts | 62 +- apps/frontend/src/app/layout/app-shell.ts | 345 +++++++- apps/frontend/src/styles/_layout.scss | 261 ++++++ 9 files changed, 1508 insertions(+), 362 deletions(-) create mode 100644 apps/backend/storage/documents/9a9850e2-1386-49b0-b169-a0786f12bd90.pdf diff --git a/apps/backend/storage/documents/9a9850e2-1386-49b0-b169-a0786f12bd90.pdf b/apps/backend/storage/documents/9a9850e2-1386-49b0-b169-a0786f12bd90.pdf new file mode 100644 index 0000000000000000000000000000000000000000..217e823d3e40f3ba656b1978d4a87d5ec40e84c9 GIT binary patch literal 80444 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∋9Yuy9@a@gSj7A4D$TtL^bj7-IO-I**r*-a-rJC2 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

    o>v&^)mW&#>Rh=43Y(A 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 AQ~&?~ literal 0 HcmV?d00001 diff --git a/apps/frontend/src/app/features/projects/furniture-grid.component.spec.ts b/apps/frontend/src/app/features/projects/furniture-grid.component.spec.ts index bb77082..e47b248 100644 --- a/apps/frontend/src/app/features/projects/furniture-grid.component.spec.ts +++ b/apps/frontend/src/app/features/projects/furniture-grid.component.spec.ts @@ -4,12 +4,7 @@ import { of } from 'rxjs'; import { vi } from 'vitest'; import { AuthService } from '../../core/auth.service'; import { FurnitureGridComponent } from './furniture-grid.component'; -import type { - FurnitureOption, - FurnitureRequirement, - FurnitureScenario, - PageResult, -} from './hauspilot-api.service'; +import type { FurnitureRequirement, PageResult } from './hauspilot-api.service'; import { HauspilotApiService } from './hauspilot-api.service'; const requirement: FurnitureRequirement = { @@ -199,78 +194,7 @@ describe('FurnitureGridComponent', () => { expect(calls[0]?.['sortBy']).toBe('sortOrder'); }); - it('assigns an option to a scenario through the existing selection endpoint', async () => { - const option: FurnitureOption = { - id: 'option-1', - version: 1, - createdAt: '2026-01-01', - updatedAt: '2026-01-01', - projectId: 'project-1', - requirementId: requirement.id, - name: 'Sofa Wunsch', - manufacturer: null, - model: null, - description: null, - retailer: null, - productUrl: null, - articleNumber: null, - unitPrice: '1000.00', - originalPrice: null, - shippingCost: '0.00', - additionalCost: '0.00', - discount: '0.00', - totalPrice: '1000.00', - currency: 'EUR', - quantity: 1, - width: null, - height: null, - depth: null, - weight: null, - color: null, - material: null, - deliveryDays: null, - expectedDeliveryDate: null, - availability: 'available', - favorite: true, - currentlySelected: false, - status: 'favorite', - notes: null, - budgetCategoryId: null, - existingItem: false, - movingCost: '0.00', - refurbishmentCost: '0.00', - deliveryStatus: 'not_ordered', - deliveredQuantity: 0, - orderNumber: null, - orderedAt: null, - }; - const scenario: FurnitureScenario = { - id: 'scenario-1', - version: 3, - createdAt: '2026-01-01', - updatedAt: '2026-01-01', - projectId: 'project-1', - name: 'Wunsch', - description: null, - type: 'preferred', - status: 'active', - isDefault: true, - total: '0.00', - selectedRequirements: 0, - openRequirements: 1, - byRoom: {}, - selections: [], - }; - const updateSelections = vi.fn(() => - of({ - ...scenario, - version: 4, - total: option.totalPrice, - selections: [ - { requirementId: requirement.id, optionId: option.id, quantity: option.quantity }, - ], - }), - ); + it('opens the assignment dialog when a scenario cell is clicked', async () => { await TestBed.configureTestingModule({ imports: [FurnitureGridComponent], providers: [ @@ -280,7 +204,6 @@ describe('FurnitureGridComponent', () => { useValue: { furnitureRequirements: () => of(page), furnitureProjectOptions: () => of({ ...page, items: [] }), - updateFurnitureScenarioSelections: updateSelections, }, }, { provide: AuthService, useValue: { user: () => ({ id: 'editor-1' }) } }, @@ -289,16 +212,12 @@ describe('FurnitureGridComponent', () => { const component = TestBed.createComponent(FurnitureGridComponent).componentInstance; component.projectId = 'project-1'; component.canEdit = true; - component.scenarios = [scenario]; - component.cellChanged({ - oldValue: '', - newValue: option.id, - data: { ...requirement, options: [option] }, - column: { getColId: () => `scenario:${scenario.id}` }, - node: { setDataValue: vi.fn() }, + const assignment = vi.fn(); + component.assignScenario.subscribe(assignment); + component.cellClicked({ + data: requirement, + column: { getColId: () => 'scenario:scenario-1' }, } as never); - expect(updateSelections).toHaveBeenCalledWith('project-1', scenario, [ - { requirementId: requirement.id, optionId: option.id, quantity: 1 }, - ]); + expect(assignment).toHaveBeenCalledWith({ requirement, scenarioId: 'scenario-1' }); }); }); diff --git a/apps/frontend/src/app/features/projects/furniture-grid.component.ts b/apps/frontend/src/app/features/projects/furniture-grid.component.ts index 03316a8..3ca0ec4 100644 --- a/apps/frontend/src/app/features/projects/furniture-grid.component.ts +++ b/apps/frontend/src/app/features/projects/furniture-grid.component.ts @@ -61,7 +61,7 @@ const requirementPriorities: Record = { const requirementStatuses: Record = { identified: { icon: '◌', label: 'Bedarf erkannt', tone: 'neutral' }, research: { icon: '⌕', label: 'Recherche', tone: 'info' }, - has_options: { icon: '≡', label: 'Alternativen vorhanden', tone: 'info' }, + has_options: { icon: '≡', label: 'Möbelvorschläge vorhanden', tone: 'info' }, decision_open: { icon: '?', label: 'Entscheidung offen', tone: 'warning' }, selected: { icon: '✓', label: 'Ausgewählt', tone: 'info' }, ordered: { icon: '▣', label: 'Bestellt', tone: 'info' }, @@ -172,8 +172,8 @@ const deliveryStatuses: Record = { } @if (view() === 'scenarios') {

    - Szenariozuweisung: Klicken Sie in eine Szenariospalte und wählen Sie eine Alternative aus. - „Nicht zugewiesen“ lässt den Bedarf im Szenario offen. + Klicken Sie auf eine Szenariozelle, um passende Möbel in Ruhe zu vergleichen und Ihre + Auswahl anschließend zu übernehmen.

    } (); @Output() addOption = new EventEmitter(); @Output() editOption = new EventEmitter(); + @Output() assignScenario = new EventEmitter<{ + requirement: FurnitureRequirement; + scenarioId: string; + }>(); @Output() dataChanged = new EventEmitter(); private readonly api = inject(HauspilotApiService); private readonly auth = inject(AuthService); @@ -352,7 +356,7 @@ export class FurnitureGridComponent implements OnChanges { delayedOnly = false; readonly views: Array<{ id: View; label: string }> = [ { id: 'requirements', label: 'Bedarfe' }, - { id: 'options', label: 'Alternativen' }, + { id: 'options', label: 'Möbelvorschläge' }, { id: 'orders', label: 'Bestellungen' }, { id: 'scenarios', label: 'Szenarien' }, ]; @@ -432,11 +436,6 @@ export class FurnitureGridComponent implements OnChanges { } cellChanged(event: CellValueChangedEvent) { if (this.rollback || !this.canEdit || event.oldValue === event.newValue || !event.data) return; - const scenarioId = this.scenarioId(event.column.getColId()); - if (scenarioId && 'requiredQuantity' in event.data) { - this.saveScenarioSelection(event, scenarioId, event.data); - return; - } const row = event.data; this.savingRow.set(row.id); this.error.set(null); @@ -464,6 +463,11 @@ export class FurnitureGridComponent implements OnChanges { if (!event.data) return; const id = event.column.getColId(); if ('requiredQuantity' in event.data) { + const scenarioId = this.scenarioId(id); + if (scenarioId && this.canEdit) { + this.assignScenario.emit({ requirement: event.data, scenarioId }); + return; + } if (id === 'actions') this.editRequirement.emit(event.data); if (id === 'addOption') this.addOption.emit(event.data); return; @@ -543,7 +547,7 @@ export class FurnitureGridComponent implements OnChanges { valueFormatter: (p) => money(p.value), valueParser: (p) => parseGermanNumber(p.newValue), }, - { field: 'optionCount', headerName: 'Alternativen' }, + { field: 'optionCount', headerName: 'Möbelvorschläge' }, { field: 'cheapestOption', headerName: 'Günstigste', @@ -586,7 +590,7 @@ export class FurnitureGridComponent implements OnChanges { }, { colId: 'addOption', - headerName: 'Alternative', + headerName: 'Möbelvorschlag', valueGetter: () => (this.canEdit ? '+ hinzufügen' : 'anzeigen'), sortable: false, }, @@ -734,28 +738,18 @@ export class FurnitureGridComponent implements OnChanges { const option = row.options.find((entry) => entry.id === params.value); return option ? `✓ ${option.name} · ${money(option.totalPrice)}` : '? Nicht zugewiesen'; }, - editable: (params) => - this.canEdit && - !!params.data && - 'requiredQuantity' in params.data && - this.selectableOptions(params.data).length > 0, - cellEditor: 'agSelectCellEditor', - cellEditorParams: (params: { data?: FurnitureRow }) => ({ - values: - params.data && 'requiredQuantity' in params.data - ? ['', ...this.selectableOptions(params.data).map((option) => option.id)] - : [''], - }), cellStyle: (params) => params.value ? { color: 'var(--color-success)', backgroundColor: 'var(--color-success-subtle)', fontWeight: '650', + cursor: this.canEdit ? 'pointer' : 'default', } : { color: 'var(--color-warning)', backgroundColor: 'var(--color-warning-subtle)', + cursor: this.canEdit ? 'pointer' : 'default', }, minWidth: 245, }), @@ -803,63 +797,6 @@ export class FurnitureGridComponent implements OnChanges { private scenarioId(columnId: string) { return columnId.startsWith('scenario:') ? columnId.slice('scenario:'.length) : null; } - private selectableOptions(requirement: FurnitureRequirement) { - return requirement.options.filter( - (option) => - !['archived', 'unavailable', 'rejected', 'returned'].includes(option.status) && - !['unavailable', 'discontinued'].includes(option.availability), - ); - } - private saveScenarioSelection( - event: CellValueChangedEvent, - scenarioId: string, - requirement: FurnitureRequirement, - ) { - const scenario = this.scenarios.find((entry) => entry.id === scenarioId); - if (!scenario) { - this.rollbackCell(event); - return; - } - const optionId = typeof event.newValue === 'string' ? event.newValue : ''; - const option = optionId - ? this.selectableOptions(requirement).find((entry) => entry.id === optionId) - : undefined; - if (optionId && !option) { - this.rollbackCell(event); - this.error.set('Diese Alternative kann dem Szenario nicht zugewiesen werden.'); - return; - } - const selections = scenario.selections - .filter((selection) => selection.requirementId !== requirement.id) - .map((selection) => ({ ...selection })); - if (option) - selections.push({ - requirementId: requirement.id, - optionId: option.id, - quantity: option.quantity, - }); - this.savingRow.set(requirement.id); - this.error.set(null); - this.api.updateFurnitureScenarioSelections(this.projectId, scenario, selections).subscribe({ - next: (saved) => { - this.scenarios = this.scenarios.map((entry) => (entry.id === saved.id ? saved : entry)); - this.savingRow.set(null); - this.refreshColumns(); - this.dataChanged.emit(); - }, - error: (error: unknown) => { - this.rollbackCell(event); - this.savingRow.set(null); - this.fail(error); - if (this.status(error) === 409) this.load(this.page()); - }, - }); - } - private rollbackCell(event: CellValueChangedEvent) { - this.rollback = true; - event.node.setDataValue(event.column.getColId(), event.oldValue); - this.rollback = false; - } private optionBody(row: FurnitureOption) { return { name: row.name, 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 1b0b823..376c02d 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 @@ -1,9 +1,67 @@ +import { registerLocaleData } from '@angular/common'; +import localeDe from '@angular/common/locales/de'; import { provideHttpClient } from '@angular/common/http'; import { TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; import { vi } from 'vitest'; +import type { + FurnitureOption, + FurnitureRequirement, + FurnitureScenario, +} from './hauspilot-api.service'; +import { HauspilotApiService } from './hauspilot-api.service'; import { FurniturePlanningComponent } from './furniture-planning.component'; +registerLocaleData(localeDe); + describe('FurniturePlanningComponent', () => { + it('explains the planning concepts and recommends the next useful step', async () => { + await TestBed.configureTestingModule({ + imports: [FurniturePlanningComponent], + providers: [provideHttpClient()], + }).compileComponents(); + const fixture = TestBed.createComponent(FurniturePlanningComponent); + fixture.componentInstance.canEdit = true; + fixture.detectChanges(); + const host: unknown = fixture.nativeElement; + if (!(host instanceof HTMLElement)) throw new Error('Test-Hostelement fehlt.'); + + expect(host.querySelector('.concept-flow')?.textContent).toContain('Was wird wo benötigt?'); + expect(host.querySelector('.concept-flow')?.textContent).toContain( + 'Was kommt konkret infrage?', + ); + expect(host.querySelector('.next-action button')?.textContent).toContain( + 'Ersten Bedarf anlegen', + ); + + fixture.componentInstance.requirements.set([ + { + id: 'requirement-1', + version: 1, + createdAt: '2026-01-01', + updatedAt: '2026-01-01', + projectId: 'project-1', + roomId: 'room-1', + name: 'Sofa', + description: null, + category: 'seating', + priority: 'normal', + requiredQuantity: 1, + status: 'identified', + responsibleUserId: null, + maximumBudget: null, + sortOrder: 0, + options: [], + }, + ]); + fixture.detectChanges(); + + expect(fixture.componentInstance.nextStep()).toBe('option'); + expect(host.querySelector('.next-action button')?.textContent).toContain( + 'Möbelvorschlag hinzufügen', + ); + }); + it('calculates option totals including quantity, shipping, extras and discounts', async () => { await TestBed.configureTestingModule({ imports: [FurniturePlanningComponent], @@ -77,4 +135,134 @@ describe('FurniturePlanningComponent', () => { expect(showModal).toHaveBeenCalledOnce(); expect(fixture.componentInstance.showRequirementForm()).toBe(true); }); + + it('guides users through an explicit scenario assignment', async () => { + const option = { + id: 'option-1', + version: 1, + createdAt: '2026-01-01', + updatedAt: '2026-01-01', + projectId: 'project-1', + requirementId: 'requirement-1', + name: 'Wunschsofa', + manufacturer: 'Nordmöbel', + model: null, + description: null, + retailer: 'Wohnwelt', + productUrl: null, + articleNumber: null, + unitPrice: '1000.00', + originalPrice: null, + shippingCost: '0.00', + additionalCost: '0.00', + discount: '0.00', + totalPrice: '1000.00', + currency: 'EUR', + quantity: 1, + width: null, + height: null, + depth: null, + weight: null, + color: null, + material: null, + deliveryDays: null, + expectedDeliveryDate: null, + availability: 'available', + favorite: false, + currentlySelected: false, + status: 'idea', + notes: null, + budgetCategoryId: null, + existingItem: false, + movingCost: '0.00', + refurbishmentCost: '0.00', + deliveryStatus: 'not_ordered', + deliveredQuantity: 0, + orderNumber: null, + orderedAt: null, + } satisfies FurnitureOption; + const requirement = { + id: 'requirement-1', + version: 1, + createdAt: '2026-01-01', + updatedAt: '2026-01-01', + projectId: 'project-1', + roomId: 'room-1', + name: 'Sofa', + description: null, + category: 'seating', + priority: 'normal', + requiredQuantity: 1, + status: 'identified', + responsibleUserId: null, + maximumBudget: null, + sortOrder: 0, + options: [option], + } satisfies FurnitureRequirement; + const scenario = { + id: 'scenario-1', + version: 1, + createdAt: '2026-01-01', + updatedAt: '2026-01-01', + projectId: 'project-1', + name: 'Wunsch', + description: null, + type: 'preferred', + status: 'draft', + isDefault: true, + total: '0.00', + selectedRequirements: 0, + openRequirements: 1, + byRoom: {}, + selections: [], + } satisfies FurnitureScenario; + const updateSelections = vi.fn(() => + of({ + ...scenario, + version: 2, + selections: [{ requirementId: requirement.id, optionId: option.id, quantity: 1 }], + }), + ); + await TestBed.configureTestingModule({ + imports: [FurniturePlanningComponent], + providers: [ + provideHttpClient(), + { + provide: HauspilotApiService, + useValue: { + updateFurnitureScenarioSelections: updateSelections, + furnitureRequirements: () => + of({ items: [requirement], page: 1, pageSize: 50, totalItems: 1, totalPages: 1 }), + }, + }, + ], + }).compileComponents(); + const fixture = TestBed.createComponent(FurniturePlanningComponent); + const component = fixture.componentInstance; + component.projectId = 'project-1'; + component.requirements.set([requirement]); + component.scenarios.set([scenario]); + vi.spyOn(component, 'load').mockImplementation(() => undefined); + + component.openAssignment(scenario.id, requirement.id, option.id); + fixture.detectChanges(); + const host: unknown = fixture.nativeElement; + if (!(host instanceof HTMLElement)) throw new Error('Test-Hostelement fehlt.'); + const choice = host.querySelector('.option-choice'); + expect(choice?.textContent).toContain('Wunschsofa'); + expect(choice?.textContent).toContain('Nordmöbel'); + expect(choice?.textContent).toContain('Wohnwelt'); + const radio = choice?.querySelector('input[type="radio"]'); + expect(radio instanceof HTMLInputElement && radio.checked).toBe(true); + const dialog = host.querySelector('#assignment-form-title')?.closest('dialog'); + if (!(dialog instanceof HTMLDialogElement)) throw new Error('Zuordnungsdialog fehlt.'); + Object.defineProperty(dialog, 'close', { value: vi.fn() }); + component.saveAssignment(); + + expect(updateSelections).toHaveBeenCalledWith('project-1', scenario, [ + { requirementId: requirement.id, optionId: option.id, quantity: 1 }, + ]); + expect(component.assignedRequirements()).toBe(1); + expect(component.showAssignmentForm()).toBe(false); + }); }); 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 fbf8984..c2b1437 100644 --- a/apps/frontend/src/app/features/projects/furniture-planning.component.ts +++ b/apps/frontend/src/app/features/projects/furniture-planning.component.ts @@ -34,7 +34,7 @@ const categoryLabels: Record = { const statusLabels: Record = { identified: 'Bedarf erkannt', research: 'Recherche', - has_options: 'Alternativen vorhanden', + has_options: 'Möbelvorschläge vorhanden', decision_open: 'Entscheidung offen', selected: 'Ausgewählt', ordered: 'Bestellt', @@ -52,14 +52,126 @@ const statusLabels: Record = {

    Möbel & Einrichtung

    -

    Bedarfe, Produktalternativen und Einrichtungsszenarien gemeinsam planen.

    +

    Planen Sie vom benötigten Möbel bis zur fertigen Einrichtungsvariante.

    - @if (canEdit) { - - }
    +
    +
    + Geführte Planung +

    Was gehört hier zusammen?

    +

    + Sie notieren zuerst, was fehlt, sammeln dafür konkrete + Möbelvorschläge und stellen daraus ein + Szenario zusammen. +

    +
    +
    + +
    BedarfWas wird wo benötigt?
    +
    + +
    + +
    MöbelvorschlagWas kommt konkret infrage?
    +
    + +
    + +
    + SzenarioWelche Vorschläge wählen wir zusammen? +
    +
    +
    +
    +
      +
    1. + +
      + Bedarf notieren{{ requirements().length }} Bedarfe erfasst +
      +
    2. +
    3. + +
      + Möbelvorschläge sammeln{{ requirementsWithOptions() }} von {{ requirements().length }} Bedarfen haben + Vorschläge +
      +
    4. +
    5. + +
      + Szenario zusammenstellen{{ assignedRequirements() }} Bedarfe sind einem Szenario zugeordnet +
      +
    6. +
    +
    +
    + Als Nächstes + @switch (nextStep()) { + @case ('need') { + Notieren Sie den ersten Möbelbedarf + Zum Beispiel „Sofa im Wohnzimmer“ – ein Produkt müssen Sie noch nicht + kennen. + } + @case ('option') { + Ergänzen Sie einen konkreten Möbelvorschlag + Das kann ein neues Produkt oder ein bereits vorhandenes Möbelstück sein. + } + @case ('scenario') { + Erstellen Sie Ihre erste Einrichtungsvariante + Ein Szenario bündelt je Bedarf genau einen ausgewählten Vorschlag. + } + @default { + Vervollständigen oder vergleichen Sie Ihre Szenarien + Ordnen Sie offene Bedarfe zu oder probieren Sie eine weitere Variante aus. + } + } +
    + @if (canEdit) { + @switch (nextStep()) { + @case ('need') { + + } + @case ('option') { + + } + @case ('scenario') { + + } + @default { + + } + } + } +
    +
    @if (error()) { } @@ -67,65 +179,36 @@ const statusLabels: Record = {

    Möbelplanung wird geladen …

    } @if (summary(); as data) { -
    -
    - Ausgewählte Möbel{{ data.selectedCost | currency: 'EUR' : 'symbol' : '1.2-2' : 'de' }}{{ data.selected }} ausgewählt -
    -
    - Günstigste Variante{{ data.cheapestCost | currency: 'EUR' : 'symbol' : '1.2-2' : 'de' }}{{ data.openPrices }} Preise noch offen -
    -
    - Tatsächliche Ausgaben{{ - data.actualExpenseCost | currency: 'EUR' : 'symbol' : '1.2-2' : 'de' - }}Verknüpfte Ausgaben, nicht doppelt gezählt -
    -
    - Entscheidungen{{ data.withoutDecision }}{{ data.withoutOption }} ohne Alternative · {{ data.delayed }} verspätet -
    -
    +
    + Kosten und Planungsstand anzeigen +
    +
    + Ausgewählte Möbel{{ data.selectedCost | currency: 'EUR' : 'symbol' : '1.2-2' : 'de' }}{{ data.selected }} ausgewählt +
    +
    + Günstigste Variante{{ data.cheapestCost | currency: 'EUR' : 'symbol' : '1.2-2' : 'de' }}{{ data.openPrices }} Preise noch offen +
    +
    + Tatsächliche Ausgaben{{ + data.actualExpenseCost | currency: 'EUR' : 'symbol' : '1.2-2' : 'de' + }}Verknüpfte Ausgaben, nicht doppelt gezählt +
    +
    + Offene Entscheidungen{{ data.withoutDecision }}{{ data.withoutOption }} ohne Möbelvorschlag · {{ data.delayed }} verspätet +
    +
    +
    } -
    - - - - - - -
    - = { } -
    - -
    - -
    - Kompakte Kartenansicht +
    +
    +
    + Ihre Planung +

    Bedarfe und Möbelvorschläge

    +

    Jeder Bedarf zeigt die konkreten Möbel, die dafür infrage kommen.

    +
    + @if (canEdit) { + + } +
    @for (requirement of requirements(); track requirement.id) {
    @@ -238,7 +318,7 @@ const statusLabels: Record = { requirement.maximumBudget || 0 | currency: 'EUR' : 'symbol' : '1.2-2' : 'de' }}Alternativen: {{ requirement.options.length }}Möbelvorschläge: {{ requirement.options.length }}
    @if (requirement.description) { @@ -252,8 +332,8 @@ const statusLabels: Record = { > {{ expandedRequirementId() === requirement.id - ? 'Alternativen schließen' - : 'Alternativen vergleichen' + ? 'Möbelvorschläge schließen' + : 'Möbelvorschläge ansehen' }} @if (canEdit) { @@ -268,14 +348,14 @@ const statusLabels: Record = { type="button" (click)="newOption(requirement)" > - Alternative hinzufügen + Möbelvorschlag hinzufügen } @if (expandedRequirementId() === requirement.id) { -
    +
    @if (!requirement.options.length) { -

    Noch keine Alternative erfasst.

    +

    Noch kein Möbelvorschlag erfasst.

    } @for (option of requirement.options; track option.id) {
    @@ -392,6 +472,27 @@ const statusLabels: Record = { } +
    + +
    + Detailtabelle und Bestellungen öffnen +

    + Für umfangreiche Planungen: Bedarfe filtern, Daten direkt bearbeiten, Bestellungen prüfen + und Szenarien tabellarisch vergleichen. +

    +
    + +
    = { @if (showOptionForm()) {

    - {{ editingOption() ? 'Alternative bearbeiten' : 'Alternative hinzufügen' }} + {{ editingOption() ? 'Möbelvorschlag bearbeiten' : 'Möbelvorschlag hinzufügen' }}

    = { }
    + + @if (showOptionPicker()) { + +
    + Schritt 2 von 3 +

    Für welchen Bedarf ist das Möbel?

    +

    + So bleibt jede Produktalternative direkt dem richtigen Raum und Bedarf zugeordnet. +

    +
    + +
    + + +
    + + } +
    +
    -

    Einrichtungsszenarien

    -

    Budget-, Wunsch- und Premiumvarianten vergleichen.

    +

    Einrichtungsvarianten

    +

    + Ein Szenario ist eine komplette Variante: Für jeden Bedarf wählen Sie darin einen + Möbelvorschlag aus. +

    @if (canEdit) {
    - -
    }
    - @if (showScenarioForm()) { -
    - -
    - }
    @for (scenario of scenarios(); track scenario.id) {
    @@ -549,6 +670,15 @@ const statusLabels: Record = {

    {{ scenario.selectedRequirements }} gewählt · {{ scenario.openRequirements }} offen

    + @if (canEdit) { + + }
    } @empty {

    Noch kein Szenario vorhanden.

    @@ -574,6 +704,165 @@ const statusLabels: Record = {
    }
    + + + @if (showScenarioForm()) { +
    +
    + Einrichtungsvariante +

    Neues Szenario erstellen

    +

    + Die Vorauswahl befüllt das Szenario automatisch. Sie können jede Zuordnung danach + ändern. +

    +
    + + + +
    + + +
    +
    + } +
    + + + @if (showAssignmentForm()) { +
    +
    + Schritt 3 von 3 +

    Möbel für ein Szenario auswählen

    +

    + Wählen Sie zuerst die Variante und den Bedarf, danach den passenden Möbelvorschlag. +

    +
    + @if (!scenarios().length) { +
    + Es fehlt noch ein Szenario.Erstellen Sie zuerst eine Einrichtungsvariante. +
    + } @else if (!assignableRequirements().length) { +
    + Es fehlt noch ein Möbelvorschlag.Erfassen Sie für mindestens einen Bedarf ein konkretes Möbel. +
    + } @else { + + +
    + Möbelvorschlag auswählen * +
    + @for (option of assignmentOptions(); track option.id) { + + } +
    +
    + } +
    + @if (!scenarios().length) { + + } @else if (!assignableRequirements().length) { + + } @else { + + } + +
    +
    + } +
    `, styles: [ ` @@ -604,6 +893,14 @@ const statusLabels: Record = { grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); gap: var(--space-4); } + .dialog-heading, + .dialog-notice { + display: grid; + gap: var(--space-1); + } + .dialog-form label small { + color: var(--color-text-muted); + } .metric, .requirement, .scenarios { @@ -675,6 +972,22 @@ const statusLabels: Record = { border: 0; padding: var(--space-6); } + .dialog-form { + display: grid; + gap: var(--space-5); + border: 0; + padding: var(--space-6); + } + .dialog-heading { + gap: var(--space-2); + padding-bottom: var(--space-4); + border-bottom: 1px solid var(--color-border); + } + .dialog-notice { + padding: var(--space-4); + border-left: 0.25rem solid var(--color-info); + background: var(--color-info-subtle); + } .check { display: flex; align-items: center; @@ -776,7 +1089,9 @@ export class FurniturePlanningComponent implements OnChanges { @Input() canEdit = false; @ViewChild('requirementDialog') private requirementDialog?: ElementRef; @ViewChild('optionDialog') private optionDialog?: ElementRef; - @ViewChild('furnitureGridHost') private furnitureGridHost?: ElementRef; + @ViewChild('optionPickerDialog') private optionPickerDialog?: ElementRef; + @ViewChild('scenarioDialog') private scenarioDialog?: ElementRef; + @ViewChild('assignmentDialog') private assignmentDialog?: ElementRef; private readonly api = inject(HauspilotApiService); readonly requirements = signal([]); readonly scenarios = signal([]); @@ -789,12 +1104,35 @@ export class FurniturePlanningComponent implements OnChanges { readonly expandedRequirementId = signal(null); readonly showRequirementForm = signal(false); readonly showOptionForm = signal(false); + readonly showOptionPicker = signal(false); readonly showScenarioForm = signal(false); + readonly showAssignmentForm = signal(false); + readonly guidedPlanning = signal(false); readonly editingRequirement = signal(null); readonly editingOption = signal(null); readonly optionRequirement = signal(null); readonly categories = Object.entries(categoryLabels); readonly requirementStatuses = Object.entries(statusLabels); + readonly requirementsWithOptions = computed( + () => this.requirements().filter((requirement) => requirement.options.length > 0).length, + ); + readonly assignedRequirements = computed( + () => + new Set( + this.scenarios().flatMap((scenario) => + scenario.selections.map((selection) => selection.requirementId), + ), + ).size, + ); + readonly nextStep = computed<'need' | 'option' | 'scenario' | 'assign'>(() => { + if (!this.requirements().length) return 'need'; + if (this.requirementsWithOptions() < this.requirements().length) return 'option'; + if (!this.scenarios().length) return 'scenario'; + return 'assign'; + }); + readonly assignableRequirements = computed(() => + this.requirements().filter((requirement) => this.selectableOptions(requirement).length > 0), + ); readonly filterForm = new FormGroup({ search: new FormControl('', { nonNullable: true }), roomId: new FormControl('', { nonNullable: true }), @@ -878,6 +1216,26 @@ export class FurniturePlanningComponent implements OnChanges { automaticSelection: new FormControl('budget', { nonNullable: true }), isDefault: new FormControl(false, { nonNullable: true }), }); + readonly optionTargetForm = new FormGroup({ + requirementId: new FormControl('', { + nonNullable: true, + validators: [(control) => Validators.required(control)], + }), + }); + readonly assignmentForm = new FormGroup({ + scenarioId: new FormControl('', { + nonNullable: true, + validators: [(control) => Validators.required(control)], + }), + requirementId: new FormControl('', { + nonNullable: true, + validators: [(control) => Validators.required(control)], + }), + optionId: new FormControl('', { + nonNullable: true, + validators: [(control) => Validators.required(control)], + }), + }); readonly calculatedTotal = computed(() => { const v = this.optionForm.getRawValue(); const acquisition = v.existingItem ? 0 : v.unitPrice * v.quantity; @@ -927,6 +1285,10 @@ export class FurniturePlanningComponent implements OnChanges { status(value: string) { return statusLabels[value] ?? value; } + startGuidedPlanning() { + this.guidedPlanning.set(true); + this.newRequirement(); + } newRequirement() { this.editingRequirement.set(null); this.requirementForm.reset({ @@ -959,10 +1321,11 @@ export class FurniturePlanningComponent implements OnChanges { this.showRequirementForm.set(true); this.openDialog(this.requirementDialog); } - closeRequirementForm() { + closeRequirementForm(keepFlow = false) { this.requirementDialog?.nativeElement.close(); this.showRequirementForm.set(false); this.editingRequirement.set(null); + if (!keepFlow) this.guidedPlanning.set(false); } saveRequirement() { if (this.requirementForm.invalid) return this.requirementForm.markAllAsTouched(); @@ -976,9 +1339,10 @@ export class FurniturePlanningComponent implements OnChanges { ) : this.api.createFurnitureRequirement(this.projectId, this.requirementForm.getRawValue()); request.subscribe({ - next: () => { + next: (saved) => { this.saving.set(false); - this.closeRequirementForm(); + this.closeRequirementForm(true); + if (!existing && this.guidedPlanning()) this.newOption(saved); this.load(); }, error: (e: unknown) => this.fail(e), @@ -987,6 +1351,32 @@ export class FurniturePlanningComponent implements OnChanges { toggleOptions(r: FurnitureRequirement) { this.expandedRequirementId.set(this.expandedRequirementId() === r.id ? null : r.id); } + openOptionPicker() { + if (!this.requirements().length) { + this.startGuidedPlanning(); + return; + } + this.guidedPlanning.set(true); + this.optionTargetForm.reset({ + requirementId: this.requirements().length === 1 ? (this.requirements()[0]?.id ?? '') : '', + }); + this.showOptionPicker.set(true); + this.openDialog(this.optionPickerDialog); + } + closeOptionPicker(keepFlow = false) { + this.optionPickerDialog?.nativeElement.close(); + this.showOptionPicker.set(false); + if (!keepFlow) this.guidedPlanning.set(false); + } + chooseOptionRequirement() { + if (this.optionTargetForm.invalid) return this.optionTargetForm.markAllAsTouched(); + const requirement = this.requirements().find( + (entry) => entry.id === this.optionTargetForm.controls.requirementId.value, + ); + if (!requirement) return; + this.closeOptionPicker(true); + this.newOption(requirement); + } newOption(r: FurnitureRequirement) { this.optionRequirement.set(r); this.editingOption.set(null); @@ -1050,11 +1440,12 @@ export class FurniturePlanningComponent implements OnChanges { this.showOptionForm.set(true); this.openDialog(this.optionDialog); } - closeOptionForm() { + closeOptionForm(keepFlow = false) { this.optionDialog?.nativeElement.close(); this.showOptionForm.set(false); this.editingOption.set(null); this.optionRequirement.set(null); + if (!keepFlow) this.guidedPlanning.set(false); } editOptionFromGrid(option: FurnitureOption) { this.api.furnitureRequirement(this.projectId, option.requirementId).subscribe({ @@ -1072,9 +1463,18 @@ export class FurniturePlanningComponent implements OnChanges { ? this.api.updateFurnitureOption(this.projectId, existing, body) : this.api.createFurnitureOption(this.projectId, requirement.id, body); request.subscribe({ - next: () => { + next: (saved) => { this.saving.set(false); - this.closeOptionForm(); + this.closeOptionForm(true); + if (!existing && this.guidedPlanning()) { + this.requirements.update((current) => { + const updated = { ...requirement, options: [...requirement.options, saved] }; + return current.some((entry) => entry.id === requirement.id) + ? current.map((entry) => (entry.id === requirement.id ? updated : entry)) + : [...current, updated]; + }); + this.openAssignment(undefined, requirement.id, saved.id); + } this.load(); }, error: (e: unknown) => this.fail(e), @@ -1103,14 +1503,18 @@ export class FurniturePlanningComponent implements OnChanges { .deliverFurnitureOption(this.projectId, o, o.quantity) .subscribe({ next: () => this.load(), error: (e: unknown) => this.fail(e) }); } - openScenarioGrid(grid: FurnitureGridComponent) { - grid.setView('scenarios'); - queueMicrotask(() => - this.furnitureGridHost?.nativeElement.scrollIntoView({ behavior: 'smooth', block: 'start' }), - ); + openScenarioForm() { + this.scenarioForm.reset({ name: '', automaticSelection: 'budget', isDefault: false }); + this.showScenarioForm.set(true); + this.openDialog(this.scenarioDialog); + } + closeScenarioForm(keepFlow = false) { + this.scenarioDialog?.nativeElement.close(); + this.showScenarioForm.set(false); + if (!keepFlow) this.guidedPlanning.set(false); } createScenario() { - if (this.scenarioForm.invalid) return; + if (this.scenarioForm.invalid) return this.scenarioForm.markAllAsTouched(); this.saving.set(true); const value = this.scenarioForm.getRawValue(); this.api @@ -1122,15 +1526,128 @@ export class FurniturePlanningComponent implements OnChanges { isDefault: value.isDefault, }) .subscribe({ - next: () => { + next: (saved) => { this.saving.set(false); - this.showScenarioForm.set(false); - this.scenarioForm.reset({ name: '', automaticSelection: 'budget', isDefault: false }); + this.closeScenarioForm(true); + this.scenarios.update((current) => [...current, saved]); + if (this.guidedPlanning()) this.openAssignment(saved.id); this.load(); }, error: (e: unknown) => this.fail(e), }); } + openAssignment(scenarioId = '', requirementId = '', optionId = '') { + const scenario = + this.scenarios().find((entry) => entry.id === scenarioId) ?? + this.scenarios().find((entry) => entry.isDefault) ?? + this.scenarios()[0]; + const requirement = + this.assignableRequirements().find((entry) => entry.id === requirementId) ?? + this.assignableRequirements()[0]; + const selection = scenario?.selections.find((entry) => entry.requirementId === requirement?.id); + this.assignmentForm.reset({ + scenarioId: scenario?.id ?? '', + requirementId: requirement?.id ?? '', + optionId: optionId || selection?.optionId || '', + }); + this.showAssignmentForm.set(true); + this.openDialog(this.assignmentDialog); + } + openScenarioAssignment(scenarioId: string, requirement: FurnitureRequirement) { + this.requirements.update((current) => + current.some((entry) => entry.id === requirement.id) + ? current.map((entry) => (entry.id === requirement.id ? requirement : entry)) + : [...current, requirement], + ); + this.openAssignment(scenarioId, requirement.id); + } + closeAssignment(keepFlow = false) { + this.assignmentDialog?.nativeElement.close(); + this.showAssignmentForm.set(false); + if (!keepFlow) this.guidedPlanning.set(false); + } + assignmentOptions() { + const requirement = this.requirements().find( + (entry) => entry.id === this.assignmentForm.controls.requirementId.value, + ); + return requirement ? this.selectableOptions(requirement) : []; + } + optionManufacturer(option: FurnitureOption) { + return [option.manufacturer, option.model].filter(Boolean).join(' · ') || 'Nicht angegeben'; + } + optionDimensions(option: FurnitureOption) { + if (option.width === null && option.height === null && option.depth === null) + return 'Nicht angegeben'; + return `${option.width ?? '–'} × ${option.height ?? '–'} × ${option.depth ?? '–'} cm`; + } + optionDelivery(option: FurnitureOption) { + return option.deliveryDays === null ? 'Nicht angegeben' : `${option.deliveryDays} Tage`; + } + optionAvailability(value: string) { + const labels: Record = { + unknown: 'Verfügbarkeit unbekannt', + available: 'Verfügbar', + limited: 'Begrenzt verfügbar', + unavailable: 'Nicht verfügbar', + discontinued: 'Nicht mehr erhältlich', + }; + return labels[value] ?? value; + } + assignmentRequirementChanged() { + const scenario = this.scenarios().find( + (entry) => entry.id === this.assignmentForm.controls.scenarioId.value, + ); + const selection = scenario?.selections.find( + (entry) => entry.requirementId === this.assignmentForm.controls.requirementId.value, + ); + this.assignmentForm.controls.optionId.setValue(selection?.optionId ?? ''); + } + continueWithScenario() { + this.guidedPlanning.set(true); + this.closeAssignment(true); + this.openScenarioForm(); + } + continueWithOption() { + this.guidedPlanning.set(true); + this.closeAssignment(true); + this.openOptionPicker(); + } + saveAssignment() { + if (this.assignmentForm.invalid) return this.assignmentForm.markAllAsTouched(); + const value = this.assignmentForm.getRawValue(); + const scenario = this.scenarios().find((entry) => entry.id === value.scenarioId); + const requirement = this.requirements().find((entry) => entry.id === value.requirementId); + const option = requirement?.options.find((entry) => entry.id === value.optionId); + if (!scenario || !requirement || !option) return; + const selections = scenario.selections + .filter((selection) => selection.requirementId !== requirement.id) + .map((selection) => ({ ...selection })); + selections.push({ + requirementId: requirement.id, + optionId: option.id, + quantity: option.quantity, + }); + this.saving.set(true); + this.api.updateFurnitureScenarioSelections(this.projectId, scenario, selections).subscribe({ + next: (saved) => { + this.saving.set(false); + this.scenarios.update((current) => + current.map((entry) => (entry.id === saved.id ? saved : entry)), + ); + this.guidedPlanning.set(false); + this.closeAssignment(); + this.load(); + }, + error: (error: unknown) => this.fail(error), + }); + } + private selectableOptions(requirement: FurnitureRequirement) { + return requirement.options.filter( + (option) => + !['archived', 'unavailable', 'rejected', 'returned'].includes(option.status) && + !['unavailable', 'discontinued'].includes(option.availability), + ); + } private fail(error: unknown) { this.saving.set(false); this.loading.set(false); diff --git a/apps/frontend/src/app/features/projects/project-workspace.page.ts b/apps/frontend/src/app/features/projects/project-workspace.page.ts index dac6ed8..a4a678b 100644 --- a/apps/frontend/src/app/features/projects/project-workspace.page.ts +++ b/apps/frontend/src/app/features/projects/project-workspace.page.ts @@ -2,7 +2,7 @@ import { CurrencyPipe, DatePipe, DecimalPipe } from '@angular/common'; import { Component, computed, inject, signal } from '@angular/core'; import type { OnInit } from '@angular/core'; import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; -import { ActivatedRoute, RouterLink } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; import type { Observable } from 'rxjs'; import type { ApiErrorBody, ProjectDto, ProjectMemberDto } from '@boilerplate/api-client'; import { ApiClientService } from '@boilerplate/api-client'; @@ -30,17 +30,6 @@ import type { Room, } from './hauspilot-api.service'; -const sections = [ - ['uebersicht', 'Übersicht'], - ['raeume', 'Räume'], - ['aufgaben', 'Aufgaben'], - ['zeitplan', 'Zeitplan'], - ['budget', 'Budget'], - ['moebel', 'Möbel & Einrichtung'], - ['dokumente', 'Dokumente'], - ['aktivitaeten', 'Aktivitäten'], -] as const; - @Component({ standalone: true, imports: [ @@ -48,7 +37,6 @@ const sections = [ DatePipe, DecimalPipe, ReactiveFormsModule, - RouterLink, UiEmptyStateComponent, UiPageHeaderComponent, UiStatusBadgeComponent, @@ -57,22 +45,11 @@ const sections = [ FurniturePlanningComponent, ], template: ` - Zurück zu den Projekten @if (project(); as project) { - @if (loading()) {

    Projektdaten werden geladen …

    } @@ -680,25 +657,6 @@ const sections = [ display: grid; gap: var(--space-5); } - .project-nav { - display: flex; - gap: var(--space-2); - overflow-x: auto; - padding-block: var(--space-2); - border-bottom: 1px solid var(--color-border); - } - .project-nav a { - white-space: nowrap; - padding: var(--space-2) var(--space-3); - border-radius: var(--radius-md); - color: var(--color-text-secondary); - text-decoration: none; - } - .project-nav a.active { - background: var(--color-primary-subtle); - color: var(--color-primary); - font-weight: 600; - } .metric-grid, .card-grid { display: grid; @@ -775,7 +733,6 @@ export class ProjectWorkspacePageComponent implements OnInit { private readonly auth = inject(AuthService); private readonly route = inject(ActivatedRoute); projectId = ''; - readonly navigation = sections; readonly section = signal('uebersicht'); readonly project = signal(null); readonly dashboard = signal(null); diff --git a/apps/frontend/src/app/layout/app-shell.spec.ts b/apps/frontend/src/app/layout/app-shell.spec.ts index 4007fb2..90dffe9 100644 --- a/apps/frontend/src/app/layout/app-shell.spec.ts +++ b/apps/frontend/src/app/layout/app-shell.spec.ts @@ -1,10 +1,15 @@ +import { Component } from '@angular/core'; import { TestBed } from '@angular/core/testing'; -import { provideRouter } from '@angular/router'; +import { Router, provideRouter } from '@angular/router'; import { of } from 'rxjs'; import { ApiClientService } from '@boilerplate/api-client'; import { AppShellComponent } from './app-shell'; +@Component({ standalone: true, template: '' }) +class EmptyPageComponent {} + describe('AppShellComponent', () => { + beforeEach(() => sessionStorage.clear()); it('hides navigation entries without matching permissions', async () => { await TestBed.configureTestingModule({ imports: [AppShellComponent], @@ -99,4 +104,59 @@ describe('AppShellComponent', () => { fixture.detectChanges(); expect((fixture.nativeElement as HTMLElement).querySelector('.sidebar.open')).toBeNull(); }); + + it('shows an opened project as a tab and its contents in the sidebar', async () => { + await TestBed.configureTestingModule({ + imports: [AppShellComponent], + providers: [ + provideRouter([{ path: 'projekte/:id/:section', component: EmptyPageComponent }]), + { + provide: ApiClientService, + useValue: { + me: () => + of({ + id: 'u1', + name: 'Ada', + email: null, + active: true, + lastLoginAt: null, + settings: { tablePageSize: 20, sidebarExpanded: true }, + roles: [ + { + id: 'r1', + name: 'user', + protected: true, + permissions: [{ id: 'projects.use', description: 'projects.use' }], + }, + ], + }), + project: () => + of({ + id: 'project-1', + name: 'Wohnung Budapest', + description: null, + role: 'owner', + createdAt: '2026-01-01', + updatedAt: '2026-01-01', + }), + unreadNotificationCount: () => of({ count: 0 }), + notifications: () => of({ items: [], total: 0, page: 1, pageSize: 20, unreadCount: 0 }), + }, + }, + ], + }).compileComponents(); + const fixture = TestBed.createComponent(AppShellComponent); + const router = TestBed.inject(Router); + await router.navigateByUrl('/projekte/project-1/moebel'); + await fixture.whenStable(); + fixture.detectChanges(); + + const host = fixture.nativeElement as HTMLElement; + expect(host.querySelector('.project-tab')?.textContent).toContain('Wohnung Budapest'); + expect(host.querySelector('.sidebar-context')?.textContent).toContain('Wohnung Budapest'); + expect(host.querySelector('.project-navigation')?.textContent).toContain('Möbel & Einrichtung'); + expect(host.querySelector('.project-navigation a.active')?.textContent).toContain( + 'Möbel & Einrichtung', + ); + }); }); diff --git a/apps/frontend/src/app/layout/app-shell.ts b/apps/frontend/src/app/layout/app-shell.ts index 9c0e926..a23eb05 100644 --- a/apps/frontend/src/app/layout/app-shell.ts +++ b/apps/frontend/src/app/layout/app-shell.ts @@ -1,6 +1,8 @@ import { Component, HostListener, computed, effect, inject, signal } from '@angular/core'; -import { RouterLink, RouterLinkActive, RouterOutlet, Router } from '@angular/router'; -import type { Permission } from '@boilerplate/api-client'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { NavigationEnd, RouterLink, RouterLinkActive, RouterOutlet, Router } from '@angular/router'; +import { ApiClientService, type Permission } from '@boilerplate/api-client'; +import { filter } from 'rxjs'; import { NotificationStore } from '../core/notification.store'; import { AuthService } from '../core/auth.service'; import { NotificationPanelComponent } from './notification-panel'; @@ -12,6 +14,23 @@ interface NavItem { permission?: Permission; } +interface ProjectTab { + id: string; + name: string; + path: string; +} + +const projectSections = [ + { id: 'uebersicht', label: 'Übersicht' }, + { id: 'raeume', label: 'Räume' }, + { id: 'aufgaben', label: 'Aufgaben' }, + { id: 'zeitplan', label: 'Zeitplan' }, + { id: 'budget', label: 'Budget' }, + { id: 'moebel', label: 'Möbel & Einrichtung' }, + { id: 'dokumente', label: 'Dokumente' }, + { id: 'aktivitaeten', label: 'Aktivitäten' }, +] as const; + @Component({ selector: 'app-shell', standalone: true, @@ -30,7 +49,28 @@ interface NavItem { @if (auth.user()) { } - HausPilot + HausPilot + @if (projectTabs().length) { + + } @if (auth.user()) { }
    - +
    } @else { @@ -131,6 +225,53 @@ interface NavItem { } .brand { white-space: nowrap; + color: var(--color-text-primary); + font-weight: var(--font-weight-bold); + text-decoration: none; + } + .project-tabs { + display: flex; + flex: 1 1 auto; + align-self: stretch; + gap: var(--space-2); + min-width: 0; + overflow-x: auto; + padding-top: var(--space-3); + } + .project-tab { + display: flex; + align-items: center; + min-width: 9rem; + max-width: 15rem; + border: 1px solid var(--color-border); + border-bottom: 0; + border-radius: var(--radius-lg) var(--radius-lg) 0 0; + background: var(--color-background); + } + .project-tab.active { + background: var(--color-surface); + border-color: var(--color-border-strong); + box-shadow: inset 0 0.2rem var(--color-primary); + } + .project-tab a { + min-width: 0; + flex: 1; + overflow: hidden; + padding: var(--space-3) var(--space-2) var(--space-3) var(--space-4); + color: var(--color-text-primary); + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; + } + .project-tab button { + width: 2rem; + height: 2rem; + flex: 0 0 auto; + border: 0; + border-radius: var(--radius-pill); + background: transparent; + color: var(--color-text-muted); + cursor: pointer; } .logout { margin-left: auto; @@ -190,20 +331,56 @@ interface NavItem { transform: translateX(-100%); transition: transform var(--transition-base) ease; z-index: var(--z-drawer); + overflow-y: auto; } .sidebar.open { transform: translateX(0); } - nav a { + .sidebar nav a { display: block; padding: var(--space-4) var(--space-5); color: var(--color-text-primary); text-decoration: none; min-height: var(--touch-target); } - nav a.active { + .sidebar nav a.active { background: var(--color-primary-subtle); border-left: 4px solid var(--color-primary); + font-weight: var(--font-weight-semibold); + } + .sidebar-context { + display: grid; + gap: var(--space-2); + padding: var(--space-5); + border-bottom: 1px solid var(--color-border); + background: var(--color-primary-subtle); + } + .sidebar-context strong { + overflow: hidden; + text-overflow: ellipsis; + } + .sidebar-eyebrow, + .nav-heading { + color: var(--color-text-muted); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-bold); + letter-spacing: 0.06em; + text-transform: uppercase; + } + .nav-heading { + display: block; + padding: var(--space-5) var(--space-5) var(--space-2); + } + .project-navigation { + padding-block: var(--space-2); + } + .sidebar-secondary { + margin-top: var(--space-3); + padding-top: var(--space-3); + border-top: 1px solid var(--color-border); + } + .sidebar-secondary a { + color: var(--color-text-secondary); } .content { min-width: 0; @@ -229,22 +406,55 @@ interface NavItem { padding: var(--space-7); } } + @media (max-width: 40rem) { + .topbar { + gap: var(--space-2); + padding-inline: var(--space-3); + } + .brand { + display: none; + } + .project-tab { + min-width: 8rem; + } + .logout { + display: none; + } + } `, ], }) export class AppShellComponent { private readonly router = inject(Router); + private readonly projectsApi = inject(ApiClientService); + private readonly loadingProjectIds = new Set(); + private readonly projectTabsStorageKey = 'hauspilot:open-projects'; readonly auth = inject(AuthService); readonly notifications = inject(NotificationStore); readonly drawerOpen = signal(false); readonly notificationPanelOpen = signal(false); - readonly title = computed( - () => this.router.routerState.snapshot.root.firstChild?.firstChild?.title ?? 'Dashboard', + readonly projectTabs = signal(this.restoreProjectTabs()); + readonly currentProjectId = signal(null); + readonly currentProjectSection = signal(null); + private readonly pageTitle = signal('Dashboard'); + readonly projectNavigation = projectSections; + readonly currentProjectName = computed( + () => this.projectTabs().find((tab) => tab.id === this.currentProjectId())?.name ?? 'Projekt', ); - readonly nav: NavItem[] = [ + readonly title = computed(() => this.pageTitle()); + readonly breadcrumb = computed(() => { + const projectId = this.currentProjectId(); + if (!projectId) return `Start / ${this.title()}`; + const section = projectSections.find((item) => item.id === this.currentProjectSection()); + return `${this.currentProjectName()} / ${section?.label ?? 'Mitglieder & Zugriff'}`; + }); + readonly workspaceNav: NavItem[] = [ { label: 'Dashboard', path: '/' }, { label: 'Projekte', path: '/projekte', permission: 'projects.use' }, { label: 'Einladungen', path: '/einladungen', permission: 'projects.use' }, + { label: 'Items', path: '/items', permission: 'items.read' }, + ]; + readonly accountNav: NavItem[] = [ { label: 'Profil', path: '/profil' }, { label: 'Sicherheit', path: '/account/security' }, { @@ -253,14 +463,23 @@ export class AppShellComponent { permission: 'notifications.readOwn', }, { label: 'Sessions', path: '/sessions', permission: 'sessions.readOwn' }, - { label: 'Items', path: '/items', permission: 'items.read' }, + ]; + readonly adminNav: NavItem[] = [ { label: 'Admin Benutzer', path: '/admin/users', permission: 'users.read' }, { label: 'Admin Rollen', path: '/admin/roles', permission: 'roles.read' }, { label: 'Admin Audit', path: '/admin/audit', permission: 'audit.read' }, ]; + readonly nav: NavItem[] = [...this.workspaceNav, ...this.accountNav, ...this.adminNav]; constructor() { this.auth.loadMe(); + this.syncProjectContext(this.router.url); + this.router.events + .pipe( + filter((event): event is NavigationEnd => event instanceof NavigationEnd), + takeUntilDestroyed(), + ) + .subscribe((event) => this.syncProjectContext(event.urlAfterRedirects)); effect(() => { if (this.auth.user()) { this.notifications.startPolling(); @@ -275,6 +494,20 @@ export class AppShellComponent { return !item.permission || this.auth.has(item.permission); } + hasVisibleAdminNavigation(): boolean { + return this.adminNav.some((item) => this.visible(item)); + } + + closeProjectTab(event: MouseEvent, projectId: string): void { + event.preventDefault(); + event.stopPropagation(); + const wasCurrent = this.currentProjectId() === projectId; + const remaining = this.projectTabs().filter((tab) => tab.id !== projectId); + this.projectTabs.set(remaining); + this.persistProjectTabs(); + if (wasCurrent) void this.router.navigateByUrl(remaining.at(-1)?.path ?? '/projekte'); + } + loginHref(): string { const returnTo = this.router.url.startsWith('/einladungen') ? this.router.url : '/'; return `/api/auth/login?returnTo=${encodeURIComponent(returnTo)}`; @@ -295,6 +528,80 @@ export class AppShellComponent { this.drawerOpen.set(false); } + private syncProjectContext(url: string): void { + this.pageTitle.set( + this.router.routerState.snapshot.root.firstChild?.firstChild?.title ?? 'Dashboard', + ); + const match = /^\/projekte\/([^/?]+)(?:\/([^/?]+))?/.exec(url); + if (!match?.[1]) { + this.currentProjectId.set(null); + this.currentProjectSection.set(null); + return; + } + const projectId = decodeURIComponent(match[1]); + const section = match[2] ? decodeURIComponent(match[2]) : null; + const path = section + ? `/projekte/${encodeURIComponent(projectId)}/${encodeURIComponent(section)}` + : `/projekte/${encodeURIComponent(projectId)}`; + this.currentProjectId.set(projectId); + this.currentProjectSection.set(section); + const existing = this.projectTabs().find((tab) => tab.id === projectId); + if (existing) { + this.upsertProjectTab({ ...existing, path }); + return; + } + this.upsertProjectTab({ id: projectId, name: 'Projekt wird geladen …', path }); + if (this.loadingProjectIds.has(projectId)) return; + this.loadingProjectIds.add(projectId); + this.projectsApi.project(projectId).subscribe({ + next: (project) => { + const current = this.projectTabs().find((tab) => tab.id === projectId); + if (current) this.upsertProjectTab({ ...current, name: project.name }); + }, + error: () => { + const current = this.projectTabs().find((tab) => tab.id === projectId); + if (current) this.upsertProjectTab({ ...current, name: 'Unbekanntes Projekt' }); + this.loadingProjectIds.delete(projectId); + }, + complete: () => this.loadingProjectIds.delete(projectId), + }); + } + + private upsertProjectTab(tab: ProjectTab): void { + const tabs = this.projectTabs(); + const next = tabs.some((entry) => entry.id === tab.id) + ? tabs.map((entry) => (entry.id === tab.id ? tab : entry)) + : [...tabs, tab].slice(-8); + this.projectTabs.set(next); + this.persistProjectTabs(); + } + + private restoreProjectTabs(): ProjectTab[] { + if (typeof sessionStorage === 'undefined') return []; + try { + const value: unknown = JSON.parse(sessionStorage.getItem(this.projectTabsStorageKey) ?? '[]'); + if (!Array.isArray(value)) return []; + return value.filter((entry: unknown): entry is ProjectTab => this.isProjectTab(entry)); + } catch { + return []; + } + } + + private persistProjectTabs(): void { + if (typeof sessionStorage !== 'undefined') + sessionStorage.setItem(this.projectTabsStorageKey, JSON.stringify(this.projectTabs())); + } + + private isProjectTab(value: unknown): value is ProjectTab { + if (typeof value !== 'object' || value === null) return false; + const candidate = value as Record; + return ( + typeof candidate['id'] === 'string' && + typeof candidate['name'] === 'string' && + typeof candidate['path'] === 'string' + ); + } + @HostListener('document:keydown.escape') closeOverlays(): void { this.drawerOpen.set(false); diff --git a/apps/frontend/src/styles/_layout.scss b/apps/frontend/src/styles/_layout.scss index d363a27..3e9afa7 100644 --- a/apps/frontend/src/styles/_layout.scss +++ b/apps/frontend/src/styles/_layout.scss @@ -108,6 +108,267 @@ border-color: var(--color-danger); } +.planning-guide { + display: grid; + gap: var(--space-6); + padding: var(--space-6); + background: linear-gradient(135deg, var(--color-primary-subtle), var(--color-surface) 55%); + border-color: var(--color-border-strong); +} + +.guide-intro { + display: grid; + gap: var(--space-3); +} + +.concept-flow { + display: grid; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr); + gap: var(--space-3); + align-items: center; + margin-top: var(--space-2); +} + +.concept-flow article { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: var(--space-3); + align-items: center; + min-height: 5rem; + padding: var(--space-4); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); +} + +.concept-flow article > span { + display: grid; + width: 2rem; + height: 2rem; + place-items: center; + color: var(--color-primary); + font-weight: var(--font-weight-bold); + background: var(--color-primary-subtle); + border-radius: var(--radius-pill); +} + +.concept-flow article div, +.next-action > div { + display: grid; + gap: var(--space-1); +} + +.concept-flow small, +.next-action small, +.detail-description { + color: var(--color-text-muted); +} + +.flow-arrow { + color: var(--color-text-muted); + font-size: var(--font-size-xl); +} + +.eyebrow { + color: var(--color-primary); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-bold); + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.guide-steps { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--space-3); + margin: 0; + padding: 0; + list-style: none; +} + +.guide-steps li { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: var(--space-3); + align-items: center; + padding: var(--space-4); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); +} + +.guide-steps li.current { + border-color: var(--color-primary); + box-shadow: 0 0 0 1px var(--color-primary); +} + +.guide-steps li.complete .step-number { + color: var(--color-success); + background: var(--color-success-subtle); +} + +.step-number { + display: grid; + width: 2rem; + height: 2rem; + place-items: center; + color: var(--color-primary); + font-weight: var(--font-weight-bold); + background: var(--color-primary-subtle); + border-radius: var(--radius-pill); +} + +.guide-steps div { + display: grid; + gap: var(--space-1); +} + +.guide-steps small { + color: var(--color-text-muted); +} + +.next-action { + display: flex; + gap: var(--space-5); + align-items: center; + justify-content: space-between; + padding: var(--space-5); + background: var(--color-surface); + border: 1px solid var(--color-primary); + border-radius: var(--radius-lg); +} + +.planning-details { + display: block; + padding: var(--space-4) var(--space-5); +} + +.planning-details summary { + color: var(--color-text-primary); + font-weight: var(--font-weight-semibold); + cursor: pointer; +} + +.planning-details[open] summary { + margin-bottom: var(--space-4); +} + +.planning-details .metric-grid { + margin-top: var(--space-4); +} + +.detail-description { + margin-bottom: var(--space-4); +} + +.option-selection { + min-width: 0; + margin: 0; + padding: 0; + border: 0; +} + +.option-selection legend { + margin-bottom: var(--space-3); + color: var(--color-text-secondary); +} + +.option-choice-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); + gap: var(--space-3); +} + +.option-choice { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: var(--space-3); + align-items: start; + padding: var(--space-4); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + cursor: pointer; + transition: + border-color var(--transition-fast) ease, + background var(--transition-fast) ease; +} + +.option-choice:hover { + border-color: var(--color-border-strong); +} + +.option-choice--selected { + background: var(--color-primary-subtle); + border-color: var(--color-primary); +} + +.option-choice input { + width: 1.25rem; + min-height: auto; + margin-top: var(--space-1); + accent-color: var(--color-primary); +} + +.option-choice__content, +.option-choice__head { + display: grid; + gap: var(--space-3); +} + +.option-choice__head { + grid-template-columns: minmax(0, 1fr) auto; + align-items: start; +} + +.option-facts { + display: grid; + grid-template-columns: minmax(7rem, auto) minmax(0, 1fr); + gap: var(--space-2) var(--space-3); + margin: 0; + font-size: var(--font-size-sm); +} + +.option-facts dt { + color: var(--color-text-muted); +} + +.option-facts dd { + margin: 0; + text-align: right; +} + +@media (max-width: 40rem) { + .planning-guide { + grid-template-columns: 1fr; + padding: var(--space-4); + } + + .guide-steps li { + grid-template-columns: auto minmax(0, 1fr); + } + + .concept-flow, + .guide-steps { + grid-template-columns: 1fr; + } + + .flow-arrow { + justify-self: center; + transform: rotate(90deg); + } + + .next-action { + align-items: stretch; + flex-direction: column; + } + + .next-action .ui-button { + width: 100%; + } +} + @media (min-width: 48rem) { .ui-page-header { grid-template-columns: minmax(0, 1fr) auto;