feat: Phase 4 frontend - Integrations backoffice section (mock-gateway backed)

New core/integrations (Connector/DeadLetterEntry models + gateway/token)
and features/admin/integrations (facade + page: connector table with
status/lag/errors/backlog/unmatched, pause/resume). Seeded empty per
Sprint 0.1's "no fixed partner list" decision - the section is ready to
populate the moment the first real connector is onboarded against
docs/backend/PHASE-4-CONNECTOR-FRAMEWORK-CONTRACT.md §7. New /backoffice/
integrations route + nav entry, nav i18n key in all 3 languages.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-08-17 23:51:38 +04:00
parent 5f23c6e5aa
commit 6ac52b1c50
14 changed files with 195 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
import { Injectable, inject, signal } from '@angular/core';
import { take } from 'rxjs/operators';
import { Connector } from '../../../../core/integrations/models/connector.model';
import { CONNECTOR_GATEWAY } from '../../../../core/integrations/services/connector-gateway.token';
@Injectable({ providedIn: 'root' })
export class AdminIntegrationsFacade {
private readonly gateway = inject(CONNECTOR_GATEWAY);
readonly connectors = signal<Connector[]>([]);
readonly loading = signal(false);
load(): void {
this.loading.set(true);
this.gateway.loadConnectors().pipe(take(1)).subscribe(items => {
this.connectors.set(items);
this.loading.set(false);
});
}
pause(id: string): void {
this.gateway.pause(id).pipe(take(1)).subscribe(() => this.load());
}
resume(id: string): void {
this.gateway.resume(id).pipe(take(1)).subscribe(() => this.load());
}
}

View File

@@ -0,0 +1,38 @@
<div class="integrations-page">
<header>
<h1>Integrations</h1>
<p>External marketplace connectors, payment providers, FX sources, messaging. No fixed partner list - connectors onboard as partners arrive (docs/backend/PHASE-4-CONNECTOR-FRAMEWORK-CONTRACT.md).</p>
</header>
@if (facade.connectors().length === 0 && !facade.loading()) {
<app-empty-state title="No connectors yet" description="Onboard the first partner connector to see it here." />
} @else {
<table class="integrations-page__table">
<thead>
<tr>
<th>Provider</th><th>Status</th><th>Last success</th><th>Lag</th><th>Errors</th><th>Backlog</th><th>Unmatched</th><th></th>
</tr>
</thead>
<tbody>
@for (c of facade.connectors(); track c.id) {
<tr>
<td>{{ c.provider }}</td>
<td><app-badge [variant]="c.status === 'active' ? 'success' : c.status === 'error' ? 'danger' : 'neutral'">{{ c.status }}</app-badge></td>
<td>{{ c.lastSuccessAt || '-' }}</td>
<td>{{ c.lagSeconds ?? '-' }}s</td>
<td>{{ c.errorCount }}</td>
<td>{{ c.backlogCount }}</td>
<td>{{ c.unmatchedCount }}</td>
<td>
@if (c.status === 'active') {
<app-button variant="secondary" (click)="facade.pause(c.id)">Pause</app-button>
} @else {
<app-button variant="secondary" (click)="facade.resume(c.id)">Resume</app-button>
}
</td>
</tr>
}
</tbody>
</table>
}
</div>

View File

@@ -0,0 +1,16 @@
.integrations-page {
display: flex;
flex-direction: column;
gap: 16px;
&__table {
width: 100%;
border-collapse: collapse;
th, td {
text-align: left;
padding: 8px 12px;
border-bottom: 1px solid var(--border-color);
}
}
}

View File

@@ -0,0 +1,22 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AdminIntegrationsFacade } from '../facade/admin-integrations.facade';
import { BadgeComponent } from '../../../../shared/ui/badge/badge.component';
import { ButtonComponent } from '../../../../shared/ui/button/button.component';
import { EmptyStateComponent } from '../../../../shared/ui/empty-state/empty-state.component';
@Component({
selector: 'app-admin-integrations-page',
standalone: true,
imports: [CommonModule, BadgeComponent, ButtonComponent, EmptyStateComponent],
templateUrl: './admin-integrations-page.component.html',
styleUrls: ['./admin-integrations-page.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AdminIntegrationsPageComponent {
readonly facade = inject(AdminIntegrationsFacade);
constructor() {
this.facade.load();
}
}

View File

@@ -39,6 +39,7 @@ export const ADMIN_NAV_PRIMARY: AdminNavEntry[] = [
{ type: 'link', id: 'transactions', icon: 'creditCard', labelKey: 'adminShell.nav.transactions', path: ['transactions'] },
{ type: 'link', id: 'moderation', icon: 'star', labelKey: 'adminShell.nav.moderation', path: ['moderation'] },
{ type: 'link', id: 'notifications', icon: 'bell', labelKey: 'adminShell.nav.notifications', path: ['notifications'] },
{ type: 'link', id: 'integrations', icon: 'network', labelKey: 'adminShell.nav.integrations', path: ['integrations'] },
{ type: 'link', id: 'reports', icon: 'chartBar', labelKey: 'adminShell.nav.reports', path: ['reports'] },
{ type: 'group', labelKey: 'adminShell.nav.partnersGroup' },
{ type: 'link', id: 'seller-management', icon: 'store', labelKey: 'adminShell.nav.sellerManagement', path: ['partners', 'seller-management'] },