Skip to content

Instantly share code, notes, and snippets.

@molavec
Created January 7, 2026 00:45
Show Gist options
  • Select an option

  • Save molavec/41c77e228b1d236e14a20fe616f2550d to your computer and use it in GitHub Desktop.

Select an option

Save molavec/41c77e228b1d236e14a20fe616f2550d to your computer and use it in GitHub Desktop.
ws context app

🔌 Documentación de APIs

Base URL: http://localhost:3000/api (development)
Método de Autenticación: Session-based (cookies)
Framework: Nitro (Nuxt Server)
Validación: Zod (tipos TypeScript)


📋 Índice de APIs

Tareas


🔐 Autenticación

🔓 Login

POST /auth/login

Descripción: Autentica un usuario y crea una sesión

Autenticación: No requerida

Body:

{
  "email": "user@example.com",
  "password": "password123"
}

Respuesta Exitosa (200):

{
  "user": {
    "id": 1,
    "firstName": "John",
    "lastName": "Doe",
    "email": "user@example.com",
    "color": "#FF5733"
  },
  "loggedInAt": "2024-01-06T10:30:00.000Z"
}

Errores:

  • 400: Missing required fields
  • 401: Invalid credentials

Código Ejemplo:

const login = async (email: string, password: string) => {
  return await $fetch('/api/auth/login', {
    method: 'POST',
    body: { email, password }
  })
}

✍️ Signup

POST /auth/signup

Descripción: Registra un nuevo usuario y lo autentica

Autenticación: No requerida

Body:

{
  "firstName": "John",
  "lastName": "Doe",
  "email": "user@example.com",
  "password": "password123"
}

Respuesta Exitosa (200):

{
  "user": {
    "id": 1,
    "firstName": "John",
    "lastName": "Doe",
    "email": "user@example.com",
    "color": null
  }
}

Errores:

  • 400: Missing required fields
  • 409: Email already exists

Código Ejemplo:

const signup = async (firstName: string, lastName: string, email: string, password: string) => {
  return await $fetch('/api/auth/signup', {
    method: 'POST',
    body: { firstName, lastName, email, password }
  })
}

🚪 Logout

POST /auth/logout

Descripción: Cierra la sesión del usuario

Autenticación: Requerida

Body: Vacío

Respuesta Exitosa (200):

{
  "status": "ok"
}

Errores:

  • 401: Unauthorized
  • 500: Could not log out

Código Ejemplo:

const logout = async () => {
  return await $fetch('/api/auth/logout', { method: 'POST' })
}

📋 Session

GET /auth/session

Descripción: Obtiene la sesión actual del usuario

Autenticación: Requerida

Respuesta Exitosa (200):

{
  "user": {
    "id": 1,
    "firstName": "John",
    "lastName": "Doe",
    "email": "user@example.com",
    "color": "#FF5733"
  },
  "loggedInAt": "2024-01-06T10:30:00.000Z"
}

Errores:

  • 401: Unauthorized

Código Ejemplo:

const getSession = async () => {
  return await $fetch('/api/auth/session')
}

🏢 Workspaces

✨ Crear Workspace

POST /workspaces

Descripción: Crea un nuevo workspace

Autenticación: Requerida

Body:

{
  "name": "Mi Workspace",
  "ownerId": 1,
  "color": "oklch(52.61% 0.201 25.13)"
}

Respuesta Exitosa (200):

{
  "id": 1,
  "name": "Mi Workspace",
  "ownerId": 1,
  "color": "oklch(52.61% 0.201 25.13)",
  "description": null,
  "createdAt": "2024-01-06T10:30:00.000Z",
  "updatedAt": "2024-01-06T10:30:00.000Z",
  "isDeleted": false
}

Errores:

  • 400: Workspace name is required
  • 500: Failed to insert workspace

Notas:

  • Crea automáticamente relación user_workspace con permiso "owner"
  • Asigna orden automáticamente

📖 Obtener Workspace

GET /workspaces/:workspaceId

Descripción: Obtiene los detalles de un workspace

Autenticación: Requerida (debe tener acceso)

Parámetros:

  • workspaceId (number) - ID del workspace

Respuesta Exitosa (200):

{
  "id": 1,
  "name": "Mi Workspace",
  "ownerId": 1,
  "color": "oklch(52.61% 0.201 25.13)",
  "description": "Descripción del workspace",
  "createdAt": "2024-01-06T10:30:00.000Z",
  "updatedAt": "2024-01-06T10:30:00.000Z",
  "isDeleted": false
}

Errores:

  • 403: Forbidden (sin acceso)
  • 404: Workspace not found

🔧 Actualizar Workspace

PUT /workspaces/:workspaceId

Descripción: Actualiza los detalles del workspace

Autenticación: Requerida (solo owner/admin)

Parámetros:

  • workspaceId (number) - ID del workspace

Body:

{
  "name": "Nuevo Nombre",
  "description": "Nueva descripción",
  "color": "#FF5733"
}

Respuesta Exitosa (200):

{
  "id": 1,
  "name": "Nuevo Nombre",
  "description": "Nueva descripción",
  "color": "#FF5733",
  ...
}

Errores:

  • 400: Missing required fields
  • 403: Forbidden
  • 404: Workspace not found

🗑️ Eliminar Workspace

DELETE /workspaces/:workspaceId

Descripción: Elimina (soft delete) un workspace

Autenticación: Requerida (solo owner)

Parámetros:

  • workspaceId (number) - ID del workspace

Respuesta Exitosa (200):

{
  "success": true
}

Errores:

  • 403: Forbidden (no es owner)
  • 404: Workspace not found

📝 Renombrar Workspace

PATCH /workspaces/rename

Descripción: Renombra un workspace

Autenticación: Requerida

Body:

{
  "id": 1,
  "name": "Nuevo Nombre"
}

Respuesta Exitosa (200):

{
  "success": true
}

Errores:

  • 400: ID and name are required
  • 500: Failed to rename workspace

📦 Archivar Workspace

PATCH /workspaces/archive

Descripción: Archiva un workspace

Autenticación: Requerida

Body:

{
  "id": 1,
  "isArchived": true
}

Respuesta Exitosa (200):

{
  "success": true
}

🔄 Reordenar Workspaces

PATCH /workspaces/reorder

Descripción: Cambia el orden de los workspaces

Autenticación: Requerida

Body:

{
  "workspaceId": 1,
  "newOrder": 2
}

Respuesta Exitosa (200):

{
  "success": true
}

📚 Obtener Workspaces del Usuario

GET /workspaces/user/:userId

Descripción: Obtiene todos los workspaces de un usuario

Autenticación: Requerida

Parámetros:

  • userId (number) - ID del usuario

Respuesta Exitosa (200):

[
  {
    "id": 1,
    "name": "Workspace 1",
    "ownerId": 1,
    "color": "#FF5733",
    ...
  },
  {
    "id": 2,
    "name": "Workspace 2",
    "ownerId": 2,
    ...
  }
]

👥 Listar Usuarios en Workspace

GET /workspaces/:workspaceId/users

Descripción: Lista todos los usuarios en un workspace

Autenticación: Requerida (debe tener acceso)

Parámetros:

  • workspaceId (number) - ID del workspace

Respuesta Exitosa (200):

[
  {
    "id": 1,
    "firstName": "John",
    "lastName": "Doe",
    "email": "john@example.com",
    "color": "#FF5733",
    "permission": "owner"
  },
  {
    "id": 2,
    "firstName": "Jane",
    "lastName": "Smith",
    "email": "jane@example.com",
    "color": "#33FF57",
    "permission": "member"
  }
]

🔐 Cambiar Permisos de Usuario

PUT /workspaces/:workspaceId/users/permission

Descripción: Cambia los permisos de un usuario en el workspace

Autenticación: Requerida (solo owner)

Parámetros:

  • workspaceId (number) - ID del workspace

Body:

{
  "userId": 2,
  "permission": "admin"
}

Permisos válidos: owner, admin, member

Respuesta Exitosa (200):

{
  "success": true
}

Errores:

  • 403: Forbidden (no es owner)
  • 400: Invalid permission

🚫 Eliminar Usuario del Workspace

DELETE /workspaces/:workspaceId/users

Descripción: Elimina un usuario del workspace

Autenticación: Requerida

Parámetros:

  • workspaceId (number) - ID del workspace

Query:

  • userId (number) - ID del usuario a eliminar

Respuesta Exitosa (200):

{
  "success": true
}

Errores:

  • 403: Forbidden
  • 404: User not in workspace

🗑️ Eliminar todos los Quicklinks del Workspace

DELETE /workspaces/:workspaceId/quicklinks

Descripción: Soft-delete todos los quicklinks en un workspace

Autenticación: Requerida

Parámetros:

  • workspaceId (number) - ID del workspace

Respuesta Exitosa (200):

{
  "success": true
}

🗑️ Eliminar todas las Tareas del Workspace

DELETE /workspaces/:workspaceId/tasks

