@cbm-common/contact-repository
v0.0.1
Published
Repositorio Angular para la gestión completa de contactos en sistemas empresariales CBM. Implementa operaciones de consulta de contactos asociados a clientes y proveedores con información detallada de identificación, comunicación y auditoría.
Readme
Contact Repository
Repositorio Angular para la gestión completa de contactos en sistemas empresariales CBM. Implementa operaciones de consulta de contactos asociados a clientes y proveedores con información detallada de identificación, comunicación y auditoría.
📦 Instalación
npm install @cbm-common/contact-repository⚙️ Configuración
Configuración del Módulo
El módulo debe configurarse en el módulo raíz de la aplicación:
import { CbmContactModule } from '@cbm-common/contact-repository';
@NgModule({
imports: [
CbmContactModule.forRoot({
baseUrl: 'https://api.cbm.com/contacts'
})
]
})
export class AppModule {}Configuración Standalone
Para aplicaciones standalone, configura el módulo en el bootstrap:
import { CbmContactModule } from '@cbm-common/contact-repository';
bootstrapApplication(AppComponent, {
providers: [
CbmContactModule.forRoot({
baseUrl: 'https://api.cbm.com/contacts'
})
]
});🎯 Inyección de Dependencias
Inyección del Servicio
import { CbmContactService } from '@cbm-common/contact-repository';
@Component({
selector: 'app-contact-manager',
standalone: true,
imports: [CbmContactService]
})
export class ContactManagerComponent {
constructor(private contactService: CbmContactService) {}
}Inyección del Repositorio
import { CbmContactRepository } from '@cbm-common/contact-repository';
@Component({
selector: 'app-contact-list',
standalone: true,
imports: [CbmContactRepository]
})
export class ContactListComponent {
constructor(private contactRepo: CbmContactRepository) {}
}🏗️ Arquitectura del Repositorio
Patrón de Diseño
El repositorio sigue el patrón Repository Pattern con Dependency Injection:
CbmContactModule
├── ICbmContactModuleConfig (configuración)
├── CbmContactService (implementa ICbmContactRepository)
├── CbmContactRepository (wrapper del service)
├── CbmContactModel (modelos de datos)
└── HttpClient (cliente HTTP)Interfaz del Repositorio
export interface ICbmContactRepository {
list(params: CbmContactModel.ListParams): Observable<CbmContactModel.ListResponse>;
}📊 Operaciones Disponibles
Listado de Contactos
// Listado de contactos con filtros avanzados
list(params: {
identification_number?: string; // Número de identificación del contacto
provider_id?: string; // ID del proveedor asociado
client_id?: string; // ID del cliente asociado
}): Observable<ListResponse>📋 Modelos de Datos
ListResponse.Data
Información completa de un contacto:
interface Data {
_id: string;
company_id?: string; // ID de la empresa
client_id?: string; // ID del cliente asociado
identification_number?: string; // Número de identificación (cédula/RUC)
full_name?: string; // Nombre completo del contacto
phone_code?: string; // Código de teléfono
cellphone?: string; // Número de celular
email?: string; // Correo electrónico
created_user?: string; // Usuario que creó el contacto
created_at?: number; // Fecha de creación (timestamp)
updated_at?: number; // Fecha de actualización (timestamp)
updated_user?: string; // Usuario que actualizó el contacto
provider_id?: string; // ID del proveedor asociado
}ListResponse
Respuesta del listado de contactos:
interface ListResponse {
success: boolean; // Indica si la operación fue exitosa
data: Data[]; // Array de contactos
}🚀 Ejemplos de Uso
Ejemplo Básico: Gestión de Contactos
import { CbmContactService } from '@cbm-common/contact-repository';
import { CbmContactModel } from '@cbm-common/contact-repository';
@Component({
selector: 'app-contact-list',
standalone: true,
template: `
<div class="contact-list">
<h2>Gestión de Contactos</h2>
<!-- Filtros de búsqueda -->
<div class="filters">
<input [(ngModel)]="filters.identification_number" placeholder="Número de identificación">
<input [(ngModel)]="filters.provider_id" placeholder="ID del proveedor">
<input [(ngModel)]="filters.client_id" placeholder="ID del cliente">
<button (click)="loadContacts()">Buscar</button>
<button (click)="clearFilters()">Limpiar</button>
</div>
<!-- Tabla de contactos -->
<div class="table-container">
<table>
<thead>
<tr>
<th>Nombre Completo</th>
<th>Identificación</th>
<th>Email</th>
<th>Celular</th>
<th>Cliente</th>
<th>Proveedor</th>
<th>Fecha Creación</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let contact of contacts">
<td>{{ contact.full_name }}</td>
<td>{{ contact.identification_number }}</td>
<td>{{ contact.email }}</td>
<td>{{ contact.phone_code }} {{ contact.cellphone }}</td>
<td>{{ contact.client_id }}</td>
<td>{{ contact.provider_id }}</td>
<td>{{ contact.created_at | date:'dd/MM/yyyy HH:mm' }}</td>
<td>
<button (click)="viewContact(contact)">Ver</button>
<button (click)="editContact(contact)">Editar</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Paginación -->
<div class="pagination" *ngIf="contacts.length > 0">
<button [disabled]="currentPage === 1" (click)="changePage(currentPage - 1)">
Anterior
</button>
<span>Página {{ currentPage }}</span>
<button [disabled]="contacts.length < pageSize" (click)="changePage(currentPage + 1)">
Siguiente
</button>
</div>
<!-- Modal de detalle de contacto -->
<div class="modal" *ngIf="selectedContact" (click)="closeModal()">
<div class="modal-content" (click)="$event.stopPropagation()">
<h3>Detalle del Contacto</h3>
<div class="contact-detail">
<div class="detail-row">
<label>Nombre:</label>
<span>{{ selectedContact.full_name }}</span>
</div>
<div class="detail-row">
<label>Identificación:</label>
<span>{{ selectedContact.identification_number }}</span>
</div>
<div class="detail-row">
<label>Email:</label>
<span>{{ selectedContact.email }}</span>
</div>
<div class="detail-row">
<label>Celular:</label>
<span>{{ selectedContact.phone_code }} {{ selectedContact.cellphone }}</span>
</div>
<div class="detail-row">
<label>ID Cliente:</label>
<span>{{ selectedContact.client_id }}</span>
</div>
<div class="detail-row">
<label>ID Proveedor:</label>
<span>{{ selectedContact.provider_id }}</span>
</div>
<div class="detail-row">
<label>Creado por:</label>
<span>{{ selectedContact.created_user }}</span>
</div>
<div class="detail-row">
<label>Fecha creación:</label>
<span>{{ selectedContact.created_at | date:'dd/MM/yyyy HH:mm' }}</span>
</div>
<div class="detail-row" *ngIf="selectedContact.updated_at">
<label>Última actualización:</label>
<span>{{ selectedContact.updated_at | date:'dd/MM/yyyy HH:mm' }}</span>
</div>
<div class="detail-row" *ngIf="selectedContact.updated_user">
<label>Actualizado por:</label>
<span>{{ selectedContact.updated_user }}</span>
</div>
</div>
<div class="modal-actions">
<button (click)="closeModal()">Cerrar</button>
</div>
</div>
</div>
</div>
`,
styles: [`
.contact-list { padding: 20px; }
.filters { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; align-items: center; }
.table-container { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
th { background-color: #f8f9fa; font-weight: bold; }
.pagination { display: flex; justify-content: center; align-items: center; gap: 10px; margin-top: 20px; }
button { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; }
button:hover { opacity: 0.8; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
/* Modal styles */
.modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
.modal-content { background: white; padding: 20px; border-radius: 8px; max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto; }
.contact-detail { margin: 20px 0; }
.detail-row { display: flex; margin-bottom: 10px; }
.detail-row label { font-weight: bold; min-width: 140px; margin-right: 10px; }
.detail-row span { flex: 1; }
.modal-actions { display: flex; justify-content: flex-end; margin-top: 20px; }
`]
})
export class ContactListComponent implements OnInit {
contacts: CbmContactModel.ListResponse.Data[] = [];
selectedContact: CbmContactModel.ListResponse.Data | null = null;
currentPage = 1;
pageSize = 20;
filters: Partial<CbmContactModel.ListParams> = {
identification_number: '',
provider_id: '',
client_id: ''
};
constructor(private contactService: CbmContactService) {}
ngOnInit() {
this.loadContacts();
}
loadContacts() {
const params: CbmContactModel.ListParams = {
...this.filters
};
// Remover filtros vacíos
Object.keys(params).forEach(key => {
if (!params[key as keyof CbmContactModel.ListParams]) {
delete params[key as keyof CbmContactModel.ListParams];
}
});
this.contactService.list(params).subscribe({
next: (response) => {
if (response.success) {
this.contacts = response.data;
}
},
error: (error) => {
console.error('Error al cargar contactos:', error);
}
});
}
clearFilters() {
this.filters = {
identification_number: '',
provider_id: '',
client_id: ''
};
this.currentPage = 1;
this.loadContacts();
}
changePage(page: number) {
this.currentPage = page;
this.loadContacts();
}
viewContact(contact: CbmContactModel.ListResponse.Data) {
this.selectedContact = contact;
}
closeModal() {
this.selectedContact = null;
}
editContact(contact: CbmContactModel.ListResponse.Data) {
// Implementar navegación a edición
console.log('Editar contacto:', contact);
}
}Ejemplo con Búsqueda Avanzada
import { CbmContactService } from '@cbm-common/contact-repository';
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-contact-search',
standalone: true,
imports: [ReactiveFormsModule, CbmContactService],
template: `
<div class="contact-search">
<h2>Búsqueda Avanzada de Contactos</h2>
<form [formGroup]="searchForm" (ngSubmit)="onSearch()">
<div class="search-form">
<div class="form-row">
<div class="form-group">
<label for="identification">Número de Identificación</label>
<input id="identification" type="text" formControlName="identification_number" placeholder="Ingrese cédula o RUC">
</div>
<div class="form-group">
<label for="client">ID del Cliente</label>
<input id="client" type="text" formControlName="client_id" placeholder="ID del cliente">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="provider">ID del Proveedor</label>
<input id="provider" type="text" formControlName="provider_id" placeholder="ID del proveedor">
</div>
<div class="form-group">
<label for="searchType">Tipo de Búsqueda</label>
<select id="searchType" formControlName="searchType">
<option value="all">Todos los campos</option>
<option value="identification">Solo identificación</option>
<option value="client">Solo cliente</option>
<option value="provider">Solo proveedor</option>
</select>
</div>
</div>
<div class="form-actions">
<button type="submit" [disabled]="searchForm.invalid">Buscar</button>
<button type="button" (click)="clearSearch()">Limpiar</button>
</div>
</div>
</form>
<!-- Resultados de búsqueda -->
<div class="search-results" *ngIf="searchResults.length > 0">
<h3>Resultados ({{ searchResults.length }})</h3>
<div class="results-grid">
<div *ngFor="let contact of searchResults" class="contact-card">
<div class="card-header">
<h4>{{ contact.full_name }}</h4>
<span class="identification">{{ contact.identification_number }}</span>
</div>
<div class="card-body">
<p *ngIf="contact.email"><strong>Email:</strong> {{ contact.email }}</p>
<p *ngIf="contact.cellphone"><strong>Celular:</strong> {{ contact.phone_code }} {{ contact.cellphone }}</p>
<p *ngIf="contact.client_id"><strong>Cliente:</strong> {{ contact.client_id }}</p>
<p *ngIf="contact.provider_id"><strong>Proveedor:</strong> {{ contact.provider_id }}</p>
<p><strong>Creado:</strong> {{ contact.created_at | date:'dd/MM/yyyy' }}</p>
</div>
<div class="card-actions">
<button (click)="selectContact(contact)">Seleccionar</button>
</div>
</div>
</div>
</div>
<!-- Mensaje cuando no hay resultados -->
<div class="no-results" *ngIf="searchPerformed && searchResults.length === 0">
<p>No se encontraron contactos que coincidan con los criterios de búsqueda.</p>
</div>
</div>
`,
styles: [`
.contact-search { padding: 20px; }
.search-form { background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
.form-row { display: flex; gap: 15px; margin-bottom: 15px; }
.form-group { flex: 1; }
.form-group label { display: block; margin-bottom: 5px; font-weight: bold; }
input, select { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
.form-actions { display: flex; gap: 10px; justify-content: flex-end; }
.search-results { margin-top: 20px; }
.results-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px; }
.contact-card { border: 1px solid #ddd; border-radius: 8px; padding: 15px; background: white; }
.card-header { margin-bottom: 10px; }
.card-header h4 { margin: 0 0 5px 0; }
.identification { color: #666; font-size: 14px; }
.card-body p { margin: 5px 0; font-size: 14px; }
.card-actions { margin-top: 15px; }
.no-results { text-align: center; padding: 40px; color: #666; }
button { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; }
button[type="submit"] { background: #007bff; color: white; }
button[type="button"] { background: #6c757d; color: white; }
button:hover { opacity: 0.8; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
`]
})
export class ContactSearchComponent implements OnInit {
searchForm!: FormGroup;
searchResults: CbmContactModel.ListResponse.Data[] = [];
searchPerformed = false;
constructor(
private fb: FormBuilder,
private contactService: CbmContactService
) {}
ngOnInit() {
this.initForm();
}
initForm() {
this.searchForm = this.fb.group({
identification_number: [''],
client_id: [''],
provider_id: [''],
searchType: ['all']
});
}
onSearch() {
if (this.searchForm.valid) {
const formValue = this.searchForm.value;
const params: CbmContactModel.ListParams = {};
// Aplicar filtros según el tipo de búsqueda
switch (formValue.searchType) {
case 'identification':
if (formValue.identification_number) {
params.identification_number = formValue.identification_number;
}
break;
case 'client':
if (formValue.client_id) {
params.client_id = formValue.client_id;
}
break;
case 'provider':
if (formValue.provider_id) {
params.provider_id = formValue.provider_id;
}
break;
default: // 'all'
if (formValue.identification_number) {
params.identification_number = formValue.identification_number;
}
if (formValue.client_id) {
params.client_id = formValue.client_id;
}
if (formValue.provider_id) {
params.provider_id = formValue.provider_id;
}
}
this.contactService.list(params).subscribe({
next: (response) => {
if (response.success) {
this.searchResults = response.data;
this.searchPerformed = true;
}
},
error: (error) => {
console.error('Error en búsqueda:', error);
this.searchResults = [];
this.searchPerformed = true;
}
});
}
}
clearSearch() {
this.searchForm.reset({
searchType: 'all'
});
this.searchResults = [];
this.searchPerformed = false;
}
selectContact(contact: CbmContactModel.ListResponse.Data) {
// Implementar selección del contacto
console.log('Contacto seleccionado:', contact);
}
}Ejemplo con Integración de Contactos
import { CbmContactService } from '@cbm-common/contact-repository';
@Component({
selector: 'app-client-contact-integration',
standalone: true,
template: `
<div class="client-contact-integration">
<h2>Contactos del Cliente</h2>
<div class="client-selector">
<label for="clientSelect">Seleccionar Cliente:</label>
<select id="clientSelect" [(ngModel)]="selectedClientId" (change)="loadClientContacts()">
<option value="">Seleccione un cliente</option>
<option *ngFor="let client of clients" [value]="client.id">
{{ client.name }} - {{ client.identification }}
</option>
</select>
</div>
<div class="contacts-section" *ngIf="selectedClientId">
<div class="section-header">
<h3>Contactos Asociados</h3>
<button (click)="addNewContact()">Agregar Contacto</button>
</div>
<div class="contacts-list" *ngIf="clientContacts.length > 0">
<div *ngFor="let contact of clientContacts" class="contact-item">
<div class="contact-info">
<div class="contact-primary">
<h4>{{ contact.full_name }}</h4>
<p class="identification">{{ contact.identification_number }}</p>
</div>
<div class="contact-details">
<p *ngIf="contact.email">📧 {{ contact.email }}</p>
<p *ngIf="contact.cellphone">📱 {{ contact.phone_code }} {{ contact.cellphone }}</p>
</div>
</div>
<div class="contact-actions">
<button (click)="editContact(contact)">Editar</button>
<button (click)="callContact(contact)">Llamar</button>
<button (click)="emailContact(contact)">Email</button>
</div>
</div>
</div>
<div class="no-contacts" *ngIf="clientContacts.length === 0 && !loading">
<p>Este cliente no tiene contactos registrados.</p>
<button (click)="addNewContact()">Agregar Primer Contacto</button>
</div>
<div class="loading" *ngIf="loading">
<p>Cargando contactos...</p>
</div>
</div>
</div>
`,
styles: [`
.client-contact-integration { padding: 20px; }
.client-selector { margin-bottom: 20px; }
.client-selector label { display: block; margin-bottom: 5px; font-weight: bold; }
.client-selector select { padding: 8px; border: 1px solid #ddd; border-radius: 4px; min-width: 200px; }
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.contacts-list { display: flex; flex-direction: column; gap: 15px; }
.contact-item { display: flex; justify-content: space-between; align-items: center; padding: 15px; border: 1px solid #ddd; border-radius: 8px; background: white; }
.contact-info { flex: 1; }
.contact-primary { margin-bottom: 10px; }
.contact-primary h4 { margin: 0 0 5px 0; }
.identification { color: #666; font-size: 14px; }
.contact-details p { margin: 3px 0; font-size: 14px; }
.contact-actions { display: flex; gap: 8px; }
.no-contacts { text-align: center; padding: 40px; color: #666; }
.loading { text-align: center; padding: 20px; }
button { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; }
button:hover { opacity: 0.8; }
`]
})
export class ClientContactIntegrationComponent implements OnInit {
clients: any[] = []; // Lista de clientes disponibles
selectedClientId = '';
clientContacts: CbmContactModel.ListResponse.Data[] = [];
loading = false;
constructor(private contactService: CbmContactService) {}
ngOnInit() {
this.loadClients();
}
loadClients() {
// Cargar lista de clientes disponibles
this.clients = [
{ id: '1', name: 'Cliente Ejemplo S.A.', identification: '1790012345001' },
{ id: '2', name: 'Empresa ABC Cía. Ltda.', identification: '1790023456001' }
];
}
loadClientContacts() {
if (!this.selectedClientId) {
this.clientContacts = [];
return;
}
this.loading = true;
const params: CbmContactModel.ListParams = {
client_id: this.selectedClientId
};
this.contactService.list(params).subscribe({
next: (response) => {
if (response.success) {
this.clientContacts = response.data;
}
this.loading = false;
},
error: (error) => {
console.error('Error al cargar contactos del cliente:', error);
this.clientContacts = [];
this.loading = false;
}
});
}
addNewContact() {
// Implementar modal o navegación para agregar nuevo contacto
console.log('Agregar nuevo contacto para cliente:', this.selectedClientId);
}
editContact(contact: CbmContactModel.ListResponse.Data) {
// Implementar edición del contacto
console.log('Editar contacto:', contact);
}
callContact(contact: CbmContactModel.ListResponse.Data) {
if (contact.cellphone) {
window.open(`tel:${contact.phone_code}${contact.cellphone}`);
}
}
emailContact(contact: CbmContactModel.ListResponse.Data) {
if (contact.email) {
window.open(`mailto:${contact.email}`);
}
}
}⚠️ Manejo de Errores
Errores de Consulta
// Manejo de errores en consultas de contactos
loadContactsWithErrorHandling(params: CbmContactModel.ListParams) {
this.contactService.list(params).subscribe({
next: (response) => {
if (response.success) {
this.contacts = response.data;
this.showSuccessMessage(`Se encontraron ${response.data.length} contactos`);
} else {
this.showErrorMessage('Error en la respuesta del servidor');
}
},
error: (error) => {
console.error('Error HTTP:', error);
if (error.status === 400) {
this.showErrorMessage('Parámetros de búsqueda inválidos. Verifique los filtros.');
} else if (error.status === 401) {
this.showErrorMessage('Sesión expirada. Inicie sesión nuevamente.');
} else if (error.status === 403) {
this.showErrorMessage('No tiene permisos para consultar contactos.');
} else if (error.status === 500) {
this.showErrorMessage('Error interno del servidor. Intente nuevamente.');
} else {
this.showErrorMessage('Error de conexión. Verifique su conexión a internet.');
}
}
});
}Validación de Filtros
// Servicio de validación para filtros de contactos
@Injectable({ providedIn: 'root' })
export class ContactFilterValidator {
validateFilters(filters: Partial<CbmContactModel.ListParams>): string[] {
const errors: string[] = [];
if (filters.identification_number) {
// Validar formato de identificación ecuatoriana
const idPattern = /^(\d{10}|\d{13})$/;
if (!idPattern.test(filters.identification_number)) {
errors.push('El número de identificación debe tener 10 o 13 dígitos');
}
}
if (filters.client_id && filters.provider_id) {
errors.push('No puede filtrar por cliente y proveedor simultáneamente');
}
return errors;
}
sanitizeFilters(filters: Partial<CbmContactModel.ListParams>): Partial<CbmContactModel.ListParams> {
const sanitized = { ...filters };
// Remover espacios en blanco
if (sanitized.identification_number) {
sanitized.identification_number = sanitized.identification_number.trim();
}
if (sanitized.client_id) {
sanitized.client_id = sanitized.client_id.trim();
}
if (sanitized.provider_id) {
sanitized.provider_id = sanitized.provider_id.trim();
}
return sanitized;
}
}
// Uso en componentes
export class ContactListComponent {
constructor(
private contactService: CbmContactService,
private filterValidator: ContactFilterValidator
) {}
applyFilters() {
const errors = this.filterValidator.validateFilters(this.filters);
if (errors.length > 0) {
this.showValidationErrors(errors);
return;
}
const sanitizedFilters = this.filterValidator.sanitizeFilters(this.filters);
this.loadContactsWithErrorHandling(sanitizedFilters);
}
}🔧 Configuración Avanzada
Configuración de Headers para Consultas Seguras
// Configuración de headers para consultas de contactos con autenticación
import { HTTP_INTERCEPTORS } from '@angular/common/http';
@NgModule({
providers: [
CbmContactModule.forRoot({
baseUrl: 'https://api.cbm.com/contacts'
}),
{
provide: HTTP_INTERCEPTORS,
useClass: ContactAuthInterceptor,
multi: true
}
]
})
export class AppModule {}Configuración de Timeouts para Consultas
// Timeouts específicos para consultas de contactos
loadContactsWithTimeout(params: CbmContactModel.ListParams, timeoutMs = 10000) {
return this.contactService.list(params).pipe(
timeout(timeoutMs),
catchError(error => {
if (error.name === 'TimeoutError') {
return throwError(() => new Error('La consulta de contactos tomó demasiado tiempo'));
}
return throwError(() => error);
})
);
}Sistema de Reintentos para Consultas
// Reintentos automáticos para consultas fallidas
loadContactsWithRetry(params: CbmContactModel.ListParams, maxRetries = 3) {
return this.contactService.list(params).pipe(
retry({
count: maxRetries,
delay: (error, retryCount) => {
console.log(`Reintento ${retryCount} para consulta de contactos`);
return timer(retryCount * 1000);
}
}),
catchError(error => {
console.error(`Falló después de ${maxRetries} intentos:`, error);
return throwError(() => error);
})
);
}📋 Dependencias
Peer Dependencies (Requeridas)
{
"@angular/common": ">=20.1.5",
"@angular/core": ">=20.1.5"
}Dependencias Internas
{
"tslib": "^2.3.0"
}🛠️ Desarrollo
Estructura del Proyecto
contact-repository/
├── src/
│ ├── lib/
│ │ ├── contact.model.ts # Modelos de datos para contactos
│ │ ├── contact.module.ts # Configuración del módulo
│ │ ├── contact.service.ts # Servicio HTTP para contactos
│ │ ├── contact.repository.ts # Interfaz del repositorio
│ │ └── index.ts # Exportaciones públicas
│ └── public-api.ts # API pública
├── ng-package.json # Configuración empaquetado
├── package.json # Dependencias
└── README.md # Esta documentaciónConstrucción
# Construir la librería
ng build contact-repository
# Construir en modo watch
ng build contact-repository --watch
# Construir para producción
ng build contact-repository --configuration productionPruebas
# Ejecutar pruebas unitarias
ng test contact-repository
# Ejecutar pruebas con coverage
ng test contact-repository --code-coverage
# Pruebas end-to-end
ng e2e contact-repository🎯 Mejores Prácticas
1. Gestión de Memoria y Recursos
// Limpiar suscripciones para evitar memory leaks
export class ContactListComponent implements OnDestroy {
private destroy$ = new Subject<void>();
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
loadContacts() {
this.contactService.list(params).pipe(
takeUntil(this.destroy$)
).subscribe({
next: (response) => {
if (response.success) {
this.contacts = response.data;
}
}
});
}
}2. Optimización de Rendimiento con Caché
// Implementar caché para contactos frecuentemente consultados
@Injectable({ providedIn: 'root' })
export class ContactCacheService {
private cache = new Map<string, { data: any; timestamp: number }>();
private readonly CACHE_DURATION = 5 * 60 * 1000; // 5 minutos
get(key: string): any | null {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION) {
return cached.data;
}
this.cache.delete(key);
return null;
}
set(key: string, data: any) {
this.cache.set(key, { data, timestamp: Date.now() });
}
clear() {
this.cache.clear();
}
}
// Uso en componentes
export class ContactListComponent {
constructor(
private contactService: CbmContactService,
private cacheService: ContactCacheService
) {}
loadContacts() {
const cacheKey = JSON.stringify(this.filters);
const cachedData = this.cacheService.get(cacheKey);
if (cachedData) {
this.contacts = cachedData;
return;
}
this.contactService.list(this.filters).subscribe({
next: (response) => {
if (response.success) {
this.contacts = response.data;
this.cacheService.set(cacheKey, response.data);
}
}
});
}
}3. Validación Completa de Contactos
// Servicio completo de validación de contactos
@Injectable({ providedIn: 'root' })
export class ContactValidationService {
// Patrones de validación ecuatorianos
private readonly ecuadorianIdPattern = /^(\d{10}|\d{13})$/;
private readonly ecuadorianPhonePattern = /^(\d{7}|\d{8}|\d{9}|\d{10})$/;
private readonly emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
validateContactData(contact: Partial<CbmContactModel.ListResponse.Data>): ValidationResult {
const result: ValidationResult = { isValid: true, errors: [] };
// Validación de identificación ecuatoriana
if (contact.identification_number) {
if (!this.ecuadorianIdPattern.test(contact.identification_number)) {
result.errors.push('Número de identificación inválido (debe tener 10 o 13 dígitos)');
} else {
// Validar dígito verificador para cédulas
if (contact.identification_number.length === 10) {
if (!this.validateEcuadorianId(contact.identification_number)) {
result.errors.push('Número de cédula inválido');
}
}
}
}
// Validación de email
if (contact.email && !this.emailPattern.test(contact.email)) {
result.errors.push('Formato de email inválido');
}
// Validación de teléfono ecuatoriano
if (contact.cellphone) {
if (!this.ecuadorianPhonePattern.test(contact.cellphone)) {
result.errors.push('Número de teléfono inválido');
}
}
// Validación de nombre
if (contact.full_name && contact.full_name.trim().length < 2) {
result.errors.push('El nombre debe tener al menos 2 caracteres');
}
result.isValid = result.errors.length === 0;
return result;
}
private validateEcuadorianId(id: string): boolean {
// Algoritmo de validación de cédula ecuatoriana
const coefficients = [2, 1, 2, 1, 2, 1, 2, 1, 2];
const digits = id.split('').map(Number);
let sum = 0;
for (let i = 0; i < 9; i++) {
let product = digits[i] * coefficients[i];
if (product >= 10) {
product -= 9;
}
sum += product;
}
const checkDigit = (10 - (sum % 10)) % 10;
return checkDigit === digits[9];
}
formatEcuadorianPhone(phone: string, code?: string): string {
// Formatear teléfono ecuatoriano
const cleanPhone = phone.replace(/\D/g, '');
if (cleanPhone.length === 9 && cleanPhone.startsWith('0')) {
return `${code || '593'} ${cleanPhone.slice(0, 2)} ${cleanPhone.slice(2, 5)} ${cleanPhone.slice(5)}`;
}
return phone;
}
}
// Uso en formularios
export class ContactFormComponent {
constructor(private validationService: ContactValidationService) {}
validateContact() {
const validation = this.validationService.validateContactData(this.contactForm.value);
if (!validation.isValid) {
this.showValidationErrors(validation.errors);
}
}
}4. Logging y Auditoría para Operaciones de Contactos
// Servicio de auditoría para operaciones de contactos
@Injectable({ providedIn: 'root' })
export class ContactAuditService {
logContactOperation(operation: string, contactData: any, userId: string) {
const auditEntry = {
timestamp: new Date().toISOString(),
operation,
contactId: contactData._id || 'NEW',
contactName: contactData.full_name,
identification: contactData.identification_number,
clientId: contactData.client_id,
providerId: contactData.provider_id,
userId,
email: contactData.email,
phone: contactData.cellphone
};
console.log(`[CONTACT_AUDIT] ${JSON.stringify(auditEntry)}`);
// Enviar a servicio de auditoría
this.auditService.record('contact_operation', auditEntry);
}
logContactSearch(searchParams: CbmContactModel.ListParams, resultsCount: number, userId: string) {
const searchEntry = {
timestamp: new Date().toISOString(),
operation: 'CONTACT_SEARCH',
searchParams,
resultsCount,
userId,
severity: resultsCount === 0 ? 'WARNING' : 'INFO'
};
console.log(`[CONTACT_SEARCH] ${JSON.stringify(searchEntry)}`);
// Enviar a servicio de monitoreo de búsquedas
this.searchAnalytics.record('contact_search', searchEntry);
}
logContactAccess(contactId: string, accessType: 'view' | 'edit' | 'call' | 'email', userId: string) {
const accessEntry = {
timestamp: new Date().toISOString(),
operation: 'CONTACT_ACCESS',
contactId,
accessType,
userId
};
console.log(`[CONTACT_ACCESS] ${JSON.stringify(accessEntry)}`);
// Enviar a servicio de análisis de uso
this.usageAnalytics.record('contact_access', accessEntry);
}
}🤝 Contribución
- Fork el repositorio
- Crea una rama para tu feature (
git checkout -b feature/nueva-funcionalidad) - Commit tus cambios (
git commit -am 'Agrega nueva funcionalidad') - Push a la rama (
git push origin feature/nueva-funcionalidad) - Abre un PullRequest
📄 Licencia
Este proyecto está bajo la Licencia MIT - ver el archivo LICENSE para más detalles.
📞 Soporte
Para soporte técnico o reportes de bugs, contacta al equipo de desarrollo de CBM:
- 📧 Email: [email protected]
- 💬 Slack: #desarrollo-cbm
- 📋 Issues: GitHub Issues
🔄 Changelog
v0.0.1
- ✅ Módulo de configuración con forRoot pattern
- ✅ Servicio que implementa interfaz del repositorio
- ✅ Operaciones de listado de contactos con filtros
- ✅ Modelos TypeScript bien tipados con datos de contactos
- ✅ Integración completa con HttpClient
- ✅ Documentación completa en español
Nota: Esta librería está optimizada para sistemas empresariales que requieren gestión de contactos asociados a clientes y proveedores, con soporte para identificación ecuatoriana, información de comunicación completa y auditoría de operaciones. Incluye validaciones específicas para el contexto ecuatoriano y formato de datos locales.
