Edifiko
Edifiko DocsDocumentación del proyecto
  • Overview
Proyecto
  • README
  • CONTRIBUTING
  • WORKFLOW
  • Workflows CI/CD
  • UI Components
  • Showcase: Works UI Components
  • Schemas
  • Servicios
  • Hooks
  • Utils
  • Types
  • Constants
  • Errors
  • API Materiales
Herramientas de IA
  • Agentes
  • Skills
  1. Documentación
  2. Hooks

Hooks

hooks/README.md

Hooks

Custom hooks que encapsulan lógica de negocio, estado de UI y efectos secundarios. Los componentes no contienen lógica propia: toda interacción con servicios, estado local complejo o side-effects vive en un hook dedicado.

Descripción General

Los hooks se organizan por dominio en subcarpetas. Siguen tres convenciones estrictas:

  • Llevan la directiva 'use client' al inicio del archivo.
  • No declaran interface ni type propios: todos los tipos se importan de @/types/<dominio>.
  • Importan useState, useEffect, useCallback, etc. directamente desde 'react' (no * as React).

Estructura de Archivos

text
hooks/
├── auth/
│   ├── useAuthListener.ts        # Escucha cambios de sesión Firebase → Zustand
│   └── useLoginForm.ts           # Estado y validación del formulario de login
├── categories/
│   ├── use-categories-page.ts    # CRUD completo de la página de categorías
│   ├── use-card-subcategory.ts   # Estado y handlers del componente CardSubcategory
│   ├── use-collapsable-category.ts # Estado y handlers del componente CollapsableCategory
│   ├── use-dialog-create-group.ts  # Estado del dialog de creación de rubro
│   ├── use-dialog-create-category.ts # Estado del dialog de creación de categoría
│   ├── use-dialog-create-subcategory.ts # Estado del dialog de creación de subcategoría
│   └── use-dialog-delete-subcategory.ts # Estado del dialog de eliminación de subcategoría
├── materials/
│   ├── use-material-catalog-page.ts       # Hook principal: CRUD + filtros + búsqueda
│   ├── use-dialog-create-material.ts      # Estado del dialog de creación de material
│   ├── use-dialog-edit-material.ts        # Estado del dialog de edición de material
│   ├── use-material-detail.ts             # Suscripción + CRUD para la vista de detalle
│   └── imports/
│       └── use-material-import-page.ts    # Flujo multi-paso de importación Excel
└── shared/
    ├── use-mobile.ts             # Detecta si el viewport es mobile (< 768px)
    └── use-mutation-wrapper.ts   # Wrapper de mutaciones con toast + isMutating

🔐 hooks/auth

useAuthListener()

Archivo: hooks/auth/useAuthListener.ts

Escucha el estado de autenticación de Firebase en tiempo real y sincroniza el usuario con el store de Zustand.

ParámetroTipoDescripción
——Sin parámetros

Comportamiento:

  1. Suscribe a subscribeToAuthChanges al montar.
  2. Si hay usuario Firebase, busca su perfil en Firestore con getUserByUid.
  3. Llama a setUser(user) o setUser(null) en el store.
  4. Desuscribe al desmontar.

useLoginForm()

Archivo: hooks/auth/useLoginForm.ts

Gestiona el estado y envío del formulario de login.


🗂️ hooks/categories

useCategoriesPage()

Archivo: hooks/categories/use-categories-page.ts
Retorna: IUseCategoriesPageReturn (de @/types/categories)

Hook principal de la página /categories. Carga el catálogo completo, gestiona el filtro de búsqueda y expone todos los handlers CRUD.

Estado interno:

StateTipoDescripción
groupsIGroupCatalogView[]Vista completa del catálogo
isLoadingbooleanIndicador de carga
errorError | nullError de carga del catálogo
searchstringTérmino de búsqueda
createGroupOpenbooleanControla dialog de creación de rubro
createCatCtxICreateCategoryCtx | nullContexto para crear categoría
createSubCtxICreateSubcategoryCtx | nullContexto para crear subcategoría
deleteSubCtxIDeleteSubcategoryCtx | nullContexto para el flow de eliminación con materiales

Flujo de datos:

useCategoriesPage() └── loadCatalog() └── getCategoryCatalog() → services/categories/category-catalog.ts └── [getAllGroups, getAllCategories, getAllSubcategories] en paralelo └── buildGroupCatalogViews() → utils/categories/build-catalog.ts └── setGroups(IGroupCatalogView[])

Todos los handlers tienen try/catch con toast.error en caso de error y toast.success en caso de éxito.


useMaterialCounts({ catalog })

Archivo: hooks/categories/use-material-counts.ts
Helper: helpers/material-counts-helpers.ts
Servicios: services/materials/material-categorization.ts

🎯 Problema que resuelve

