feat: Add Health Check Endpoint

This commit is contained in:
diallolatoile 2026-01-17 11:36:05 +00:00
parent d26feb396f
commit 5044aa7573
65 changed files with 153 additions and 934 deletions

View File

@ -1,11 +0,0 @@
{
"folders": [
{
"path": "../../../../../dcb-user-service"
},
{
"path": "../../../.."
}
],
"settings": {}
}

View File

@ -68,13 +68,26 @@ export interface ReportParams {
endDate?: string; endDate?: string;
merchantPartnerId?: number; merchantPartnerId?: number;
} }
export interface HealthCheckStatus { export interface HealthCheckStatus {
service: string; service: string;
url: string; url: string;
status: 'UP' | 'DOWN'; status: 'UP' | 'DOWN';
statusCode: number; statusCode: number;
checkedAt: string; checkedAt: string;
responseTime: string;
uptime?: number;
note?: string;
error?: string;
}
export interface HealthCheckResponse {
summary: {
total: number;
up: number;
down: number;
timestamp: string;
};
details: HealthCheckStatus[];
} }
// ChartDataNormalized : normalisation des données pour tous types de chart // ChartDataNormalized : normalisation des données pour tous types de chart

View File

@ -8,6 +8,7 @@ import {
SubscriptionReport, SubscriptionReport,
SyncResponse, SyncResponse,
HealthCheckStatus, HealthCheckStatus,
HealthCheckResponse,
ChartDataNormalized ChartDataNormalized
} from '../models/dcb-reporting.models'; } from '../models/dcb-reporting.models';
import { environment } from '@environments/environment'; import { environment } from '@environments/environment';
@ -282,7 +283,7 @@ export class ReportService {
} }
// --------------------- // ---------------------
// Health checks (rest of the code remains the same) // Health checks
// --------------------- // ---------------------
private checkApiAvailability( private checkApiAvailability(
@ -316,6 +317,8 @@ export class ReportService {
timeout(this.DEFAULT_TIMEOUT), timeout(this.DEFAULT_TIMEOUT),
map((resp: HttpResponse<any>) => { map((resp: HttpResponse<any>) => {
const finalResponseTime = Date.now() - startTime; const finalResponseTime = Date.now() - startTime;
const body: any = resp.body;
return { return {
service, service,
url, url,
@ -323,6 +326,7 @@ export class ReportService {
statusCode: resp.status, statusCode: resp.status,
checkedAt: new Date().toISOString(), checkedAt: new Date().toISOString(),
responseTime: `${finalResponseTime}ms`, responseTime: `${finalResponseTime}ms`,
uptime: body?.uptime,
note: 'Used GET fallback' note: 'Used GET fallback'
}; };
}), }),
@ -397,16 +401,17 @@ export class ReportService {
/** /**
* Health check global de toutes les APIs * Health check global de toutes les APIs
* Scanne chaque URL d'API directement * Scanne chaque URL d'API directement
*/ */
private buildHealthUrl(baseUrl: string): string {
return `${baseUrl.replace(/\/$/, '')}/health`;
}
globalHealthCheck(): Observable<HealthCheckStatus[]> { globalHealthCheck(): Observable<HealthCheckStatus[]> {
const healthChecks: Observable<HealthCheckStatus>[] = []; return forkJoin(
Object.entries(this.apiEndpoints).map(([service, url]) =>
// Vérifiez chaque service avec sa racine this.checkApiAvailability(service, this.buildHealthUrl(url))
Object.entries(this.apiEndpoints).forEach(([service, url]) => { )
healthChecks.push(this.checkApiAvailability(service, url)); );
});
return forkJoin(healthChecks);
} }
/** /**
@ -438,10 +443,7 @@ export class ReportService {
/** /**
* Health check détaillé avec métriques * Health check détaillé avec métriques
*/ */
detailedHealthCheck(): Observable<{ detailedHealthCheck(): Observable<HealthCheckResponse> {
summary: { total: number; up: number; down: number; timestamp: string };
details: HealthCheckStatus[];
}> {
return this.globalHealthCheck().pipe( return this.globalHealthCheck().pipe(
map(results => { map(results => {
const adjustedResults = results.map(result => ({ const adjustedResults = results.map(result => ({
@ -462,7 +464,24 @@ export class ReportService {
details: adjustedResults details: adjustedResults
}; };
}), }),
catchError(err => this.handleError(err)) catchError(err => this.handleHealthError(err))
); );
} }
/**
* Gestion des erreurs
*/
private handleHealthError(error: any): Observable<HealthCheckResponse> {
console.error('Health check error:', error);
return of({
summary: {
total: 0,
up: 0,
down: 0,
timestamp: new Date().toISOString()
},
details: []
});
}
} }

View File

@ -1 +0,0 @@
<p>Integrations</p>

View File

@ -1,2 +0,0 @@
import { Integrations } from './integrations';
describe('Integrations', () => {});

View File

@ -1,7 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-integrations',
templateUrl: './integrations.html',
})
export class Integrations {}

View File

@ -1,8 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class IntegrationsService {
constructor() {}
}

View File

@ -730,105 +730,81 @@
</div> </div>
</div> </div>
<!-- CONFIGURATIONS TECHNIQUES --> <!-- LISTE DES CONFIGURATIONS (READONLY) -->
<div class="row g-3 mb-4"> <div class="row g-3">
<div class="col-12"> <div class="col-12">
<div class="d-flex justify-content-between align-items-center border-bottom pb-2"> <div class="d-flex justify-content-between align-items-center border-bottom pb-2 mb-3">
<h6 class="mb-0 text-primary"> <h6 class="mb-0 text-primary">
<ng-icon name="lucideSettings" class="me-2"></ng-icon> <ng-icon name="lucideSettings" class="me-2"></ng-icon>
Configurations Techniques Configurations
</h6> </h6>
<button <span class="badge bg-secondary">{{ selectedMerchantForEdit.configs.length || 0 }} config(s)</span>
type="button"
class="btn btn-outline-primary btn-sm"
(click)="addConfigInEdit()"
[disabled]="updatingMerchant"
>
<ng-icon name="lucidePlus" class="me-1"></ng-icon>
Ajouter une configuration
</button>
</div> </div>
</div> </div>
@if (!selectedMerchantForEdit.configs || selectedMerchantForEdit.configs.length === 0) { @if (!selectedMerchantForEdit.configs || selectedMerchantForEdit.configs.length === 0) {
<div class="col-12"> <div class="col-12">
<div class="alert alert-warning"> <div class="alert alert-info">
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon> <ng-icon name="lucideInfo" class="me-2"></ng-icon>
Au moins une configuration est requise Aucune configuration disponible
</div> </div>
</div> </div>
} }
<!-- Liste des configurations --> <!-- Liste des configurations en mode lecture -->
@for (config of selectedMerchantForEdit.configs; track trackByConfigId($index, config); let i = $index) { @for (config of selectedMerchantForEdit.configs; track config.id || $index; let i = $index) {
<div class="col-12"> <div class="col-12">
<div class="card border-0 shadow-sm"> <div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-light py-2 d-flex justify-content-between align-items-center"> <div class="card-header bg-light py-2 d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<ng-icon [name]="getConfigTypeIconSafe(config.name)" class="me-2 text-primary"></ng-icon> <ng-icon name="lucideSettings" class="me-2 text-primary"></ng-icon>
<span class="fw-semibold">Configuration {{ i + 1 }}</span> <span class="fw-semibold">Configuration {{ i + 1 }}</span>
</div> </div>
@if (selectedMerchantForEdit.configs.length > 1) { @if (config.name.includes('SECRET') || config.name.includes('KEY') || config.value.includes('password')) {
<button <span class="badge bg-warning text-dark">
type="button" <ng-icon name="lucideShield" class="me-1"></ng-icon>
class="btn btn-sm btn-outline-danger" Sensible
(click)="removeConfigInEdit(i)" </span>
[disabled]="updatingMerchant"
>
<ng-icon name="lucideTrash2" class="me-1"></ng-icon>
Supprimer
</button>
} }
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row g-3"> <div class="row g-3">
<!-- Type de configuration -->
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Type <span class="text-danger">*</span></label> <div class="mb-2">
<select <small class="text-muted d-block">Type</small>
class="form-select" <div class="d-flex align-items-center">
[(ngModel)]="config.name" <ng-icon name="lucideSettings" class="me-2 text-muted"></ng-icon>
[name]="'editConfigType_' + i" <span class="fw-medium">
required {{ config.name || 'Non spécifié' }}
[disabled]="updatingMerchant" </span>
>
<option value="" disabled>Sélectionnez un type</option>
@for (type of configTypes; track type.name) {
<option [value]="type.name">{{ type.label }}</option>
}
</select>
</div>
<div class="col-md-6">
<label class="form-label">Opérateur <span class="text-danger">*</span></label>
<select
class="form-select"
[(ngModel)]="config.operatorId"
[name]="'editOperatorId_' + i"
required
[disabled]="updatingMerchant"
>
<option value="" disabled>Sélectionnez un opérateur</option>
@for (operator of operators; track operator.id) {
<option [value]="operator.id">{{ operator.name }}</option>
}
</select>
</div>
<div class="col-12">
<label class="form-label">Valeur <span class="text-danger">*</span></label>
<textarea
class="form-control font-monospace"
[(ngModel)]="config.value"
[name]="'editValue_' + i"
required
[disabled]="updatingMerchant"
rows="3"
placeholder="Valeur de configuration"
></textarea>
@if (isSensitiveConfig(config)) {
<div class="form-text text-warning">
<ng-icon name="lucideShield" class="me-1"></ng-icon>
Cette configuration contient des informations sensibles
</div> </div>
} </div>
</div>
<!-- Opérateur -->
<div class="col-md-6">
<div class="mb-2">
<small class="text-muted d-block">Opérateur ID</small>
<div class="d-flex align-items-center">
<ng-icon name="lucideBuilding" class="me-2 text-muted"></ng-icon>
<span class="fw-medium">
{{ config.operatorId || 'Non spécifié' }}
</span>
</div>
</div>
</div>
<!-- Valeur -->
<div class="col-12">
<div class="mb-2">
<small class="text-muted d-block">Valeur</small>
<div class="p-3 bg-light rounded border">
<pre class="mb-0" style="white-space: pre-wrap; word-break: break-all;">
{{ config.value || 'Aucune valeur' }}
</pre>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -837,23 +813,15 @@
} }
</div> </div>
<!-- CONTACTS TECHNIQUES --> <!-- CONTACTS TECHNIQUES (READONLY) -->
<div class="row g-3"> <div class="row g-3 mt-4">
<div class="col-12"> <div class="col-12">
<div class="d-flex justify-content-between align-items-center border-bottom pb-2"> <div class="d-flex justify-content-between align-items-center border-bottom pb-2 mb-3">
<h6 class="mb-0 text-primary"> <h6 class="mb-0 text-primary">
<ng-icon name="lucideUsers" class="me-2"></ng-icon> <ng-icon name="lucideUsers" class="me-2"></ng-icon>
Contacts Techniques Contacts Techniques
</h6> </h6>
<button <span class="badge bg-secondary">{{ selectedMerchantForEdit.technicalContacts.length || 0 }} contact(s)</span>
type="button"
class="btn btn-outline-primary btn-sm"
(click)="addTechnicalContactInEdit()"
[disabled]="updatingMerchant"
>
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
Ajouter un contact
</button>
</div> </div>
</div> </div>
@ -861,89 +829,69 @@
<div class="col-12"> <div class="col-12">
<div class="alert alert-warning"> <div class="alert alert-warning">
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon> <ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
Au moins un contact technique est requis Aucun contact technique défini
</div> </div>
</div> </div>
} }
<!-- Liste des contacts techniques en mode lecture -->
<!-- Liste des contacts techniques --> @for (contact of selectedMerchantForEdit.technicalContacts; track contact.id || $index; let i = $index) {
@for (contact of selectedMerchantForEdit.technicalContacts; track trackByContactId($index, contact); let i = $index) { <div class="col-12 col-md-6 col-lg-4">
<div class="col-12"> <div class="card border-0 shadow-sm h-100">
<div class="card border-0 shadow-sm"> <div class="card-header bg-light py-2">
<div class="card-header bg-light py-2 d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<ng-icon name="lucideUser" class="me-2 text-primary"></ng-icon> <ng-icon name="lucideUser" class="me-2 text-primary"></ng-icon>
<span class="fw-semibold">Contact {{ i + 1 }}</span> <span class="fw-semibold">Contact {{ i + 1 }}</span>
</div> </div>
@if (selectedMerchantForEdit.technicalContacts.length > 1) {
<button
type="button"
class="btn btn-sm btn-outline-danger"
(click)="removeTechnicalContactInEdit(i)"
[disabled]="updatingMerchant"
>
<ng-icon name="lucideTrash2" class="me-1"></ng-icon>
Supprimer
</button>
}
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row g-3"> <!-- Nom complet -->
<div class="col-md-6"> <div class="mb-3">
<label class="form-label">Prénom <span class="text-danger">*</span></label> <small class="text-muted d-block">Nom complet</small>
<input <div class="d-flex align-items-center">
type="text" <ng-icon name="lucideUser" class="me-2 text-muted"></ng-icon>
class="form-control" <span class="fw-medium">
[(ngModel)]="contact.firstName" {{ contact.firstName || '' }} {{ contact.lastName || '' }}
[name]="'editFirstName_' + i" @if (!contact.firstName && !contact.lastName) {
required <span class="text-muted fst-italic">Non spécifié</span>
[disabled]="updatingMerchant" }
placeholder="Prénom" </span>
>
</div> </div>
<div class="col-md-6"> </div>
<label class="form-label">Nom <span class="text-danger">*</span></label>
<input <!-- Téléphone -->
type="text" <div class="mb-3">
class="form-control" <small class="text-muted d-block">Téléphone</small>
[(ngModel)]="contact.lastName" <div class="d-flex align-items-center">
[name]="'editLastName_' + i" <ng-icon name="lucidePhone" class="me-2 text-muted"></ng-icon>
required @if (contact.phone) {
[disabled]="updatingMerchant" <a href="tel:{{ contact.phone }}" class="text-decoration-none">
placeholder="Nom" {{ contact.phone }}
> </a>
} @else {
<span class="text-muted fst-italic">Non spécifié</span>
}
</div> </div>
<div class="col-md-6"> </div>
<label class="form-label">Téléphone <span class="text-danger">*</span></label>
<input <!-- Email -->
type="text" <div class="mb-3">
class="form-control" <small class="text-muted d-block">Email</small>
[(ngModel)]="contact.phone" <div class="d-flex align-items-center">
[name]="'editPhone_' + i" <ng-icon name="lucideMail" class="me-2 text-muted"></ng-icon>
required @if (contact.email) {
[disabled]="updatingMerchant" <a href="mailto:{{ contact.email }}" class="text-decoration-none">
placeholder="+XX X XX XX XX XX" {{ contact.email }}
> </a>
</div> } @else {
<div class="col-md-6"> <span class="text-muted fst-italic">Non spécifié</span>
<label class="form-label">Email <span class="text-danger">*</span></label> }
<input
type="email"
class="form-control"
[(ngModel)]="contact.email"
[name]="'editEmail_' + i"
required
[disabled]="updatingMerchant"
placeholder="email@exemple.com"
>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
} }
</div> </div>
<div class="modal-footer mt-4 border-top pt-3"> <div class="modal-footer mt-4 border-top pt-3">
<button <button
type="button" type="button"

View File

@ -102,8 +102,6 @@ export class MerchantConfigService {
} }
getMerchantById(userId: number): Observable<Merchant> { getMerchantById(userId: number): Observable<Merchant> {
//const numericId = this.convertIdToNumber(id);
console.log(`📥 Loading merchant ${userId}`); console.log(`📥 Loading merchant ${userId}`);
return this.http.get<ApiMerchant>(`${this.baseApiUrl}/${userId}`).pipe( return this.http.get<ApiMerchant>(`${this.baseApiUrl}/${userId}`).pipe(
@ -118,8 +116,6 @@ export class MerchantConfigService {
} }
updateMerchant(id: number, updateMerchantDto: UpdateMerchantDto): Observable<Merchant> { updateMerchant(id: number, updateMerchantDto: UpdateMerchantDto): Observable<Merchant> {
//const numericId = this.convertIdToNumber(id);
const apiDto = this.dataAdapter.convertUpdateMerchantToApi(updateMerchantDto); const apiDto = this.dataAdapter.convertUpdateMerchantToApi(updateMerchantDto);
console.log(`📤 Updating merchant ${id}:`, apiDto); console.log(`📤 Updating merchant ${id}:`, apiDto);

View File

@ -310,27 +310,6 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
} }
} }
// Gestion des contacts dans l'édition
addTechnicalContactInEdit(): void {
if (!this.selectedMerchantForEdit?.technicalContacts) {
this.selectedMerchantForEdit!.technicalContacts = [];
}
this.selectedMerchantForEdit?.technicalContacts.push({
firstName: '',
lastName: '',
phone: '',
email: ''
});
}
removeTechnicalContactInEdit(index: number): void {
if (this.selectedMerchantForEdit?.technicalContacts &&
this.selectedMerchantForEdit.technicalContacts.length > 1) {
this.selectedMerchantForEdit.technicalContacts.splice(index, 1);
}
}
/** /**
* Méthodes pour la gestion des configurations * Méthodes pour la gestion des configurations
*/ */
@ -352,25 +331,6 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
} }
} }
//Gestion des configs dans l'édition
addConfigInEdit(): void {
if (!this.selectedMerchantForEdit?.configs) {
this.selectedMerchantForEdit!.configs = [];
}
this.selectedMerchantForEdit?.configs.push({
name: ConfigType.API_KEY,
value: '',
operatorId: Operator.ORANGE_OSN
});
}
removeConfigInEdit(index: number): void {
if (this.selectedMerchantForEdit?.configs && this.selectedMerchantForEdit.configs.length > 1) {
this.selectedMerchantForEdit.configs.splice(index, 1);
}
}
// ==================== CONVERSION IDS ==================== // ==================== CONVERSION IDS ====================
/** /**
@ -891,7 +851,7 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
} }
/** /**
* Appel API pour mettre à jour le marchand (version avec switchMap) * Appel API pour mettre à jour le marchand
*/ */
updateMerchant(): void { updateMerchant(): void {
if (!this.selectedMerchantForEdit) { if (!this.selectedMerchantForEdit) {
@ -1210,57 +1170,12 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
errors.push('Le téléphone est requis'); errors.push('Le téléphone est requis');
} }
// Validation des configurations
if (!merchant.configs || merchant.configs.length === 0) {
errors.push('Au moins une configuration est requise');
} else {
merchant.configs.forEach((config, index) => {
if (!config.name?.trim()) {
errors.push(`Le type de configuration ${index + 1} est requis`);
}
if (!config.value?.trim()) {
errors.push(`La valeur de configuration ${index + 1} est requise`);
}
if (!config.operatorId) {
errors.push(`L'opérateur de configuration ${index + 1} est requis`);
}
});
}
// Validation des contacts techniques
if (!merchant.technicalContacts || merchant.technicalContacts.length === 0) {
errors.push('Au moins un contact technique est requis');
} else {
merchant.technicalContacts.forEach((contact, index) => {
if (!contact.firstName?.trim()) {
errors.push(`Le prénom du contact ${index + 1} est requis`);
}
if (!contact.lastName?.trim()) {
errors.push(`Le nom du contact ${index + 1} est requis`);
}
if (!contact.phone?.trim()) {
errors.push(`Le téléphone du contact ${index + 1} est requis`);
}
if (!contact.email?.trim()) {
errors.push(`L'email du contact ${index + 1} est requis`);
} else if (!this.isValidEmail(contact.email)) {
errors.push(`L'email du contact ${index + 1} est invalide`);
}
});
}
return { return {
isValid: errors.length === 0, isValid: errors.length === 0,
errors: errors errors: errors
}; };
} }
// Validation d'email
private isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
confirmDeleteMerchant(): void { confirmDeleteMerchant(): void {
if (!this.selectedMerchantForDelete) { if (!this.selectedMerchantForDelete) {
this.deleteMerchantError = 'Aucun marchand sélectionné pour suppression'; this.deleteMerchantError = 'Aucun marchand sélectionné pour suppression';

View File

@ -29,7 +29,7 @@ export class MerchantDataAdapter {
return { return {
...apiMerchant, ...apiMerchant,
id: apiMerchant.id, //this.convertIdToString(apiMerchant.id), id: apiMerchant.id,
configs: (apiMerchant.configs || []).map(config => configs: (apiMerchant.configs || []).map(config =>
this.convertApiConfigToFrontend(config) this.convertApiConfigToFrontend(config)
), ),
@ -117,33 +117,6 @@ export class MerchantDataAdapter {
if (dto.adresse !== undefined) updateData.adresse = dto.adresse?.trim(); if (dto.adresse !== undefined) updateData.adresse = dto.adresse?.trim();
if (dto.phone !== undefined) updateData.phone = dto.phone?.trim(); if (dto.phone !== undefined) updateData.phone = dto.phone?.trim();
// Configurations - seulement si présentes dans le DTO
if (dto.configs !== undefined) {
updateData.configs = (dto.configs || []).map(config => {
const apiConfig: any = {
name: config.name,
value: config.value?.trim(),
operatorId: this.validateOperatorId(config.operatorId)
};
return apiConfig;
});
}
// Contacts techniques - seulement si présents dans le DTO
if (dto.technicalContacts !== undefined) {
updateData.technicalContacts = (dto.technicalContacts || []).map(contact => {
const apiContact: any = {
firstName: contact.firstName?.trim(),
lastName: contact.lastName?.trim(),
phone: contact.phone?.trim(),
email: contact.email?.trim()
};
return apiContact;
});
}
return updateData; return updateData;
} }
@ -250,49 +223,6 @@ export class MerchantDataAdapter {
errors.push('Le téléphone est requis'); errors.push('Le téléphone est requis');
} }
// Validation des configurations si présentes
if (dto.configs !== undefined) {
if (dto.configs.length === 0) {
errors.push('Au moins une configuration est requise');
} else {
dto.configs.forEach((config, index) => {
if (!config.name?.trim()) {
errors.push(`Le type de configuration ${index + 1} est requis`);
}
if (!config.value?.trim()) {
errors.push(`La valeur de configuration ${index + 1} est requise`);
}
if (!config.operatorId) {
errors.push(`L'opérateur de configuration ${index + 1} est requis`);
}
});
}
}
// Validation des contacts si présents
if (dto.technicalContacts !== undefined) {
if (dto.technicalContacts.length === 0) {
errors.push('Au moins un contact technique est requis');
} else {
dto.technicalContacts.forEach((contact, index) => {
if (!contact.firstName?.trim()) {
errors.push(`Le prénom du contact ${index + 1} est requis`);
}
if (!contact.lastName?.trim()) {
errors.push(`Le nom du contact ${index + 1} est requis`);
}
if (!contact.phone?.trim()) {
errors.push(`Le téléphone du contact ${index + 1} est requis`);
}
if (!contact.email?.trim()) {
errors.push(`L'email du contact ${index + 1} est requis`);
} else if (!this.isValidEmail(contact.email)) {
errors.push(`L'email du contact ${index + 1} est invalide`);
}
});
}
}
if (errors.length > 0) { if (errors.length > 0) {
throw new Error(`Validation failed: ${errors.join(', ')}`); throw new Error(`Validation failed: ${errors.join(', ')}`);
} }

View File

@ -7,15 +7,9 @@ import { MerchantUsersManagement } from '@modules/hub-users-management/merchant-
// Composants principaux // Composants principaux
import { DcbReportingDashboard } from '@modules/dcb-dashboard/dcb-reporting-dashboard'; import { DcbReportingDashboard } from '@modules/dcb-dashboard/dcb-reporting-dashboard';
import { Team } from '@modules/team/team';
import { Transactions } from '@modules/transactions/transactions'; import { Transactions } from '@modules/transactions/transactions';
import { OperatorsConfig } from '@modules/operators/config/config';
import { OperatorsStats } from '@modules/operators/stats/stats';
import { WebhooksHistory } from '@modules/webhooks/history/history';
import { WebhooksStatus } from '@modules/webhooks/status/status';
import { WebhooksRetry } from '@modules/webhooks/retry/retry';
import { Settings } from '@modules/settings/settings';
import { Integrations } from '@modules/integrations/integrations';
import { MyProfile } from '@modules/profile/profile'; import { MyProfile } from '@modules/profile/profile';
import { Documentation } from '@modules/documentation/documentation'; import { Documentation } from '@modules/documentation/documentation';
import { Help } from '@modules/help/help'; import { Help } from '@modules/help/help';
@ -38,18 +32,6 @@ const routes: Routes = [
} }
}, },
// ---------------------------
// Team
// ---------------------------
{
path: 'team',
component: Team,
canActivate: [authGuard, roleGuard],
data: {
title: 'Team',
module: 'team'
}
},
// --------------------------- // ---------------------------
// Transactions // Transactions
@ -143,93 +125,6 @@ const routes: Routes = [
} }
}, },
// ---------------------------
// Operators (Admin seulement)
// ---------------------------
{
path: 'operators',
canActivate: [authGuard, roleGuard],
data: { module: 'operators' },
children: [
{
path: 'config',
component: OperatorsConfig,
data: {
title: 'Paramètres d\'Intégration',
module: 'operators/config'
}
},
{
path: 'stats',
component: OperatorsStats,
data: {
title: 'Performance & Monitoring',
module: 'operators/stats'
}
},
]
},
// ---------------------------
// Webhooks
// ---------------------------
{
path: 'webhooks',
canActivate: [authGuard, roleGuard],
data: { module: 'webhooks' },
children: [
{
path: 'history',
component: WebhooksHistory,
data: {
title: 'Historique',
module: 'webhooks/history'
}
},
{
path: 'status',
component: WebhooksStatus,
data: {
title: 'Statut des Requêtes',
module: 'webhooks/status'
}
},
{
path: 'retry',
component: WebhooksRetry,
data: {
title: 'Relancer Webhook',
module: 'webhooks/retry'
}
},
]
},
// ---------------------------
// Settings
// ---------------------------
{
path: 'settings',
component: Settings,
canActivate: [authGuard, roleGuard],
data: {
title: 'Paramètres Système',
module: 'settings'
}
},
// ---------------------------
// Integrations (Admin seulement)
// ---------------------------
{
path: 'integrations',
component: Integrations,
canActivate: [authGuard, roleGuard],
data: {
title: 'Intégrations Externes',
module: 'integrations'
}
},
// --------------------------- // ---------------------------
// Profile (Tous les utilisateurs authentifiés) // Profile (Tous les utilisateurs authentifiés)

