+page.svelte 11.1 KB
<script>
  let { data } = $props();

  let searchQuery = $state('');
  let allMunicipios = data.clasificador.filter(d => d.nivel === 'municipio' && d.municipio_ubigeo);
  let highlightedMuni = $state(null);
  let sidebarOpen = $state(false);

  function titleCase(str) {
    if (!str) return '';
    return str.toLowerCase().replace(/\b\w/g, c => c.toUpperCase());
  }

  // Agrupar por departamento (calculado una vez)
  const deptoMap = {};
  allMunicipios.forEach(m => {
    const dept = m.desc_departamento;
    if (!deptoMap[dept]) {
      deptoMap[dept] = { nombre: dept, municipios: [] };
    }
    deptoMap[dept].municipios.push(m);
  });
  let departamentos = Object.values(deptoMap).sort((a, b) => a.nombre.localeCompare(b.nombre));
  let selectedDepto = $state(departamentos[0] || null);


  function searchGlobal(query) {
    if (!query || query.length < 2) return [];
    const q = query.toLowerCase();
    return allMunicipios
      .filter(m =>
        m.desc_ubigeo?.toLowerCase().includes(q) ||
        m.desc_departamento?.toLowerCase().includes(q) ||
        m.municipio_ubigeo?.toString().includes(q)
      )
      .slice(0, 20);
  }

  function selectDepto(depto) {
    selectedDepto = depto;
    searchQuery = '';
    highlightedMuni = null;
    sidebarOpen = false;
  }

  function goToSearchResult(item) {
    const targetDepto = departamentos.find(d => d.nombre === item.desc_departamento);
    if (targetDepto) {
      selectedDepto = targetDepto;
      highlightedMuni = item.municipio_ubigeo;
      searchQuery = '';
      sidebarOpen = false;
      setTimeout(() => {
        const el = document.getElementById(`muni-${item.municipio_ubigeo}`);
        el?.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }, 100);
    }
  }

  function formatGestiones(gestiones) {
    if (!gestiones) return '';
    const years = gestiones.split(',').map(y => parseInt(y.trim())).sort((a, b) => a - b);
    if (years.length <= 1) return years[0]?.toString() || '';
    const ranges = [];
    let start = years[0], 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 searchResults = $derived(searchGlobal(searchQuery));
  let isSearching = $derived(searchQuery.length >= 2);
  let totalMunicipios = $derived(allMunicipios.length);
  let totalDeptos = $derived(departamentos.length);
</script>

<svelte:head>
  <title>Geográfico | Presupuesto Público</title>
</svelte:head>

<div class="min-h-screen" style="font-family: var(--font-sans); background-color: var(--theme-body); color: var(--theme-titulo);">
  <!-- Header -->
  <header class="border-b" style="border-color: var(--theme-borde);">
    <div class="max-w-screen-xl mx-auto px-4 sm:px-6 py-6">
      <nav class="mb-4" style="font-size: 0.75rem; color: var(--theme-texto);">
        <div class="flex items-center gap-2">
          <a href="/" class="hover:opacity-80">Inicio</a>
          <span style="opacity: 0.5;">/</span>
          <a href="/clasificadores" class="hover:opacity-80">Clasificadores</a>
        </div>
      </nav>

      <div class="max-w-3xl">
        <p class="text-xs uppercase tracking-widest mb-2" style="color: var(--theme-texto);">Clasificador geográfico</p>
        <h1 class="text-3xl mb-3" style="font-weight: 700;">¿Dónde se gasta?</h1>
        <p class="leading-relaxed mb-3" style="color: var(--theme-texto);">
          Este clasificador organiza el gasto público por <strong style="color: var(--theme-titulo);">ubicación geográfica</strong>:
          departamentos y municipios donde se ejecuta el presupuesto, independientemente de la institución que lo ejecuta.
        </p>

        {#if totalMunicipios > 0}
          <p class="text-sm mb-5" style="color: var(--theme-texto);">
            <span style="color: var(--theme-titulo); font-weight: 500;">{totalMunicipios}</span> municipios en
            <span style="color: var(--theme-titulo); font-weight: 500;">{totalDeptos}</span> departamentos
          </p>
        {/if}

        <div class="flex flex-wrap items-center gap-2 text-xs mb-6">
          <span class="px-2.5 py-1 rounded-full font-medium" style="background-color: var(--theme-accent); color: var(--theme-body);">
            Departamento
          </span>
          <span style="color: var(--theme-texto);">→</span>
          <span class="px-2.5 py-1 rounded-full" style="border: 1px solid var(--theme-borde); color: var(--theme-titulo);">
            Municipio
          </span>
        </div>
      </div>
    </div>
  </header>

    <!-- Botón móvil sidebar -->
    <div class="lg:hidden px-4 py-3 border-b" style="border-color: var(--theme-borde); background-color: var(--theme-fill);">
      <button
        onclick={() => sidebarOpen = !sidebarOpen}
        class="flex items-center gap-2 text-sm"
        style="color: var(--theme-texto);"
      >
        <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
        </svg>
        <span class="font-medium">{selectedDepto ? titleCase(selectedDepto.nombre) : 'Departamentos'}</span>
        <svg class="w-4 h-4 ml-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
        </svg>
      </button>
    </div>

    <div class="clasificador-layout max-w-screen-xl mx-auto flex px-4" style="display: flex !important; flex-direction: row;">
      <!-- Sidebar: Departamentos -->
      {#if sidebarOpen}
        <div class="fixed inset-0 bg-black/30 z-40 lg:hidden" onclick={() => sidebarOpen = false}></div>
      {/if}
      <aside class="
        {sidebarOpen ? 'translate-x-0' : '-translate-x-full'}
        lg:translate-x-0
        fixed lg:relative
        inset-y-0 left-0
        w-72 lg:w-64
        z-50 lg:z-auto
        transition-transform duration-200 ease-in-out
        lg:flex-shrink-0
        shadow-xl lg:shadow-none
        sidebar-left
      ">
        <div class="h-full lg:h-screen lg:sticky lg:top-0 overflow-y-auto py-6 px-4 lg:px-0 lg:pr-6" style="background-color: var(--theme-body);">
          <!-- Cerrar en móvil -->
          <div class="flex justify-between items-center mb-4 lg:hidden">
            <span class="text-sm font-medium" style="color: var(--theme-titulo);">Departamentos</span>
            <button onclick={() => sidebarOpen = false} style="color: var(--theme-texto);">
              <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
              </svg>
            </button>
          </div>

          <!-- Buscador -->
          <div class="mb-6">
            <input
              type="text"
              bind:value={searchQuery}
              placeholder="Buscar municipio..."
              class="w-full px-3 py-2 text-sm rounded-md focus:outline-none focus:ring-2 focus:ring-offset-0"
              style="border: 1px solid var(--theme-borde); background-color: var(--theme-surface); color: var(--theme-titulo);"
            />
          </div>

          <!-- Resultados de búsqueda -->
          {#if isSearching}
            <div class="mb-4">
              <p class="text-xs uppercase tracking-wide mb-2" style="color: var(--theme-texto);">
                {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 transition-all"
                    style="color: var(--theme-titulo);"
                    onclick={() => goToSearchResult(result)}
                  >
                    <span class="text-xs block" style="color: var(--theme-texto);">{titleCase(result.desc_departamento)}</span>
                    <span>{titleCase(result.desc_ubigeo)}</span>
                  </button>
                {/each}
                {#if searchResults.length === 0}
                  <p class="text-sm py-4 text-center" style="color: var(--theme-texto);">Sin resultados</p>
                {/if}
              </div>
            </div>
          {:else}
            <!-- Lista de departamentos -->
            <div class="space-y-0.5">
              {#each departamentos as depto}
                <button
                  class="w-full text-left px-3 py-2.5 text-sm rounded-lg transition-all flex items-center justify-between"
                  style="{selectedDepto?.nombre === depto.nombre
                    ? `background-color: var(--theme-accent); color: var(--theme-body);`
                    : `color: var(--theme-titulo);`}"
                  onclick={() => selectDepto(depto)}
                >
                  <span class="font-medium">{titleCase(depto.nombre)}</span>
                  <span class="text-xs" style="opacity: 0.6;">{depto.municipios.length}</span>
                </button>
              {/each}
            </div>
          {/if}
        </div>
      </aside>

      <!-- Contenido: Municipios -->
      <main class="flex-1 min-w-0 py-6 lg:pl-6 lg:border-l" style="border-color: var(--theme-borde);">
        {#if selectedDepto}
          <div class="mb-6">
            <h2 class="text-xl font-bold mb-1" style="color: var(--theme-titulo);">{titleCase(selectedDepto.nombre)}</h2>
            <p class="text-sm" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);">
              {selectedDepto.municipios.length} municipios
            </p>
          </div>

          <div class="space-y-2">
            {#each [...selectedDepto.municipios].sort((a, b) => a.desc_ubigeo.localeCompare(b.desc_ubigeo)) as muni}
              <a
                href="/ubicacion/{muni.municipio_ubigeo}"
                id="muni-{muni.municipio_ubigeo}"
                class="block p-4 rounded-xl border transition-all hover:shadow-sm"
                style="border-color: {highlightedMuni === muni.municipio_ubigeo ? 'var(--theme-accent)' : 'var(--theme-borde)'}; background-color: var(--theme-surface);"
              >
                <div class="flex items-center justify-between">
                  <div>
                    <span class="font-medium" style="color: var(--theme-titulo);">{titleCase(muni.desc_ubigeo)}</span>
                    <span class="text-xs ml-2" style="color: var(--theme-texto); opacity: 0.5;">{muni.municipio_ubigeo}</span>
                  </div>
                  <div class="text-xs" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);">
                    {muni.n_gestiones} años
                  </div>
                </div>
                <div class="text-xs mt-1" style="font-family: 'DM Mono', monospace; color: var(--theme-texto); opacity: 0.6;">
                  {formatGestiones(muni.gestiones)}
                </div>
              </a>
            {/each}
          </div>
        {/if}
      </main>
    </div>
</div>