npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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ón

Construcció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 production

Pruebas

# 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

  1. Fork el repositorio
  2. Crea una rama para tu feature (git checkout -b feature/nueva-funcionalidad)
  3. Commit tus cambios (git commit -am 'Agrega nueva funcionalidad')
  4. Push a la rama (git push origin feature/nueva-funcionalidad)
  5. 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:

🔄 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.