Descripción: Soft-delete todas las tareas en un workspace

Autenticación: Requerida

Parámetros:

  • workspaceId (number) - ID del workspace

Respuesta Exitosa (200):

{
  "success": true
}

📨 Invitaciones

📧 Crear Invitación

POST /workspaces/invitations

Descripción: Envía una invitación a un usuario para unirse al workspace

Autenticación: Requerida (solo owner/admin)

Body:

{
  "workspaceId": 1,
  "email": "newuser@example.com"
}

Respuesta Exitosa (200):

{
  "success": true,
  "token": "inv_abc123def456"
}

Errores:

  • 400: Email is required
  • 403: Forbidden
  • 409: User already invited

🔗 Obtener Invitaciones del Workspace

GET /workspaces/invitations/:workspaceId

Descripción: Lista las invitaciones pendientes en un workspace

Autenticación: Requerida

Parámetros:

  • workspaceId (number) - ID del workspace

Respuesta Exitosa (200):

[
  {
    "id": 1,
    "workspaceId": 1,
    "email": "invited@example.com",
    "token": "inv_abc123",
    "status": "pending",
    "expiresAt": "2024-01-13T10:30:00.000Z",
    "createdAt": "2024-01-06T10:30:00.000Z"
  }
]

✅ Aceptar Invitación

POST /workspaces/invitations/join

Descripción: Acepta una invitación y se une al workspace

Autenticación: No requerida (usa token)

Body:

{
  "token": "inv_abc123def456"
}

Respuesta Exitosa (200):

{
  "success": true,
  "workspaceId": 1,
  "workspace": {
    "id": 1,
    "name": "Workspace Name",
    ...
  }
}

Errores:

  • 400: Token is required
  • 401: Invalid or expired token
  • 409: User already member

❌ Rechazar Invitación

DELETE /workspaces/invitations

Descripción: Rechaza una invitación

Autenticación: No requerida (usa token)

Query:

  • token (string) - Token de invitación

Respuesta Exitosa (200):

{
  "success": true
}

✅ Tareas

➕ Crear Tarea

POST /tasks

Descripción: Crea una nueva tarea

Autenticación: Requerida

Body:

{
  "name": "Mi Tarea",
  "description": "Descripción de la tarea",
  "workspaceId": 1,
  "taskSectionId": 5,
  "userId": 1,
  "deadlineDate": "2024-01-15T00:00:00Z",
  "estimatedTime": 25,
  "isDone": false,
  "order": 0
}

Respuesta Exitosa (200):

{
  "id": 1,
  "name": "Mi Tarea",
  "description": "Descripción de la tarea",
  "workspaceId": 1,
  "taskSectionId": 5,
  "userId": 1,
  "deadlineDate": "2024-01-15T00:00:00Z",
  "estimatedTime": 25,
  "isDone": false,
  "order": 0,
  "createdAt": "2024-01-06T10:30:00.000Z",
  "updatedAt": "2024-01-06T10:30:00.000Z",
  "isDeleted": false
}

Errores:

  • 400: Name is required / workspaceId is required
  • 403: Forbidden (sin acceso al workspace)

📋 Obtener Mis Tareas

GET /tasks

Descripción: Obtiene todas las tareas del usuario autenticado

Autenticación: Requerida

Query (opcional):

  • isDone (boolean) - Filtrar por completadas

Respuesta Exitosa (200):

[
  {
    "id": 1,
    "name": "Mi Tarea",
    "description": "Descripción",
    "workspaceId": 1,
    "taskSectionId": 5,
    "userId": 1,
    "deadlineDate": "2024-01-15T00:00:00Z",
    "estimatedTime": 25,
    "isDone": false,
    "workspaceName": "Workspace 1",
    ...
  }
]

Notas:

  • Retorna solo tareas no eliminadas
  • Ordena por isDone y luego por orden

📚 Obtener Tareas del Workspace

GET /workspaces/:workspaceId/tasks

Descripción: Obtiene todas las tareas de un workspace

Autenticación: Requerida

Parámetros:

  • workspaceId (number) - ID del workspace

Respuesta Exitosa (200):

[
  {
    "id": 1,
    "name": "Tarea 1",
    "workspaceId": 1,
    "taskSectionId": 5,
    ...
  }
]

✏️ Actualizar Tarea

PUT /tasks

Descripción: Actualiza una tarea existente

Autenticación: Requerida

Body:

{
  "id": 1,
  "name": "Nombre actualizado",
  "description": "Nueva descripción",
  "isDone": true,
  "deadlineDate": "2024-01-20T00:00:00Z",
  "estimatedTime": 30
}

Respuesta Exitosa (200):

{
  "id": 1,
  "name": "Nombre actualizado",
  ...
}

Errores:

  • 400: ID is required
  • 404: Task not found

🗑️ Eliminar Tarea

DELETE /tasks

Descripción: Soft-delete una tarea

Autenticación: Requerida

Query:

  • id (number) - ID de la tarea

Respuesta Exitosa (200):

{
  "success": true
}

🗑️ Eliminar Tareas en Lote

DELETE /tasks/batch

Descripción: Soft-delete múltiples tareas

Autenticación: Requerida

Body:

{
  "ids": [1, 2, 3]
}

Respuesta Exitosa (200):

{
  "success": true,
  "deletedCount": 3
}

🔄 Reordenar Tareas

PATCH /tasks/reorder

Descripción: Cambia el orden de las tareas

Autenticación: Requerida

Body:

{
  "taskId": 1,
  "newSectionId": 5,
  "newIndex": 2
}

Respuesta Exitosa (200):

{
  "success": true
}

📖 Obtener Tareas de una Sección

GET /workspaces/:workspaceId/task-sections/:sectionId/tasks

Descripción: Obtiene todas las tareas en una sección

Autenticación: Requerida

Parámetros:

  • workspaceId (number)
  • sectionId (number)

Respuesta Exitosa (200):

[
  {
    "id": 1,
    "name": "Tarea en Sección",
    "taskSectionId": 5,
    ...
  }
]

🔗 Vincular Quicklink a Tarea

POST /tasks/:taskId/quicklinks

Descripción: Vincula un quicklink existente a una tarea

Autenticación: Requerida

Parámetros:

  • taskId (number)

Body:

{
  "quicklinkId": 3,
  "workspaceId": 1
}

Respuesta Exitosa (200):

{
  "success": true
}

Errores:

  • 400: quicklinkId is required
  • 404: Task not found
  • 400: Invalid quicklink (not in same workspace)

🔗 Obtener Quicklinks de una Tarea

GET /tasks/:taskId/quicklinks

Descripción: Obtiene todos los quicklinks vinculados a una tarea

Autenticación: Requerida

Parámetros:

  • taskId (number)

Respuesta Exitosa (200):

[
  {
    "id": 3,
    "name": "Google",
    "url": "https://google.com",
    "faviconUrl": "https://google.com/favicon.ico",
    "workspaceId": 1,
    "quicklinkSectionId": null
  }
]

🔌 Desvinicular Quicklink de Tarea

DELETE /tasks/:taskId/quicklinks/:quicklinkId

Descripción: Desvincula un quicklink de una tarea

Autenticación: Requerida

Parámetros:

  • taskId (number)
  • quicklinkId (number)

Respuesta Exitosa (200):

{
  "success": true
}

📋 Secciones de Tareas

➕ Crear Sección de Tareas

POST /task-sections

Descripción: Crea una nueva sección de tareas

Autenticación: Requerida

Body:

{
  "name": "Mi Sección",
  "workspaceId": 1,
  "order": 0
}

Respuesta Exitosa (200):

{
  "id": 5,
  "name": "Mi Sección",
  "workspaceId": 1,
  "order": 0,
  ...
}

📚 Obtener Secciones de Tareas

GET /workspaces/:workspaceId/task-sections

Descripción: Obtiene todas las secciones de tareas en un workspace

Autenticación: Requerida

Parámetros:

  • workspaceId (number)

Respuesta Exitosa (200):

[
  {
    "id": 5,
    "name": "Sección 1",
    "workspaceId": 1,
    "order": 0,
    ...
  },
  {
    "id": 6,
    "name": "Sección 2",
    "workspaceId": 1,
    "order": 1,
    ...
  }
]

✏️ Actualizar Sección de Tareas

PUT /task-sections

Descripción: Actualiza una sección de tareas

Autenticación: Requerida

Body:

{
  "id": 5,
  "name": "Nombre Actualizado",
  "workspaceId": 1
}

Respuesta Exitosa (200):

{
  "id": 5,
  "name": "Nombre Actualizado",
  ...
}

📝 Renombrar Sección de Tareas

PATCH /task-sections/rename

Descripción: Renombra una sección de tareas

Autenticación: Requerida

Body:

{
  "id": 5,
  "name": "Nuevo Nombre"
}

Respuesta Exitosa (200):

{
  "success": true
}

🗑️ Eliminar Sección de Tareas

DELETE /task-sections