View File

@ -1 +0,0 @@
<p>Notifications - Actions</p>

View File

@ -1,2 +0,0 @@
import { NotificationsActions } from './actions';
describe('NotificationsActions', () => {});

View File

@ -1,7 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-notifications-actions',
templateUrl: './actions.html',
})
export class NotificationsActions {}

View File

@ -1 +0,0 @@
<p>Notifications - Filters</p>

View File

@ -1,2 +0,0 @@
import { NotificationsFilters } from './filters';
describe('NotificationsFilters', () => {});

View File

@ -1,7 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-notifications-filters',
templateUrl: './filters.html',
})
export class NotificationsFilters {}

View File

@ -1 +0,0 @@
<p>Notifications - List</p>

View File

@ -1,2 +0,0 @@
import { NotificationsList } from './list';
describe('NotificationsList', () => {});

View File

@ -1,7 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-notifications-list',
templateUrl: './list.html',
})
export class NotificationsList {}

View File

@ -1 +0,0 @@
<p>Notifications</p>

View File

@ -1,2 +0,0 @@
import { Notifications } from './notifications';
describe('Notifications', () => {});

View File

@ -1,7 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-notifications',
templateUrl: './notifications.html',
})
export class Notifications {}

View File

@ -1,8 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class NotificationsActionsService {
constructor() {}
}

