Compare commits
2 Commits
62d3045f0c
...
c83d783ff7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c83d783ff7 | ||
|
|
a116c4f592 |
@@ -228,6 +228,15 @@ const coreRoutes: Routes = [
|
|||||||
breadcrumb: [{ labelKey: 'adminShell.nav.marketplaces' }]
|
breadcrumb: [{ labelKey: 'adminShell.nav.marketplaces' }]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'partner-hierarchy',
|
||||||
|
loadComponent: () => import('./features/admin/partner-hierarchy/pages/admin-partner-hierarchy-page.component').then(m => m.AdminPartnerHierarchyPageComponent),
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.nav.partnerHierarchy',
|
||||||
|
descriptionKey: 'adminShell.nav.partnerHierarchy',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.nav.partnerHierarchy' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'audit',
|
path: 'audit',
|
||||||
loadComponent: () => import('./features/admin/audit/pages/admin-audit-page.component').then(m => m.AdminAuditPageComponent),
|
loadComponent: () => import('./features/admin/audit/pages/admin-audit-page.component').then(m => m.AdminAuditPageComponent),
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { Injectable, computed, inject, signal } from '@angular/core';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { PARTNER_HIERARCHY_GATEWAY } from '../../../../core/partner-hierarchy/services/partner-hierarchy-gateway.token';
|
||||||
|
import { PartnerCredential } from '../../../../core/partner-hierarchy/models/partner-credential.model';
|
||||||
|
import {
|
||||||
|
Environment,
|
||||||
|
NodeLevel,
|
||||||
|
ProvisioningNode,
|
||||||
|
childLevelOf,
|
||||||
|
childrenOf,
|
||||||
|
} from '../../../../core/partner-hierarchy/models/provisioning-node.model';
|
||||||
|
|
||||||
|
/** One node plus its depth, flattened for rendering a tree as a list. */
|
||||||
|
export interface HierarchyRow {
|
||||||
|
node: ProvisioningNode;
|
||||||
|
depth: number;
|
||||||
|
/** The level a child of this node would be, or null at the leaf. */
|
||||||
|
childLevel: NodeLevel | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read/act surface for the partner-provisioned hierarchy.
|
||||||
|
* Contract: docs/backend/PARTNER-PROVISIONING-API-CONTRACT.md §4.
|
||||||
|
*
|
||||||
|
* The company id is currently a constant: Company creation is out of band
|
||||||
|
* (§4.3 - partners provision INSIDE a company we create commercially), and
|
||||||
|
* there is no company-selector UI yet. When one exists this becomes an input.
|
||||||
|
*/
|
||||||
|
const DEFAULT_COMPANY_ID = 'cmp_1';
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AdminPartnerHierarchyFacade {
|
||||||
|
private readonly gateway = inject(PARTNER_HIERARCHY_GATEWAY);
|
||||||
|
|
||||||
|
readonly companyId = signal(DEFAULT_COMPANY_ID);
|
||||||
|
readonly environment = signal<Environment>('TEST');
|
||||||
|
readonly nodes = signal<ProvisioningNode[]>([]);
|
||||||
|
readonly credentials = signal<PartnerCredential[]>([]);
|
||||||
|
readonly loading = signal(false);
|
||||||
|
readonly error = signal<string | null>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Depth-first flattening, so the template renders a tree without recursion.
|
||||||
|
* Roots are nodes whose parent is absent from the loaded set - not
|
||||||
|
* necessarily level 'company', so a partially-loaded subtree still renders
|
||||||
|
* instead of coming back empty.
|
||||||
|
*/
|
||||||
|
readonly rows = computed<HierarchyRow[]>(() => {
|
||||||
|
const all = this.nodes();
|
||||||
|
const ids = new Set(all.map(n => n.id));
|
||||||
|
const roots = all
|
||||||
|
.filter(n => n.parentId === null || !ids.has(n.parentId))
|
||||||
|
.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
||||||
|
|
||||||
|
const rows: HierarchyRow[] = [];
|
||||||
|
const walk = (node: ProvisioningNode, depth: number): void => {
|
||||||
|
rows.push({ node, depth, childLevel: childLevelOf(node.level) });
|
||||||
|
for (const child of childrenOf(node.id, all)) {
|
||||||
|
walk(child, depth + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
roots.forEach(root => walk(root, 0));
|
||||||
|
return rows;
|
||||||
|
});
|
||||||
|
|
||||||
|
readonly paymentPointCount = computed(
|
||||||
|
() => this.nodes().filter(n => n.level === 'payment_point').length,
|
||||||
|
);
|
||||||
|
|
||||||
|
readonly suspendedCount = computed(
|
||||||
|
() => this.nodes().filter(n => n.status === 'suspended').length,
|
||||||
|
);
|
||||||
|
|
||||||
|
load(): void {
|
||||||
|
this.loading.set(true);
|
||||||
|
this.error.set(null);
|
||||||
|
|
||||||
|
this.gateway
|
||||||
|
.loadHierarchy(this.companyId(), this.environment())
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe({
|
||||||
|
next: nodes => {
|
||||||
|
this.nodes.set(nodes);
|
||||||
|
this.loading.set(false);
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
// Empty the tree rather than leaving the previous environment's
|
||||||
|
// nodes on screen labelled as the newly selected one.
|
||||||
|
this.nodes.set([]);
|
||||||
|
this.loading.set(false);
|
||||||
|
this.error.set('Could not load the hierarchy.');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.gateway
|
||||||
|
.loadCredentials(this.companyId())
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe({
|
||||||
|
next: credentials => this.credentials.set(credentials),
|
||||||
|
error: () => this.credentials.set([]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setEnvironment(environment: Environment): void {
|
||||||
|
if (environment === this.environment()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.environment.set(environment);
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend(nodeId: string): void {
|
||||||
|
this.act(this.gateway.setStatus(nodeId, 'suspended'));
|
||||||
|
}
|
||||||
|
|
||||||
|
activate(nodeId: string): void {
|
||||||
|
this.act(this.gateway.setStatus(nodeId, 'active'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Terminal and cascading - callers must confirm with the operator first. */
|
||||||
|
disable(nodeId: string): void {
|
||||||
|
this.act(this.gateway.disable(nodeId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private act(action: ReturnType<typeof this.gateway.disable>): void {
|
||||||
|
this.error.set(null);
|
||||||
|
action.pipe(take(1)).subscribe({
|
||||||
|
// Reload rather than patching one node locally: disable cascades to
|
||||||
|
// descendants server-side, so a local patch would leave children
|
||||||
|
// showing a status they no longer have.
|
||||||
|
next: () => this.load(),
|
||||||
|
error: (err: Error) => this.error.set(err.message || 'Action failed.'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<div class="partner-hierarchy-page">
|
||||||
|
<header class="partner-hierarchy-page__header">
|
||||||
|
<h1>Partner hierarchy</h1>
|
||||||
|
<p>
|
||||||
|
Company → Project → Store → Payment point, as provisioned through the
|
||||||
|
partner API. This is the same view a partner sees, read through the same endpoints,
|
||||||
|
so it cannot drift from what they have.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="partner-hierarchy-page__toolbar">
|
||||||
|
<div class="partner-hierarchy-page__envs" role="group" aria-label="Environment">
|
||||||
|
@for (env of environments; track env) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="partner-hierarchy-page__env"
|
||||||
|
[class.partner-hierarchy-page__env--active]="facade.environment() === env"
|
||||||
|
[attr.aria-pressed]="facade.environment() === env"
|
||||||
|
(click)="facade.setEnvironment(env)">
|
||||||
|
{{ env }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="partner-hierarchy-page__summary">
|
||||||
|
{{ facade.rows().length }} nodes ·
|
||||||
|
{{ facade.paymentPointCount() }} payment points ·
|
||||||
|
{{ facade.suspendedCount() }} suspended
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (facade.error(); as message) {
|
||||||
|
<p class="partner-hierarchy-page__error" role="alert">{{ message }}</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (facade.loading()) {
|
||||||
|
<p class="partner-hierarchy-page__empty">Loading…</p>
|
||||||
|
} @else if (facade.rows().length === 0) {
|
||||||
|
<p class="partner-hierarchy-page__empty">
|
||||||
|
No nodes provisioned in {{ facade.environment() }} yet.
|
||||||
|
</p>
|
||||||
|
} @else {
|
||||||
|
<ul class="partner-hierarchy-page__tree">
|
||||||
|
@for (row of facade.rows(); track row.node.id) {
|
||||||
|
<li class="partner-hierarchy-page__row" [style.padding-left]="indentFor(row.depth)">
|
||||||
|
<div class="partner-hierarchy-page__node">
|
||||||
|
<span class="partner-hierarchy-page__level">{{ levelLabel(row.node.level) }}</span>
|
||||||
|
<strong class="partner-hierarchy-page__name">{{ row.node.displayName }}</strong>
|
||||||
|
<app-badge [variant]="badgeVariantFor(row.node.status)">{{ row.node.status }}</app-badge>
|
||||||
|
<code class="partner-hierarchy-page__ref">{{ row.node.externalReference }}</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="partner-hierarchy-page__actions">
|
||||||
|
@if (row.node.status === 'active') {
|
||||||
|
<button type="button" (click)="facade.suspend(row.node.id)">Suspend</button>
|
||||||
|
}
|
||||||
|
@if (row.node.status === 'suspended') {
|
||||||
|
<button type="button" (click)="facade.activate(row.node.id)">Activate</button>
|
||||||
|
}
|
||||||
|
@if (row.node.status !== 'disabled') {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="partner-hierarchy-page__danger"
|
||||||
|
(click)="requestDisable(row.node)">
|
||||||
|
Disable
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
|
||||||
|
<section class="partner-hierarchy-page__credentials">
|
||||||
|
<h2>API credentials</h2>
|
||||||
|
<p>
|
||||||
|
We hold the public key only. The private half is generated by the partner and is never
|
||||||
|
transmitted to us, never stored here, never logged.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@if (facade.credentials().length === 0) {
|
||||||
|
<p class="partner-hierarchy-page__empty">No credentials registered.</p>
|
||||||
|
} @else {
|
||||||
|
<ul class="partner-hierarchy-page__creds">
|
||||||
|
@for (credential of facade.credentials(); track credential.keyId) {
|
||||||
|
<li>
|
||||||
|
<code>{{ credential.keyId }}</code>
|
||||||
|
<app-badge [variant]="credential.status === 'active' ? 'success' : 'neutral'">
|
||||||
|
{{ credential.status }}
|
||||||
|
</app-badge>
|
||||||
|
<span>scope: {{ credential.scopeNodeId }}</span>
|
||||||
|
<span>{{ credential.algorithm }}</span>
|
||||||
|
<span>{{ credential.environment }}</span>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-confirm-dialog
|
||||||
|
[open]="pendingDisable() !== null"
|
||||||
|
titleText="Disable this node?"
|
||||||
|
[message]="
|
||||||
|
'Disabling is permanent and cascades to every node underneath it. ' +
|
||||||
|
'It cannot be undone - re-provisioning creates a new node with a new id.'
|
||||||
|
"
|
||||||
|
confirmLabel="Disable"
|
||||||
|
[destructive]="true"
|
||||||
|
(confirmed)="confirmDisable()"
|
||||||
|
(cancelled)="cancelDisable()" />
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
.partner-hierarchy-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
&__header p {
|
||||||
|
max-width: 70ch;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__envs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__env {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
// Environment is a hard partition (contract §3), so the active one is
|
||||||
|
// stated with a filled background rather than a subtle outline - picking
|
||||||
|
// the wrong one here is not a cosmetic mistake.
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: var(--primary-contrast, #fff);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__summary {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__error {
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid var(--danger-color, #b3261e);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--danger-color, #b3261e);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__empty {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tree,
|
||||||
|
&__creds {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__node {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__level {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__ref {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-sm, 6px);
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__danger {
|
||||||
|
border-color: var(--danger-color, #b3261e) !important;
|
||||||
|
color: var(--danger-color, #b3261e);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__credentials {
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
|
||||||
|
p {
|
||||||
|
max-width: 70ch;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { AdminPartnerHierarchyFacade } from '../facade/admin-partner-hierarchy.facade';
|
||||||
|
import { BadgeComponent } from '../../../../shared/ui/badge/badge.component';
|
||||||
|
import { ConfirmDialogComponent } from '../../../../shared/ui/confirm-dialog/confirm-dialog.component';
|
||||||
|
import { Environment, NodeStatus, ProvisioningNode } from '../../../../core/partner-hierarchy/models/provisioning-node.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-admin-partner-hierarchy-page',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, BadgeComponent, ConfirmDialogComponent],
|
||||||
|
templateUrl: './admin-partner-hierarchy-page.component.html',
|
||||||
|
styleUrls: ['./admin-partner-hierarchy-page.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class AdminPartnerHierarchyPageComponent {
|
||||||
|
readonly facade = inject(AdminPartnerHierarchyFacade);
|
||||||
|
|
||||||
|
readonly environments: Environment[] = ['TEST', 'LIVE'];
|
||||||
|
|
||||||
|
/** Node pending a disable confirmation, or null when no dialog is open. */
|
||||||
|
readonly pendingDisable = signal<ProvisioningNode | null>(null);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.facade.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Indentation for a tree rendered as a flat list. */
|
||||||
|
indentFor(depth: number): string {
|
||||||
|
return `${depth * 1.5}rem`;
|
||||||
|
}
|
||||||
|
|
||||||
|
badgeVariantFor(status: NodeStatus): 'success' | 'warning' | 'neutral' {
|
||||||
|
switch (status) {
|
||||||
|
case 'active': return 'success';
|
||||||
|
case 'suspended': return 'warning';
|
||||||
|
case 'disabled': return 'neutral';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
levelLabel(level: string): string {
|
||||||
|
return level.replace('_', ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable is terminal and cascades to every descendant, so it always goes
|
||||||
|
* through a confirmation - there is no undo on the other side of it.
|
||||||
|
*/
|
||||||
|
requestDisable(node: ProvisioningNode): void {
|
||||||
|
this.pendingDisable.set(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmDisable(): void {
|
||||||
|
const node = this.pendingDisable();
|
||||||
|
if (node) {
|
||||||
|
this.facade.disable(node.id);
|
||||||
|
}
|
||||||
|
this.pendingDisable.set(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelDisable(): void {
|
||||||
|
this.pendingDisable.set(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ export type AdminNavEntry = AdminNavLink | AdminNavGroup | AdminNavAction;
|
|||||||
export const ADMIN_NAV_PRIMARY: AdminNavEntry[] = [
|
export const ADMIN_NAV_PRIMARY: AdminNavEntry[] = [
|
||||||
{ type: 'link', id: 'dashboard', icon: 'home', labelKey: 'adminShell.nav.dashboard', path: ['dashboard'] },
|
{ type: 'link', id: 'dashboard', icon: 'home', labelKey: 'adminShell.nav.dashboard', path: ['dashboard'] },
|
||||||
{ type: 'link', id: 'marketplaces', icon: 'network', labelKey: 'adminShell.nav.marketplaces', path: ['marketplaces'] },
|
{ type: 'link', id: 'marketplaces', icon: 'network', labelKey: 'adminShell.nav.marketplaces', path: ['marketplaces'] },
|
||||||
|
{ type: 'link', id: 'partnerHierarchy', icon: 'network', labelKey: 'adminShell.nav.partnerHierarchy', path: ['partner-hierarchy'] },
|
||||||
{ type: 'group', labelKey: 'adminShell.nav.catalogGroup' },
|
{ type: 'group', labelKey: 'adminShell.nav.catalogGroup' },
|
||||||
{ type: 'link', id: 'products', icon: 'package', labelKey: 'adminShell.nav.products', path: ['products'] },
|
{ type: 'link', id: 'products', icon: 'package', labelKey: 'adminShell.nav.products', path: ['products'] },
|
||||||
{ type: 'link', id: 'categories', icon: 'tags', labelKey: 'adminShell.nav.categories', path: ['categories'] },
|
{ type: 'link', id: 'categories', icon: 'tags', labelKey: 'adminShell.nav.categories', path: ['categories'] },
|
||||||
|
|||||||
@@ -2058,6 +2058,7 @@ export const en: Translations = {
|
|||||||
integrations: 'Integrations',
|
integrations: 'Integrations',
|
||||||
finance: 'Payments & Finance',
|
finance: 'Payments & Finance',
|
||||||
marketplaces: 'Marketplaces',
|
marketplaces: 'Marketplaces',
|
||||||
|
partnerHierarchy: 'Partner hierarchy',
|
||||||
audit: 'Audit & Security',
|
audit: 'Audit & Security',
|
||||||
reviews: 'Reviews',
|
reviews: 'Reviews',
|
||||||
reports: 'Reports',
|
reports: 'Reports',
|
||||||
|
|||||||
@@ -2052,6 +2052,7 @@ export const hy: Translations = {
|
|||||||
integrations: 'Ինտեգրումներ',
|
integrations: 'Ինտեգրումներ',
|
||||||
finance: 'Վճարումներ և ֆինանսներ',
|
finance: 'Վճարումներ և ֆինանսներ',
|
||||||
marketplaces: 'Մարկետփլեյսներ',
|
marketplaces: 'Մարկետփլեյսներ',
|
||||||
|
partnerHierarchy: 'Գործընկերոջ հիերարխիա',
|
||||||
audit: 'Աուդիտ և անվտանգություն',
|
audit: 'Աուդիտ և անվտանգություն',
|
||||||
transactions: 'Գործարքներ',
|
transactions: 'Գործարքներ',
|
||||||
reviews: 'Կարծիքներ',
|
reviews: 'Կարծիքներ',
|
||||||
|
|||||||
@@ -2052,6 +2052,7 @@ export const ru: Translations = {
|
|||||||
integrations: 'Интеграции',
|
integrations: 'Интеграции',
|
||||||
finance: 'Платежи и финансы',
|
finance: 'Платежи и финансы',
|
||||||
marketplaces: 'Маркетплейсы',
|
marketplaces: 'Маркетплейсы',
|
||||||
|
partnerHierarchy: 'Иерархия партнёра',
|
||||||
audit: 'Аудит и безопасность',
|
audit: 'Аудит и безопасность',
|
||||||
transactions: 'Транзакции',
|
transactions: 'Транзакции',
|
||||||
reviews: 'Отзывы',
|
reviews: 'Отзывы',
|
||||||
|
|||||||
@@ -2067,6 +2067,7 @@ export interface Translations {
|
|||||||
integrations: string;
|
integrations: string;
|
||||||
finance: string;
|
finance: string;
|
||||||
marketplaces: string;
|
marketplaces: string;
|
||||||
|
partnerHierarchy: string;
|
||||||
audit: string;
|
audit: string;
|
||||||
reports: string;
|
reports: string;
|
||||||
partnersGroup: string;
|
partnersGroup: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user