El UI del módulo de categorías muestra materialsCount en subcategorías. Antes de este cambio, el valor era un placeholder (se rellenaba con 0), por lo que el badge y los flujos de eliminación podían comportarse incorrectamente.

✅ Solución implementada

useMaterialCounts() calcula, para el catálogo recibido, los conteos de materiales por:

  • Subcategoría (subcategoryId)
  • Categoría (categoryId)
  • Rubro/Grupo (groupId)

La lógica se divide así:

  1. Hook: useMaterialCounts({ catalog })

    • Construye una firma (signature) estable a partir de los IDs del catálogo para re-ejecutar solo cuando cambie el conjunto.
    • Actualiza subcategoryCounts, categoryCounts y groupCounts.
    • Mantiene countsError (primer error) y expone isLoadingCounts.
  2. Helper: fetchMaterialCountsForCatalog(catalog, fns)

    • Deduplica IDs con Set.
    • Ejecuta todas las consultas en paralelo con Promise.all.
    • Si una consulta falla, asigna 0 solo a ese nodo (fallback parcial) y devuelve el resto.
  3. Servicios: services/materials/material-categorization.ts

    • getMaterialCountByGroup(groupId)
    • getMaterialCountByCategory(categoryId)
    • getMaterialCountBySubcategory(subcategoryId)

    Estos servicios usan agregación Firestore vía getCollectionCount (que internamente llama a getCountFromServer), evitando descargar documentos.

🔌 Dónde se integra

  1. Página de categorías: hooks/categories/use-categories-page.ts

    • Llama a useMaterialCounts({ catalog: categoryCatalog }).
    • Inyecta los conteos en buildGroupCatalogViews(...) para que ISubcategoryView.materialsCount sea real.
    • handleDeleteSubcategory usa el conteo para decidir entre eliminar o mostrar el dialog con materiales asignados.
  2. Construcción de la vista del catálogo: utils/categories/build-catalog.ts

    • Asigna materialsCount: subcategoryCounts?.[sub.id] ?? 0 al mapear subcategorías.

🧩 Tipos agregados

En types/categories/index.ts se incorporaron:

  • IUseMaterialCountsParams
  • IUseMaterialCountsReturn

⚡ Consideraciones de performance y errores

  • Performance: consultas en paralelo con Promise.all para todos los IDs (grupo/categoría/subcategoría).
  • Manejo de errores:
    • countsError se conserva con el primer error.
    • Los nodos que fallan se convierten a 0.
    • Si el hook falla completo, se aplican mapas vacíos ({}), lo que termina en 0 por defecto en la UI.

🗂️ Archivos creados/modificados

  • Creados:
    • hooks/categories/use-material-counts.ts
    • helpers/material-counts-helpers.ts
    • hooks/categories/tests/use-material-counts.test.ts
  • Modificados / Integrados:
    • services/materials/material-categorization.ts
    • hooks/categories/use-categories-page.ts
    • utils/categories/build-catalog.ts
    • types/categories/index.ts
    • (Docs) utils/README.md

🔎 Cómo validar

Ejecuta:

bash
pnpm lint
pnpm type-check
pnpm test
pnpm build

Hooks por Componente

Cada componente de components/categories/ tiene un hook dedicado que extrae su estado y handlers.

HookComponenteTipos importados
useCardSubcategoryCardSubcategoryIUseCardSubcategoryParams, IUseCardSubcategoryReturn
useCollapsableCategoryCollapsableCategoryIUseCollapsableCategoryParams, IUseCollapsableCategoryReturn
useDialogCreateGroupDialogCreateGroupIUseDialogCreateGroupParams, IUseDialogCreateGroupReturn
useDialogCreateCategoryDialogCreateCategoryIUseDialogCreateCategoryParams, IUseDialogCreateCategoryReturn
useDialogCreateSubcategoryDialogCreateSubcategoryIUseDialogCreateSubcategoryParams, IUseDialogCreateSubcategoryReturn
useDialogDeleteSubcategoryDialogDeleteSubcategoryIUseDialogDeleteSubcategoryParams, IUseDialogDeleteSubcategoryReturn

Todos estos hooks siguen el mismo patrón:

ts
'use client';
import { useState } from 'react';
import type { IUseXxxParams, IUseXxxReturn } from '@/types/categories';

export function useXxx({ onOpenChange, onConfirm }: IUseXxxParams): IUseXxxReturn {
  const [name, setName] = useState('');

  const handleOpenChange = (value: boolean): void => {
    if (!value) setName('');
    onOpenChange(value);
  };

  const handleConfirm = (): void => {
    onConfirm?.({ name });
    handleOpenChange(false);
  };

  return { name, setName, handleOpenChange, handleConfirm };
}

� hooks/materials

useMaterialCatalogPage()

