diff --git a/projects/matez/src/lib/components/select-autocomplete/select-field/select-field.component.ts b/projects/matez/src/lib/components/select-autocomplete/select-field/select-field.component.ts index 85c72dd50..86ea81779 100644 --- a/projects/matez/src/lib/components/select-autocomplete/select-field/select-field.component.ts +++ b/projects/matez/src/lib/components/select-autocomplete/select-field/select-field.component.ts @@ -1,4 +1,6 @@ -import { Component, EventEmitter, Input, Output, booleanAttribute } from '@angular/core'; +import { first, timer } from 'rxjs'; + +import { Component, EventEmitter, Input, OnInit, Output, booleanAttribute } from '@angular/core'; import { MatFormFieldAppearance } from '@angular/material/form-field'; import { FormControlSuperclass, createControlProviders } from '../../../utils'; @@ -13,7 +15,10 @@ import { getHintText } from '../utils/get-hint-text'; providers: createControlProviders(() => SelectFieldComponent), standalone: false, }) -export class SelectFieldComponent extends FormControlSuperclass { +export class SelectFieldComponent + extends FormControlSuperclass + implements OnInit +{ @Input() options: Option[] = []; @Output() searchChange = new EventEmitter(); @@ -32,6 +37,17 @@ export class SelectFieldComponent extends FormControlSuperclass { + this.searchChange.emit(String(this.control.value)); + }); + } + } + get hintText() { return getHintText( this.options, diff --git a/projects/matez/src/lib/services/app-mode/app-mode.service.ts b/projects/matez/src/lib/services/app-mode/app-mode.service.ts new file mode 100644 index 000000000..d5ab2413d --- /dev/null +++ b/projects/matez/src/lib/services/app-mode/app-mode.service.ts @@ -0,0 +1,28 @@ +import { Injectable, computed, effect, signal } from '@angular/core'; + +type AppMode = 'simple' | 'advanced'; + +@Injectable({ + providedIn: 'root', +}) +export class AppModeService { + mode = signal(this.getInitialMode()); + + isSimple = computed(() => this.mode() === 'simple'); + isAdvanced = computed(() => this.mode() === 'advanced'); + + constructor() { + effect(() => { + localStorage.setItem('app-mode', this.mode()); + }); + } + + private getInitialMode(): AppMode { + const stored = localStorage.getItem('app-mode'); + return (stored as AppMode) || 'simple'; + } + + setMode(mode: AppMode): void { + this.mode.set(mode); + } +} diff --git a/projects/matez/src/lib/services/app-mode/index.ts b/projects/matez/src/lib/services/app-mode/index.ts new file mode 100644 index 000000000..258e19693 --- /dev/null +++ b/projects/matez/src/lib/services/app-mode/index.ts @@ -0,0 +1 @@ +export * from './app-mode.service'; diff --git a/projects/matez/src/lib/services/index.ts b/projects/matez/src/lib/services/index.ts index cd96d871a..142d41a01 100644 --- a/projects/matez/src/lib/services/index.ts +++ b/projects/matez/src/lib/services/index.ts @@ -5,3 +5,4 @@ export * from './query-params'; export * from './url.service'; export * from './toast'; export * from './theme'; +export * from './app-mode'; diff --git a/src/app/app.component.html b/src/app/app.component.html index 7c6ee2a75..05ab91d01 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -31,13 +31,22 @@ > {{ username().slice(0, 2).toUpperCase() }} - +
+ + +
+ + + + diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 84e0589d1..99a8b9004 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -13,6 +13,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { Router, RouterOutlet } from '@angular/router'; import { + AppModeService, BaseLink, CmdkModule, CmdkOption, @@ -228,6 +229,7 @@ export class AppComponent { sidenavInfoService = inject(SidenavInfoService); themeService = inject(ThemeService); + protected modeService = inject(AppModeService); links = createNavLinks(); username = this.keycloakUserService.username; @@ -282,6 +284,10 @@ export class AppComponent { } } + getModeIcon(mode = this.modeService.mode()): string { + return mode === 'advanced' ? 'school' : 'wand_stars'; + } + private registerConsoleUtils() { Object.assign(window as never as object, { ccSwitchLogging: () => { diff --git a/src/app/deposits/deposits.component.ts b/src/app/deposits/deposits.component.ts index 96de89378..888203721 100644 --- a/src/app/deposits/deposits.component.ts +++ b/src/app/deposits/deposits.component.ts @@ -26,7 +26,7 @@ import { import { getUnionKey } from '@vality/ng-thrift'; import { QueryDsl } from '~/api/fistful-stat'; -import { createCurrencyColumn } from '~/utils'; +import { createCurrencyColumn, createDomainObjectColumn } from '~/utils'; import { DATE_RANGE_DAYS, DEBOUNCE_TIME_MS } from '../tokens'; @@ -89,9 +89,9 @@ export class DepositsComponent implements OnInit { field: 'created_at', cell: { type: 'datetime' }, }, - { - field: 'destination_id', - }, + createDomainObjectColumn((d) => ({ ref: { wallet_config: { id: d.destination_id } } }), { + header: 'Destination', + }), { field: 'identity_id', }, diff --git a/src/app/parties/party/routing-rules/candidates/candidates.component.html b/src/app/parties/party/routing-rules/candidates/candidates.component.html new file mode 100644 index 000000000..ff74ef072 --- /dev/null +++ b/src/app/parties/party/routing-rules/candidates/candidates.component.html @@ -0,0 +1,101 @@ + + + + + @if (appMode.isAdvanced()) { + + } @else { + @if (candidateGroups$ | async; as groups) { + + + @if (!!item?.terminal) { +
+
+
+ {{ item.terminal?.object?.terminal?.data?.name }} +
+
+ {{ item.weightPercent }}% +
+
+ @if (!!item?.fullAllowedStr) { +
+ {{ item.fullAllowedStr }} +
+ } +
+ +
+ + +
+ + + + + +
+
+ } @else { + Loading... + } +
+
+ } + } +
diff --git a/src/app/parties/party/routing-rules/routing-ruleset/routing-ruleset.component.ts b/src/app/parties/party/routing-rules/candidates/candidates.component.ts similarity index 52% rename from src/app/parties/party/routing-rules/routing-ruleset/routing-ruleset.component.ts rename to src/app/parties/party/routing-rules/candidates/candidates.component.ts index 932796bb9..c1bc03c1c 100644 --- a/src/app/parties/party/routing-rules/routing-ruleset/routing-ruleset.component.ts +++ b/src/app/parties/party/routing-rules/candidates/candidates.component.ts @@ -1,48 +1,122 @@ +import { isNil, upperFirst } from 'lodash-es'; import cloneDeep from 'lodash-es/cloneDeep'; -import { Observable, combineLatest, filter } from 'rxjs'; -import { first, map, switchMap, take, withLatestFrom } from 'rxjs/operators'; +import { BehaviorSubject, Observable, combineLatest, filter, of } from 'rxjs'; +import { first, map, shareReplay, switchMap, take, withLatestFrom } from 'rxjs/operators'; -import { Component, DestroyRef, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Component, DestroyRef, Injector, inject, runInInjectionContext } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatPaginatorModule } from '@angular/material/paginator'; +import { MatRadioModule } from '@angular/material/radio'; +import { MatSelectModule } from '@angular/material/select'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { Sort } from '@angular/material/sort'; -import { ActivatedRoute } from '@angular/router'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { ActivatedRoute, RouterModule } from '@angular/router'; -import { RoutingCandidate } from '@vality/domain-proto/domain'; +import { Predicate, RoutingCandidate } from '@vality/domain-proto/domain'; +import { VersionedObject } from '@vality/domain-proto/domain_config_v2'; import { + AppModeService, Column, + DialogModule, DialogResponseStatus, DialogService, DragDrop, NotifyLogService, + TableModule, correctPriorities, createMenuColumn, + switchCombineWith, } from '@vality/matez'; -import { toJson } from '@vality/ng-thrift'; +import { ThriftViewerModule, toJson } from '@vality/ng-thrift'; -import { DomainService, RoutingRulesStoreService } from '~/api/domain-config'; +import { + DomainObjectsStoreService, + DomainService, + RoutingRulesStoreService, +} from '~/api/domain-config'; import { CandidateCardComponent } from '~/components/candidate-card/candidate-card.component'; +import { PageLayoutModule } from '~/components/page-layout'; import { SidenavInfoService } from '~/components/sidenav-info'; import { DomainThriftFormDialogComponent, UpdateThriftDialogComponent, } from '~/components/thrift-api-crud'; import { DomainObjectCardComponent } from '~/components/thrift-api-crud/domain'; -import { createDomainObjectColumn, createPredicateColumn, getPredicateBoolean } from '~/utils'; +import { + createDomainObjectColumn, + createPredicateColumn, + formatPredicate, + getPredicateBoolean, +} from '~/utils'; import { RoutingRulesService } from '../services/routing-rules'; import { RoutingRulesType } from '../types/routing-rules-type'; import { changeCandidatesAllowed } from '../utils/toggle-candidate-allowed'; -import { RoutingRulesetService } from './routing-ruleset.service'; +import { CandidatesService } from './candidates.service'; +import { DndCardsComponent, Item } from './components/dnd-cards/dnd-cards.component'; +import { EditCandidateDialogComponent } from './components/edit-candidate-dialog/edit-candidate-dialog.component'; + +function getAllowStr(predicate: Predicate, hasTrueFalse = false): string { + const allowed = formatPredicate(predicate).toLowerCase(); + if (isNil(predicate) || allowed === 'true') return hasTrueFalse ? 'allowed' : ''; + if (allowed === 'false') return hasTrueFalse ? 'denied' : ''; + return allowed; +} +interface Row { + idx: number; + candidate: RoutingCandidate; + terminal: VersionedObject; + allowed: boolean; + globalAllow: boolean; + fullAllowedStr: string; +} @Component({ - templateUrl: 'routing-ruleset.component.html', - providers: [RoutingRulesetService], - standalone: false, + templateUrl: 'candidates.component.html', + providers: [CandidatesService], + imports: [ + CommonModule, + MatButtonModule, + MatDialogModule, + MatDividerModule, + ReactiveFormsModule, + MatFormFieldModule, + MatInputModule, + RouterModule, + MatIconModule, + MatMenuModule, + MatPaginatorModule, + MatCardModule, + MatSelectModule, + MatRadioModule, + MatExpansionModule, + MatAutocompleteModule, + TableModule, + ThriftViewerModule, + DialogModule, + PageLayoutModule, + DndCardsComponent, + MatTooltipModule, + MatSlideToggleModule, + ], }) -export class RoutingRulesetComponent { +export class CandidatesComponent { private dialog = inject(DialogService); - private routingRulesetService = inject(RoutingRulesetService); + private routingRulesetService = inject(CandidatesService); private routingRulesService = inject(RoutingRulesService); private routingRulesStoreService = inject(RoutingRulesStoreService); private domainService = inject(DomainService); @@ -50,6 +124,11 @@ export class RoutingRulesetComponent { private route = inject(ActivatedRoute); private sidenavInfoService = inject(SidenavInfoService); private destroyRef = inject(DestroyRef); + protected appMode = inject(AppModeService); + protected domainObjectsStoreService = inject(DomainObjectsStoreService); + private injector = inject(Injector); + + private updateCandidateGroups$ = new BehaviorSubject(undefined); ruleset$ = this.routingRulesetService.ruleset$; partyID$ = this.routingRulesetService.partyID$; @@ -60,6 +139,88 @@ export class RoutingRulesetComponent { candidates$ = this.routingRulesetService.ruleset$.pipe(map((r) => r.data.decisions.candidates)); isLoading$ = this.routingRulesStoreService.isLoading$; + candidateGroups$: Observable[][]> = combineLatest([ + this.candidates$, + this.candidates$.pipe( + switchMap((candidates) => + this.domainService.get( + candidates.map((c) => ({ terminal: { id: c.terminal.id } })), + ), + ), + ), + this.routingRulesType$, + this.updateCandidateGroups$, + ]).pipe( + map(([candidates, terminals, type]) => { + const groups = candidates.reduce((groups, candidate, idx) => { + const terminal = terminals.find( + (t) => t.object.terminal.ref.id === candidate.terminal.id, + ); + const terms = terminal?.object?.terminal?.data?.terms; + const globalAllowPredicate = + type === RoutingRulesType.Payment + ? terms?.payments?.global_allow + : terms?.wallet?.withdrawals?.global_allow; + const newItem = { + idx, + candidate, + terminal, + + allowed: getPredicateBoolean(candidate.allowed), + globalAllow: getPredicateBoolean(globalAllowPredicate), + + fullAllowedStr: [ + upperFirst( + getAllowStr(candidate.allowed, !!getAllowStr(globalAllowPredicate)), + ), + getAllowStr(globalAllowPredicate) && + `(Global ${upperFirst(getAllowStr(globalAllowPredicate))})`, + ] + .filter(Boolean) + .join(' '), + }; + if (groups.has(candidate.priority)) { + groups.get(candidate.priority)?.push(newItem); + } else { + groups.set(candidate.priority, [newItem]); + } + return groups; + }, new Map()); + return Array.from(groups.values()) + .map((group) => { + const sum = group.reduce( + (acc, item) => + acc + (item.candidate.allowed ? item.candidate.weight || 0 : 0), + 0, + ); + const allowedCount = group.filter((item) => item.allowed).length; + return group.map((item) => { + const weight = item.candidate.weight || 0; + let weightPercent = 0; + if (item.allowed === false) { + weightPercent = 0; + } else if (allowedCount === 1) { + weightPercent = 100; + } else if (sum > 0) { + weightPercent = Math.round((weight / sum) * 100); + } else { + weightPercent = 0; + } + return { + value: { ...item, weightPercent }, + width: weightPercent, + disabled: item.globalAllow === false || item.allowed === false, + }; + }); + }) + .sort( + (a, b) => + (b[0].value.candidate.priority || 0) - (a[0].value.candidate.priority || 0), + ); + }), + shareReplay({ refCount: true, bufferSize: 1 }), + ); + columns: Column[] = [ { field: 'priority' }, { @@ -268,14 +429,61 @@ export class RoutingRulesetComponent { }); } - toggleAllow(candidateIdx: number) { + edit(idx: number) { this.routingRulesetService.refID$ - .pipe(first(), takeUntilDestroyed(this.destroyRef)) - .subscribe((refId) => { - changeCandidatesAllowed([{ refId, candidateIdx }]); + .pipe( + switchCombineWith((refId) => [ + this.routingRulesService.getCandidate(refId, idx), + this.candidates$, + ]), + first(), + switchCombineWith(([_, candidate, candidates]) => [ + this.dialog + .open(EditCandidateDialogComponent, { + candidate, + othersWeight: candidates.reduce( + (acc, c, cIdx) => + acc + + (c.priority === candidate.priority && idx !== cIdx + ? c.weight || 0 + : 0), + 0, + ), + }) + .afterClosed(), + ]), + switchMap(([[refId], res]) => + res.status === DialogResponseStatus.Success + ? this.routingRulesService.updateRules([ + { refId, candidateIdx: idx, newCandidate: res.data }, + ]) + : of(null), + ), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe({ + next: (res) => { + if (res) { + this.routingRulesStoreService.reload(); + this.log.successOperation('update', 'Candidate'); + } + }, + error: (err) => { + this.log.error(err); + }, }); } + toggleAllow(candidateIdx: number) { + runInInjectionContext(this.injector, () => + this.routingRulesetService.refID$ + .pipe(first(), takeUntilDestroyed(this.destroyRef)) + .subscribe((refId) => { + changeCandidatesAllowed([{ refId, candidateIdx }]); + }), + ); + } + removeRule(idx: number) { this.routingRulesetService.removeRule(idx); } @@ -335,6 +543,57 @@ export class RoutingRulesetComponent { }); } + cardDrop(newRows: Item[][]) { + const maxPriority = newRows.length; + const newCandidates: RoutingCandidate[] = newRows + .map((group, idx) => { + const priority = maxPriority - idx; + return group.map((item) => ({ + ...item.value.candidate, + priority, + })); + }) + .flat(); + this.candidates$ + .pipe( + first(), + switchMap((candidates) => + this.dialog + .open(UpdateThriftDialogComponent, { + object: newCandidates, + prevObject: candidates, + }) + .afterClosed(), + ), + withLatestFrom(this.routingRulesetService.ruleset$), + switchMap(([res, ruleset]) => { + if (res.status === DialogResponseStatus.Success) { + const newRuleset = cloneDeep(ruleset); + newRuleset.data.decisions.candidates = res.data + .object as RoutingCandidate[]; + return this.routingRulesStoreService.commit([ + { update: { object: { routing_rules: newRuleset } } }, + ]); + } else { + return of(null); + } + }), + ) + .subscribe({ + next: (res) => { + if (res) { + this.log.successOperation('update', 'candidates'); + this.routingRulesStoreService.reload(); + } else { + this.updateCandidateGroups$.next(); + } + }, + error: (err) => { + this.log.error(err); + }, + }); + } + openRefId() { this.ruleset$.pipe(take(1), filter(Boolean)).subscribe(({ ref }) => { this.sidenavInfoService.toggle(DomainObjectCardComponent, { diff --git a/src/app/parties/party/routing-rules/routing-ruleset/routing-ruleset.service.ts b/src/app/parties/party/routing-rules/candidates/candidates.service.ts similarity index 98% rename from src/app/parties/party/routing-rules/routing-ruleset/routing-ruleset.service.ts rename to src/app/parties/party/routing-rules/candidates/candidates.service.ts index 874331f3a..68bcfb05e 100644 --- a/src/app/parties/party/routing-rules/routing-ruleset/routing-ruleset.service.ts +++ b/src/app/parties/party/routing-rules/candidates/candidates.service.ts @@ -15,7 +15,7 @@ import { import { RoutingRulesService as RoutingRulesDamselService } from '../services/routing-rules'; @Injectable() -export class RoutingRulesetService { +export class CandidatesService { private routingRulesService = inject(RoutingRulesDamselService); private route = inject(ActivatedRoute); private log = inject(NotifyLogService); diff --git a/src/app/parties/party/routing-rules/candidates/components/dnd-cards/dnd-cards.component.html b/src/app/parties/party/routing-rules/candidates/components/dnd-cards/dnd-cards.component.html new file mode 100644 index 000000000..5b74c25d4 --- /dev/null +++ b/src/app/parties/party/routing-rules/candidates/components/dnd-cards/dnd-cards.component.html @@ -0,0 +1,53 @@ +
+
+ @for (row of rows(); track $index) { +
+ @for (item of row; track item) { + + +
+ +
+
+
+ } +
+
+ } +
diff --git a/src/app/parties/party/routing-rules/candidates/components/dnd-cards/dnd-cards.component.scss b/src/app/parties/party/routing-rules/candidates/components/dnd-cards/dnd-cards.component.scss new file mode 100644 index 000000000..ae803ae1a --- /dev/null +++ b/src/app/parties/party/routing-rules/candidates/components/dnd-cards/dnd-cards.component.scss @@ -0,0 +1,33 @@ +$v-gap: 22px; +$insert-card-margin: 2px; + +.insert-zone { + min-height: $v-gap; +} + +.cdk-drag-preview { + opacity: 0.8; +} + +.cdk-drag-placeholder { + border-style: dashed; + + .content { + visibility: hidden; + } + + .insert-zone & { + $height: $v-gap - ($insert-card-margin * 2); + + margin: $insert-card-margin 0; + height: $height; + } +} + +.cdk-drag-animating { + transition: transform 300ms cubic-bezier(0, 0, 0.2, 1); +} + +.disabled { + background-color: var(--mat-sys-surface-variant); +} diff --git a/src/app/parties/party/routing-rules/candidates/components/dnd-cards/dnd-cards.component.ts b/src/app/parties/party/routing-rules/candidates/components/dnd-cards/dnd-cards.component.ts new file mode 100644 index 000000000..895e600ac --- /dev/null +++ b/src/app/parties/party/routing-rules/candidates/components/dnd-cards/dnd-cards.component.ts @@ -0,0 +1,97 @@ +import { + CdkDrag, + CdkDragDrop, + CdkDropList, + CdkDropListGroup, + moveItemInArray, + transferArrayItem, +} from '@angular/cdk/drag-drop'; +import { CommonModule, NgTemplateOutlet } from '@angular/common'; +import { + Component, + TemplateRef, + contentChild, + effect, + model, + output, + untracked, +} from '@angular/core'; +import { MatCardModule } from '@angular/material/card'; + +export interface Item { + value: T; + width?: number; + disabled?: boolean; +} + +@Component({ + selector: 'cc-dnd-cards', + templateUrl: 'dnd-cards.component.html', + styleUrl: 'dnd-cards.component.scss', + imports: [ + CommonModule, + NgTemplateOutlet, + CdkDropListGroup, + CdkDropList, + CdkDrag, + MatCardModule, + ], +}) +export class DndCardsComponent { + rows = model.required[][]>(); + cardTpl = contentChild.required>('card'); + isDragging = false; + zoneData: Item[] = []; + dropped = output[][]>(); + + constructor() { + effect(() => { + const rows = this.rows(); + const sorted = rows.map((r) => this.sortRow(r)); + const changed = sorted.some((r, i) => r.some((item, j) => item !== rows[i][j])); + if (changed) untracked(() => this.rows.set(sorted)); + }); + } + + dropInRow(event: CdkDragDrop[]>) { + const rows = this.rows(); + if (event.previousContainer === event.container) { + moveItemInArray(event.container.data, event.previousIndex, event.currentIndex); + } else { + transferArrayItem( + event.previousContainer.data, + event.container.data, + event.previousIndex, + event.currentIndex, + ); + } + this.rows.set(rows.filter((r) => r.length > 0).map((r) => this.sortRow(r))); + this.drop(); + } + + dropInZone(event: CdkDragDrop[]>, insertIndex: number) { + const rows = this.rows(); + const source = event.previousContainer.data; + const [item] = source.splice(event.previousIndex, 1); + const sourceRowIndex = rows.indexOf(source); + let adjusted = insertIndex; + if (source.length === 0 && sourceRowIndex !== -1 && sourceRowIndex < insertIndex) { + adjusted--; + } + const filtered = rows.filter((r) => r.length > 0); + filtered.splice(adjusted, 0, [item]); + this.rows.set(filtered.map((r) => this.sortRow(r))); + this.drop(); + } + + drop() { + this.dropped.emit(this.rows()); + } + + private sortRow(row: Item[]): Item[] { + return [...row].sort((a, b) => { + if (a.disabled !== b.disabled) return a.disabled ? 1 : -1; + return (b.width ?? 0) - (a.width ?? 0); + }); + } +} diff --git a/src/app/parties/party/routing-rules/candidates/components/edit-candidate-dialog/edit-candidate-dialog.component.html b/src/app/parties/party/routing-rules/candidates/components/edit-candidate-dialog/edit-candidate-dialog.component.html new file mode 100644 index 000000000..787dc014c --- /dev/null +++ b/src/app/parties/party/routing-rules/candidates/components/edit-candidate-dialog/edit-candidate-dialog.component.html @@ -0,0 +1,31 @@ + +
+ + +
+ + +
+ +
+ + + +
diff --git a/src/app/parties/party/routing-rules/candidates/components/edit-candidate-dialog/edit-candidate-dialog.component.ts b/src/app/parties/party/routing-rules/candidates/components/edit-candidate-dialog/edit-candidate-dialog.component.ts new file mode 100644 index 000000000..c05383da2 --- /dev/null +++ b/src/app/parties/party/routing-rules/candidates/components/edit-candidate-dialog/edit-candidate-dialog.component.ts @@ -0,0 +1,104 @@ +import { round } from 'lodash-es'; +import { distinctUntilChanged, map, shareReplay } from 'rxjs'; + +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; + +import { RoutingCandidate } from '@vality/domain-proto/domain'; +import { + DialogModule, + DialogSuperclass, + InputFieldModule, + getValue, + getValueChanges, +} from '@vality/matez'; + +import { + DomainObjectFieldComponent, + DomainThriftFormComponent, +} from '~/components/thrift-api-crud'; + +@Component({ + selector: 'cc-edit-candidate-dialog', + templateUrl: 'edit-candidate-dialog.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + DialogModule, + ReactiveFormsModule, + MatButtonModule, + InputFieldModule, + DomainObjectFieldComponent, + DomainThriftFormComponent, + ], +}) +export class EditCandidateDialogComponent + extends DialogSuperclass< + EditCandidateDialogComponent, + { candidate: RoutingCandidate; othersWeight: number }, + RoutingCandidate + > + implements OnInit +{ + private fb = inject(FormBuilder); + private dr = inject(DestroyRef); + + form = this.fb.nonNullable.group({ + terminal: this.dialogData.candidate.terminal.id, + description: this.dialogData.candidate.description, + weight: this.dialogData.candidate.weight, + weightPercent: this.calcPercent(this.dialogData.candidate.weight), + allowed: this.dialogData.candidate.allowed, + }); + percent$ = getValueChanges(this.form.controls.weight).pipe( + distinctUntilChanged(), + map((weight) => this.calcPercent(Number(weight || 0))), + shareReplay({ bufferSize: 1, refCount: true }), + ); + weight$ = getValueChanges(this.form.controls.weightPercent).pipe( + distinctUntilChanged(), + map((weightPercent) => this.calcWeight(Number(weightPercent || 0))), + shareReplay({ bufferSize: 1, refCount: true }), + ); + + ngOnInit() { + this.weight$.pipe(takeUntilDestroyed(this.dr)).subscribe((weight) => { + this.form.controls.weight.setValue(Math.round(weight), { + emitEvent: false, + }); + }); + this.percent$.pipe(takeUntilDestroyed(this.dr)).subscribe((weightPercent) => { + this.form.controls.weightPercent.setValue(weightPercent, { + emitEvent: false, + }); + }); + } + + confirm() { + const { terminal, ...value } = getValue(this.form); + this.closeWithSuccess({ + ...this.dialogData.candidate, + terminal: { id: terminal }, + ...value, + }); + } + + private calcPercent(weight: number): number { + const res = this.dialogData.othersWeight + ? (weight / (this.dialogData.othersWeight + weight)) * 100 + : 100; + return round(Math.max(Math.min(res, 100), 0), 2); + } + + private calcWeight(weightPercent: number): number { + const res = this.dialogData.othersWeight + ? (weightPercent * this.dialogData.othersWeight) / (100 - weightPercent) + : weightPercent + ? 1 + : 0; + return round(Math.max(Math.min(res, 1_000_000), 0), 2); + } +} diff --git a/src/app/parties/party/routing-rules/candidates/index.ts b/src/app/parties/party/routing-rules/candidates/index.ts new file mode 100644 index 000000000..9ed61249d --- /dev/null +++ b/src/app/parties/party/routing-rules/candidates/index.ts @@ -0,0 +1 @@ +export * from './candidates.component'; diff --git a/src/app/parties/party/routing-rules/components/routing-rules-list/routing-rules-list.component.html b/src/app/parties/party/routing-rules/components/routing-rules-list/routing-rules-list.component.html index ddb6448fc..df152c828 100644 --- a/src/app/parties/party/routing-rules/components/routing-rules-list/routing-rules-list.component.html +++ b/src/app/parties/party/routing-rules/components/routing-rules-list/routing-rules-list.component.html @@ -2,6 +2,7 @@ [columns]="columns()" [data]="data" [progress]="progress" + [sort]="{ active: 'name', direction: 'asc' }" name="routingRulesList" standaloneFilter > diff --git a/src/app/parties/party/routing-rules/party-delegate-rulesets/party-delegate-rulesets-routing.module.ts b/src/app/parties/party/routing-rules/party-delegate-rulesets/party-delegate-rulesets-routing.module.ts index 65a285a9d..7ab2ba883 100644 --- a/src/app/parties/party/routing-rules/party-delegate-rulesets/party-delegate-rulesets-routing.module.ts +++ b/src/app/parties/party/routing-rules/party-delegate-rulesets/party-delegate-rulesets-routing.module.ts @@ -1,7 +1,9 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; -import { canActivateAuthRole } from '~/services'; +import { Service, canActivateAuthRole } from '~/services'; + +import { CandidatesComponent } from '../candidates'; import { PartyDelegateRulesetsComponent } from './party-delegate-rulesets.component'; import { ROUTING_CONFIG } from './routing-config'; @@ -26,9 +28,10 @@ import { ROUTING_CONFIG } from './routing-config'; ), }, { - path: '', - loadChildren: () => - import('../routing-ruleset').then((m) => m.RoutingRulesetModule), + path: ':partyRefID/delegate/:refID', + component: CandidatesComponent, + canActivate: [canActivateAuthRole], + data: { services: [Service.DMT] }, }, ], }, diff --git a/src/app/parties/party/routing-rules/party-routing-ruleset/party-routing-ruleset.component.ts b/src/app/parties/party/routing-rules/party-routing-ruleset/party-routing-ruleset.component.ts index 8ee283096..5a2e51820 100644 --- a/src/app/parties/party/routing-rules/party-routing-ruleset/party-routing-ruleset.component.ts +++ b/src/app/parties/party/routing-rules/party-routing-ruleset/party-routing-ruleset.component.ts @@ -1,12 +1,13 @@ import { combineLatest } from 'rxjs'; import { filter, first, map, shareReplay, startWith, switchMap, take } from 'rxjs/operators'; -import { Component, DestroyRef, inject } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Component, DestroyRef, Injector, inject } from '@angular/core'; +import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop'; import { ActivatedRoute, Router } from '@angular/router'; import { RoutingDelegate } from '@vality/domain-proto/domain'; import { + AppModeService, Column, DialogResponseStatus, DialogService, @@ -17,7 +18,7 @@ import { import { RoutingRulesStoreService } from '~/api/domain-config'; import { SidenavInfoService } from '~/components/sidenav-info'; import { DomainObjectCardComponent } from '~/components/thrift-api-crud/domain'; -import { createShopColumn, createWalletColumn } from '~/utils'; +import { createDomainObjectColumn, createShopColumn, createWalletColumn } from '~/utils'; import { RoutingRulesListItem } from '../components/routing-rules-list'; import { PartyDelegateRulesetsService } from '../party-delegate-rulesets'; @@ -44,6 +45,8 @@ export class PartyRoutingRulesetComponent { private sidenavInfoService = inject(SidenavInfoService); private log = inject(NotifyLogService); protected routingRulesTypeService = inject(RoutingRulesTypeService); + private appMode = inject(AppModeService); + private injector = inject(Injector); partyRuleset$ = this.partyRoutingRulesetService.partyRuleset$; partyID$ = this.partyRoutingRulesetService.partyID$; @@ -56,43 +59,47 @@ export class PartyRoutingRulesetComponent { shareReplay({ refCount: true, bufferSize: 1 }), ); + private baseColumns: Column>[] = [ + createDomainObjectColumn((d) => ({ ref: { routing_rules: d.item?.ruleset } }), { + header: 'Delegate', + hidden: toObservable(this.appMode.isSimple, { injector: this.injector }), + }), + ]; shopsDisplayedColumns: Column>[] = [ - { - field: 'id', - header: 'Delegate (Ruleset Ref ID)', - cell: (d) => ({ - value: d.item?.description || `#${d.item?.ruleset?.id}`, - description: d.item?.ruleset?.id, - click: () => this.navigateToDelegate(d.parentRefId, d.delegateIdx), - }), - }, - createShopColumn((d) => - this.partyRoutingRulesetService.partyID$.pipe( - map((partyId) => ({ - shopId: d.item?.allowed?.condition?.party?.definition?.shop_is, - partyId, - })), - ), + createShopColumn( + (d) => + this.partyRoutingRulesetService.partyID$.pipe( + map((partyId) => ({ + shopId: d.item?.allowed?.condition?.party?.definition?.shop_is, + partyId, + })), + ), + { + field: 'name', + cell: (d) => ({ + click: () => this.navigateToDelegate(d.parentRefId, d.delegateIdx), + }), + }, ), + ...this.baseColumns, ]; walletsDisplayedColumns: Column>[] = [ - { - field: 'id', - header: 'Delegate (Ruleset Ref ID)', - cell: (d) => ({ - value: d.item?.description || `#${d.item?.ruleset?.id}`, - description: d.item?.ruleset?.id, - click: () => this.navigateToDelegate(d.parentRefId, d.delegateIdx), - }), - }, - createWalletColumn((d) => - this.partyRoutingRulesetService.partyID$.pipe( - map((partyId) => ({ - id: d.item?.allowed?.condition?.party?.definition?.wallet_is, - partyId, - })), - ), + createWalletColumn( + (d) => + this.partyRoutingRulesetService.partyID$.pipe( + map((partyId) => ({ + id: d.item?.allowed?.condition?.party?.definition?.wallet_is, + partyId, + })), + ), + { + field: 'name', + cell: (d) => ({ + click: () => this.navigateToDelegate(d.parentRefId, d.delegateIdx), + }), + }, ), + ...this.baseColumns, ]; shopsData$ = this.partyRuleset$.pipe( filter(Boolean), diff --git a/src/app/parties/party/routing-rules/routing-ruleset/index.ts b/src/app/parties/party/routing-rules/routing-ruleset/index.ts deleted file mode 100644 index 9b2055501..000000000 --- a/src/app/parties/party/routing-rules/routing-ruleset/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './routing-ruleset.module'; diff --git a/src/app/parties/party/routing-rules/routing-ruleset/routing-config.ts b/src/app/parties/party/routing-rules/routing-ruleset/routing-config.ts deleted file mode 100644 index fa8001e6f..000000000 --- a/src/app/parties/party/routing-rules/routing-ruleset/routing-config.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { RoutingConfig, Service } from '~/services'; - -export const ROUTING_CONFIG: RoutingConfig = { - services: [Service.DMT], -}; diff --git a/src/app/parties/party/routing-rules/routing-ruleset/routing-ruleset-routing.module.ts b/src/app/parties/party/routing-rules/routing-ruleset/routing-ruleset-routing.module.ts deleted file mode 100644 index ff5979916..000000000 --- a/src/app/parties/party/routing-rules/routing-ruleset/routing-ruleset-routing.module.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; - -import { canActivateAuthRole } from '~/services'; - -import { ROUTING_CONFIG } from './routing-config'; -import { RoutingRulesetComponent } from './routing-ruleset.component'; - -@NgModule({ - imports: [ - RouterModule.forChild([ - { - path: ':partyRefID/delegate/:refID', - component: RoutingRulesetComponent, - canActivate: [canActivateAuthRole], - data: ROUTING_CONFIG, - }, - ]), - ], -}) -export class RoutingRulesetRoutingModule {} diff --git a/src/app/parties/party/routing-rules/routing-ruleset/routing-ruleset.component.html b/src/app/parties/party/routing-rules/routing-ruleset/routing-ruleset.component.html deleted file mode 100644 index 98a085023..000000000 --- a/src/app/parties/party/routing-rules/routing-ruleset/routing-ruleset.component.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - diff --git a/src/app/parties/party/routing-rules/routing-ruleset/routing-ruleset.module.ts b/src/app/parties/party/routing-rules/routing-ruleset/routing-ruleset.module.ts deleted file mode 100644 index e5471d289..000000000 --- a/src/app/parties/party/routing-rules/routing-ruleset/routing-ruleset.module.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { ReactiveFormsModule } from '@angular/forms'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCardModule } from '@angular/material/card'; -import { MatDialogModule } from '@angular/material/dialog'; -import { MatDividerModule } from '@angular/material/divider'; -import { MatExpansionModule } from '@angular/material/expansion'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; -import { MatMenuModule } from '@angular/material/menu'; -import { MatPaginatorModule } from '@angular/material/paginator'; -import { MatRadioModule } from '@angular/material/radio'; -import { MatSelectModule } from '@angular/material/select'; -import { RouterModule } from '@angular/router'; - -import { DialogModule, TableModule } from '@vality/matez'; -import { ThriftViewerModule } from '@vality/ng-thrift'; - -import { PageLayoutModule } from '~/components/page-layout'; -import { DomainThriftViewerComponent } from '~/components/thrift-api-crud'; - -import { RoutingRulesetRoutingModule } from './routing-ruleset-routing.module'; -import { RoutingRulesetComponent } from './routing-ruleset.component'; - -@NgModule({ - imports: [ - RoutingRulesetRoutingModule, - CommonModule, - MatButtonModule, - MatDialogModule, - MatDividerModule, - ReactiveFormsModule, - MatFormFieldModule, - MatInputModule, - RouterModule, - MatIconModule, - MatMenuModule, - MatPaginatorModule, - MatCardModule, - MatSelectModule, - MatRadioModule, - MatExpansionModule, - MatAutocompleteModule, - TableModule, - DomainThriftViewerComponent, - ThriftViewerModule, - DialogModule, - PageLayoutModule, - ], - declarations: [RoutingRulesetComponent], -}) -export class RoutingRulesetModule {} diff --git a/src/components/thrift-api-crud/domain/domain-object-field/domain-object-field.component.ts b/src/components/thrift-api-crud/domain/domain-object-field/domain-object-field.component.ts index 1493a89ff..542ff0fad 100644 --- a/src/components/thrift-api-crud/domain/domain-object-field/domain-object-field.component.ts +++ b/src/components/thrift-api-crud/domain/domain-object-field/domain-object-field.component.ts @@ -45,7 +45,9 @@ export class DomainObjectFieldComponent extends FormC objs.map((obj) => ({ value: getReferenceId(obj.ref), label: obj.name || `#${getReferenceId(obj.ref)}`, - description: obj.description, + description: [`#${getReferenceId(obj.ref)}`, obj.description] + .filter(Boolean) + .join(' - '), })), ), ); diff --git a/src/utils/thrift/provide-thrift-services.ts b/src/utils/thrift/provide-thrift-services.ts index 2cc0ca47a..cee4c9361 100644 --- a/src/utils/thrift/provide-thrift-services.ts +++ b/src/utils/thrift/provide-thrift-services.ts @@ -87,10 +87,8 @@ const logger: ConnectOptions['loggingFn'] = (params) => { if (LOGGING.fullLogging) { console.log('Arguments'); console.log(JSON.stringify(toJson(params.args), null, 2)); - - console.groupCollapsed('Headers'); - console.table(params.headers); - console.groupEnd(); + console.log('Headers'); + console.log(params.headers); } console.groupEnd(); return; @@ -102,10 +100,8 @@ const logger: ConnectOptions['loggingFn'] = (params) => { console.log(JSON.stringify(toJson(params.args), null, 2)); console.log('Response'); console.log(JSON.stringify(toJson(params.response), null, 2)); - - console.groupCollapsed('Headers'); - console.table(params.headers); - console.groupEnd(); + console.log('Headers'); + console.log(params.headers); console.groupEnd(); } return;