View File

@ -1,8 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class NotificationsFiltersService {
constructor() {}
}

View File

@ -1,8 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class NotificationsListService {
constructor() {}
}

View File

@ -1,55 +0,0 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '@environments/environment';
import { Observable } from 'rxjs';
export interface Notification {
id: string;
type: 'SMS' | 'EMAIL' | 'PUSH' | 'SYSTEM';
title: string;
message: string;
recipient: string;
status: 'SENT' | 'DELIVERED' | 'FAILED' | 'PENDING';
createdAt: Date;
sentAt?: Date;
errorMessage?: string;
}
export interface NotificationFilter {
type?: string;
status?: string;
startDate?: Date;
endDate?: Date;
recipient?: string;
}
@Injectable({ providedIn: 'root' })
export class NotificationService {
private http = inject(HttpClient);
private apiUrl = `${environment.localServiceTestApiUrl}/notifications`;
getNotifications(filters?: NotificationFilter): Observable<Notification[]> {
return this.http.post<Notification[]>(
`${this.apiUrl}/list`,
filters
);
}
sendNotification(notification: Partial<Notification>): Observable<Notification> {
return this.http.post<Notification>(
`${this.apiUrl}/send`,
notification
);
}
getNotificationStats(): Observable<any> {
return this.http.get(`${this.apiUrl}/stats`);
}
retryNotification(notificationId: string): Observable<Notification> {
return this.http.post<Notification>(
`${this.apiUrl}/${notificationId}/retry`,
{}
);
}
}