Descripción: Soft-delete una sección de tareas

Autenticación: Requerida

Query:

  • id (number) - ID de la sección

Respuesta Exitosa (200):

{
  "success": true
}

🔄 Reordenar Secciones de Tareas

PATCH /task-sections/reorder

Descripción: Cambia el orden de las secciones

Autenticación: Requerida

Body:

{
  "sectionId": 5,
  "newOrder": 2
}

Respuesta Exitosa (200):

{
  "success": true
}

🔗 Quicklinks

➕ Crear Quicklink

POST /quicklinks

Descripción: Crea un nuevo quicklink

Autenticación: Requerida

Body:

{
  "name": "Google",
  "url": "https://google.com",
  "faviconUrl": "https://google.com/favicon.ico",
  "description": "Search engine",
  "workspaceId": 1,
  "quicklinkSectionId": 10,
  "order": 0
}

Respuesta Exitosa (200):

{
  "id": 3,
  "name": "Google",
  "url": "https://google.com",
  "faviconUrl": "https://google.com/favicon.ico",
  "description": "Search engine",
  "workspaceId": 1,
  "quicklinkSectionId": 10,
  "order": 0,
  ...
}

Errores:

  • 400: Name is required / workspaceId is required

📚 Obtener Quicklinks del Workspace

GET /workspaces/:workspaceId/quicklinks

Descripción: Obtiene todos los quicklinks en un workspace

Autenticación: Requerida

Parámetros:

  • workspaceId (number)

Respuesta Exitosa (200):

[
  {
    "id": 3,
    "name": "Google",
    "url": "https://google.com",
    ...
  }
]

✏️ Actualizar Quicklink

PUT /quicklinks

Descripción: Actualiza un quicklink

Autenticación: Requerida

Body:

{
  "id": 3,
  "name": "Google Search",
  "url": "https://google.com",
  "faviconUrl": "...",
  "description": "Updated description"
}

Respuesta Exitosa (200):

{
  "id": 3,
  "name": "Google Search",
  ...
}

🗑️ Eliminar Quicklink

DELETE /quicklinks

Descripción: Soft-delete un quicklink

Autenticación: Requerida

Query:

  • id (number) - ID del quicklink

Respuesta Exitosa (200):

{
  "success": true
}

🗑️ Eliminar Quicklinks en Lote

DELETE /quicklinks/batch

Descripción: Soft-delete múltiples quicklinks

Autenticación: Requerida

Body:

{
  "ids": [3, 4, 5]
}

Respuesta Exitosa (200):

{
  "success": true,
  "deletedCount": 3
}

🔄 Reordenar Quicklinks

PATCH /quicklinks/reorder

Descripción: Cambia el orden de los quicklinks

Autenticación: Requerida

Body:

{
  "quicklinkId": 3,
  "newSectionId": 10,
  "newIndex": 2
}

Respuesta Exitosa (200):

{
  "success": true
}

🖼️ Obtener Favicon

GET /quicklinks/favicon

Descripción: Obtiene el favicon de un dominio

Autenticación: No requerida

Query:

  • domain (string) - Dominio (ej: "google.com")

Respuesta Exitosa (200):

https://google.com/favicon.ico

Nota: Retorna la URL del favicon


📄 Obtener Meta de URL

GET /quicklinks/meta

Descripción: Obtiene metadatos (título, descripción) de una URL

Autenticación: No requerida

Query:

  • url (string) - URL completa

Respuesta Exitosa (200):

{
  "title": "Google Search",
  "description": "The most powerful search engine"
}

📁 Secciones de Quicklinks

➕ Crear Sección de Quicklinks

POST /quicklink-sections

Descripción: Crea una nueva sección de quicklinks

Autenticación: Requerida

Body:

{
  "name": "Mi Sección",
  "workspaceId": 1,
  "order": 0
}

Respuesta Exitosa (200):

{
  "id": 10,
  "name": "Mi Sección",
  "workspaceId": 1,
  "order": 0,
  ...
}

📚 Obtener Secciones de Quicklinks

GET /workspaces/:workspaceId/quicklink-sections

Descripción: Obtiene todas las secciones de quicklinks

Autenticación: Requerida

Parámetros:

  • workspaceId (number)

Respuesta Exitosa (200):

[
  {
    "id": 10,
    "name": "Sección 1",
    "workspaceId": 1,
    "order": 0,
    ...
  }
]

✏️ Actualizar Sección de Quicklinks

PUT /quicklink-sections

Descripción: Actualiza una sección de quicklinks

Autenticación: Requerida

Body:

{
  "id": 10,
  "name": "Nombre Actualizado",
  "workspaceId": 1
}

Respuesta Exitosa (200):

{
  "id": 10,
  "name": "Nombre Actualizado",
  ...
}

📝 Renombrar Sección de Quicklinks

PATCH /quicklink-sections/rename

Descripción: Renombra una sección de quicklinks

Autenticación: Requerida

Body:

{
  "id": 10,
  "name": "Nuevo Nombre"
}

Respuesta Exitosa (200):

{
  "success": true
}

🗑️ Eliminar Sección de Quicklinks

DELETE /quicklink-sections

Descripción: Soft-delete una sección de quicklinks

Autenticación: Requerida

Query:

  • id (number) - ID de la sección

Respuesta Exitosa (200):

{
  "success": true
}

🔄 Reordenar Secciones de Quicklinks

PATCH /quicklink-sections/reorder

Descripción: Cambia el orden de las secciones

Autenticación: Requerida

Body:

{
  "sectionId": 10,
  "newOrder": 2
}

Respuesta Exitosa (200):

{
  "success": true
}

📚 Obtener Quicklinks de una Sección

GET /workspaces/:workspaceId/quicklink-sections/:sectionId/quicklinks

Descripción: Obtiene todos los quicklinks en una sección

Autenticación: Requerida

Parámetros:

  • workspaceId (number)
  • sectionId (number)

Respuesta Exitosa (200):

[
  {
    "id": 3,
    "name": "Google",
    "url": "https://google.com",
    "quicklinkSectionId": 10,
    ...
  }
]

👤 Usuarios

📖 Obtener Usuario

GET /users/:id

Descripción: Obtiene información del usuario

Autenticación: Requerida

Parámetros:

  • id (number) - ID del usuario

Respuesta Exitosa (200):

{
  "id": 1,
  "firstName": "John",
  "lastName": "Doe",
  "email": "john@example.com",
  "color": "#FF5733",
  "createdAt": "2024-01-06T10:30:00.000Z",
  ...
}

✏️ Actualizar Usuario

PUT /users/:id

Descripción: Actualiza información del usuario

Autenticación: Requerida (solo el usuario)

Parámetros:

  • id (number)

Body:

{
  "firstName": "John Updated",
  "lastName": "Doe",
  "color": "#33FF57"
}

Respuesta Exitosa (200):

{
  "id": 1,
  "firstName": "John Updated",
  ...
}

Errores:

  • 403: Forbidden (intentando actualizar otro usuario)

🔐 Cambiar Contraseña

PUT /users/:id/change-password

Descripción: Cambia la contraseña del usuario

Autenticación: Requerida

Parámetros:

  • id (number)

Body:

{
  "currentPassword": "old_password",
  "newPassword": "new_password"
}

Respuesta Exitosa (200):

{
  "success": true
}

Errores:

  • 400: Current password is incorrect
  • 403: Forbidden

ℹ️ Información

📦 Versión

GET /version

Descripción: Obtiene la versión de la aplicación

Autenticación: No requerida

Respuesta Exitosa (200):

{
  "version": "0.7.5"
}

🔒 Seguridad

Validaciones Generales

  • ✅ Todas las operaciones validan workspaceId
  • ✅ Permisos verificados mediante validateWorkspaceAccess()
  • ✅ Contraseñas hasheadas con bcrypt
  • ✅ Sessions manejadas con cookies HTTP-only

Soft Deletes

  • ✅ Registros no se eliminan, se marcan como isDeleted: true
  • ✅ Las queries siempre filtran registros eliminados
  • ✅ Permite recuperación de datos

📊 Patrones Comunes

Autenticación en Frontend

const { data: session } = useAuth()

Llamadas a API

const response = await $fetch('/api/endpoint', {
  method: 'POST',
  body: { ... }
})

Manejo de Errores

try {
  await $fetch(...)
} catch (error) {
  console.error(error)
  setError(error.data?.statusMessage || 'Error')
}

Última actualización: 6 de enero de 2026

📖 Casos de Uso - Dashboard [workspaceId]

Componente: /app/pages/dashboard/[workspaceId]/index.vue
Ruta: /dashboard/:workspaceId
Responsabilidad: Página principal del workspace donde se visualizan y gestionan tareas y quicklinks


📊 Estructura del Componente

Archivo

app/pages/dashboard/[workspaceId]/index.vue

Propósito General