Archivo: hooks/materials/use-material-catalog-page.ts
Retorna: IUseMaterialCatalogPageReturn (de @/types/materials)

Hook principal de la página /material-catalog. Carga materiales y catálogo de categorías, gestiona filtros, búsqueda y todos los handlers CRUD.

Estado interno:

StateTipoDescripción
materialsIMaterial[]Materiales crudos de Firestore
categoryCatalogICategoryCatalog[]Catálogo completo (grupos + categorías + subs)
isLoadingbooleanIndicador de carga inicial
searchstringTérmino de búsqueda
filtersIMaterialFiltersFiltros activos (groupId, categoryId, subcategoryId)
createOpenbooleanControla dialog de creación
editCtxIEditMaterialCtx | nullContexto para editar un material
deleteCtxIDeleteMaterialCtx | nullContexto para confirmar eliminación

filteredMaterials (useMemo): Aplica primero los filtros de filters y luego matchesSearch sobre el nombre. Se recalcula cuando cambian materials, filters o search.

CRUD: Todos los handlers (handleCreate, handleEdit, handleDelete) usan useMutationWrapper e invalidan la cache de Next.js con revalidateTag('materials') a través de la API.


useDialogCreateMaterial({ categoryCatalog, onOpenChange, onConfirm })

Archivo: hooks/materials/use-dialog-create-material.ts

Gestiona el estado del formulario de creación de un material. Al cambiar subcategoryId, llama automáticamente a resolveFullCategorization(subcategoryId, categoryCatalog) y actualiza de forma atómica los 7 campos del formulario: name, subcategoryId, categoryId, groupId, groupName, categoryName, subcategoryName.

Parámetros:

ParámetroTipoDescripción
categoryCatalogICategoryCatalog[]Catálogo completo para resolver nombres
onOpenChange(open: boolean) => voidCallback de control del dialog
onConfirm(data: ICreateMaterialData) => voidCallback al confirmar creación

useDialogEditMaterial({ open, categoryCatalog, initialData, onOpenChange, onConfirm })

Archivo: hooks/materials/use-dialog-edit-material.ts

Mismo patrón que useDialogCreateMaterial, con la diferencia de que sincroniza initialData al abrir el dialog via useEffect. Al cambiar subcategoryId también resuelve la categorización completa.

Parámetros adicionales:

ParámetroTipoDescripción
openbooleanEstado del dialog (controla el efecto de sync)
initialDataIEditMaterialData?Datos pre-poblados al abrir para edición

useMaterialDetail(id)

Archivo: hooks/materials/use-material-detail.ts

Se suscribe a un material individual por ID usando subscribeToMaterial. Expone el material actual, su categorización resuelta y handlers para editar/eliminar.


useMaterialImportPage()

Archivo: hooks/materials/imports/use-material-import-page.ts

Gestiona el flujo de importación masiva de materiales desde Excel. Tiene tres pasos:

  1. Upload: Lee el archivo Excel y lo envía al endpoint /api/materials/import para parseo.
  2. Preview: Muestra los renglones parseados en una tabla editable. Cada fila se puede corregir antes de confirmar.
  3. Confirm: Para cada fila llama a resolveFullCategorization(row.subcategoryId, catalog) y construye el payload completo, incluyendo todos los campos de categorización. Llama a createMaterialsBatch(items) con el array completo.

El batch usa writeBatch del SDK cliente y asigna id y path a cada documento desde la referencia generada por Firestore antes de persistir.


🔧 hooks/shared

useIsMobile()

Archivo: hooks/shared/use-mobile.ts

Retorna true si el ancho del viewport es menor a 768px. Usa matchMedia con listener de cambio.


useMutationWrapper()

Archivo: hooks/shared/use-mutation-wrapper.ts

Wrapper obligatorio para todas las operaciones de mutación (crear, editar, eliminar) en hooks de página. Expone isMutating (para deshabilitar botones durante la operación) y executeMutation (que envuelve el servicio en try/catch, muestra toast.success/toast.error y llama al callback onSuccess).

ts
const { isMutating, executeMutation } = useMutationWrapper();

const handleCreate = useCallback(
  (data: ICreateMaterialData) =>
    executeMutation(() => createMaterial(data), {
      successMessage: 'Material creado exitosamente',
      errorMessage: 'Error al crear el material',
      onSuccess: reloadData,
    }),
  [executeMutation, reloadData],
);

Cómo Agregar un Nuevo Hook

  1. Crear el archivo en hooks/<dominio>/use-nombre.ts.
  2. Agregar directiva 'use client' al inicio.
  3. Declarar los tipos de parámetros y retorno en @/types/<dominio>/index.ts.
  4. Importar todos los tipos — nunca declarar interface en el archivo del hook.
  5. Para operaciones de mutación, usar useMutationWrapper en lugar de try/catch manuales.