# Sprint 8 — Etapa 2: Repositorios y tipos de dominio > **Tipo:** Backend TypeScript — dominio + repos > **Rama:** main > **Estimación:** 1.5-2 horas > **Dependencias:** Etapa 1 completada ✅ (tabla `areas` y columna `area_id` en `processes` ya existen en DB) --- ## Objetivo Extender la capa de dominio y repositorios para soportar el nuevo campo `area_id`. Esto es **no destructivo**: se agregan los campos nuevos sin eliminar los viejos (`clientName`, `groupId`), que todavía son usados por la UI hasta que Etapas 3 y 4 los remplacen. Al terminar esta etapa, el dominio conoce `areaId/areaName`, el repositorio lee y escribe `area_id` en Supabase, y el código de UI existente sigue funcionando sin cambios. --- ## Contexto del código actual Leer estos archivos antes de modificar nada: - `src/domain/types.ts` — `Process` tiene `clientName: string`, `groupId: UUID | null`, `orgId`, `orgName`. Sin `areaId` aún. - `src/persistence/supabase/process-repo.ts` — `fromRow()` extrae `clientName` y `groupId`; `toRow()` escribe `client` y `group_id`; `getById()` hace JOIN con `organizations!org_id(name)`. - `src/persistence/supabase/group-repo.ts` — repositorio del agrupador viejo `process_groups`. **No tocar** — sigue siendo usado por la Settings page. - `src/domain/schemas.ts` — leer el contenido actual antes de modificar. --- ## Alcance ### 1. Actualizar `src/domain/types.ts` **A. Agregar interface `Area`:** ```typescript export interface Area { id: string orgId: string name: string description: string | null createdAt: number // timestamp ms, igual que Process.createdAt } ``` Agregar después de la interface `ProcessGroup` existente (o en una sección nueva de tipos de organización). **B. Extender interface `Process`:** Agregar `areaId` y `areaName` **después** de `orgName`: ```typescript orgId: string | null // 🆕 Sprint 7 orgName?: string | null // 🆕 Sprint 7 — poblado via JOIN en getById areaId: string | null // 🆕 Sprint 8 — área de la org (null = sin área) areaName?: string | null // 🆕 Sprint 8 — poblado via JOIN en getById ``` **NO eliminar** `clientName: string` ni `groupId: UUID | null` — siguen siendo necesarios hasta Etapas 3-4. ### 2. Actualizar `src/domain/schemas.ts` Leer el archivo primero y reportar su contenido. Agregar `areaId` al schema de Process: ```typescript areaId: z.string().uuid().nullable(), ``` **NO eliminar** `clientName` ni `groupId` del schema — todavía se usan. Si `schemas.ts` no tiene un schema de Process (posiblemente solo tiene schemas para formularios), reportarlo antes de modificar. ### 3. Crear `src/persistence/supabase/area-repo.ts` Nuevo repositorio para la tabla `areas`. Seguir el mismo patrón que `group-repo.ts`. ```typescript import { supabase } from '@/lib/supabase' import type { Area } from '@/domain/types' function fromRow(row: Record): Area { return { id: row.id as string, orgId: row.org_id as string, name: row.name as string, description: (row.description as string | null) ?? null, createdAt: new Date(row.created_at as string).getTime(), } } export const supabaseAreaRepo = { // Todas las áreas de una organización, ordenadas por nombre async getByOrg(orgId: string): Promise { const { data, error } = await supabase .from('areas') .select('*') .eq('org_id', orgId) .order('name', { ascending: true }) if (error) throw error return (data ?? []).map(fromRow) }, // Crear área nueva en una org async create(data: { orgId: string; name: string; description?: string }): Promise { const { data: row, error } = await supabase .from('areas') .insert({ org_id: data.orgId, name: data.name, description: data.description ?? null, }) .select('*') .single() if (error) throw error return fromRow(row as Record) }, // Actualizar nombre y/o descripción de un área async update(id: string, data: { name?: string; description?: string }): Promise { const { data: row, error } = await supabase .from('areas') .update({ ...(data.name !== undefined && { name: data.name }), ...(data.description !== undefined && { description: data.description }), updated_at: new Date().toISOString(), }) .eq('id', id) .select('*') .single() if (error) throw error return fromRow(row as Record) }, // Eliminar área. Los procesos con esta área pasan a area_id = null (ON DELETE SET NULL) async delete(id: string): Promise { const { error } = await supabase.from('areas').delete().eq('id', id) if (error) throw error }, } ``` ### 4. Actualizar `src/persistence/supabase/process-repo.ts` Cuatro cambios específicos en el archivo existente: **4A — `fromRow()`: extraer `areaId` y `areaName`** Agregar a la función existente, después de la extracción de `org`: ```typescript // area puede estar presente si el query hizo JOIN con areas (getById lo incluye) const area = row.area as { name: string } | null ``` Y en el objeto de retorno, después de `orgName`: ```typescript areaId: (row.area_id as string | null) ?? null, areaName: area?.name ?? null, ``` **4B — `toRow()`: incluir `area_id`** Agregar al objeto de retorno de `toRow()`, después de `group_id`: ```typescript area_id: process.areaId, ``` **4C — `updateEditable()`: incluir `area_id`** Agregar al objeto del `.update()`, después de `group_id`: ```typescript area_id: process.areaId, ``` **4D — `getById()`: extender el JOIN para incluir área** Cambiar el select actual: ```typescript // Antes: .select('*, org:organizations!org_id(name)') // Después: .select('*, org:organizations!org_id(name), area:areas!area_id(name)') ``` **Lo que NO cambia en `process-repo.ts`:** - `client: process.clientName` en `toRow()` — permanece - `group_id: process.groupId` en `toRow()` — permanece - `client: process.clientName` en `updateEditable()` — permanece - `group_id: process.groupId` en `updateEditable()` — permanece - `fromRow()` sigue extrayendo `clientName` y `groupId` — permanece - `getAll()` — no cambia (no hace JOIN con areas, solo devuelve `area_id` como UUID crudo via `select('*')`) --- ## Tests Agregar al menos 2 tests unitarios para `area-repo.ts`. Seguir el patrón de los tests existentes de repositorios (probablemente con `vi.mock('@/lib/supabase')`). Tests mínimos: 1. `getByOrg()` devuelve lista correcta de áreas 2. `create()` inserta y retorna el área creada --- ## Criterios de validación - [ ] `Area` interface existe en `types.ts` - [ ] `Process.areaId` y `Process.areaName` existen en `types.ts` (sin eliminar `clientName`/`groupId`) - [ ] `area-repo.ts` existe con las 4 operaciones (getByOrg, create, update, delete) - [ ] `process-repo.ts`: `fromRow()` extrae `areaId` y `areaName`, `toRow()` incluye `area_id`, `updateEditable()` incluye `area_id`, `getById()` hace JOIN con `areas` - [ ] `npm run build` sin errores TypeScript - [ ] `npm run test` sin regresiones + 2 tests nuevos de `area-repo.ts` - [ ] Ningún archivo de UI modificado (LibraryPage, WorkspacePage, etc. no cambian) --- ## Restricciones explícitas - **NO eliminar** `clientName`, `groupId` de `types.ts` — aún usados por UI - **NO modificar** `group-repo.ts` — sigue siendo usado por la Settings page - **NO modificar** `library-store.ts` — los cambios de store van en Etapa 3 - **NO modificar** ningún componente React — solo capa de dominio y persistencia - **NO agregar** `area_id` al `select('*')` de `getAll()` explícitamente — ya está incluido en el wildcard --- ## Archivos autorizados a modificar/crear ``` src/ ├── domain/ │ ├── types.ts ← agregar Area interface + areaId/areaName a Process │ └── schemas.ts ← agregar areaId al schema (leer primero) └── persistence/ └── supabase/ ├── area-repo.ts ← crear └── process-repo.ts ← 4 cambios puntuales ``` Opcionalmente, un archivo de tests para `area-repo.ts` en `tests/`. --- ## Commit esperado ``` feat(area-repo): repositorio CRUD de áreas por organización feat(process-repo): extender fromRow/toRow/getById con areaId y areaName feat(domain): interface Area + campos areaId/areaName en Process ``` --- ## Reporte esperado 1. Contenido actual de `schemas.ts` (antes de modificar) 2. Diff compacto de `types.ts` 3. Diff compacto de `process-repo.ts` 4. Contenido de `area-repo.ts` creado 5. Resultado de `npm run build` y `npm run test` (con conteo de tests) 6. Confirmar que `clientName` y `groupId` siguen presentes en `types.ts`