Renderizar el dashboard principal de un workspace específico, permitiendo al usuario:

  • Ver toda la información del workspace
  • Gestionar tareas (crear, editar, eliminar)
  • Gestionar quicklinks (crear, editar, eliminar)
  • Compartir el workspace con otros usuarios
  • Cambiar el nombre del workspace
  • Acceder a configuración avanzada

🔄 Casos de Uso Principales

1️⃣ Cargar Datos del Workspace

Cuándo: Cuando se accede a /dashboard/:workspaceId

Flujo:

1. Usuario navega a /dashboard/123
2. Se extrae workspaceId de la ruta
3. Se realiza fetch GET /api/workspaces/123
4. Se realiza fetch GET /api/workspaces/123/users
5. Si hay error → mostrar "Workspace Not Found"
6. Si éxito → renderizar contenido del dashboard

Código:

const workspaceId = computed(() => Number(route.params.workspaceId));

const {data: workspace, error} = await useFetch<Workspace>(
  '/api/workspaces/' + workspaceId.value
);
const {data: users, error: usersError} = await useFetch<User[]>(
  `/api/workspaces/${workspaceId.value}/users`
);

if(error.value || usersError.value) {
  setError('Failed to load workspace. Please try again later.');
}

dashboardStore.deactiveDashboardLoading()

Estados:

  • ✅ Workspace cargado → mostrar contenido
  • ❌ Workspace no encontrado → mostrar mensaje "Workspace Not Found"
  • ⚠️ Error en carga → mostrar alerta de error

Datos Utilizados:

interface Workspace {
  id: number
  name: string
  description?: string
  color?: string
  ownerId: number
  createdAt: Date
  updatedAt: Date
  isDeleted: boolean
}

interface User {
  id: number
  firstName: string
  lastName: string
  email: string
  color?: string
}

2️⃣ Renombrar el Workspace

Cuándo: El usuario edita el nombre en RenameableInput

Flujo:

1. Usuario hace clic en el nombre del workspace
2. Se activa el modo edición (RenameableInput)
3. Usuario escribe nuevo nombre
4. Usuario presiona Enter/confirma
5. Se realiza PATCH /api/workspaces/rename
6. Si éxito → mostrar alerta de éxito
7. Si error → mostrar alerta de error

Código:

const renameHandler = async (name: string) => {
  try {
    await $fetch(`/api/workspaces/rename`, {
      method: 'PATCH',
      body: {
        id: workspaceId.value,
        name: name,
      },
    });
    setSuccess('Workspace renamed successfully');
  } catch (error) {
    setError('Error renaming section: ' + error);
  }
}

Componente Involucrado:

  • RenameableInput.vue - Captura el nuevo nombre

Estados:

  • 📝 Modo edición activo
  • 💾 Guardando nombre
  • ✅ Nombre guardado exitosamente
  • ❌ Error al guardar

Datos Enviados:

{
  "id": 123,
  "name": "Nuevo nombre del workspace"
}

3️⃣ Compartir Workspace con Otros Usuarios

Cuándo: Usuario hace clic en el icono "Share"

Flujo:

1. Usuario hace clic en ShareIcon
2. Se abre ShareWorkspaceModal
3. Modal muestra:
   - Email del usuario a invitar
   - Botones para enviar invitación
4. Usuario ingresa email y confirma
5. Se envía invitación (en backend)
6. Modal se cierra
7. Se muestra alerta de éxito

Código:

const shareModalVisible = ref(false)

// En el template
<div class="btn btn-ghost cursor-pointer flex items-center gap-2 p-2 rounded transition-colors">
  <ShareIcon class="size-6" @click="() => shareModalVisible = true" />
</div>

<ShareWorkspaceModal 
  :workspace="workspace" 
  v-model:is-visible="shareModalVisible"
/>

Componente Involucrado:

  • ShareWorkspaceModal.vue - Gestiona la invitación

Estados:

  • 🔓 Modal cerrado
  • 📧 Modal abierto esperando email
  • ⏳ Enviando invitación
  • ✅ Invitación enviada
  • ❌ Error al enviar

Datos del Modal:

interface ShareWorkspaceModal {
  workspace: Workspace
  isVisible: boolean
}

4️⃣ Ver Avatares de Usuarios en el Workspace

Cuándo: Dashboard se carga

Flujo:

1. Se obtiene lista de usuarios del workspace (ya cargada)
2. Para cada usuario:
   - Se renderiza UserBubble con avatar
   - Se moestra firstName, lastName, color
3. Los avatares se apilan en la esquina superior derecha

Código:

<ClientOnly>
  <div class="flex items-end gap-2">
    <UserBubble 
      v-for="user in users" 
      :key="user.id" 
      :firstName="user.firstName"
      :lastName="user.lastName"
      :color="user.color"
      class="ml-[-16px] hover:z-100"/>
  </div>
</ClientOnly>

Componente Involucrado:

  • UserBubble.vue - Renderiza avatar individual

Datos Utilizados:

users: User[] = [
  { id: 1, firstName: 'John', lastName: 'Doe', color: '#FF5733' },
  { id: 2, firstName: 'Jane', lastName: 'Smith', color: '#33FF57' }
]

5️⃣ Buscar/Filtrar Tareas y Quicklinks

Cuándo: Usuario escribe en el input de búsqueda

Flujo:

1. Usuario escribe en el input de búsqueda
2. Se actualiza filterText en Filter Store
3. Componentes TasksBlock y QuickLinksBlock reaccionan
4. Filtran items por coincidencia con filterText
5. Se muestran solo items coincidentes

Código:

const { filterText } = storeToRefs(useFilterStore());

// En el template
<input 
  v-model="filterText"
  type="text" 
  placeholder="Search..." 
  class="input input-bordered w-full"
/>

Store Involucrado:

  • Filter Store - Mantiene estado de búsqueda global

Flujo de Datos:

Input (cambio) 
  ↓
filterText (Store) 
  ↓
TasksBlock (observa filterText)
  ↓
QuickLinksBlock (observa filterText)
  ↓
Componentes filtran items localmente

6️⃣ Mostrar Loader Global

Cuándo: Se está cargando el dashboard

Flujo:

1. Estado isDashboardLoading = true (en dashboard store)
2. Se muestra overlay fijo con spinner
3. Usuario puede hacer clic para cerrar el loader
4. Cuando termina carga → isDashboardLoading = false
5. Se oculta el overlay

Código:

const deactivateDashboardLoading = () => {
  dashboardStore.setSelectedWorkspace(workspaceId.value)
  isDashboardLoading.value = false
}

// En el template
<div 
  v-if="isDashboardLoading" 
  class="fixed top-0 left-0 flex items-center justify-center bg-base-300/80 h-lvh w-full" 
  @click="deactivateDashboardLoading">
  <div>
    <span class="loading loading-spinner loading-xl mr-2"></span>
    Loading...
  </div>
</div>

Store Involucrado:

  • Dashboard Store - Controla isDashboardLoading

Estados:

  • ⏳ Loading visible
  • ✅ Loading oculto

7️⃣ Cambiar Tema (Dark/Light)

Cuándo: Usuario hace clic en el botón de tema

Flujo:

1. Se renderiza SwitchTheme como ClientOnly (lado cliente)
2. Usuario hace clic en el botón
3. Se cambia el tema (tema del navegador/localStorage)
4. La interfaz se actualiza al nuevo tema

Código:

<div>
  <ClientOnly>
    <SwitchTheme />
  </ClientOnly>
</div>

Componente Involucrado:

  • SwitchTheme.vue - Gestiona cambio de tema

Notas:

  • ClientOnly previene renderizado en servidor (SSR)
  • El cambio de tema es persistente (localStorage)

8️⃣ Acceder a Configuración del Workspace

Cuándo: Usuario hace clic en el icono de engranaje

Flujo:

1. Usuario ve icono AdjustmentsVerticalIcon
2. Usuario hace clic
3. Se navega a /dashboard/:workspaceId/settings
4. Se abre la página de configuración

Código:

<NuxtLink 
  :to="`/dashboard/${workspaceId}/settings`" 
  class="btn btn-ghost flex items-center gap-2 p-2 rounded transition-colors">
  <AdjustmentsVerticalIcon class="size-6"/>
</NuxtLink>

Página Destino:

  • /app/pages/dashboard/[workspaceId]/settings.vue

Funcionalidades en Settings:

  • Editar nombre, descripción, color del workspace
  • Gestionar miembros y permisos
  • Ver roles de usuarios
  • Transferir propiedad
  • Eliminar workspace

9️⃣ Renderizar Bloque de Quicklinks

Cuándo: Dashboard se carga

Flujo:

1. Se renderiza componente QuickLinksBlock
2. Se pasa workspaceId como prop
3. El componente:
   - Fetch quicklink sections
   - Fetch quicklinks por sección
   - Renderiza UI con secciones y items
   - Permite crear, editar, eliminar quicklinks
4. Usa FilterStore para filtrar resultados

Código:

<ClientOnly>
  <QuickLinksBlock :workspace-id="workspaceId" />