View File

@ -1 +0,0 @@
<p>Operators - Config</p>

View File

@ -1,2 +0,0 @@
import { OperatorsConfig } from './config';
describe('OperatorsConfig', () => {});

View File

@ -1,36 +0,0 @@
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { UiCard } from '@app/components/ui-card';
import { InputFields } from '@/app/modules/components/input-fields';
import { CheckboxesAndRadios } from '@/app/modules/components/checkboxes-and-radios';
import { InputTouchspin } from '@/app/modules/components/input-touchspin';
@Component({
selector: 'app-operator-config',
//imports: [FormsModule, UiCard, InputFields, CheckboxesAndRadios, InputTouchspin],
templateUrl: './config.html',
})
export class OperatorsConfig {
operatorConfig = {
name: '',
apiEndpoint: '',
apiKey: '',
secretKey: '',
timeout: 30,
retryAttempts: 3,
webhookUrl: '',
isActive: true,
supportedCountries: [] as string[],
supportedServices: ['DCB', 'SMS', 'USSD']
};
countries = ['CIV', 'SEN', 'CMR', 'COD', 'TUN', 'BFA', 'MLI', 'GIN'];
saveConfig() {
console.log('Saving operator config:', this.operatorConfig);
}
testConnection() {
console.log('Testing connection to:', this.operatorConfig.apiEndpoint);
}
}

