mirror of
https://github.com/valitydev/dashboard.git
synced 2024-11-06 10:35:21 +00:00
Epic. Invoices refactoring (#351)
* filters for invoices refactoring (#320) * refunds refactoring * fixes and expanded-id-manager.ts improvements * row width fix * invoices filters * fixes * more fixes * Expanded id manager custom fragment (#319) * more more fixes * more more more fixes * invoices filters * fix after conflict resolving * more fixes * Update build image to build:917afcdd0c0a07bf4155d597bbba72e962e1a34a * try 4.0.3 * 4.2.0 * revert to 4.3.0 * fix * structure improvement * fixes * more fixes * prettier x2 * fix * new constant and alias for it * prettier x2 * fixes * fixes for Rinat Co-authored-by: Ildar Galeev <KeinAsylum@gmail.com> Co-authored-by: Grigory Antsiferov <azr@bakka.su> Co-authored-by: Rinat Arsaev <krickray@gmail.com> * invoices refactoring panels (#338) * table to panels (no details yet) * undelete create invoice button * fixes * invoices refactoring main details (#339) * table to panels (no details yet) * undelete create invoice button * invoice details and cart * fixes * fixes * replay subject 1 * search limit to 10 * fix * invoices refactoring actions (#346) * actions * improvements * fixes * fixes * untilDestroyed * fixes for kisalisa * payments for invoice details (#348) * actions * improvements * payments for invoice details * fixes * fixes * untilDestroyed * fixes * paginator service * fixes for kisalisa * fixes Co-authored-by: Ildar Galeev <KeinAsylum@gmail.com> Co-authored-by: Grigory Antsiferov <azr@bakka.su> Co-authored-by: Rinat Arsaev <krickray@gmail.com>
This commit is contained in:
parent
08852ca70e
commit
e01798ba19
@ -1,8 +1,8 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { Invoice, InvoiceParams, Reason } from '@dsh/api-codegen/capi';
|
||||
import { InvoicesService } from '@dsh/api-codegen/capi/invoices.service';
|
||||
import { Invoice, InvoiceParams } from '@dsh/api-codegen/capi/swagger-codegen';
|
||||
|
||||
import { Replace } from '../../../type-utils';
|
||||
import { genXRequestID } from '../utils';
|
||||
@ -29,4 +29,12 @@ export class InvoiceService {
|
||||
getInvoicePaymentMethods(invoiceID: string) {
|
||||
return this.invoicesService.getInvoicePaymentMethods(genXRequestID(), invoiceID);
|
||||
}
|
||||
|
||||
fulfillInvoice(invoiceID: string, reason: Reason) {
|
||||
return this.invoicesService.fulfillInvoice(genXRequestID(), invoiceID, reason);
|
||||
}
|
||||
|
||||
rescindInvoice(invoiceID: string, reason: Reason) {
|
||||
return this.invoicesService.rescindInvoice(genXRequestID(), invoiceID, reason);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { CaptureParams, PaymentSearchResult, PaymentsService } from '@dsh/api-codegen/capi/swagger-codegen';
|
||||
import { CaptureParams, Payment, PaymentsService } from '@dsh/api-codegen/capi/swagger-codegen';
|
||||
|
||||
import { genXRequestID } from '../utils';
|
||||
|
||||
@ -17,7 +17,11 @@ export class PaymentService {
|
||||
return this.paymentsService.capturePayment(genXRequestID(), invoiceID, paymentID, params);
|
||||
}
|
||||
|
||||
getPaymentByID(invoiceID: string, paymentID: string): Observable<PaymentSearchResult> {
|
||||
getPaymentByID(invoiceID: string, paymentID: string): Observable<Payment> {
|
||||
return this.paymentsService.getPaymentByID(genXRequestID(), invoiceID, paymentID);
|
||||
}
|
||||
|
||||
getPayments(invoiceID: string): Observable<Payment[]> {
|
||||
return this.paymentsService.getPayments(genXRequestID(), invoiceID);
|
||||
}
|
||||
}
|
||||
|
@ -2,5 +2,5 @@ import { InjectionToken } from '@angular/core';
|
||||
|
||||
export const LAYOUT_GAP = new InjectionToken<string>('layoutGap');
|
||||
|
||||
export const DEFAULT_SEARCH_LIMIT = 20;
|
||||
export const DEFAULT_SEARCH_LIMIT = 10;
|
||||
export const SEARCH_LIMIT = new InjectionToken('searchLimit');
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
import { Params } from '@angular/router';
|
||||
import pickBy from 'lodash.pickby';
|
||||
|
||||
import { QueryParamsStore } from '@dsh/app/shared/services';
|
||||
@ -11,10 +11,6 @@ const shopTypeToArray = (v, k) => typeof v === 'string' && k === 'shopIDs';
|
||||
|
||||
@Injectable()
|
||||
export class AnalyticsSearchFiltersStore extends QueryParamsStore<SearchParams> {
|
||||
constructor(protected route: ActivatedRoute, protected router: Router) {
|
||||
super(router, route);
|
||||
}
|
||||
|
||||
mapToData(queryParams: Params): Partial<SearchParams> {
|
||||
return {
|
||||
...queryParams,
|
||||
|
@ -1,12 +1,4 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import { combineLatest, Observable, ReplaySubject, Subject } from 'rxjs';
|
||||
import { distinctUntilChanged, map, scan, shareReplay, switchMap, take } from 'rxjs/operators';
|
||||
@ -15,10 +7,10 @@ import { Shop } from '@dsh/api-codegen/capi';
|
||||
import { ApiShopsService } from '@dsh/api/shop';
|
||||
import { Daterange } from '@dsh/pipes/daterange';
|
||||
|
||||
import { ComponentChanges } from '../../../../../type-utils';
|
||||
import { daterangeFromStr, strToDaterange } from '../../../../shared/utils';
|
||||
import { filterShopsByRealm, removeEmptyProperties } from '../../operations/operators';
|
||||
import { searchFilterParamsToDaterange } from '../../reports/reports-search-filters/search-filter-params-to-daterange';
|
||||
import { SearchParams } from '../search-params';
|
||||
import { daterangeToSearchParams } from './daterange-to-search-params';
|
||||
import { getDefaultDaterange } from './get-default-daterange';
|
||||
import { shopsToCurrencies } from './shops-to-currencies';
|
||||
|
||||
@ -84,19 +76,9 @@ export class AnalyticsSearchFiltersComponent implements OnChanges {
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges({ initParams }: SimpleChanges) {
|
||||
ngOnChanges({ initParams }: ComponentChanges<AnalyticsSearchFiltersComponent>) {
|
||||
if (initParams && initParams.firstChange && initParams.currentValue) {
|
||||
const v = initParams.currentValue;
|
||||
this.daterange = !(v.fromTime || v.toTime) ? getDefaultDaterange() : searchFilterParamsToDaterange(v);
|
||||
this.daterangeSelectionChange(this.daterange);
|
||||
if (v.currency) {
|
||||
this.selectedCurrency$.next(v.currency);
|
||||
} else {
|
||||
this.currencies$.pipe(take(1)).subscribe((currencies) => this.selectedCurrency$.next(currencies[0]));
|
||||
}
|
||||
if (v.shopIDs) {
|
||||
this.selectedShopIDs$.next(v.shopIDs);
|
||||
}
|
||||
this.initSearchParams(initParams.currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,7 +87,7 @@ export class AnalyticsSearchFiltersComponent implements OnChanges {
|
||||
if (v === null) {
|
||||
this.daterange = daterange;
|
||||
}
|
||||
this.searchParams$.next(daterangeToSearchParams(daterange));
|
||||
this.searchParams$.next(daterangeFromStr(daterange));
|
||||
}
|
||||
|
||||
shopSelectionChange(shops: Shop[]) {
|
||||
@ -116,4 +98,17 @@ export class AnalyticsSearchFiltersComponent implements OnChanges {
|
||||
currencySelectionChange(currency: string) {
|
||||
this.selectedCurrency$.next(currency);
|
||||
}
|
||||
|
||||
initSearchParams({ fromTime, toTime, shopIDs, currency }: SearchParams) {
|
||||
this.daterange = fromTime && toTime ? strToDaterange({ fromTime, toTime }) : getDefaultDaterange();
|
||||
this.daterangeSelectionChange(this.daterange);
|
||||
if (currency) {
|
||||
this.selectedCurrency$.next(currency);
|
||||
} else {
|
||||
this.currencies$.pipe(take(1)).subscribe((currencies) => this.selectedCurrency$.next(currencies[0]));
|
||||
}
|
||||
if (shopIDs) {
|
||||
this.selectedShopIDs$.next(shopIDs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
import { Daterange } from '@dsh/pipes/daterange';
|
||||
|
||||
import { SearchParams } from '../search-params';
|
||||
|
||||
export const daterangeToSearchParams = ({ begin, end }: Daterange): Partial<SearchParams> => ({
|
||||
fromTime: begin.utc().format(),
|
||||
toTime: end.utc().format(),
|
||||
});
|
@ -1,10 +0,0 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import { Daterange } from '@dsh/pipes/daterange';
|
||||
|
||||
import { SearchParams } from '../search-params';
|
||||
|
||||
export const searchParamsToDaterange = ({ fromTime, toTime }: SearchParams): Daterange => ({
|
||||
begin: moment(fromTime),
|
||||
end: moment(toTime),
|
||||
});
|
@ -3,9 +3,9 @@ import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable, ReplaySubject } from 'rxjs';
|
||||
import { pluck, shareReplay, take } from 'rxjs/operators';
|
||||
|
||||
import { Shop } from '@dsh/api-codegen/capi/swagger-codegen';
|
||||
import { SpinnerType } from '@dsh/components/indicators';
|
||||
|
||||
import { PaymentInstitutionRealm } from '../../../api/model';
|
||||
import { AnalyticsSearchFiltersStore } from './analytics-search-filters-store.service';
|
||||
import { SearchParams } from './search-params';
|
||||
|
||||
@ -20,7 +20,7 @@ export class AnalyticsComponent {
|
||||
|
||||
initSearchParams$ = this.analyticsSearchFiltersStore.data$.pipe(take(1));
|
||||
|
||||
realm$: Observable<Shop[]> = this.route.params.pipe(pluck('realm'), shareReplay(1));
|
||||
realm$: Observable<PaymentInstitutionRealm> = this.route.params.pipe(pluck('realm'), shareReplay(1));
|
||||
|
||||
constructor(private analyticsSearchFiltersStore: AnalyticsSearchFiltersStore, private route: ActivatedRoute) {}
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
export * from './create-invoice-dialog.component';
|
@ -1,6 +1,6 @@
|
||||
<dsh-create-invoice
|
||||
*transloco="let t"
|
||||
[shops]="data?.shops$ | async"
|
||||
[shops]="shops"
|
||||
(next)="create($event)"
|
||||
(back)="cancel()"
|
||||
[backButton]="t('cancel')"
|
@ -1,10 +1,10 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { Router } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { Invoice, Shop } from '@dsh/api-codegen/capi';
|
||||
|
||||
import { CreateInvoiceDialogResponse } from '../../types/create-invoice-dialog-response';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-create-invoice-dialog',
|
||||
templateUrl: 'create-invoice-dialog.component.html',
|
||||
@ -12,17 +12,15 @@ import { Invoice, Shop } from '@dsh/api-codegen/capi';
|
||||
})
|
||||
export class CreateInvoiceDialogComponent {
|
||||
constructor(
|
||||
private dialogRef: MatDialogRef<CreateInvoiceDialogComponent, 'cancel' | 'create'>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: { shops$: Observable<Shop[]> },
|
||||
private router: Router
|
||||
private dialogRef: MatDialogRef<CreateInvoiceDialogComponent, CreateInvoiceDialogResponse>,
|
||||
@Inject(MAT_DIALOG_DATA) public shops: Shop[]
|
||||
) {}
|
||||
|
||||
cancel() {
|
||||
this.dialogRef.close('cancel');
|
||||
}
|
||||
|
||||
create({ id }: Invoice) {
|
||||
this.dialogRef.close('create');
|
||||
this.router.navigate(['invoice', id]);
|
||||
create(invoice: Invoice) {
|
||||
this.dialogRef.close(invoice);
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { CreateInvoiceModule as FormCreateInvoiceModule } from '../../../../create-invoice';
|
||||
import { CreateInvoiceDialogComponent } from './components/create-invoice-dialog/create-invoice-dialog.component';
|
||||
import { CreateInvoiceService } from './create-invoice.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, TranslocoModule, FormCreateInvoiceModule],
|
||||
declarations: [CreateInvoiceDialogComponent],
|
||||
providers: [CreateInvoiceService],
|
||||
})
|
||||
export class CreateInvoiceModule {}
|
@ -0,0 +1,55 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { TranslocoService } from '@ngneat/transloco';
|
||||
import { Observable, of, ReplaySubject } from 'rxjs';
|
||||
import { filter, pluck, switchMap, take } from 'rxjs/operators';
|
||||
|
||||
import { Shop } from '@dsh/api-codegen/capi';
|
||||
import { PaymentInstitutionRealm } from '@dsh/api/model';
|
||||
import { ApiShopsService } from '@dsh/api/shop';
|
||||
|
||||
import { filterShopsByRealm } from '../../operators';
|
||||
import { CreateInvoiceDialogComponent } from './components/create-invoice-dialog/create-invoice-dialog.component';
|
||||
|
||||
@Injectable()
|
||||
export class CreateInvoiceService {
|
||||
constructor(
|
||||
private apiShopsService: ApiShopsService,
|
||||
private dialog: MatDialog,
|
||||
private transloco: TranslocoService,
|
||||
private snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
createInvoice(realm: PaymentInstitutionRealm): Observable<string> {
|
||||
const invoiceCreated$ = new ReplaySubject<string>(1);
|
||||
of(realm)
|
||||
.pipe(
|
||||
filterShopsByRealm(this.apiShopsService.shops$),
|
||||
switchMap((shops) =>
|
||||
this.dialog
|
||||
.open<CreateInvoiceDialogComponent, Shop[]>(CreateInvoiceDialogComponent, {
|
||||
width: '720px',
|
||||
maxHeight: '90vh',
|
||||
disableClose: true,
|
||||
data: shops,
|
||||
})
|
||||
.afterClosed()
|
||||
),
|
||||
take(1),
|
||||
filter((res) => res !== 'cancel'),
|
||||
pluck('id')
|
||||
)
|
||||
.subscribe((id) => {
|
||||
invoiceCreated$.next(id);
|
||||
this.snackBar.open(
|
||||
this.transloco.translate('invoices.actions.invoiceCreated', null, 'operations'),
|
||||
'OK',
|
||||
{
|
||||
duration: 2000,
|
||||
}
|
||||
);
|
||||
});
|
||||
return invoiceCreated$;
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export * from './create-invoice.module';
|
||||
export * from './create-invoice.service';
|
@ -0,0 +1,3 @@
|
||||
import { Invoice } from '@dsh/api-codegen/capi';
|
||||
|
||||
export type CreateInvoiceDialogResponse = Invoice | 'cancel';
|
@ -0,0 +1,13 @@
|
||||
<dsh-row
|
||||
*transloco="let t; scope: 'operations'; read: 'operations.invoices.headerRow'"
|
||||
fxLayout="row"
|
||||
fxLayoutAlign="space-between center"
|
||||
fxLayoutGap="24px"
|
||||
color="primary"
|
||||
>
|
||||
<dsh-row-header-label fxFlex="40">{{ t('productName') }}</dsh-row-header-label>
|
||||
<dsh-row-header-label fxFlex="10">{{ t('amount') }}</dsh-row-header-label>
|
||||
<dsh-row-header-label fxFlex="10">{{ t('status') }}</dsh-row-header-label>
|
||||
<dsh-row-header-label fxFlex="20">{{ t('createdAt') }}</dsh-row-header-label>
|
||||
<dsh-row-header-label fxFlex="20" fxHide.lt-md> {{ t('shop') }} </dsh-row-header-label>
|
||||
</dsh-row>
|
@ -0,0 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-invoice-row-header',
|
||||
templateUrl: 'invoice-row-header.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InvoiceRowHeaderComponent {}
|
@ -0,0 +1,15 @@
|
||||
<dsh-row fxLayout="row" fxLayoutAlign="space-between center" fxLayoutGap="24px">
|
||||
<dsh-row-label fxFlex="40">
|
||||
{{ invoice.product }}
|
||||
</dsh-row-label>
|
||||
<dsh-row-label fxFlex="10">
|
||||
<span class="dsh-body-2">{{ invoice.amount | toMajor | currency: invoice.currency }}</span>
|
||||
</dsh-row-label>
|
||||
<dsh-row-label fxFlex="10">
|
||||
<dsh-status [color]="invoice.status | invoiceStatusColor">{{ invoice.status | invoiceStatusName }}</dsh-status>
|
||||
</dsh-row-label>
|
||||
<dsh-row-label fxFlex="20">{{ invoice.createdAt | date: 'dd MMMM yyyy, HH:mm' }}</dsh-row-label>
|
||||
<dsh-row-label fxFlex="20" fxHide.lt-md>
|
||||
{{ invoice.shopID | shopDetails }}
|
||||
</dsh-row-label>
|
||||
</dsh-row>
|
@ -0,0 +1,12 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
|
||||
import { Invoice } from '../../../../../../../api-codegen/anapi';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-invoice-row',
|
||||
templateUrl: 'invoice-row.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InvoiceRowComponent {
|
||||
@Input() invoice: Invoice;
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export * from './invoices-list.component';
|
||||
export * from './invoices-list.module';
|
@ -0,0 +1,31 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexModule } from '@angular/flex-layout';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { ButtonModule } from '@dsh/components/buttons';
|
||||
|
||||
import { CreateInvoiceModule as FormCreateInvoiceModule } from '../../../../../../create-invoice';
|
||||
import { CreatePaymentLinkModule as ApiCreatePaymentLinkModule } from '../../../../../../create-payment-link';
|
||||
import { CancelInvoiceDialogComponent } from './components/cancel-invoice-dialog/cancel-invoice-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
TranslocoModule,
|
||||
FormCreateInvoiceModule,
|
||||
ApiCreatePaymentLinkModule,
|
||||
FlexModule,
|
||||
ReactiveFormsModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatDialogModule,
|
||||
ButtonModule,
|
||||
],
|
||||
declarations: [CancelInvoiceDialogComponent],
|
||||
})
|
||||
export class CancelInvoiceModule {}
|
@ -0,0 +1,44 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { TranslocoService } from '@ngneat/transloco';
|
||||
import { Observable, ReplaySubject } from 'rxjs';
|
||||
import { filter, switchMap, take } from 'rxjs/operators';
|
||||
|
||||
import { InvoiceService } from '@dsh/api/invoice';
|
||||
|
||||
import { CancelInvoiceDialogComponent } from './components/cancel-invoice-dialog/cancel-invoice-dialog.component';
|
||||
|
||||
@Injectable()
|
||||
export class CancelInvoiceService {
|
||||
constructor(
|
||||
private invoiceService: InvoiceService,
|
||||
private dialog: MatDialog,
|
||||
private snackBar: MatSnackBar,
|
||||
private transloco: TranslocoService
|
||||
) {}
|
||||
|
||||
cancelInvoice(invoiceID: string): Observable<void> {
|
||||
const invoiceCancelled$ = new ReplaySubject<void>(1);
|
||||
this.dialog
|
||||
.open(CancelInvoiceDialogComponent, {
|
||||
width: '720px',
|
||||
maxHeight: '90vh',
|
||||
disableClose: true,
|
||||
})
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
take(1),
|
||||
filter((value) => value !== 'cancel'),
|
||||
switchMap((reason) => this.invoiceService.rescindInvoice(invoiceID, reason))
|
||||
)
|
||||
.subscribe(() => {
|
||||
invoiceCancelled$.next();
|
||||
this.snackBar.open(
|
||||
this.transloco.translate('invoices.actions.invoiceCancelled', null, 'operations'),
|
||||
'OK'
|
||||
);
|
||||
});
|
||||
return invoiceCancelled$;
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
<div fxLayout="column" fxLayoutGap="32px" *transloco="let t; scope: 'operations'; read: 'operations.invoices.actions'">
|
||||
<div class="dsh-headline">{{ t('cancelInvoice') }}</div>
|
||||
<mat-dialog-content>
|
||||
<mat-form-field fxFlex>
|
||||
<mat-label>{{ t('reason') }}</mat-label>
|
||||
<input [formControl]="reason" required matInput type="text" autocomplete="off" />
|
||||
</mat-form-field>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions fxLayout="row" fxLayoutAlign="space-between center" *transloco="let c">
|
||||
<button fxFlex="126px" dsh-stroked-button color="warn" (click)="cancel()">{{ c('cancel') }}</button>
|
||||
<button class="action-button" dsh-button color="accent" [disabled]="!reason.valid" (click)="accept()">
|
||||
{{ c('confirm') }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
@ -0,0 +1,3 @@
|
||||
mat-dialog-actions {
|
||||
padding-bottom: 24px;
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { MatDialogRef } from '@angular/material/dialog';
|
||||
import { FormControl } from '@ngneat/reactive-forms';
|
||||
|
||||
import { CancelInvoiceDialogResponse } from '../../types/cancel-invoice-dialog-response';
|
||||
|
||||
@Component({
|
||||
templateUrl: 'cancel-invoice-dialog.component.html',
|
||||
styleUrls: ['cancel-invoice-dialog.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class CancelInvoiceDialogComponent {
|
||||
constructor(private dialogRef: MatDialogRef<CancelInvoiceDialogComponent, CancelInvoiceDialogResponse>) {}
|
||||
|
||||
reason = new FormControl<string>();
|
||||
|
||||
cancel() {
|
||||
this.dialogRef.close('cancel');
|
||||
}
|
||||
|
||||
accept() {
|
||||
this.dialogRef.close({ reason: this.reason.value });
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export * from './cancel-invoice.module';
|
||||
export * from './cancel-invoice.service';
|
@ -0,0 +1,3 @@
|
||||
import { Reason } from '@dsh/api-codegen/capi';
|
||||
|
||||
export type CancelInvoiceDialogResponse = Reason | 'cancel';
|
@ -0,0 +1,14 @@
|
||||
<div fxLayout="column" fxLayoutGap="24px" *transloco="let t; scope: 'operations'; read: 'operations.invoices.actions'">
|
||||
<div class="mat-title">
|
||||
{{ t('title') }}
|
||||
</div>
|
||||
<div fxLayout fxLayoutGap="24px">
|
||||
<ng-container *ngIf="invoice.status === 'unpaid'">
|
||||
<button dsh-button color="accent" (click)="createPaymentLink()">{{ t('createPaymentLink') }}</button>
|
||||
<button dsh-button color="warn" (click)="cancelInvoice()">{{ t('cancelInvoice') }}</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="invoice.status === 'paid'">
|
||||
<button dsh-button color="warn" (click)="fulfillInvoice()">{{ t('fulfillInvoice') }}</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,44 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
|
||||
import { Invoice } from '../../../../../../../../api-codegen/anapi';
|
||||
import { CancelInvoiceService } from '../../cancel-invoice';
|
||||
import { CreatePaymentLinkService } from '../../create-payment-link';
|
||||
import { FulfillInvoiceService } from '../../fulfill-invoice';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'dsh-invoice-actions',
|
||||
templateUrl: 'invoice-actions.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [CancelInvoiceService, FulfillInvoiceService],
|
||||
})
|
||||
export class InvoiceActionsComponent {
|
||||
@Input() invoice: Invoice;
|
||||
|
||||
@Output() refreshData = new EventEmitter<void>();
|
||||
|
||||
constructor(
|
||||
private createPaymentLinkService: CreatePaymentLinkService,
|
||||
private fulfillInvoiceService: FulfillInvoiceService,
|
||||
private cancelInvoiceService: CancelInvoiceService
|
||||
) {}
|
||||
|
||||
createPaymentLink() {
|
||||
this.createPaymentLinkService.createPaymentLink({ invoice: this.invoice });
|
||||
}
|
||||
|
||||
cancelInvoice() {
|
||||
this.cancelInvoiceService
|
||||
.cancelInvoice(this.invoice.id)
|
||||
.pipe(untilDestroyed(this))
|
||||
.subscribe(() => this.refreshData.emit());
|
||||
}
|
||||
|
||||
fulfillInvoice() {
|
||||
this.fulfillInvoiceService
|
||||
.fulfillInvoice(this.invoice.id)
|
||||
.pipe(untilDestroyed(this))
|
||||
.subscribe(() => this.refreshData.emit());
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
<div fxLayout="column" fxLayoutGap="24px" *transloco="let t; scope: 'operations'; read: 'operations.invoices.cartInfo'">
|
||||
<dsh-details-item [title]="t('description')" fxFlex>
|
||||
{{ line.product }}
|
||||
</dsh-details-item>
|
||||
<div fxLayout fxLayout.xs="column" fxLayoutGap="24px">
|
||||
<dsh-details-item [title]="t('price')" fxFlex>
|
||||
{{ line.price | toMajor | currency: currency }}
|
||||
</dsh-details-item>
|
||||
<dsh-details-item [title]="t('quantity')" fxFlex>
|
||||
{{ line.quantity }}
|
||||
</dsh-details-item>
|
||||
<dsh-details-item *ngIf="line.taxMode; else emptyBlock" [title]="t('tax')" fxFlex>
|
||||
{{ line.taxMode | taxModeToTaxRate }}
|
||||
</dsh-details-item>
|
||||
<ng-template #emptyBlock>
|
||||
<div fxFlex></div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,15 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
|
||||
import { InvoiceLine } from '../../../../../../../../../api-codegen/anapi';
|
||||
import { ReceiveInvoiceService } from '../../../services/receive-invoice/receive-invoice.service';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-invoice-cart-line',
|
||||
templateUrl: 'invoice-cart-line.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [ReceiveInvoiceService],
|
||||
})
|
||||
export class InvoiceCartLineComponent {
|
||||
@Input() line: InvoiceLine;
|
||||
@Input() currency: string;
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
<div fxLayout="column" fxLayoutGap="24px" *transloco="let t; scope: 'operations'; read: 'operations.invoices.cartInfo'">
|
||||
<div class="mat-title">
|
||||
{{ t('title') }}
|
||||
</div>
|
||||
<ng-container *ngFor="let line of cart; index as i">
|
||||
<dsh-invoice-cart-line [line]="line" [currency]="currency"></dsh-invoice-cart-line>
|
||||
<mat-divider *ngIf="i < cart.length - 1"></mat-divider>
|
||||
</ng-container>
|
||||
</div>
|
@ -0,0 +1,15 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
|
||||
import { InvoiceCart } from '../../../../../../../../api-codegen/anapi';
|
||||
import { ReceiveInvoiceService } from '../../services/receive-invoice/receive-invoice.service';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-invoice-cart-info',
|
||||
templateUrl: 'invoice-cart-info.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [ReceiveInvoiceService],
|
||||
})
|
||||
export class InvoiceCartInfoComponent {
|
||||
@Input() cart: InvoiceCart;
|
||||
@Input() currency: string;
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
<div fxLayout="column" fxLayoutGap="24px" *transloco="let t; scope: 'operations'; read: 'operations.invoices'">
|
||||
<dsh-invoice-details [invoice]="invoice"></dsh-invoice-details>
|
||||
<dsh-details-item [title]="t('shop')">
|
||||
{{ invoice.shopID | shopDetails }}
|
||||
</dsh-details-item>
|
||||
</div>
|
@ -0,0 +1,12 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
|
||||
import { Invoice } from '../../../../../../../../api-codegen/anapi';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-invoice-main-info',
|
||||
templateUrl: 'invoice-main-info.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InvoiceMainInfoComponent {
|
||||
@Input() invoice: Invoice;
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
<div fxLayout="column" fxLayoutGap="24px" *transloco="let t; scope: 'operations'; read: 'operations.invoices.payments'">
|
||||
<div class="mat-title">
|
||||
{{ t('title') }}
|
||||
</div>
|
||||
<div *ngFor="let payment of payments$ | async as payments; index as i" fxLayout="column" fxLayoutGap="24px">
|
||||
<div class="dsh-subheading-2">{{ t('payment') }} #{{ payment.id }}</div>
|
||||
<dsh-payment-details [payment]="payment"></dsh-payment-details>
|
||||
<mat-divider *ngIf="i < payments.length - 1"></mat-divider>
|
||||
</div>
|
||||
<div *ngIf="hasMore$ | async">
|
||||
<button dsh-button fxFlex (click)="showMore()">{{ t('showMore') }}</button>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,27 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
|
||||
import { Payment } from '@dsh/api-codegen/capi';
|
||||
import { FakePaginatorService } from '@dsh/app/shared/services';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-invoice-payments',
|
||||
templateUrl: 'invoice-payments.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [FakePaginatorService],
|
||||
})
|
||||
export class InvoicePaymentsComponent implements OnInit {
|
||||
@Input() payments: Payment[];
|
||||
|
||||
payments$ = this.paginationService.values$;
|
||||
hasMore$ = this.paginationService.hasMore$;
|
||||
|
||||
constructor(private paginationService: FakePaginatorService<Payment>) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.paginationService.init(this.payments);
|
||||
}
|
||||
|
||||
showMore() {
|
||||
this.paginationService.showMore();
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
<dsh-create-payment-link cancelButton (cancel)="cancel()" [invoice]="data.invoice"></dsh-create-payment-link>
|
@ -0,0 +1,20 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
|
||||
import { Invoice } from '../../../../../../../../../api-codegen/capi';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-create-payment-link-dialog',
|
||||
templateUrl: 'create-payment-link-dialog.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class CreatePaymentLinkDialogComponent {
|
||||
constructor(
|
||||
private dialogRef: MatDialogRef<CreatePaymentLinkDialogComponent, 'cancel'>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: { invoice: Invoice }
|
||||
) {}
|
||||
|
||||
cancel() {
|
||||
this.dialogRef.close('cancel');
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { CreateInvoiceModule as FormCreateInvoiceModule } from '../../../../../../create-invoice';
|
||||
import { CreatePaymentLinkModule as ApiCreatePaymentLinkModule } from '../../../../../../create-payment-link';
|
||||
import { CreatePaymentLinkDialogComponent } from './components/create-payment-link-dialog/create-payment-link-dialog.component';
|
||||
import { CreatePaymentLinkService } from './create-payment-link.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, TranslocoModule, FormCreateInvoiceModule, ApiCreatePaymentLinkModule],
|
||||
declarations: [CreatePaymentLinkDialogComponent],
|
||||
providers: [CreatePaymentLinkService],
|
||||
})
|
||||
export class CreatePaymentLinkModule {}
|
@ -0,0 +1,22 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
|
||||
import { CreatePaymentLinkDialogComponent } from './components/create-payment-link-dialog/create-payment-link-dialog.component';
|
||||
import { CreatePaymentLinkDialogConfig } from './types/create-payment-link-dialog-config';
|
||||
|
||||
@Injectable()
|
||||
export class CreatePaymentLinkService {
|
||||
constructor(private dialog: MatDialog) {}
|
||||
|
||||
createPaymentLink(config: CreatePaymentLinkDialogConfig): void {
|
||||
this.dialog.open<CreatePaymentLinkDialogComponent, CreatePaymentLinkDialogConfig>(
|
||||
CreatePaymentLinkDialogComponent,
|
||||
{
|
||||
width: '720px',
|
||||
maxHeight: '90vh',
|
||||
disableClose: true,
|
||||
data: config,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export * from './create-payment-link.module';
|
||||
export * from './create-payment-link.service';
|
@ -0,0 +1,5 @@
|
||||
import { Invoice } from '../../../../../../../../api-codegen/capi';
|
||||
|
||||
export interface CreatePaymentLinkDialogConfig {
|
||||
invoice: Invoice;
|
||||
}
|
@ -0,0 +1 @@
|
||||
export type CreatePaymentLinkDialogResponse = 'create' | 'cancel';
|
@ -0,0 +1,15 @@
|
||||
<div fxLayout="column" fxLayoutGap="32px" *transloco="let t; scope: 'operations'; read: 'operations.invoices.actions'">
|
||||
<div class="dsh-headline">{{ t('fulfillInvoice') }}</div>
|
||||
<mat-dialog-content>
|
||||
<mat-form-field fxFlex>
|
||||
<mat-label>{{ t('reason') }}</mat-label>
|
||||
<input [formControl]="reason" required matInput type="text" autocomplete="off" />
|
||||
</mat-form-field>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions fxLayout="row" fxLayoutAlign="space-between center" *transloco="let c">
|
||||
<button fxFlex="126px" dsh-stroked-button color="warn" (click)="cancel()">{{ c('cancel') }}</button>
|
||||
<button class="action-button" dsh-button color="accent" [disabled]="!reason.valid" (click)="accept()">
|
||||
{{ c('confirm') }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
@ -0,0 +1,3 @@
|
||||
mat-dialog-actions {
|
||||
padding-bottom: 24px;
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { MatDialogRef } from '@angular/material/dialog';
|
||||
import { FormControl } from '@ngneat/reactive-forms';
|
||||
|
||||
import { FulfillInvoiceDialogResponse } from '../../types/fulfill-invoice-dialog-response';
|
||||
|
||||
@Component({
|
||||
templateUrl: 'fulfill-invoice-dialog.component.html',
|
||||
styleUrls: ['fulfill-invoice-dialog.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FulfillInvoiceDialogComponent {
|
||||
constructor(private dialogRef: MatDialogRef<FulfillInvoiceDialogComponent, FulfillInvoiceDialogResponse>) {}
|
||||
|
||||
reason = new FormControl<string>();
|
||||
|
||||
cancel() {
|
||||
this.dialogRef.close('cancel');
|
||||
}
|
||||
|
||||
accept() {
|
||||
this.dialogRef.close({ reason: this.reason.value });
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexModule } from '@angular/flex-layout';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { ButtonModule } from '@dsh/components/buttons';
|
||||
|
||||
import { CreateInvoiceModule as FormCreateInvoiceModule } from '../../../../../../create-invoice';
|
||||
import { CreatePaymentLinkModule as ApiCreatePaymentLinkModule } from '../../../../../../create-payment-link';
|
||||
import { FulfillInvoiceDialogComponent } from './components/cancel-invoice-dialog/fulfill-invoice-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
TranslocoModule,
|
||||
FormCreateInvoiceModule,
|
||||
ApiCreatePaymentLinkModule,
|
||||
FlexModule,
|
||||
ReactiveFormsModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatDialogModule,
|
||||
ButtonModule,
|
||||
],
|
||||
declarations: [FulfillInvoiceDialogComponent],
|
||||
})
|
||||
export class FulfillInvoiceModule {}
|
@ -0,0 +1,44 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { TranslocoService } from '@ngneat/transloco';
|
||||
import { Observable, ReplaySubject } from 'rxjs';
|
||||
import { filter, switchMap, take } from 'rxjs/operators';
|
||||
|
||||
import { InvoiceService } from '@dsh/api/invoice';
|
||||
|
||||
import { FulfillInvoiceDialogComponent } from './components/cancel-invoice-dialog/fulfill-invoice-dialog.component';
|
||||
|
||||
@Injectable()
|
||||
export class FulfillInvoiceService {
|
||||
constructor(
|
||||
private invoiceService: InvoiceService,
|
||||
private dialog: MatDialog,
|
||||
private snackBar: MatSnackBar,
|
||||
private transloco: TranslocoService
|
||||
) {}
|
||||
|
||||
fulfillInvoice(invoiceID: string): Observable<void> {
|
||||
const invoiceFulfilled$ = new ReplaySubject<void>(1);
|
||||
this.dialog
|
||||
.open(FulfillInvoiceDialogComponent, {
|
||||
width: '720px',
|
||||
maxHeight: '90vh',
|
||||
disableClose: true,
|
||||
})
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
take(1),
|
||||
filter((value) => value !== 'cancel'),
|
||||
switchMap((reason) => this.invoiceService.fulfillInvoice(invoiceID, reason))
|
||||
)
|
||||
.subscribe(() => {
|
||||
invoiceFulfilled$.next();
|
||||
this.snackBar.open(
|
||||
this.transloco.translate('invoices.actions.invoiceFulfilled', null, 'operations'),
|
||||
'OK'
|
||||
);
|
||||
});
|
||||
return invoiceFulfilled$;
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export * from './fulfill-invoice.module';
|
||||
export * from './fulfill-invoice.service';
|
@ -0,0 +1,3 @@
|
||||
import { Reason } from '@dsh/api-codegen/capi';
|
||||
|
||||
export type FulfillInvoiceDialogResponse = Reason | 'cancel';
|
@ -0,0 +1,6 @@
|
||||
import { Reason } from '@dsh/api-codegen/capi';
|
||||
|
||||
export interface FulfillInvoiceParams {
|
||||
invoiceID: string;
|
||||
reason: Reason;
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './invoice-details.module';
|
@ -0,0 +1,15 @@
|
||||
<div fxLayout="column" fxLayoutGap="24px">
|
||||
<dsh-invoice-main-info [invoice]="invoice"></dsh-invoice-main-info>
|
||||
<ng-container *ngIf="isCartAvailable(invoice.cart)">
|
||||
<mat-divider></mat-divider>
|
||||
<dsh-invoice-cart-info [cart]="invoice.cart" [currency]="invoice.currency"></dsh-invoice-cart-info>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="isActionsAvailable(invoice?.status)">
|
||||
<mat-divider></mat-divider>
|
||||
<dsh-invoice-actions [invoice]="invoice" (refreshData)="refreshData.emit()"></dsh-invoice-actions>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="(payments$ | async)?.length">
|
||||
<mat-divider></mat-divider>
|
||||
<dsh-invoice-payments [payments]="payments$ | async"></dsh-invoice-payments>
|
||||
</ng-container>
|
||||
</div>
|
@ -0,0 +1,32 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
|
||||
import { Invoice, InvoiceCart } from '@dsh/api-codegen/anapi';
|
||||
|
||||
import { ReceivePaymentsService } from './services/receive-payments/receive-payments.service';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-invoice-invoice-details',
|
||||
templateUrl: 'invoice-details.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [ReceivePaymentsService],
|
||||
})
|
||||
export class InvoiceDetailsComponent implements OnInit {
|
||||
@Input() invoice: Invoice;
|
||||
@Output() refreshData = new EventEmitter<void>();
|
||||
|
||||
payments$ = this.receivePaymentsService.payments$;
|
||||
|
||||
constructor(private receivePaymentsService: ReceivePaymentsService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.receivePaymentsService.receivePayments(this.invoice.id);
|
||||
}
|
||||
|
||||
isActionsAvailable(status: Invoice.StatusEnum): boolean {
|
||||
return ['paid', 'unpaid'].includes(status);
|
||||
}
|
||||
|
||||
isCartAvailable(cart: InvoiceCart): boolean {
|
||||
return Boolean(cart?.length);
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { PaymentModule } from '@dsh/api/payment';
|
||||
import {
|
||||
InvoiceDetailsModule as InvoiceInvoiceDetailsModule,
|
||||
PaymentDetailsModule,
|
||||
RefundDetailsModule as ApiRefundDetailsModule,
|
||||
} from '@dsh/app/shared/components';
|
||||
import { ApiModelRefsModule } from '@dsh/app/shared/pipes';
|
||||
import { ButtonModule } from '@dsh/components/buttons';
|
||||
import { IndicatorsModule } from '@dsh/components/indicators';
|
||||
import { LayoutModule } from '@dsh/components/layout';
|
||||
|
||||
import { ToMajorModule } from '../../../../../../to-major';
|
||||
import { CancelInvoiceModule } from './cancel-invoice';
|
||||
import { InvoiceActionsComponent } from './components/invoice-actions/invoice-actions.component';
|
||||
import { InvoiceCartLineComponent } from './components/invoice-cart-info/cart-info/invoice-cart-line.component';
|
||||
import { InvoiceCartInfoComponent } from './components/invoice-cart-info/invoice-cart-info.component';
|
||||
import { InvoiceMainInfoComponent } from './components/invoice-main-info/invoice-main-info.component';
|
||||
import { InvoicePaymentsComponent } from './components/invoice-payments/invoice-payments.component';
|
||||
import { CreatePaymentLinkModule } from './create-payment-link';
|
||||
import { FulfillInvoiceModule } from './fulfill-invoice';
|
||||
import { InvoiceDetailsComponent } from './invoice-details.component';
|
||||
import { TaxModeToTaxRatePipe } from './pipes/tax-mode-to-tax-rate/tax-mode-to-tax-rate.pipe';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
TranslocoModule,
|
||||
LayoutModule,
|
||||
ButtonModule,
|
||||
FlexLayoutModule,
|
||||
CommonModule,
|
||||
MatSnackBarModule,
|
||||
MatDividerModule,
|
||||
IndicatorsModule,
|
||||
ApiModelRefsModule,
|
||||
ApiRefundDetailsModule,
|
||||
PaymentDetailsModule,
|
||||
PaymentModule,
|
||||
InvoiceInvoiceDetailsModule,
|
||||
ToMajorModule,
|
||||
CreatePaymentLinkModule,
|
||||
CancelInvoiceModule,
|
||||
FulfillInvoiceModule,
|
||||
],
|
||||
declarations: [
|
||||
InvoiceDetailsComponent,
|
||||
InvoiceMainInfoComponent,
|
||||
InvoiceCartInfoComponent,
|
||||
InvoiceCartLineComponent,
|
||||
TaxModeToTaxRatePipe,
|
||||
InvoiceActionsComponent,
|
||||
InvoicePaymentsComponent,
|
||||
],
|
||||
exports: [InvoiceDetailsComponent],
|
||||
})
|
||||
export class InvoiceDetailsModule {}
|
@ -0,0 +1,15 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
import { InvoiceLineTaxMode, InvoiceLineTaxVAT } from '../../../../../../../../api-codegen/anapi';
|
||||
|
||||
@Pipe({
|
||||
name: 'taxModeToTaxRate',
|
||||
})
|
||||
export class TaxModeToTaxRatePipe implements PipeTransform {
|
||||
transform(taxMode: InvoiceLineTaxMode): string {
|
||||
switch (taxMode.type) {
|
||||
case InvoiceLineTaxMode.TypeEnum.InvoiceLineTaxVAT:
|
||||
return (taxMode as InvoiceLineTaxVAT).rate;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { BehaviorSubject, Observable, of, ReplaySubject, Subject } from 'rxjs';
|
||||
import { catchError, filter, map, switchMap, tap } from 'rxjs/operators';
|
||||
|
||||
import { Invoice } from '@dsh/api-codegen/capi';
|
||||
import { InvoiceService } from '@dsh/api/invoice';
|
||||
|
||||
@UntilDestroy()
|
||||
@Injectable()
|
||||
export class ReceiveInvoiceService {
|
||||
isLoading$: Observable<boolean>;
|
||||
errorOccurred$: Observable<boolean>;
|
||||
invoice$: Observable<Invoice>;
|
||||
|
||||
private receiveInvoice$ = new Subject<string>();
|
||||
private loading$ = new BehaviorSubject(false);
|
||||
private error$ = new Subject<boolean>();
|
||||
private receivedInvoice$ = new ReplaySubject<Invoice>(1);
|
||||
|
||||
constructor(private invoiceService: InvoiceService) {
|
||||
this.isLoading$ = this.loading$.asObservable();
|
||||
this.errorOccurred$ = this.error$.asObservable();
|
||||
this.invoice$ = this.receivedInvoice$.asObservable();
|
||||
this.receiveInvoice$
|
||||
.pipe(
|
||||
tap(() => this.loading$.next(true)),
|
||||
switchMap((invoiceID: string) =>
|
||||
this.invoiceService.getInvoiceByID(invoiceID).pipe(
|
||||
catchError((e) => {
|
||||
console.error(e);
|
||||
this.loading$.next(false);
|
||||
this.error$.next();
|
||||
return of('error');
|
||||
})
|
||||
)
|
||||
),
|
||||
filter((result) => result !== 'error'),
|
||||
map((r) => r as Invoice),
|
||||
untilDestroyed(this)
|
||||
)
|
||||
.subscribe((invoice: Invoice) => {
|
||||
this.loading$.next(false);
|
||||
this.receivedInvoice$.next(invoice);
|
||||
});
|
||||
}
|
||||
|
||||
receivePayment(invoiceID: string) {
|
||||
this.receiveInvoice$.next(invoiceID);
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import moment from 'moment';
|
||||
import { BehaviorSubject, Observable, of, ReplaySubject, Subject } from 'rxjs';
|
||||
import { catchError, filter, map, switchMap, tap } from 'rxjs/operators';
|
||||
|
||||
import { Payment } from '@dsh/api-codegen/capi';
|
||||
import { PaymentService } from '@dsh/api/payment';
|
||||
|
||||
@UntilDestroy()
|
||||
@Injectable()
|
||||
export class ReceivePaymentsService {
|
||||
isLoading$: Observable<boolean>;
|
||||
errorOccurred$: Observable<boolean>;
|
||||
payments$: Observable<Payment[]>;
|
||||
|
||||
private receivePayments$ = new Subject<string>();
|
||||
private loading$ = new BehaviorSubject(false);
|
||||
private error$ = new Subject<boolean>();
|
||||
private receivedPayments$ = new ReplaySubject<Payment[]>(1);
|
||||
|
||||
constructor(private paymentService: PaymentService) {
|
||||
this.isLoading$ = this.loading$.asObservable();
|
||||
this.errorOccurred$ = this.error$.asObservable();
|
||||
this.payments$ = this.receivedPayments$.asObservable();
|
||||
this.receivePayments$
|
||||
.pipe(
|
||||
tap(() => this.loading$.next(true)),
|
||||
switchMap((invoiceID) =>
|
||||
this.paymentService.getPayments(invoiceID).pipe(
|
||||
catchError((e) => {
|
||||
console.error(e);
|
||||
this.loading$.next(false);
|
||||
this.error$.next();
|
||||
return of('error');
|
||||
})
|
||||
)
|
||||
),
|
||||
filter((result) => result !== 'error'),
|
||||
map((r) => r as Payment[]),
|
||||
map((payments) =>
|
||||
payments.sort((a, b) => moment(b.createdAt).valueOf() - moment(a.createdAt).valueOf())
|
||||
),
|
||||
untilDestroyed(this)
|
||||
)
|
||||
.subscribe((payments: Payment[]) => {
|
||||
this.loading$.next(false);
|
||||
this.receivedPayments$.next(payments);
|
||||
});
|
||||
}
|
||||
|
||||
receivePayments(invoiceID: string) {
|
||||
this.receivePayments$.next(invoiceID);
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
<div fxLayout="column" fxLayoutGap="16px">
|
||||
<dsh-last-updated [lastUpdated]="lastUpdated" (update)="refreshData.emit()"></dsh-last-updated>
|
||||
<dsh-invoice-row-header></dsh-invoice-row-header>
|
||||
<dsh-accordion
|
||||
fxLayout="column"
|
||||
fxLayoutGap="16px"
|
||||
(expandedChange)="expandedIdChange.emit($event)"
|
||||
[expanded]="expandedId"
|
||||
>
|
||||
<dsh-accordion-item *ngFor="let invoice of invoices" #accordionItem>
|
||||
<dsh-invoice-row [invoice]="invoice"></dsh-invoice-row>
|
||||
<ng-template dshLazyPanelContent>
|
||||
<dsh-card fxFlexFill fxLayout="column" fxLayoutGap="32px">
|
||||
<dsh-accordion-item-content-header
|
||||
*transloco="let t; scope: 'operations'; read: 'operations.invoices'"
|
||||
(collapse)="accordionItem.collapse($event)"
|
||||
>{{ t('header') }} #{{ invoice.id }}</dsh-accordion-item-content-header
|
||||
>
|
||||
<dsh-invoice-invoice-details
|
||||
[invoice]="invoice"
|
||||
(refreshData)="refreshData.emit()"
|
||||
></dsh-invoice-invoice-details>
|
||||
</dsh-card>
|
||||
</ng-template>
|
||||
</dsh-accordion-item>
|
||||
</dsh-accordion>
|
||||
</div>
|
@ -0,0 +1,15 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
|
||||
import { Invoice } from '../../../../../api-codegen/anapi';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-invoices-list',
|
||||
templateUrl: 'invoices-list.component.html',
|
||||
})
|
||||
export class InvoicesListComponent {
|
||||
@Input() invoices: Invoice[];
|
||||
@Input() expandedId: number;
|
||||
@Input() lastUpdated: string;
|
||||
@Output() expandedIdChange = new EventEmitter<number>();
|
||||
@Output() refreshData = new EventEmitter<void>();
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { InvoiceModule } from '@dsh/api/invoice';
|
||||
import { InvoiceDetailsModule as ApiInvoiceDetailsModule } from '@dsh/app/shared/components';
|
||||
import { ApiModelRefsModule } from '@dsh/app/shared/pipes';
|
||||
import { IndicatorsModule } from '@dsh/components/indicators';
|
||||
import { LayoutModule } from '@dsh/components/layout';
|
||||
|
||||
import { ToMajorModule } from '../../../../../to-major';
|
||||
import { InvoiceRowHeaderComponent } from './components/invoice-row-header/invoice-row-header.component';
|
||||
import { InvoiceRowComponent } from './components/invoice-row/invoice-row.component';
|
||||
import { InvoiceDetailsModule } from './invoice-details';
|
||||
import { InvoicesListComponent } from './invoices-list.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
TranslocoModule,
|
||||
MatSnackBarModule,
|
||||
LayoutModule,
|
||||
FlexLayoutModule,
|
||||
CommonModule,
|
||||
IndicatorsModule,
|
||||
ToMajorModule,
|
||||
ApiModelRefsModule,
|
||||
InvoiceModule,
|
||||
InvoiceDetailsModule,
|
||||
InvoiceDetailsModule,
|
||||
ApiInvoiceDetailsModule,
|
||||
],
|
||||
declarations: [InvoicesListComponent, InvoiceRowHeaderComponent, InvoiceRowComponent],
|
||||
exports: [InvoicesListComponent],
|
||||
})
|
||||
export class InvoicesListModule {}
|
@ -0,0 +1,8 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import { Daterange } from '@dsh/pipes/daterange';
|
||||
|
||||
export const getDefaultDaterange = (): Daterange => ({
|
||||
begin: moment().startOf('M'),
|
||||
end: moment().endOf('M'),
|
||||
});
|
@ -0,0 +1,3 @@
|
||||
export * from './invoices-search-filters.module';
|
||||
export * from './invoices-search-filters.component';
|
||||
export * from './search-filters-params';
|
@ -0,0 +1,19 @@
|
||||
<div fxLayout="row" fxLayoutGap="16px">
|
||||
<dsh-daterange-filter
|
||||
[selected]="daterange"
|
||||
(selectedChange)="daterangeSelectionChange($event)"
|
||||
></dsh-daterange-filter>
|
||||
<dsh-invoices-filter
|
||||
[selected]="initParams?.invoiceIDs"
|
||||
(selectedChange)="invoiceSelectionChange($event)"
|
||||
></dsh-invoices-filter>
|
||||
<dsh-filter-shops
|
||||
[shops]="shops$ | async"
|
||||
[selected]="selectedShops$ | async"
|
||||
(selectedChange)="shopSelectionChange($event)"
|
||||
></dsh-filter-shops>
|
||||
<dsh-invoice-status-filter
|
||||
[selected]="initParams?.invoiceStatus"
|
||||
(selectedChange)="statusSelectionChange($event)"
|
||||
></dsh-invoice-status-filter>
|
||||
</div>
|
@ -0,0 +1,103 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import isNil from 'lodash.isnil';
|
||||
import { Observable, ReplaySubject, Subject } from 'rxjs';
|
||||
import { distinctUntilChanged, map, scan, shareReplay, switchMap, take } from 'rxjs/operators';
|
||||
|
||||
import { Daterange } from '@dsh/pipes/daterange';
|
||||
|
||||
import { ComponentChanges } from '../../../../../../type-utils';
|
||||
import { Invoice } from '../../../../../api-codegen/anapi/swagger-codegen';
|
||||
import { Shop } from '../../../../../api-codegen/capi/swagger-codegen';
|
||||
import { PaymentInstitutionRealm } from '../../../../../api/model';
|
||||
import { ApiShopsService } from '../../../../../api/shop';
|
||||
import { SHARE_REPLAY_CONF } from '../../../../../custom-operators';
|
||||
import { daterangeFromStr, strToDaterange } from '../../../../../shared/utils';
|
||||
import { filterShopsByRealm } from '../../operators';
|
||||
import { getDefaultDaterange } from './get-default-daterange';
|
||||
import { SearchFiltersParams } from './search-filters-params';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-invoices-search-filters',
|
||||
templateUrl: 'invoices-search-filters.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InvoicesSearchFiltersComponent implements OnChanges, OnInit {
|
||||
@Input() initParams: SearchFiltersParams;
|
||||
|
||||
@Input() set realm(realm: PaymentInstitutionRealm) {
|
||||
this.realm$.next(realm);
|
||||
}
|
||||
|
||||
@Output() searchParamsChanges: EventEmitter<SearchFiltersParams> = new EventEmitter();
|
||||
|
||||
daterange: Daterange;
|
||||
|
||||
shops$: Observable<Shop[]>;
|
||||
|
||||
selectedShops$: Observable<Shop[]>;
|
||||
|
||||
private realm$ = new ReplaySubject<PaymentInstitutionRealm>();
|
||||
private selectedShopIDs$ = new ReplaySubject<string[]>(1);
|
||||
private searchParams$: Subject<Partial<SearchFiltersParams>> = new ReplaySubject(1);
|
||||
|
||||
constructor(private shopService: ApiShopsService) {
|
||||
this.shops$ = this.realm$.pipe(filterShopsByRealm(this.shopService.shops$), shareReplay(SHARE_REPLAY_CONF));
|
||||
this.selectedShops$ = this.selectedShopIDs$.pipe(
|
||||
switchMap((ids) =>
|
||||
this.shops$.pipe(
|
||||
take(1),
|
||||
map((shops) => shops.filter((shop) => ids.includes(shop.id)))
|
||||
)
|
||||
),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.selectedShopIDs$
|
||||
.pipe(map((ids) => (ids.length ? ids : null)))
|
||||
.subscribe((shopIDs) => this.searchParams$.next({ shopIDs }));
|
||||
this.searchParams$
|
||||
.pipe(
|
||||
distinctUntilChanged(isEqual),
|
||||
scan((acc, current) => ({ ...acc, ...current }), this.initParams)
|
||||
)
|
||||
.subscribe((v) => this.searchParamsChanges.emit(v));
|
||||
}
|
||||
|
||||
ngOnChanges({ initParams }: ComponentChanges<InvoicesSearchFiltersComponent>) {
|
||||
if (initParams && initParams.firstChange && initParams.currentValue) {
|
||||
this.initSearchParams(initParams.currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
daterangeSelectionChange(range: Daterange | null) {
|
||||
const daterange = isNil(range) ? getDefaultDaterange() : range;
|
||||
if (isNil(range)) {
|
||||
this.daterange = daterange;
|
||||
}
|
||||
this.searchParams$.next(daterangeFromStr(daterange));
|
||||
}
|
||||
|
||||
shopSelectionChange(shops: Shop[]) {
|
||||
const shopIDs = shops.map((shop) => shop.id);
|
||||
this.selectedShopIDs$.next(shopIDs);
|
||||
}
|
||||
|
||||
invoiceSelectionChange(invoiceIDs: string[]) {
|
||||
this.searchParams$.next({ invoiceIDs: invoiceIDs?.length ? invoiceIDs : null });
|
||||
}
|
||||
|
||||
statusSelectionChange(invoiceStatus: Invoice.StatusEnum) {
|
||||
this.searchParams$.next({ invoiceStatus });
|
||||
}
|
||||
|
||||
private initSearchParams({ fromTime, toTime, shopIDs }: SearchFiltersParams) {
|
||||
this.daterange = fromTime && toTime ? strToDaterange({ fromTime, toTime }) : getDefaultDaterange();
|
||||
this.daterangeSelectionChange(this.daterange);
|
||||
if (shopIDs) {
|
||||
this.selectedShopIDs$.next(shopIDs);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import {
|
||||
FilterShopsModule,
|
||||
InvoicesFilterModule,
|
||||
InvoiceStatusFilterModule,
|
||||
RefundStatusFilterModule,
|
||||
} from '@dsh/app/shared/components';
|
||||
import { DaterangeFilterModule } from '@dsh/components/filters/daterange-filter';
|
||||
|
||||
import { InvoicesSearchFiltersComponent } from './invoices-search-filters.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
TranslocoModule,
|
||||
FlexLayoutModule,
|
||||
InvoicesFilterModule,
|
||||
FilterShopsModule,
|
||||
RefundStatusFilterModule,
|
||||
InvoiceStatusFilterModule,
|
||||
DaterangeFilterModule,
|
||||
],
|
||||
declarations: [InvoicesSearchFiltersComponent],
|
||||
exports: [InvoicesSearchFiltersComponent],
|
||||
})
|
||||
export class InvoicesSearchFiltersModule {}
|
@ -0,0 +1,9 @@
|
||||
import { Invoice } from '../../../../../api-codegen/anapi/swagger-codegen';
|
||||
|
||||
export interface SearchFiltersParams {
|
||||
fromTime: string;
|
||||
toTime: string;
|
||||
invoiceIDs: string[];
|
||||
shopIDs?: string[];
|
||||
invoiceStatus?: Invoice.StatusEnum;
|
||||
}
|
@ -1,23 +1,31 @@
|
||||
<div fxLayout="column" fxLayoutGap="20px" *transloco="let i; scope: 'invoices'; read: 'invoices'">
|
||||
<div fxLayout="column" fxLayoutAlign=" end">
|
||||
<button dsh-stroked-button color="accent" (click)="create()">{{ i('create.button') }}</button>
|
||||
</div>
|
||||
<dsh-search-form (formValueChanges)="search($event)"></dsh-search-form>
|
||||
<dsh-card>
|
||||
<dsh-card-content *transloco="let t" fxLayout="column" fxLayoutGap="15px">
|
||||
<dsh-last-updated
|
||||
*ngIf="!(isLoading$ | async)"
|
||||
(update)="refresh()"
|
||||
[lastUpdated]="lastUpdated$ | async"
|
||||
></dsh-last-updated>
|
||||
<dsh-spinner *ngIf="isLoading$ | async" [type]="spinnerType" size="20"></dsh-spinner>
|
||||
<dsh-invoices-table [data]="tableData$ | async"></dsh-invoices-table>
|
||||
<dsh-empty-search-result
|
||||
*ngIf="!(tableData$ | async)?.length && !(doAction$ | async)"
|
||||
></dsh-empty-search-result>
|
||||
<button *ngIf="hasMoreInvoices$ | async" (click)="fetchMore()" dsh-button>
|
||||
{{ (isLoading$ | async) ? t('loading') : t('showMore') }}
|
||||
<div fxLayout="column" fxLayoutGap="32px">
|
||||
<div fxLayout fxLayoutAlign="space-between" fxLayoutGap="24px">
|
||||
<dsh-invoices-search-filters
|
||||
[initParams]="initSearchParams$ | async"
|
||||
[realm]="realm$ | async"
|
||||
(searchParamsChanges)="searchParamsChanges($event)"
|
||||
></dsh-invoices-search-filters>
|
||||
<button *transloco="let i; scope: 'invoices'; read: 'invoices'" dsh-button color="accent" (click)="create()">
|
||||
{{ i('create.button') }}
|
||||
</button>
|
||||
</dsh-card-content>
|
||||
</dsh-card>
|
||||
</div>
|
||||
<div fxLayout="column" fxLayoutGap="16px">
|
||||
<dsh-invoices-list
|
||||
[expandedId]="expandedId$ | async"
|
||||
(expandedIdChange)="expandedIdChange($event)"
|
||||
[invoices]="invoices$ | async"
|
||||
[lastUpdated]="lastUpdated$ | async"
|
||||
(refreshData)="refresh()"
|
||||
></dsh-invoices-list>
|
||||
<dsh-show-more-panel
|
||||
*ngIf="hasMore$ | async"
|
||||
[isLoading]="isLoading$ | async"
|
||||
(showMore)="fetchMore()"
|
||||
></dsh-show-more-panel>
|
||||
</div>
|
||||
<dsh-empty-search-result
|
||||
*ngIf="!(fetchErrors$ | async) && (invoices$ | async)?.length === 0"
|
||||
></dsh-empty-search-result>
|
||||
<dsh-spinner *ngIf="isLoading$ | async" fxLayoutAlign="center"></dsh-spinner>
|
||||
</div>
|
||||
<dsh-scroll-up></dsh-scroll-up>
|
||||
|
@ -1,40 +1,61 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TranslocoService } from '@ngneat/transloco';
|
||||
import { shareReplay } from 'rxjs/operators';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { Observable } from 'rxjs';
|
||||
import { pluck, shareReplay, take } from 'rxjs/operators';
|
||||
|
||||
import { PaymentInstitutionRealm } from '@dsh/api/model';
|
||||
import { SpinnerType } from '@dsh/components/indicators';
|
||||
|
||||
import { booleanDebounceTime, SHARE_REPLAY_CONF } from '../../../../custom-operators';
|
||||
import { CreateInvoiceDialogComponent } from './create-invoice-dialog';
|
||||
import { InvoicesService } from './invoices.service';
|
||||
import { InvoiceSearchFormValue } from './search-form';
|
||||
import { CreateInvoiceService } from './create-invoice';
|
||||
import { SearchFiltersParams } from './invoices-search-filters';
|
||||
import { FetchInvoicesService } from './services/fetch-invoices/fetch-invoices.service';
|
||||
import { InvoicesExpandedIdManager } from './services/invoices-expanded-id-manager/invoices-expanded-id-manager.service';
|
||||
import { InvoicesSearchFiltersStore } from './services/invoices-search-filters-store/invoices-search-filters-store.service';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'dsh-invoices',
|
||||
templateUrl: 'invoices.component.html',
|
||||
providers: [InvoicesService],
|
||||
providers: [FetchInvoicesService, InvoicesSearchFiltersStore, InvoicesExpandedIdManager],
|
||||
})
|
||||
export class InvoicesComponent {
|
||||
tableData$ = this.invoicesService.invoicesTableData$;
|
||||
hasMoreInvoices$ = this.invoicesService.hasMore$;
|
||||
export class InvoicesComponent implements OnInit {
|
||||
invoices$ = this.invoicesService.searchResult$;
|
||||
hasMore$ = this.invoicesService.hasMore$;
|
||||
lastUpdated$ = this.invoicesService.lastUpdated$;
|
||||
doAction$ = this.invoicesService.doAction$;
|
||||
isLoading$ = this.doAction$.pipe(booleanDebounceTime(), shareReplay(SHARE_REPLAY_CONF));
|
||||
isLoading$ = this.invoicesService.isLoading$;
|
||||
expandedId$ = this.invoicesExpandedIdManager.expandedId$;
|
||||
initSearchParams$ = this.invoicesSearchFiltersStore.data$.pipe(take(1));
|
||||
fetchErrors$ = this.invoicesService.errors$;
|
||||
spinnerType = SpinnerType.FulfillingBouncingCircle;
|
||||
|
||||
realm$: Observable<PaymentInstitutionRealm> = this.route.params.pipe(pluck('realm'), shareReplay(1));
|
||||
|
||||
constructor(
|
||||
private invoicesService: InvoicesService,
|
||||
private invoicesService: FetchInvoicesService,
|
||||
private createInvoiceService: CreateInvoiceService,
|
||||
private invoicesSearchFiltersStore: InvoicesSearchFiltersStore,
|
||||
private invoicesExpandedIdManager: InvoicesExpandedIdManager,
|
||||
private snackBar: MatSnackBar,
|
||||
private transloco: TranslocoService,
|
||||
private dialog: MatDialog
|
||||
) {
|
||||
this.invoicesService.errors$.subscribe(() => this.snackBar.open(this.transloco.translate('commonError'), 'OK'));
|
||||
private route: ActivatedRoute
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.invoicesService.errors$
|
||||
.pipe(untilDestroyed(this))
|
||||
.subscribe(() => this.snackBar.open(this.transloco.translate('commonError'), 'OK'));
|
||||
}
|
||||
|
||||
search(val: InvoiceSearchFormValue) {
|
||||
this.invoicesService.search(val);
|
||||
searchParamsChanges(p: SearchFiltersParams) {
|
||||
this.invoicesService.search(p);
|
||||
this.invoicesSearchFiltersStore.preserve(p);
|
||||
}
|
||||
|
||||
expandedIdChange(id: number) {
|
||||
this.invoicesExpandedIdManager.expandedIdChange(id);
|
||||
}
|
||||
|
||||
fetchMore() {
|
||||
@ -46,13 +67,19 @@ export class InvoicesComponent {
|
||||
}
|
||||
|
||||
create() {
|
||||
this.dialog.open(CreateInvoiceDialogComponent, {
|
||||
width: '720px',
|
||||
maxHeight: '90vh',
|
||||
disableClose: true,
|
||||
data: {
|
||||
shops$: this.invoicesService.shops$,
|
||||
},
|
||||
});
|
||||
this.route.params.pipe(pluck('realm'), take(1)).subscribe((realm: PaymentInstitutionRealm) =>
|
||||
this.createInvoiceService
|
||||
.createInvoice(realm)
|
||||
.pipe(untilDestroyed(this))
|
||||
.subscribe((invoiceID) => this.refreshAndShowNewInvoice(invoiceID))
|
||||
);
|
||||
}
|
||||
|
||||
refreshAndShowNewInvoice(invoiceID: string) {
|
||||
this.refresh();
|
||||
// TODO: open created invoice panel
|
||||
// this.invoices$.pipe(take(1), map(invoices => invoices.findIndex((invoice) => invoice.id === invoiceID))).subscribe((id) => {
|
||||
// this.expandedIdChange(id)
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
@ -14,24 +14,24 @@ import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { TranslocoModule, TRANSLOCO_SCOPE } from '@ngneat/transloco';
|
||||
|
||||
import { InvoiceModule } from '@dsh/api/invoice';
|
||||
import { InvoiceDetailsModule } from '@dsh/app/shared/components';
|
||||
import { ButtonModule } from '@dsh/components/buttons';
|
||||
import { EmptySearchResultModule } from '@dsh/components/empty-search-result';
|
||||
import { FormControlsModule, RangeDatepickerModule } from '@dsh/components/form-controls';
|
||||
import { IndicatorsModule } from '@dsh/components/indicators';
|
||||
import { LayoutModule } from '@dsh/components/layout';
|
||||
import { StateNavModule } from '@dsh/components/navigation';
|
||||
import { ScrollUpModule, StateNavModule } from '@dsh/components/navigation';
|
||||
import { ShowMorePanelModule } from '@dsh/components/show-more-panel';
|
||||
import { TableModule } from '@dsh/components/table';
|
||||
|
||||
import { LanguageModule } from '../../../../language';
|
||||
import { ToMajorModule } from '../../../../to-major';
|
||||
import { CreateInvoiceModule } from '../../../create-invoice';
|
||||
import { ShopSelectorModule } from '../../../shop-selector';
|
||||
import { CreateInvoiceDialogComponent } from './create-invoice-dialog';
|
||||
import { CreateInvoiceModule } from './create-invoice';
|
||||
import { InvoicesListModule } from './invoices-list';
|
||||
import { InvoicesRoutingModule } from './invoices-routing.module';
|
||||
import { InvoicesSearchFiltersModule } from './invoices-search-filters';
|
||||
import { InvoicesComponent } from './invoices.component';
|
||||
import { SearchFormComponent } from './search-form';
|
||||
import { InvoiceStatusColorPipe } from './status-color.pipe';
|
||||
import { TableComponent } from './table';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@ -62,14 +62,13 @@ import { TableComponent } from './table';
|
||||
MatDividerModule,
|
||||
ShopSelectorModule,
|
||||
CreateInvoiceModule,
|
||||
InvoicesSearchFiltersModule,
|
||||
InvoiceDetailsModule,
|
||||
InvoicesListModule,
|
||||
ScrollUpModule,
|
||||
ShowMorePanelModule,
|
||||
],
|
||||
declarations: [
|
||||
InvoicesComponent,
|
||||
SearchFormComponent,
|
||||
InvoiceStatusColorPipe,
|
||||
TableComponent,
|
||||
CreateInvoiceDialogComponent,
|
||||
],
|
||||
declarations: [InvoicesComponent],
|
||||
providers: [{ provide: TRANSLOCO_SCOPE, useValue: 'main' }],
|
||||
})
|
||||
export class InvoicesModule {}
|
||||
|
@ -1,70 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TranslocoService } from '@ngneat/transloco';
|
||||
import { combineLatest, Observable } from 'rxjs';
|
||||
import { catchError, pluck, shareReplay, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { Invoice } from '@dsh/api-codegen/anapi';
|
||||
import { Shop } from '@dsh/api-codegen/capi';
|
||||
import { InvoiceSearchService } from '@dsh/api/search';
|
||||
import { ApiShopsService } from '@dsh/api/shop';
|
||||
|
||||
import { SHARE_REPLAY_CONF } from '../../../../custom-operators';
|
||||
import { FetchResult, PartialFetcher } from '../../../partial-fetcher';
|
||||
import { filterShopsByRealm, mapToTimestamp } from '../operators';
|
||||
import { mapToInvoicesTableData } from './map-to-invoices-table-data';
|
||||
import { InvoiceSearchFormValue } from './search-form';
|
||||
import { InvoicesTableData } from './table';
|
||||
|
||||
@Injectable()
|
||||
export class InvoicesService extends PartialFetcher<Invoice, InvoiceSearchFormValue> {
|
||||
private readonly searchLimit = 20;
|
||||
|
||||
lastUpdated$: Observable<string> = this.searchResult$.pipe(mapToTimestamp);
|
||||
|
||||
invoicesTableData$: Observable<InvoicesTableData[]> = combineLatest([
|
||||
this.searchResult$,
|
||||
this.shopService.shops$,
|
||||
]).pipe(
|
||||
mapToInvoicesTableData,
|
||||
catchError(() => {
|
||||
this.snackBar.open(this.transloco.translate('httpError'), 'OK');
|
||||
return [];
|
||||
})
|
||||
);
|
||||
|
||||
shops$: Observable<Shop[]> = this.route.params.pipe(
|
||||
pluck('realm'),
|
||||
filterShopsByRealm(this.shopService.shops$),
|
||||
shareReplay(SHARE_REPLAY_CONF)
|
||||
);
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private invoiceSearchService: InvoiceSearchService,
|
||||
private shopService: ApiShopsService,
|
||||
private snackBar: MatSnackBar,
|
||||
private transloco: TranslocoService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected fetch(params: InvoiceSearchFormValue, continuationToken: string): Observable<FetchResult<Invoice>> {
|
||||
return this.route.params.pipe(
|
||||
pluck('realm'),
|
||||
switchMap((paymentInstitutionRealm) =>
|
||||
this.invoiceSearchService.searchInvoices(
|
||||
params.date.begin.utc().format(),
|
||||
params.date.end.utc().format(),
|
||||
{
|
||||
...params,
|
||||
paymentInstitutionRealm,
|
||||
},
|
||||
this.searchLimit,
|
||||
continuationToken
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from './invoice-search-form-value';
|
||||
export * from './search-form.component';
|
@ -1,30 +0,0 @@
|
||||
import {
|
||||
BankCardPaymentSystem,
|
||||
BankCardTokenProvider,
|
||||
Invoice,
|
||||
PaymentStatus,
|
||||
PaymentTerminalProvider,
|
||||
} from '@dsh/api-codegen/anapi/swagger-codegen';
|
||||
|
||||
import { SearchFormValue } from '../../../search-form-value';
|
||||
|
||||
export interface InvoiceSearchFormValue extends SearchFormValue {
|
||||
invoiceStatus?: Invoice.StatusEnum;
|
||||
invoiceAmount?: number;
|
||||
shopIDs?: string[];
|
||||
paymentStatus?: PaymentStatus.StatusEnum;
|
||||
paymentFlow?: 'instant' | 'hold';
|
||||
paymentMethod?: 'bankCard' | 'paymentTerminal';
|
||||
paymentTerminalProvider?: PaymentTerminalProvider;
|
||||
invoiceID?: string;
|
||||
paymentID?: string;
|
||||
payerEmail?: string;
|
||||
payerIP?: string;
|
||||
payerFingerprint?: string;
|
||||
customerID?: string;
|
||||
first6?: string;
|
||||
last4?: string;
|
||||
bankCardTokenProvider?: BankCardTokenProvider;
|
||||
bankCardPaymentSystem?: BankCardPaymentSystem;
|
||||
paymentAmount?: number;
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
<dsh-float-panel
|
||||
*transloco="let t; scope: 'operations'; read: 'operations.invoices.filter'"
|
||||
[formGroup]="searchForm"
|
||||
novalidate
|
||||
[(expanded)]="expanded"
|
||||
>
|
||||
<ng-container *transloco="let c">
|
||||
<dsh-justify-wrapper fxLayout fxLayout.lt-md="column" [fxLayoutGap]="layoutGap">
|
||||
<dsh-range-datepicker formControlName="date" fxFlex></dsh-range-datepicker>
|
||||
<dsh-shop-selector formControlName="shopIDs" fxFlex></dsh-shop-selector>
|
||||
<mat-form-field fxFlex>
|
||||
<mat-label>{{ t('invoiceStatus') }}</mat-label>
|
||||
<mat-select formControlName="invoiceStatus">
|
||||
<mat-option>
|
||||
{{ c('any') }}
|
||||
</mat-option>
|
||||
<mat-option *ngFor="let status of statuses" [value]="status">
|
||||
<span *transloco="let invoiceStatus; read: 'invoiceStatus'">{{ invoiceStatus(status) }}</span>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</dsh-justify-wrapper>
|
||||
<dsh-float-panel-actions>
|
||||
<button dsh-button (click)="reset()">
|
||||
{{ c('resetSearchParams') }}
|
||||
</button>
|
||||
</dsh-float-panel-actions>
|
||||
<dsh-float-panel-more>
|
||||
<div gdAuto gdGap="0 {{ layoutGap }}" gdColumns="1fr 1fr 1fr" gdColumns.lt-md="1fr">
|
||||
<mat-form-field>
|
||||
<mat-label>{{ t('invoiceID') }}</mat-label>
|
||||
<input matInput formControlName="invoiceID" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</dsh-float-panel-more>
|
||||
</ng-container>
|
||||
</dsh-float-panel>
|
@ -1,36 +0,0 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
|
||||
import { Invoice } from '@dsh/api-codegen/anapi/swagger-codegen';
|
||||
|
||||
import { InvoiceSearchFormValue } from './invoice-search-form-value';
|
||||
import { SearchFormService } from './search-form.service';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-search-form',
|
||||
templateUrl: 'search-form.component.html',
|
||||
providers: [SearchFormService],
|
||||
})
|
||||
export class SearchFormComponent implements OnInit {
|
||||
@Input() valueDebounceTime = 300;
|
||||
@Input() layoutGap = '20px';
|
||||
|
||||
@Output() formValueChanges: EventEmitter<InvoiceSearchFormValue> = new EventEmitter<InvoiceSearchFormValue>();
|
||||
|
||||
searchForm: FormGroup = this.searchFormService.searchForm;
|
||||
expanded = false;
|
||||
statuses: Invoice.StatusEnum[] = Object.values(Invoice.StatusEnum);
|
||||
|
||||
constructor(private searchFormService: SearchFormService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.searchFormService.formValueChanges$
|
||||
.pipe(debounceTime(this.valueDebounceTime))
|
||||
.subscribe((v) => this.formValueChanges.emit(v));
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.searchFormService.reset();
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import * as moment from 'moment';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, shareReplay, startWith, take } from 'rxjs/operators';
|
||||
|
||||
import { removeEmptyProperties } from '../../operators';
|
||||
import { toFormValue } from '../../to-form-value';
|
||||
import { toQueryParams } from '../../to-query-params';
|
||||
import { InvoiceSearchFormValue } from './invoice-search-form-value';
|
||||
|
||||
@Injectable()
|
||||
export class SearchFormService {
|
||||
searchForm: FormGroup = this.initForm();
|
||||
private defaultValues: InvoiceSearchFormValue = this.searchForm.value;
|
||||
formValueChanges$: Observable<InvoiceSearchFormValue> = this.searchForm.valueChanges.pipe(
|
||||
startWith(this.defaultValues),
|
||||
filter(() => this.searchForm.status === 'VALID'),
|
||||
removeEmptyProperties,
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
constructor(private fb: FormBuilder, private router: Router, private route: ActivatedRoute) {
|
||||
this.formValueChanges$.subscribe((formValues) =>
|
||||
this.router.navigate([location.pathname], { queryParams: toQueryParams(formValues) })
|
||||
);
|
||||
this.defaultValues = this.searchForm.value;
|
||||
this.init();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.searchForm.reset(this.defaultValues);
|
||||
}
|
||||
|
||||
private init() {
|
||||
this.route.queryParams
|
||||
.pipe(
|
||||
take(1),
|
||||
filter((p) => !isEmpty(p))
|
||||
)
|
||||
.subscribe((p) => this.searchForm.patchValue(toFormValue<InvoiceSearchFormValue>(p)));
|
||||
}
|
||||
|
||||
private initForm(defaultLimit = 20): FormGroup {
|
||||
return this.fb.group({
|
||||
date: {
|
||||
begin: moment().startOf('month'),
|
||||
end: moment().endOf('month'),
|
||||
},
|
||||
limit: [defaultLimit, Validators.required],
|
||||
invoiceStatus: '',
|
||||
shopIDs: [],
|
||||
invoiceID: '',
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { pluck, shareReplay, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { InvoiceSearchService } from '../../../../../../api';
|
||||
import { Invoice } from '../../../../../../api-codegen/anapi';
|
||||
import { booleanDebounceTime } from '../../../../../../custom-operators';
|
||||
import { SEARCH_LIMIT } from '../../../../../constants';
|
||||
import { FetchResult, PartialFetcher } from '../../../../../partial-fetcher';
|
||||
import { mapToTimestamp } from '../../../operators';
|
||||
import { SearchFiltersParams } from '../../invoices-search-filters';
|
||||
|
||||
@Injectable()
|
||||
export class FetchInvoicesService extends PartialFetcher<Invoice, SearchFiltersParams> {
|
||||
isLoading$: Observable<boolean> = this.doAction$.pipe(booleanDebounceTime(), shareReplay(1));
|
||||
lastUpdated$: Observable<string> = this.searchResult$.pipe(mapToTimestamp);
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private invoiceSearchService: InvoiceSearchService,
|
||||
@Inject(SEARCH_LIMIT)
|
||||
private searchLimit: number
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected fetch(params: SearchFiltersParams, continuationToken: string): Observable<FetchResult<Invoice>> {
|
||||
return this.route.params.pipe(
|
||||
pluck('realm'),
|
||||
switchMap((paymentInstitutionRealm) =>
|
||||
this.invoiceSearchService.searchInvoices(
|
||||
params.fromTime,
|
||||
params.toTime,
|
||||
{
|
||||
...params,
|
||||
paymentInstitutionRealm,
|
||||
},
|
||||
this.searchLimit,
|
||||
continuationToken
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { ExpandedIdManager } from '@dsh/app/shared/services';
|
||||
|
||||
import { Invoice } from '../../../../../../api-codegen/anapi';
|
||||
import { FetchInvoicesService } from '../fetch-invoices/fetch-invoices.service';
|
||||
|
||||
@Injectable()
|
||||
export class InvoicesExpandedIdManager extends ExpandedIdManager<Invoice> {
|
||||
constructor(
|
||||
protected route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
private fetchInvoicesService: FetchInvoicesService
|
||||
) {
|
||||
super(route, router);
|
||||
}
|
||||
|
||||
protected fragmentNotFound(): void {
|
||||
this.fetchInvoicesService.fetchMore();
|
||||
}
|
||||
|
||||
protected get dataSet$(): Observable<Invoice[]> {
|
||||
return this.fetchInvoicesService.searchResult$;
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Params } from '@angular/router';
|
||||
import pickBy from 'lodash.pickby';
|
||||
|
||||
import { QueryParamsStore } from '@dsh/app/shared/services';
|
||||
|
||||
import { wrapValuesToArray } from '../../../../../../../utils';
|
||||
import { SearchFiltersParams } from '../../invoices-search-filters';
|
||||
|
||||
@Injectable()
|
||||
export class InvoicesSearchFiltersStore extends QueryParamsStore<SearchFiltersParams> {
|
||||
mapToData(queryParams: Params): Partial<SearchFiltersParams> {
|
||||
return {
|
||||
...queryParams,
|
||||
...wrapValuesToArray(this.pickShopsAndInvoices(queryParams)),
|
||||
};
|
||||
}
|
||||
|
||||
mapToParams(data: SearchFiltersParams): Params {
|
||||
return data;
|
||||
}
|
||||
|
||||
pickShopsAndInvoices(params: any) {
|
||||
return pickBy(params, (v, k) => typeof v === 'string' && ['shopIDs', 'invoiceIDs'].includes(k));
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
import { Invoice } from '@dsh/api-codegen/anapi/swagger-codegen';
|
||||
|
||||
import { StatusColor } from '../../../../theme-manager';
|
||||
|
||||
@Pipe({
|
||||
name: 'invoiceStatusColor',
|
||||
})
|
||||
export class InvoiceStatusColorPipe implements PipeTransform {
|
||||
transform(status: Invoice.StatusEnum): StatusColor {
|
||||
const statusEnum = Invoice.StatusEnum;
|
||||
switch (status) {
|
||||
case statusEnum.Paid:
|
||||
case statusEnum.Fulfilled:
|
||||
return StatusColor.success;
|
||||
case statusEnum.Cancelled:
|
||||
return StatusColor.warn;
|
||||
case statusEnum.Unpaid:
|
||||
return StatusColor.pending;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from './invoices-table-data';
|
||||
export * from './table.component';
|
@ -1,11 +0,0 @@
|
||||
import { InvoiceStatus } from '@dsh/api-codegen/anapi';
|
||||
|
||||
export interface InvoicesTableData {
|
||||
amount: number;
|
||||
currency: string;
|
||||
status: InvoiceStatus.StatusEnum;
|
||||
createdAt: string;
|
||||
invoiceID: string;
|
||||
shopName: string;
|
||||
product: string;
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
<div *transloco="let t; scope: 'operations'; read: 'operations.invoices.table'" class="table-container">
|
||||
<table dshTable [dataSource]="data">
|
||||
<ng-container dshColumnDef="invoiceID">
|
||||
<th dshHeaderCell fxFlex="130px" *dshHeaderCellDef>
|
||||
{{ t('invoiceID') }}
|
||||
</th>
|
||||
<td dshCell fxFlex="130px" *dshCellDef="let element">
|
||||
{{ element.invoiceID }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container dshColumnDef="product">
|
||||
<th dshHeaderCell fxFlex *dshHeaderCellDef>
|
||||
{{ t('product') }}
|
||||
</th>
|
||||
<td dshCell *dshCellDef="let element">
|
||||
<span class="truncate">{{ element.product }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container dshColumnDef="amount">
|
||||
<th dshHeaderCell *dshHeaderCellDef>
|
||||
{{ t('amount') }}
|
||||
</th>
|
||||
<td dshCell *dshCellDef="let element">
|
||||
{{ element.amount | toMajor | currency: element.currency }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container dshColumnDef="status">
|
||||
<th dshHeaderCell fxFlex="130px" *dshHeaderCellDef>
|
||||
{{ t('status') }}
|
||||
</th>
|
||||
<td dshCell fxFlex="130px" *dshCellDef="let element">
|
||||
<dsh-status
|
||||
*transloco="let invoiceStatus; read: 'invoiceStatus'"
|
||||
[color]="element.status | invoiceStatusColor"
|
||||
>{{ invoiceStatus(element.status) }}</dsh-status
|
||||
>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container dshColumnDef="createdAt">
|
||||
<th dshHeaderCell fxFlex="130px" *dshHeaderCellDef>
|
||||
{{ t('createdAt') }}
|
||||
</th>
|
||||
<td dshCell fxFlex="130px" *dshCellDef="let element">
|
||||
{{ element.createdAt | date: 'dd.MM.yyyy, HH:mm' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container dshColumnDef="shop">
|
||||
<th dshHeaderCell *dshHeaderCellDef>{{ t('shop') }}</th>
|
||||
<td dshCell *dshCellDef="let element">
|
||||
<span class="truncate">{{ element.shopName }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container dshColumnDef="actions">
|
||||
<th dshHeaderCell fxFlex="42px" *dshHeaderCellDef></th>
|
||||
<td dshCell fxFlex="42px" *dshCellDef="let element">
|
||||
<button dsh-icon-button [matMenuTriggerFor]="menu">
|
||||
<mat-icon svgIcon="more_vert"></mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item (click)="goToInvoiceDetails(element)">
|
||||
{{ t('actions.invoiceDetails') }}
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr dsh-header-row *dshHeaderRowDef="displayedColumns"></tr>
|
||||
<tr dsh-table-row *dshRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
</div>
|
@ -1,13 +0,0 @@
|
||||
@import '../../../../../../styles/utils/typography';
|
||||
|
||||
.truncate {
|
||||
@include typo-truncate(100%, 289px);
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.dsh-table {
|
||||
width: 100%;
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { InvoicesTableData } from './invoices-table-data';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-invoices-table',
|
||||
templateUrl: 'table.component.html',
|
||||
styleUrls: ['table.component.scss'],
|
||||
})
|
||||
export class TableComponent {
|
||||
@Input() data: MatTableDataSource<InvoicesTableData>;
|
||||
|
||||
displayedColumns: string[] = ['invoiceID', 'product', 'amount', 'status', 'createdAt', 'shop', 'actions'];
|
||||
|
||||
constructor(private router: Router) {}
|
||||
|
||||
goToInvoiceDetails({ invoiceID }: InvoicesTableData) {
|
||||
this.router.navigate(['invoice', invoiceID]);
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { ExpandedIdManager, Fragment } from '@dsh/app/shared/services';
|
||||
|
||||
import { RefundSearchResult } from '../../../../api-codegen/capi';
|
||||
import { FetchRefundsService } from './services/fetch-refunds/fetch-refunds.service';
|
||||
|
||||
@Injectable()
|
||||
export class RefundsExpandedIdManager extends ExpandedIdManager<RefundSearchResult> {
|
||||
constructor(
|
||||
protected route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
private fetchRefundsService: FetchRefundsService
|
||||
) {
|
||||
super(route, router);
|
||||
}
|
||||
|
||||
protected toFragment(r: RefundSearchResult): Fragment {
|
||||
return `${r.invoiceID}${r.paymentID}${r.id}`;
|
||||
}
|
||||
|
||||
protected get dataSet$(): Observable<RefundSearchResult[]> {
|
||||
return this.fetchRefundsService.searchResult$;
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<dsh-row fxLayout="row" fxLayoutAlign="space-between center" fxLayoutGap="24px">
|
||||
<dsh-row-label fxFlex>
|
||||
{{ refund.amount | toMajor | currency: refund.currency }}
|
||||
<span class="dsh-body-2">{{ refund.amount | toMajor | currency: refund.currency }}</span>
|
||||
</dsh-row-label>
|
||||
<dsh-row-label fxFlex>
|
||||
<dsh-status [color]="refund.status | refundStatusColor">{{ refund.status | refundStatusName }}</dsh-status>
|
||||
|
@ -7,7 +7,7 @@
|
||||
{{ t('title') }} <span dshSecondaryTitle>#{{ (invoice$ | async)?.id }}</span>
|
||||
</div>
|
||||
<ng-container *ngIf="isLoading$ | async; else afterLoading">
|
||||
<div class="dsh-body-1">{{ t('loading') }}</div>
|
||||
<div *transloco="let c" class="dsh-body-1">{{ c('loading') }}</div>
|
||||
</ng-container>
|
||||
<ng-template #afterLoading>
|
||||
<div *ngIf="errorOccurred$ | async; else content" class="dsh-body-1">{{ t('error') }}</div>
|
||||
|
@ -5,7 +5,7 @@
|
||||
>
|
||||
<div class="mat-title">{{ t('title') }}</div>
|
||||
<ng-container *ngIf="isLoading$ | async; else afterLoading">
|
||||
<div class="dsh-body-1">{{ t('loading') }}</div>
|
||||
<div *transloco="let c" class="dsh-body-1">{{ c('loading') }}</div>
|
||||
</ng-container>
|
||||
<ng-template #afterLoading>
|
||||
<div *ngIf="errorOccurred$ | async; else content" class="dsh-body-1">{{ t('error') }}</div>
|
||||
|
@ -0,0 +1 @@
|
||||
export * from './refund-row-header.component';
|
@ -0,0 +1,12 @@
|
||||
<dsh-row
|
||||
*transloco="let t; scope: 'operations'; read: 'operations.refunds.headerRow'"
|
||||
fxLayout="row"
|
||||
fxLayoutAlign="space-between center"
|
||||
fxLayoutGap="24px"
|
||||
color="primary"
|
||||
>
|
||||
<dsh-row-header-label fxFlex>{{ t('amount') }}</dsh-row-header-label>
|
||||
<dsh-row-header-label fxFlex>{{ t('status') }}</dsh-row-header-label>
|
||||
<dsh-row-header-label fxFlex>{{ t('updatedAt') }}</dsh-row-header-label>
|
||||
<dsh-row-header-label fxFlex fxHide.lt-md> {{ t('shop') }} </dsh-row-header-label>
|
||||
</dsh-row>
|
@ -0,0 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-refund-row-header',
|
||||
templateUrl: 'refund-row-header.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class RefundRowHeaderComponent {}
|
@ -0,0 +1,29 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
import isString from 'lodash.isstring';
|
||||
import pickBy from 'lodash.pickby';
|
||||
|
||||
import { QueryParamsStore } from '@dsh/app/shared/services';
|
||||
|
||||
import { wrapValuesToArray } from '../../../../../utils';
|
||||
import { SearchFiltersParams } from './refunds-search-filters';
|
||||
|
||||
const shopsAndInvoicesToArray = (v: any, k: string) => isString(v) && ['shopIDs', 'invoiceIDs'].includes(k);
|
||||
|
||||
@Injectable()
|
||||
export class RefundsSearchFiltersStore extends QueryParamsStore<SearchFiltersParams> {
|
||||
constructor(protected route: ActivatedRoute, protected router: Router) {
|
||||
super(router, route);
|
||||
}
|
||||
|
||||
mapToData(queryParams: Params): Partial<SearchFiltersParams> {
|
||||
return {
|
||||
...queryParams,
|
||||
...wrapValuesToArray(pickBy(queryParams, shopsAndInvoicesToArray)),
|
||||
};
|
||||
}
|
||||
|
||||
mapToParams(data: SearchFiltersParams): Params {
|
||||
return data;
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import { Daterange } from '@dsh/pipes/daterange';
|
||||
|
||||
import { SearchFiltersParams } from '../types/search-filters-params';
|
||||
|
||||
export const daterangeToSearchFilterParams = ({ begin, end }: Daterange): Partial<SearchFiltersParams> => ({
|
||||
fromTime: begin.utc().format(),
|
||||
toTime: end.utc().format(),
|
||||
});
|
@ -20,11 +20,10 @@ import { ApiShopsService } from '@dsh/api/shop';
|
||||
import { Daterange } from '@dsh/pipes/daterange';
|
||||
|
||||
import { SHARE_REPLAY_CONF } from '../../../../../custom-operators';
|
||||
import { daterangeFromStr, strToDaterange } from '../../../../../shared/utils';
|
||||
import { filterShopsByRealm } from '../../operators';
|
||||
import { SearchFiltersParams } from '../types/search-filters-params';
|
||||
import { daterangeToSearchFilterParams } from './daterange-to-search-filter-params';
|
||||
import { getDefaultDaterange } from './get-default-daterange';
|
||||
import { searchFilterParamsToDaterange } from './search-filter-params-to-daterange';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-refunds-search-filters',
|
||||
@ -84,7 +83,7 @@ export class RefundsSearchFiltersComponent implements OnChanges, OnInit {
|
||||
if (isNil(range)) {
|
||||
this.daterange = daterange;
|
||||
}
|
||||
this.searchParams$.next(daterangeToSearchFilterParams(daterange));
|
||||
this.searchParams$.next(daterangeFromStr(daterange));
|
||||
}
|
||||
|
||||
shopSelectionChange(shops: Shop[]) {
|
||||
@ -103,7 +102,7 @@ export class RefundsSearchFiltersComponent implements OnChanges, OnInit {
|
||||
private init(initParams: SimpleChange) {
|
||||
if (initParams && initParams.firstChange && initParams.currentValue) {
|
||||
const v = initParams.currentValue;
|
||||
this.daterange = !(v.fromTime || v.toTime) ? getDefaultDaterange() : searchFilterParamsToDaterange(v);
|
||||
this.daterange = !(v.fromTime || v.toTime) ? getDefaultDaterange() : strToDaterange(v);
|
||||
this.daterangeSelectionChange(this.daterange);
|
||||
if (Array.isArray(v.shopIDs)) {
|
||||
this.selectedShopIDs$.next(v.shopIDs);
|
||||
|
@ -1,10 +0,0 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import { Daterange } from '@dsh/pipes/daterange';
|
||||
|
||||
import { SearchFiltersParams } from '../types/search-filters-params';
|
||||
|
||||
export const searchFilterParamsToDaterange = ({ fromTime, toTime }: SearchFiltersParams): Daterange => ({
|
||||
begin: moment(fromTime),
|
||||
end: moment(toTime),
|
||||
});
|
@ -0,0 +1,9 @@
|
||||
import { RefundSearchResult } from '../../../../../api-codegen/capi/swagger-codegen';
|
||||
|
||||
export interface SearchFiltersParams {
|
||||
fromTime: string;
|
||||
toTime: string;
|
||||
invoiceIDs: string[];
|
||||
shopIDs: string[];
|
||||
refundStatus: RefundSearchResult.StatusEnum;
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
import isString from 'lodash.isstring';
|
||||
import { Params } from '@angular/router';
|
||||
import pickBy from 'lodash.pickby';
|
||||
|
||||
import { QueryParamsStore } from '@dsh/app/shared/services';
|
||||
@ -8,22 +7,20 @@ import { QueryParamsStore } from '@dsh/app/shared/services';
|
||||
import { wrapValuesToArray } from '../../../../../../../utils';
|
||||
import { SearchFiltersParams } from '../../refunds-search-filters';
|
||||
|
||||
const shopsAndInvoicesToArray = (v: any, k: string) => isString(v) && ['shopIDs', 'invoiceIDs'].includes(k);
|
||||
|
||||
@Injectable()
|
||||
export class RefundsSearchFiltersStore extends QueryParamsStore<SearchFiltersParams> {
|
||||
constructor(protected route: ActivatedRoute, protected router: Router) {
|
||||
super(router, route);
|
||||
}
|
||||
|
||||
mapToData(queryParams: Params): Partial<SearchFiltersParams> {
|
||||
return {
|
||||
...queryParams,
|
||||
...wrapValuesToArray(pickBy(queryParams, shopsAndInvoicesToArray)),
|
||||
...wrapValuesToArray(this.pickShopsAndInvoices(queryParams)),
|
||||
};
|
||||
}
|
||||
|
||||
mapToParams(data: SearchFiltersParams): Params {
|
||||
return data;
|
||||
}
|
||||
|
||||
private pickShopsAndInvoices(params: any) {
|
||||
return pickBy(params, (v, k) => typeof v === 'string' && ['shopIDs', 'invoiceIDs'].includes(k));
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,8 @@ import { NgModule } from '@angular/core';
|
||||
|
||||
import { ShopModule } from '@dsh/api/shop';
|
||||
import { WalletModule } from '@dsh/api/wallet';
|
||||
import { DEFAULT_SEARCH_LIMIT, LAYOUT_GAP, SEARCH_LIMIT } from '@dsh/app/sections/constants';
|
||||
|
||||
import { LAYOUT_GAP } from './constants';
|
||||
import { MainModule } from './main';
|
||||
import { SectionsRoutingModule } from './sections-routing.module';
|
||||
import { SectionsComponent } from './sections.component';
|
||||
@ -12,6 +12,9 @@ import { SectionsComponent } from './sections.component';
|
||||
imports: [MainModule, SectionsRoutingModule, ShopModule, WalletModule],
|
||||
declarations: [SectionsComponent],
|
||||
exports: [SectionsComponent],
|
||||
providers: [{ provide: LAYOUT_GAP, useValue: '20px' }],
|
||||
providers: [
|
||||
{ provide: LAYOUT_GAP, useValue: '20px' },
|
||||
{ provide: SEARCH_LIMIT, useValue: DEFAULT_SEARCH_LIMIT },
|
||||
],
|
||||
})
|
||||
export class SectionsModule {}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user