</ClientOnly>

Componente Involucrado:

  • QuickLinksBlock.vue - Componente principal
  • QuicklinksSection.vue - Cada sección
  • QuickLinkItem.vue - Cada item
  • QuicklinkDrawer.vue - Editor de quicklink
  • useQuicklinks() - Composable de lógica

Datos Obtenidos:

// QuickLink Sections
GET /api/workspaces/:workspaceId/quicklink-sections

// Quicklinks por sección
GET /api/workspaces/:workspaceId/quicklink-sections/:sectionId/quicklinks

Operaciones Soportadas:

  • ✅ Crear sección de quicklinks
  • ✅ Crear quicklink
  • ✅ Editar quicklink
  • ✅ Eliminar quicklink
  • ✅ Reordenar quicklinks (drag & drop)
  • ✅ Vincular quicklink a tarea

🔟 Renderizar Bloque de Tareas

Cuándo: Dashboard se carga

Flujo:

1. Se renderiza componente TasksBlock
2. Se pasa workspaceId como prop
3. El componente:
   - Fetch task sections
   - Fetch tareas por sección
   - Renderiza UI con secciones e items
   - Permite crear, editar, eliminar tareas
4. Usa FilterStore para filtrar resultados

Código:

<ClientOnly>
  <TasksBlock :workspace-id="workspaceId" />
</ClientOnly>

Componente Involucrado:

  • TasksBlock.vue - Componente principal
  • TasksSection.vue - Cada sección
  • TaskItem.vue - Cada item
  • TaskDrawer.vue - Editor de tarea
  • useTasks() - Composable de lógica

Datos Obtenidos:

// Task Sections
GET /api/workspaces/:workspaceId/task-sections

// Tareas por sección
GET /api/workspaces/:workspaceId/task-sections/:sectionId/tasks

Operaciones Soportadas:

  • ✅ Crear sección de tareas
  • ✅ Crear tarea
  • ✅ Editar tarea
  • ✅ Eliminar tarea
  • ✅ Marcar tarea como completada
  • ✅ Reordenar tareas (drag & drop)
  • ✅ Vincular quicklinks a tarea
  • ✅ Asignar tarea a usuario
  • ✅ Establecer fecha límite
  • ✅ Establecer tiempo estimado

📦 Sidebar - WorkspaceSidebarList

Componente: WorkspaceSidebarList.vue

Ubicación en Template:

<template #sidebar-content>
  <WorkspaceSidebarList :workspace-id="workspace?.id" />
</template>

Propósito:

  • Mostrar lista de workspaces del usuario
  • Permitir navegar entre workspaces
  • Mostrar workspaces activos e inactivos
  • Permitir filtrar y reordenar workspaces

Características:

  • Fetch workspaces del usuario actual
  • Drag & drop para reordenar
  • Tab para "Active" y "Archived"
  • Botón para crear nuevo workspace
  • Mostrar workspace actualmente seleccionado

🎯 Flujo Completo de User Journey

1. Usuario navega a /dashboard/123
   ↓
2. Se cargan datos del workspace y usuarios
   ↓
3. Se muestra overlay de carga (si es lento)
   ↓
4. Se renderiza dashboard con:
   - Nombre del workspace (editable)
   - Avatares de usuarios
   - Input de búsqueda
   - Botones de acciones (Share, Settings, Theme)
   - Bloque de quicklinks
   - Bloque de tareas
   ↓
5. Usuario interactúa con:
   - Crear/editar/eliminar tareas
   - Crear/editar/eliminar quicklinks
   - Filtrar por búsqueda
   - Compartir workspace
   - Cambiar configuración
   - Cambiar tema

📊 Datos Clave por Caso de Uso

Caso 1: Cargar Workspace

Endpoint: GET /api/workspaces/:id
Respuesta: Workspace | null

Caso 2: Renombrar Workspace

Endpoint: PATCH /api/workspaces/rename
Body: { id: number, name: string }
Respuesta: { success: boolean } | error

Caso 3: Compartir Workspace

Endpoint: (en ShareWorkspaceModal)
POST /api/workspaces/:id/invite
Body: { email: string }
Respuesta: { success: boolean } | error

Caso 5: Filtrar Tareas/Quicklinks

Store: Filter Store
Acción: setFilterText(query: string)
Uso: TasksBlock y QuickLinksBlock filtran localmente

Caso 9: Quicklinks

Endpoints:
- GET /api/workspaces/:id/quicklink-sections
- GET /api/workspaces/:id/quicklink-sections/:sectionId/quicklinks
- POST /api/quicklinks
- PATCH /api/quicklinks/:id
- DELETE /api/quicklinks/:id

Caso 10: Tareas

Endpoints:
- GET /api/workspaces/:id/task-sections
- GET /api/workspaces/:id/task-sections/:sectionId/tasks
- POST /api/tasks
- PATCH /api/tasks/:id
- DELETE /api/tasks/:id

🔌 Stores Utilizados

Store Casos de Uso Acciones
Dashboard Store 1, 7 setSelectedWorkspace(), activeDashboardLoading(), deactiveDashboardLoading()
Filter Store 5 setFilterText()
Task Store 10 (ver en composables)
Quicklink Store 9 (ver en composables)
Alerts Store 2, 3 setSuccess(), setError()

🔗 Dependencias de Componentes

Dashboard Page
├── WorkspaceSidebarList (sidebar)
├── RenameableInput (nombre editable)
├── SwitchTheme (cambiar tema)
├── UserAvatar (avatar del usuario actual)
├── UserBubble (avatares de miembros)
├── ShareWorkspaceModal (modal de compartir)
├── QuickLinksBlock
│   ├── QuicklinksSection
│   │   └── QuickLinkItem
│   └── QuicklinkDrawer
└── TasksBlock
    ├── TasksSection
    │   └── TaskItem
    └── TaskDrawer

🎨 Estilos y Layout

Layout Principal

  • Grid/Flex: flex-wrap md:flex-nowrap
  • Gap: gap-4
  • Quiclinks: lado izquierdo
  • Tasks: lado derecho (en md screens)

Header

  • Ícono workspace + Nombre (editable)
  • Input de búsqueda centrado
  • Acciones a la derecha (Share, Settings, Theme)

Responsive

  • Mobile: flex-wrap (apiladas)
  • Tablet+: flex-nowrap (lado a lado)
  • Padding: p-2 md:p-4

⚠️ Casos de Error

  1. Workspace no encontrado

    • Mostrar: "Workspace Not Found"
    • Opción: Botón "Back to Home"
  2. Error en fetch de datos

    • Mostrar: Toast de error
    • Acción: Usuario puede intentar recarga de página
  3. Error en renombrar

    • Mostrar: Toast de error específico
    • Estado: Revierte cambio en input
  4. Error en compartir

    • Mostrar: Error en modal
    • Acción: Usuario puede intentar nuevamente

🔐 Validaciones y Permisos

  • ✅ Solo usuarios del workspace pueden acceder
  • ✅ Solo dueño puede renombrar workspace (verificado en backend)
  • ✅ Solo dueño o admin puede invitar usuarios (verificado en backend)
  • ✅ Acceso a settings requiere permisos específicos
  • ✅ Todas las operaciones validadas en /server/utils/validateWorkspaceAccess

Última actualización: 6 de enero de 2026

📋 Contexto Técnico - Workspices Application

Versión: 0.7.5
Descripción: Aplicación para gestionar proyectos con tareas y quicklinks


🏗️ Arquitectura General

Stack Tecnológico Principal

  • Framework Frontend: Nuxt 4.1.2 (Vue 3.5.13)
  • Framework Backend: Nitro (integrado en Nuxt)
  • Base de Datos: PostgreSQL (con Drizzle ORM)
  • Gestor de Paquetes: pnpm 10.26.2
  • Lenguaje: TypeScript 5.8.3
  • Deployment: Netlify / Vercel (configurado con Nitro presets)

📦 Dependencias Principales

Frontend

Paquete Versión Propósito
@nuxt/eslint 1.3.1 Linting para Nuxt
@pinia/nuxt 0.11.1 State management
@vueuse/core 14.1.0 Composables reutilizables
@heroicons/vue 2.2.0 Iconografía
@unhead/vue 2.0.12 Meta tags y head
tailwindcss 4.1.6 CSS framework
daisyui 5.0.43 Componentes UI
vuedraggable 4.1.0 Drag & drop
zod 4.0.14 Validación de esquemas

Backend

Paquete Versión Propósito
drizzle-orm 0.44.2 ORM para PostgreSQL
postgres 3.4.7 Driver PostgreSQL
bcrypt 6.0.0 Hash de contraseñas
dotenv 17.2.1 Variables de entorno
nuxt-auth-utils 0.5.20 Autenticación
nuxt-authorization 0.3.5 Autorización

Testing

Paquete Versión Propósito
cypress 14.5.4 E2E testing
@cypress/webpack-preprocessor 7.0.1 Webpack para Cypress