View File

@ -1 +0,0 @@
<p>Operators</p>

View File

@ -1,2 +0,0 @@
import { Operators } from './operators';
describe('Operators', () => {});

View File

@ -1,7 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-operators',
templateUrl: './operators.html',
})
export class Operators {}

View File

@ -1,8 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class OperatorsConfigService {
constructor() {}
}

View File

@ -1,47 +0,0 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '@environments/environment';
import { Observable } from 'rxjs';
export interface Operator {
id: string;
name: string;
country: string;
status: 'ACTIVE' | 'INACTIVE';
config: OperatorConfig;
}
export interface OperatorConfig {
apiEndpoint: string;
apiKey: string;
secretKey: string;
timeout: number;
retryAttempts: number;
webhookUrl: string;
isActive: boolean;
supportedServices: string[];
}
@Injectable({ providedIn: 'root' })
export class OperatorService {
private http = inject(HttpClient);
private apiUrl = `${environment.localServiceTestApiUrl}/operators`;
getOperators(): Observable<Operator[]> {
return this.http.get<Operator[]>(`${this.apiUrl}`);
}
updateOperatorConfig(operatorId: string, config: OperatorConfig): Observable<Operator> {
return this.http.put<Operator>(
`${this.apiUrl}/${operatorId}/config`,
config
);
}
testConnection(operatorId: string): Observable<{ success: boolean; latency: number }> {
return this.http.post<{ success: boolean; latency: number }>(
`${this.apiUrl}/${operatorId}/test-connection`,
{}
);
}
}

