feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature
This commit is contained in:
parent
fb5eeb6a8c
commit
7091f1665d
239
src/app/core/models/merchant-config.model.ts
Normal file
239
src/app/core/models/merchant-config.model.ts
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
// === ENUMS COHÉRENTS ===
|
||||||
|
export enum UserType {
|
||||||
|
HUB = 'HUB',
|
||||||
|
MERCHANT_PARTNER = 'MERCHANT'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum UserRole {
|
||||||
|
// Rôles Hub (sans merchantPartnerId)
|
||||||
|
DCB_ADMIN = 'dcb-admin',
|
||||||
|
DCB_SUPPORT = 'dcb-support',
|
||||||
|
DCB_PARTNER = 'dcb-partner',
|
||||||
|
|
||||||
|
// Rôles Merchant Partner (avec merchantPartnerId obligatoire)
|
||||||
|
DCB_PARTNER_ADMIN = 'dcb-partner-admin',
|
||||||
|
DCB_PARTNER_MANAGER = 'dcb-partner-manager',
|
||||||
|
DCB_PARTNER_SUPPORT = 'dcb-partner-support'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MerchantStatus {
|
||||||
|
ACTIVE = 'ACTIVE',
|
||||||
|
INACTIVE = 'INACTIVE',
|
||||||
|
PENDING = 'PENDING',
|
||||||
|
SUSPENDED = 'SUSPENDED'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ConfigType {
|
||||||
|
API_KEY = 'API_KEY',
|
||||||
|
SECRET_KEY = 'SECRET_KEY',
|
||||||
|
WEBHOOK_URL = 'WEBHOOK_URL',
|
||||||
|
CALLBACK_URL = 'CALLBACK_URL',
|
||||||
|
TIMEOUT = 'TIMEOUT',
|
||||||
|
RETRY_COUNT = 'RETRY_COUNT',
|
||||||
|
CUSTOM = 'CUSTOM'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Operator {
|
||||||
|
ORANGE_CI = 1,
|
||||||
|
MTN_CI = 2,
|
||||||
|
MOOV_CI = 3,
|
||||||
|
WAVE = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
// === MODÈLES PRINCIPAUX ===
|
||||||
|
export interface MerchantConfig {
|
||||||
|
id?: number;
|
||||||
|
name: ConfigType | string;
|
||||||
|
value: string;
|
||||||
|
operatorId: Operator;
|
||||||
|
merchantId?: number;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TechnicalContact {
|
||||||
|
id?: number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
phone: string;
|
||||||
|
email: string;
|
||||||
|
merchantId?: number;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MerchantUser {
|
||||||
|
userId: string;
|
||||||
|
role: UserRole; // Utilisation de vos rôles existants
|
||||||
|
username?: string;
|
||||||
|
email?: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
merchantPartnerId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Merchant {
|
||||||
|
id?: number;
|
||||||
|
name: string;
|
||||||
|
logo?: string;
|
||||||
|
description?: string;
|
||||||
|
adresse: string;
|
||||||
|
phone: string;
|
||||||
|
status?: MerchantStatus;
|
||||||
|
configs: MerchantConfig[];
|
||||||
|
users: MerchantUser[];
|
||||||
|
technicalContacts: TechnicalContact[];
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
createdBy?: string;
|
||||||
|
createdByUsername?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === DTOs CRUD ===
|
||||||
|
export interface CreateMerchantDto {
|
||||||
|
name: string;
|
||||||
|
logo?: string;
|
||||||
|
description?: string;
|
||||||
|
adresse: string;
|
||||||
|
phone: string;
|
||||||
|
configs: Omit<MerchantConfig, 'id' | 'merchantId' | 'createdAt' | 'updatedAt'>[];
|
||||||
|
technicalContacts: Omit<TechnicalContact, 'id' | 'merchantId' | 'createdAt' | 'updatedAt'>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateMerchantDto extends Partial<CreateMerchantDto> {
|
||||||
|
status?: MerchantStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddUserToMerchantDto {
|
||||||
|
userId: string;
|
||||||
|
role: UserRole; // Utilisation de vos rôles existants
|
||||||
|
merchantPartnerId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateUserRoleDto {
|
||||||
|
role: UserRole; // Utilisation de vos rôles existants
|
||||||
|
}
|
||||||
|
|
||||||
|
// === RÉPONSES API ===
|
||||||
|
export interface ApiResponse<T> {
|
||||||
|
success: boolean;
|
||||||
|
data?: T;
|
||||||
|
error?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedResponse<T> {
|
||||||
|
items: T[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MerchantStatsResponse {
|
||||||
|
totalMerchants: number;
|
||||||
|
activeMerchants: number;
|
||||||
|
inactiveMerchants: number;
|
||||||
|
pendingMerchants: number;
|
||||||
|
totalConfigs: number;
|
||||||
|
totalTechnicalContacts: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === SEARCH ===
|
||||||
|
export interface SearchMerchantsParams {
|
||||||
|
query?: string;
|
||||||
|
status?: MerchantStatus;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === UTILITAIRES ===
|
||||||
|
export class MerchantUtils {
|
||||||
|
static getStatusDisplayName(status: MerchantStatus): string {
|
||||||
|
const statusNames = {
|
||||||
|
[MerchantStatus.ACTIVE]: 'Actif',
|
||||||
|
[MerchantStatus.INACTIVE]: 'Inactif',
|
||||||
|
[MerchantStatus.PENDING]: 'En attente',
|
||||||
|
[MerchantStatus.SUSPENDED]: 'Suspendu'
|
||||||
|
};
|
||||||
|
return statusNames[status] || status;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getStatusBadgeClass(status: MerchantStatus): string {
|
||||||
|
const statusClasses = {
|
||||||
|
[MerchantStatus.ACTIVE]: 'badge bg-success',
|
||||||
|
[MerchantStatus.INACTIVE]: 'badge bg-secondary',
|
||||||
|
[MerchantStatus.PENDING]: 'badge bg-warning',
|
||||||
|
[MerchantStatus.SUSPENDED]: 'badge bg-danger'
|
||||||
|
};
|
||||||
|
return statusClasses[status] || 'badge bg-secondary';
|
||||||
|
}
|
||||||
|
|
||||||
|
static getOperatorName(operatorId: Operator): string {
|
||||||
|
const operatorNames = {
|
||||||
|
[Operator.ORANGE_CI]: 'Orange CI',
|
||||||
|
[Operator.MTN_CI]: 'MTN CI',
|
||||||
|
[Operator.MOOV_CI]: 'Moov CI',
|
||||||
|
[Operator.WAVE]: 'Wave'
|
||||||
|
};
|
||||||
|
return operatorNames[operatorId] || 'Inconnu';
|
||||||
|
}
|
||||||
|
|
||||||
|
static getConfigTypeName(configName: ConfigType | string): string {
|
||||||
|
const configTypeNames = {
|
||||||
|
[ConfigType.API_KEY]: 'Clé API',
|
||||||
|
[ConfigType.SECRET_KEY]: 'Clé Secrète',
|
||||||
|
[ConfigType.WEBHOOK_URL]: 'URL Webhook',
|
||||||
|
[ConfigType.CALLBACK_URL]: 'URL Callback',
|
||||||
|
[ConfigType.TIMEOUT]: 'Timeout (ms)',
|
||||||
|
[ConfigType.RETRY_COUNT]: 'Nombre de tentatives',
|
||||||
|
[ConfigType.CUSTOM]: 'Personnalisé'
|
||||||
|
};
|
||||||
|
return configTypeNames[configName as ConfigType] || configName;
|
||||||
|
}
|
||||||
|
|
||||||
|
static validateMerchantCreation(merchant: CreateMerchantDto): string[] {
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
if (!merchant.name?.trim()) {
|
||||||
|
errors.push('Le nom du merchant est requis');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!merchant.adresse?.trim()) {
|
||||||
|
errors.push('L\'adresse est requise');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!merchant.phone?.trim()) {
|
||||||
|
errors.push('Le téléphone est requis');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!merchant.technicalContacts || merchant.technicalContacts.length === 0) {
|
||||||
|
errors.push('Au moins un contact technique est requis');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!merchant.configs || merchant.configs.length === 0) {
|
||||||
|
errors.push('Au moins une configuration est requise');
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthode pour obtenir les rôles disponibles pour les merchants
|
||||||
|
static getAvailableMerchantRoles(): UserRole[] {
|
||||||
|
return [
|
||||||
|
UserRole.DCB_PARTNER_ADMIN,
|
||||||
|
UserRole.DCB_PARTNER_MANAGER,
|
||||||
|
UserRole.DCB_PARTNER_SUPPORT
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si un rôle est valide pour un merchant
|
||||||
|
static isValidMerchantRole(role: UserRole): boolean {
|
||||||
|
const merchantRoles = [
|
||||||
|
UserRole.DCB_PARTNER_ADMIN,
|
||||||
|
UserRole.DCB_PARTNER_MANAGER,
|
||||||
|
UserRole.DCB_PARTNER_SUPPORT
|
||||||
|
];
|
||||||
|
return merchantRoles.includes(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/app/core/services/HUB-DCB-APIs.code-workspace
Normal file
11
src/app/core/services/HUB-DCB-APIs.code-workspace
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "../../../../../dcb-user-service"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../../.."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
@ -117,7 +117,7 @@ export class MenuService {
|
|||||||
},
|
},
|
||||||
|
|
||||||
{ label: 'Configurations', isTitle: true },
|
{ label: 'Configurations', isTitle: true },
|
||||||
{ label: 'Merchant Configs', icon: 'lucideStore', url: '/merchant-configs' },
|
{ label: 'Merchant Config', icon: 'lucideStore', url: '/merchant-config' },
|
||||||
{ label: 'Paramètres Système', icon: 'lucideSettings', url: '/settings' },
|
{ label: 'Paramètres Système', icon: 'lucideSettings', url: '/settings' },
|
||||||
{ label: 'Intégrations Externes', icon: 'lucidePlug', url: '/integrations' },
|
{ label: 'Intégrations Externes', icon: 'lucidePlug', url: '/integrations' },
|
||||||
|
|
||||||
|
|||||||
@ -76,7 +76,7 @@ export class PermissionsService {
|
|||||||
|
|
||||||
// Settings - Tout le monde
|
// Settings - Tout le monde
|
||||||
{
|
{
|
||||||
module: 'merchant-configs',
|
module: 'merchant-config',
|
||||||
roles: this.allRoles
|
roles: this.allRoles
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -126,7 +126,7 @@ export const menuItems: MenuItemType[] = [
|
|||||||
// Paramètres & Intégrations
|
// Paramètres & Intégrations
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
{ label: 'Configurations', isTitle: true },
|
{ label: 'Configurations', isTitle: true },
|
||||||
{ label: 'Merchant Configs', icon: 'lucideStore', url: '/merchant-configs' },
|
{ label: 'Merchant Config', icon: 'lucideStore', url: '/merchant-config' },
|
||||||
{ label: 'Paramètres Système', icon: 'lucideSettings', url: '/settings' },
|
{ label: 'Paramètres Système', icon: 'lucideSettings', url: '/settings' },
|
||||||
{ label: 'Intégrations Externes', icon: 'lucidePlug', url: '/integrations' },
|
{ label: 'Intégrations Externes', icon: 'lucidePlug', url: '/integrations' },
|
||||||
|
|
||||||
|
|||||||
335
src/app/modules/merchant-config/merchant-config.html
Normal file
335
src/app/modules/merchant-config/merchant-config.html
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
<app-ui-card title="Configuration Merchant">
|
||||||
|
<span helper-text class="badge badge-soft-primary badge-label fs-xxs py-1">
|
||||||
|
Gestion des Merchants
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="ins-wizard" card-body>
|
||||||
|
<!-- Progress Bar -->
|
||||||
|
<ngb-progressbar
|
||||||
|
class="mb-4"
|
||||||
|
[value]="progressValue"
|
||||||
|
type="primary"
|
||||||
|
height="6px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Navigation Steps -->
|
||||||
|
<ul class="nav nav-tabs wizard-tabs" role="tablist">
|
||||||
|
@for (step of wizardSteps; track $index; let i = $index) {
|
||||||
|
<li class="nav-item">
|
||||||
|
<a
|
||||||
|
href="javascript:void(0);"
|
||||||
|
[class.active]="i === currentStep"
|
||||||
|
class="nav-link"
|
||||||
|
[class.disabled]="!isStepAccessible(i)"
|
||||||
|
[class.wizard-item-done]="i < currentStep"
|
||||||
|
(click)="goToStep(i)"
|
||||||
|
>
|
||||||
|
<span class="d-flex align-items-center">
|
||||||
|
<ng-icon [name]="step.icon" class="fs-32" />
|
||||||
|
<span class="flex-grow-1 ms-2 text-truncate">
|
||||||
|
<span class="mb-0 lh-base d-block fw-semibold text-body fs-base">
|
||||||
|
{{ step.title }}
|
||||||
|
</span>
|
||||||
|
<span class="mb-0 fw-normal">{{ step.subtitle }}</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Messages -->
|
||||||
|
@if (configError) {
|
||||||
|
<div class="alert alert-danger mt-3">
|
||||||
|
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||||
|
{{ configError }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (configSuccess) {
|
||||||
|
<div class="alert alert-success mt-3">
|
||||||
|
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
|
||||||
|
{{ configSuccess }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Contenu des Steps -->
|
||||||
|
<div class="tab-content pt-3">
|
||||||
|
@for (step of wizardSteps; track $index; let i = $index) {
|
||||||
|
<div
|
||||||
|
class="tab-pane fade"
|
||||||
|
[class.show]="currentStep === i"
|
||||||
|
[class.active]="currentStep === i"
|
||||||
|
>
|
||||||
|
<form [formGroup]="merchantForm">
|
||||||
|
<!-- Step 1: Informations de Base -->
|
||||||
|
@if (i === 0) {
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Nom du Merchant *</label>
|
||||||
|
<div formGroupName="basicInfo">
|
||||||
|
<input type="text" class="form-control" formControlName="name"
|
||||||
|
placeholder="Nom commercial" />
|
||||||
|
@if (basicInfo.get('name')?.invalid && basicInfo.get('name')?.touched) {
|
||||||
|
<div class="text-danger small">Le nom du merchant est requis</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Logo URL</label>
|
||||||
|
<div formGroupName="basicInfo">
|
||||||
|
<input type="url" class="form-control" formControlName="logo"
|
||||||
|
placeholder="https://..." />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mb-3">
|
||||||
|
<label class="form-label">Description</label>
|
||||||
|
<div formGroupName="basicInfo">
|
||||||
|
<textarea class="form-control" formControlName="description"
|
||||||
|
placeholder="Description du merchant..." rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mb-3">
|
||||||
|
<label class="form-label">Adresse *</label>
|
||||||
|
<div formGroupName="basicInfo">
|
||||||
|
<input type="text" class="form-control" formControlName="adresse"
|
||||||
|
placeholder="Adresse complète" />
|
||||||
|
@if (basicInfo.get('adresse')?.invalid && basicInfo.get('adresse')?.touched) {
|
||||||
|
<div class="text-danger small">L'adresse est requise</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Téléphone *</label>
|
||||||
|
<div formGroupName="basicInfo">
|
||||||
|
<input type="tel" class="form-control" formControlName="phone"
|
||||||
|
placeholder="+225 XX XX XX XX" />
|
||||||
|
@if (basicInfo.get('phone')?.invalid && basicInfo.get('phone')?.touched) {
|
||||||
|
<div class="text-danger small">Le téléphone est requis et doit être valide</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Step 2: Contacts Techniques -->
|
||||||
|
@if (i === 1) {
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 mb-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="border-bottom pb-2">Contacts Techniques</h6>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="addTechnicalContact()">
|
||||||
|
<ng-icon name="lucidePlus" class="me-1"></ng-icon>
|
||||||
|
Ajouter un contact
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@for (contact of technicalContacts.controls; track $index; let idx = $index) {
|
||||||
|
<div class="col-12 mb-4 p-3 border rounded">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h6 class="mb-0">Contact #{{ idx + 1 }}</h6>
|
||||||
|
@if (technicalContacts.length > 1) {
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||||
|
(click)="removeTechnicalContact(idx)">
|
||||||
|
<ng-icon name="lucideTrash2"></ng-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Prénom *</label>
|
||||||
|
<input type="text" class="form-control"
|
||||||
|
[formControl]="getContactControl(contact, 'firstName')"
|
||||||
|
placeholder="Prénom" />
|
||||||
|
@if (getContactControl(contact, 'firstName').invalid && getContactControl(contact, 'firstName').touched) {
|
||||||
|
<div class="text-danger small">Le prénom est requis</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Nom *</label>
|
||||||
|
<input type="text" class="form-control"
|
||||||
|
[formControl]="getContactControl(contact, 'lastName')"
|
||||||
|
placeholder="Nom" />
|
||||||
|
@if (getContactControl(contact, 'lastName').invalid && getContactControl(contact, 'lastName').touched) {
|
||||||
|
<div class="text-danger small">Le nom est requis</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Téléphone *</label>
|
||||||
|
<input type="tel" class="form-control"
|
||||||
|
[formControl]="getContactControl(contact, 'phone')"
|
||||||
|
placeholder="+225 XX XX XX XX" />
|
||||||
|
@if (getContactControl(contact, 'phone').invalid && getContactControl(contact, 'phone').touched) {
|
||||||
|
<div class="text-danger small">Le téléphone est requis</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Email *</label>
|
||||||
|
<input type="email" class="form-control"
|
||||||
|
[formControl]="getContactControl(contact, 'email')"
|
||||||
|
placeholder="email@entreprise.com" />
|
||||||
|
@if (getContactControl(contact, 'email').invalid && getContactControl(contact, 'email').touched) {
|
||||||
|
<div class="text-danger small">L'email est requis et doit être valide</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Step 3: Configurations -->
|
||||||
|
@if (i === 2) {
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 mb-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="border-bottom pb-2">Configurations Techniques</h6>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="addConfig()">
|
||||||
|
<ng-icon name="lucidePlus" class="me-1"></ng-icon>
|
||||||
|
Ajouter une configuration
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@for (config of configs.controls; track $index; let idx = $index) {
|
||||||
|
<div class="col-12 mb-4 p-3 border rounded">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h6 class="mb-0">Configuration #{{ idx + 1 }}</h6>
|
||||||
|
@if (configs.length > 1) {
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||||
|
(click)="removeConfig(idx)">
|
||||||
|
<ng-icon name="lucideTrash2"></ng-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label class="form-label">Nom *</label>
|
||||||
|
<select class="form-select"
|
||||||
|
[formControl]="getConfigControl(config, 'name')">
|
||||||
|
<option value="">Sélectionnez un type</option>
|
||||||
|
@for (type of configTypes; track type.name) {
|
||||||
|
<option [value]="type.name">{{ type.label }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
@if (getConfigControl(config, 'name').invalid && getConfigControl(config, 'name').touched) {
|
||||||
|
<div class="text-danger small">Le nom est requis</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label class="form-label">Valeur *</label>
|
||||||
|
<input type="text" class="form-control"
|
||||||
|
[formControl]="getConfigControl(config, 'value')"
|
||||||
|
placeholder="Valeur de configuration" />
|
||||||
|
@if (getConfigControl(config, 'value').invalid && getConfigControl(config, 'value').touched) {
|
||||||
|
<div class="text-danger small">La valeur est requise</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label class="form-label">Opérateur *</label>
|
||||||
|
<select class="form-select"
|
||||||
|
[formControl]="getConfigControl(config, 'operatorId')">
|
||||||
|
@for (operator of operators; track operator.id) {
|
||||||
|
<option [value]="operator.id">{{ operator.name }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
@if (getConfigControl(config, 'operatorId').invalid && getConfigControl(config, 'operatorId').touched) {
|
||||||
|
<div class="text-danger small">L'opérateur est requis</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Step 4: Validation -->
|
||||||
|
@if (i === 4) {
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
|
||||||
|
Vérifiez les informations avant de créer le merchant
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title">Récapitulatif</h6>
|
||||||
|
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Informations de Base:</strong><br>
|
||||||
|
<strong>Nom:</strong> {{ basicInfo.value.name || 'Non renseigné' }}<br>
|
||||||
|
<strong>Description:</strong> {{ basicInfo.value.description || 'Non renseigné' }}<br>
|
||||||
|
<strong>Adresse:</strong> {{ basicInfo.value.adresse || 'Non renseigné' }}<br>
|
||||||
|
<strong>Téléphone:</strong> {{ basicInfo.value.phone || 'Non renseigné' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<strong>Contacts Techniques:</strong>
|
||||||
|
@for (contact of technicalContacts.value; track $index; let idx = $index) {
|
||||||
|
<div class="mt-2 p-2 bg-light rounded">
|
||||||
|
<strong>Contact #{{ idx + 1 }}:</strong>
|
||||||
|
{{ contact.firstName }} {{ contact.lastName }} -
|
||||||
|
{{ contact.phone }} - {{ contact.email }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<strong>Configurations:</strong>
|
||||||
|
@for (config of configs.value; track $index; let idx = $index) {
|
||||||
|
<div class="mt-2 p-2 bg-light rounded">
|
||||||
|
<strong>Configuration #{{ idx + 1 }}:</strong>
|
||||||
|
{{ getConfigTypeName(config.name) }} = {{ config.value }}
|
||||||
|
({{ getOperatorName(config.operatorId) }})
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Navigation Buttons -->
|
||||||
|
<div class="d-flex justify-content-between mt-4">
|
||||||
|
@if (i > 0) {
|
||||||
|
<button type="button" class="btn btn-secondary" (click)="previousStep()">
|
||||||
|
← Précédent
|
||||||
|
</button>
|
||||||
|
} @else {
|
||||||
|
<div></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (i < wizardSteps.length - 1) {
|
||||||
|
<button type="button" class="btn btn-primary" (click)="nextStep()"
|
||||||
|
[disabled]="!isStepValid(i)">
|
||||||
|
Suivant →
|
||||||
|
</button>
|
||||||
|
} @else {
|
||||||
|
<button type="button" class="btn btn-success"
|
||||||
|
(click)="submitForm()" [disabled]="configLoading">
|
||||||
|
@if (configLoading) {
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
Créer le Merchant
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</app-ui-card>
|
||||||
315
src/app/modules/merchant-config/merchant-config.service.ts
Normal file
315
src/app/modules/merchant-config/merchant-config.service.ts
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { environment } from '@environments/environment';
|
||||||
|
import { Observable, map, catchError, throwError } from 'rxjs';
|
||||||
|
|
||||||
|
// Import de vos modèles existants
|
||||||
|
import { UserRole } from '@core/models/dcb-bo-hub-user.model';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Merchant,
|
||||||
|
CreateMerchantDto,
|
||||||
|
UpdateMerchantDto,
|
||||||
|
MerchantUser,
|
||||||
|
AddUserToMerchantDto,
|
||||||
|
UpdateUserRoleDto,
|
||||||
|
MerchantConfig,
|
||||||
|
TechnicalContact,
|
||||||
|
ApiResponse,
|
||||||
|
PaginatedResponse,
|
||||||
|
MerchantStatsResponse,
|
||||||
|
SearchMerchantsParams,
|
||||||
|
MerchantStatus
|
||||||
|
} from '@core/models/merchant-config.model';
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class MerchantConfigService {
|
||||||
|
private http = inject(HttpClient);
|
||||||
|
private baseApiUrl = `${environment.configApiUrl}/merchants`;
|
||||||
|
|
||||||
|
// Merchant CRUD Operations
|
||||||
|
createMerchant(createMerchantDto: CreateMerchantDto): Observable<Merchant> {
|
||||||
|
return this.http.post<ApiResponse<Merchant>>(this.baseApiUrl, createMerchantDto).pipe(
|
||||||
|
map(response => {
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || 'Failed to create merchant');
|
||||||
|
}
|
||||||
|
return response.data!;
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error('Error creating merchant:', error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMerchants(page: number = 1, limit: number = 10, params?: SearchMerchantsParams): Observable<PaginatedResponse<Merchant>> {
|
||||||
|
let httpParams = new HttpParams()
|
||||||
|
.set('page', page.toString())
|
||||||
|
.set('limit', limit.toString());
|
||||||
|
|
||||||
|
if (params?.query) {
|
||||||
|
httpParams = httpParams.set('query', params.query);
|
||||||
|
}
|
||||||
|
if (params?.status) {
|
||||||
|
httpParams = httpParams.set('status', params.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.get<ApiResponse<PaginatedResponse<Merchant>>>(this.baseApiUrl, { params: httpParams }).pipe(
|
||||||
|
map(response => {
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || 'Failed to load merchants');
|
||||||
|
}
|
||||||
|
return response.data!;
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error('Error loading merchants:', error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMerchantById(id: number): Observable<Merchant> {
|
||||||
|
return this.http.get<ApiResponse<Merchant>>(`${this.baseApiUrl}/${id}`).pipe(
|
||||||
|
map(response => {
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || 'Failed to load merchant');
|
||||||
|
}
|
||||||
|
return response.data!;
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error(`Error loading merchant ${id}:`, error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMerchant(id: number, updateMerchantDto: UpdateMerchantDto): Observable<Merchant> {
|
||||||
|
return this.http.patch<ApiResponse<Merchant>>(`${this.baseApiUrl}/${id}`, updateMerchantDto).pipe(
|
||||||
|
map(response => {
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || 'Failed to update merchant');
|
||||||
|
}
|
||||||
|
return response.data!;
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error(`Error updating merchant ${id}:`, error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteMerchant(id: number): Observable<void> {
|
||||||
|
return this.http.delete<ApiResponse<void>>(`${this.baseApiUrl}/${id}`).pipe(
|
||||||
|
map(response => {
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || 'Failed to delete merchant');
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error(`Error deleting merchant ${id}:`, error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// User Management
|
||||||
|
addUserToMerchant(addUserDto: AddUserToMerchantDto): Observable<MerchantUser> {
|
||||||
|
return this.http.post<ApiResponse<MerchantUser>>(`${this.baseApiUrl}/users`, addUserDto).pipe(
|
||||||
|
map(response => {
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || 'Failed to add user to merchant');
|
||||||
|
}
|
||||||
|
return response.data!;
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error('Error adding user to merchant:', error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMerchantUsers(merchantId: number): Observable<MerchantUser[]> {
|
||||||
|
return this.http.get<ApiResponse<MerchantUser[]>>(`${this.baseApiUrl}/${merchantId}/users`).pipe(
|
||||||
|
map(response => {
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || 'Failed to load merchant users');
|
||||||
|
}
|
||||||
|
return response.data!;
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error(`Error loading users for merchant ${merchantId}:`, error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUserRole(merchantId: number, userId: string, updateRoleDto: UpdateUserRoleDto): Observable<MerchantUser> {
|
||||||
|
return this.http.patch<ApiResponse<MerchantUser>>(
|
||||||
|
`${this.baseApiUrl}/${merchantId}/users/${userId}/role`,
|
||||||
|
updateRoleDto
|
||||||
|
).pipe(
|
||||||
|
map(response => {
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || 'Failed to update user role');
|
||||||
|
}
|
||||||
|
return response.data!;
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error(`Error updating user role for merchant ${merchantId}, user ${userId}:`, error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeUserFromMerchant(merchantId: number, userId: string): Observable<void> {
|
||||||
|
return this.http.delete<ApiResponse<void>>(`${this.baseApiUrl}/${merchantId}/users/${userId}`).pipe(
|
||||||
|
map(response => {
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || 'Failed to remove user from merchant');
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error(`Error removing user ${userId} from merchant ${merchantId}:`, error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserMerchants(userId: string): Observable<Merchant[]> {
|
||||||
|
return this.http.get<ApiResponse<Merchant[]>>(`${this.baseApiUrl}/user/${userId}`).pipe(
|
||||||
|
map(response => {
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || 'Failed to load user merchants');
|
||||||
|
}
|
||||||
|
return response.data!;
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error(`Error loading merchants for user ${userId}:`, error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config Management
|
||||||
|
addConfigToMerchant(merchantId: number, config: Omit<MerchantConfig, 'id' | 'merchantId'>): Observable<MerchantConfig> {
|
||||||
|
return this.http.post<ApiResponse<MerchantConfig>>(`${this.baseApiUrl}/${merchantId}/configs`, config).pipe(
|
||||||
|
map(response => {
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || 'Failed to add config to merchant');
|
||||||
|
}
|
||||||
|
return response.data!;
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error(`Error adding config to merchant ${merchantId}:`, error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConfig(configId: number, config: Partial<MerchantConfig>): Observable<MerchantConfig> {
|
||||||
|
return this.http.patch<ApiResponse<MerchantConfig>>(`${this.baseApiUrl}/configs/${configId}`, config).pipe(
|
||||||
|
map(response => {
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || 'Failed to update config');
|
||||||
|
}
|
||||||
|
return response.data!;
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error(`Error updating config ${configId}:`, error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteConfig(configId: number): Observable<void> {
|
||||||
|
return this.http.delete<ApiResponse<void>>(`${this.baseApiUrl}/configs/${configId}`).pipe(
|
||||||
|
map(response => {
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || 'Failed to delete config');
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error(`Error deleting config ${configId}:`, error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Technical Contacts Management
|
||||||
|
addTechnicalContact(merchantId: number, contact: Omit<TechnicalContact, 'id' | 'merchantId'>): Observable<TechnicalContact> {
|
||||||
|
return this.http.post<ApiResponse<TechnicalContact>>(`${this.baseApiUrl}/${merchantId}/contacts`, contact).pipe(
|
||||||
|
map(response => {
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || 'Failed to add technical contact');
|
||||||
|
}
|
||||||
|
return response.data!;
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error(`Error adding technical contact to merchant ${merchantId}:`, error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTechnicalContact(contactId: number, contact: Partial<TechnicalContact>): Observable<TechnicalContact> {
|
||||||
|
return this.http.patch<ApiResponse<TechnicalContact>>(`${this.baseApiUrl}/contacts/${contactId}`, contact).pipe(
|
||||||
|
map(response => {
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || 'Failed to update technical contact');
|
||||||
|
}
|
||||||
|
return response.data!;
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error(`Error updating technical contact ${contactId}:`, error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTechnicalContact(contactId: number): Observable<void> {
|
||||||
|
return this.http.delete<ApiResponse<void>>(`${this.baseApiUrl}/contacts/${contactId}`).pipe(
|
||||||
|
map(response => {
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || 'Failed to delete technical contact');
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error(`Error deleting technical contact ${contactId}:`, error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
getMerchantStats(): Observable<MerchantStatsResponse> {
|
||||||
|
return this.http.get<ApiResponse<MerchantStatsResponse>>(`${this.baseApiUrl}/stats`).pipe(
|
||||||
|
map(response => {
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || 'Failed to load merchant statistics');
|
||||||
|
}
|
||||||
|
return response.data!;
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error('Error loading merchant statistics:', error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status Management
|
||||||
|
updateMerchantStatus(merchantId: number, status: MerchantStatus): Observable<Merchant> {
|
||||||
|
return this.http.patch<ApiResponse<Merchant>>(`${this.baseApiUrl}/${merchantId}/status`, { status }).pipe(
|
||||||
|
map(response => {
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || 'Failed to update merchant status');
|
||||||
|
}
|
||||||
|
return response.data!;
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error(`Error updating status for merchant ${merchantId}:`, error);
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
294
src/app/modules/merchant-config/merchant-config.ts
Normal file
294
src/app/modules/merchant-config/merchant-config.ts
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
import { ChangeDetectorRef, Component, inject, OnInit } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule, ReactiveFormsModule, FormBuilder, Validators, FormArray, FormGroup, FormControl } from '@angular/forms';
|
||||||
|
import { NgIcon } from '@ng-icons/core';
|
||||||
|
import { NgbProgressbarModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { UiCard } from '@app/components/ui-card';
|
||||||
|
import { MerchantConfigService } from './merchant-config.service';
|
||||||
|
import { CreateMerchantDto, MerchantConfig as MerchantConfigModel, TechnicalContact, MerchantUtils, ConfigType, Operator } from '@core/models/merchant-config.model';
|
||||||
|
import { UserRole } from '@core/models/dcb-bo-hub-user.model';
|
||||||
|
import { firstValueFrom, Subject } from 'rxjs';
|
||||||
|
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-merchant-config',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NgIcon,
|
||||||
|
NgbProgressbarModule,
|
||||||
|
UiCard
|
||||||
|
],
|
||||||
|
templateUrl: './merchant-config.html'
|
||||||
|
})
|
||||||
|
export class MerchantConfig implements OnInit {
|
||||||
|
private fb = inject(FormBuilder);
|
||||||
|
private merchantConfigService = inject(MerchantConfigService);
|
||||||
|
protected roleService = inject(RoleManagementService);
|
||||||
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
// Configuration wizard
|
||||||
|
currentStep = 0;
|
||||||
|
wizardSteps = [
|
||||||
|
{ id: 'basic-info', icon: 'lucideBuilding', title: 'Informations de Base', subtitle: 'Détails du merchant' },
|
||||||
|
{ id: 'technical-contacts', icon: 'lucideUsers', title: 'Contacts Techniques', subtitle: 'Personnes de contact' },
|
||||||
|
{ id: 'configurations', icon: 'lucideSettings', title: 'Configurations', subtitle: 'Paramètres techniques' },
|
||||||
|
{ id: 'review', icon: 'lucideCheckCircle', title: 'Validation', subtitle: 'Vérification finale' }
|
||||||
|
];
|
||||||
|
|
||||||
|
configLoading = false;
|
||||||
|
configError = '';
|
||||||
|
configSuccess = '';
|
||||||
|
|
||||||
|
// Formulaires
|
||||||
|
merchantForm = this.fb.group({
|
||||||
|
basicInfo: this.fb.group({
|
||||||
|
name: ['', [Validators.required, Validators.minLength(2)]],
|
||||||
|
logo: [''],
|
||||||
|
description: [''],
|
||||||
|
adresse: ['', [Validators.required]],
|
||||||
|
phone: ['', [Validators.required, Validators.pattern(/^\+?[0-9\s\-\(\)]+$/)]]
|
||||||
|
}),
|
||||||
|
technicalContacts: this.fb.array([]),
|
||||||
|
configs: this.fb.array([])
|
||||||
|
});
|
||||||
|
|
||||||
|
// Opérateurs disponibles
|
||||||
|
operators = [
|
||||||
|
{ id: Operator.ORANGE_CI, name: 'Orange CI' },
|
||||||
|
{ id: Operator.MTN_CI, name: 'MTN CI' },
|
||||||
|
{ id: Operator.MOOV_CI, name: 'Moov CI' },
|
||||||
|
{ id: Operator.WAVE, name: 'Wave' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Types de configuration
|
||||||
|
configTypes = [
|
||||||
|
{ name: ConfigType.API_KEY, label: 'Clé API' },
|
||||||
|
{ name: ConfigType.SECRET_KEY, label: 'Clé Secrète' },
|
||||||
|
{ name: ConfigType.WEBHOOK_URL, label: 'URL Webhook' },
|
||||||
|
{ name: ConfigType.CALLBACK_URL, label: 'URL Callback' },
|
||||||
|
{ name: ConfigType.TIMEOUT, label: 'Timeout (ms)' },
|
||||||
|
{ name: ConfigType.RETRY_COUNT, label: 'Nombre de tentatives' },
|
||||||
|
{ name: ConfigType.CUSTOM, label: 'Personnalisé' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Rôles disponibles pour les merchants (utilisation de vos rôles existants)
|
||||||
|
availableMerchantRoles = MerchantUtils.getAvailableMerchantRoles();
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
// Ajouter un contact technique par défaut
|
||||||
|
this.addTechnicalContact();
|
||||||
|
// Ajouter une configuration par défaut
|
||||||
|
this.addConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation du wizard
|
||||||
|
get progressValue(): number {
|
||||||
|
return ((this.currentStep + 1) / this.wizardSteps.length) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextStep() {
|
||||||
|
if (this.currentStep < this.wizardSteps.length - 1 && this.isStepValid(this.currentStep)) {
|
||||||
|
this.currentStep++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
previousStep() {
|
||||||
|
if (this.currentStep > 0) {
|
||||||
|
this.currentStep--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
goToStep(index: number) {
|
||||||
|
if (this.isStepAccessible(index)) {
|
||||||
|
this.currentStep = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isStepAccessible(index: number): boolean {
|
||||||
|
if (index === 0) return true;
|
||||||
|
|
||||||
|
for (let i = 0; i < index; i++) {
|
||||||
|
if (!this.isStepValid(i)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
isStepValid(stepIndex: number): boolean {
|
||||||
|
switch (stepIndex) {
|
||||||
|
case 0: // Basic Info
|
||||||
|
return this.basicInfo.valid;
|
||||||
|
case 1: // Technical Contacts
|
||||||
|
return this.technicalContacts.valid && this.technicalContacts.length > 0;
|
||||||
|
case 2: // Configurations
|
||||||
|
return this.configs.valid;
|
||||||
|
case 3: // Review
|
||||||
|
return this.merchantForm.valid;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestion des contacts techniques
|
||||||
|
get technicalContacts(): FormArray {
|
||||||
|
return this.merchantForm.get('technicalContacts') as FormArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTechnicalContact() {
|
||||||
|
const contactGroup = this.fb.group({
|
||||||
|
firstName: ['', [Validators.required]],
|
||||||
|
lastName: ['', [Validators.required]],
|
||||||
|
phone: ['', [Validators.required, Validators.pattern(/^\+?[0-9\s\-\(\)]+$/)]],
|
||||||
|
email: ['', [Validators.required, Validators.email]]
|
||||||
|
});
|
||||||
|
this.technicalContacts.push(contactGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTechnicalContact(index: number) {
|
||||||
|
if (this.technicalContacts.length > 1) {
|
||||||
|
this.technicalContacts.removeAt(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getContactControl(contact: any, field: string): FormControl {
|
||||||
|
return contact.get(field) as FormControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestion des configurations
|
||||||
|
get configs(): FormArray {
|
||||||
|
return this.merchantForm.get('configs') as FormArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
addConfig() {
|
||||||
|
const configGroup = this.fb.group({
|
||||||
|
name: ['', [Validators.required]],
|
||||||
|
value: ['', [Validators.required]],
|
||||||
|
operatorId: [Operator.ORANGE_CI, [Validators.required]]
|
||||||
|
});
|
||||||
|
this.configs.push(configGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeConfig(index: number) {
|
||||||
|
if (this.configs.length > 1) {
|
||||||
|
this.configs.removeAt(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfigControl(config: any, field: string): FormControl {
|
||||||
|
return config.get(field) as FormControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Soumission du formulaire
|
||||||
|
async submitForm() {
|
||||||
|
if (this.merchantForm.valid) {
|
||||||
|
this.configLoading = true;
|
||||||
|
this.configError = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = this.merchantForm.value;
|
||||||
|
|
||||||
|
const createMerchantDto: CreateMerchantDto = {
|
||||||
|
name: this.safeString(formData.basicInfo?.name),
|
||||||
|
logo: this.safeString(formData.basicInfo?.logo),
|
||||||
|
description: this.safeString(formData.basicInfo?.description),
|
||||||
|
adresse: this.safeString(formData.basicInfo?.adresse),
|
||||||
|
phone: this.safeString(formData.basicInfo?.phone),
|
||||||
|
technicalContacts: (formData.technicalContacts || []).map((contact: any) => ({
|
||||||
|
firstName: this.safeString(contact.firstName),
|
||||||
|
lastName: this.safeString(contact.lastName),
|
||||||
|
phone: this.safeString(contact.phone),
|
||||||
|
email: this.safeString(contact.email)
|
||||||
|
})),
|
||||||
|
configs: (formData.configs || []).map((config: any) => ({
|
||||||
|
name: this.safeString(config.name),
|
||||||
|
value: this.safeString(config.value),
|
||||||
|
operatorId: this.safeNumber(config.operatorId)
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validation avant envoi
|
||||||
|
const validationErrors = MerchantUtils.validateMerchantCreation(createMerchantDto);
|
||||||
|
if (validationErrors.length > 0) {
|
||||||
|
this.configError = validationErrors.join(', ');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.merchantConfigService.createMerchant(createMerchantDto)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.configSuccess = `Merchant créé avec succès! ID: ${response.id}`;
|
||||||
|
this.merchantForm.reset();
|
||||||
|
this.currentStep = 0;
|
||||||
|
|
||||||
|
// Réinitialiser les tableaux
|
||||||
|
this.technicalContacts.clear();
|
||||||
|
this.configs.clear();
|
||||||
|
this.addTechnicalContact();
|
||||||
|
this.addConfig();
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
this.configError = error.message || 'Erreur lors de la création du merchant';
|
||||||
|
console.error('Error creating merchant:', error);
|
||||||
|
} finally {
|
||||||
|
this.configLoading = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.configError = 'Veuillez corriger les erreurs dans le formulaire';
|
||||||
|
this.markAllFieldsAsTouched();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes utilitaires
|
||||||
|
private safeString(value: string | null | undefined): string {
|
||||||
|
return value || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private safeNumber(value: number | null | undefined): number {
|
||||||
|
return value || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private markAllFieldsAsTouched() {
|
||||||
|
Object.keys(this.merchantForm.controls).forEach(key => {
|
||||||
|
const control = this.merchantForm.get(key);
|
||||||
|
if (control instanceof FormGroup) {
|
||||||
|
Object.keys(control.controls).forEach(subKey => {
|
||||||
|
control.get(subKey)?.markAsTouched();
|
||||||
|
});
|
||||||
|
} else if (control instanceof FormArray) {
|
||||||
|
control.controls.forEach((arrayControl: any) => {
|
||||||
|
if (arrayControl instanceof FormGroup) {
|
||||||
|
Object.keys(arrayControl.controls).forEach(subKey => {
|
||||||
|
arrayControl.get(subKey)?.markAsTouched();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
control?.markAsTouched();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters pour les formulaires
|
||||||
|
get basicInfo() {
|
||||||
|
return this.merchantForm.get('basicInfo') as FormGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes pour le template
|
||||||
|
getOperatorName(operatorId: Operator): string {
|
||||||
|
return MerchantUtils.getOperatorName(operatorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfigTypeName(configName: string): string {
|
||||||
|
return MerchantUtils.getConfigTypeName(configName);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoleLabel(role: UserRole): string {
|
||||||
|
return this.roleService.getRoleLabel(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,6 +23,7 @@ import { Help } from '@modules/help/help';
|
|||||||
import { About } from '@modules/about/about';
|
import { About } from '@modules/about/about';
|
||||||
import { SubscriptionsManagement } from './subscriptions/subscriptions';
|
import { SubscriptionsManagement } from './subscriptions/subscriptions';
|
||||||
import { SubscriptionPayments } from './subscriptions/subscription-payments/subscription-payments';
|
import { SubscriptionPayments } from './subscriptions/subscription-payments/subscription-payments';
|
||||||
|
import { MerchantConfig } from './merchant-config/merchant-config';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
@ -164,7 +165,7 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// Partners (existant - gardé pour référence)
|
// Partners
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
{
|
{
|
||||||
path: 'merchant-users-management',
|
path: 'merchant-users-management',
|
||||||
@ -184,6 +185,20 @@ const routes: Routes = [
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// Merchant Config
|
||||||
|
// ---------------------------
|
||||||
|
{
|
||||||
|
path: 'merchant-config',
|
||||||
|
component: MerchantConfig,
|
||||||
|
canActivate: [authGuard, roleGuard],
|
||||||
|
data: {
|
||||||
|
title: 'Merchant Config',
|
||||||
|
module: 'merchant-config'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// Operators (Admin seulement)
|
// Operators (Admin seulement)
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
|
|||||||
@ -2,5 +2,6 @@ export const environment = {
|
|||||||
production: true,
|
production: true,
|
||||||
localServiceTestApiUrl: "https://backoffice.dcb.pixpay.sn/api/v1",
|
localServiceTestApiUrl: "https://backoffice.dcb.pixpay.sn/api/v1",
|
||||||
iamApiUrl: "https://api-user-service.dcb.pixpay.sn/api/v1",
|
iamApiUrl: "https://api-user-service.dcb.pixpay.sn/api/v1",
|
||||||
|
configApiUrl: 'https://api-merchant-config-service.dcb.pixpay.sn/api/v1',
|
||||||
apiCoreUrl: 'https://api-core-service.dcb.pixpay.sn/api/v1',
|
apiCoreUrl: 'https://api-core-service.dcb.pixpay.sn/api/v1',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,5 +2,6 @@ export const environment = {
|
|||||||
production: true,
|
production: true,
|
||||||
localServiceTestApiUrl: "https://backoffice.dcb.pixpay.sn/api/v1",
|
localServiceTestApiUrl: "https://backoffice.dcb.pixpay.sn/api/v1",
|
||||||
iamApiUrl: "https://api-user-service.dcb.pixpay.sn/api/v1",
|
iamApiUrl: "https://api-user-service.dcb.pixpay.sn/api/v1",
|
||||||
|
configApiUrl: 'https://api-merchant-config-service.dcb.pixpay.sn/api/v1',
|
||||||
apiCoreUrl: 'https://api-core-service.dcb.pixpay.sn/api/v1',
|
apiCoreUrl: 'https://api-core-service.dcb.pixpay.sn/api/v1',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,5 +2,6 @@ export const environment = {
|
|||||||
production: false,
|
production: false,
|
||||||
localServiceTestApiUrl: "http://localhost:4200/api/v1",
|
localServiceTestApiUrl: "http://localhost:4200/api/v1",
|
||||||
iamApiUrl: "http://localhost:3000/api/v1",
|
iamApiUrl: "http://localhost:3000/api/v1",
|
||||||
|
configApiUrl: 'https://api-merchant-config-service.dcb.pixpay.sn/api/v1',
|
||||||
apiCoreUrl: 'https://api-core-service.dcb.pixpay.sn/api/v1',
|
apiCoreUrl: 'https://api-core-service.dcb.pixpay.sn/api/v1',
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user