📂 Estructura de Carpetas

/
├── app/                       # Aplicación Nuxt
│   ├── assets/               # Estilos CSS y fuentes
│   ├── components/           # Componentes Vue reutilizables
│   ├── composables/          # Composables (lógica reutilizable)
│   │   ├── useQuicklinks.ts
│   │   ├── useTasks.ts
│   │   └── useWorkspaces.ts
│   ├── layouts/              # Layouts de página
│   ├── middleware/           # Middleware de Nuxt
│   ├── pages/                # Páginas (rutas automáticas)
│   ├── plugins/              # Plugins de Nuxt
│   ├── utils/                # Utilidades
│   ├── app.vue               # Componente raíz
│   └── error.vue             # Página de error
│
├── server/                   # Backend Nitro
│   ├── api/                  # Endpoints de API
│   ├── routes/               # Rutas de servidor
│   ├── plugins/              # Plugins de servidor
│   ├── utils/                # Utilidades del servidor
│   └── assets/               # Assets del servidor
│
├── stores/                   # Pinia stores
│   ├── alerts.ts
│   ├── dashboard.ts
│   ├── filter.ts
│   ├── modal.ts
│   ├── quicklink.ts
│   ├── sidebar.ts
│   └── task.ts
│
├── shared/                   # Código compartido frontend/backend
│   ├── auth.d.ts             # Tipos de autenticación
│   ├── types/                # Tipos TypeScript compartidos
│   └── utils/                # Utilidades compartidas
│
├── drizzle/                  # Migraciones dev
├── drizzle-prod/             # Migraciones y esquema producción
├── dockerfiles/              # Configuración Docker
├── public/                   # Archivos estáticos
├── cypress/                  # Tests E2E
└── tmp/                      # Archivos temporales


🗄️ Base de Datos

ORM & Migraciones

  • ORM: Drizzle ORM
  • Archivo Esquema: drizzle-prod/schema.ts
  • Configuración:
    • Dev: drizzle.config.ts
    • Prod: drizzle-prod.config.ts

Base de Datos Soportadas

  • PostgreSQL (Supabase en producción)
  • PostgreSQL Local (Docker para desarrollo)

Comandos de Base de Datos

pnpm db:generate       # Generar migraciones
pnpm db:push           # Aplicar migraciones
pnpm db:migrate        # Ejecutar migraciones
pnpm db:studio         # Abrir Drizzle Studio

# Producción
pnpm db:generate:prod
pnpm db:push:prod
pnpm db:migrate:prod

🔐 Autenticación & Autorización

Sistemas

  • Auth Utils: nuxt-auth-utils (0.5.20)
  • Authorization: nuxt-authorization (0.3.5)
  • Contraseñas: Hasheadas con bcrypt

Variables de Entorno Requeridas

# Autenticación
NUXT_SESSION_PASSWORD=<contraseña-sesión>

# Supabase (Producción)
SUPABASE_URL=<url-supabase>
SUPABASE_KEY=<key-supabase>

# Base de Datos
DATABASE_URL=<url-conexion>
DATABASE_DIRECT_URL=<url-directa>

🎨 Frontend Architecture

Estado (Pinia)

Ubicación: /stores

Stores disponibles:

  • alerts.ts - Gestión de alertas
  • dashboard.ts - Estado del dashboard
  • filter.ts - Filtros aplicados
  • modal.ts - Modales abiertos
  • quicklink.ts - Quicklinks
  • sidebar.ts - Estado del sidebar
  • task.ts - Tareas

Composables

Ubicación: /app/composables

Composables principales:

  • useQuicklinks() - Lógica de quicklinks
  • useTasks() - Lógica de tareas
  • useWorkspaces() - Lógica de workspaces

Componentes

Ubicación: /app/components

Componentes principales:

  • TasksBlock.vue - Bloque de tareas
  • QuickLinksBlock.vue - Bloque de quicklinks
  • TaskDrawer.vue - Drawer de tareas
  • QuicklinkDrawer.vue - Drawer de quicklinks
  • ShareWorkspaceModal.vue - Modal de compartir workspace
  • RenameableInput.vue - Input editable
  • ModalAlert.vue - Alerta modal
  • ToastAlert.vue - Toast notificación
  • UserAvatar.vue - Avatar de usuario
  • SwitchTheme.vue - Selector de tema

Estilos

  • CSS Framework: Tailwind CSS 4.1.6
  • UI Components: DaisyUI 5.0.43
  • Archivo Principal: /app/assets/css/main.css

🛣️ Backend API