View File

@ -1,44 +0,0 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '@environments/environment';
import { Observable } from 'rxjs';
export interface OperatorStats {
operatorId: string;
totalTransactions: number;
successRate: number;
totalRevenue: number;
averageLatency: number;
errorCount: number;
uptime: number;
dailyStats: DailyStat[];
}
export interface DailyStat {
date: string;
transactions: number;
successRate: number;
revenue: number;
}
@Injectable({ providedIn: 'root' })
export class OperatorStatsService {
private http = inject(HttpClient);
private apiUrl = `${environment.localServiceTestApiUrl}/operators`;
getOperatorStats(operatorId: string): Observable<OperatorStats> {
return this.http.get<OperatorStats>(
`${this.apiUrl}/${operatorId}/stats`
);
}
getOperatorsComparison(): Observable<any[]> {
return this.http.get<any[]>(`${this.apiUrl}/comparison`);
}
getPerformanceMetrics(operatorId: string, period: string): Observable<any> {
return this.http.get(
`${this.apiUrl}/${operatorId}/metrics?period=${period}`
);
}
}

View File

@ -1 +0,0 @@
<p>Operators - Stats</p>

View File

@ -1,2 +0,0 @@
import { OperatorsStats } from './stats';
describe('OperatorsStats', () => {});

