diff --git a/package-lock.json b/package-lock.json index 9b52ccff4..e3b434a95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,10 +22,10 @@ "@angular/router": "^20.1.0", "@ngneat/input-mask": "^6.1.0", "@tailwindcss/postcss": "^4.1.11", - "@vality/domain-proto": "^2.0.1-7a97267.0", + "@vality/domain-proto": "^2.0.1-5c25c2e.0", "@vality/fistful-proto": "^2.0.1-7c61ac5.0", "@vality/machinegun-proto": "^1.0.1-cc2c27c.0", - "@vality/magista-proto": "^2.0.2-2de1ebf.0", + "@vality/magista-proto": "^2.0.2-6cafe01.0", "@vality/ng-monaco-editor": "^20.0.0", "@vality/repairer-proto": "^2.0.2-1a48729.0", "@vality/scrooge-proto": "^0.1.1-42aba67.0", @@ -5955,9 +5955,9 @@ } }, "node_modules/@vality/domain-proto": { - "version": "2.0.1-7a97267.0", - "resolved": "https://registry.npmjs.org/@vality/domain-proto/-/domain-proto-2.0.1-7a97267.0.tgz", - "integrity": "sha512-77vfl9TEjn2ZTSWLNAQNsgDSWhr6oWr5gLze98HPMUGgCC2Sg886e0VWe7KXZcluZ+mgy7G6cSChvE7D925v8g==", + "version": "2.0.1-5c25c2e.0", + "resolved": "https://registry.npmjs.org/@vality/domain-proto/-/domain-proto-2.0.1-5c25c2e.0.tgz", + "integrity": "sha512-sqlfDYQbIu7kJX45JOEVrO6RwtkP7E/9M9AbYkYZ/n77yZTJQr4ZPQFyttGXiIyr4QjgkKBU/sW+W1IzeIZDaw==", "license": "Apache-2.0" }, "node_modules/@vality/fistful-proto": { @@ -5973,9 +5973,9 @@ "license": "Apache-2.0" }, "node_modules/@vality/magista-proto": { - "version": "2.0.2-2de1ebf.0", - "resolved": "https://registry.npmjs.org/@vality/magista-proto/-/magista-proto-2.0.2-2de1ebf.0.tgz", - "integrity": "sha512-kldv1gnEysjiBs1EnDG67wG3TcXrL9tRMT0/qgwmh9iPumtOW6BkXibc/VrcexeAsNRiM6R718YgYOJFF1+Rsw==", + "version": "2.0.2-6cafe01.0", + "resolved": "https://registry.npmjs.org/@vality/magista-proto/-/magista-proto-2.0.2-6cafe01.0.tgz", + "integrity": "sha512-/r8bjvRBBqkMJ8rrbtcgj8NdNZ1SJfYqHPMrUTUp5B2MVAq6wW923O5v1Q4slPjJ5q6dTEeBH+mJwSeBiVecDA==", "license": "Apache-2.0" }, "node_modules/@vality/matez": { diff --git a/package.json b/package.json index 88f7bc18a..d27bf2bca 100644 --- a/package.json +++ b/package.json @@ -33,10 +33,10 @@ "@angular/router": "^20.1.0", "@ngneat/input-mask": "^6.1.0", "@tailwindcss/postcss": "^4.1.11", - "@vality/domain-proto": "^2.0.1-7a97267.0", + "@vality/domain-proto": "^2.0.1-5c25c2e.0", "@vality/fistful-proto": "^2.0.1-7c61ac5.0", "@vality/machinegun-proto": "^1.0.1-cc2c27c.0", - "@vality/magista-proto": "^2.0.2-2de1ebf.0", + "@vality/magista-proto": "^2.0.2-6cafe01.0", "@vality/ng-monaco-editor": "^20.0.0", "@vality/repairer-proto": "^2.0.2-1a48729.0", "@vality/scrooge-proto": "^0.1.1-42aba67.0", diff --git a/src/app/parties/parties.component.ts b/src/app/parties/parties.component.ts index 551d303c9..9e2a826c8 100644 --- a/src/app/parties/parties.component.ts +++ b/src/app/parties/parties.component.ts @@ -42,6 +42,14 @@ export class PartiesComponent implements OnInit { field: 'id', cell: (party) => ({ value: party.ref.id }), }, + { + field: 'name', + cell: (party) => ({ + value: party.data.name, + description: party.data.description, + link: () => `/parties/${party.ref.id}`, + }), + }, { field: 'email', cell: (party) => ({ @@ -49,7 +57,6 @@ export class PartiesComponent implements OnInit { description: (party.data.contact_info.manager_contact_emails || []) .filter(Boolean) .join(', '), - link: () => `/parties/${party.ref.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 index ff74ef072..565b29d5b 100644 --- a/src/app/parties/party/routing-rules/candidates/candidates.component.html +++ b/src/app/parties/party/routing-rules/candidates/candidates.component.html @@ -76,7 +76,7 @@ - diff --git a/src/app/parties/party/routing-rules/candidates/candidates.component.ts b/src/app/parties/party/routing-rules/candidates/candidates.component.ts index 94a3b25e1..7d59a5ef7 100644 --- a/src/app/parties/party/routing-rules/candidates/candidates.component.ts +++ b/src/app/parties/party/routing-rules/candidates/candidates.component.ts @@ -123,7 +123,7 @@ export class CandidatesComponent { private log = inject(NotifyLogService); private route = inject(ActivatedRoute); private sidenavInfoService = inject(SidenavInfoService); - private destroyRef = inject(DestroyRef); + private dr = inject(DestroyRef); protected appMode = inject(AppModeService); protected domainObjectsStoreService = inject(DomainObjectsStoreService); private injector = inject(Injector); @@ -188,24 +188,10 @@ export class CandidatesComponent { }, 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; + const sum = group.reduce((acc, item) => acc + (item.candidate.weight || 0), 0); 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; - } + const weightPercent = sum ? Math.round((weight / sum) * 100) : 0; return { value: { ...item, weightPercent }, width: weightPercent, @@ -269,7 +255,7 @@ export class CandidatesComponent { predicate: d.allowed, toggle: () => { this.getCandidateIdx(d) - .pipe(takeUntilDestroyed(this.destroyRef)) + .pipe(takeUntilDestroyed(this.dr)) .subscribe((idx) => { void this.toggleAllow(idx); }); @@ -293,9 +279,9 @@ export class CandidatesComponent { label: 'Edit', click: () => { this.getCandidateIdx(d) - .pipe(takeUntilDestroyed(this.destroyRef)) + .pipe(takeUntilDestroyed(this.dr)) .subscribe((idx) => { - this.editRule(idx); + this.advancedEdit(idx); }); }, }, @@ -303,7 +289,7 @@ export class CandidatesComponent { label: 'Duplicate', click: () => { this.getCandidateIdx(d) - .pipe(takeUntilDestroyed(this.destroyRef)) + .pipe(takeUntilDestroyed(this.dr)) .subscribe((idx) => { void this.duplicateRule(idx); }); @@ -313,7 +299,7 @@ export class CandidatesComponent { label: getPredicateBoolean(d.allowed) ? 'Deny' : 'Allow', click: () => { this.getCandidateIdx(d) - .pipe(takeUntilDestroyed(this.destroyRef)) + .pipe(takeUntilDestroyed(this.dr)) .subscribe((idx) => { void this.toggleAllow(idx); }); @@ -323,7 +309,7 @@ export class CandidatesComponent { label: 'Remove', click: () => { this.getCandidateIdx(d) - .pipe(takeUntilDestroyed(this.destroyRef)) + .pipe(takeUntilDestroyed(this.dr)) .subscribe((idx) => { void this.removeRule(idx); }); @@ -349,7 +335,7 @@ export class CandidatesComponent { .afterClosed(), ), ) - .pipe(takeUntilDestroyed(this.destroyRef)) + .pipe(takeUntilDestroyed(this.dr)) .subscribe({ next: (res) => { if (res.status === DialogResponseStatus.Success) { @@ -363,7 +349,7 @@ export class CandidatesComponent { }); } - editRule(idx: number) { + advancedEdit(idx: number) { this.routingRulesetService.refID$ .pipe( first(), @@ -383,7 +369,7 @@ export class CandidatesComponent { .afterClosed(), ), ) - .pipe(takeUntilDestroyed(this.destroyRef)) + .pipe(takeUntilDestroyed(this.dr)) .subscribe({ next: (res) => { if (res.status === DialogResponseStatus.Success) { @@ -415,7 +401,7 @@ export class CandidatesComponent { .afterClosed(), ), ) - .pipe(takeUntilDestroyed(this.destroyRef)) + .pipe(takeUntilDestroyed(this.dr)) .subscribe({ next: (res) => { if (res.status === DialogResponseStatus.Success) { @@ -430,46 +416,34 @@ export class CandidatesComponent { } edit(candidateIdx: number) { - this.routingRulesetService.refID$ + combineLatest([this.routingRulesetService.refID$, this.candidates$]) .pipe( - switchCombineWith((refId) => [ - this.routingRulesService.getCandidate(refId, candidateIdx), - this.candidates$, - ]), first(), - switchCombineWith(([_, candidate, candidates]) => { - const others = candidates.filter( - (c) => c !== candidate && c.priority === candidate.priority && c.allowed, - ); - const ids = others.map((c) => candidates.findIndex((cd) => cd === c)); - return [ - this.dialog - .open(EditCandidateDialogComponent, { - candidate, - others, - }) - .afterClosed(), - ids, - others, - ]; - }), - switchMap(([[refId], res, ids]) => + switchCombineWith(([_, candidates]) => [ + this.dialog + .open(EditCandidateDialogComponent, { + candidates: candidates + .map((candidate, idx) => ({ idx, candidate })) + .filter( + (c) => + c.candidate.priority === candidates[candidateIdx].priority, + ), + idx: candidateIdx, + }) + .afterClosed(), + ]), + switchMap(([[refId], res]) => res.status === DialogResponseStatus.Success - ? this.routingRulesService.updateRules([ - { - refId, - candidateIdx: candidateIdx, - newCandidate: res.data.candidate, - }, - ...res.data.others.map((newCandidate, idx) => ({ + ? this.routingRulesService.updateRules( + res.data.candidates.map((c) => ({ refId, - candidateIdx: ids[idx], - newCandidate, + candidateIdx: c.idx, + newCandidate: c.candidate, })), - ]) + ) : of(null), ), - takeUntilDestroyed(this.destroyRef), + takeUntilDestroyed(this.dr), ) .subscribe({ next: (res) => { @@ -487,7 +461,7 @@ export class CandidatesComponent { toggleAllow(candidateIdx: number) { runInInjectionContext(this.injector, () => this.routingRulesetService.refID$ - .pipe(first(), takeUntilDestroyed(this.destroyRef)) + .pipe(first(), takeUntilDestroyed(this.dr)) .subscribe((refId) => { changeCandidatesAllowed([{ refId, candidateIdx }]); }), 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 index 81bfae796..314968616 100644 --- 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 @@ -4,17 +4,31 @@
- + label="Type" + > +
+
+
+ @for (item of weightsPreview$ | async; track $index; let isFirst = $first) { +
+ {{ item.percent }} +
+ } +
- implements OnInit -{ +export class EditCandidateDialogComponent extends DialogSuperclass< + EditCandidateDialogComponent, + { candidates: CandidateWithIdx[]; idx: number }, + { candidates: CandidateWithIdx[]; idx: number } +> { private fb = inject(FormBuilder); - private dr = inject(DestroyRef); - private othersWeight = this.dialogData.others.reduce((acc, c) => acc + (c.weight || 0), 0); + private currentCandidate = this.dialogData.candidates.find((c) => c.idx === this.dialogData.idx) + .candidate; + private otherCandidates = this.dialogData.candidates.filter( + (c) => c.idx !== this.dialogData.idx, + ); + private othersWeight = this.otherCandidates.reduce((acc, c) => acc + c.candidate.weight, 0); form = this.fb.nonNullable.group({ - terminal: this.dialogData.candidate.terminal.id, - description: this.dialogData.candidate.description, - weight: this.dialogData.candidate.weight, - allowed: this.dialogData.candidate.allowed, + terminal: this.currentCandidate.terminal.id, + description: this.currentCandidate.description, + weight: this.getPercentWeight(this.currentCandidate.weight), + allowed: this.currentCandidate.allowed, }); - weightPercentControl = this.fb.nonNullable.control(null); + weightTypeControl = this.fb.nonNullable.control('weight_percent'); + weightOptions = [ + { label: 'Weight', value: 'weight' }, + { label: 'Percent', value: 'weight_percent' }, + ]; - ngOnInit() { - getValueChanges(this.weightPercentControl) - .pipe(takeUntilDestroyed(this.dr)) - .subscribe(() => { - this.form.controls.weight.setValue(null, { - emitEvent: false, - }); + weightsPreview$ = combineLatest([ + getValueChanges(this.weightTypeControl), + getValueChanges(this.form), + ]).pipe( + map(([weightType]) => { + const newCandidates = this.getNewCandidates().sort((a, b) => + a.idx === this.dialogData.idx + ? -1 + : b.idx === this.dialogData.idx + ? 1 + : b.candidate.weight - a.candidate.weight, + ); + return newCandidates.map((c) => { + if (weightType === 'weight_percent') + return { + description: c.candidate.terminal.id, + percent: c.candidate.weight + '%', + }; + const allWeight = newCandidates.reduce((acc, c) => acc + c.candidate.weight, 0); + return { + description: c.candidate.terminal.id, + percent: + normalizePercent((c.candidate.weight / allWeight) * 100) + + `% (${c.candidate.weight})`, + }; }); - getValueChanges(this.form.controls.weight) - .pipe(takeUntilDestroyed(this.dr)) - .subscribe(() => { - this.weightPercentControl.setValue(null, { - emitEvent: false, - }); - }); - } + }), + shareReplay({ refCount: true, bufferSize: 1 }), + ); confirm() { - const { terminal, weight, ...value } = getValue(this.form); - const percent = getValue(this.weightPercentControl); - const isPercent = !!percent; - const weightNum = isPercent ? Number(percent) : Number(weight || 0); - this.closeWithSuccess({ + ...this.dialogData, + candidates: this.getNewCandidates(), + }); + } + + private getNewCandidates(): CandidateWithIdx[] { + if (this.weightTypeControl.value === 'weight') { + const weight = Number(this.form.value.weight) || 0; + return this.dialogData.candidates.map((c) => ({ + ...c, + candidate: { + ...c.candidate, + weight: c.idx === this.dialogData.idx ? weight : c.candidate.weight, + }, + })); + } + const percentWeight = normalizePercent(this.form.value.weight); + const availableWeight = MAX_PERCENT_WEIGHT - percentWeight; + + return this.dialogData.candidates.map((c) => ({ + ...c, candidate: { - ...this.dialogData.candidate, - terminal: { id: terminal }, - ...value, - weight: weightNum, + ...c.candidate, + weight: + c.idx === this.dialogData.idx + ? percentWeight + : normalizePercent( + this.othersWeight === 0 + ? normalizePercent(availableWeight / this.otherCandidates.length) + : (c.candidate.weight / this.othersWeight) * availableWeight, + ), }, - others: isPercent - ? this.dialogData.others.map((c) => ({ - ...c, - weight: - c.weight && this.othersWeight - ? Math.round( - (c.weight / this.othersWeight) * - (this.othersWeight - - (weight || 0) + - (percent - ? Math.round((percent / 100) * this.othersWeight) - : 0)), - ) - : 0, - })) - : this.dialogData.others, - }); + })); + } + + private getPercentWeight( + weight: number = 0, + othersWeight: number = this.othersWeight || 0, + count: number = this.otherCandidates.length + 1, + ): number { + if (othersWeight === 0) { + if (weight === 0) return normalizePercent(MAX_PERCENT_WEIGHT / count); + return 100; + } + if (weight === 0) return 0; + const allWeight = weight + othersWeight; + return normalizePercent((weight / allWeight) * MAX_PERCENT_WEIGHT); } }