Estructura

  • Ubicación: /server/api
  • Framework: Nitro
  • Rutas: Auto-generadas desde /server/api/**

Funcionalidades Principales

  • Gestión de tareas
  • Gestión de quicklinks
  • Gestión de workspaces
  • Autenticación y autorización

🧪 Testing

E2E Testing

  • Framework: Cypress 14.5.4
  • Ubicación: /cypress
  • Comando: pnpm cy:test
  • Modo interactivo: pnpm cy:open

Archivos de Configuración

  • cypress.config.ts - Configuración de Cypress
  • cypress/tsconfig.json - TypeScript para Cypress

🐳 Docker

Desarrollo

pnpm docker:up          # Iniciar contenedores
pnpm docker:down        # Parar contenedores
pnpm docker:logs        # Ver logs
pnpm docker:ps          # Ver estado de contenedores
pnpm docker:rm          # Remover contenedores
pnpm docker:sh          # Acceso a PostgreSQL

Producción

pnpm docker:prod:build  # Build imagen
pnpm docker:prod:up     # Iniciar producción
pnpm docker:prod:down   # Parar producción

Configuración

  • Dev: dockerfiles/docker-compose.dev.yaml
  • Prod Local: dockerfiles/docker-compose.prod-local.yaml
  • Dockerfile: dockerfiles/Dockerfile
  • Entrypoint: dockerfiles/entrypoint.sh

📜 Scripts NPM Principales

Comando Descripción
pnpm dev Inicia servidor de desarrollo
pnpm build Build para producción
pnpm preview Vista previa del build
pnpm generate Generar sitio estático
pnpm cy:test Ejecutar tests E2E
pnpm cy:open Abrir Cypress UI
pnpm db:generate Generar migraciones DB
pnpm db:studio Abrir Drizzle Studio

⚙️ Configuración Nuxt

Archivo Principal

nuxt.config.ts

Módulos Instalados

  1. @nuxt/eslint - Linting
  2. nuxt-auth-utils - Autenticación
  3. @pinia/nuxt - State management
  4. @nuxt/scripts - Scripts (GTM)
  5. nuxt-authorization - Autorización

Vite Configuration

  • Tailwind CSS Plugin
  • Hosts Permitidos:
    • workspices.online
    • alfa.workspices.online
    • beta.workspices.online
    • app.workspices.online
    • Ngrok y Cloudflare para desarrollo

SSR

  • Habilitado (default en Nuxt 3)

Analytics

  • Google Tag Manager: GTM-5TG6GHQ5 (solo producción)

🌍 Deployment

Plataformas Soportadas

  • Netlify (configurado con preset)
  • Vercel (edge functions disponibles)
  • Docker (contenedores producción)

Configuración

  • netlify.toml - Configuración Netlify
  • Nitro presets para diferentes plataformas

📝 Convenciones de Código

Vue Components (SFC)

Orden en archivos .vue:

  1. <script setup lang="ts"> - Composición API
  2. <template>
  3. <style scoped>

Imports

  • Usar ~/ para imports de shared, server, public y store
  • Preferir composables para lógica reutilizable
  • Importar tipos con type keyword

Composables Structure

Orden en composables:

  1. Imports
  2. Composables usados
  3. Constants
  4. Reactive states (ref, reactive)
  5. Fetches/API calls
  6. Computed
  7. Watch
  8. Functions
  9. Handlers

Funciones

  • Preferir arrow functions

🔧 Herramientas de Desarrollo

Code Quality

  • Linter: ESLint 9.0.0
  • TypeScript: 5.8.3
  • Formatter Config: .eslintrc.mjs

Task Runner

  • Justfile presente (comando: just)

Workspace Management

  • pnpm workspaces (ver pnpm-workspace.yaml)

📊 Versiones Node & Package Manager

  • Node: ^22.15.17 (recomendado)
  • pnpm: 10.26.2 (requiere exactamente esta versión)
  • Tipo de módulo: ESM (ES Modules)

🚀 Quick Start

Instalación

pnpm install

Variables de Entorno

cp .env.example .env
# Editar .env con tus credenciales

Desarrollo

pnpm docker:up      # Iniciar BD
pnpm db:generate    # Generar migraciones
pnpm db:migrate     # Aplicar migraciones
pnpm dev            # Iniciar servidor (http://localhost:3000)

Testing

pnpm cy:open        # Tests interactivos
pnpm cy:test        # Tests automatizados

📚 Documentación Oficial


Última actualización: 6 de enero de 2026

🏪 Guía de Stores - Pinia

Ubicación: /stores
Framework: Pinia 3.0.3
Composición: Composition API (setup stores)


📚 Índice de Stores

  1. Alerts Store
  2. Task Store
  3. Quicklink Store
  4. Modal Store
  5. Dashboard Store
  6. Sidebar Store
  7. Filter Store

🚨 Alerts Store

Archivo: /stores/alerts.ts
Export: useAlertsStore()

Propósito

Gestionar notificaciones y alertas globales de la aplicación con diferentes niveles de severidad (info, warning, success, error).

Estado

const info = ref<string | null>(null)        // Mensajes informativos
const warning = ref<string | null>(null)     // Advertencias
const success = ref<string | null>(null)     // Mensajes de éxito
const error = ref<string | null>(null)       // Mensajes de error

Acciones

Acción Parámetros Descripción
setInfo() (message: string, timeout?: number) Mostrar alerta informativa
setWarning() (message: string, timeout?: number) Mostrar alerta de advertencia
setSuccess() (message: string, timeout?: number) Mostrar alerta de éxito
setError() (message: string, timeout?: number) Mostrar alerta de error

Características

  • Auto-limpieza: Los mensajes se limpian automáticamente después del timeout (default 5s)
  • Timeout personalizable: Permite especificar duración del mensaje
  • Múltiples niveles: Soporta 4 tipos diferentes de alertas

Cuándo Usar

Operaciones Exitosas

const { setSuccess } = useAlertsStore()

// Crear una tarea
await createTask(data)
setSuccess('Tarea creada exitosamente')

Operaciones con Error

const { setError } = useAlertsStore()

try {
  await deleteTask(taskId)
  setSuccess('Tarea eliminada')
} catch (err) {
  setError('No se pudo eliminar la tarea')
}

Advertencias

const { setWarning } = useAlertsStore()

if (unsavedChanges) {
  setWarning('Tienes cambios sin guardar')
}

Información General

const { setInfo } = useAlertsStore()

setInfo('Sincronizando datos...', 3000)

Componentes que lo Utilizan

  • ToastAlert.vue - Renderiza los mensajes
  • Cualquier componente que realice operaciones CRUD

✅ Task Store

Archivo: /stores/task.ts
Export: useTaskStore()

Propósito

Gestionar el estado de las tareas, incluyendo la tarea activa (en edición), drawer de tareas, y listados de tareas por sección.

Estado

// Tarea activa (siendo editada)
const activeTask = ref<Task | null>(null)
const initialActiveTask = ref<Task | null>(null)
const isTaskDrawerOpen = ref(false)

// Mapa de tareas por sección
const tasksMap = ref<Record<number, Task[]>>({})

// Estados de carga por sección
const loadingStates = ref<Record<number, boolean>>({})

Getters

getTasksForSection(sectionId: number)   // Obtener tareas de una sección
isLoading(sectionId: number)            // ¿Se está cargando una sección?

Acciones Principales

Crear Nueva Tarea

createNewTask(sectionId: number, workspaceId: number)

Cuándo usar: Cuando el usuario hace clic en "Crear tarea"

  • Abre el drawer de tareas
  • Inicializa una tarea vacía con valores por defecto
  • Prepara el estado para edición

Resetear Cambios

resetActiveTask()

Cuándo usar: Cuando el usuario cancela la edición

  • Revierte los cambios a los valores iniciales
  • Restaura la tarea a su estado original antes de la edición

Abrir/Cerrar Drawer

openTaskDrawer()
closeTaskDrawer()

Guardar Tarea

saveActiveTask()

Cuándo usar: Cuando el usuario confirma la edición

  • Guarda o actualiza la tarea
  • Limpia el estado de edición

Actualizar Mapa de Tareas

updateTasksMap(sectionId: number, tasks: Task[])

Cuándo usar: Después de obtener tareas del servidor

  • Actualiza el caché local de tareas

Cambiar Orden de Tareas

updateTaskOrder(sectionId: number, fromIndex: number, toIndex: number)

Cuándo usar: Después de un drag & drop

Interfaz Task

interface Task {
  id: number
  name: string
  description: string
  isDone: boolean
  workspaceId: number
  taskSectionId: number
  order: number
  mytasks_order: number
  deadlineDate: Date | null
  userId: number | null
  estimatedTime: number        // En minutos (default 25)
  createdAt: Date
  updatedAt: Date
  deletedAt: Date | null
  isDeleted: boolean
}

Cuándo Usar

Flujo de Creación

const taskStore = useTaskStore()

// 1. Usuario hace clic en "crear tarea"
taskStore.createNewTask(sectionId, workspaceId)

// 2. Drawer se abre con tarea vacía
// Usuario edita en el componente TaskDrawer

// 3. Usuario guarda
taskStore.saveActiveTask()
taskStore.setSuccess('Tarea creada')

Flujo de Edición

// 1. Usuario hace clic en una tarea
taskStore.setActiveTask(existingTask)
taskStore.openTaskDrawer()

// 2. Usuario edita
// Cambios se reflejan en activeTask.value

// 3. Usuario cancela
taskStore.resetActiveTask()

// 4. O usuario guarda
taskStore.saveActiveTask()

Cargar Tareas de una Sección

const { getTasksForSection, updateTasksMap } = useTaskStore()

// En el composable useTask()
const tasks = await fetchTasksBySection(sectionId)
updateTasksMap(sectionId, tasks)

// En el template
const tasks = computed(() => getTasksForSection(sectionId))

Componentes que lo Utilizan

  • TaskDrawer.vue - Edición de tareas
  • TaskItem.vue - Ítem individual
  • TasksBlock.vue - Lista de tareas
  • TasksSection.vue - Sección completa
  • Composable useTasks()

🔗 Quicklink Store

Archivo: /stores/quicklink.ts
Export: useQuicklinkStore()

Propósito

Gestionar el estado de los quicklinks, similar al task store pero para referencias a URLs.

Estado

// Quicklink activo (siendo editado)
const activeQuicklink = ref<Quicklink | null>(null)
const initialActiveQuicklink = ref<Quicklink | null>(null)
const isQuicklinkDrawerOpen = ref(false)

// Mapa de quicklinks por sección
const quicklinksMap = ref<Record<number, Quicklink[]>>({})

// Estados de carga por sección
const loadingStates = ref<Record<number, boolean>>({})

Getters

getQuicklinksForSection(sectionId: number)  // Obtener quicklinks de una sección
isLoading(sectionId: number)                // ¿Se está cargando?

Acciones Principales

Crear Nuevo Quicklink

createNewQuicklink(sectionId: number, workspaceId: number)

Resetear Cambios

resetActiveQuicklink()

Guardar Quicklink

saveActiveQuicklink()

Actualizar Mapa

updateQuicklinksMap(sectionId: number, quicklinks: Quicklink[])

Eliminar Quicklink

deleteQuicklink(quicklinkId: number)

Interfaz Quicklink

interface Quicklink {
  id: number
  name: string
  url: string
  faviconUrl: string | null
  workspaceId: number
  quicklinkSectionId: number
  order: number
  createdAt: Date
  updatedAt: Date
  deletedAt: Date | null
  isDeleted: boolean
}

Cuándo Usar

Crear Quicklink

const quicklinkStore = useQuicklinkStore()

// Usuario hace clic en "agregar quicklink"
quicklinkStore.createNewQuicklink(sectionId, workspaceId)
quicklinkStore.openQuicklinkDrawer()

// Usuario edita en QuicklinkDrawer.vue
// Usuario guarda
quicklinkStore.saveActiveQuicklink()

Listar Quicklinks

const { getQuicklinksForSection, updateQuicklinksMap } = useQuicklinkStore()

// Cargar desde API
const quicklinks = await fetchQuicklinks(sectionId)
updateQuicklinksMap(sectionId, quicklinks)

// En template
const quicklinks = computed(() => getQuicklinksForSection(sectionId))

Componentes que lo Utilizan

  • QuicklinkDrawer.vue - Edición de quicklinks
  • QuickLinkItem.vue - Ítem individual
  • QuickLinksBlock.vue - Lista de quicklinks
  • QuicklinksSection.vue - Sección completa
  • Composable useQuicklinks()

🎯 Modal Store

Archivo: /stores/modal.ts
Export: useModalAlertStore()

Propósito

Gestionar modales de confirmación/alerta con callbacks personalizados.

Estado

const isVisible = ref(false)
const title = ref('')
const message = ref('')
const confirmValue = ref<any>(null)
const cancelValue = ref<any>(null)
const secondaryCta = ref('Cancel')        // Botón secundario
const errorCta = ref('Delete')            // Botón de acción principal
const onConfirm = ref<Function | undefined>()
const onCancel = ref<Function | undefined>()

Interfaz ModalOptions

interface ModalOptions {
  title?: string
  message?: string
  confirmValue?: any
  cancelValue?: any
  secondaryCta?: string           // Texto del botón "Cancel"
  errorCta?: string              // Texto del botón "Delete/Confirm"
  onConfirm?: (value?: any) => void
  onCancel?: (value?: any) => void
}

Acciones

Acción Descripción
showModal(options) Mostrar modal con configuración
hideModal() Ocultar modal y limpiar estado
confirm() Ejecutar callback de confirmación
cancel() Ejecutar callback de cancelación

Cuándo Usar

Confirmar Eliminación

const modalStore = useModalAlertStore()

const deleteTask = (task: Task) => {
  modalStore.showModal({
    title: 'Eliminar tarea',
    message: `¿Estás seguro de que deseas eliminar "${task.name}"?`,
    errorCta: 'Eliminar',
    secondaryCta: 'Cancelar',
    onConfirm: async () => {
      await api.deleteTask(task.id)
      alertsStore.setSuccess('Tarea eliminada')
    },
    onCancel: () => {
      alertsStore.setInfo('Operación cancelada')
    }
  })
}

Confirmar Acción Importante

const shareWorkspace = (workspace: Workspace) => {
  modalStore.showModal({
    title: 'Compartir workspace',
    message: `¿Compartir "${workspace.name}" con tu equipo?`,
    confirmValue: workspace.id,
    onConfirm: async (workspaceId) => {
      await api.shareWorkspace(workspaceId)
      alertsStore.setSuccess('Workspace compartido')
    }
  })
}

Modal Personalizado

modalStore.showModal({
  title: 'Confirmación',
  message: 'Mensaje personalizado',
  errorCta: 'Aceptar',
  secondaryCta: 'Rechazar',
  onConfirm: () => console.log('Confirmado'),
  onCancel: () => console.log('Cancelado')
})

Componentes que lo Utilizan

  • ModalAlert.vue - Renderiza el modal
  • Cualquier componente con acciones destructivas

📊 Dashboard Store

Archivo: /stores/dashboard.ts
Export: useDashboardStore()

Propósito

Gestionar el estado global del dashboard (workspace seleccionado, estado de carga).

Estado

const isDashboardLoading = ref<boolean>(false)
const selectedWorkspace = ref<number | null>(null)

Acciones

Acción Parámetros Descripción
setSelectedWorkspace() workspaceId: number Seleccionar workspace activo
activeDashboardLoading() - Marcar como cargando
deactiveDashboardLoading() - Terminar carga

Cuándo Usar

Cambiar Workspace

const dashboardStore = useDashboardStore()

const selectWorkspace = (workspaceId: number) => {
  dashboardStore.setSelectedWorkspace(workspaceId)
  // Recargar datos del dashboard
  fetchDashboardData(workspaceId)
}

Mostrar Loading Global

const dashboardStore = useDashboardStore()

const loadData = async () => {
  dashboardStore.activeDashboardLoading()
  try {
    await fetchAllData()
  } finally {
    dashboardStore.deactiveDashboardLoading()
  }
}

Verificar Workspace Seleccionado

const { selectedWorkspace } = useDashboardStore()

if (selectedWorkspace.value) {
  // Mostrar dashboard
}

Componentes que lo Utilizan

  • Dashboard principal
  • Componentes que dependen del workspace actual
  • Composables de datos

🗂️ Sidebar Store

Archivo: /stores/sidebar.ts
Export: useSidebarStore()

Propósito

Gestionar el estado colapsado/expandido del sidebar.

Estado

const isSidebarCollapsed = ref<boolean>(false)

Acciones

Acción Descripción
toogleIsSidebarCollapsed() Cambiar estado del sidebar

Cuándo Usar

Toggle Sidebar

const sidebarStore = useSidebarStore()

// Botón hamburguesa
const toggleSidebar = () => {
  sidebarStore.toogleIsSidebarCollapsed()
}

Aplicar Estilos Dinámicos

<template>
  <aside :class="{ collapsed: sidebarStore.isSidebarCollapsed }">
    <!-- contenido -->
  </aside>
</template>

<script setup lang="ts">
const sidebarStore = useSidebarStore()
</script>

Componentes que lo Utilizan

  • Layout principal
  • DrawerContainer.vue
  • Componentes responsivos

🔍 Filter Store

Archivo: /stores/filter.ts
Export: useFilterStore()

Propósito

Gestionar el texto de filtro/búsqueda global en la aplicación.

Estado

const filterText = ref('')

Acciones

Acción Parámetros Descripción
setFilterText() text: string Actualizar texto de búsqueda

Cuándo Usar

Input de Búsqueda

const filterStore = useFilterStore()

const onSearch = (query: string) => {
  filterStore.setFilterText(query)
}

Filtrar Items

const filteredTasks = computed(() => {
  const query = filterStore.filterText.toLowerCase()
  return tasks.value.filter(task => 
    task.name.toLowerCase().includes(query)
  )
})

Limpiar Búsqueda

const clearSearch = () => {
  filterStore.setFilterText('')
}

Componentes que lo Utilizan

  • Search/filter components
  • Listas filtradas de tareas y quicklinks

📋 Patrón de Uso General

Setup en un Componente

<script setup lang="ts">
import { useAlertsStore } from '~/stores/alerts'
import { useTaskStore } from '~/stores/task'
import { useModalAlertStore } from '~/stores/modal'

const alertsStore = useAlertsStore()
const taskStore = useTaskStore()
const modalStore = useModalAlertStore()

// Usar directamente en template o funciones
</script>

Setup en un Composable

// app/composables/useTasks.ts
import { useTaskStore } from '~/stores/task'
import { useAlertsStore } from '~/stores/alerts'

export const useTasks = () => {
  const taskStore = useTaskStore()
  const alertsStore = useAlertsStore()

  const createTask = async (data: CreateTaskDTO) => {
    try {
      const response = await $fetch('/api/tasks', {
        method: 'POST',
        body: data
      })
      taskStore.updateTasksMap(data.sectionId, response)
      alertsStore.setSuccess('Tarea creada')
    } catch (err) {
      alertsStore.setError('Error al crear tarea')
    }
  }

  return { createTask }
}

🎯 Best Practices

✅ DO's

  1. Usar stores para estado compartido

    // ✅ Correcto: Estado compartido en store
    const alertsStore = useAlertsStore()
    alertsStore.setSuccess('Guardado')
  2. Composables para lógica de negocio

    // ✅ Correcto: Lógica en composable, store en acciones
    export const useTasks = () => {
      const taskStore = useTaskStore()
      const fetchTasks = async () => { /* ... */ }
      return { fetchTasks }
    }
  3. Acciones para modificar estado

    // ✅ Correcto: Usar acciones del store
    taskStore.updateTasksMap(sectionId, tasks)

