+page.svelte 11.6 KB
<script>
  import { supabase } from '$lib/supabase';
  import { onMount } from 'svelte';

  let loading = $state(true);
  let searchQuery = $state('');
  let areas = $state([]);
  let allEntidades = $state([]);
  let selectedArea = $state(null);
  let highlightedEntidad = $state(null);

  onMount(async () => {
    const { data, error } = await supabase
      .schema('ppto')
      .from('clas_institucional')
      .select('*')
      .order('desc_area')
      .order('desc_entidad');

    if (!error && data) {
      allEntidades = data;

      // Agrupar por área (segundo nivel)
      const areasMap = {};

      data.forEach(item => {
        const areaKey = item.area;

        if (!areasMap[areaKey]) {
          areasMap[areaKey] = {
            codigo: item.area,
            nombre: item.desc_area,
            sector: item.desc_sector,
            entidades: []
          };
        }

        areasMap[areaKey].entidades.push({
          codigo: item.entidad,
          nombre: item.desc_entidad,
          sigla: item.sigla_entidad,
          años: item.n_gestiones,
          gestiones: item.gestiones,
          subarea: item.desc_subarea
        });
      });

      areas = Object.values(areasMap).sort((a, b) => a.nombre.localeCompare(b.nombre));

      if (areas.length > 0) {
        selectedArea = areas[0];
      }
    }

    loading = false;
  });

  // Agrupar entidades por subárea dentro del área seleccionada
  function getSubareasForArea(areaEntidades) {
    if (!areaEntidades) return [];

    const subareasMap = {};

    areaEntidades.forEach(ent => {
      const subareaKey = ent.subarea || 'Sin subárea';

      if (!subareasMap[subareaKey]) {
        subareasMap[subareaKey] = {
          nombre: subareaKey,
          entidades: []
        };
      }

      subareasMap[subareaKey].entidades.push(ent);
    });

    return Object.values(subareasMap).sort((a, b) => a.nombre.localeCompare(b.nombre));
  }

  // Búsqueda global
  function searchGlobal(query) {
    if (!query || query.length < 2) return [];
    const q = query.toLowerCase();

    return allEntidades
      .filter(item =>
        item.entidad?.toString().includes(q) ||
        item.desc_entidad?.toLowerCase().includes(q) ||
        item.sigla_entidad?.toLowerCase().includes(q)
      )
      .slice(0, 20);
  }

  function selectArea(area) {
    selectedArea = area;
    searchQuery = '';
    highlightedEntidad = null;
  }

  function goToSearchResult(item) {
    const targetArea = areas.find(a => a.codigo === item.area);

    if (targetArea) {
      selectedArea = targetArea;
      highlightedEntidad = item.entidad;
      searchQuery = '';

      setTimeout(() => {
        const element = document.getElementById(`ent-${item.entidad}`);
        if (element) {
          element.scrollIntoView({ behavior: 'smooth', block: 'center' });
        }
      }, 100);
    }
  }

  // Convertir lista de años a rangos legibles
  function formatGestiones(gestiones) {
    if (!gestiones) return '';

    const years = gestiones.split(',').map(y => parseInt(y.trim())).sort((a, b) => a - b);
    if (years.length === 0) return '';
    if (years.length === 1) return years[0].toString();

    const ranges = [];
    let start = years[0];
    let end = years[0];

    for (let i = 1; i < years.length; i++) {
      if (years[i] === end + 1) {
        end = years[i];
      } else {
        ranges.push(start === end ? `${start}` : `${start}-${end}`);
        start = years[i];
        end = years[i];
      }
    }
    ranges.push(start === end ? `${start}` : `${start}-${end}`);

    return ranges.join(', ');
  }

  let areaSubareas = $derived(selectedArea ? getSubareasForArea(selectedArea.entidades) : []);
  let searchResults = $derived(searchGlobal(searchQuery));
  let isSearching = $derived(searchQuery.length >= 2);
  let totalEntidades = $derived(allEntidades.length);
</script>

<svelte:head>
  <title>Institucional | Presupuesto Público</title>
</svelte:head>