View File

@ -1,7 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-operators-stats',
templateUrl: './stats.html',
})
export class OperatorsStats {}

View File

@ -1,8 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class SettingsService {
constructor() {}
}

View File

@ -1 +0,0 @@
<p>Settings</p>

View File

@ -1,15 +0,0 @@
import { Routes } from '@angular/router';
import { Settings } from './settings';
import { authGuard } from '../../core/guards/auth.guard';
import { roleGuard } from '../../core/guards/role.guard';
export const SETTINGS_ROUTES: Routes = [
{
path: 'settings',
canActivate: [authGuard, roleGuard],
component: Settings,
data: {
title: 'Configuration',
}
}
];

View File

@ -1,2 +0,0 @@
import { Settings } from './settings';
describe('Settings', () => {});

View File

@ -1,7 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-settings',
templateUrl: './settings.html',
})
export class Settings {}

View File

@ -1 +0,0 @@
<p>Team</p>

View File

@ -1,2 +0,0 @@
import { Team } from './team';
describe('Team', () => {});

View File

@ -1,7 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-team',
templateUrl: './team.html',
})
export class Team {}

View File

@ -1,15 +0,0 @@
export type ContactType = {
name: string
avatar: string
country: {
name: string
flag: string
}
jobTitle: string
about: string
verified?: boolean
rating: number
campaigns: number
contacts: number
engagement: string
}