❌ DON'Ts

  1. No modificar estado directamente desde componentes

    // ❌ Evitar: Modificación directa
    taskStore.activeTask.value.name = 'nuevo nombre'
    
    // ✅ Mejor: Usar acciones
    taskStore.updateActiveTask({ name: 'nuevo nombre' })
  2. No duplicar estado

    // ❌ Evitar: Duplicar en local ref
    const tasks = ref([])
    
    // ✅ Mejor: Usar computed sobre el store
    const tasks = computed(() => taskStore.getTasksForSection(id))
  3. No hacer lógica compleja en stores

    // ❌ Evitar: Lógica de negocio en store
    const complexLogic = () => { /* ... */ }
    
    // ✅ Mejor: En composable
    export const useComplexLogic = () => { /* ... */ }

🔄 Flujo de Datos

Componente
    ↓
Composable (lógica)
    ↓
Store (estado)
    ↓
API / Backend
    ↓
Store (actualiza estado)
    ↓
Componente (re-renderiza)

Ejemplo Completo

// 1. Componente dispara acción
const { createTask } = useTasks()
await createTask(formData)

// 2. Composable hace llamada API
const response = await $fetch('/api/tasks', { method: 'POST', body: formData })

// 3. Store actualiza estado
taskStore.updateTasksMap(sectionId, response)

// 4. Alerta de éxito
alertsStore.setSuccess('Tarea creada')

// 5. Template reactivo se actualiza
const tasks = computed(() => taskStore.getTasksForSection(sectionId))

📊 Matriz de Stores

Store Propósito Datos Alcance Persistencia
Alerts Notificaciones Strings Global Session
Task Gestión de tareas Tasks[] Global Session
Quicklink Gestión de links Quicklinks[] Global Session
Modal Confirmaciones Config + callbacks Global Session
Dashboard Estado del dashboard workspace + loading Global Session
Sidebar UI del sidebar boolean Global Local Storage
Filter Búsqueda global string Global Session

🚀 Inicialización

Los stores se inicializan automáticamente cuando se utilizan en componentes o composables con useXxxStore().

En app.vue o layout principal

// No es necesario inicializar explícitamente
// Pinia se inicializa automáticamente con @pinia/nuxt

Configuración (nuxt.config.ts)

pinia: {
  storesDirs: ['./stores/**'],
},

Última actualización: 6 de enero de 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment