feat: vínculo con usuario Monday en landing + alta prellenada desde leads
API: - GET /api/monday/usuarios: usuarios activos no-guest del workspace - PATCH /api/usuarios/me/monday: vincula/desvincula el mondayUserId del usuario autenticado (valida contra el workspace) - GET /api/leads/buscar?q=: búsqueda de leads por nombre/teléfono/email (sin acentos, orden updatedAt desc, límite 15) - aPublico/me ahora incluyen mondayUserId Landing (/): - Nueva sección 'Configuración de cuenta': login inline, selector del usuario de Monday, guardar/desvincular, cerrar sesión Alta de clientes (/client): - Búsqueda de leads de Monday con debounce en la pantalla de entrada; al elegir uno prellena nombre/email/celular y guarda leadMondayId - El picker (borradores + alta nueva + búsqueda de lead) es ahora siempre la pantalla de entrada Tests: api 294, landing 8, alta-clientes 96 — todos verdes
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
import {
|
import {
|
||||||
|
buscarLeads,
|
||||||
|
buscarLeadsEndpoint,
|
||||||
cambiarEstadoLead,
|
cambiarEstadoLead,
|
||||||
crearLead,
|
crearLead,
|
||||||
descartarLead,
|
descartarLead,
|
||||||
@@ -131,3 +133,55 @@ describe('descartarLead (POST /api/leads/:itemId/descartar)', () => {
|
|||||||
expect(monday.motivos).toEqual([])
|
expect(monday.motivos).toEqual([])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('buscarLeads (GET /api/leads/buscar)', () => {
|
||||||
|
const lead = (overrides: Partial<LeadMonday>): LeadMonday => ({
|
||||||
|
id: 'lead-1',
|
||||||
|
nombre: 'Lead Prueba',
|
||||||
|
createdAt: '2026-08-01T00:00:00Z',
|
||||||
|
updatedAt: '2026-08-01T00:00:00Z',
|
||||||
|
estado: 'NUEVO',
|
||||||
|
origen: '',
|
||||||
|
telefono: '',
|
||||||
|
email: '',
|
||||||
|
vendedorIds: [],
|
||||||
|
...overrides,
|
||||||
|
})
|
||||||
|
|
||||||
|
it('matchea por nombre, teléfono y email (case-insensitive)', () => {
|
||||||
|
const leads = [
|
||||||
|
lead({ id: 'a', nombre: 'MARÍA FERNANDA LÓPEZ' }),
|
||||||
|
lead({ id: 'b', telefono: '477 123 4567' }),
|
||||||
|
lead({ id: 'c', email: 'Cliente@Correo.com' }),
|
||||||
|
lead({ id: 'd', nombre: 'Otro', telefono: '333', email: '' }),
|
||||||
|
]
|
||||||
|
expect(buscarLeads(leads, 'fernanda').map((l) => l.id)).toEqual(['a'])
|
||||||
|
expect(buscarLeads(leads, '123').map((l) => l.id)).toEqual(['b'])
|
||||||
|
expect(buscarLeads(leads, 'cliente@').map((l) => l.id)).toEqual(['c'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ignora acentos tanto en el lead como en la búsqueda', () => {
|
||||||
|
const leads = [lead({ id: 'a', nombre: 'MARÍA FERNANDA LÓPEZ', email: 'andres@correo.com' })]
|
||||||
|
expect(buscarLeads(leads, 'maria')).toHaveLength(1)
|
||||||
|
expect(buscarLeads(leads, 'andrés')).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ordena por updatedAt descendente y limita a 15', () => {
|
||||||
|
const leads = Array.from({ length: 20 }, (_, i) =>
|
||||||
|
lead({ id: `l-${i}`, nombre: `Cliente Común ${i}`, updatedAt: `2026-08-${String(i + 1).padStart(2, '0')}T00:00:00Z` }),
|
||||||
|
)
|
||||||
|
const r = buscarLeads(leads, 'común')
|
||||||
|
expect(r).toHaveLength(15)
|
||||||
|
expect(r[0]?.id).toBe('l-19') // el más reciente primero
|
||||||
|
})
|
||||||
|
|
||||||
|
it('el endpoint responde 400 con q de menos de 2 caracteres', async () => {
|
||||||
|
const res = { json: vi.fn(), status: vi.fn() } as unknown as Parameters<typeof buscarLeadsEndpoint>[1]
|
||||||
|
await expect(
|
||||||
|
buscarLeadsEndpoint({ query: { q: 'a' } } as unknown as Parameters<typeof buscarLeadsEndpoint>[0], res),
|
||||||
|
).rejects.toMatchObject({ status: 400 })
|
||||||
|
await expect(
|
||||||
|
buscarLeadsEndpoint({ query: {} } as unknown as Parameters<typeof buscarLeadsEndpoint>[0], res),
|
||||||
|
).rejects.toMatchObject({ status: 400 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -35,6 +35,29 @@ const descartarSchema = z.object({
|
|||||||
comentario: z.string().trim().min(1).optional(),
|
comentario: z.string().trim().min(1).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ---------- Búsqueda de leads (prellenado del alta de clientes) ----------
|
||||||
|
|
||||||
|
const LIMITE_BUSQUEDA_LEADS = 15
|
||||||
|
|
||||||
|
/** Minúsculas y sin diacríticos, para comparar nombres/teléfonos con acentos */
|
||||||
|
const sinAcentos = (s: string): string =>
|
||||||
|
s
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[̀-ͯ]/g, '') // diacríticos U+0300–U+036F
|
||||||
|
.toLowerCase()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filtra leads por q en nombre/teléfono/email (case-insensitive, sin acentos),
|
||||||
|
* ordena por actualización reciente y limita a 15 resultados.
|
||||||
|
*/
|
||||||
|
export function buscarLeads(leads: LeadMonday[], q: string): LeadMonday[] {
|
||||||
|
const needle = sinAcentos(q)
|
||||||
|
return leads
|
||||||
|
.filter((l) => [l.nombre, l.telefono, l.email].some((campo) => sinAcentos(campo).includes(needle)))
|
||||||
|
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
||||||
|
.slice(0, LIMITE_BUSQUEDA_LEADS)
|
||||||
|
}
|
||||||
|
|
||||||
type MondayLeads = Pick<
|
type MondayLeads = Pick<
|
||||||
MondayService,
|
MondayService,
|
||||||
| 'cambiarValorColumna'
|
| 'cambiarValorColumna'
|
||||||
@@ -144,3 +167,23 @@ export async function descartarLeadEndpoint(req: Request, res: Response): Promis
|
|||||||
const motivoItemId = await descartarLead(mondayService, itemId, body.motivo, body.comentario)
|
const motivoItemId = await descartarLead(mondayService, itemId, body.motivo, body.comentario)
|
||||||
res.json({ itemId, estado: 'DESCARTADO', motivo: body.motivo, motivoItemId })
|
res.json({ itemId, estado: 'DESCARTADO', motivo: body.motivo, motivoItemId })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/leads/buscar?q=texto — autocompletado para prellenar el alta de
|
||||||
|
* clientes desde un lead. q mínimo 2 caracteres; devuelve máximo 15.
|
||||||
|
*/
|
||||||
|
export async function buscarLeadsEndpoint(req: Request, res: Response): Promise<void> {
|
||||||
|
const q = String(req.query['q'] ?? '').trim()
|
||||||
|
if (q.length < 2) throw new AppError(400, 'La búsqueda requiere al menos 2 caracteres')
|
||||||
|
const encontrados = buscarLeads(await mondayService.getLeads(), q)
|
||||||
|
res.json(
|
||||||
|
encontrados.map((l) => ({
|
||||||
|
id: l.id,
|
||||||
|
nombre: l.nombre,
|
||||||
|
telefono: l.telefono,
|
||||||
|
email: l.email,
|
||||||
|
estado: l.estado,
|
||||||
|
origen: l.origen,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
139
api-gateway/src/controllers/usuarios.controller.test.ts
Normal file
139
api-gateway/src/controllers/usuarios.controller.test.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import type { Request, Response } from 'express'
|
||||||
|
import type { Usuario } from '@prisma/client'
|
||||||
|
import {
|
||||||
|
actualizarVinculoMondayEndpoint,
|
||||||
|
listarUsuariosMondayEndpoint,
|
||||||
|
} from './usuarios.controller.js'
|
||||||
|
import { AppError } from '../utils/errors.js'
|
||||||
|
|
||||||
|
// ---------- Monday y Prisma mockeados (los tests no tocan red ni BD) ----------
|
||||||
|
|
||||||
|
const mondayMock = vi.hoisted(() => ({ getUsuariosWorkspace: vi.fn() }))
|
||||||
|
const prismaMock = vi.hoisted(() => ({ usuario: { update: vi.fn() } }))
|
||||||
|
|
||||||
|
vi.mock('../services/monday.service.js', () => ({ mondayService: mondayMock }))
|
||||||
|
vi.mock('../services/prisma.js', () => ({ prisma: prismaMock }))
|
||||||
|
|
||||||
|
const USUARIOS_WORKSPACE = [
|
||||||
|
{ id: 13676, nombre: 'Erick Peña', email: 'erick@frasa.mx' },
|
||||||
|
{ id: 20400, nombre: 'Cristina Barrera', email: 'cristina@frasa.mx' },
|
||||||
|
]
|
||||||
|
|
||||||
|
function usuarioFake(overrides: Partial<Usuario> = {}): Usuario {
|
||||||
|
return {
|
||||||
|
id: 'u-1',
|
||||||
|
username: 'erick israel pena ramos',
|
||||||
|
nombre: 'ERICK ISRAEL PEÑA RAMOS',
|
||||||
|
passwordHash: 'hash',
|
||||||
|
rol: 'vendedor',
|
||||||
|
asignacion: 'MONTEBELLO',
|
||||||
|
empleadoId: 13676,
|
||||||
|
mondayUserId: null,
|
||||||
|
activo: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
...overrides,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reqFake(overrides: Partial<Request> = {}): Request {
|
||||||
|
return {
|
||||||
|
body: {},
|
||||||
|
usuarioToken: { sub: 'u-1', username: 'erick israel pena ramos', rol: 'vendedor' },
|
||||||
|
...overrides,
|
||||||
|
} as Request
|
||||||
|
}
|
||||||
|
|
||||||
|
function resFake() {
|
||||||
|
const json = vi.fn()
|
||||||
|
const status = vi.fn().mockReturnValue({ json })
|
||||||
|
return { res: { json, status } as unknown as Response, status, json }
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
mondayMock.getUsuariosWorkspace.mockResolvedValue(USUARIOS_WORKSPACE)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('listarUsuariosMondayEndpoint (GET /api/monday/usuarios)', () => {
|
||||||
|
it('devuelve los usuarios del workspace con id numérico', async () => {
|
||||||
|
const { res, json } = resFake()
|
||||||
|
|
||||||
|
await listarUsuariosMondayEndpoint(reqFake(), res)
|
||||||
|
|
||||||
|
expect(json).toHaveBeenCalledWith([
|
||||||
|
{ id: 13676, nombre: 'Erick Peña', email: 'erick@frasa.mx' },
|
||||||
|
{ id: 20400, nombre: 'Cristina Barrera', email: 'cristina@frasa.mx' },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('propaga el error si Monday falla (el error handler responde 5xx)', async () => {
|
||||||
|
mondayMock.getUsuariosWorkspace.mockRejectedValue(new Error('Monday caído'))
|
||||||
|
const { res } = resFake()
|
||||||
|
await expect(listarUsuariosMondayEndpoint(reqFake(), res)).rejects.toThrow('Monday caído')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('actualizarVinculoMondayEndpoint (PATCH /api/usuarios/me/monday)', () => {
|
||||||
|
it('con id válido guarda el vínculo y devuelve el perfil actualizado', async () => {
|
||||||
|
prismaMock.usuario.update.mockResolvedValue(usuarioFake({ mondayUserId: 13676 }))
|
||||||
|
const { res, json } = resFake()
|
||||||
|
|
||||||
|
await actualizarVinculoMondayEndpoint(reqFake({ body: { mondayUserId: 13676 } }), res)
|
||||||
|
|
||||||
|
expect(prismaMock.usuario.update).toHaveBeenCalledWith({
|
||||||
|
where: { id: 'u-1' },
|
||||||
|
data: { mondayUserId: 13676 },
|
||||||
|
})
|
||||||
|
expect(json).toHaveBeenCalledWith({
|
||||||
|
nombre: 'ERICK ISRAEL PEÑA RAMOS',
|
||||||
|
rol: 'vendedor',
|
||||||
|
asignacion: 'MONTEBELLO',
|
||||||
|
mondayUserId: 13676,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('con id inexistente en el workspace responde 400 y no toca la BD', async () => {
|
||||||
|
const { res } = resFake()
|
||||||
|
|
||||||
|
const error = await actualizarVinculoMondayEndpoint(
|
||||||
|
reqFake({ body: { mondayUserId: 99999 } }),
|
||||||
|
res,
|
||||||
|
).catch((e: unknown) => e)
|
||||||
|
|
||||||
|
expect(error).toBeInstanceOf(AppError)
|
||||||
|
expect(error).toMatchObject({ status: 400 })
|
||||||
|
expect(prismaMock.usuario.update).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('con null desvincula sin consultar Monday', async () => {
|
||||||
|
prismaMock.usuario.update.mockResolvedValue(usuarioFake({ mondayUserId: null }))
|
||||||
|
const { res, json } = resFake()
|
||||||
|
|
||||||
|
await actualizarVinculoMondayEndpoint(reqFake({ body: { mondayUserId: null } }), res)
|
||||||
|
|
||||||
|
expect(mondayMock.getUsuariosWorkspace).not.toHaveBeenCalled()
|
||||||
|
expect(prismaMock.usuario.update).toHaveBeenCalledWith({
|
||||||
|
where: { id: 'u-1' },
|
||||||
|
data: { mondayUserId: null },
|
||||||
|
})
|
||||||
|
expect(json).toHaveBeenCalledWith(expect.objectContaining({ mondayUserId: null }))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sin token verificado: 400', async () => {
|
||||||
|
const { res } = resFake()
|
||||||
|
await expect(
|
||||||
|
actualizarVinculoMondayEndpoint(reqFake({ usuarioToken: undefined }), res),
|
||||||
|
).rejects.toThrow('Falta el token verificado')
|
||||||
|
expect(prismaMock.usuario.update).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('body inválido (string): ZodError (el error handler lo traduce a 400)', async () => {
|
||||||
|
const { res } = resFake()
|
||||||
|
await expect(
|
||||||
|
actualizarVinculoMondayEndpoint(reqFake({ body: { mondayUserId: '13676' } }), res),
|
||||||
|
).rejects.toThrow()
|
||||||
|
expect(prismaMock.usuario.update).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
44
api-gateway/src/controllers/usuarios.controller.ts
Normal file
44
api-gateway/src/controllers/usuarios.controller.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import type { Request, Response } from 'express'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { mondayService } from '../services/monday.service.js'
|
||||||
|
import { prisma } from '../services/prisma.js'
|
||||||
|
import { aPublico } from '../services/auth.service.js'
|
||||||
|
import { BadRequestError } from '../utils/errors.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vínculo usuario del ecosistema ↔ usuario de Monday (people). Es el mapeo
|
||||||
|
* que alimenta round-robin, mis-leads, actividades y alertas; el usuario lo
|
||||||
|
* elige él mismo desde la landing (Configuración de cuenta).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** GET /api/monday/usuarios (Bearer) → usuarios activos del workspace */
|
||||||
|
export async function listarUsuariosMondayEndpoint(_req: Request, res: Response): Promise<void> {
|
||||||
|
const usuarios = await mondayService.getUsuariosWorkspace()
|
||||||
|
res.json(usuarios.map((u) => ({ id: u.id, nombre: u.nombre, email: u.email })))
|
||||||
|
}
|
||||||
|
|
||||||
|
const vinculoMondaySchema = z.object({
|
||||||
|
mondayUserId: z.number().int().positive().nullable(),
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH /api/usuarios/me/monday (Bearer) { mondayUserId: number | null }
|
||||||
|
* null desvincula. Un id que no existe en el workspace → 400.
|
||||||
|
*/
|
||||||
|
export async function actualizarVinculoMondayEndpoint(req: Request, res: Response): Promise<void> {
|
||||||
|
if (!req.usuarioToken) throw new BadRequestError('Falta el token verificado')
|
||||||
|
const { mondayUserId } = vinculoMondaySchema.parse(req.body)
|
||||||
|
|
||||||
|
if (mondayUserId !== null) {
|
||||||
|
const usuarios = await mondayService.getUsuariosWorkspace()
|
||||||
|
if (!usuarios.some((u) => u.id === mondayUserId)) {
|
||||||
|
throw new BadRequestError(`El usuario de Monday ${mondayUserId} no existe en el workspace`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const usuario = await prisma.usuario.update({
|
||||||
|
where: { id: req.usuarioToken.sub },
|
||||||
|
data: { mondayUserId },
|
||||||
|
})
|
||||||
|
res.json(aPublico(usuario))
|
||||||
|
}
|
||||||
@@ -45,8 +45,13 @@ import { correrRecordatorios, listarRecordatorios } from '../controllers/recorda
|
|||||||
import {
|
import {
|
||||||
cambiarEstadoLeadEndpoint,
|
cambiarEstadoLeadEndpoint,
|
||||||
crearLeadEndpoint,
|
crearLeadEndpoint,
|
||||||
|
buscarLeadsEndpoint,
|
||||||
descartarLeadEndpoint,
|
descartarLeadEndpoint,
|
||||||
} from '../controllers/leads.controller.js'
|
} from '../controllers/leads.controller.js'
|
||||||
|
import {
|
||||||
|
actualizarVinculoMondayEndpoint,
|
||||||
|
listarUsuariosMondayEndpoint,
|
||||||
|
} from '../controllers/usuarios.controller.js'
|
||||||
import { resumenMiDiaEndpoint } from '../controllers/mi-dia.controller.js'
|
import { resumenMiDiaEndpoint } from '../controllers/mi-dia.controller.js'
|
||||||
import { webhookMonday } from '../controllers/webhooks.controller.js'
|
import { webhookMonday } from '../controllers/webhooks.controller.js'
|
||||||
import {
|
import {
|
||||||
@@ -121,10 +126,15 @@ apiRouter.post('/oportunidades/:mondayItemId/perder', perderOportunidadEndpoint)
|
|||||||
|
|
||||||
apiRouter.get('/mi-dia/resumen', resumenMiDiaEndpoint)
|
apiRouter.get('/mi-dia/resumen', resumenMiDiaEndpoint)
|
||||||
|
|
||||||
|
// GET estático: va ANTES de /leads/:itemId para no colisionar con el parámetro
|
||||||
|
apiRouter.get('/leads/buscar', buscarLeadsEndpoint)
|
||||||
apiRouter.post('/leads', crearLeadEndpoint)
|
apiRouter.post('/leads', crearLeadEndpoint)
|
||||||
apiRouter.post('/leads/:itemId/estado', cambiarEstadoLeadEndpoint)
|
apiRouter.post('/leads/:itemId/estado', cambiarEstadoLeadEndpoint)
|
||||||
apiRouter.post('/leads/:itemId/descartar', descartarLeadEndpoint)
|
apiRouter.post('/leads/:itemId/descartar', descartarLeadEndpoint)
|
||||||
|
|
||||||
|
apiRouter.get('/monday/usuarios', listarUsuariosMondayEndpoint)
|
||||||
|
apiRouter.patch('/usuarios/me/monday', actualizarVinculoMondayEndpoint)
|
||||||
|
|
||||||
apiRouter.get('/reportes/resumen', resumenReportes)
|
apiRouter.get('/reportes/resumen', resumenReportes)
|
||||||
|
|
||||||
apiRouter.post('/webhooks/monday', webhookMonday)
|
apiRouter.post('/webhooks/monday', webhookMonday)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ function usuarioFake(overrides: Partial<Usuario> = {}): Usuario {
|
|||||||
rol: 'vendedor',
|
rol: 'vendedor',
|
||||||
asignacion: 'MONTEBELLO',
|
asignacion: 'MONTEBELLO',
|
||||||
empleadoId: 13676,
|
empleadoId: 13676,
|
||||||
|
mondayUserId: null,
|
||||||
activo: true,
|
activo: true,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
@@ -52,6 +53,7 @@ describe('login', () => {
|
|||||||
nombre: 'ERICK ISRAEL PEÑA RAMOS',
|
nombre: 'ERICK ISRAEL PEÑA RAMOS',
|
||||||
rol: 'vendedor',
|
rol: 'vendedor',
|
||||||
asignacion: 'MONTEBELLO',
|
asignacion: 'MONTEBELLO',
|
||||||
|
mondayUserId: null,
|
||||||
})
|
})
|
||||||
expect(verificarToken(resultado.token).username).toBe('erick israel pena ramos')
|
expect(verificarToken(resultado.token).username).toBe('erick israel pena ramos')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ export interface UsuarioPublico {
|
|||||||
nombre: string
|
nombre: string
|
||||||
rol: string
|
rol: string
|
||||||
asignacion: string | null
|
asignacion: string | null
|
||||||
|
// User ID de Monday vinculado (null = sin vincular; ver PATCH /usuarios/me/monday)
|
||||||
|
mondayUserId: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginResult {
|
export interface LoginResult {
|
||||||
@@ -30,7 +32,12 @@ export function normalizarUsername(nombre: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function aPublico(usuario: Usuario): UsuarioPublico {
|
export function aPublico(usuario: Usuario): UsuarioPublico {
|
||||||
return { nombre: usuario.nombre, rol: usuario.rol, asignacion: usuario.asignacion }
|
return {
|
||||||
|
nombre: usuario.nombre,
|
||||||
|
rol: usuario.rol,
|
||||||
|
asignacion: usuario.asignacion,
|
||||||
|
mondayUserId: usuario.mondayUserId,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
682
apps/alta-clientes/package-lock.json
generated
682
apps/alta-clientes/package-lock.json
generated
@@ -23,15 +23,39 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"@testing-library/dom": "^10.4.1",
|
||||||
|
"@testing-library/react": "^16.3.3",
|
||||||
"@types/react": "^19.0.2",
|
"@types/react": "^19.0.2",
|
||||||
"@types/react-dom": "^19.0.2",
|
"@types/react-dom": "^19.0.2",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"jsdom": "^26.1.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"typescript": "~5.6.3",
|
"typescript": "~5.6.3",
|
||||||
"vite": "^6.0.7",
|
"vite": "^6.0.7",
|
||||||
"vitest": "^2.1.8"
|
"vitest": "^2.1.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@asamuzakjp/css-color": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@csstools/css-calc": "^2.1.3",
|
||||||
|
"@csstools/css-color-parser": "^3.0.9",
|
||||||
|
"@csstools/css-parser-algorithms": "^3.0.4",
|
||||||
|
"@csstools/css-tokenizer": "^3.0.3",
|
||||||
|
"lru-cache": "^10.4.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
|
||||||
|
"version": "10.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.29.7",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||||
@@ -323,6 +347,121 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@csstools/color-helpers": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT-0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-calc": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||||
|
"@csstools/css-tokenizer": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-color-parser": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@csstools/color-helpers": "^5.1.0",
|
||||||
|
"@csstools/css-calc": "^2.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||||
|
"@csstools/css-tokenizer": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-parser-algorithms": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-tokenizer": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-tokenizer": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||||
@@ -1479,6 +1618,61 @@
|
|||||||
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@testing-library/dom": {
|
||||||
|
"version": "10.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
||||||
|
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/code-frame": "^7.10.4",
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@types/aria-query": "^5.0.1",
|
||||||
|
"aria-query": "5.3.0",
|
||||||
|
"dom-accessibility-api": "^0.5.9",
|
||||||
|
"lz-string": "^1.5.0",
|
||||||
|
"picocolors": "1.1.1",
|
||||||
|
"pretty-format": "^27.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@testing-library/react": {
|
||||||
|
"version": "16.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz",
|
||||||
|
"integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@testing-library/dom": "^10.0.0",
|
||||||
|
"@types/react": "^18.0.0 || ^19.0.0",
|
||||||
|
"@types/react-dom": "^18.0.0 || ^19.0.0",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/aria-query": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -1671,6 +1865,49 @@
|
|||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/agent-base": {
|
||||||
|
"version": "7.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
|
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-styles": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aria-query": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"dequal": "^2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/assertion-error": {
|
"node_modules/assertion-error": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||||
@@ -1900,6 +2137,20 @@
|
|||||||
"utrie": "^1.0.2"
|
"utrie": "^1.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cssstyle": {
|
||||||
|
"version": "4.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
|
||||||
|
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@asamuzakjp/css-color": "^3.2.0",
|
||||||
|
"rrweb-cssom": "^0.8.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
@@ -1907,6 +2158,57 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/data-urls": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-mimetype": "^4.0.0",
|
||||||
|
"whatwg-url": "^14.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/data-urls/node_modules/tr46": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "^2.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/data-urls/node_modules/webidl-conversions": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/data-urls/node_modules/whatwg-url": {
|
||||||
|
"version": "14.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
||||||
|
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "^5.1.0",
|
||||||
|
"webidl-conversions": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -1925,6 +2227,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decimal.js": {
|
||||||
|
"version": "10.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||||
|
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/deep-eql": {
|
"node_modules/deep-eql": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
||||||
@@ -1935,6 +2244,16 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dequal": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
@@ -1945,6 +2264,13 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-accessibility-api": {
|
||||||
|
"version": "0.5.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||||
|
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/dompurify": {
|
"node_modules/dompurify": {
|
||||||
"version": "2.5.9",
|
"version": "2.5.9",
|
||||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.9.tgz",
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.9.tgz",
|
||||||
@@ -1973,6 +2299,19 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-module-lexer": {
|
"node_modules/es-module-lexer": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||||
@@ -2120,6 +2459,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/html-encoding-sniffer": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-encoding": "^3.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/html2canvas": {
|
"node_modules/html2canvas": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||||
@@ -2134,12 +2486,60 @@
|
|||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/http-proxy-agent": {
|
||||||
|
"version": "7.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||||
|
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.0",
|
||||||
|
"debug": "^4.3.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/https-proxy-agent": {
|
||||||
|
"version": "7.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||||
|
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.2",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/idb-keyval": {
|
"node_modules/idb-keyval": {
|
||||||
"version": "6.3.0",
|
"version": "6.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.3.0.tgz",
|
||||||
"integrity": "sha512-um+2dgAWmYsu615EXpWVwSmapJhON0G43t3Ka/EVaohzPQXSMqKEqeDK/oIW3Ow+BXaF2PvSc+oBTFp793A5Ow==",
|
"integrity": "sha512-um+2dgAWmYsu615EXpWVwSmapJhON0G43t3Ka/EVaohzPQXSMqKEqeDK/oIW3Ow+BXaF2PvSc+oBTFp793A5Ow==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/is-potential-custom-element-name": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-url": {
|
"node_modules/is-url": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
|
||||||
@@ -2162,6 +2562,83 @@
|
|||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/jsdom": {
|
||||||
|
"version": "26.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
|
||||||
|
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cssstyle": "^4.2.1",
|
||||||
|
"data-urls": "^5.0.0",
|
||||||
|
"decimal.js": "^10.5.0",
|
||||||
|
"html-encoding-sniffer": "^4.0.0",
|
||||||
|
"http-proxy-agent": "^7.0.2",
|
||||||
|
"https-proxy-agent": "^7.0.6",
|
||||||
|
"is-potential-custom-element-name": "^1.0.1",
|
||||||
|
"nwsapi": "^2.2.16",
|
||||||
|
"parse5": "^7.2.1",
|
||||||
|
"rrweb-cssom": "^0.8.0",
|
||||||
|
"saxes": "^6.0.0",
|
||||||
|
"symbol-tree": "^3.2.4",
|
||||||
|
"tough-cookie": "^5.1.1",
|
||||||
|
"w3c-xmlserializer": "^5.0.0",
|
||||||
|
"webidl-conversions": "^7.0.0",
|
||||||
|
"whatwg-encoding": "^3.1.1",
|
||||||
|
"whatwg-mimetype": "^4.0.0",
|
||||||
|
"whatwg-url": "^14.1.1",
|
||||||
|
"ws": "^8.18.0",
|
||||||
|
"xml-name-validator": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"canvas": "^3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"canvas": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsdom/node_modules/tr46": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "^2.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsdom/node_modules/webidl-conversions": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsdom/node_modules/whatwg-url": {
|
||||||
|
"version": "14.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
||||||
|
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "^5.1.0",
|
||||||
|
"webidl-conversions": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jsesc": {
|
"node_modules/jsesc": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||||
@@ -2505,6 +2982,16 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lz-string": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"lz-string": "bin/bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
@@ -2571,6 +3058,13 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nwsapi": {
|
||||||
|
"version": "2.2.25",
|
||||||
|
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.25.tgz",
|
||||||
|
"integrity": "sha512-kktSgNDIbyEIs/LrE6dtEMnfzxSDpXViFl6c4gFjz/CgtnL4GDCR3wyZXzjvAXsNB8dXU4dAfvIOgLMH2/vwLg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
@@ -2589,6 +3083,19 @@
|
|||||||
"opencollective-postinstall": "index.js"
|
"opencollective-postinstall": "index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/parse5": {
|
||||||
|
"version": "7.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||||
|
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"entities": "^6.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pathe": {
|
"node_modules/pathe": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
|
||||||
@@ -2662,6 +3169,28 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pretty-format": {
|
||||||
|
"version": "27.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||||
|
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1",
|
||||||
|
"ansi-styles": "^5.0.0",
|
||||||
|
"react-is": "^17.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pretty-format/node_modules/react-is": {
|
||||||
|
"version": "17.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
@@ -2673,6 +3202,16 @@
|
|||||||
"react-is": "^16.13.1"
|
"react-is": "^16.13.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/punycode": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/raf": {
|
"node_modules/raf": {
|
||||||
"version": "3.4.1",
|
"version": "3.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||||
@@ -2883,6 +3422,33 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rrweb-cssom": {
|
||||||
|
"version": "0.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
||||||
|
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/saxes": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"xmlchars": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v12.22.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/scheduler": {
|
"node_modules/scheduler": {
|
||||||
"version": "0.27.0",
|
"version": "0.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||||
@@ -2962,6 +3528,13 @@
|
|||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/symbol-tree": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.3.3",
|
"version": "4.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
|
||||||
@@ -3078,6 +3651,39 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tldts": {
|
||||||
|
"version": "6.1.86",
|
||||||
|
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
|
||||||
|
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tldts-core": "^6.1.86"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"tldts": "bin/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tldts-core": {
|
||||||
|
"version": "6.1.86",
|
||||||
|
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
|
||||||
|
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tough-cookie": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"tldts": "^6.1.32"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tr46": {
|
"node_modules/tr46": {
|
||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
@@ -4322,6 +4928,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/w3c-xmlserializer": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"xml-name-validator": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/wasm-feature-detect": {
|
"node_modules/wasm-feature-detect": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.9.0.tgz",
|
||||||
@@ -4334,6 +4953,30 @@
|
|||||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/whatwg-encoding": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||||
|
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": "0.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-mimetype": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/whatwg-url": {
|
"node_modules/whatwg-url": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
@@ -4361,6 +5004,45 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.21.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
|
||||||
|
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xml-name-validator": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlchars": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
|
|||||||
@@ -26,9 +26,12 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"@testing-library/dom": "^10.4.1",
|
||||||
|
"@testing-library/react": "^16.3.3",
|
||||||
"@types/react": "^19.0.2",
|
"@types/react": "^19.0.2",
|
||||||
"@types/react-dom": "^19.0.2",
|
"@types/react-dom": "^19.0.2",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"jsdom": "^26.1.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"typescript": "~5.6.3",
|
"typescript": "~5.6.3",
|
||||||
"vite": "^6.0.7",
|
"vite": "^6.0.7",
|
||||||
|
|||||||
69
apps/alta-clientes/src/components/BusquedaLead.test.tsx
Normal file
69
apps/alta-clientes/src/components/BusquedaLead.test.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||||
|
import BusquedaLead from './BusquedaLead'
|
||||||
|
import type { LeadResumen } from '../services/api'
|
||||||
|
|
||||||
|
type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
||||||
|
|
||||||
|
const LEADS: LeadResumen[] = [
|
||||||
|
{ id: 'lead-1', nombre: 'MARÍA FERNANDA LÓPEZ', telefono: '4771234567', email: 'maria@correo.com', estado: 'NUEVO', origen: 'Web' },
|
||||||
|
{ id: 'lead-2', nombre: 'Juan Pérez', telefono: '', email: 'juan@correo.com', estado: 'CONTACTADO', origen: '' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const respuestaOk = (data: unknown) =>
|
||||||
|
new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } })
|
||||||
|
|
||||||
|
let fetchMock: ReturnType<typeof vi.fn<FetchFn>>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fetchMock = vi.fn<FetchFn>().mockResolvedValue(respuestaOk(LEADS))
|
||||||
|
vi.stubGlobal('fetch', fetchMock)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup()
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
})
|
||||||
|
|
||||||
|
const llamadasBusqueda = () => fetchMock.mock.calls.filter(([url]) => String(url).includes('/leads/buscar'))
|
||||||
|
|
||||||
|
describe('BusquedaLead', () => {
|
||||||
|
it('con menos de 2 caracteres no llama al API', async () => {
|
||||||
|
render(<BusquedaLead onSeleccion={() => undefined} />)
|
||||||
|
fireEvent.change(screen.getByLabelText(/Buscar lead/), { target: { value: 'm' } })
|
||||||
|
await new Promise((r) => setTimeout(r, 400)) // más que el debounce
|
||||||
|
expect(llamadasBusqueda()).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renderiza los resultados de la búsqueda (nombre, teléfono, estado)', async () => {
|
||||||
|
render(<BusquedaLead onSeleccion={() => undefined} />)
|
||||||
|
fireEvent.change(screen.getByLabelText(/Buscar lead/), { target: { value: 'maria' } })
|
||||||
|
|
||||||
|
expect(await screen.findByText('MARÍA FERNANDA LÓPEZ')).toBeTruthy()
|
||||||
|
expect(screen.getByText(/4771234567 · NUEVO/)).toBeTruthy()
|
||||||
|
expect(screen.getByText('Juan Pérez')).toBeTruthy()
|
||||||
|
const [url] = llamadasBusqueda()[0] as [string]
|
||||||
|
expect(url).toContain('/leads/buscar?q=maria')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('al elegir un resultado avisa con el lead completo', async () => {
|
||||||
|
const onSeleccion = vi.fn()
|
||||||
|
render(<BusquedaLead onSeleccion={onSeleccion} />)
|
||||||
|
fireEvent.change(screen.getByLabelText(/Buscar lead/), { target: { value: 'maria' } })
|
||||||
|
|
||||||
|
fireEvent.click(await screen.findByText('MARÍA FERNANDA LÓPEZ'))
|
||||||
|
expect(onSeleccion).toHaveBeenCalledWith(LEADS[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('muestra "sin resultados" y errores de red', async () => {
|
||||||
|
render(<BusquedaLead onSeleccion={() => undefined} />)
|
||||||
|
|
||||||
|
fetchMock.mockResolvedValueOnce(respuestaOk([]))
|
||||||
|
fireEvent.change(screen.getByLabelText(/Buscar lead/), { target: { value: 'nadie' } })
|
||||||
|
expect(await screen.findByText('Sin leads que coincidan.')).toBeTruthy()
|
||||||
|
|
||||||
|
fetchMock.mockRejectedValueOnce(new Error('red caída'))
|
||||||
|
fireEvent.change(screen.getByLabelText(/Buscar lead/), { target: { value: 'otro' } })
|
||||||
|
expect((await screen.findByRole('alert')).textContent).toContain('No se pudo buscar en Monday')
|
||||||
|
})
|
||||||
|
})
|
||||||
92
apps/alta-clientes/src/components/BusquedaLead.tsx
Normal file
92
apps/alta-clientes/src/components/BusquedaLead.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { buscarLeads, type LeadResumen } from '../services/api'
|
||||||
|
|
||||||
|
const DEBOUNCE_MS = 300
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onSeleccion: (lead: LeadResumen) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buscador con debounce de leads de Monday (GET /leads/buscar). No llama al
|
||||||
|
* API con menos de 2 caracteres; al elegir un resultado avisa con onSeleccion.
|
||||||
|
*/
|
||||||
|
export default function BusquedaLead({ onSeleccion }: Props) {
|
||||||
|
const [q, setQ] = useState('')
|
||||||
|
const [resultados, setResultados] = useState<LeadResumen[] | null>(null)
|
||||||
|
const [buscando, setBuscando] = useState(false)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const texto = q.trim()
|
||||||
|
if (texto.length < 2) {
|
||||||
|
setResultados(null)
|
||||||
|
setError('')
|
||||||
|
setBuscando(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setBuscando(true)
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
buscarLeads(texto)
|
||||||
|
.then((lista) => {
|
||||||
|
setResultados(lista)
|
||||||
|
setError('')
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setResultados(null)
|
||||||
|
setError('No se pudo buscar en Monday. Intenta de nuevo.')
|
||||||
|
})
|
||||||
|
.finally(() => setBuscando(false))
|
||||||
|
}, DEBOUNCE_MS)
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}, [q])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="busqueda-lead"
|
||||||
|
className="mb-1 block text-xs font-bold uppercase tracking-widest text-neutral-500"
|
||||||
|
>
|
||||||
|
Buscar lead (nombre, teléfono o email)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="busqueda-lead"
|
||||||
|
type="search"
|
||||||
|
value={q}
|
||||||
|
onChange={(e) => setQ(e.target.value)}
|
||||||
|
placeholder="Escribe al menos 2 caracteres…"
|
||||||
|
className="w-full max-w-md border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 focus:border-neutral-900"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{buscando && <p className="mt-3 text-xs font-light text-neutral-500">Buscando…</p>}
|
||||||
|
{error && (
|
||||||
|
<p role="alert" className="mt-3 text-xs text-red-700">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{!buscando && !error && resultados !== null && resultados.length === 0 && (
|
||||||
|
<p className="mt-3 text-xs font-light text-neutral-500">Sin leads que coincidan.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{resultados !== null && resultados.length > 0 && (
|
||||||
|
<ul className="mt-3 max-w-md divide-y divide-neutral-200 border border-neutral-200 bg-white">
|
||||||
|
{resultados.map((lead) => (
|
||||||
|
<li key={lead.id}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onSeleccion(lead)}
|
||||||
|
className="flex w-full flex-wrap items-baseline justify-between gap-2 px-4 py-3 text-left transition-colors duration-150 hover:bg-neutral-50"
|
||||||
|
>
|
||||||
|
<span className="text-sm text-neutral-900">{lead.nombre}</span>
|
||||||
|
<span className="text-xs font-light text-neutral-500">
|
||||||
|
{lead.telefono || 'sin teléfono'}
|
||||||
|
{lead.estado ? ` · ${lead.estado}` : ''}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -57,6 +57,7 @@ const economicosVacios = (): AltaCliente['economicos'] => ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const estadoInicial = (): Omit<AltaCliente, 'documentos'> => ({
|
const estadoInicial = (): Omit<AltaCliente, 'documentos'> => ({
|
||||||
|
leadMondayId: null,
|
||||||
datosGenerales: datosGeneralesVacios(),
|
datosGenerales: datosGeneralesVacios(),
|
||||||
empleo: empleoVacio(),
|
empleo: empleoVacio(),
|
||||||
economicos: economicosVacios(),
|
economicos: economicosVacios(),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const FIRMA_PRUEBA =
|
|||||||
* (PASCUAL MARTIN TELLEZ CHIGUINDO — pdf_formatos.txt)
|
* (PASCUAL MARTIN TELLEZ CHIGUINDO — pdf_formatos.txt)
|
||||||
*/
|
*/
|
||||||
export const fixtureAlta: AltaCliente = {
|
export const fixtureAlta: AltaCliente = {
|
||||||
|
leadMondayId: null,
|
||||||
datosGenerales: {
|
datosGenerales: {
|
||||||
campaniaVenta: '',
|
campaniaVenta: '',
|
||||||
fecha: '2026-07-19',
|
fecha: '2026-07-19',
|
||||||
|
|||||||
25
apps/alta-clientes/src/lib/leadAlta.test.ts
Normal file
25
apps/alta-clientes/src/lib/leadAlta.test.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { leadAPrellenado } from './leadAlta'
|
||||||
|
|
||||||
|
describe('leadAPrellenado', () => {
|
||||||
|
it('prellena nombre (mayúsculas), email y celular cuando el lead los trae', () => {
|
||||||
|
const pre = leadAPrellenado({ nombre: ' María Fernanda López ', email: 'maria@correo.com', telefono: '4771234567' })
|
||||||
|
expect(pre).toEqual({
|
||||||
|
nombreCompleto: 'MARÍA FERNANDA LÓPEZ',
|
||||||
|
email: 'maria@correo.com',
|
||||||
|
celular: '4771234567',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('omite email y celular cuando el lead no los tiene (no inventa datos)', () => {
|
||||||
|
expect(leadAPrellenado({ nombre: 'Lead Sin Contacto', email: '', telefono: '' })).toEqual({
|
||||||
|
nombreCompleto: 'LEAD SIN CONTACTO',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('prellena solo lo que haya (email sin teléfono)', () => {
|
||||||
|
const pre = leadAPrellenado({ nombre: 'Solo Email', email: 'solo@correo.com', telefono: '' })
|
||||||
|
expect(pre).toEqual({ nombreCompleto: 'SOLO EMAIL', email: 'solo@correo.com' })
|
||||||
|
expect(pre.celular).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
16
apps/alta-clientes/src/lib/leadAlta.ts
Normal file
16
apps/alta-clientes/src/lib/leadAlta.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { DatosGenerales } from '../types'
|
||||||
|
import type { LeadResumen } from '../services/api'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transformación pura lead de Monday → campos del wizard de alta.
|
||||||
|
* Prellena solo lo que el lead tenga (nombre en mayúsculas, como el formato).
|
||||||
|
*/
|
||||||
|
export function leadAPrellenado(
|
||||||
|
lead: Pick<LeadResumen, 'nombre' | 'email' | 'telefono'>,
|
||||||
|
): Partial<DatosGenerales> {
|
||||||
|
return {
|
||||||
|
nombreCompleto: lead.nombre.trim().toUpperCase(),
|
||||||
|
...(lead.email ? { email: lead.email.trim() } : {}),
|
||||||
|
...(lead.telefono ? { celular: lead.telefono.trim() } : {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
57
apps/alta-clientes/src/pages/AltaClientePage.test.tsx
Normal file
57
apps/alta-clientes/src/pages/AltaClientePage.test.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||||
|
import AltaClientePage from './AltaClientePage'
|
||||||
|
import type { LeadResumen } from '../services/api'
|
||||||
|
|
||||||
|
type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
||||||
|
|
||||||
|
const LEAD: LeadResumen = {
|
||||||
|
id: 'lead-9',
|
||||||
|
nombre: 'María Fernanda López',
|
||||||
|
telefono: '4771234567',
|
||||||
|
email: 'maria@correo.com',
|
||||||
|
estado: 'NUEVO',
|
||||||
|
origen: 'Web',
|
||||||
|
}
|
||||||
|
|
||||||
|
const respuestaOk = (data: unknown) =>
|
||||||
|
new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } })
|
||||||
|
|
||||||
|
let fetchMock: ReturnType<typeof vi.fn<FetchFn>>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fetchMock = vi.fn<FetchFn>().mockImplementation((input) => {
|
||||||
|
const url = String(input)
|
||||||
|
if (url.includes('/borradores')) {
|
||||||
|
// Un borrador para que la pantalla inicial sea el picker
|
||||||
|
return Promise.resolve(
|
||||||
|
respuestaOk([{ id: 'b-1', nombre: 'Borrador previo', paso: 0, updatedAt: '2026-08-20T10:00:00Z' }]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (url.includes('/leads/buscar')) return Promise.resolve(respuestaOk([LEAD]))
|
||||||
|
return Promise.reject(new Error(`fetch inesperado: ${url}`))
|
||||||
|
})
|
||||||
|
vi.stubGlobal('fetch', fetchMock)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup()
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('AltaClientePage — alta desde lead de Monday', () => {
|
||||||
|
it('al elegir un lead el wizard arranca con datosGenerales prellenados', async () => {
|
||||||
|
render(<AltaClientePage />)
|
||||||
|
|
||||||
|
// Picker visible con la sección de búsqueda
|
||||||
|
const busqueda = await screen.findByLabelText(/Buscar lead/)
|
||||||
|
fireEvent.change(busqueda, { target: { value: 'maria' } })
|
||||||
|
fireEvent.click(await screen.findByText('María Fernanda López'))
|
||||||
|
|
||||||
|
// Wizard paso 1 con los campos del lead ya capturados
|
||||||
|
const nombre = (await screen.findByLabelText(/Nombre completo/)) as HTMLInputElement
|
||||||
|
expect(nombre.value).toBe('MARÍA FERNANDA LÓPEZ')
|
||||||
|
expect((screen.getByLabelText(/Correo electrónico|Email/i) as HTMLInputElement).value).toBe('maria@correo.com')
|
||||||
|
expect((screen.getByLabelText(/Celular/i) as HTMLInputElement).value).toBe('4771234567')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -19,8 +19,11 @@ import {
|
|||||||
obtenerCotizacion,
|
obtenerCotizacion,
|
||||||
subirDocumentos,
|
subirDocumentos,
|
||||||
type BorradorResumen,
|
type BorradorResumen,
|
||||||
|
type LeadResumen,
|
||||||
} from '../services/api'
|
} from '../services/api'
|
||||||
import { cotizacionAPrellenado } from '../lib/cotizacionAlta'
|
import { cotizacionAPrellenado } from '../lib/cotizacionAlta'
|
||||||
|
import { leadAPrellenado } from '../lib/leadAlta'
|
||||||
|
import BusquedaLead from '../components/BusquedaLead'
|
||||||
|
|
||||||
/** Lee ?cotizacion=<id> del query string o del hash (#cotizacion=<id>) */
|
/** Lee ?cotizacion=<id> del query string o del hash (#cotizacion=<id>) */
|
||||||
function paramCotizacion(): string | null {
|
function paramCotizacion(): string | null {
|
||||||
@@ -82,7 +85,9 @@ export default function AltaClientePage() {
|
|||||||
listarBorradores()
|
listarBorradores()
|
||||||
.then((lista) => {
|
.then((lista) => {
|
||||||
setBorradores(lista)
|
setBorradores(lista)
|
||||||
setPantalla(lista.length > 0 ? 'picker' : 'wizard')
|
// El picker es siempre la entrada: además de los borradores ofrece
|
||||||
|
// "Empezar nuevo" y la búsqueda de leads de Monday.
|
||||||
|
setPantalla('picker')
|
||||||
})
|
})
|
||||||
.catch(() => setPantalla('wizard')) // sin conexión: el wizard funciona igual
|
.catch(() => setPantalla('wizard')) // sin conexión: el wizard funciona igual
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -103,11 +108,9 @@ export default function AltaClientePage() {
|
|||||||
const borrarBorrador = async (id: string) => {
|
const borrarBorrador = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
await eliminarBorrador(id)
|
await eliminarBorrador(id)
|
||||||
setBorradores((prev) => {
|
// Al borrar el último borrador el picker sigue siendo la pantalla de
|
||||||
const restantes = prev.filter((b) => b.id !== id)
|
// entrada (ofrece "Empezar nuevo" y la búsqueda de leads).
|
||||||
if (restantes.length === 0) setPantalla('wizard')
|
setBorradores((prev) => prev.filter((b) => b.id !== id))
|
||||||
return restantes
|
|
||||||
})
|
|
||||||
} catch {
|
} catch {
|
||||||
setErrorCarga('No se pudo eliminar el borrador.')
|
setErrorCarga('No se pudo eliminar el borrador.')
|
||||||
}
|
}
|
||||||
@@ -120,6 +123,16 @@ export default function AltaClientePage() {
|
|||||||
setPantalla('wizard')
|
setPantalla('wizard')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Alta nueva prellenada desde un lead de Monday (nombre, email, celular)
|
||||||
|
const empezarDesdeLead = (lead: LeadResumen) => {
|
||||||
|
reiniciar()
|
||||||
|
setBorradorId(null)
|
||||||
|
setCargoDeBorrador(false)
|
||||||
|
actualizar('datosGenerales', { ...datos.datosGenerales, ...leadAPrellenado(lead) })
|
||||||
|
actualizar('leadMondayId', lead.id)
|
||||||
|
setPantalla('wizard')
|
||||||
|
}
|
||||||
|
|
||||||
const guardar = async () => {
|
const guardar = async () => {
|
||||||
try {
|
try {
|
||||||
const b = await guardarBorrador({ ...(borradorId ? { id: borradorId } : {}), paso, datos })
|
const b = await guardarBorrador({ ...(borradorId ? { id: borradorId } : {}), paso, datos })
|
||||||
@@ -193,39 +206,43 @@ export default function AltaClientePage() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className="mx-auto max-w-6xl px-4 py-16">
|
<main className="mx-auto max-w-6xl px-4 py-16">
|
||||||
<h2 className="text-xs font-bold uppercase tracking-widest text-neutral-500">
|
{borradores.length > 0 && (
|
||||||
Tienes {borradores.length} borrador{borradores.length === 1 ? '' : 'es'} guardado
|
<>
|
||||||
{borradores.length === 1 ? '' : 's'}
|
<h2 className="text-xs font-bold uppercase tracking-widest text-neutral-500">
|
||||||
</h2>
|
Tienes {borradores.length} borrador{borradores.length === 1 ? '' : 'es'} guardado
|
||||||
|
{borradores.length === 1 ? '' : 's'}
|
||||||
|
</h2>
|
||||||
|
<ul className="mt-6 divide-y divide-neutral-200 border border-neutral-200 bg-white">
|
||||||
|
{borradores.map((b) => (
|
||||||
|
<li key={b.id} className="flex flex-wrap items-center justify-between gap-4 px-4 py-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="truncate text-sm text-neutral-900">{b.nombre}</p>
|
||||||
|
<p className="mt-1 text-xs font-light text-neutral-500">
|
||||||
|
Paso {b.paso + 1} de {PASOS.length} · {formatoFechaHora(b.updatedAt)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex shrink-0 gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void continuarBorrador(b.id)}
|
||||||
|
className="border border-accent bg-white px-3 py-1.5 text-xs font-bold uppercase tracking-widest text-accent transition-colors duration-150 hover:bg-accent hover:text-white"
|
||||||
|
>
|
||||||
|
Continuar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void borrarBorrador(b.id)}
|
||||||
|
className="border border-red-300 bg-white px-3 py-1.5 text-xs font-bold uppercase tracking-widest text-red-700 transition-colors duration-150 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
Eliminar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{errorCarga && <p className="mt-4 text-xs text-red-700">{errorCarga}</p>}
|
{errorCarga && <p className="mt-4 text-xs text-red-700">{errorCarga}</p>}
|
||||||
<ul className="mt-6 divide-y divide-neutral-200 border border-neutral-200 bg-white">
|
|
||||||
{borradores.map((b) => (
|
|
||||||
<li key={b.id} className="flex flex-wrap items-center justify-between gap-4 px-4 py-3">
|
|
||||||
<div className="min-w-0">
|
|
||||||
<p className="truncate text-sm text-neutral-900">{b.nombre}</p>
|
|
||||||
<p className="mt-1 text-xs font-light text-neutral-500">
|
|
||||||
Paso {b.paso + 1} de {PASOS.length} · {formatoFechaHora(b.updatedAt)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex shrink-0 gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => void continuarBorrador(b.id)}
|
|
||||||
className="border border-accent bg-white px-3 py-1.5 text-xs font-bold uppercase tracking-widest text-accent transition-colors duration-150 hover:bg-accent hover:text-white"
|
|
||||||
>
|
|
||||||
Continuar
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => void borrarBorrador(b.id)}
|
|
||||||
className="border border-red-300 bg-white px-3 py-1.5 text-xs font-bold uppercase tracking-widest text-red-700 transition-colors duration-150 hover:bg-red-50"
|
|
||||||
>
|
|
||||||
Eliminar
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={empezarNuevo}
|
onClick={empezarNuevo}
|
||||||
@@ -233,6 +250,18 @@ export default function AltaClientePage() {
|
|||||||
>
|
>
|
||||||
Empezar nuevo
|
Empezar nuevo
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<section aria-labelledby="titulo-lead" className="mt-12 border-t border-neutral-200 pt-8">
|
||||||
|
<h3 id="titulo-lead" className="text-xs font-bold uppercase tracking-widest text-neutral-500">
|
||||||
|
Desde un lead de Monday
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-xs font-light text-neutral-500">
|
||||||
|
Empieza un alta nueva con el nombre, email y celular del lead ya capturados.
|
||||||
|
</p>
|
||||||
|
<div className="mt-4">
|
||||||
|
<BusquedaLead onSeleccion={empezarDesdeLead} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -164,6 +164,25 @@ export async function obtenerCotizacion(id: string): Promise<import('../lib/coti
|
|||||||
return (await res.json()) as import('../lib/cotizacionAlta').CotizacionParaAlta
|
return (await res.json()) as import('../lib/cotizacionAlta').CotizacionParaAlta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- Pre-llenado desde lead de Monday (auth) ----------
|
||||||
|
|
||||||
|
export interface LeadResumen {
|
||||||
|
id: string
|
||||||
|
nombre: string
|
||||||
|
telefono: string
|
||||||
|
email: string
|
||||||
|
estado: string
|
||||||
|
origen: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /leads/buscar?q= — autocompletado para prellenar el alta desde un lead */
|
||||||
|
export async function buscarLeads(q: string): Promise<LeadResumen[]> {
|
||||||
|
const res = await fetch(`${API_URL}/leads/buscar?q=${encodeURIComponent(q)}`, conAuth())
|
||||||
|
manejar401(res)
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
return (await res.json()) as LeadResumen[]
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- Liga remota de firma (auth) ----------
|
// ---------- Liga remota de firma (auth) ----------
|
||||||
|
|
||||||
export interface LigaRemota {
|
export interface LigaRemota {
|
||||||
|
|||||||
@@ -146,6 +146,9 @@ export interface DatosFirma {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AltaCliente {
|
export interface AltaCliente {
|
||||||
|
// ID del item del tablero Leads de Monday cuando el alta se prellenó desde
|
||||||
|
// un lead (null = captura manual). Passthrough hasta payloadJson del API.
|
||||||
|
leadMondayId: string | null
|
||||||
datosGenerales: DatosGenerales
|
datosGenerales: DatosGenerales
|
||||||
empleo: DatosEmpleo
|
empleo: DatosEmpleo
|
||||||
economicos: DatosEconomicos
|
economicos: DatosEconomicos
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { defineConfig } from 'vitest/config'
|
|||||||
// Config de tests separada para no mezclar los tipos de Vite y Vitest
|
// Config de tests separada para no mezclar los tipos de Vite y Vitest
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
environment: 'node',
|
// jsdom para los tests de componentes; los tests puros corren igual
|
||||||
|
environment: 'jsdom',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
655
apps/landing/package-lock.json
generated
655
apps/landing/package-lock.json
generated
@@ -14,10 +14,13 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"@testing-library/dom": "^10.4.1",
|
||||||
|
"@testing-library/react": "^16.3.3",
|
||||||
"@types/react": "^19.0.2",
|
"@types/react": "^19.0.2",
|
||||||
"@types/react-dom": "^19.0.2",
|
"@types/react-dom": "^19.0.2",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"@vitest/coverage-v8": "^2.1.8",
|
"@vitest/coverage-v8": "^2.1.8",
|
||||||
|
"jsdom": "^26.1.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"typescript": "~5.6.3",
|
"typescript": "~5.6.3",
|
||||||
"vite": "^6.0.7",
|
"vite": "^6.0.7",
|
||||||
@@ -38,6 +41,27 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@asamuzakjp/css-color": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@csstools/css-calc": "^2.1.3",
|
||||||
|
"@csstools/css-color-parser": "^3.0.9",
|
||||||
|
"@csstools/css-parser-algorithms": "^3.0.4",
|
||||||
|
"@csstools/css-tokenizer": "^3.0.3",
|
||||||
|
"lru-cache": "^10.4.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
|
||||||
|
"version": "10.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.29.7",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||||
@@ -272,6 +296,16 @@
|
|||||||
"@babel/core": "^7.0.0-0"
|
"@babel/core": "^7.0.0-0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.29.7",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
||||||
@@ -327,6 +361,121 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@csstools/color-helpers": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT-0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-calc": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||||
|
"@csstools/css-tokenizer": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-color-parser": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@csstools/color-helpers": "^5.1.0",
|
||||||
|
"@csstools/css-calc": "^2.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||||
|
"@csstools/css-tokenizer": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-parser-algorithms": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-tokenizer": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-tokenizer": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||||
@@ -1513,6 +1662,61 @@
|
|||||||
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@testing-library/dom": {
|
||||||
|
"version": "10.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
||||||
|
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/code-frame": "^7.10.4",
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@types/aria-query": "^5.0.1",
|
||||||
|
"aria-query": "5.3.0",
|
||||||
|
"dom-accessibility-api": "^0.5.9",
|
||||||
|
"lz-string": "^1.5.0",
|
||||||
|
"picocolors": "1.1.1",
|
||||||
|
"pretty-format": "^27.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@testing-library/react": {
|
||||||
|
"version": "16.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz",
|
||||||
|
"integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@testing-library/dom": "^10.0.0",
|
||||||
|
"@types/react": "^18.0.0 || ^19.0.0",
|
||||||
|
"@types/react-dom": "^18.0.0 || ^19.0.0",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/aria-query": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -1725,6 +1929,16 @@
|
|||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/agent-base": {
|
||||||
|
"version": "7.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
|
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "6.3.0",
|
"version": "6.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz",
|
||||||
@@ -1751,6 +1965,16 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/aria-query": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"dequal": "^2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/assertion-error": {
|
"node_modules/assertion-error": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||||
@@ -1931,6 +2155,20 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cssstyle": {
|
||||||
|
"version": "4.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
|
||||||
|
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@asamuzakjp/css-color": "^3.2.0",
|
||||||
|
"rrweb-cssom": "^0.8.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
@@ -1938,6 +2176,20 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/data-urls": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-mimetype": "^4.0.0",
|
||||||
|
"whatwg-url": "^14.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -1956,6 +2208,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decimal.js": {
|
||||||
|
"version": "10.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||||
|
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/deep-eql": {
|
"node_modules/deep-eql": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
||||||
@@ -1966,6 +2225,16 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dequal": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
@@ -1976,6 +2245,13 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-accessibility-api": {
|
||||||
|
"version": "0.5.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||||
|
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/eastasianwidth": {
|
"node_modules/eastasianwidth": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||||
@@ -2011,6 +2287,19 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-module-lexer": {
|
"node_modules/es-module-lexer": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||||
@@ -2222,6 +2511,19 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html-encoding-sniffer": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-encoding": "^3.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/html-escaper": {
|
"node_modules/html-escaper": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||||
@@ -2229,6 +2531,47 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/http-proxy-agent": {
|
||||||
|
"version": "7.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||||
|
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.0",
|
||||||
|
"debug": "^4.3.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/https-proxy-agent": {
|
||||||
|
"version": "7.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||||
|
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.2",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-fullwidth-code-point": {
|
"node_modules/is-fullwidth-code-point": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
@@ -2239,6 +2582,13 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-potential-custom-element-name": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/isexe": {
|
"node_modules/isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
@@ -2333,6 +2683,46 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/jsdom": {
|
||||||
|
"version": "26.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
|
||||||
|
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cssstyle": "^4.2.1",
|
||||||
|
"data-urls": "^5.0.0",
|
||||||
|
"decimal.js": "^10.5.0",
|
||||||
|
"html-encoding-sniffer": "^4.0.0",
|
||||||
|
"http-proxy-agent": "^7.0.2",
|
||||||
|
"https-proxy-agent": "^7.0.6",
|
||||||
|
"is-potential-custom-element-name": "^1.0.1",
|
||||||
|
"nwsapi": "^2.2.16",
|
||||||
|
"parse5": "^7.2.1",
|
||||||
|
"rrweb-cssom": "^0.8.0",
|
||||||
|
"saxes": "^6.0.0",
|
||||||
|
"symbol-tree": "^3.2.4",
|
||||||
|
"tough-cookie": "^5.1.1",
|
||||||
|
"w3c-xmlserializer": "^5.0.0",
|
||||||
|
"webidl-conversions": "^7.0.0",
|
||||||
|
"whatwg-encoding": "^3.1.1",
|
||||||
|
"whatwg-mimetype": "^4.0.0",
|
||||||
|
"whatwg-url": "^14.1.1",
|
||||||
|
"ws": "^8.18.0",
|
||||||
|
"xml-name-validator": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"canvas": "^3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"canvas": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jsesc": {
|
"node_modules/jsesc": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||||
@@ -2637,6 +3027,16 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lz-string": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"lz-string": "bin/bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
@@ -2750,6 +3150,13 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nwsapi": {
|
||||||
|
"version": "2.2.25",
|
||||||
|
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.25.tgz",
|
||||||
|
"integrity": "sha512-kktSgNDIbyEIs/LrE6dtEMnfzxSDpXViFl6c4gFjz/CgtnL4GDCR3wyZXzjvAXsNB8dXU4dAfvIOgLMH2/vwLg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/package-json-from-dist": {
|
"node_modules/package-json-from-dist": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||||
@@ -2757,6 +3164,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BlueOak-1.0.0"
|
"license": "BlueOak-1.0.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/parse5": {
|
||||||
|
"version": "7.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||||
|
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"entities": "^6.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-key": {
|
"node_modules/path-key": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||||
@@ -2857,6 +3277,54 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pretty-format": {
|
||||||
|
"version": "27.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||||
|
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1",
|
||||||
|
"ansi-styles": "^5.0.0",
|
||||||
|
"react-is": "^17.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pretty-format/node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pretty-format/node_modules/ansi-styles": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/punycode": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.2.8",
|
"version": "19.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
||||||
@@ -2878,6 +3346,13 @@
|
|||||||
"react": "^19.2.8"
|
"react": "^19.2.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-is": {
|
||||||
|
"version": "17.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.17.0",
|
"version": "0.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||||
@@ -2934,6 +3409,33 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rrweb-cssom": {
|
||||||
|
"version": "0.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
||||||
|
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/saxes": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"xmlchars": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v12.22.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/scheduler": {
|
"node_modules/scheduler": {
|
||||||
"version": "0.27.0",
|
"version": "0.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||||
@@ -3134,6 +3636,13 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/symbol-tree": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.3.3",
|
"version": "4.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
|
||||||
@@ -3231,6 +3740,52 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tldts": {
|
||||||
|
"version": "6.1.86",
|
||||||
|
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
|
||||||
|
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tldts-core": "^6.1.86"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"tldts": "bin/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tldts-core": {
|
||||||
|
"version": "6.1.86",
|
||||||
|
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
|
||||||
|
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tough-cookie": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"tldts": "^6.1.32"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "^2.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.6.3",
|
"version": "5.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
||||||
@@ -4447,6 +5002,67 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/w3c-xmlserializer": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"xml-name-validator": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/webidl-conversions": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-encoding": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||||
|
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": "0.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-mimetype": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-url": {
|
||||||
|
"version": "14.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
||||||
|
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "^5.1.0",
|
||||||
|
"webidl-conversions": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
@@ -4578,6 +5194,45 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.21.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
|
||||||
|
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xml-name-validator": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlchars": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
|
|||||||
@@ -17,10 +17,13 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"@testing-library/dom": "^10.4.1",
|
||||||
|
"@testing-library/react": "^16.3.3",
|
||||||
"@types/react": "^19.0.2",
|
"@types/react": "^19.0.2",
|
||||||
"@types/react-dom": "^19.0.2",
|
"@types/react-dom": "^19.0.2",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"@vitest/coverage-v8": "^2.1.8",
|
"@vitest/coverage-v8": "^2.1.8",
|
||||||
|
"jsdom": "^26.1.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"typescript": "~5.6.3",
|
"typescript": "~5.6.3",
|
||||||
"vite": "^6.0.7",
|
"vite": "^6.0.7",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { MODULOS } from './data/modulos'
|
import { MODULOS } from './data/modulos'
|
||||||
import { TarjetaModulo } from './components/TarjetaModulo'
|
import { TarjetaModulo } from './components/TarjetaModulo'
|
||||||
|
import { ConfiguracionCuenta } from './components/ConfiguracionCuenta'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
@@ -19,6 +20,8 @@ export default function App() {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<ConfiguracionCuenta />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer className="px-6 pt-24 pb-10 sm:px-12 lg:px-24">
|
<footer className="px-6 pt-24 pb-10 sm:px-12 lg:px-24">
|
||||||
|
|||||||
134
apps/landing/src/components/ConfiguracionCuenta.test.tsx
Normal file
134
apps/landing/src/components/ConfiguracionCuenta.test.tsx
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import { ConfiguracionCuenta } from './ConfiguracionCuenta'
|
||||||
|
|
||||||
|
type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
||||||
|
|
||||||
|
/** Stub sencillo de localStorage (jsdom no lo expone como global con vitest 2) */
|
||||||
|
function crearStorageStub(): Storage {
|
||||||
|
const mapa = new Map<string, string>()
|
||||||
|
return {
|
||||||
|
getItem: (k: string) => mapa.get(k) ?? null,
|
||||||
|
setItem: (k: string, v: string) => void mapa.set(k, v),
|
||||||
|
removeItem: (k: string) => void mapa.delete(k),
|
||||||
|
clear: () => mapa.clear(),
|
||||||
|
key: () => null,
|
||||||
|
get length() {
|
||||||
|
return mapa.size
|
||||||
|
},
|
||||||
|
} as Storage
|
||||||
|
}
|
||||||
|
|
||||||
|
const USUARIOS_MONDAY = [
|
||||||
|
{ id: 13676, nombre: 'Erick Peña', email: 'erick@frasa.mx' },
|
||||||
|
{ id: 20400, nombre: 'Cristina Barrera', email: 'cristina@frasa.mx' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const sesionFake = {
|
||||||
|
token: 'jwt-de-prueba',
|
||||||
|
usuario: { nombre: 'ERICK ISRAEL PEÑA RAMOS', rol: 'vendedor', asignacion: 'MONTEBELLO', mondayUserId: null },
|
||||||
|
}
|
||||||
|
|
||||||
|
const respuestaOk = (data: unknown) =>
|
||||||
|
new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } })
|
||||||
|
|
||||||
|
let fetchMock: ReturnType<typeof vi.fn<FetchFn>>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fetchMock = vi.fn<FetchFn>()
|
||||||
|
vi.stubGlobal('fetch', fetchMock)
|
||||||
|
vi.stubGlobal('localStorage', crearStorageStub())
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup()
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ConfiguracionCuenta sin sesión', () => {
|
||||||
|
it('muestra el formulario de login y no llama al API', () => {
|
||||||
|
render(<ConfiguracionCuenta />)
|
||||||
|
expect(screen.getByRole('heading', { name: 'Configuración de cuenta' })).toBeTruthy()
|
||||||
|
expect(screen.getByLabelText('Usuario')).toBeTruthy()
|
||||||
|
expect(screen.getByLabelText('Contraseña')).toBeTruthy()
|
||||||
|
expect(fetchMock).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('login fallido muestra el mensaje de error', async () => {
|
||||||
|
fetchMock.mockResolvedValue(new Response('{}', { status: 401 }))
|
||||||
|
render(<ConfiguracionCuenta />)
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText('Usuario'), { target: { value: 'nadie' } })
|
||||||
|
fireEvent.change(screen.getByLabelText('Contraseña'), { target: { value: 'mala1' } })
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Entrar' }))
|
||||||
|
|
||||||
|
expect((await screen.findByRole('alert')).textContent).toContain('Usuario o contraseña incorrectos')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ConfiguracionCuenta con sesión', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.setItem('frasa_auth', JSON.stringify(sesionFake))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('muestra el perfil, el dropdown de usuarios Monday y guarda el vínculo con PATCH', async () => {
|
||||||
|
fetchMock.mockImplementation((input, init) => {
|
||||||
|
const url = String(input)
|
||||||
|
if (url.endsWith('/auth/me')) return Promise.resolve(respuestaOk(sesionFake.usuario))
|
||||||
|
if (url.endsWith('/monday/usuarios')) return Promise.resolve(respuestaOk(USUARIOS_MONDAY))
|
||||||
|
if (url.endsWith('/usuarios/me/monday') && init?.method === 'PATCH') {
|
||||||
|
return Promise.resolve(respuestaOk({ ...sesionFake.usuario, mondayUserId: 13676 }))
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(`fetch inesperado: ${url}`))
|
||||||
|
})
|
||||||
|
|
||||||
|
render(<ConfiguracionCuenta />)
|
||||||
|
|
||||||
|
// Perfil y dropdown cargados
|
||||||
|
expect(await screen.findByText('ERICK ISRAEL PEÑA RAMOS')).toBeTruthy()
|
||||||
|
const dropdown = await screen.findByLabelText('Tu usuario de Monday')
|
||||||
|
expect(screen.getByText('sin vincular')).toBeTruthy()
|
||||||
|
|
||||||
|
fireEvent.change(dropdown, { target: { value: '13676' } })
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Guardar' }))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const llamadaPatch = fetchMock.mock.calls.find(
|
||||||
|
([url, init]) => String(url).endsWith('/usuarios/me/monday') && init?.method === 'PATCH',
|
||||||
|
)
|
||||||
|
expect(llamadaPatch).toBeTruthy()
|
||||||
|
expect(JSON.parse(String(llamadaPatch?.[1]?.body))).toEqual({ mondayUserId: 13676 })
|
||||||
|
})
|
||||||
|
expect((await screen.findByRole('status')).textContent).toContain('Vínculo con Monday guardado')
|
||||||
|
// El PATCH fue con Bearer
|
||||||
|
const [, initPatch] = fetchMock.mock.calls.find(
|
||||||
|
([url, init]) => String(url).endsWith('/usuarios/me/monday') && init?.method === 'PATCH',
|
||||||
|
) as [string, RequestInit]
|
||||||
|
expect((initPatch.headers as Record<string, string>)['Authorization']).toBe('Bearer jwt-de-prueba')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('si falla /monday/usuarios muestra error reintentable y el reintento carga la lista', async () => {
|
||||||
|
let intentos = 0
|
||||||
|
fetchMock.mockImplementation((input) => {
|
||||||
|
const url = String(input)
|
||||||
|
if (url.endsWith('/auth/me')) return Promise.resolve(respuestaOk(sesionFake.usuario))
|
||||||
|
if (url.endsWith('/monday/usuarios')) {
|
||||||
|
intentos += 1
|
||||||
|
return intentos === 1
|
||||||
|
? Promise.resolve(new Response('{}', { status: 500 }))
|
||||||
|
: Promise.resolve(respuestaOk(USUARIOS_MONDAY))
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(`fetch inesperado: ${url}`))
|
||||||
|
})
|
||||||
|
|
||||||
|
render(<ConfiguracionCuenta />)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await screen.findByText('No se pudo cargar la lista de usuarios de Monday.'),
|
||||||
|
).toBeTruthy()
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Reintentar' }))
|
||||||
|
expect(await screen.findByLabelText('Tu usuario de Monday')).toBeTruthy()
|
||||||
|
expect(intentos).toBe(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
251
apps/landing/src/components/ConfiguracionCuenta.tsx
Normal file
251
apps/landing/src/components/ConfiguracionCuenta.tsx
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
import { useEffect, useState, type FormEvent } from 'react'
|
||||||
|
import { ErrorLogin, getSesion, login, logout, type UsuarioSesion } from '../services/auth'
|
||||||
|
import { getMe, getUsuariosMonday, guardarVinculoMonday, type UsuarioMonday } from '../services/api'
|
||||||
|
|
||||||
|
const labelEstilo = 'block text-xs font-bold tracking-widest text-neutral-500 uppercase'
|
||||||
|
const inputEstilo =
|
||||||
|
'mt-2 w-full border border-neutral-200 bg-white px-3 py-2 text-sm text-neutral-900 focus:border-neutral-900'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sección "Configuración de cuenta": login inline y, con sesión, el vínculo
|
||||||
|
* con el usuario de Monday (alimenta round-robin, mis-leads y alertas).
|
||||||
|
*/
|
||||||
|
export function ConfiguracionCuenta() {
|
||||||
|
const [conSesion, setConSesion] = useState(() => getSesion() !== null)
|
||||||
|
|
||||||
|
// ---------- Login ----------
|
||||||
|
const [username, setUsername] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [entrando, setEntrando] = useState(false)
|
||||||
|
const [errorLogin, setErrorLogin] = useState('')
|
||||||
|
|
||||||
|
// ---------- Perfil + usuarios Monday ----------
|
||||||
|
const [perfil, setPerfil] = useState<UsuarioSesion | null>(null)
|
||||||
|
const [cargandoPerfil, setCargandoPerfil] = useState(false)
|
||||||
|
const [usuarios, setUsuarios] = useState<UsuarioMonday[] | null>(null)
|
||||||
|
const [errorUsuarios, setErrorUsuarios] = useState('')
|
||||||
|
const [seleccion, setSeleccion] = useState('')
|
||||||
|
const [guardando, setGuardando] = useState(false)
|
||||||
|
const [aviso, setAviso] = useState('')
|
||||||
|
const [errorGuardar, setErrorGuardar] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!conSesion) return
|
||||||
|
setCargandoPerfil(true)
|
||||||
|
getMe()
|
||||||
|
.then((u) => {
|
||||||
|
setPerfil(u)
|
||||||
|
setSeleccion(u.mondayUserId !== null ? String(u.mondayUserId) : '')
|
||||||
|
})
|
||||||
|
.catch(() => setErrorGuardar('No se pudo cargar tu perfil.'))
|
||||||
|
.finally(() => setCargandoPerfil(false))
|
||||||
|
cargarUsuarios()
|
||||||
|
}, [conSesion])
|
||||||
|
|
||||||
|
const cargarUsuarios = () => {
|
||||||
|
setErrorUsuarios('')
|
||||||
|
getUsuariosMonday()
|
||||||
|
.then(setUsuarios)
|
||||||
|
.catch(() => {
|
||||||
|
setUsuarios(null)
|
||||||
|
setErrorUsuarios('No se pudo cargar la lista de usuarios de Monday.')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const entrar = async (e: FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setEntrando(true)
|
||||||
|
setErrorLogin('')
|
||||||
|
try {
|
||||||
|
await login(username, password)
|
||||||
|
setUsername('')
|
||||||
|
setPassword('')
|
||||||
|
setConSesion(true)
|
||||||
|
} catch (err) {
|
||||||
|
setErrorLogin(err instanceof ErrorLogin ? err.message : 'No se pudo iniciar sesión')
|
||||||
|
} finally {
|
||||||
|
setEntrando(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const guardar = async (mondayUserId: number | null) => {
|
||||||
|
setGuardando(true)
|
||||||
|
setAviso('')
|
||||||
|
setErrorGuardar('')
|
||||||
|
try {
|
||||||
|
const actualizado = await guardarVinculoMonday(mondayUserId)
|
||||||
|
setPerfil(actualizado)
|
||||||
|
setSeleccion(actualizado.mondayUserId !== null ? String(actualizado.mondayUserId) : '')
|
||||||
|
setAviso(mondayUserId === null ? 'Vínculo con Monday eliminado' : 'Vínculo con Monday guardado')
|
||||||
|
} catch {
|
||||||
|
setErrorGuardar('No se pudo guardar el vínculo. Intenta de nuevo.')
|
||||||
|
} finally {
|
||||||
|
setGuardando(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cerrarSesion = () => {
|
||||||
|
logout()
|
||||||
|
setPerfil(null)
|
||||||
|
setUsuarios(null)
|
||||||
|
setConSesion(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const vinculado =
|
||||||
|
perfil?.mondayUserId != null ? (usuarios?.find((u) => u.id === perfil.mondayUserId) ?? null) : null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section aria-labelledby="titulo-cuenta" className="mt-24 border-t border-neutral-200 pt-16">
|
||||||
|
<h2 id="titulo-cuenta" className="text-xs font-bold tracking-widest text-accent uppercase">
|
||||||
|
Configuración de cuenta
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{!conSesion ? (
|
||||||
|
<form onSubmit={(e) => void entrar(e)} className="mt-8 max-w-sm">
|
||||||
|
<p className="text-sm leading-relaxed text-neutral-500">
|
||||||
|
Inicia sesión para vincular tu usuario de Monday.
|
||||||
|
</p>
|
||||||
|
<div className="mt-6">
|
||||||
|
<label htmlFor="cuenta-username" className={labelEstilo}>
|
||||||
|
Usuario
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="cuenta-username"
|
||||||
|
type="text"
|
||||||
|
autoComplete="username"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
className={inputEstilo}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<label htmlFor="cuenta-password" className={labelEstilo}>
|
||||||
|
Contraseña
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="cuenta-password"
|
||||||
|
type="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className={inputEstilo}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errorLogin && (
|
||||||
|
<p role="alert" className="mt-4 text-xs text-red-700">
|
||||||
|
{errorLogin}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={entrando}
|
||||||
|
className="mt-6 bg-accent px-5 py-2.5 text-xs font-bold tracking-widest text-white uppercase transition-colors duration-150 hover:bg-neutral-900 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{entrando ? 'Entrando…' : 'Entrar'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<div className="mt-8 max-w-md">
|
||||||
|
{cargandoPerfil && <p className="text-sm text-neutral-500">Cargando tu perfil…</p>}
|
||||||
|
|
||||||
|
{perfil && (
|
||||||
|
<>
|
||||||
|
<p className="text-sm text-neutral-900">
|
||||||
|
Sesión: <span className="font-medium">{perfil.nombre}</span>
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-sm text-neutral-500">
|
||||||
|
Vínculo Monday actual:{' '}
|
||||||
|
{perfil.mondayUserId === null ? (
|
||||||
|
<span className="text-neutral-400">sin vincular</span>
|
||||||
|
) : vinculado ? (
|
||||||
|
<span className="text-neutral-900">
|
||||||
|
{vinculado.nombre} ({vinculado.email})
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-neutral-900">ID {perfil.mondayUserId}</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{errorUsuarios ? (
|
||||||
|
<div className="mt-6">
|
||||||
|
<p role="alert" className="text-xs text-red-700">
|
||||||
|
{errorUsuarios}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={cargarUsuarios}
|
||||||
|
className="mt-3 border border-neutral-900 bg-transparent px-4 py-2 text-xs font-bold tracking-widest text-neutral-900 uppercase transition-colors duration-150 hover:bg-neutral-900 hover:text-white"
|
||||||
|
>
|
||||||
|
Reintentar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : usuarios === null ? (
|
||||||
|
<p className="mt-6 text-sm text-neutral-500">Cargando usuarios de Monday…</p>
|
||||||
|
) : (
|
||||||
|
<div className="mt-6">
|
||||||
|
<label htmlFor="cuenta-monday" className={labelEstilo}>
|
||||||
|
Tu usuario de Monday
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="cuenta-monday"
|
||||||
|
value={seleccion}
|
||||||
|
onChange={(e) => setSeleccion(e.target.value)}
|
||||||
|
className={inputEstilo}
|
||||||
|
>
|
||||||
|
<option value="">— Sin vincular —</option>
|
||||||
|
{usuarios.map((u) => (
|
||||||
|
<option key={u.id} value={String(u.id)}>
|
||||||
|
{u.nombre} ({u.email})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div className="mt-4 flex flex-wrap gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={guardando}
|
||||||
|
onClick={() => void guardar(seleccion === '' ? null : Number(seleccion))}
|
||||||
|
className="bg-accent px-5 py-2.5 text-xs font-bold tracking-widest text-white uppercase transition-colors duration-150 hover:bg-neutral-900 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{guardando ? 'Guardando…' : 'Guardar'}
|
||||||
|
</button>
|
||||||
|
{perfil.mondayUserId !== null && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={guardando}
|
||||||
|
onClick={() => void guardar(null)}
|
||||||
|
className="border border-red-300 bg-white px-5 py-2.5 text-xs font-bold tracking-widest text-red-700 uppercase transition-colors duration-150 hover:bg-red-50 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Desvincular
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{aviso && (
|
||||||
|
<p role="status" className="mt-4 text-xs font-bold tracking-widest text-accent uppercase">
|
||||||
|
{aviso}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{errorGuardar && (
|
||||||
|
<p role="alert" className="mt-4 text-xs text-red-700">
|
||||||
|
{errorGuardar}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={cerrarSesion}
|
||||||
|
className="mt-8 border border-neutral-900 bg-transparent px-4 py-2 text-xs font-bold tracking-widest text-neutral-900 uppercase transition-colors duration-150 hover:bg-neutral-900 hover:text-white"
|
||||||
|
>
|
||||||
|
Cerrar sesión
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
38
apps/landing/src/services/api.ts
Normal file
38
apps/landing/src/services/api.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { API_URL, conAuth, manejar401, type UsuarioSesion } from './auth'
|
||||||
|
|
||||||
|
export interface UsuarioMonday {
|
||||||
|
id: number
|
||||||
|
nombre: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /auth/me — perfil del usuario autenticado (incluye mondayUserId) */
|
||||||
|
export async function getMe(): Promise<UsuarioSesion> {
|
||||||
|
const resp = await fetch(`${API_URL}/auth/me`, conAuth())
|
||||||
|
manejar401(resp)
|
||||||
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||||
|
return (await resp.json()) as UsuarioSesion
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /monday/usuarios — usuarios activos del workspace para el dropdown */
|
||||||
|
export async function getUsuariosMonday(): Promise<UsuarioMonday[]> {
|
||||||
|
const resp = await fetch(`${API_URL}/monday/usuarios`, conAuth())
|
||||||
|
manejar401(resp)
|
||||||
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||||
|
return (await resp.json()) as UsuarioMonday[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** PATCH /usuarios/me/monday — vincula (number) o desvincula (null) la cuenta Monday */
|
||||||
|
export async function guardarVinculoMonday(mondayUserId: number | null): Promise<UsuarioSesion> {
|
||||||
|
const resp = await fetch(
|
||||||
|
`${API_URL}/usuarios/me/monday`,
|
||||||
|
conAuth({
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ mondayUserId }),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
manejar401(resp)
|
||||||
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||||
|
return (await resp.json()) as UsuarioSesion
|
||||||
|
}
|
||||||
78
apps/landing/src/services/auth.ts
Normal file
78
apps/landing/src/services/auth.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// Sesión de autenticación contra el API Gateway (JWT guardado en localStorage)
|
||||||
|
export const API_URL = import.meta.env.VITE_API_URL ?? '/api'
|
||||||
|
const STORAGE_KEY = 'frasa_auth'
|
||||||
|
|
||||||
|
export interface UsuarioSesion {
|
||||||
|
nombre: string
|
||||||
|
rol: string
|
||||||
|
asignacion: string | null
|
||||||
|
mondayUserId: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Sesion {
|
||||||
|
token: string
|
||||||
|
usuario: UsuarioSesion
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Error de login con mensaje genérico (no filtra si el usuario existe) */
|
||||||
|
export class ErrorLogin extends Error {
|
||||||
|
constructor(message = 'Usuario o contraseña incorrectos') {
|
||||||
|
super(message)
|
||||||
|
this.name = 'ErrorLogin'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSesion(): Sesion | null {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY)
|
||||||
|
if (!raw) return null
|
||||||
|
const sesion = JSON.parse(raw) as Sesion
|
||||||
|
if (typeof sesion?.token !== 'string' || !sesion.usuario) return null
|
||||||
|
return sesion
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getToken(): string | null {
|
||||||
|
return getSesion()?.token ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logout(): void {
|
||||||
|
localStorage.removeItem(STORAGE_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /auth/login; guarda token + usuario en localStorage */
|
||||||
|
export async function login(username: string, password: string): Promise<UsuarioSesion> {
|
||||||
|
let res: Response
|
||||||
|
try {
|
||||||
|
res = await fetch(`${API_URL}/auth/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, password }),
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
throw new ErrorLogin('No se pudo conectar con el servidor')
|
||||||
|
}
|
||||||
|
if (!res.ok) throw new ErrorLogin()
|
||||||
|
const data = (await res.json()) as Sesion
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify({ token: data.token, usuario: data.usuario }))
|
||||||
|
return data.usuario
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agrega Authorization: Bearer a un RequestInit si hay sesión.
|
||||||
|
* Sin token devuelve el init tal cual (las llamadas siguen iguales que antes).
|
||||||
|
*/
|
||||||
|
export function conAuth(init: RequestInit = {}): RequestInit {
|
||||||
|
const token = getToken()
|
||||||
|
if (!token) return init
|
||||||
|
return { ...init, headers: { ...(init.headers as Record<string, string>), Authorization: `Bearer ${token}` } }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Si el API responde 401: cierra sesión y recarga para volver al login */
|
||||||
|
export function manejar401(res: Response): void {
|
||||||
|
if (res.status !== 401) return
|
||||||
|
logout()
|
||||||
|
if (typeof window !== 'undefined') window.location.reload()
|
||||||
|
}
|
||||||
@@ -3,7 +3,8 @@ import { defineConfig } from 'vitest/config'
|
|||||||
// Config de tests separada para no mezclar los tipos de Vite y Vitest
|
// Config de tests separada para no mezclar los tipos de Vite y Vitest
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
environment: 'node',
|
// jsdom para los tests de componentes; los tests puros corren igual
|
||||||
|
environment: 'jsdom',
|
||||||
coverage: {
|
coverage: {
|
||||||
provider: 'v8',
|
provider: 'v8',
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user