diff --git a/package-lock.json b/package-lock.json index f8def5ab8..163968d4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,8 @@ "@ngneat/input-mask": "^6.1.0", "@sentry/angular": "^10.68.0", "@tailwindcss/postcss": "^4.3.1", - "@vality/domain-proto": "^2.0.2-239638b.0", - "@vality/fistful-proto": "^2.0.1-9864622.0", + "@vality/domain-proto": "^2.0.2-99c91c9.0", + "@vality/fistful-proto": "^2.0.1-441d9bc.0", "@vality/machinegun-proto": "^1.0.1-273f2f3.0", "@vality/magista-proto": "^2.0.2-bd58cea.0", "@vality/repairer-proto": "^2.0.2-07b2a51.0", @@ -5722,11 +5722,15 @@ ] }, "node_modules/@vality/domain-proto": { - "version": "2.0.2-b07c077.0", + "version": "2.0.2-99c91c9.0", + "resolved": "https://registry.npmjs.org/@vality/domain-proto/-/domain-proto-2.0.2-99c91c9.0.tgz", + "integrity": "sha512-tLLIJjlnldBnhq+qDMDnLS1lYzDCcFRFSd/eyCliW8d6Ifxg/wvddUZZlNjT3CyWx6Tl/OpaHWbP8eE8Eeg03A==", "license": "Apache-2.0" }, "node_modules/@vality/fistful-proto": { - "version": "2.0.1-9864622.0", + "version": "2.0.1-441d9bc.0", + "resolved": "https://registry.npmjs.org/@vality/fistful-proto/-/fistful-proto-2.0.1-441d9bc.0.tgz", + "integrity": "sha512-d03yAxK0DKM0K55vPXz24kMep/ZyrMJFVh4ndHIYpl+eneAOuasvqMpKccaP/3+CBrmWZK8IgJ1hWePQtH/mBw==", "license": "Apache-2.0" }, "node_modules/@vality/machinegun-proto": { diff --git a/package.json b/package.json index 72527def3..e7ae91a56 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,8 @@ "@ngneat/input-mask": "^6.1.0", "@sentry/angular": "^10.68.0", "@tailwindcss/postcss": "^4.3.1", - "@vality/domain-proto": "^2.0.2-239638b.0", - "@vality/fistful-proto": "^2.0.1-9864622.0", + "@vality/domain-proto": "^2.0.2-99c91c9.0", + "@vality/fistful-proto": "^2.0.1-441d9bc.0", "@vality/machinegun-proto": "^1.0.1-273f2f3.0", "@vality/magista-proto": "^2.0.2-bd58cea.0", "@vality/repairer-proto": "^2.0.2-07b2a51.0", diff --git a/projects/matez/src/lib/components/select-autocomplete/select-field/select-field.component.html b/projects/matez/src/lib/components/select-autocomplete/select-field/select-field.component.html index 101eec25b..7c28bbd01 100644 --- a/projects/matez/src/lib/components/select-autocomplete/select-field/select-field.component.html +++ b/projects/matez/src/lib/components/select-autocomplete/select-field/select-field.component.html @@ -5,7 +5,11 @@ #select [closeOnSelect]="!multiple" [formControl]="control" - [items]="options" + [items]=" + options?.length || !control?.value + ? options + : [{ value: control.value, label: control.value }] + " [loading]="progress" [markFirst]="true" [multiple]="multiple" diff --git a/src/app/payments/payments.component.html b/src/app/payments/payments.component.html index 9b87aa444..aebab56d3 100644 --- a/src/app/payments/payments.component.html +++ b/src/app/payments/payments.component.html @@ -55,7 +55,7 @@ > Fail machines - + @if ((selected$ | async)?.length) { - - } @else { - - } + +
+ + + + + + + + + + + + URL + + content_copy + +
- @if (template) { - - } @else { - - } +
diff --git a/src/components/create-invoice-template-dialog/create-invoice-template-dialog.component.ts b/src/components/create-invoice-template-dialog/create-invoice-template-dialog.component.ts index 0dc9366ac..822e927ca 100644 --- a/src/components/create-invoice-template-dialog/create-invoice-template-dialog.component.ts +++ b/src/components/create-invoice-template-dialog/create-invoice-template-dialog.component.ts @@ -1,23 +1,21 @@ -import { - Subject, - catchError, - combineLatest, - debounceTime, - distinctUntilChanged, - map, - of, - shareReplay, - switchMap, - tap, -} from 'rxjs'; +import { EMPTY, of } from 'rxjs'; +import { catchError, distinctUntilChanged, map, shareReplay, switchMap } from 'rxjs/operators'; import { Clipboard } from '@angular/cdk/clipboard'; import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, DestroyRef, inject, signal } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { FormBuilder, FormControl, ReactiveFormsModule } from '@angular/forms'; +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + OnInit, + inject, + signal, +} from '@angular/core'; +import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop'; +import { ReactiveFormsModule } from '@angular/forms'; +import { FormField, form } from '@angular/forms/signals'; import { MatButtonModule } from '@angular/material/button'; -import { MatDividerModule } from '@angular/material/divider'; +import { MatDivider } from '@angular/material/divider'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; @@ -28,15 +26,26 @@ import { DialogSuperclass, InputFieldModule, NotifyLogService, - getValueChanges, - progressTo, + observableResource, } from '@vality/matez'; import { DomainObjectsStoreService } from '~/api/domain-config'; import { ThriftInvoiceTemplatingService } from '~/api/services'; -import { ConfigService } from '~/services'; -import { DomainMetadataFormExtensionsService, DomainThriftFormComponent } from '../thrift-api-crud'; +import { Duration, DurationFieldComponent } from '../duration-field'; +import { InvoiceTemplateDetailsFieldComponent } from '../invoice-template-details-field'; +import { PartyShop, ShopMerchantFieldComponent } from '../shop-merchant-field'; + +interface PaymentLinkParams extends Pick { + partyShop: PartyShop; + lifetime: Duration; + name: string; + description: string; + email: string; + redirectUrl: string; + cancelUrl: string; + locale: string; +} @Component({ templateUrl: './create-invoice-template-dialog.component.html', @@ -44,130 +53,93 @@ import { DomainMetadataFormExtensionsService, DomainThriftFormComponent } from ' imports: [ CommonModule, DialogModule, - DomainThriftFormComponent, ReactiveFormsModule, + InputFieldModule, + FormField, + DurationFieldComponent, + InvoiceTemplateDetailsFieldComponent, + ShopMerchantFieldComponent, MatButtonModule, + MatDivider, MatFormFieldModule, MatInputModule, MatIconModule, - InputFieldModule, - MatDividerModule, ], }) -export class CreateInvoiceTemplateDialogComponent extends DialogSuperclass { +export class CreateInvoiceTemplateDialogComponent + extends DialogSuperclass + implements OnInit +{ private invoiceTemplatingService = inject(ThriftInvoiceTemplatingService); private log = inject(NotifyLogService); - private dr = inject(DestroyRef); - private createTemplate$ = new Subject(); - private configService = inject(ConfigService); private clipboard = inject(Clipboard); - private fb = inject(FormBuilder); - private domainMetadataFormExtensionsService = inject(DomainMetadataFormExtensionsService); + private dr = inject(DestroyRef); private domainStoreService = inject(DomainObjectsStoreService); - linkForm = this.fb.group({ - name: null, - description: null, - email: null, - redirectUrl: null, - cancelUrl: null, - locale: null, + controlModel = signal({ + lifetime: { unit: 'days', amount: 30 }, + name: '', + description: '', + email: '', + redirectUrl: '', + cancelUrl: '', + locale: '', + + partyShop: { + party_id: null, + shop_id: null, + }, + details: null, }); - template$ = this.createTemplate$.pipe( - switchMap(() => - this.invoiceTemplatingService.Create(this.control.value).pipe( - progressTo(this.progress), - tap(() => { - this.log.success('Invoice template created successfully'); - }), - catchError((err) => { - this.log.error('Failed to create invoice template', err); - return of(null); - }), - ), - ), - takeUntilDestroyed(this.dr), - shareReplay(1), - ); - control = new FormControl( - { context: { type: 'application/json', data: '{}' } } as InvoiceTemplateCreateParams, - { nonNullable: true }, - ); - progress = signal(0); - link$ = combineLatest([ - this.template$, - this.configService.config.value$, - getValueChanges(this.linkForm).pipe(debounceTime(1000)), - ]).pipe( - map(([template, config, linkValues]) => { - const url = new URL( - `http${(config.checkout.https ?? true) ? 's' : ''}://${config.checkout.hostname}${config.checkout.path ?? '/v1/checkout.html'}`, - ); - url.searchParams.set('invoiceTemplateID', template.invoice_template.id); - url.searchParams.set( - 'invoiceTemplateAccessToken', - template.invoice_template_access_token.payload, - ); - const nonEmptyLinkValues = Object.entries(linkValues).filter(([, v]) => !!v); - for (const [key, value] of nonEmptyLinkValues) { - url.searchParams.set(key, value as string); - } - return url.toString(); - }), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - extensions$ = combineLatest([ - this.domainMetadataFormExtensionsService.createFullDomainObjectsOptionsByType( - 'ShopConfigObject', - 'shop_config', - getValueChanges(this.control).pipe( - map((value) => value?.party_id?.id), - distinctUntilChanged(), - map((partyId) => - partyId - ? (obj) => obj.object.shop_config.data.party_ref.id === partyId - : () => true, - ), - ), + control = form(this.controlModel); + currency$ = toObservable(this.controlModel).pipe( + map(({ partyShop }) => partyShop.shop_id), + distinctUntilChanged(), + switchMap((shopId) => + shopId + ? this.domainStoreService.getObject({ shop_config: { id: shopId } }).value$ + : of(null), ), - this.domainMetadataFormExtensionsService.createFullDomainObjectsOptionsByType( - 'CurrencyObject', - 'currency', - getValueChanges(this.control).pipe( - map((value) => value?.shop_id?.id), - distinctUntilChanged(), - switchMap((shopId) => - shopId - ? this.domainStoreService.getObject({ - shop_config: { id: shopId }, - }).value$ - : of(null), - ), - map((shop) => - shop - ? (obj) => - obj.object.currency.data.symbolic_code === - shop.object.shop_config.data.account.currency.symbolic_code - : () => true, - ), - ), + map( + (shop) => + shop?.object?.shop_config?.data?.account?.currency?.symbolic_code ?? undefined, ), - ]).pipe( - map((extensionGroups) => extensionGroups.flat()), - shareReplay({ refCount: true, bufferSize: 1 }), + shareReplay({ bufferSize: 1, refCount: true }), ); + invoiceTemplate = observableResource({ + params: EMPTY, + loader: ({ partyShop, details, lifetime, ...params }: PaymentLinkParams) => + this.invoiceTemplatingService + .Create({ + shop_id: { id: partyShop.shop_id }, + party_id: { id: partyShop.party_id }, + details, + invoice_lifetime: { [lifetime.unit]: lifetime.amount }, + url_params: new Map(Object.entries(params)), + context: { type: 'application/json', data: '{}' }, + }) + .pipe( + catchError((err) => { + this.log.error(err, 'Failed to create payment link'); + return EMPTY; + }), + ), + }); - create() { - this.createTemplate$.next(null); + ngOnInit() { + this.invoiceTemplate.value$.pipe(takeUntilDestroyed(this.dr)).subscribe((result) => { + this.clipboard.copy(result.invoice_template_url.url); + this.log.success('Link copied to clipboard'); + }); } - confirm() { - this.closeWithSuccess(); + createAndCopyLink() { + this.invoiceTemplate.setParams(this.controlModel()); } copyLink() { - this.link$.pipe(takeUntilDestroyed(this.dr)).subscribe((link) => { - this.clipboard.copy(link); + this.invoiceTemplate.getFirstValue().subscribe((result) => { + this.clipboard.copy(result.invoice_template_url.url); this.log.success('Link copied to clipboard'); }); } diff --git a/src/components/duration-field/duration-field.component.html b/src/components/duration-field/duration-field.component.html new file mode 100644 index 000000000..c89364509 --- /dev/null +++ b/src/components/duration-field/duration-field.component.html @@ -0,0 +1,14 @@ +
+ + +
diff --git a/src/components/duration-field/duration-field.component.ts b/src/components/duration-field/duration-field.component.ts new file mode 100644 index 000000000..025667abe --- /dev/null +++ b/src/components/duration-field/duration-field.component.ts @@ -0,0 +1,44 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input, model } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { FormField, FormValueControl, form } from '@angular/forms/signals'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatSelectModule } from '@angular/material/select'; + +import { LifetimeInterval } from '@vality/domain-proto/domain'; +import { InputFieldModule, Option, SelectFieldModule } from '@vality/matez'; + +type DurationUnit = keyof LifetimeInterval; + +export interface Duration { + amount: number; + unit: DurationUnit; +} + +@Component({ + selector: 'cc-duration-field', + templateUrl: './duration-field.component.html', + imports: [ + CommonModule, + ReactiveFormsModule, + MatFormFieldModule, + MatSelectModule, + InputFieldModule, + SelectFieldModule, + FormField, + ], +}) +export class DurationFieldComponent implements FormValueControl { + @Input() label = 'Duration'; + + unitOptions: Option[] = [ + { value: 'seconds', label: 'Seconds' }, + { value: 'minutes', label: 'Minutes' }, + { value: 'hours', label: 'Hours' }, + { value: 'days', label: 'Days' }, + { value: 'months', label: 'Months' }, + { value: 'years', label: 'Years' }, + ]; + value = model({ amount: 1, unit: 'hours' }); + control = form(this.value); +} diff --git a/src/components/duration-field/index.ts b/src/components/duration-field/index.ts new file mode 100644 index 000000000..674f990ea --- /dev/null +++ b/src/components/duration-field/index.ts @@ -0,0 +1 @@ +export * from './duration-field.component'; diff --git a/src/components/invoice-template-details-field/components/invoice-line-field/invoice-line-field.component.html b/src/components/invoice-template-details-field/components/invoice-line-field/invoice-line-field.component.html new file mode 100644 index 000000000..19c2fb946 --- /dev/null +++ b/src/components/invoice-template-details-field/components/invoice-line-field/invoice-line-field.component.html @@ -0,0 +1,15 @@ +
+ +
+ + +
+
diff --git a/src/components/invoice-template-details-field/components/invoice-line-field/invoice-line-field.component.ts b/src/components/invoice-template-details-field/components/invoice-line-field/invoice-line-field.component.ts new file mode 100644 index 000000000..2d14d46e1 --- /dev/null +++ b/src/components/invoice-template-details-field/components/invoice-line-field/invoice-line-field.component.ts @@ -0,0 +1,81 @@ +import { Overwrite } from 'utility-types'; + +import { Component, effect, input, model, untracked } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { FormField, FormValueControl, form, transformedValue } from '@angular/forms/signals'; + +import { Cash, InvoiceLine } from '@vality/domain-proto/domain'; +import { InputFieldModule } from '@vality/matez'; + +import { + SourceCash, + SourceCashFieldComponent, +} from '~/components/source-cash-field/source-cash-field.component'; + +type InvoiceLineFormValue = Overwrite; + +@Component({ + selector: 'cc-invoice-line-field', + templateUrl: './invoice-line-field.component.html', + imports: [ReactiveFormsModule, FormField, InputFieldModule, SourceCashFieldComponent], +}) +export class InvoiceLineFieldComponent implements FormValueControl { + currency = input(); + value = model(null); + formValue = transformedValue(this.value, { + parse: (value) => ({ value: this.toInvoiceLine(value) }), + format: (value) => this.toFormValue(value), + }); + control = form(this.formValue); + + constructor() { + effect(() => { + const currencySymbolicCode = this.currency(); + if (currencySymbolicCode) { + untracked(() => { + this.formValue.update((value) => ({ + ...value, + price: { + ...value.price, + sourceId: null, + currencySymbolicCode, + }, + })); + }); + } + }); + } + + private toInvoiceLine(value: InvoiceLineFormValue): InvoiceLine { + return value + ? { + ...value, + price: this.toCash(value.price), + } + : null; + } + + private toFormValue(value: InvoiceLine): InvoiceLineFormValue { + return value + ? { + ...value, + price: { + amount: value.price?.amount, + sourceId: null, + currencySymbolicCode: this.currency() || value.price?.currency?.symbolic_code, + }, + } + : null; + } + + private toCash(value: SourceCash): Cash { + return value + ? { + amount: value.amount, + currency: { + symbolic_code: this.currency() || value.currencySymbolicCode, + }, + } + : null; + } +} diff --git a/src/components/invoice-template-details-field/index.ts b/src/components/invoice-template-details-field/index.ts new file mode 100644 index 000000000..380467e53 --- /dev/null +++ b/src/components/invoice-template-details-field/index.ts @@ -0,0 +1 @@ +export * from './invoice-template-details-field.component'; diff --git a/src/components/invoice-template-details-field/invoice-template-details-field.component.html b/src/components/invoice-template-details-field/invoice-template-details-field.component.html new file mode 100644 index 000000000..4de7eb7fc --- /dev/null +++ b/src/components/invoice-template-details-field/invoice-template-details-field.component.html @@ -0,0 +1,48 @@ +
+ + Product + Cart + + + @if (detailsType() === 'cart') { + + @for (line of formValue()?.cart?.lines ?? []; track $index) { + + + Product #{{ $index + 1 }} + + + + + } + + + } @else { + + } +
diff --git a/src/components/invoice-template-details-field/invoice-template-details-field.component.ts b/src/components/invoice-template-details-field/invoice-template-details-field.component.ts new file mode 100644 index 000000000..3cc2a72ac --- /dev/null +++ b/src/components/invoice-template-details-field/invoice-template-details-field.component.ts @@ -0,0 +1,125 @@ +import { Overwrite } from 'utility-types'; + +import { Component, computed, input, model } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { FormField, FormValueControl, form, transformedValue } from '@angular/forms/signals'; +import { MatButtonModule } from '@angular/material/button'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatIconModule } from '@angular/material/icon'; +import { MatRadioModule } from '@angular/material/radio'; + +import { Cash, InvoiceLine, InvoiceTemplateDetails } from '@vality/domain-proto/domain'; + +import { InvoiceLineFieldComponent } from './components/invoice-line-field/invoice-line-field.component'; + +type InvoiceTemplateDetailsFormValue = Overwrite; + +@Component({ + selector: 'cc-invoice-template-details-field', + templateUrl: './invoice-template-details-field.component.html', + imports: [ + ReactiveFormsModule, + FormField, + InvoiceLineFieldComponent, + MatButtonModule, + MatExpansionModule, + MatIconModule, + MatRadioModule, + ], +}) +export class InvoiceTemplateDetailsFieldComponent implements FormValueControl { + currency = input(); + value = model(null); + formValue = transformedValue( + this.value, + { + parse: (value) => ({ value: this.toInvoiceTemplateDetails(value) }), + format: (value) => this.toFormValue(value), + }, + ); + control = form(this.formValue); + detailsType = computed<'cart' | 'product'>(() => (this.formValue()?.cart ? 'cart' : 'product')); + + selectDetailsType(type: 'cart' | 'product') { + if (type !== this.detailsType()) { + this.formValue.set( + type === 'cart' + ? { cart: { lines: [this.createInvoiceLine()] } } + : { product: this.createInvoiceLine() }, + ); + } + } + + addInvoiceLine() { + this.formValue.update((value) => ({ + cart: { + lines: [...(value?.cart?.lines ?? []), this.createInvoiceLine()], + }, + })); + } + + removeInvoiceLine(index: number) { + this.formValue.update((value) => ({ + cart: { + lines: + (value?.cart?.lines.length ?? 0) > 1 + ? value.cart.lines.filter((_, i) => i !== index) + : value.cart.lines, + }, + })); + } + + private createCash(): Cash { + return { + amount: null, + currency: { symbolic_code: this.currency() ?? '' }, + }; + } + + private createInvoiceLine(): InvoiceLine { + return { + product: '', + quantity: 1, + price: this.createCash(), + metadata: new Map(), + }; + } + + private toInvoiceTemplateDetails( + value: InvoiceTemplateDetailsFormValue, + ): InvoiceTemplateDetails { + if (value?.product) { + const { product, price, metadata } = value.product; + return { + product: { + product, + price: { fixed: price }, + metadata, + }, + }; + } + return value?.cart ? { cart: value.cart } : null; + } + + private toFormValue(value: InvoiceTemplateDetails): InvoiceTemplateDetailsFormValue { + if (value?.product) { + return { + product: { + product: value.product.product, + quantity: 1, + price: value.product.price.fixed ?? this.createCash(), + metadata: value.product.metadata, + }, + }; + } + return value?.cart + ? { + cart: { + lines: value.cart.lines.length + ? value.cart.lines + : [this.createInvoiceLine()], + }, + } + : { product: this.createInvoiceLine() }; + } +} diff --git a/src/components/shop-field/shop-field.component.html b/src/components/shop-field/shop-field.component.html index 083292a94..d65cbdb7e 100644 --- a/src/components/shop-field/shop-field.component.html +++ b/src/components/shop-field/shop-field.component.html @@ -1,13 +1,13 @@ diff --git a/src/components/shop-field/shop-field.component.ts b/src/components/shop-field/shop-field.component.ts index 15b077fc1..7c08aa4c7 100644 --- a/src/components/shop-field/shop-field.component.ts +++ b/src/components/shop-field/shop-field.component.ts @@ -1,4 +1,4 @@ -import { Observable } from 'rxjs'; +import { of } from 'rxjs'; import { map } from 'rxjs/operators'; import { @@ -9,6 +9,7 @@ import { inject, input, } from '@angular/core'; +import { toObservable } from '@angular/core/rxjs-interop'; import { DomainObjectType, PartyConfigRef, ShopID } from '@vality/domain-proto/domain'; import { @@ -16,9 +17,11 @@ import { Option, SelectFieldComponent, createControlProviders, + observableResource, } from '@vality/matez'; import { FetchDomainObjectsService } from '~/api/domain-config'; +import { ThriftRepositoryService } from '~/api/services'; @Component({ selector: 'cc-shop-field', @@ -28,7 +31,7 @@ import { FetchDomainObjectsService } from '~/api/domain-config'; standalone: false, }) export class ShopFieldComponent extends FormControlSuperclass { - private fetchDomainObjectsService = inject(FetchDomainObjectsService); + private repositoryService = inject(ThriftRepositoryService); @Input() label: string; @Input({ transform: booleanAttribute }) required: boolean; @@ -36,23 +39,35 @@ export class ShopFieldComponent extends FormControlSuperclass @Input() appearance?: SelectFieldComponent['appearance']; @Input() hint?: string; multiple = input(false, { transform: booleanAttribute }); + partyId = input(); - options$: Observable[]> = - this.fetchDomainObjectsService.result$.pipe( - map((objs) => - objs.map((obj) => ({ - value: obj.ref.shop_config.id, - label: obj.name || `#${obj.ref.shop_config.id}`, - description: obj.description, - })), - ), - ); - progress$ = this.fetchDomainObjectsService.isLoading$; - - search(search: string) { - this.fetchDomainObjectsService.load( - { type: DomainObjectType.shop_config, query: search }, - { size: 1000 }, - ); - } + shops = observableResource({ + params: toObservable(this.partyId).pipe(map((partyId) => ({ partyId, query: '' }))), + loader: ({ partyId, query }) => + !partyId && !query + ? of([]) + : (partyId + ? this.repositoryService + .GetRelatedGraph({ + ref: { party_config: { id: partyId } }, + type: DomainObjectType.shop_config, + }) + .pipe(map(({ nodes }) => Array.from(nodes))) + : this.repositoryService + .SearchObjects({ + type: DomainObjectType.shop_config, + query: query || '*', + limit: 1000, + }) + .pipe(map((res) => res.result || [])) + ).pipe( + map((objs): Option[] => + objs.map((obj) => ({ + value: obj.ref.shop_config.id, + label: obj.name || `#${obj.ref.shop_config.id}`, + description: obj.description, + })), + ), + ), + }); } diff --git a/src/components/shop-merchant-field/index.ts b/src/components/shop-merchant-field/index.ts new file mode 100644 index 000000000..4a57af2ee --- /dev/null +++ b/src/components/shop-merchant-field/index.ts @@ -0,0 +1 @@ +export * from './shop-merchant-field.component'; diff --git a/src/components/shop-merchant-field/shop-merchant-field.component.html b/src/components/shop-merchant-field/shop-merchant-field.component.html new file mode 100644 index 000000000..418445720 --- /dev/null +++ b/src/components/shop-merchant-field/shop-merchant-field.component.html @@ -0,0 +1,4 @@ +
+ + +
diff --git a/src/components/shop-merchant-field/shop-merchant-field.component.ts b/src/components/shop-merchant-field/shop-merchant-field.component.ts new file mode 100644 index 000000000..3d476b27e --- /dev/null +++ b/src/components/shop-merchant-field/shop-merchant-field.component.ts @@ -0,0 +1,53 @@ +import { distinctUntilChanged, filter, map, switchMap } from 'rxjs'; + +import { CommonModule } from '@angular/common'; +import { Component, DestroyRef, Injector, OnInit, inject, model } from '@angular/core'; +import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop'; +import { ReactiveFormsModule } from '@angular/forms'; +import { FormField, FormValueControl, form } from '@angular/forms/signals'; + +import { PartyConfigRef, ShopID } from '@vality/domain-proto/domain'; + +import { DomainService } from '~/api/domain-config'; + +import { MerchantFieldModule } from '../merchant-field'; +import { ShopFieldModule } from '../shop-field'; + +export interface PartyShop { + party_id: PartyConfigRef['id']; + shop_id: ShopID; +} + +@Component({ + selector: 'cc-shop-merchant-field', + templateUrl: './shop-merchant-field.component.html', + imports: [CommonModule, ReactiveFormsModule, ShopFieldModule, MerchantFieldModule, FormField], +}) +export class ShopMerchantFieldComponent implements FormValueControl, OnInit { + private dr = inject(DestroyRef); + private domainService = inject(DomainService); + private injector = inject(Injector); + + value = model({ + party_id: null, + shop_id: null, + }); + control = form(this.value); + + ngOnInit() { + toObservable(this.value, { injector: this.injector }) + .pipe( + map((v) => v.shop_id), + filter(Boolean), + distinctUntilChanged(), + switchMap((shopId) => this.domainService.get({ shop_config: { id: shopId } })), + map((shop) => shop.object.shop_config.data.party_ref.id), + takeUntilDestroyed(this.dr), + ) + .subscribe((party_id) => { + if (party_id !== this.value().party_id) { + this.value.update((v) => ({ ...v, party_id })); + } + }); + } +} diff --git a/src/components/source-cash-field/source-cash-field.component.ts b/src/components/source-cash-field/source-cash-field.component.ts index 5b6e2b833..b13511119 100644 --- a/src/components/source-cash-field/source-cash-field.component.ts +++ b/src/components/source-cash-field/source-cash-field.component.ts @@ -15,7 +15,7 @@ import { } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormControl, ReactiveFormsModule, ValidationErrors, Validator } from '@angular/forms'; -import { MatFormField } from '@angular/material/form-field'; +import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { InputMaskModule, createMask } from '@ngneat/input-mask'; @@ -50,7 +50,7 @@ const RADIX_POINT = '.'; providers: createControlProviders(() => SourceCashFieldComponent), changeDetection: ChangeDetectionStrategy.Eager, imports: [ - MatFormField, + MatFormFieldModule, ReactiveFormsModule, InputMaskModule, SelectFieldModule, diff --git a/src/utils/table/create-currency-column.ts b/src/utils/table/create-currency-column.ts index 1f4d6a31b..2de7dc542 100644 --- a/src/utils/table/create-currency-column.ts +++ b/src/utils/table/create-currency-column.ts @@ -1,4 +1,4 @@ -import { groupBy, uniq } from 'lodash-es'; +import { groupBy, isNil, uniq } from 'lodash-es'; import { combineLatest, of } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; @@ -38,7 +38,7 @@ export const createCurrencyColumn = createColumn( (currencyValue: CurrencyValue | { values: CurrencyValue[]; isSum?: boolean }) => { const isSum = 'isSum' in currencyValue ? currencyValue.isSum : false; const currencyValues = ('values' in currencyValue ? currencyValue.values : [currencyValue]) - .filter(Boolean) + .filter((v) => !isNil(v?.amount) && !isNil(v?.code)) .sort((a, b) => b.amount - a.amount); if (!currencyValues?.length) { return of(undefined); diff --git a/src/utils/thrift/provide-thrift-services.ts b/src/utils/thrift/provide-thrift-services.ts index a27aeeaca..a066aea93 100644 --- a/src/utils/thrift/provide-thrift-services.ts +++ b/src/utils/thrift/provide-thrift-services.ts @@ -7,7 +7,6 @@ import { Type, inject, isDevMode, makeEnvironmentProviders } from '@angular/core import * as Sentry from '@sentry/angular'; import { ConnectOptions } from '@vality/domain-proto'; -import { toJson } from '@vality/ng-thrift'; import { ConfigService, KeycloakUserService } from '~/services'; @@ -116,10 +115,18 @@ const logger: ConnectOptions['loggingFn'] = (params) => { ); console.error(parsedError.error); if (LOGGING.fullLogging) { - console.log('Arguments'); - console.log(JSON.stringify(toJson(params.args), null, 2)); - console.log('Headers'); - console.log(params.headers); + console.dir( + { + Arguments: params.args, + Headers: params.headers, + }, + { + depth: null, + compact: false, + maxArrayLength: null, + maxStringLength: null, + }, + ); } console.groupEnd(); return; @@ -127,12 +134,19 @@ const logger: ConnectOptions['loggingFn'] = (params) => { case 'success': { if (LOGGING.fullLogging) { console.groupCollapsed(`🟢\u00A0${info}`); - console.log('Arguments'); - console.log(JSON.stringify(toJson(params.args), null, 2)); - console.log('Response'); - console.log(JSON.stringify(toJson(params.response), null, 2)); - console.log('Headers'); - console.log(params.headers); + console.dir( + { + Arguments: params.args, + Response: params.response, + Headers: params.headers, + }, + { + depth: null, + compact: false, + maxArrayLength: null, + maxStringLength: null, + }, + ); console.groupEnd(); } return;