Skip to main content

Overview

The photo management interface allows administrators to view, search, and synchronize student/staff photos between the SIAD database and HikCentral biometric system.

Photo List Interface

The main photo management view (estudiantes_foto.vue) displays a paginated table with:
  • Student photo from SIAD (local database)
  • Photo from HikCentral (biometric system)
  • Registration status in HikCentral
  • Career/Department filter
  • Real-time search by CI (Cédula) or name

Component Structure

<template>
  <div class="flex flex-col gap-4">
    <!-- Search and Filter Section -->
    <form class="flex-grow">
      <input 
        type="text" 
        placeholder="Ingresa la cédula o nombre a buscar..." 
        v-model="searchQuery"
        @input="debouncedFilter"
      />
    </form>
    
    <!-- Career Filter -->
    <select v-model="selectedCarrera" @change="debouncedFilter">
      <option value="Todos">Todas las Carreras</option>
      <option v-for="carrera in carrerasList" :value="carrera.id">
        {{ carrera.nombre }}
      </option>
    </select>
    
    <!-- Photo Table -->
    <table>
      <!-- Student rows with photos -->
    </table>
  </div>
</template>

Search and Filter Workflow

1

Real-Time Search

The search input uses a debounced filter (900ms delay) to avoid excessive API calls:
created() {
  this.debouncedFilter = debounce(() => {
    this.filterAndFetch();
  }, 900);
}
When users type in the search box, the system waits 900ms before sending a request to the backend.
2

Apply Filters

The filterAndFetch() method resets to page 1 and fetches filtered results:
filterAndFetch() {
  this.currentPage = 1;
  this.getAdministrativosD(this.currentPage, this.searchQuery, this.selectedCarrera);
}
3

Backend API Request

The API endpoint receives search and filter parameters:
async getAdministrativosD(page = 1, searchQuery = "", carreraName = "Todos") {
  const params = {
    page: page,
    search_query: searchQuery,
    carrera_name: carreraName === 'Todos' ? '' : carreraName,
  };

  const response = await API.get(`${this.baseUrl}/estudiantesfoto`, { params });
  this.filteredpostulaciones = response.data?.data || [];
}
4

Display Results

Results are displayed in a table with photos from both sources:
<tr v-for="post in filteredpostulaciones" :key="post.CIInfPer">
  <td>
    <img :src="getPhotoUrl(post.CIInfPer)" /> <!-- SIAD Photo -->
  </td>
  <td>
    <img :src="getPhotoUrl2(post.CIInfPer)" /> <!-- HikCentral Photo -->
  </td>
</tr>

Photo URL Generation

Photos are loaded dynamically from the backend:
getPhotoUrl(ci) {
  const baseURL2 = API.defaults.baseURL;
  return `${baseURL2}/biometrico/fotografia/${ci}?v=${this.refreshKey}`;
}

getPhotoUrl2(ci) {
  const baseURL2 = API.defaults.baseURL;
  return `${baseURL2}/biometrico/gethick/${ci}?v=${this.refreshKey}`;
}
The refreshKey is a timestamp that forces the browser to reload images when they change.

HikCentral Registration Status

The system checks if each student is registered in HikCentral:
async verificarRegistrosMasivos() {
  const items = this.filteredpostulaciones;

  for (let post of items) {
    try {
      const res = await API.get(`${this.baseUrl}/getperson-est/${post.CIInfPer}`);
      post.estaRegistradoHC = res.data.registrado;
    } catch (e) {
      post.estaRegistradoHC = false;
    }
    
    // 50ms delay to avoid overloading the server
    await new Promise(resolve => setTimeout(resolve, 50));
  }
}

Status Display

<span v-if="post.estaRegistradoHC === null">
  <svg class="animate-spin">...</svg> Verificando...
</span>

<span v-else-if="post.estaRegistradoHC === true" 
      class="bg-green-100 text-green-800">
  ✓ Sí
</span>

<span v-else class="bg-red-100 text-red-800">
  ✗ No
</span>

Individual Photo Sync

1

Open Photo Details Modal

Click on a student row to open the edit modal:
abrirModalEdicion(user) {
  this.objetoeditar = {
    CIInfPer: user.CIInfPer,
    nombre_us: user.NombInfPer + ' ' + user.ApellMatInfPer + ' ' + user.ApellInfPer,
    mailInst: user.mailInst,
  };
  this.$.setupState.isEditModalOpen = true;
  this.verificarRegistroHC(user.CIInfPer);
}
2

Compare Photos (Optional)

Compare the SIAD photo with the HikCentral photo using facial recognition:
async ejecutarComparacion() {
  this.comparando = true;
  const ci = this.objetoeditar.CIInfPer;
  const { data } = await API.get(`${this.baseUrl}/compare-hikdoc-est/${ci}`);

  if (data.identicas) {
    alert(`✅ Match: ${data.similitud} de similitud.`);
  } else {
    alert(`❌ Diferentes: Solo ${data.similitud} de parecido.`);
  }
}
3

Sync to HikCentral

Register or update the photo in HikCentral:
async registrarEnHikCentral(post) {
  if (!confirm(`¿Deseas registrar a ${post} en HikCentral?`)) return;

  const response = await API.post(`${this.baseUrl}/sync-hikdoc/${post}`);

  if (response.data.code === "0" || response.data.msg === "Success") {
    alert(`✅ Registrado con éxito. ID en HC: ${response.data.data}`);
    post.estaRegistradoHC = true;
  }
}

Pagination

Navigate through pages of results:
nextPage() {
  if (this.currentPage < this.lastPage && !this.cargando) {
    this.getAdministrativosD(this.currentPage + 1, this.searchQuery, this.selectedCarrera);
  }
}

previousPage() {
  if (this.currentPage > 1 && !this.cargando) {
    this.getAdministrativosD(this.currentPage - 1, this.searchQuery, this.selectedCarrera);
  }
}
<button @click="previousPage" :disabled="currentPage === 1">
  <i class="fas fa-angle-left"></i>
</button>
<span>Página {{ currentPage }} de {{ lastPage }}</span>
<button @click="nextPage" :disabled="currentPage === lastPage">
  <i class="fas fa-angle-right"></i>
</button>

Image Error Handling

If a photo fails to load, display a default avatar:
handleImageError(event) {
  event.target.src = "https://upload.wikimedia.org/wikipedia/commons/thumb/1/12/User_icon_2.svg/480px-User_icon_2.svg.png";
}
<img :src="getPhotoUrl(post.CIInfPer)" @error="handleImageError" />
Use the v query parameter with a timestamp to bypass browser cache when photos are updated.

Build docs developers (and LLMs) love