<div class="min-h-screen bg-white">
  <!-- Header -->
  <header class="border-b bg-slate-50">
    <div class="max-w-screen-2xl mx-auto px-6 py-4">
      <nav class="text-sm text-slate-500 mb-2">
        <a href="/" class="hover:text-slate-700">Inicio</a>
        <span class="mx-2">/</span>
        <a href="/clasificadores" class="hover:text-slate-700">Clasificadores</a>
        <span class="mx-2">/</span>
        <span class="text-slate-900">Institucional</span>
      </nav>
      <div class="flex items-center justify-between">
        <div>
          <h1 class="text-2xl font-light text-slate-900">Clasificador Institucional</h1>
          <p class="text-sm text-slate-500 mt-1">{totalEntidades || '...'} entidades públicas</p>
        </div>
      </div>
    </div>
  </header>

  {#if loading}
    <div class="flex items-center justify-center py-20">
      <p class="text-slate-500">Cargando clasificador...</p>
    </div>
  {:else}
    <div class="max-w-screen-2xl mx-auto flex">
      <!-- Sidebar izquierda: Áreas -->
      <aside class="w-64 flex-shrink-0 border-r bg-slate-50/50">
        <div class="sticky top-0 h-screen overflow-y-auto p-4">
          <!-- Buscador -->
          <div class="mb-6">
            <input
              type="text"
              bind:value={searchQuery}
              placeholder="Buscar entidad..."
              class="w-full px-3 py-2 text-sm border border-slate-200 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
            />
          </div>

          <!-- Resultados de búsqueda -->
          {#if isSearching}
            <div class="mb-4">
              <p class="text-xs text-slate-500 uppercase tracking-wide mb-2">
                {searchResults.length} resultados
              </p>
              <div class="space-y-1">
                {#each searchResults as result}
                  <button
                    class="w-full text-left px-2 py-2 text-sm rounded hover:bg-white hover:shadow-sm transition-all"
                    onclick={() => goToSearchResult(result)}
                  >
                    <span class="text-xs text-slate-400 block">{result.desc_area}</span>
                    <span class="text-slate-700">{result.desc_entidad}</span>
                    {#if result.sigla_entidad}
                      <span class="text-slate-400 text-xs ml-1">({result.sigla_entidad})</span>
                    {/if}
                  </button>
                {/each}
                {#if searchResults.length === 0}
                  <p class="text-sm text-slate-400 px-2">Sin resultados</p>
                {/if}
              </div>
            </div>
          {:else}
            <!-- Lista de áreas -->
            <nav>
              <p class="text-xs text-slate-500 uppercase tracking-wide mb-3 px-2">Áreas</p>
              <ul class="space-y-1">
                {#each areas as area}
                  <li>
                    <button
                      class="w-full text-left px-3 py-2 rounded-md text-sm transition-all
                        {selectedArea?.codigo === area.codigo
                          ? 'bg-blue-50 text-blue-900 font-medium border-l-2 border-blue-500'
                          : 'text-slate-600 hover:bg-white hover:text-slate-900'}"
                      onclick={() => selectArea(area)}
                    >
                      <span class="flex justify-between items-center">
                        <span class="truncate">{area.nombre}</span>
                        <span class="text-xs text-slate-400 ml-2 flex-shrink-0">{area.entidades.length}</span>
                      </span>
                    </button>
                  </li>
                {/each}
              </ul>
            </nav>
          {/if}
        </div>
      </aside>

      <!-- Contenido principal -->
      <main class="flex-1 min-w-0">
        <div class="px-8 py-6">
          {#if selectedArea}
            <!-- Título del área -->
            <div class="mb-8 pb-6 border-b">
              <h2 class="text-xl font-medium text-slate-900 mb-1">{selectedArea.nombre}</h2>
              <p class="text-sm text-slate-500">{selectedArea.entidades.length} entidades</p>
            </div>

            <!-- Subáreas y entidades -->
            <div class="space-y-8">
              {#each areaSubareas as subarea}
                <section id="subarea-{subarea.nombre.replace(/\s+/g, '-')}" class="scroll-mt-4">
                  {#if areaSubareas.length > 1 || subarea.nombre !== selectedArea.nombre}
                    <div class="flex items-start gap-4 mb-4">
                      <div class="flex-1">
                        <h3 class="text-lg font-medium text-slate-900">{subarea.nombre}</h3>
                        <p class="text-sm text-slate-500">{subarea.entidades.length} entidades</p>
                      </div>
                    </div>
                  {/if}

                  <!-- Entidades -->
                  <div class="border rounded-lg divide-y">
                    {#each subarea.entidades as entidad}
                      <div
                        id="ent-{entidad.codigo}"
                        class="scroll-mt-4 {highlightedEntidad === entidad.codigo ? 'ring-2 ring-blue-200' : ''}"
                      >
                        <a
                          href="/entidad/{entidad.codigo}"
                          class="flex items-center justify-between px-4 py-3 hover:bg-slate-50 transition-colors"
                        >
                          <div class="flex-1 min-w-0">
                            <div class="flex items-center gap-2">
                              <span class="text-slate-900">{entidad.nombre}</span>
                              {#if entidad.sigla}
                                <span class="text-sm text-slate-400">({entidad.sigla})</span>
                              {/if}
                            </div>
                            <p class="text-xs text-slate-400 mt-0.5">
                              Código: {entidad.codigo} · {formatGestiones(entidad.gestiones)}
                            </p>
                          </div>
                          <svg class="w-4 h-4 text-slate-300 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
                          </svg>
                        </a>
                      </div>
                    {/each}
                  </div>
                </section>
              {/each}
            </div>
          {/if}
        </div>
      </main>

      <!-- Sidebar derecha: En esta página -->
      <aside class="w-56 flex-shrink-0 hidden xl:block">
        <div class="sticky top-0 h-screen overflow-y-auto p-4 border-l">
          <p class="text-xs text-slate-500 uppercase tracking-wide mb-3">En esta página</p>
          {#if selectedArea && !isSearching}
            <nav class="space-y-2">
              {#each areaSubareas as subarea}
                {#if areaSubareas.length > 1 || subarea.nombre !== selectedArea.nombre}
                  <a
                    href="#subarea-{subarea.nombre.replace(/\s+/g, '-')}"
                    class="block text-sm text-slate-600 hover:text-blue-600 truncate"
                    title={subarea.nombre}
                  >
                    {subarea.nombre}
                    <span class="text-xs text-slate-400 ml-1">({subarea.entidades.length})</span>
                  </a>
                {/if}
              {/each}
              {#if areaSubareas.length === 1 && areaSubareas[0].nombre === selectedArea.nombre}
                <p class="text-sm text-slate-400">{selectedArea.entidades.length} entidades</p>
              {/if}
            </nav>
          {/if}
        </div>
      </aside>
    </div>
  {/if}
</div>