| @@ -45,6 +45,7 @@ import { NewTaskNoteComponent } from './tasks/new-task-note/new-task-note.compon | |||
| import { DocumentsDetailComponent } from './documents/documents-detail/documents-detail.component'; | |||
| import { SalesComponent } from './sales/sales.component'; | |||
| import { SalesDetailComponent } from './sales/sales-detail/sales-detail.component'; | |||
| import { NewSaleComponent } from './sales/new-sale/new-sale.component'; | |||
| export function apiConfigFactory(): Configuration { | |||
| const params: ConfigurationParameters = { | |||
| @@ -110,7 +111,8 @@ export function HttpLoaderFactory(http: HttpClient) { | |||
| NewTaskNoteComponent, | |||
| DocumentsDetailComponent, | |||
| SalesComponent, | |||
| SalesDetailComponent | |||
| SalesDetailComponent, | |||
| NewSaleComponent | |||
| ], | |||
| providers: [ | |||
| {provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true}, | |||
| @@ -16,7 +16,7 @@ | |||
| <label for="partner" class="form-label">{{ 'form.partner' | translate }}:</label> | |||
| <input type="text" class="form-control" id="partner" [ngbTypeahead]="searchPartners" | |||
| [inputFormatter]="formatter" [value]="documentForm.get('partnerName')?.value" | |||
| [resultFormatter]="formatter" [editable]="false" (selectItem)="onAssignedToSelectPartner($event)"/> | |||
| [resultFormatter]="formatter" [editable]="false" (selectItem)="onPartnerSelect($event)"/> | |||
| <input type="hidden" formControlName="partner"/> | |||
| </div> | |||
| @@ -24,7 +24,7 @@ | |||
| <label for="product" class="form-label">{{ 'form.product' | translate }}:</label> | |||
| <input type="text" class="form-control" id="product" [ngbTypeahead]="searchProducts" | |||
| [inputFormatter]="formatter" [value]="documentForm.get('productName')?.value" | |||
| [resultFormatter]="formatter" [editable]="false" (selectItem)="onAssignedToSelectProduct($event)"/> | |||
| [resultFormatter]="formatter" [editable]="false" (selectItem)="onProductSelect($event)"/> | |||
| <input type="hidden" formControlName="product"/> | |||
| </div> | |||
| @@ -82,11 +82,11 @@ export class NewDocumentComponent implements OnInit { | |||
| ); | |||
| } | |||
| protected onAssignedToSelectPartner(selectedItem: any): void { | |||
| protected onPartnerSelect(selectedItem: any): void { | |||
| this.documentForm.get('partner')?.setValue(selectedItem.item.id); | |||
| } | |||
| protected onAssignedToSelectProduct(selectedItem: any): void { | |||
| protected onProductSelect(selectedItem: any): void { | |||
| this.documentForm.get('product')?.setValue(selectedItem.item.id); | |||
| } | |||
| @@ -0,0 +1,80 @@ | |||
| <h2 *ngIf="!sale.id">{{ 'basic.new-sale' | translate }}</h2> | |||
| <h2 *ngIf="sale.id">{{ 'basic.edit-sale' | translate }}</h2> | |||
| <div class="spt-form"> | |||
| <form [formGroup]="saleForm" (ngSubmit)="onSubmit()"> | |||
| <div class="mb-3"> | |||
| <label for="partner" class="form-label">{{ 'form.partner' | translate }}:</label> | |||
| <input type="text" class="form-control" id="partner" [ngbTypeahead]="searchPartners" | |||
| [inputFormatter]="formatter" [value]="saleForm.get('partnerName')?.value" | |||
| [resultFormatter]="formatter" [editable]="false" (selectItem)="onPartnerSelect($event)"/> | |||
| <input type="hidden" formControlName="partner"/> | |||
| </div> | |||
| <div class="mb-3"> | |||
| <label for="product" class="form-label">{{ 'form.product' | translate }}:</label> | |||
| <input type="text" class="form-control" id="product" [ngbTypeahead]="searchProducts" | |||
| [inputFormatter]="formatter" [value]="saleForm.get('productName')?.value" | |||
| [resultFormatter]="formatter" [editable]="false" (selectItem)="onProductSelect($event)"/> | |||
| <input type="hidden" formControlName="product"/> | |||
| </div> | |||
| <div class="mb-3"> | |||
| <label for="turnover" class="form-label">{{ 'form.turnover' | translate }}:</label> | |||
| <input type="number" class="form-control" id="turnover" formControlName="turnover" min="0" step="1" /> | |||
| <div class="form-text" *ngIf="saleForm.get('turnover')?.invalid && saleForm.get('turnover')?.touched"> | |||
| {{ 'form.turnover' | translate }} {{ 'form.mandatory' | translate }}. | |||
| </div> | |||
| </div> | |||
| <div class="mb-3"> | |||
| <label for="profit" class="form-label">{{ 'form.profit' | translate }}:</label> | |||
| <input type="number" class="form-control" id="profit" formControlName="profit" min="0" step="1" /> | |||
| </div> | |||
| <!-- <div class="mb-3">--> | |||
| <!-- <label for="street" class="form-label">{{ 'form.street' | translate }}:</label>--> | |||
| <!-- <input type="text" class="form-control" id="street" formControlName="street"/>--> | |||
| <!-- </div>--> | |||
| <!-- <div class="mb-3">--> | |||
| <!-- <label for="streetNo" class="form-label">{{ 'form.street-no' | translate }}:</label>--> | |||
| <!-- <input type="text" class="form-control" id="streetNo" formControlName="streetNo"/>--> | |||
| <!-- </div>--> | |||
| <!-- <div class="mb-3">--> | |||
| <!-- <label for="zip" class="form-label">{{ 'form.zip' | translate }}:</label>--> | |||
| <!-- <input type="text" class="form-control" id="zip" formControlName="zip"/>--> | |||
| <!-- </div>--> | |||
| <!-- <div class="mb-3">--> | |||
| <!-- <label for="city" class="form-label">{{ 'form.city' | translate }}:</label>--> | |||
| <!-- <input type="text" class="form-control" id="city" formControlName="city"/>--> | |||
| <!-- </div>--> | |||
| <!-- <div class="mb-3">--> | |||
| <!-- <label for="country" class="form-label">{{ 'form.country' | translate }}:</label>--> | |||
| <!-- <input type="text" class="form-control" id="country" formControlName="country"/>--> | |||
| <!-- </div>--> | |||
| <!-- <div class="mb-3">--> | |||
| <!-- <label for="website" class="form-label">{{ 'form.website' | translate }}:</label>--> | |||
| <!-- <input type="text" class="form-control" id="website" formControlName="website"/>--> | |||
| <!-- </div>--> | |||
| <!-- <div class="mb-3" *ngIf="partnerForm.get('logoUrl')?.value === null">--> | |||
| <!-- <label for="logo" class="form-label">{{ 'form.upload-image' | translate }}:</label>--> | |||
| <!-- <input type="file" class="form-control" id="logo" (change)="onFileSelected($event)" accept="image/*"/>--> | |||
| <!-- </div>--> | |||
| <!-- <div class="mb-3" *ngIf="partnerForm.get('logoUrl')?.value !== null">--> | |||
| <!-- <div class="delete-image" (click)="onDeleteImage()">--> | |||
| <!-- <img src="{{partner.logoUrl}}" width="40" height="40"/>--> | |||
| <!-- <p class="mb-0 ms-3">{{ 'system.delete-image' | translate }}</p>--> | |||
| <!-- </div>--> | |||
| <!-- </div>--> | |||
| <button type="submit" class="btn btn-primary" [disabled]="saleForm.invalid">{{ 'form.send' | translate }} | |||
| </button> | |||
| </form> | |||
| </div> | |||
| @@ -0,0 +1,23 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
| import { NewSaleComponent } from './new-sale.component'; | |||
| describe('NewSaleComponent', () => { | |||
| let component: NewSaleComponent; | |||
| let fixture: ComponentFixture<NewSaleComponent>; | |||
| beforeEach(async () => { | |||
| await TestBed.configureTestingModule({ | |||
| declarations: [NewSaleComponent] | |||
| }) | |||
| .compileComponents(); | |||
| fixture = TestBed.createComponent(NewSaleComponent); | |||
| component = fixture.componentInstance; | |||
| fixture.detectChanges(); | |||
| }); | |||
| it('should create', () => { | |||
| expect(component).toBeTruthy(); | |||
| }); | |||
| }); | |||
| @@ -0,0 +1,126 @@ | |||
| import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; | |||
| import { | |||
| MediaObjectService, | |||
| PartnerJsonld, | |||
| PartnerService, | |||
| ProductService, | |||
| SaleJsonld, | |||
| SaleService | |||
| } from "@app/core/api/v1"; | |||
| import {ModalStatus} from "@app/_helpers/modal.states"; | |||
| import {FormGroup} from "@angular/forms"; | |||
| import {debounceTime, distinctUntilChanged, Observable, OperatorFunction, Subscription, switchMap} from "rxjs"; | |||
| import {TranslateService} from "@ngx-translate/core"; | |||
| import {FormGroupInitializer} from "@app/_helpers/formgroup.initializer"; | |||
| import {saleForm} from "@app/_forms/apiForms"; | |||
| import {ApiConverter} from "@app/_helpers/api.converter"; | |||
| import {filter, map} from "rxjs/operators"; | |||
| @Component({ | |||
| selector: 'app-new-sale', | |||
| templateUrl: './new-sale.component.html', | |||
| styleUrl: './new-sale.component.scss' | |||
| }) | |||
| export class NewSaleComponent implements OnInit { | |||
| @Input() public sale!: SaleJsonld; | |||
| @Output() public submit: EventEmitter<ModalStatus> = new EventEmitter<ModalStatus>(); | |||
| protected saleForm: FormGroup; | |||
| protected saleSub: Subscription; | |||
| protected formatter = (apiData: any) => apiData.name; | |||
| constructor( | |||
| private saleService: SaleService, | |||
| private partnerService: PartnerService, | |||
| private productService: ProductService, | |||
| private translateService: TranslateService | |||
| ) { | |||
| this.saleForm = saleForm; | |||
| this.saleSub = new Subscription(); | |||
| } | |||
| ngOnInit(): void { | |||
| this.saleForm = FormGroupInitializer.initFormGroup(this.saleForm, this.sale); | |||
| } | |||
| protected searchPartners: OperatorFunction<string, readonly { | |||
| id: any; | |||
| name: any | |||
| }[]> = (text$: Observable<string>) => | |||
| text$.pipe( | |||
| debounceTime(200), | |||
| distinctUntilChanged(), | |||
| filter((term) => term.length >= 2), | |||
| switchMap((term) => this.fetchPartners(term)), | |||
| map((partners) => partners.slice(0, 10)), | |||
| ); | |||
| protected searchProducts: OperatorFunction<string, readonly { | |||
| id: any; | |||
| name: any | |||
| }[]> = (text$: Observable<string>) => | |||
| text$.pipe( | |||
| debounceTime(200), | |||
| distinctUntilChanged(), | |||
| filter((term) => term.length >= 2), | |||
| switchMap((term) => this.fetchProducts(term)), | |||
| map((products) => products.slice(0, 10)), | |||
| ); | |||
| protected fetchPartners(term: string): Observable<{ id: any; name: any }[]> { | |||
| return this.partnerService.partnersGetCollection(1, 50, undefined, undefined, term).pipe( | |||
| map((response) => response['hydra:member'].map(partner => ({id: partner.id, name: partner.name}))), | |||
| ); | |||
| } | |||
| protected fetchProducts(term: string): Observable<{ id: any; name: any }[]> { | |||
| return this.productService.productsGetCollection(1, 50, term).pipe( | |||
| map((response) => response['hydra:member'].map(product => ({id: product.id, name: product.name}))), | |||
| ); | |||
| } | |||
| protected onPartnerSelect(selectedItem: any): void { | |||
| this.saleForm.get('partner')?.setValue(selectedItem.item.id); | |||
| } | |||
| protected onProductSelect(selectedItem: any): void { | |||
| this.saleForm.get('product')?.setValue(selectedItem.item.id); | |||
| } | |||
| onSubmit() { | |||
| if (this.saleForm.valid) { | |||
| if (this.saleForm.get('profit')?.value > this.saleForm.get('turnover')?.value) { | |||
| let alertMessage = ""; | |||
| this.translateService.get('system.profit-larger-turnover').subscribe((translation: string) => { | |||
| alertMessage = translation; | |||
| }); | |||
| alert(alertMessage); | |||
| return; | |||
| } | |||
| if (this.sale.id === null || this.sale.id === undefined) { | |||
| // Create new sale | |||
| this.saleSub = this.saleService.salesPost( | |||
| this.saleForm.value as SaleJsonld | |||
| ).subscribe( | |||
| data => { | |||
| this.saleForm.reset(); | |||
| this.submit.emit(ModalStatus.Submitted); | |||
| } | |||
| ); | |||
| } else { | |||
| // Edit sale | |||
| this.saleSub = this.saleService.salesIdPatch( | |||
| ApiConverter.extractId(this.sale.id), | |||
| this.saleForm.value as SaleJsonld | |||
| ).subscribe( | |||
| data => { | |||
| this.saleForm.reset(); | |||
| this.submit.emit(ModalStatus.Submitted); | |||
| } | |||
| ); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -19,6 +19,7 @@ import {ApiConverter} from "@app/_helpers/api.converter"; | |||
| import {Router} from "@angular/router"; | |||
| import {registerLocaleData} from "@angular/common"; | |||
| import localeDe from '@angular/common/locales/de'; | |||
| import {NewSaleComponent} from "@app/sales/new-sale/new-sale.component"; | |||
| registerLocaleData(localeDe); | |||
| @@ -163,13 +164,14 @@ export class SalesComponent implements OnInit { | |||
| } | |||
| openModalNewSale() { | |||
| const modalRefContact = this.modalService.open(NewPartnerComponent, this.modalOptions); | |||
| const modalRefSale = this.modalService.open(NewSaleComponent, this.modalOptions); | |||
| let sale: SaleJsonld = {} as SaleJsonld; | |||
| modalRefContact.componentInstance.partner = sale; | |||
| modalRefContact.componentInstance.submit.subscribe((modalStatus: ModalStatus) => { | |||
| modalRefSale.componentInstance.sale = sale; | |||
| modalRefSale.componentInstance.submit.subscribe((modalStatus: ModalStatus) => { | |||
| if (modalStatus === ModalStatus.Submitted) { | |||
| modalRefContact.dismiss(); | |||
| modalRefSale.dismiss(); | |||
| this.getSalesData(); | |||
| this.getSalesSummaryData(); | |||
| } | |||
| }); | |||
| } | |||
| @@ -4,7 +4,8 @@ | |||
| "delete-image": "Bild löschen", | |||
| "confirm-delete-image": "Möchten Sie das Bild wirklich löschen?", | |||
| "delete-file": "Datei löschen", | |||
| "confirm-delete-file": "Möchten Sie die Datei wirklich löschen?" | |||
| "confirm-delete-file": "Möchten Sie die Datei wirklich löschen?", | |||
| "profit-larger-turnover": "Der Gewinn ist größer als der Umsatz!" | |||
| }, | |||
| "basic": | |||
| { | |||
| @@ -41,6 +42,7 @@ | |||
| "edit-post": "Notiz bearbeiten", | |||
| "edit-comment": "Kommentar bearbeiten", | |||
| "edit-task-note": "Anmerkung bearbeiten", | |||
| "edit-sale": "Verkauf bearbeiten", | |||
| "details": "Details", | |||
| "comment-it": "Kommentieren", | |||
| "show-comments": "Kommentare anzeigen", | |||
| @@ -107,6 +109,8 @@ | |||
| "prio-high": "hoch", | |||
| "partner": "Partner", | |||
| "product": "Produkt", | |||
| "turnover": "Umsatz", | |||
| "profit": "Gewinn", | |||
| "send": "Abschicken" | |||
| }, | |||
| "sales": | |||