View File

@ -1 +0,0 @@
<p>Webhooks - History</p>

View File

@ -1,2 +0,0 @@
import { WebhooksHistory } from './history';
describe('WebhooksHistory', () => {});

View File

@ -1,7 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-webhooks-history',
templateUrl: './history.html',
})
export class WebhooksHistory {}

View File

@ -1 +0,0 @@
<p>Webhooks - Retry</p>

View File

@ -1,2 +0,0 @@
import { WebhooksRetry } from './retry';
describe('WebhooksRetry', () => {});

View File

@ -1,7 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-webhooks-retry',
templateUrl: './retry.html',
})
export class WebhooksRetry {}

View File

@ -1,8 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class WebhooksHistoryService {
constructor() {}
}

View File

@ -1,32 +0,0 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '@environments/environment';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class WebhookRetryService {
private http = inject(HttpClient);
private apiUrl = `${environment.localServiceTestApiUrl}/webhooks`;
retryWebhook(webhookId: string): Observable<{ success: boolean }> {
return this.http.post<{ success: boolean }>(
`${this.apiUrl}/${webhookId}/retry`,
{}
);
}
bulkRetryWebhooks(webhookIds: string[]): Observable<{ success: number; failed: number }> {
return this.http.post<{ success: number; failed: number }>(
`${this.apiUrl}/bulk-retry`,
{ webhookIds }
);
}
getRetryConfig(): Observable<any> {
return this.http.get(`${this.apiUrl}/retry-config`);
}
updateRetryConfig(config: any): Observable<any> {
return this.http.put(`${this.apiUrl}/retry-config`, config);
}
}

View File

@ -1,8 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class WebhooksStatusService {
constructor() {}
}

View File

@ -1,45 +0,0 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '@environments/environment';
import { Observable } from 'rxjs';
export interface WebhookEvent {
id: string;
url: string;
eventType: string;
payload: any;
status: 'SUCCESS' | 'FAILED' | 'PENDING';
retryCount: number;
createdAt: Date;
lastAttempt?: Date;
errorMessage?: string;
}
export interface WebhookFilter {
status?: string;
eventType?: string;
startDate?: Date;
endDate?: Date;
}
@Injectable({ providedIn: 'root' })
export class WebhookService {
private http = inject(HttpClient);
private apiUrl = `${environment.localServiceTestApiUrl}/webhooks`;
getWebhookHistory(filters?: WebhookFilter): Observable<WebhookEvent[]> {
return this.http.post<WebhookEvent[]>(
`${this.apiUrl}/history`,
filters
);
}
getWebhookStatus(): Observable<{
total: number;
success: number;
failed: number;
pending: number;
}> {
return this.http.get<any>(`${this.apiUrl}/status`);
}
}

View File

@ -1 +0,0 @@
<p>Webhooks - Status</p>

View File

@ -1,2 +0,0 @@
import { WebhooksStatus } from './status';
describe('WebhooksStatus', () => {});

View File

@ -1,7 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-webhooks-status',
templateUrl: './status.html',
})
export class WebhooksStatus {}

View File

@ -1 +0,0 @@
<p>Webhooks</p>

View File

@ -1,2 +0,0 @@
import { Webhooks } from './webhooks';
describe('Webhooks', () => {});

View File

@ -1,7 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-webhooks',
templateUrl: './webhooks.html',
})
export class Webhooks {}