diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..37d38bbbb --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true \ No newline at end of file diff --git a/.env b/.env deleted file mode 100644 index b885521b4..000000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -NODE_PATH=./src diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..44fbd6e36 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,86 @@ +const { resolve } = require('node:path'); + +const project = resolve(__dirname, 'tsconfig.eslint.json'); + +module.exports = { + root: true, + extends: [ + require.resolve('@vercel/style-guide/eslint/node'), + require.resolve('@vercel/style-guide/eslint/typescript'), + require.resolve('@vercel/style-guide/eslint/browser'), + require.resolve('@vercel/style-guide/eslint/react'), + require.resolve('@vercel/style-guide/eslint/next'), + ], + parserOptions: { + project, + tsconfigRootDir: __dirname, + }, + settings: { + 'import/resolver': { + typescript: { + project, + }, + }, + }, + overrides: [ + { + files: ['*.js', '*.jsx'], + rules: { + '@typescript-eslint/restrict-template-expressions': 'off', + }, + }, + ], + rules: { + '@typescript-eslint/no-unused-vars': [ + 'error', + { + ignoreRestSiblings: true, + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + '@typescript-eslint/no-empty-interface': [ + 'error', + { + allowSingleExtends: true, + }, + ], + '@typescript-eslint/no-shadow': [ + 'error', + { + ignoreOnInitialization: true, + }, + ], + 'import/newline-after-import': 'error', + 'react/jsx-uses-react': 'error', + 'react/react-in-jsx-scope': 'error', + 'unicorn/filename-case': [ + 'error', + { + cases: { + kebabCase: true, // personal style + pascalCase: true, + }, + }, + ], + + // Deactivated + '@typescript-eslint/dot-notation': 'off', // paths are used with a dot notation + '@typescript-eslint/no-misused-promises': 'off', // onClick with async fails + '@typescript-eslint/no-non-null-assertion': 'off', // sometimes compiler is unable to detect + '@typescript-eslint/no-unnecessary-condition': 'off', // remove when no static data is used + '@typescript-eslint/require-await': 'off', // Server Actions require async flag always + '@typescript-eslint/prefer-nullish-coalescing': 'off', // personal style + '@typescript-eslint/restrict-template-expressions': 'off', + 'import/no-default-export': 'off', // Next.js components must be exported as default + 'import/no-extraneous-dependencies': 'off', // conflict with sort-imports plugin + 'import/order': 'off', // using custom sort plugin + 'no-nested-ternary': 'off', // personal style + 'no-redeclare': 'off', // conflict with TypeScript function overloads + 'react/jsx-fragments': 'off', // personal style + 'react/prop-types': 'off', // TypeScript is used for type checking + + '@next/next/no-img-element': 'off', // Temporary disabled + }, +}; diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..ff0af5dff --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,7 @@ +{ + "extends": ["next/core-web-vitals"], + "rules": { + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-shadow": "off" + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..fbd75d33c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=lf \ No newline at end of file diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 index 960b87d28..7f85cc4d5 --- a/.gitignore +++ b/.gitignore @@ -10,15 +10,23 @@ # production /build +out +.next # misc .DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local +.eslintcache +.idea +/.env +/.env.local +/.env.development.local +/.env.test.local +/.env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* -yarn.lock \ No newline at end of file +".yarn/install-state.gz" +".yarnrc.yml" +"yarn.lock" +.env*.local diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..4b460c0cc --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +# Skip optional dependencies to avoid failing downloads for optional binaries like canvas +optional=false diff --git a/.vercel/README.txt b/.vercel/README.txt new file mode 100644 index 000000000..525d8ce8e --- /dev/null +++ b/.vercel/README.txt @@ -0,0 +1,11 @@ +> Why do I have a folder named ".vercel" in my project? +The ".vercel" folder is created when you link a directory to a Vercel project. + +> What does the "project.json" file contain? +The "project.json" file contains: +- The ID of the Vercel project that you linked ("projectId") +- The ID of the user or team your Vercel project is owned by ("orgId") + +> Should I commit the ".vercel" folder? +No, you should not share the ".vercel" folder with anyone. +Upon creation, it will be automatically added to your ".gitignore" file. diff --git a/.vercel/project.json b/.vercel/project.json new file mode 100644 index 000000000..77fb8f8b7 --- /dev/null +++ b/.vercel/project.json @@ -0,0 +1 @@ +{"projectId":"prj_oU2m9oOxetFxk79UTvvrqbCR8ZrP","orgId":"team_vxOATwTzrvi0sgLiOBBQ6G3a","projectName":"membros-fatosda-bolsa"} \ No newline at end of file diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 000000000..49e884ccb --- /dev/null +++ b/.yarnrc @@ -0,0 +1 @@ +ignore-optional true diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 000000000..3186f3f07 --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/0 b/0 new file mode 100644 index 000000000..d3ad077b6 --- /dev/null +++ b/0 @@ -0,0 +1,29 @@ +Configura as cores padrÆo de primeiro plano e tela de fundo do console. + +COLOR [attr] + + attr Especifica os atributos de cor da sa¡da do console + +Atributos de cor sÆo especificados por DOIS d¡gitos hexadecimais. O primeiro +corresponde … cor de tela de fundo; o segundo … cor de primeiro plano. Cada +d¡gito pode ter apenas um dos seguintes valores: + + 0 = Preto 8 = Cinza + 1 = Azul 9 = Azul claro + 2 = Verde A = Verde claro + 3 = Verde- gua B = Verde- gua claro + 4 = Vermelho C = Vermelho claro + 5 = Roxo D = Lil s + 6 = Amarelo E = Amarelo claro + 7 = Branco F = Branco brilhante + +Caso nenhum argumento seja passado, este comando restaurar  a cor de +antes do CMD.EXE ser executado. Este valor vem ou da janela atual do +console, ou da op‡Æo /T da linha de comando, ou do valor de DefaultColor +no Registro. + +O comando COLOR altera ERRORLEVEL para 1 se for tentado se executar o +comando COLOR com as mesmas cores de primeiro plano e de tela de +fundo. + +Exemplo: "COLOR fc" gera o vermelho claro na tela de fundo branca brilhante diff --git a/6.12.0 b/6.12.0 new file mode 100644 index 000000000..e69de29bb diff --git a/CHANGELOG.md b/CHANGELOG.md index d8d421c27..1a832e626 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,26 +1,106 @@ +# Changelog + +## v4.0.0 + +###### Mar 8, 2024 + +- Add `TypeScript` +- Refactor code +- Replace `date-fns` with `dayjs` +- Replace `Formik` with `React Hook Form` +- Replace `Hero Icons` with `Phosphor Icons` +- Replace `pages` router with `app router` +- Replace `Yup` with `Zod` +- Update `eslint` rules +- Update auth implementation +- Update dependencies +- Update design system + +## v3.0.0 + +###### Feb 24, 2023 + +- Refactor components +- Replace authentication +- Update dependencies +- Update design system + +## v2.1.0 + +###### Sep 15, 2022 + +- Integrate Zalter Authentication +- Update dependencies + +## v2.0.0 + +###### Nov 8, 2021 + +- Migrate to Next.js +- Update design system + # Change Log -## [0.1.1] 2019-05-11 +## v1.0.0 + +###### Aug 7, 2020 + +- Add `eslint` +- Add `Feather Icons` +- Add `Formik` for login/register pages +- Implement `react-router` v6 routing method +- Remove `node-sass` dependency +- Remove extra views +- Update all components to match the PRO version style +- Update dependencies +- Update folder structure to remove folder depth +- Update theme configuration + +## v0.4.0 + +###### Jul 24, 2019 + +- Adjust theme colors +- Implement `useStyle` hook instead of `withStyles` HOC +- Implement a custom Route component to wrap views in layouts +- Remove `services` and `data` folders, each component has its own data +- Remove unused `.scss` files from `assets` folder +- Replace `.jsx` with `.js` +- Replace Class Components with Function Components +- Replace custom components (Portlet) with Material-UI built-in components +- Replace dependency `classnames` with `clsx` +- Update dependencies +- Update the layout to match the PRO version + +## v0.3.0 + +###### May 13, 2019 + +- Implement `jsconfig.json` file and removed `.env` to match React v16.8.6 absolute paths +- Update chart styles and options +- Update Dashboard view top widgets styles and structure +- Update few icons to match @material-ui v4 updates +- Update React version to 16.8.6 to support React Hooks +- Update to @material-ui to 4.0.0-beta -### Updates +## v0.2.0 -- Updated README.md -- Added docs for IE11 polyfill -- Removed unused scss from assets -- Removed unused components from shared components -- Removed `authGuard` since it won't be used in this version -- Removed `auth` service folder since it won't be implemented for this version -- Removed "status" from `ProductCard` component since it was not part of released design -- Changed icon in `Users` widget (ArrowDropDown with ArrowDropUp) +###### May 11, 2019 -### Fixed bugs +- Add docs for IE11 polyfill +- Fix `DisplayMode` component size, when used as a flex child it could grow/shrink +- Fix `ProductCard` component description height +- Fix `Typography` view responsiveness for small devices +- Fix charts responsiveness +- Remove "status" from `ProductCard` component since it was not part of released design +- Remove `auth` service folder since it won't be implemented for this version +- Remove `authGuard` since it won't be used in this version +- Remove unused components from shared components +- Remove unused scss from assets +- Update README.md -- Fixed charts responsiveness -- Fixed `DisplayMode` component size, when used as a flex child it could grow/shrink -- Fixed `Typography` view responsiveness for small devices -- Fixed `ProductCard` component description height - https://github.com/devias-io/react-material-dashboard/pull/2 +## v0.1.0 -## [0.1.0] 2019-05-02 +###### May 2, 2019 ### Initial commit diff --git a/Export b/Export new file mode 100644 index 000000000..e69de29bb diff --git a/LICENSE.md b/LICENSE.md index b10fe1f33..c8fd93cf2 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Devias +Copyright (c) 2024 Fatos da Bolsa Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MIGRATION_INSTRUCTIONS.md b/MIGRATION_INSTRUCTIONS.md new file mode 100644 index 000000000..4a8ab34e5 --- /dev/null +++ b/MIGRATION_INSTRUCTIONS.md @@ -0,0 +1,122 @@ +# Instruções para Aplicar Migration - Sistema de Migração de Planos + +## âš ï¸ IMPORTANTE: Migration Pendente + +A implementação da funcionalidade de migração de planos está completa, mas a migration do Prisma precisa ser aplicada manualmente no banco de dados. + +## 🔧 Como Aplicar a Migration + +Execute o seguinte comando na raiz do projeto: + +```bash +npx prisma migrate dev --name add_payment_fields_and_plan_migrations +``` + +## 📋 O que será criado no banco de dados: + +### 1. **Novos campos na tabela `Purchase`:** + - `paymentMethod` (String, nullable) - Forma de pagamento normalizada + - `installments` (Int, default: 1) - Número de parcelas + - `platform` (String, nullable) - Plataforma de origem (HOTMART, KIWIFY, EDUZZ) + +### 2. **Nova tabela `plan_migrations`:** + - `id` - ID único + - `userId` - ID do usuário que migrou + - `planOriginal` - Plano original (LITE) + - `planNovo` - Novo plano (VIP) + - `valorOriginal` - Valor do plano original + - `valorNovo` - Valor do novo plano + - `mesesRestantes` - Meses restantes do plano original + - `valorDesconto` - Desconto aplicado + - `valorFinal` - Valor final da migração + - `status` - Status da migração (PENDING, COMPLETED, CANCELLED) + - `observacoes` - Observações adicionais + - `processadoPor` - ID do admin que processou + - `dataCriacao` - Data de criação do registro + - `dataProcessamento` - Data de processamento da migração + - `createdAt` - Timestamp de criação + - `updatedAt` - Timestamp de atualização + +## 🚀 Após Aplicar a Migration + +Execute o seguinte comando para gerar o cliente Prisma atualizado: + +```bash +npx prisma generate +``` + +## 📠Como Acessar a Página + +Após aplicar as migrations, acesse: +- **URL:** `/dashboard/admin/migracao-plano` +- **Requisito:** Permissão de administrador + +## 🧪 Como Testar + +1. Acesse a página de migração de planos +2. Digite o e-mail de um cliente que está no plano LITE +3. Clique em "Buscar Cliente" +4. Verifique os dados apresentados: + - Informações do cliente + - Dados da compra original (com forma de pagamento e parcelas) + - Cálculo automático da migração + - Observações importantes +5. Clique em "Processar Migração" para efetivar + +## 📦 Arquivos Criados/Modificados + +### Novos Arquivos: +1. `/src/lib/payment-normalization.ts` - Funções de normalização de pagamento +2. `/src/app/api/admin/migracao-plano/calcular/route.ts` - API de cálculo +3. `/src/app/api/admin/migracao-plano/processar/route.ts` - API de processamento +4. `/src/app/dashboard/admin/migracao-plano/page.tsx` - Página de interface + +### Arquivos Modificados: +1. `/prisma/schema.prisma` - Novos campos e modelo +2. `/src/app/api/webhooks/hotmart/[id]/route.ts` - Captura de dados de pagamento +3. `/src/app/api/webhooks/kiwify/[token]/route.ts` - Captura de dados de pagamento +4. `/src/app/api/webhooks/eduzz/[token]/route.ts` - Captura de dados de pagamento + +## 💡 Funcionalidades Implementadas + +### ✅ Captura de Dados de Pagamento +- Forma de pagamento (cartão, PIX, boleto, etc.) +- Número de parcelas +- Plataforma de origem + +### ✅ Cálculo Automático de Migração +- Meses já utilizados do plano LITE +- Meses restantes até expiração +- Diferença mensal entre planos +- Desconto proporcional +- Valor final da migração + +### ✅ Interface Completa +- Busca de cliente por e-mail +- Exibição detalhada dos dados +- Validações completas +- Processamento seguro com transações + +### ✅ Histórico de Migrações +- Registro de todas as migrações realizadas +- Rastreabilidade completa +- Status e observações + +## 🔠Segurança + +- ✅ Verificação de autenticação +- ✅ Verificação de permissões de admin +- ✅ Validações de dados +- ✅ Transações atômicas no banco + +## 📞 Suporte + +Se encontrar algum problema, verifique: +1. As migrations foram aplicadas corretamente? +2. O Prisma client foi gerado? +3. O usuário tem permissão de admin? +4. O cliente está realmente no plano LITE? + +--- + +**Implementação concluída!** 🎉 diff --git a/README.md b/README.md index aff8a6a01..7c796b40f 100755 --- a/README.md +++ b/README.md @@ -1,135 +1,76 @@ -## [Brainalytica - Material React Dashboard Free](https://devias.io/products/material-react-dashboard) [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social&logo=twitter)](https://twitter.com/intent/tweet?text=%F0%9F%9A%A8Devias%20Freebie%20Alert%20-%20An%20awesome%20ready-to-use%20register%20page%20made%20with%20%23material%20%23react%0D%0Ahttps%3A%2F%2Fdevias.io%20%23createreactapp%20%23devias%20%23material%20%23freebie%20%40devias-io) +# Fatos da Bolsa Hub -![license](https://img.shields.io/badge/license-MIT-blue.svg) [![GitHub issues open](https://img.shields.io/github/issues/devias-io/react-material-dashboard.svg?maxAge=2592000)](https://github.com/devias-io/react-material-dashboard/issues?q=is%3Aopen+is%3Aissue) [![GitHub issues closed](https://img.shields.io/github/issues-closed-raw/devias-io/react-material-dashboard.svg?maxAge=2592000)](https://github.com/devias-io/react-material-dashboard/issues?q=is%3Aissue+is%3Aclosed) [![Chat](https://img.shields.io/badge/chat-on%20discord-7289da.svg)](https://discord.gg/BSHaUGR) +Fatos da Bolsa Hub é um painel administrativo desenvolvido para equipes que acompanham dados de mercado, materiais educacionais e relatórios internos sobre a bolsa de valores. O projeto combina Next.js, React e Material UI para oferecer uma base robusta que pode ser facilmente estendida com integrações proprietárias, automações e fluxos de trabalho colaborativos. -![Register Page](https://s3.eu-west-2.amazonaws.com/devias/products/react-material-dashboard/material-react-dashboard-free.gif) +## Recursos principais -> Free React Dashboard made with [Material UI's](https://material-ui/?ref=devias-io) components, [React](https://reactjs.org/?ref=devias-io) and of course [create-react-app](https://facebook.github.io/create-react-app/?ref=devias-io) to boost your app development process! We'll launch a pro version soon, so if you are interested subscribe to our personal emailing list on [https://devias.io/](https://devias.io/) +- **Dashboard personalizável:** visualize KPIs, gráficos e status de campanhas em tempo real. +- **Gestão de membros e clientes:** organize contatos, preferências e permissões com formulários validados. +- **Fluxos de autenticação completos:** telas de login, cadastro, redefinição de senha e convites com layout consistente. +- **Componentes reutilizáveis:** bibliotecas de cartões, tabelas, gráficos e formulários para acelerar novas páginas. +- **Integração com serviços externos:** estrutura preparada para conectar APIs de dados financeiros, automação de e-mail e armazenamento em nuvem. -## Table of Contents +## Tecnologias -- [Demo](#demo) -- [Quick Start](#quick-start) -- [Documentation](#documentation) -- [Design Files](#design-files) -- [File Structure](#file-structure) -- [Resources](#resources) -- [Reporting Issues](#reporting-issues) -- [Licensing](#licensing) -- [Contact Us](#contact-us) +- [Next.js](https://nextjs.org) 14 +- [React](https://react.dev) +- [Material UI](https://mui.com) +- [Prisma](https://www.prisma.io) +- [TypeScript](https://www.typescriptlang.org) -## Demo +## Primeiros passos -- [Dashboard Page](http://react-material-dashboard.devias.io/dashboard?ref=github-readme) -- [Users Page](http://react-material-dashboard.devias.io/users?ref=github-readme) -- [Products Page](http://react-material-dashboard.devias.io/products?ref=github-readme) -- [Register Page](http://react-material-dashboard.devias.io/sign-up?ref=github-readme) -- [Typography Page](http://react-material-dashboard.devias.io/typography?ref=github-readme) -- [Account Page](http://react-material-dashboard.devias.io/account?ref=github-readme) -- [Settings Page](http://react-material-dashboard.devias.io/settings?ref=github-readme) +1. Instale as dependências: -## Quick start + ```bash + npm install + ``` -- [Download from Github](https://github.com/devias-io/react-material-dashboard/archive/master.zip) or [Download from Devias](https://devias.io/products/material-react-dashboard) or clone the repo: `git clone https://github.com/devias-io/react-material-dashboard.git` +2. Configure as variáveis de ambiente necessárias na raiz do projeto (`.env.local`). -- Install dependencies: `npm install` or `yarn` +3. Execute as migrações e gere o cliente Prisma: -- Start the server: `npm run start` or `yarn start` + ```bash + npx prisma migrate deploy + npm run postinstall + ``` -- Views are on: `localhost:3000` +4. Inicie o servidor de desenvolvimento: -## Documentation + ```bash + npm run dev + ``` -The documentation for the React Material Kit is can be found [here](https://material-ui.com?ref=devias-io). +5. Acesse [`http://localhost:3000`](http://localhost:3000) para visualizar a aplicação. -## Design Files +## Scripts disponíveis -[Download .sketch file](https://s3.eu-west-2.amazonaws.com/devias/products/react-material-dashboard/react-material-dashboard-free.sketch) +- `npm run dev` – inicia o ambiente de desenvolvimento com hot reload. +- `npm run build` – gera a versão otimizada para produção após executar `prisma generate`. +- `npm run start` – executa o build gerado. +- `npm run lint` – valida o código com as regras ESLint configuradas para o projeto. +- `npm run typecheck` – garante que não há erros de tipagem TypeScript. +- `npm run format:write` – aplica a formatação consistente definida pelo Prettier. -## File Structure - -Within the download you'll find the following directories and files: +## Estrutura do projeto ``` -material-react-dashboard - -├── .eslintrc -├── .prettierrc -├── LICENSE.md -├── package.json -├── README.md +┌── prisma ├── public -├── docs -└── src - ├── assets - │ └── scss - ├── common - │ ├── colors - │ ├── validators - │ └── serviceWorker.js - ├── components - │ ├── DisplayMode - │ ├── Paper - │ ├── Portlet - │ ├── PortletContent - │ ├── PortletFooter - │ ├── PortletHeader - │ ├── DisplayMode - │ ├── PortletLabel - │ ├── PortletToolbar - │ ├── SearchInput - │ ├── Status - │ └── SearchInput - ├── data - ├── helpers - ├── icons - │ ├── Facebook - │ └── Google - ├── layouts - │ ├── Dashboard - │ │ └── components - │ │ ├── Footer - │ │ ├── Sidebar - │ │ └── Topbar - ├── services - │ ├── notification - │ ├── order - │ ├── product - │ ├── user - ├── theme - │ ├── overrides - │ ├── pallete.js - │ └── typography.js - ├── views - │ ├── Account - │ ├── Dashboard - │ ├── Icons - │ ├── NotFound - │ ├── ProductList - │ ├── Settings - │ ├── SignIn - │ ├── SignUp - │ ├── Typography - │ ├── UnderDevelopment - │ └── UserList - ├── App.jsx - ├── index.jsx - └── Routes.jsx +├── src +│ ├── components +│ ├── layouts +│ ├── lib +│ ├── pages e views protegidas +│ └── styles e temas +├── package.json +└── README.md ``` -## Resources - -- More freebies like this one: - -## Reporting Issues: - -- [Github Issues Page](https://github.com/devias-io/react-material-dashboard/issues?ref=devias-io) - -## Licensing +## Suporte -- Licensed under MIT (https://github.com/devias-io/react-material-dashboard/blob/master/LICENSE.md) +Este repositório é mantido pela equipe Fatos da Bolsa. Para dúvidas, sugestões ou suporte personalizado, entre em contato pelo e-mail [contato@fatosdabolsa.com.br](mailto:contato@fatosdabolsa.com.br). -## Contact Us +## Licença -- Email Us: contact@devias.io -- [Follow us on Instagram](https://www.instagram.com/deviasio/) +Distribuído sob a licença MIT. Veja `LICENSE.md` para mais detalhes. diff --git a/all_mobile_detection.txt b/all_mobile_detection.txt new file mode 100644 index 000000000..e69de29bb diff --git a/analise_responsivo.txt b/analise_responsivo.txt new file mode 100644 index 000000000..e69de29bb diff --git a/api_estrutura.txt b/api_estrutura.txt new file mode 100644 index 000000000..47f69b344 --- /dev/null +++ b/api_estrutura.txt @@ -0,0 +1,6 @@ +Listagem de caminhos de pasta para o volume Acer +O número de série do volume é 20AE-A7B2 +C:\USERS\JACBD\ONEDRIVE\DOCUMENTOS\GITHUB\MEMBROSFATOSDABOLSA\PAGES\API +Caminho inválido - \USERS\JACBD\ONEDRIVE\DOCUMENTOS\GITHUB\MEMBROSFATOSDABOLSA\PAGES\API +Não existem subpastas + diff --git a/backup_$(date b/backup_$(date new file mode 100644 index 000000000..e69de29bb diff --git a/backup_before_migration.sql b/backup_before_migration.sql new file mode 100644 index 000000000..e69de29bb diff --git a/carteiras.js b/carteiras.js new file mode 100644 index 000000000..e69de29bb diff --git a/context_usage.txt b/context_usage.txt new file mode 100644 index 000000000..e69de29bb diff --git a/create-admin-final.js b/create-admin-final.js new file mode 100644 index 000000000..0afe9d07d --- /dev/null +++ b/create-admin-final.js @@ -0,0 +1,25 @@ +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); + +async function createAdmin() { + try { + const admin = await prisma.user.create({ + data: { + firstName: 'Admin', + lastName: 'Sistema', + email: 'admin@sistema.local', + password: 'senha123', // Senha simples por enquanto + plan: 'ADMIN', + status: 'ACTIVE' + } + }); + + console.log('✅ Admin criado:', admin.email, '- Plan:', admin.plan); + } catch (error) { + console.error('⌠Erro:', error.message); + } finally { + await prisma.$disconnect(); + } +} + +createAdmin(); \ No newline at end of file diff --git a/create-admin-simple.sql b/create-admin-simple.sql new file mode 100644 index 000000000..a9b13f6bf --- /dev/null +++ b/create-admin-simple.sql @@ -0,0 +1,29 @@ +const { PrismaClient } = require('@prisma/client'); +const bcrypt = require('bcryptjs'); + +const prisma = new PrismaClient(); + +async function main() { + console.log('ðŸ›¡ï¸ Criando admin...'); + + const hashedPassword = await bcrypt.hash('Admin123!', 10); + + const admin = await prisma.user.upsert({ + where: { email: 'admin@fatosdobolsa.com' }, + update: {}, + create: { + email: 'admin@fatosdobolsa.com', + firstName: 'Admin', + lastName: 'Sistema', + password: hashedPassword, + plan: 'ADMIN', + status: 'ACTIVE' + } + }); + + console.log('✅ Admin criado:', admin.email); +} + +main() + .catch(console.error) + .finally(() => prisma.$disconnect()); \ No newline at end of file diff --git a/create-real-admin.js b/create-real-admin.js new file mode 100644 index 000000000..cfd551596 --- /dev/null +++ b/create-real-admin.js @@ -0,0 +1,35 @@ +const { PrismaClient } = require('@prisma/client'); +const bcrypt = require('bcryptjs'); + +process.env.DATABASE_URL = 'postgresql://neondb_owner:npg_aOq2Kmpjiv9Q@ep-noisy-wind-a8b1knv2-pooler.eastus2.azure.neon.tech/neondb?sslmode=require&channel_binding=require'; + +const prisma = new PrismaClient(); + +async function createAdmin() { + console.log('Criando admin real...'); + + const password = await bcrypt.hash('Admin123!', 10); + console.log('Senha hash criada'); + + const admin = await prisma.user.upsert({ + where: { email: 'admin@fatosdobolsa.com' }, + update: { + password: password, + plan: 'ADMIN', + status: 'ACTIVE' + }, + create: { + email: 'admin@fatosdobolsa.com', + firstName: 'Admin', + lastName: 'Sistema', + password: password, + plan: 'ADMIN', + status: 'ACTIVE' + } + }); + + console.log('✅ Admin criado:', admin.email); + await prisma.$disconnect(); +} + +createAdmin().catch(console.error); \ No newline at end of file diff --git a/current_menu.txt b/current_menu.txt new file mode 100644 index 000000000..e69de29bb diff --git a/docs/Polyfill-IE11.md b/docs/Polyfill-IE11.md deleted file mode 100644 index 7c6b26b6d..000000000 --- a/docs/Polyfill-IE11.md +++ /dev/null @@ -1,34 +0,0 @@ -# Polyfill for IE11 - -As part of this [issue](https://github.com/devias-io/react-material-dashboard/issues/1) here are the steps to add basic support for IE11. - -## Install dependencies - -`yarn add react-app-polyfill core-js` - -## Import polyfills - -Create a file `src/polyfill.js` with the following imports - -```javascript -import 'react-app-polyfill/ie11'; -import 'core-js/features/array/find'; -import 'core-js/features/array/includes'; -import 'core-js/features/number/is-nan'; -``` - -Import polyfill file in `src/index.js` - -```javascript -import './polyfills'; -``` - -## Update browserlist - -Open `package.json` and update - -```diff -"browserslist": [ -- "not ie <= 11", -+ "not ie < 10", -``` diff --git a/docs/cache-strategy.md b/docs/cache-strategy.md new file mode 100644 index 000000000..d3b216e79 --- /dev/null +++ b/docs/cache-strategy.md @@ -0,0 +1,36 @@ +# Cache, deduplicação e observabilidade + +Este projeto agora possui uma camada de cache única (`CacheService`) que prioriza Redis da Upstash quando as variáveis de ambiente estão configuradas e faz fallback automático para cache em memória com deduplicação de promessas (request coalescing). + +## Como funciona +- **Deduplicação**: requisições iguais em paralelo reaproveitam a mesma `Promise`, evitando stampede. +- **Tags**: rotas podem invalidar grupos específicos com `CacheService.invalidateTags([...])`. +- **TTL por tipo de dado**: + - Cotações/notificações: 30–60s + - Financial quotes/market data: 5 min + - Proventos: 1h + - Carteiras e meus-ativos: 10 min +- **Logs**: cada rota otimizada envia um log estruturado `[api-metric]` com hit/miss, backend e latência. + +## Configuração do Upstash Redis (opção recomendada) +1. Criar database no painel do Upstash. +2. Copiar as credenciais REST e definir no Vercel/ambiente local: + - `UPSTASH_REDIS_REST_URL` + - `UPSTASH_REDIS_REST_TOKEN` +3. Deploy. O `CacheService` detecta as variáveis e usa Redis automaticamente. + +## Fallback em memória (opção gratuita) +- Nenhuma configuração extra. É adequado para desenvolvimento/local, porém não é compartilhado entre instâncias e pode crescer na memória. TTLs curtos ajudam a controlar. + +## Rotas otimizadas +- `/api/notifications` e `/api/notifications/[id]` (cache curto + invalidação ao criar/editar/deletar) +- `/api/proventos/[ticker]` (1h) +- `/api/auth/check` (60s por usuário autenticado) +- `/api/carteiras` e `/api/carteiras/minhas` (10 min por usuário/parâmetros) +- `/api/meus-ativos/[carteira]` (cache mestre de 10 min com deduplicação) +- `/api/financial/market-data` e `/api/financial/quotes` (5 min) + +## Próximos passos sugeridos +- Conectar um painel de logs/metrics (DataDog/New Relic/Vercel Logs) para consultar os eventos `[api-metric]`. +- Adicionar rate limiting (ex.: `@upstash/ratelimit`) nas rotas que chamam fornecedores externos. +- Incluir invalidação explícita nos fluxos de escrita que alteram carteiras/ativos para derrubar caches relacionados. diff --git a/docs/cache-validation-audit.md b/docs/cache-validation-audit.md new file mode 100644 index 000000000..00630692d --- /dev/null +++ b/docs/cache-validation-audit.md @@ -0,0 +1,16 @@ +# Diagnóstico de cache, validação de payload e rate limiting + +## Correções aplicadas + +- `src/app/api/proventos/route.ts` + - `GET` agora devolve cabeçalho `Cache-Control: public, s-maxage=120, stale-while-revalidate=60`, alinhado ao TTL do `CacheService`. ã€F:src/app/api/proventos/route.ts†L78-L133】 + - `POST` validado via Zod (`proventosUploadSchema`) e protegido com rate limiting de 5 requisições/minuto por IP. ã€F:src/app/api/proventos/route.ts†L136-L199】 +- `src/app/api/relatorios/route.ts` + - `GET` responde com caching público (`s-maxage=300, stale-while-revalidate=120`). ã€F:src/app/api/relatorios/route.ts†L5-L74】 + - `POST` agora usa schema Zod para todos os campos e rate limiting de 20 requisições/minuto por IP. ã€F:src/app/api/relatorios/route.ts†L77-L140】 +- `src/app/api/faq/route.ts` — respostas públicas enviam `Cache-Control: public, s-maxage=300, stale-while-revalidate=120`. ã€F:src/app/api/faq/route.ts†L1-L26】 +- `src/app/api/auth/login/route.ts` — payload validado com Zod e rate limiting de 10 tentativas/minuto por IP, retornando `429` com `Retry-After` quando excedido. ã€F:src/app/api/auth/login/route.ts†L1-L57】 + +## Observações + +- Criado utilitário compartilhado `enforceRateLimit` (`src/lib/rate-limit.ts`) reutilizável com Redis/memória para novas rotas. ã€F:src/lib/rate-limit.ts†L1-L42】 diff --git a/docs/data-usage-optimizations.md b/docs/data-usage-optimizations.md new file mode 100644 index 000000000..f8d055925 --- /dev/null +++ b/docs/data-usage-optimizations.md @@ -0,0 +1,41 @@ +# Auditoria rápida de consumo de dados (Vercel e Redis) + +A revisão priorizou pontos que podem gerar consumo excessivo de dados (chamadas externas, logs e operações no Redis) quando a base crescer para ~4.000 usuários. + +## 1) Cache de cotações isolado por instância (Brapi) +- **Onde:** `src/lib/brapi-service.ts` +- **Por que consome:** o serviço mantém um `Map` em memória com TTL de 60s. Em Vercel cada função/lâmbda tem memória isolada, então cada instância refaz chamadas à Brapi mesmo para símbolos iguais, multiplicando tráfego externo e latência. +- **Risco:** com muitos usuários pedindo os mesmos tickers, as instâncias vão duplicar downloads e logs para a Brapi. +- **Sugerido:** substituir o `Map` local por `CacheService` (Redis) com TTL >= 5 minutos (alinhado ao revalidate da rota `/api/financial/quotes`). Isso permite cache compartilhado entre instâncias e invalidação por tags quando preciso. +- **Referência de código:** leitura e gravação no `quoteCache` local e TTL de 60s.ã€F:src/lib/brapi-service.ts†L4-L90】 + +## 2) Rate-limit gravando estruturas grandes no Redis +- **Onde:** `src/lib/rate-limit.ts` e rotas que reutilizam `CacheService` para contadores (ex.: geração de PDF). +- **Por que consome:** cada requisição faz `get`/`set` de objetos JSON (`{ count, expiresAt }`), aumentando payload e número de operações. Em escala alta, o volume de escrita/leitura cresce e pode estourar limites da Upstash. +- **Sugerido:** usar operações atômicas (`INCR` com `EX`/`PX` ou `SETNX`) no Redis para manter apenas o contador e o TTL, evitando serialização de objetos. Para rotas simples (ex.: `/api/generate-pdf`), uma única chave com `INCR` e `EX` por usuário já resolve. +- **Referência de código:** contador salvo com `CacheService.set` e recálculo de TTL a cada chamada.ã€F:src/lib/rate-limit.ts†L9-L35】ã€F:src/app/api/generate-pdf/route.ts†L16-L39】 + +## 3) Volume de logs em rotas de cotações +- **Onde:** `src/app/api/financial/quotes/route.ts` e `src/lib/brapi-service.ts`. +- **Por que consome:** a rota e o client logam todas as entradas, URLs mascaradas e status. Em Vercel, logs extensos contam como uso de dados e podem crescer rápido com milhares de requisições. +- **Sugerido:** reduzir logs em produção (p.ex., usar `NODE_ENV` para logar apenas erros), agregar métricas via `logApiMetric` e remover `console.log` de sucesso. Também considerar amostrar logs ou limitar detalhes dos símbolos. +- **Referência de código:** múltiplos `console.log` para cada request e resposta.ã€F:src/app/api/financial/quotes/route.ts†L25-L71】ã€F:src/lib/brapi-service.ts†L18-L89】 + +## 4) Padronizar TTLs e tags do `CacheService` +- **Onde:** `src/lib/redis.ts` e rotas que usam cache. +- **Por que consome:** TTL padrão é 5 min; algumas rotas usam valores diferentes sem tags consistentes, o que dificulta invalidação e pode gerar reprocessamento desnecessário (mais leituras/gravações no Redis e recomputo em Vercel). +- **Sugerido:** definir convenção de TTL por tipo de dado (ex.: quotes 5m, proventos 60m) e usar `tags` para invalidar apenas quando houver alteração. Com 4k usuários, evitar `forceRefresh` e aumentar TTL onde dados mudam pouco reduz tráfego. +- **Referência de código:** `DEFAULT_TTL_SECONDS` e construção de chaves/tags no serviço de cache.ã€F:src/lib/redis.ts†L114-L220】 + +## 5) Cache em rotas que agregam múltiplas fontes +- **Onde:** `src/app/api/ativo/[ticker]/route.ts`. +- **Por que consome:** a rota consulta três fontes externas em paralelo e só cacheia o resultado final por 3–5 minutos. Sem deduplicação por usuário, várias chamadas simultâneas podem disparar requisições repetidas. +- **Sugerido:** usar `CacheService.withCache` (com chave derivada do ticker) para encapsular o fetch e garantir deduplicação automática via `inflight` map. Ajustar TTL conforme a variabilidade dos dados e considerar tag para invalidação manual. +- **Referência de código:** cache manual com `CacheService.get/set` e TTL curto baseado em qualidade de dados.ã€F:src/app/api/ativo/[ticker]/route.ts†L6-L44】 + +### Próximos passos rápidos +1) Migrar caches locais (Brapi) para Redis compartilhado com TTL mais longo. +2) Reescrever rate limits com `INCR` + `EX` no Redis para reduzir payload e chamadas. +3) Enxugar logs informativos em produção; manter métricas estruturadas. +4) Definir uma matriz de TTL + tags por recurso e aplicar nas rotas. +5) Revisar rotas que fazem chamadas paralelas para usar `withCache` e evitar explosão de requisições simultâneas. diff --git a/docs/load-testing.md b/docs/load-testing.md new file mode 100644 index 000000000..3bb37b27f --- /dev/null +++ b/docs/load-testing.md @@ -0,0 +1,44 @@ +# Teste de carga (guia rápido) + +Este guia mostra como simular 200 usuários simultâneos (com rampa e sustentação) +usando k6 para validar throughput, latência e consumo de dados das rotas públicas. + +## Pré-requisitos + +- Node 18+ (para rodar o app localmente, se necessário) +- [k6](https://k6.io/) instalado + +## Alvo do teste + +Você pode apontar o teste para: + +- Ambiente local: `http://localhost:3000` +- Ambiente de staging/produção: `https://seu-dominio` + +## Cenário sugerido (200 usuários simultâneos) + +Este cenário faz rampa até 200 VUs, mantém por 5 minutos e desce suavemente. +As rotas são agrupadas para simular tráfego típico de painéis públicos de mercado. + +### Executar + +```bash +k6 run -e BASE_URL=https://seu-dominio scripts/k6/public-market.js +``` + +## Ajustes rápidos + +- Para simular 4000 usuários totais (não simultâneos), aumente a duração do teste. +- Para simular 4000 simultâneos, ajuste `target` nos stages (cuidado com limites do ambiente). + +## O que observar + +- **Latência p95/p99**: sinais de gargalo de CPU, I/O ou rate limit. +- **Erros 429/5xx**: limite de upstream ou saturação do servidor. +- **Cache hit ratio**: aumenta quando `Cache-Control`/CDN estão funcionando. + +## Métricas mínimas esperadas + +- p95 < 500ms para endpoints cacheados +- taxa de erro < 1% +- throughput estável durante a sustentação diff --git a/docs/performance-audit.md b/docs/performance-audit.md new file mode 100644 index 000000000..2e25d902b --- /dev/null +++ b/docs/performance-audit.md @@ -0,0 +1,17 @@ +# Auditoria de Performance e Custos + +## 🔴 Crítico +- **/api/generate-pdf** abre uma instância completa do Chromium via `puppeteer.launch` a cada requisição sem qualquer autenticação, limitação de taxa ou TTL de cache. Isso consome memória/CPU rapidamente, escala pessimamente sob volume e permite uso abusivo para geração arbitrária de PDFs gravados no S3. Sugere-se proteger com auth/rate limiting, mover o trabalho para fila/background ou serviço dedicado, e reutilizar navegador via pool ou API de renderização (ex.: Playwright service) com timeouts agressivos.ã€F:src/app/api/generate-pdf/route.ts†L7-L36】 +- **/api/instagram-cadastro** faz fallback para buscar qualquer usuário admin quando não há autenticação, o que permite que requisições anônimas façam upsert em `instagram_cadastros` usando um ID de administrador. Além do risco de segurança, a consulta `findFirst` em toda a tabela de usuários é executada em cada chamada sem cache ou rate limiting. Exigir autenticação obrigatória, remover fallback e aplicar limite de requisições são ações imediatas.ã€F:src/app/api/instagram-cadastro/route.ts†L7-L189】 + +## 🟠 Alto +- **/api/proventos GET/DELETE** retornam e manipulam todo o dataset sem paginação ou filtros (`findMany` simples) e sem seleção de campos, o que degrada latência, aumenta transferência e risco de timeouts conforme o volume cresce. Implementar paginação/streaming, selects enxutos e cache para leituras; para deleção em massa, mover para job assíncrono em vez de laço síncrono com `deleteMany` + sleeps.ã€F:src/app/api/proventos/route.ts†L5-L195】 +- **/api/proventos POST** processa uploads sequenciais lote a lote com `setTimeout` e, na fase de estatísticas, roda `aggregate` e `upsert` para cada ticker de forma serial. Isso gera N consultas síncronas e bloqueia o route handler; para muitos tickers, a duração pode exceder limites do Vercel. Paralelize com `Promise.allSettled` em lotes controlados, mova o recálculo para worker/cron e armazene progresso/estado em Redis para retentativas.ã€F:src/app/api/proventos/route.ts†L22-L263】 + +## 🟡 Médio +- **/api/relatorios GET** não aplica paginação e carrega `reportFiles` (com `take: 1`) para todos os relatórios de uma vez, mesmo quando filtrados por ticker. Em crescimento, isso pressiona memória e I/O e não usa nenhum cache/etag. Adotar paginação + cache (Redis/Next revalidate) e limitar campos retornados reduz custo e latência.ã€F:src/app/api/relatorios/route.ts†L6-L71】 +- **Notificações (POST)** cria notificações para todos os destinatários em uma única transação `prisma.$transaction([...])` sem chunking, validação de limites ou rate limiting. Um broadcast grande pode estourar tempo/consumo de conexão no banco. Quebrar em lotes (ex.: 500 IDs), usar fila/background worker e aplicar limites evita picos de custo.ã€F:src/app/api/notifications/route.ts†L88-L146】 + +## 🟢 Observações adicionais +- Muitos handlers não definem headers `Cache-Control` nem TTLs (exceto notificações), perdendo oportunidade de cache em Redis/Edge e aumentando custo de banco. +- Várias operações críticas não validam payloads com schema (ex.: Zod/Yup) e não aplicam rate limiting (ex.: middleware) — risco de abuso e requisições caras. diff --git a/docs/token-audit.md b/docs/token-audit.md new file mode 100644 index 000000000..e774a1d4d --- /dev/null +++ b/docs/token-audit.md @@ -0,0 +1,10 @@ +# Auditoria de exposição de tokens no cliente + +## Mitigações implementadas +- **Tokens carregados apenas no servidor**: os tokens reais agora são resolvidos por variáveis de ambiente em `src/server/integration-secrets.ts` e nunca entram nos bundles client-side. +- **Datasets separados**: `src/data/integrations.ts` expõe apenas metadados públicos (sem tokens) para o front-end, enquanto as rotas de webhook consomem as versões completas geradas no servidor. +- **Interfaces atualizadas**: páginas client-side mostram apenas URLs de webhook com placeholders e não copiam mais tokens para a interface. + +## Próximos passos +- Rotacionar os tokens antigos (já que foram expostos previamente) e preenchê-los via variáveis de ambiente documentadas. +- Considerar persistir integrações em banco de dados para reduzir dependência de arquivos estáticos. diff --git a/estrutura.txt b/estrutura.txt new file mode 100644 index 000000000..245573add --- /dev/null +++ b/estrutura.txt @@ -0,0 +1,2 @@ + fatos-da-bolsa-pro.png + fatos-da-bolsa-pro.png diff --git a/estrutura_completa.txt b/estrutura_completa.txt new file mode 100644 index 000000000..1bb771617 --- /dev/null +++ b/estrutura_completa.txt @@ -0,0 +1,2 @@ +| | fatos-da-bolsa-pro.png +| | fatos-da-bolsa-pro.png diff --git a/estrutura_limpa.txt b/estrutura_limpa.txt new file mode 100644 index 000000000..58200fb5b --- /dev/null +++ b/estrutura_limpa.txt @@ -0,0 +1,12078 @@ +Listagem de caminhos de pasta para o volume Acer +O número de série do volume é 20AE-A7B2 +C:. ++---.next +| +---cache +| | +---swc +| | | \---plugins +| | | \---v7_windows_x86_64_0.106.15 +| | \---webpack +| | +---client-development +| | +---client-development-fallback +| | +---client-production +| | +---edge-server-production +| | +---server-development +| | \---server-production +| +---server +| | +---app +| | | +---api +| | | | +---admin +| | | | | +---bulk-import +| | | | | +---carteiras +| | | | | +---instagram-cadastros +| | | | | +---integracoes +| | | | | | \---hotmart +| | | | | +---renovacoes +| | | | | \---users +| | | | | \---[id] +| | | | +---auth +| | | | | +---change-password +| | | | | +---login +| | | | | +---me +| | | | | \---reset-password +| | | | | \---[token] +| | | | +---carteiras +| | | | | +---minhas +| | | | | +---status-usuario +| | | | | +---upload +| | | | | \---[id] +| | | | | \---download-pdf +| | | | +---check-admin +| | | | +---dados +| | | | +---financial +| | | | | +---fii-market-data +| | | | | +---international-data +| | | | | +---market-data +| | | | | \---quotes +| | | | +---hotmart +| | | | | \---webhook +| | | | +---ifix +| | | | +---instagram-cadastro +| | | | +---market +| | | | | \---ifix +| | | | +---relatorio-semanal +| | | | +---user +| | | | | +---me +| | | | | \---profile +| | | | \---webhooks +| | | | +---eduzz +| | | | | \---[token] +| | | | +---hotmart +| | | | | \---[id] +| | | | \---kiwify +| | | | \---[token] +| | | +---auth +| | | | +---change-password +| | | | +---reset-password +| | | | | \---[token] +| | | | +---sign-in +| | | | \---sign-up +| | | +---dados-ifix +| | | +---dashboard +| | | | +---account +| | | | +---admin +| | | | | +---analises-trimesestrais +| | | | | +---carteiras +| | | | | +---import-usuarios +| | | | | +---instagram-cadastros +| | | | | +---integracoes +| | | | | | +---eduzz +| | | | | | +---hotmart +| | | | | | \---kiwify +| | | | | +---relatorio-semanal +| | | | | +---renovacoes +| | | | | \---usuarios +| | | | | +---novo +| | | | | \---[id] +| | | | +---ativo +| | | | | \---[ticker] +| | | | +---central-agenda +| | | | +---central-proventos +| | | | +---central-relatorios +| | | | +---customers +| | | | +---empresa +| | | | | \---[ticker] +| | | | +---empresa-exterior +| | | | | \---[ticker] +| | | | +---gerenciamento +| | | | +---integrations +| | | | +---internacional +| | | | | +---dividendos +| | | | | +---etfs +| | | | | +---projeto-america +| | | | | \---stocks +| | | | +---overview +| | | | +---recursos-exclusivos +| | | | | +---analise-de-carteira +| | | | | +---dicas-de-investimentos +| | | | | +---ebooks +| | | | | +---imposto-de-renda +| | | | | +---lives-e-aulas +| | | | | +---milhas-aereas +| | | | | +---planilhas +| | | | | +---reserva-emergencia +| | | | | \---telegram +| | | | +---relatorio-semanal +| | | | +---rentabilidades +| | | | \---settings +| | | +---errors +| | | | \---not-found +| | | +---teste +| | | \---_not-found +| | +---chunks +| | \---pages +| +---static +| | +---chunks +| | | +---app +| | | | +---auth +| | | | | +---change-password +| | | | | +---reset-password +| | | | | | \---[token] +| | | | | +---sign-in +| | | | | \---sign-up +| | | | +---dashboard +| | | | | +---account +| | | | | +---admin +| | | | | | +---analises-trimesestrais +| | | | | | +---carteiras +| | | | | | +---import-usuarios +| | | | | | +---instagram-cadastros +| | | | | | +---integracoes +| | | | | | | +---eduzz +| | | | | | | +---hotmart +| | | | | | | \---kiwify +| | | | | | +---relatorio-semanal +| | | | | | +---renovacoes +| | | | | | \---usuarios +| | | | | | +---novo +| | | | | | \---[id] +| | | | | +---ativo +| | | | | | \---[ticker] +| | | | | +---central-agenda +| | | | | +---central-proventos +| | | | | +---central-relatorios +| | | | | +---customers +| | | | | +---empresa +| | | | | | \---[ticker] +| | | | | +---empresa-exterior +| | | | | | \---[ticker] +| | | | | +---gerenciamento +| | | | | +---integrations +| | | | | +---internacional +| | | | | | +---dividendos +| | | | | | +---etfs +| | | | | | +---projeto-america +| | | | | | \---stocks +| | | | | +---overview +| | | | | +---recursos-exclusivos +| | | | | | +---analise-de-carteira +| | | | | | +---dicas-de-investimentos +| | | | | | +---ebooks +| | | | | | +---imposto-de-renda +| | | | | | +---lives-e-aulas +| | | | | | +---milhas-aereas +| | | | | | +---planilhas +| | | | | | +---reserva-emergencia +| | | | | | \---telegram +| | | | | +---relatorio-semanal +| | | | | +---rentabilidades +| | | | | \---settings +| | | | +---errors +| | | | | \---not-found +| | | | +---teste +| | | | \---_not-found +| | | \---pages +| | +---css +| | +---dDhLnvj9y54OxmomDBYiQ +| | \---media +| \---types +| \---app +| +---api +| | +---admin +| | | +---bulk-import +| | | +---carteiras +| | | +---instagram-cadastros +| | | +---integracoes +| | | | \---hotmart +| | | +---renovacoes +| | | \---users +| | | \---[id] +| | +---auth +| | | +---change-password +| | | +---login +| | | +---me +| | | \---reset-password +| | | \---[token] +| | +---carteiras +| | | +---minhas +| | | +---status-usuario +| | | +---upload +| | | \---[id] +| | | \---download-pdf +| | +---check-admin +| | +---dados +| | +---financial +| | | +---fii-market-data +| | | +---international-data +| | | +---market-data +| | | \---quotes +| | +---hotmart +| | | \---webhook +| | +---ifix +| | +---instagram-cadastro +| | +---market +| | | \---ifix +| | +---relatorio-semanal +| | +---user +| | | +---me +| | | \---profile +| | \---webhooks +| | +---eduzz +| | | \---[token] +| | +---hotmart +| | | \---[id] +| | \---kiwify +| | \---[token] +| +---auth +| | +---change-password +| | +---reset-password +| | | \---[token] +| | +---sign-in +| | \---sign-up +| +---dados-ifix +| +---dashboard +| | +---account +| | +---admin +| | | +---analises-trimesestrais +| | | +---carteiras +| | | +---import-usuarios +| | | +---instagram-cadastros +| | | +---integracoes +| | | | +---eduzz +| | | | +---hotmart +| | | | \---kiwify +| | | +---relatorio-semanal +| | | +---renovacoes +| | | \---usuarios +| | | +---novo +| | | \---[id] +| | +---ativo +| | | \---[ticker] +| | +---central-agenda +| | +---central-proventos +| | +---central-relatorios +| | +---customers +| | +---empresa +| | | \---[ticker] +| | +---empresa-exterior +| | | \---[ticker] +| | +---gerenciamento +| | +---integrations +| | +---internacional +| | | +---dividendos +| | | +---etfs +| | | +---projeto-america +| | | \---stocks +| | +---overview +| | +---recursos-exclusivos +| | | +---analise-de-carteira +| | | +---dicas-de-investimentos +| | | +---ebooks +| | | +---imposto-de-renda +| | | +---lives-e-aulas +| | | +---milhas-aereas +| | | +---planilhas +| | | +---reserva-emergencia +| | | \---telegram +| | +---relatorio-semanal +| | +---rentabilidades +| | \---settings +| +---errors +| | \---not-found +| \---teste ++---components ++---hooks ++---MembrosFatosdaBolsa +| +---.next +| | +---cache +| | | \---webpack +| | | +---client-development +| | | \---server-development +| | +---server +| | | +---app +| | | | +---api +| | | | | \---financial +| | | | | \---market-data +| | | | \---dashboard +| | | | \---overview +| | | \---vendor-chunks +| | +---static +| | | +---chunks +| | | | \---app +| | | | \---dashboard +| | | | \---overview +| | | +---css +| | | | \---app +| | | +---development +| | | +---media +| | | \---webpack +| | \---types +| | \---app +| | +---api +| | | \---financial +| | | \---market-data +| | \---dashboard +| | \---overview +| | +---.bin +| | +---.cache +| | | \---prisma +| | | \---master +| | | \---9b628578b3b7cae625e8c927178f15a170e74a9c +| | | \---windows +| | +---.prisma +| | | \---client +| | +---@adobe +| | | \---css-tools +| | | \---dist +| | +---@ampproject +| | | \---remapping +| | | \---dist +| | | \---types +| | +---@babel +| | | +---code-frame +| | | | \---lib +| | | +---compat-data +| | | | \---data +| | | +---core +| | | | +---lib +| | | | | +---config +| | | | | | +---files +| | | | | | +---helpers +| | | | | | \---validation +| | | | | +---errors +| | | | | +---gensync-utils +| | | | | +---parser +| | | | | | \---util +| | | | | +---tools +| | | | | +---transformation +| | | | | | +---file +| | | | | | \---util +| | | | | \---vendor +| | | | | +---.bin +| | | | | +---convert-source-map +| | | | | \---semver +| | | | | \---bin +| | | | \---src +| | | | \---config +| | | | \---files +| | | +---eslint-parser +| | | | +---lib +| | | | | +---convert +| | | | | +---utils +| | | | | \---worker +| | | | +---.bin +| | | | \---semver +| | | | \---bin +| | | +---generator +| | | | \---lib +| | | | +---generators +| | | | \---node +| | | +---helper-compilation-targets +| | | | +---lib +| | | | +---.bin +| | | | \---semver +| | | | \---bin +| | | +---helper-module-imports +| | | | \---lib +| | | +---helper-module-transforms +| | | | \---lib +| | | +---helper-plugin-utils +| | | | \---lib +| | | +---helper-string-parser +| | | | \---lib +| | | +---helper-validator-identifier +| | | | \---lib +| | | +---helper-validator-option +| | | | \---lib +| | | +---helpers +| | | | \---lib +| | | | \---helpers +| | | +---parser +| | | | +---bin +| | | | +---lib +| | | | \---typings +| | | +---plugin-syntax-async-generators +| | | | \---lib +| | | +---plugin-syntax-bigint +| | | | \---lib +| | | +---plugin-syntax-class-properties +| | | | \---lib +| | | +---plugin-syntax-class-static-block +| | | | \---lib +| | | +---plugin-syntax-import-attributes +| | | | \---lib +| | | +---plugin-syntax-import-meta +| | | | \---lib +| | | +---plugin-syntax-json-strings +| | | | \---lib +| | | +---plugin-syntax-jsx +| | | | \---lib +| | | +---plugin-syntax-logical-assignment-operators +| | | | \---lib +| | | +---plugin-syntax-nullish-coalescing-operator +| | | | \---lib +| | | +---plugin-syntax-numeric-separator +| | | | \---lib +| | | +---plugin-syntax-object-rest-spread +| | | | \---lib +| | | +---plugin-syntax-optional-catch-binding +| | | | \---lib +| | | +---plugin-syntax-optional-chaining +| | | | \---lib +| | | +---plugin-syntax-private-property-in-object +| | | | \---lib +| | | +---plugin-syntax-top-level-await +| | | | \---lib +| | | +---plugin-syntax-typescript +| | | | \---lib +| | | +---runtime +| | | | +---helpers +| | | | | \---esm +| | | | \---regenerator +| | | +---template +| | | | \---lib +| | | +---traverse +| | | | \---lib +| | | | +---path +| | | | | +---inference +| | | | | \---lib +| | | | \---scope +| | | | \---lib +| | | \---types +| | | \---lib +| | | +---asserts +| | | | \---generated +| | | +---ast-types +| | | | \---generated +| | | +---builders +| | | | +---flow +| | | | +---generated +| | | | +---react +| | | | \---typescript +| | | +---clone +| | | +---comments +| | | +---constants +| | | | \---generated +| | | +---converters +| | | +---definitions +| | | +---modifications +| | | | +---flow +| | | | \---typescript +| | | +---retrievers +| | | +---traverse +| | | +---utils +| | | | \---react +| | | \---validators +| | | +---generated +| | | \---react +| | +---@bcoe +| | | \---v8-coverage +| | | +---dist +| | | | \---lib +| | | | \---_src +| | | \---src +| | | +---lib +| | | \---test +| | +---@emotion +| | | +---babel-plugin +| | | | +---dist +| | | | | \---@emotion +| | | | | \---memoize +| | | | | +---dist +| | | | | | \---declarations +| | | | | | \---src +| | | | | \---src +| | | | \---src +| | | | \---utils +| | | +---cache +| | | | +---dist +| | | | | \---declarations +| | | | | +---src +| | | | | \---types +| | | | +---src +| | | | \---types +| | | +---hash +| | | | +---dist +| | | | | \---declarations +| | | | | \---src +| | | | \---src +| | | +---is-prop-valid +| | | | +---dist +| | | | | \---declarations +| | | | | \---src +| | | | | \---@emotion +| | | | | \---memoize +| | | | | +---dist +| | | | | | \---declarations +| | | | | | \---src +| | | | | \---src +| | | | \---src +| | | +---memoize +| | | | +---dist +| | | | | \---declarations +| | | | | +---src +| | | | | \---types +| | | | +---src +| | | | \---types +| | | +---react +| | | | +---dist +| | | | | \---declarations +| | | | | +---src +| | | | | \---types +| | | | +---jsx-dev-runtime +| | | | | \---dist +| | | | +---jsx-runtime +| | | | | \---dist +| | | | +---src +| | | | +---types +| | | | \---_isolated-hnrs +| | | | \---dist +| | | +---serialize +| | | | +---dist +| | | | | \---declarations +| | | | | \---src +| | | | | \---@emotion +| | | | | \---memoize +| | | | | +---dist +| | | | | | \---declarations +| | | | | | \---src +| | | | | \---src +| | | | \---src +| | | | \---conditions +| | | +---server +| | | | +---create-instance +| | | | | \---dist +| | | | +---dist +| | | | | \---declarations +| | | | | +---src +| | | | | | \---create-instance +| | | | | \---types +| | | | +---src +| | | | | \---create-instance +| | | | \---types +| | | +---sheet +| | | | +---dist +| | | | | \---declarations +| | | | | \---src +| | | | \---src +| | | | \---conditions +| | | +---styled +| | | | +---base +| | | | | \---dist +| | | | +---dist +| | | | | \---declarations +| | | | | +---src +| | | | | \---types +| | | | +---src +| | | | \---types +| | | +---unitless +| | | | +---dist +| | | | | \---declarations +| | | | | \---src +| | | | \---src +| | | +---use-insertion-effect-with-fallbacks +| | | | +---dist +| | | | | \---declarations +| | | | | \---src +| | | | \---src +| | | | \---conditions +| | | +---utils +| | | | +---dist +| | | | | \---declarations +| | | | | \---src +| | | | \---src +| | | | \---conditions +| | | \---weak-memoize +| | | +---dist +| | | | \---declarations +| | | | +---src +| | | | \---types +| | | +---src +| | | \---types +| | +---@eslint +| | | +---eslintrc +| | | | +---conf +| | | | +---dist +| | | | +---lib +| | | | | +---config-array +| | | | | \---shared +| | | | +---brace-expansion +| | | | +---globals +| | | | +---minimatch +| | | | \---type-fest +| | | | +---source +| | | | \---ts41 +| | | \---js +| | | \---src +| | | \---configs +| | +---@eslint-community +| | | +---eslint-utils +| | | | \---eslint-visitor-keys +| | | | +---dist +| | | | \---lib +| | | \---regexpp +| | +---@floating-ui +| | | +---core +| | | | \---dist +| | | +---dom +| | | | \---dist +| | | +---react-dom +| | | | \---dist +| | | \---utils +| | | +---dist +| | | \---dom +| | +---@fontsource +| | | +---inter +| | | | +---files +| | | | \---scss +| | | +---plus-jakarta-sans +| | | | +---files +| | | | \---scss +| | | \---roboto-mono +| | | +---files +| | | \---scss +| | +---@hookform +| | | \---resolvers +| | | +---ajv +| | | | +---dist +| | | | \---src +| | | | \---__tests__ +| | | | +---__fixtures__ +| | | | \---__snapshots__ +| | | +---arktype +| | | | +---dist +| | | | \---src +| | | | \---__tests__ +| | | | +---__fixtures__ +| | | | \---__snapshots__ +| | | +---class-validator +| | | | +---dist +| | | | \---src +| | | | \---__tests__ +| | | | +---__fixtures__ +| | | | \---__snapshots__ +| | | +---computed-types +| | | | +---dist +| | | | \---src +| | | | \---__tests__ +| | | | +---__fixtures__ +| | | | \---__snapshots__ +| | | +---dist +| | | +---effect-ts +| | | | +---dist +| | | | \---src +| | | | \---__tests__ +| | | | +---__fixtures__ +| | | | \---__snapshots__ +| | | +---io-ts +| | | | +---dist +| | | | \---src +| | | | \---__tests__ +| | | | +---__fixtures__ +| | | | \---__snapshots__ +| | | +---joi +| | | | +---dist +| | | | \---src +| | | | \---__tests__ +| | | | +---__fixtures__ +| | | | \---__snapshots__ +| | | +---nope +| | | | +---dist +| | | | \---src +| | | | \---__tests__ +| | | | +---__fixtures__ +| | | | \---__snapshots__ +| | | +---superstruct +| | | | +---dist +| | | | \---src +| | | | \---__tests__ +| | | | +---__fixtures__ +| | | | \---__snapshots__ +| | | +---typanion +| | | | +---dist +| | | | \---src +| | | | \---__tests__ +| | | | +---__fixtures__ +| | | | \---__snapshots__ +| | | +---typebox +| | | | +---dist +| | | | \---src +| | | | \---__tests__ +| | | | +---__fixtures__ +| | | | \---__snapshots__ +| | | +---valibot +| | | | +---dist +| | | | \---src +| | | | \---__tests__ +| | | | +---__fixtures__ +| | | | \---__snapshots__ +| | | +---vest +| | | | +---dist +| | | | \---src +| | | | \---__tests__ +| | | | +---__fixtures__ +| | | | \---__snapshots__ +| | | +---yup +| | | | +---dist +| | | | \---src +| | | | \---__tests__ +| | | | +---__fixtures__ +| | | | \---__snapshots__ +| | | \---zod +| | | +---dist +| | | \---src +| | | \---__tests__ +| | | +---__fixtures__ +| | | \---__snapshots__ +| | +---@humanwhocodes +| | | +---config-array +| | | | +---brace-expansion +| | | | \---minimatch +| | | +---module-importer +| | | | +---dist +| | | | \---src +| | | \---object-schema +| | | \---src +| | +---@ianvs +| | | \---prettier-plugin-sort-imports +| | | +---lib +| | | | \---src +| | | | +---natural-sort +| | | | +---preprocessors +| | | | \---utils +| | | \---types +| | +---@isaacs +| | | \---cliui +| | | +---build +| | | | \---lib +| | | +---ansi-regex +| | | \---strip-ansi +| | +---@istanbuljs +| | | +---load-nyc-config +| | | | +---.bin +| | | | +---argparse +| | | | | \---lib +| | | | | +---action +| | | | | | +---append +| | | | | | \---store +| | | | | +---argument +| | | | | \---help +| | | | +---find-up +| | | | +---js-yaml +| | | | | +---bin +| | | | | +---dist +| | | | | \---lib +| | | | | \---js-yaml +| | | | | +---schema +| | | | | \---type +| | | | | \---js +| | | | +---locate-path +| | | | +---p-limit +| | | | +---p-locate +| | | | \---resolve-from +| | | \---schema +| | +---@jest +| | | +---console +| | | | \---build +| | | +---core +| | | | +---build +| | | | | +---cli +| | | | | +---lib +| | | | | \---plugins +| | | | +---ansi-styles +| | | | +---ci-info +| | | | \---pretty-format +| | | | \---build +| | | | \---plugins +| | | | \---lib +| | | +---environment +| | | | \---build +| | | +---expect +| | | | \---build +| | | +---expect-utils +| | | | \---build +| | | +---fake-timers +| | | | \---build +| | | +---globals +| | | | \---build +| | | +---reporters +| | | | +---assets +| | | | +---build +| | | | +---brace-expansion +| | | | +---glob +| | | | \---minimatch +| | | +---schemas +| | | | \---build +| | | +---source-map +| | | | \---build +| | | +---test-result +| | | | \---build +| | | +---test-sequencer +| | | | \---build +| | | +---transform +| | | | +---build +| | | | \---convert-source-map +| | | \---types +| | | \---build +| | +---@jridgewell +| | | +---gen-mapping +| | | | +---dist +| | | | +---src +| | | | \---types +| | | +---resolve-uri +| | | | \---dist +| | | | \---types +| | | +---sourcemap-codec +| | | | +---dist +| | | | +---src +| | | | \---types +| | | \---trace-mapping +| | | +---dist +| | | +---src +| | | \---types +| | +---@microsoft +| | | +---tsdoc +| | | | +---lib +| | | | | +---beta +| | | | | | \---__tests__ +| | | | | +---configuration +| | | | | +---details +| | | | | +---emitters +| | | | | | \---__tests__ +| | | | | +---nodes +| | | | | +---parser +| | | | | | \---__tests__ +| | | | | +---transforms +| | | | | \---__tests__ +| | | | +---lib-commonjs +| | | | | +---beta +| | | | | | \---__tests__ +| | | | | +---configuration +| | | | | +---details +| | | | | +---emitters +| | | | | | \---__tests__ +| | | | | +---nodes +| | | | | +---parser +| | | | | | \---__tests__ +| | | | | +---transforms +| | | | | \---__tests__ +| | | | \---schemas +| | | \---tsdoc-config +| | | +---lib +| | | | \---__tests__ +| | | | \---assets +| | | | +---e1 +| | | | +---e2 +| | | | +---e3 +| | | | +---e4 +| | | | +---e5 +| | | | +---e6 +| | | | +---e7 +| | | | +---p1 +| | | | +---p10 +| | | | | \---base1 +| | | | +---p11 +| | | | | +---base1 +| | | | | \---base2 +| | | | +---p12 +| | | | +---p2 +| | | | +---p3 +| | | | | +---base1 +| | | | | \---base2 +| | | | +---p4 +| | | | | \---example-lib +| | | | | \---dist +| | | | +---p5 +| | | | | +---base1 +| | | | | \---base2 +| | | | +---p6 +| | | | | \---base1 +| | | | +---p7 +| | | | +---p8 +| | | | | \---base1 +| | | | \---p9 +| | | | \---base1 +| | | \---resolve +| | | +---.github +| | | | \---workflows +| | | +---example +| | | +---lib +| | | \---test +| | | +---dotdot +| | | | \---abc +| | | +---module_dir +| | | | +---xmodules +| | | | | \---aaa +| | | | +---ymodules +| | | | | \---aaa +| | | | \---zmodules +| | | | \---bbb +| | | +---node_path +| | | | +---x +| | | | | +---aaa +| | | | | \---ccc +| | | | \---y +| | | | +---bbb +| | | | \---ccc +| | | +---pathfilter +| | | | \---deep_ref +| | | +---precedence +| | | | +---aaa +| | | | \---bbb +| | | +---resolver +| | | | +---baz +| | | | +---browser_field +| | | | +---dot_main +| | | | +---dot_slash_main +| | | | +---incorrect_main +| | | | +---invalid_main +| | | | +---multirepo +| | | | | \---packages +| | | | | +---package-a +| | | | | \---package-b +| | | | +---nested_symlinks +| | | | | \---mylib +| | | | +---other_path +| | | | | \---lib +| | | | +---quux +| | | | | \---foo +| | | | +---same_names +| | | | | \---foo +| | | | +---symlinked +| | | | | +---package +| | | | | \---_ +| | | | | \---symlink_target +| | | | \---without_basedir +| | | \---shadowed_core +| | | \---util +| | +---@mui +| | | +---base +| | | | +---Badge +| | | | +---Button +| | | | +---ClassNameGenerator +| | | | +---ClickAwayListener +| | | | +---composeClasses +| | | | +---Dropdown +| | | | +---FocusTrap +| | | | +---FormControl +| | | | +---generateUtilityClass +| | | | +---generateUtilityClasses +| | | | +---Input +| | | | +---legacy +| | | | | +---Badge +| | | | | +---Button +| | | | | +---ClassNameGenerator +| | | | | +---ClickAwayListener +| | | | | +---composeClasses +| | | | | +---Dropdown +| | | | | +---FocusTrap +| | | | | +---FormControl +| | | | | +---generateUtilityClass +| | | | | +---generateUtilityClasses +| | | | | +---Input +| | | | | +---Menu +| | | | | +---MenuButton +| | | | | +---MenuItem +| | | | | +---Modal +| | | | | +---MultiSelect +| | | | | +---NoSsr +| | | | | +---Option +| | | | | +---OptionGroup +| | | | | +---Popper +| | | | | +---Portal +| | | | | +---Select +| | | | | +---Slider +| | | | | +---Snackbar +| | | | | +---Switch +| | | | | +---Tab +| | | | | +---TablePagination +| | | | | +---TabPanel +| | | | | +---Tabs +| | | | | +---TabsList +| | | | | +---TextareaAutosize +| | | | | +---Transitions +| | | | | +---Unstable_NumberInput +| | | | | +---Unstable_Popup +| | | | | +---unstable_useModal +| | | | | +---unstable_useNumberInput +| | | | | +---useAutocomplete +| | | | | +---useBadge +| | | | | +---useButton +| | | | | +---useCompound +| | | | | +---useDropdown +| | | | | +---useInput +| | | | | +---useList +| | | | | +---useMenu +| | | | | +---useMenuButton +| | | | | +---useMenuItem +| | | | | +---useOption +| | | | | +---useSelect +| | | | | +---useSlider +| | | | | +---useSnackbar +| | | | | +---useSwitch +| | | | | +---useTab +| | | | | +---useTabPanel +| | | | | +---useTabs +| | | | | +---useTabsList +| | | | | +---useTransition +| | | | | \---utils +| | | | +---Menu +| | | | +---MenuButton +| | | | +---MenuItem +| | | | +---Modal +| | | | +---modern +| | | | | +---Badge +| | | | | +---Button +| | | | | +---ClassNameGenerator +| | | | | +---ClickAwayListener +| | | | | +---composeClasses +| | | | | +---Dropdown +| | | | | +---FocusTrap +| | | | | +---FormControl +| | | | | +---generateUtilityClass +| | | | | +---generateUtilityClasses +| | | | | +---Input +| | | | | +---Menu +| | | | | +---MenuButton +| | | | | +---MenuItem +| | | | | +---Modal +| | | | | +---MultiSelect +| | | | | +---NoSsr +| | | | | +---Option +| | | | | +---OptionGroup +| | | | | +---Popper +| | | | | +---Portal +| | | | | +---Select +| | | | | +---Slider +| | | | | +---Snackbar +| | | | | +---Switch +| | | | | +---Tab +| | | | | +---TablePagination +| | | | | +---TabPanel +| | | | | +---Tabs +| | | | | +---TabsList +| | | | | +---TextareaAutosize +| | | | | +---Transitions +| | | | | +---Unstable_NumberInput +| | | | | +---Unstable_Popup +| | | | | +---unstable_useModal +| | | | | +---unstable_useNumberInput +| | | | | +---useAutocomplete +| | | | | +---useBadge +| | | | | +---useButton +| | | | | +---useCompound +| | | | | +---useDropdown +| | | | | +---useInput +| | | | | +---useList +| | | | | +---useMenu +| | | | | +---useMenuButton +| | | | | +---useMenuItem +| | | | | +---useOption +| | | | | +---useSelect +| | | | | +---useSlider +| | | | | +---useSnackbar +| | | | | +---useSwitch +| | | | | +---useTab +| | | | | +---useTabPanel +| | | | | +---useTabs +| | | | | +---useTabsList +| | | | | +---useTransition +| | | | | \---utils +| | | | +---MultiSelect +| | | | +---node +| | | | | +---Badge +| | | | | +---Button +| | | | | +---ClassNameGenerator +| | | | | +---ClickAwayListener +| | | | | +---composeClasses +| | | | | +---Dropdown +| | | | | +---FocusTrap +| | | | | +---FormControl +| | | | | +---generateUtilityClass +| | | | | +---generateUtilityClasses +| | | | | +---Input +| | | | | +---Menu +| | | | | +---MenuButton +| | | | | +---MenuItem +| | | | | +---Modal +| | | | | +---MultiSelect +| | | | | +---NoSsr +| | | | | +---Option +| | | | | +---OptionGroup +| | | | | +---Popper +| | | | | +---Portal +| | | | | +---Select +| | | | | +---Slider +| | | | | +---Snackbar +| | | | | +---Switch +| | | | | +---Tab +| | | | | +---TablePagination +| | | | | +---TabPanel +| | | | | +---Tabs +| | | | | +---TabsList +| | | | | +---TextareaAutosize +| | | | | +---Transitions +| | | | | +---Unstable_NumberInput +| | | | | +---Unstable_Popup +| | | | | +---unstable_useModal +| | | | | +---unstable_useNumberInput +| | | | | +---useAutocomplete +| | | | | +---useBadge +| | | | | +---useButton +| | | | | +---useCompound +| | | | | +---useDropdown +| | | | | +---useInput +| | | | | +---useList +| | | | | +---useMenu +| | | | | +---useMenuButton +| | | | | +---useMenuItem +| | | | | +---useOption +| | | | | +---useSelect +| | | | | +---useSlider +| | | | | +---useSnackbar +| | | | | +---useSwitch +| | | | | +---useTab +| | | | | +---useTabPanel +| | | | | +---useTabs +| | | | | +---useTabsList +| | | | | +---useTransition +| | | | | \---utils +| | | | +---NoSsr +| | | | +---Option +| | | | +---OptionGroup +| | | | +---Popper +| | | | +---Portal +| | | | +---Select +| | | | +---Slider +| | | | +---Snackbar +| | | | +---Switch +| | | | +---Tab +| | | | +---TablePagination +| | | | +---TabPanel +| | | | +---Tabs +| | | | +---TabsList +| | | | +---TextareaAutosize +| | | | +---Transitions +| | | | +---Unstable_NumberInput +| | | | +---Unstable_Popup +| | | | +---unstable_useModal +| | | | +---unstable_useNumberInput +| | | | +---useAutocomplete +| | | | +---useBadge +| | | | +---useButton +| | | | +---useCompound +| | | | +---useDropdown +| | | | +---useInput +| | | | +---useList +| | | | +---useMenu +| | | | +---useMenuButton +| | | | +---useMenuItem +| | | | +---useOption +| | | | +---useSelect +| | | | +---useSlider +| | | | +---useSnackbar +| | | | +---useSwitch +| | | | +---useTab +| | | | +---useTabPanel +| | | | +---useTabs +| | | | +---useTabsList +| | | | +---useTransition +| | | | \---utils +| | | +---core-downloads-tracker +| | | +---lab +| | | | +---AdapterDateFns +| | | | +---AdapterDayjs +| | | | +---AdapterLuxon +| | | | +---AdapterMoment +| | | | +---Alert +| | | | +---AlertTitle +| | | | +---Autocomplete +| | | | +---AvatarGroup +| | | | +---CalendarPicker +| | | | +---CalendarPickerSkeleton +| | | | +---ClockPicker +| | | | +---DatePicker +| | | | +---DateRangePicker +| | | | +---DateRangePickerDay +| | | | +---DateTimePicker +| | | | +---DesktopDatePicker +| | | | +---DesktopDateRangePicker +| | | | +---DesktopDateTimePicker +| | | | +---DesktopTimePicker +| | | | +---internal +| | | | | \---svg-icons +| | | | +---legacy +| | | | | +---AdapterDateFns +| | | | | +---AdapterDayjs +| | | | | +---AdapterLuxon +| | | | | +---AdapterMoment +| | | | | +---Alert +| | | | | +---AlertTitle +| | | | | +---Autocomplete +| | | | | +---AvatarGroup +| | | | | +---CalendarPicker +| | | | | +---CalendarPickerSkeleton +| | | | | +---ClockPicker +| | | | | +---DatePicker +| | | | | +---DateRangePicker +| | | | | +---DateRangePickerDay +| | | | | +---DateTimePicker +| | | | | +---DesktopDatePicker +| | | | | +---DesktopDateRangePicker +| | | | | +---DesktopDateTimePicker +| | | | | +---DesktopTimePicker +| | | | | +---internal +| | | | | | \---svg-icons +| | | | | +---LoadingButton +| | | | | +---LocalizationProvider +| | | | | +---Masonry +| | | | | +---MobileDatePicker +| | | | | +---MobileDateRangePicker +| | | | | +---MobileDateTimePicker +| | | | | +---MobileTimePicker +| | | | | +---MonthPicker +| | | | | +---Pagination +| | | | | +---PaginationItem +| | | | | +---PickersDay +| | | | | +---Rating +| | | | | +---Skeleton +| | | | | +---SpeedDial +| | | | | +---SpeedDialAction +| | | | | +---SpeedDialIcon +| | | | | +---StaticDatePicker +| | | | | +---StaticDateRangePicker +| | | | | +---StaticDateTimePicker +| | | | | +---StaticTimePicker +| | | | | +---TabContext +| | | | | +---TabList +| | | | | +---TabPanel +| | | | | +---themeAugmentation +| | | | | +---Timeline +| | | | | +---TimelineConnector +| | | | | +---TimelineContent +| | | | | +---TimelineDot +| | | | | +---TimelineItem +| | | | | +---TimelineOppositeContent +| | | | | +---TimelineSeparator +| | | | | +---TimePicker +| | | | | +---ToggleButton +| | | | | +---ToggleButtonGroup +| | | | | +---TreeItem +| | | | | +---TreeView +| | | | | +---useAutocomplete +| | | | | \---YearPicker +| | | | +---LoadingButton +| | | | +---LocalizationProvider +| | | | +---Masonry +| | | | +---MobileDatePicker +| | | | +---MobileDateRangePicker +| | | | +---MobileDateTimePicker +| | | | +---MobileTimePicker +| | | | +---modern +| | | | | +---AdapterDateFns +| | | | | +---AdapterDayjs +| | | | | +---AdapterLuxon +| | | | | +---AdapterMoment +| | | | | +---Alert +| | | | | +---AlertTitle +| | | | | +---Autocomplete +| | | | | +---AvatarGroup +| | | | | +---CalendarPicker +| | | | | +---CalendarPickerSkeleton +| | | | | +---ClockPicker +| | | | | +---DatePicker +| | | | | +---DateRangePicker +| | | | | +---DateRangePickerDay +| | | | | +---DateTimePicker +| | | | | +---DesktopDatePicker +| | | | | +---DesktopDateRangePicker +| | | | | +---DesktopDateTimePicker +| | | | | +---DesktopTimePicker +| | | | | +---internal +| | | | | | \---svg-icons +| | | | | +---LoadingButton +| | | | | +---LocalizationProvider +| | | | | +---Masonry +| | | | | +---MobileDatePicker +| | | | | +---MobileDateRangePicker +| | | | | +---MobileDateTimePicker +| | | | | +---MobileTimePicker +| | | | | +---MonthPicker +| | | | | +---Pagination +| | | | | +---PaginationItem +| | | | | +---PickersDay +| | | | | +---Rating +| | | | | +---Skeleton +| | | | | +---SpeedDial +| | | | | +---SpeedDialAction +| | | | | +---SpeedDialIcon +| | | | | +---StaticDatePicker +| | | | | +---StaticDateRangePicker +| | | | | +---StaticDateTimePicker +| | | | | +---StaticTimePicker +| | | | | +---TabContext +| | | | | +---TabList +| | | | | +---TabPanel +| | | | | +---themeAugmentation +| | | | | +---Timeline +| | | | | +---TimelineConnector +| | | | | +---TimelineContent +| | | | | +---TimelineDot +| | | | | +---TimelineItem +| | | | | +---TimelineOppositeContent +| | | | | +---TimelineSeparator +| | | | | +---TimePicker +| | | | | +---ToggleButton +| | | | | +---ToggleButtonGroup +| | | | | +---TreeItem +| | | | | +---TreeView +| | | | | +---useAutocomplete +| | | | | \---YearPicker +| | | | +---MonthPicker +| | | | +---node +| | | | | +---AdapterDateFns +| | | | | +---AdapterDayjs +| | | | | +---AdapterLuxon +| | | | | +---AdapterMoment +| | | | | +---Alert +| | | | | +---AlertTitle +| | | | | +---Autocomplete +| | | | | +---AvatarGroup +| | | | | +---CalendarPicker +| | | | | +---CalendarPickerSkeleton +| | | | | +---ClockPicker +| | | | | +---DatePicker +| | | | | +---DateRangePicker +| | | | | +---DateRangePickerDay +| | | | | +---DateTimePicker +| | | | | +---DesktopDatePicker +| | | | | +---DesktopDateRangePicker +| | | | | +---DesktopDateTimePicker +| | | | | +---DesktopTimePicker +| | | | | +---internal +| | | | | | \---svg-icons +| | | | | +---LoadingButton +| | | | | +---LocalizationProvider +| | | | | +---Masonry +| | | | | +---MobileDatePicker +| | | | | +---MobileDateRangePicker +| | | | | +---MobileDateTimePicker +| | | | | +---MobileTimePicker +| | | | | +---MonthPicker +| | | | | +---Pagination +| | | | | +---PaginationItem +| | | | | +---PickersDay +| | | | | +---Rating +| | | | | +---Skeleton +| | | | | +---SpeedDial +| | | | | +---SpeedDialAction +| | | | | +---SpeedDialIcon +| | | | | +---StaticDatePicker +| | | | | +---StaticDateRangePicker +| | | | | +---StaticDateTimePicker +| | | | | +---StaticTimePicker +| | | | | +---TabContext +| | | | | +---TabList +| | | | | +---TabPanel +| | | | | +---themeAugmentation +| | | | | +---Timeline +| | | | | +---TimelineConnector +| | | | | +---TimelineContent +| | | | | +---TimelineDot +| | | | | +---TimelineItem +| | | | | +---TimelineOppositeContent +| | | | | +---TimelineSeparator +| | | | | +---TimePicker +| | | | | +---ToggleButton +| | | | | +---ToggleButtonGroup +| | | | | +---TreeItem +| | | | | +---TreeView +| | | | | +---useAutocomplete +| | | | | \---YearPicker +| | | | +---Pagination +| | | | +---PaginationItem +| | | | +---PickersDay +| | | | +---Rating +| | | | +---Skeleton +| | | | +---SpeedDial +| | | | +---SpeedDialAction +| | | | +---SpeedDialIcon +| | | | +---StaticDatePicker +| | | | +---StaticDateRangePicker +| | | | +---StaticDateTimePicker +| | | | +---StaticTimePicker +| | | | +---TabContext +| | | | +---TabList +| | | | +---TabPanel +| | | | +---themeAugmentation +| | | | +---Timeline +| | | | +---TimelineConnector +| | | | +---TimelineContent +| | | | +---TimelineDot +| | | | +---TimelineItem +| | | | +---TimelineOppositeContent +| | | | +---TimelineSeparator +| | | | +---TimePicker +| | | | +---ToggleButton +| | | | +---ToggleButtonGroup +| | | | +---TreeItem +| | | | +---TreeView +| | | | +---useAutocomplete +| | | | \---YearPicker +| | | +---material +| | | | +---Accordion +| | | | +---AccordionActions +| | | | +---AccordionDetails +| | | | +---AccordionSummary +| | | | +---Alert +| | | | +---AlertTitle +| | | | +---AppBar +| | | | +---Autocomplete +| | | | +---Avatar +| | | | +---AvatarGroup +| | | | +---Backdrop +| | | | +---Badge +| | | | +---BottomNavigation +| | | | +---BottomNavigationAction +| | | | +---Box +| | | | +---Breadcrumbs +| | | | +---Button +| | | | +---ButtonBase +| | | | +---ButtonGroup +| | | | +---Card +| | | | +---CardActionArea +| | | | +---CardActions +| | | | +---CardContent +| | | | +---CardHeader +| | | | +---CardMedia +| | | | +---Checkbox +| | | | +---Chip +| | | | +---CircularProgress +| | | | +---className +| | | | +---ClickAwayListener +| | | | +---Collapse +| | | | +---colors +| | | | +---Container +| | | | +---CssBaseline +| | | | +---darkScrollbar +| | | | +---Dialog +| | | | +---DialogActions +| | | | +---DialogContent +| | | | +---DialogContentText +| | | | +---DialogTitle +| | | | +---Divider +| | | | +---Drawer +| | | | +---Fab +| | | | +---Fade +| | | | +---FilledInput +| | | | +---FormControl +| | | | +---FormControlLabel +| | | | +---FormGroup +| | | | +---FormHelperText +| | | | +---FormLabel +| | | | +---generateUtilityClass +| | | | +---generateUtilityClasses +| | | | +---GlobalStyles +| | | | +---Grid +| | | | +---Grow +| | | | +---Hidden +| | | | +---Icon +| | | | +---IconButton +| | | | +---ImageList +| | | | +---ImageListItem +| | | | +---ImageListItemBar +| | | | +---Input +| | | | +---InputAdornment +| | | | +---InputBase +| | | | +---InputLabel +| | | | +---internal +| | | | | \---svg-icons +| | | | +---legacy +| | | | | +---Accordion +| | | | | +---AccordionActions +| | | | | +---AccordionDetails +| | | | | +---AccordionSummary +| | | | | +---Alert +| | | | | +---AlertTitle +| | | | | +---AppBar +| | | | | +---Autocomplete +| | | | | +---Avatar +| | | | | +---AvatarGroup +| | | | | +---Backdrop +| | | | | +---Badge +| | | | | +---BottomNavigation +| | | | | +---BottomNavigationAction +| | | | | +---Box +| | | | | +---Breadcrumbs +| | | | | +---Button +| | | | | +---ButtonBase +| | | | | +---ButtonGroup +| | | | | +---Card +| | | | | +---CardActionArea +| | | | | +---CardActions +| | | | | +---CardContent +| | | | | +---CardHeader +| | | | | +---CardMedia +| | | | | +---Checkbox +| | | | | +---Chip +| | | | | +---CircularProgress +| | | | | +---className +| | | | | +---ClickAwayListener +| | | | | +---Collapse +| | | | | +---colors +| | | | | +---Container +| | | | | +---CssBaseline +| | | | | +---darkScrollbar +| | | | | +---Dialog +| | | | | +---DialogActions +| | | | | +---DialogContent +| | | | | +---DialogContentText +| | | | | +---DialogTitle +| | | | | +---Divider +| | | | | +---Drawer +| | | | | +---Fab +| | | | | +---Fade +| | | | | +---FilledInput +| | | | | +---FormControl +| | | | | +---FormControlLabel +| | | | | +---FormGroup +| | | | | +---FormHelperText +| | | | | +---FormLabel +| | | | | +---generateUtilityClass +| | | | | +---generateUtilityClasses +| | | | | +---GlobalStyles +| | | | | +---Grid +| | | | | +---Grow +| | | | | +---Hidden +| | | | | +---Icon +| | | | | +---IconButton +| | | | | +---ImageList +| | | | | +---ImageListItem +| | | | | +---ImageListItemBar +| | | | | +---Input +| | | | | +---InputAdornment +| | | | | +---InputBase +| | | | | +---InputLabel +| | | | | +---internal +| | | | | | \---svg-icons +| | | | | +---LinearProgress +| | | | | +---Link +| | | | | +---List +| | | | | +---ListItem +| | | | | +---ListItemAvatar +| | | | | +---ListItemButton +| | | | | +---ListItemIcon +| | | | | +---ListItemSecondaryAction +| | | | | +---ListItemText +| | | | | +---ListSubheader +| | | | | +---locale +| | | | | +---Menu +| | | | | +---MenuItem +| | | | | +---MenuList +| | | | | +---MobileStepper +| | | | | +---Modal +| | | | | +---NativeSelect +| | | | | +---NoSsr +| | | | | +---OutlinedInput +| | | | | +---Pagination +| | | | | +---PaginationItem +| | | | | +---Paper +| | | | | +---Popover +| | | | | +---Popper +| | | | | +---Portal +| | | | | +---Radio +| | | | | +---RadioGroup +| | | | | +---Rating +| | | | | +---ScopedCssBaseline +| | | | | +---Select +| | | | | +---Skeleton +| | | | | +---Slide +| | | | | +---Slider +| | | | | +---Snackbar +| | | | | +---SnackbarContent +| | | | | +---SpeedDial +| | | | | +---SpeedDialAction +| | | | | +---SpeedDialIcon +| | | | | +---Stack +| | | | | +---Step +| | | | | +---StepButton +| | | | | +---StepConnector +| | | | | +---StepContent +| | | | | +---StepIcon +| | | | | +---StepLabel +| | | | | +---Stepper +| | | | | +---StyledEngineProvider +| | | | | +---styles +| | | | | +---SvgIcon +| | | | | +---SwipeableDrawer +| | | | | +---Switch +| | | | | +---Tab +| | | | | +---Table +| | | | | +---TableBody +| | | | | +---TableCell +| | | | | +---TableContainer +| | | | | +---TableFooter +| | | | | +---TableHead +| | | | | +---TablePagination +| | | | | +---TableRow +| | | | | +---TableSortLabel +| | | | | +---Tabs +| | | | | +---TabScrollButton +| | | | | +---TextareaAutosize +| | | | | +---TextField +| | | | | +---ToggleButton +| | | | | +---ToggleButtonGroup +| | | | | +---Toolbar +| | | | | +---Tooltip +| | | | | +---transitions +| | | | | +---types +| | | | | +---Typography +| | | | | +---Unstable_Grid2 +| | | | | +---Unstable_TrapFocus +| | | | | +---useAutocomplete +| | | | | +---useMediaQuery +| | | | | +---usePagination +| | | | | +---useScrollTrigger +| | | | | +---useTouchRipple +| | | | | +---utils +| | | | | +---zero-styled +| | | | | \---Zoom +| | | | +---LinearProgress +| | | | +---Link +| | | | +---List +| | | | +---ListItem +| | | | +---ListItemAvatar +| | | | +---ListItemButton +| | | | +---ListItemIcon +| | | | +---ListItemSecondaryAction +| | | | +---ListItemText +| | | | +---ListSubheader +| | | | +---locale +| | | | +---Menu +| | | | +---MenuItem +| | | | +---MenuList +| | | | +---MobileStepper +| | | | +---Modal +| | | | +---modern +| | | | | +---Accordion +| | | | | +---AccordionActions +| | | | | +---AccordionDetails +| | | | | +---AccordionSummary +| | | | | +---Alert +| | | | | +---AlertTitle +| | | | | +---AppBar +| | | | | +---Autocomplete +| | | | | +---Avatar +| | | | | +---AvatarGroup +| | | | | +---Backdrop +| | | | | +---Badge +| | | | | +---BottomNavigation +| | | | | +---BottomNavigationAction +| | | | | +---Box +| | | | | +---Breadcrumbs +| | | | | +---Button +| | | | | +---ButtonBase +| | | | | +---ButtonGroup +| | | | | +---Card +| | | | | +---CardActionArea +| | | | | +---CardActions +| | | | | +---CardContent +| | | | | +---CardHeader +| | | | | +---CardMedia +| | | | | +---Checkbox +| | | | | +---Chip +| | | | | +---CircularProgress +| | | | | +---className +| | | | | +---ClickAwayListener +| | | | | +---Collapse +| | | | | +---colors +| | | | | +---Container +| | | | | +---CssBaseline +| | | | | +---darkScrollbar +| | | | | +---Dialog +| | | | | +---DialogActions +| | | | | +---DialogContent +| | | | | +---DialogContentText +| | | | | +---DialogTitle +| | | | | +---Divider +| | | | | +---Drawer +| | | | | +---Fab +| | | | | +---Fade +| | | | | +---FilledInput +| | | | | +---FormControl +| | | | | +---FormControlLabel +| | | | | +---FormGroup +| | | | | +---FormHelperText +| | | | | +---FormLabel +| | | | | +---generateUtilityClass +| | | | | +---generateUtilityClasses +| | | | | +---GlobalStyles +| | | | | +---Grid +| | | | | +---Grow +| | | | | +---Hidden +| | | | | +---Icon +| | | | | +---IconButton +| | | | | +---ImageList +| | | | | +---ImageListItem +| | | | | +---ImageListItemBar +| | | | | +---Input +| | | | | +---InputAdornment +| | | | | +---InputBase +| | | | | +---InputLabel +| | | | | +---internal +| | | | | | \---svg-icons +| | | | | +---LinearProgress +| | | | | +---Link +| | | | | +---List +| | | | | +---ListItem +| | | | | +---ListItemAvatar +| | | | | +---ListItemButton +| | | | | +---ListItemIcon +| | | | | +---ListItemSecondaryAction +| | | | | +---ListItemText +| | | | | +---ListSubheader +| | | | | +---locale +| | | | | +---Menu +| | | | | +---MenuItem +| | | | | +---MenuList +| | | | | +---MobileStepper +| | | | | +---Modal +| | | | | +---NativeSelect +| | | | | +---NoSsr +| | | | | +---OutlinedInput +| | | | | +---Pagination +| | | | | +---PaginationItem +| | | | | +---Paper +| | | | | +---Popover +| | | | | +---Popper +| | | | | +---Portal +| | | | | +---Radio +| | | | | +---RadioGroup +| | | | | +---Rating +| | | | | +---ScopedCssBaseline +| | | | | +---Select +| | | | | +---Skeleton +| | | | | +---Slide +| | | | | +---Slider +| | | | | +---Snackbar +| | | | | +---SnackbarContent +| | | | | +---SpeedDial +| | | | | +---SpeedDialAction +| | | | | +---SpeedDialIcon +| | | | | +---Stack +| | | | | +---Step +| | | | | +---StepButton +| | | | | +---StepConnector +| | | | | +---StepContent +| | | | | +---StepIcon +| | | | | +---StepLabel +| | | | | +---Stepper +| | | | | +---StyledEngineProvider +| | | | | +---styles +| | | | | +---SvgIcon +| | | | | +---SwipeableDrawer +| | | | | +---Switch +| | | | | +---Tab +| | | | | +---Table +| | | | | +---TableBody +| | | | | +---TableCell +| | | | | +---TableContainer +| | | | | +---TableFooter +| | | | | +---TableHead +| | | | | +---TablePagination +| | | | | +---TableRow +| | | | | +---TableSortLabel +| | | | | +---Tabs +| | | | | +---TabScrollButton +| | | | | +---TextareaAutosize +| | | | | +---TextField +| | | | | +---ToggleButton +| | | | | +---ToggleButtonGroup +| | | | | +---Toolbar +| | | | | +---Tooltip +| | | | | +---transitions +| | | | | +---types +| | | | | +---Typography +| | | | | +---Unstable_Grid2 +| | | | | +---Unstable_TrapFocus +| | | | | +---useAutocomplete +| | | | | +---useMediaQuery +| | | | | +---usePagination +| | | | | +---useScrollTrigger +| | | | | +---useTouchRipple +| | | | | +---utils +| | | | | +---zero-styled +| | | | | \---Zoom +| | | | +---NativeSelect +| | | | +---node +| | | | | +---Accordion +| | | | | +---AccordionActions +| | | | | +---AccordionDetails +| | | | | +---AccordionSummary +| | | | | +---Alert +| | | | | +---AlertTitle +| | | | | +---AppBar +| | | | | +---Autocomplete +| | | | | +---Avatar +| | | | | +---AvatarGroup +| | | | | +---Backdrop +| | | | | +---Badge +| | | | | +---BottomNavigation +| | | | | +---BottomNavigationAction +| | | | | +---Box +| | | | | +---Breadcrumbs +| | | | | +---Button +| | | | | +---ButtonBase +| | | | | +---ButtonGroup +| | | | | +---Card +| | | | | +---CardActionArea +| | | | | +---CardActions +| | | | | +---CardContent +| | | | | +---CardHeader +| | | | | +---CardMedia +| | | | | +---Checkbox +| | | | | +---Chip +| | | | | +---CircularProgress +| | | | | +---className +| | | | | +---ClickAwayListener +| | | | | +---Collapse +| | | | | +---colors +| | | | | +---Container +| | | | | +---CssBaseline +| | | | | +---darkScrollbar +| | | | | +---Dialog +| | | | | +---DialogActions +| | | | | +---DialogContent +| | | | | +---DialogContentText +| | | | | +---DialogTitle +| | | | | +---Divider +| | | | | +---Drawer +| | | | | +---Fab +| | | | | +---Fade +| | | | | +---FilledInput +| | | | | +---FormControl +| | | | | +---FormControlLabel +| | | | | +---FormGroup +| | | | | +---FormHelperText +| | | | | +---FormLabel +| | | | | +---generateUtilityClass +| | | | | +---generateUtilityClasses +| | | | | +---GlobalStyles +| | | | | +---Grid +| | | | | +---Grow +| | | | | +---Hidden +| | | | | +---Icon +| | | | | +---IconButton +| | | | | +---ImageList +| | | | | +---ImageListItem +| | | | | +---ImageListItemBar +| | | | | +---Input +| | | | | +---InputAdornment +| | | | | +---InputBase +| | | | | +---InputLabel +| | | | | +---internal +| | | | | | \---svg-icons +| | | | | +---LinearProgress +| | | | | +---Link +| | | | | +---List +| | | | | +---ListItem +| | | | | +---ListItemAvatar +| | | | | +---ListItemButton +| | | | | +---ListItemIcon +| | | | | +---ListItemSecondaryAction +| | | | | +---ListItemText +| | | | | +---ListSubheader +| | | | | +---locale +| | | | | +---Menu +| | | | | +---MenuItem +| | | | | +---MenuList +| | | | | +---MobileStepper +| | | | | +---Modal +| | | | | +---NativeSelect +| | | | | +---NoSsr +| | | | | +---OutlinedInput +| | | | | +---Pagination +| | | | | +---PaginationItem +| | | | | +---Paper +| | | | | +---Popover +| | | | | +---Popper +| | | | | +---Portal +| | | | | +---Radio +| | | | | +---RadioGroup +| | | | | +---Rating +| | | | | +---ScopedCssBaseline +| | | | | +---Select +| | | | | +---Skeleton +| | | | | +---Slide +| | | | | +---Slider +| | | | | +---Snackbar +| | | | | +---SnackbarContent +| | | | | +---SpeedDial +| | | | | +---SpeedDialAction +| | | | | +---SpeedDialIcon +| | | | | +---Stack +| | | | | +---Step +| | | | | +---StepButton +| | | | | +---StepConnector +| | | | | +---StepContent +| | | | | +---StepIcon +| | | | | +---StepLabel +| | | | | +---Stepper +| | | | | +---StyledEngineProvider +| | | | | +---styles +| | | | | +---SvgIcon +| | | | | +---SwipeableDrawer +| | | | | +---Switch +| | | | | +---Tab +| | | | | +---Table +| | | | | +---TableBody +| | | | | +---TableCell +| | | | | +---TableContainer +| | | | | +---TableFooter +| | | | | +---TableHead +| | | | | +---TablePagination +| | | | | +---TableRow +| | | | | +---TableSortLabel +| | | | | +---Tabs +| | | | | +---TabScrollButton +| | | | | +---TextareaAutosize +| | | | | +---TextField +| | | | | +---ToggleButton +| | | | | +---ToggleButtonGroup +| | | | | +---Toolbar +| | | | | +---Tooltip +| | | | | +---transitions +| | | | | +---types +| | | | | +---Typography +| | | | | +---Unstable_Grid2 +| | | | | +---Unstable_TrapFocus +| | | | | +---useAutocomplete +| | | | | +---useMediaQuery +| | | | | +---usePagination +| | | | | +---useScrollTrigger +| | | | | +---useTouchRipple +| | | | | +---utils +| | | | | +---zero-styled +| | | | | \---Zoom +| | | | +---NoSsr +| | | | +---OutlinedInput +| | | | +---Pagination +| | | | +---PaginationItem +| | | | +---Paper +| | | | +---Popover +| | | | +---Popper +| | | | +---Portal +| | | | +---Radio +| | | | +---RadioGroup +| | | | +---Rating +| | | | +---ScopedCssBaseline +| | | | +---Select +| | | | +---Skeleton +| | | | +---Slide +| | | | +---Slider +| | | | +---Snackbar +| | | | +---SnackbarContent +| | | | +---SpeedDial +| | | | +---SpeedDialAction +| | | | +---SpeedDialIcon +| | | | +---Stack +| | | | +---Step +| | | | +---StepButton +| | | | +---StepConnector +| | | | +---StepContent +| | | | +---StepIcon +| | | | +---StepLabel +| | | | +---Stepper +| | | | +---StyledEngineProvider +| | | | +---styles +| | | | +---SvgIcon +| | | | +---SwipeableDrawer +| | | | +---Switch +| | | | +---Tab +| | | | +---Table +| | | | +---TableBody +| | | | +---TableCell +| | | | +---TableContainer +| | | | +---TableFooter +| | | | +---TableHead +| | | | +---TablePagination +| | | | +---TableRow +| | | | +---TableSortLabel +| | | | +---Tabs +| | | | +---TabScrollButton +| | | | +---TextareaAutosize +| | | | +---TextField +| | | | +---themeCssVarsAugmentation +| | | | +---ToggleButton +| | | | +---ToggleButtonGroup +| | | | +---Toolbar +| | | | +---Tooltip +| | | | +---transitions +| | | | +---types +| | | | +---Typography +| | | | +---umd +| | | | +---Unstable_Grid2 +| | | | +---Unstable_TrapFocus +| | | | +---useAutocomplete +| | | | +---useMediaQuery +| | | | +---usePagination +| | | | +---useScrollTrigger +| | | | +---useTouchRipple +| | | | +---utils +| | | | +---zero-styled +| | | | \---Zoom +| | | +---private-theming +| | | | +---defaultTheme +| | | | +---legacy +| | | | | +---ThemeProvider +| | | | | \---useTheme +| | | | +---modern +| | | | | +---ThemeProvider +| | | | | \---useTheme +| | | | +---node +| | | | | +---ThemeProvider +| | | | | \---useTheme +| | | | | +---@mui +| | | | | | +---types +| | | | | | \---utils +| | | | | | +---appendOwnerState +| | | | | | +---capitalize +| | | | | | +---chainPropTypes +| | | | | | +---clamp +| | | | | | +---ClassNameGenerator +| | | | | | +---composeClasses +| | | | | | +---createChainedFunction +| | | | | | +---debounce +| | | | | | +---deepmerge +| | | | | | +---deprecatedPropType +| | | | | | +---elementAcceptingRef +| | | | | | +---elementTypeAcceptingRef +| | | | | | +---esm +| | | | | | | +---appendOwnerState +| | | | | | | +---capitalize +| | | | | | | +---chainPropTypes +| | | | | | | +---clamp +| | | | | | | +---ClassNameGenerator +| | | | | | | +---composeClasses +| | | | | | | +---createChainedFunction +| | | | | | | +---debounce +| | | | | | | +---deepmerge +| | | | | | | +---deprecatedPropType +| | | | | | | +---elementAcceptingRef +| | | | | | | +---elementTypeAcceptingRef +| | | | | | | +---exactProp +| | | | | | | +---extractEventHandlers +| | | | | | | +---formatMuiErrorMessage +| | | | | | | +---generateUtilityClass +| | | | | | | +---generateUtilityClasses +| | | | | | | +---getDisplayName +| | | | | | | +---getReactElementRef +| | | | | | | +---getScrollbarSize +| | | | | | | +---getValidReactChildren +| | | | | | | +---HTMLElementType +| | | | | | | +---integerPropType +| | | | | | | +---isHostComponent +| | | | | | | +---isMuiElement +| | | | | | | +---mergeSlotProps +| | | | | | | +---omitEventHandlers +| | | | | | | +---ownerDocument +| | | | | | | +---ownerWindow +| | | | | | | +---ponyfillGlobal +| | | | | | | +---refType +| | | | | | | +---requirePropFactory +| | | | | | | +---resolveComponentProps +| | | | | | | +---resolveProps +| | | | | | | +---scrollLeft +| | | | | | | +---setRef +| | | | | | | +---unsupportedProp +| | | | | | | +---useControlled +| | | | | | | +---useEnhancedEffect +| | | | | | | +---useEventCallback +| | | | | | | +---useForkRef +| | | | | | | +---useId +| | | | | | | +---useIsFocusVisible +| | | | | | | +---useLazyRef +| | | | | | | +---useLocalStorageState +| | | | | | | +---useOnMount +| | | | | | | +---usePreviousProps +| | | | | | | +---useSlotProps +| | | | | | | +---useTimeout +| | | | | | | \---visuallyHidden +| | | | | | +---exactProp +| | | | | | +---extractEventHandlers +| | | | | | +---formatMuiErrorMessage +| | | | | | +---generateUtilityClass +| | | | | | +---generateUtilityClasses +| | | | | | +---getDisplayName +| | | | | | +---getReactElementRef +| | | | | | +---getScrollbarSize +| | | | | | +---getValidReactChildren +| | | | | | +---HTMLElementType +| | | | | | +---integerPropType +| | | | | | +---isHostComponent +| | | | | | +---isMuiElement +| | | | | | +---legacy +| | | | | | | +---appendOwnerState +| | | | | | | +---capitalize +| | | | | | | +---chainPropTypes +| | | | | | | +---clamp +| | | | | | | +---ClassNameGenerator +| | | | | | | +---composeClasses +| | | | | | | +---createChainedFunction +| | | | | | | +---debounce +| | | | | | | +---deepmerge +| | | | | | | +---deprecatedPropType +| | | | | | | +---elementAcceptingRef +| | | | | | | +---elementTypeAcceptingRef +| | | | | | | +---exactProp +| | | | | | | +---extractEventHandlers +| | | | | | | +---formatMuiErrorMessage +| | | | | | | +---generateUtilityClass +| | | | | | | +---generateUtilityClasses +| | | | | | | +---getDisplayName +| | | | | | | +---getReactElementRef +| | | | | | | +---getScrollbarSize +| | | | | | | +---getValidReactChildren +| | | | | | | +---HTMLElementType +| | | | | | | +---integerPropType +| | | | | | | +---isHostComponent +| | | | | | | +---isMuiElement +| | | | | | | +---mergeSlotProps +| | | | | | | +---omitEventHandlers +| | | | | | | +---ownerDocument +| | | | | | | +---ownerWindow +| | | | | | | +---ponyfillGlobal +| | | | | | | +---refType +| | | | | | | +---requirePropFactory +| | | | | | | +---resolveComponentProps +| | | | | | | +---resolveProps +| | | | | | | +---scrollLeft +| | | | | | | +---setRef +| | | | | | | +---unsupportedProp +| | | | | | | +---useControlled +| | | | | | | +---useEnhancedEffect +| | | | | | | +---useEventCallback +| | | | | | | +---useForkRef +| | | | | | | +---useId +| | | | | | | +---useIsFocusVisible +| | | | | | | +---useLazyRef +| | | | | | | +---useLocalStorageState +| | | | | | | +---useOnMount +| | | | | | | +---usePreviousProps +| | | | | | | +---useSlotProps +| | | | | | | +---useTimeout +| | | | | | | \---visuallyHidden +| | | | | | +---mergeSlotProps +| | | | | | +---modern +| | | | | | | +---appendOwnerState +| | | | | | | +---capitalize +| | | | | | | +---chainPropTypes +| | | | | | | +---clamp +| | | | | | | +---ClassNameGenerator +| | | | | | | +---composeClasses +| | | | | | | +---createChainedFunction +| | | | | | | +---debounce +| | | | | | | +---deepmerge +| | | | | | | +---deprecatedPropType +| | | | | | | +---elementAcceptingRef +| | | | | | | +---elementTypeAcceptingRef +| | | | | | | +---exactProp +| | | | | | | +---extractEventHandlers +| | | | | | | +---formatMuiErrorMessage +| | | | | | | +---generateUtilityClass +| | | | | | | +---generateUtilityClasses +| | | | | | | +---getDisplayName +| | | | | | | +---getReactElementRef +| | | | | | | +---getScrollbarSize +| | | | | | | +---getValidReactChildren +| | | | | | | +---HTMLElementType +| | | | | | | +---integerPropType +| | | | | | | +---isHostComponent +| | | | | | | +---isMuiElement +| | | | | | | +---mergeSlotProps +| | | | | | | +---omitEventHandlers +| | | | | | | +---ownerDocument +| | | | | | | +---ownerWindow +| | | | | | | +---ponyfillGlobal +| | | | | | | +---refType +| | | | | | | +---requirePropFactory +| | | | | | | +---resolveComponentProps +| | | | | | | +---resolveProps +| | | | | | | +---scrollLeft +| | | | | | | +---setRef +| | | | | | | +---unsupportedProp +| | | | | | | +---useControlled +| | | | | | | +---useEnhancedEffect +| | | | | | | +---useEventCallback +| | | | | | | +---useForkRef +| | | | | | | +---useId +| | | | | | | +---useIsFocusVisible +| | | | | | | +---useLazyRef +| | | | | | | +---useLocalStorageState +| | | | | | | +---useOnMount +| | | | | | | +---usePreviousProps +| | | | | | | +---useSlotProps +| | | | | | | +---useTimeout +| | | | | | | \---visuallyHidden +| | | | | | +---omitEventHandlers +| | | | | | +---ownerDocument +| | | | | | +---ownerWindow +| | | | | | +---ponyfillGlobal +| | | | | | +---refType +| | | | | | +---requirePropFactory +| | | | | | +---resolveComponentProps +| | | | | | +---resolveProps +| | | | | | +---scrollLeft +| | | | | | +---setRef +| | | | | | +---unsupportedProp +| | | | | | +---useControlled +| | | | | | +---useEnhancedEffect +| | | | | | +---useEventCallback +| | | | | | +---useForkRef +| | | | | | +---useId +| | | | | | +---useIsFocusVisible +| | | | | | +---useLazyRef +| | | | | | +---useLocalStorageState +| | | | | | +---useOnMount +| | | | | | +---usePreviousProps +| | | | | | +---useSlotProps +| | | | | | +---useTimeout +| | | | | | \---visuallyHidden +| | | | | \---react-is +| | | | | \---cjs +| | | | +---ThemeProvider +| | | | \---useTheme +| | | +---styled-engine +| | | | +---GlobalStyles +| | | | +---legacy +| | | | | +---GlobalStyles +| | | | | \---StyledEngineProvider +| | | | +---modern +| | | | | +---GlobalStyles +| | | | | \---StyledEngineProvider +| | | | +---node +| | | | | +---GlobalStyles +| | | | | \---StyledEngineProvider +| | | | | \---@emotion +| | | | | +---cache +| | | | | | +---dist +| | | | | | | \---declarations +| | | | | | | \---src +| | | | | | \---src +| | | | | | \---conditions +| | | | | +---memoize +| | | | | | +---dist +| | | | | | | \---declarations +| | | | | | | \---src +| | | | | | \---src +| | | | | \---weak-memoize +| | | | | +---dist +| | | | | | \---declarations +| | | | | | \---src +| | | | | \---src +| | | | \---StyledEngineProvider +| | | +---system +| | | | +---Box +| | | | +---Container +| | | | +---createTheme +| | | | +---cssVars +| | | | +---esm +| | | | | +---Box +| | | | | +---Container +| | | | | +---createTheme +| | | | | +---cssVars +| | | | | +---GlobalStyles +| | | | | +---RtlProvider +| | | | | +---Stack +| | | | | +---styleFunctionSx +| | | | | +---ThemeProvider +| | | | | +---Unstable_Grid +| | | | | +---useMediaQuery +| | | | | \---useThemeProps +| | | | +---GlobalStyles +| | | | +---legacy +| | | | | +---Box +| | | | | +---Container +| | | | | +---createTheme +| | | | | +---cssVars +| | | | | +---GlobalStyles +| | | | | +---RtlProvider +| | | | | +---Stack +| | | | | +---styleFunctionSx +| | | | | +---ThemeProvider +| | | | | +---Unstable_Grid +| | | | | +---useMediaQuery +| | | | | \---useThemeProps +| | | | +---modern +| | | | | +---Box +| | | | | +---Container +| | | | | +---createTheme +| | | | | +---cssVars +| | | | | +---GlobalStyles +| | | | | +---RtlProvider +| | | | | +---Stack +| | | | | +---styleFunctionSx +| | | | | +---ThemeProvider +| | | | | +---Unstable_Grid +| | | | | +---useMediaQuery +| | | | | \---useThemeProps +| | | | +---RtlProvider +| | | | +---Stack +| | | | +---styleFunctionSx +| | | | +---ThemeProvider +| | | | +---Unstable_Grid +| | | | +---useMediaQuery +| | | | \---useThemeProps +| | | +---types +| | | | \---esm +| | | +---utils +| | | | +---capitalize +| | | | +---chainPropTypes +| | | | +---clamp +| | | | +---ClassNameGenerator +| | | | +---composeClasses +| | | | +---createChainedFunction +| | | | +---debounce +| | | | +---deepmerge +| | | | +---deprecatedPropType +| | | | +---elementAcceptingRef +| | | | +---elementTypeAcceptingRef +| | | | +---exactProp +| | | | +---formatMuiErrorMessage +| | | | +---generateUtilityClass +| | | | +---generateUtilityClasses +| | | | +---getDisplayName +| | | | +---getScrollbarSize +| | | | +---getValidReactChildren +| | | | +---HTMLElementType +| | | | +---integerPropType +| | | | +---isMuiElement +| | | | +---legacy +| | | | | +---capitalize +| | | | | +---chainPropTypes +| | | | | +---clamp +| | | | | +---ClassNameGenerator +| | | | | +---composeClasses +| | | | | +---createChainedFunction +| | | | | +---debounce +| | | | | +---deepmerge +| | | | | +---deprecatedPropType +| | | | | +---elementAcceptingRef +| | | | | +---elementTypeAcceptingRef +| | | | | +---exactProp +| | | | | +---formatMuiErrorMessage +| | | | | +---generateUtilityClass +| | | | | +---generateUtilityClasses +| | | | | +---getDisplayName +| | | | | +---getScrollbarSize +| | | | | +---getValidReactChildren +| | | | | +---HTMLElementType +| | | | | +---integerPropType +| | | | | +---isMuiElement +| | | | | +---ownerDocument +| | | | | +---ownerWindow +| | | | | +---ponyfillGlobal +| | | | | +---refType +| | | | | +---requirePropFactory +| | | | | +---resolveProps +| | | | | +---scrollLeft +| | | | | +---setRef +| | | | | +---unsupportedProp +| | | | | +---useControlled +| | | | | +---useEnhancedEffect +| | | | | +---useEventCallback +| | | | | +---useForkRef +| | | | | +---useId +| | | | | +---useIsFocusVisible +| | | | | +---useLazyRef +| | | | | +---useLocalStorageState +| | | | | +---useOnMount +| | | | | +---usePreviousProps +| | | | | +---useTimeout +| | | | | \---visuallyHidden +| | | | +---modern +| | | | | +---capitalize +| | | | | +---chainPropTypes +| | | | | +---clamp +| | | | | +---ClassNameGenerator +| | | | | +---composeClasses +| | | | | +---createChainedFunction +| | | | | +---debounce +| | | | | +---deepmerge +| | | | | +---deprecatedPropType +| | | | | +---elementAcceptingRef +| | | | | +---elementTypeAcceptingRef +| | | | | +---exactProp +| | | | | +---formatMuiErrorMessage +| | | | | +---generateUtilityClass +| | | | | +---generateUtilityClasses +| | | | | +---getDisplayName +| | | | | +---getScrollbarSize +| | | | | +---getValidReactChildren +| | | | | +---HTMLElementType +| | | | | +---integerPropType +| | | | | +---isMuiElement +| | | | | +---ownerDocument +| | | | | +---ownerWindow +| | | | | +---ponyfillGlobal +| | | | | +---refType +| | | | | +---requirePropFactory +| | | | | +---resolveProps +| | | | | +---scrollLeft +| | | | | +---setRef +| | | | | +---unsupportedProp +| | | | | +---useControlled +| | | | | +---useEnhancedEffect +| | | | | +---useEventCallback +| | | | | +---useForkRef +| | | | | +---useId +| | | | | +---useIsFocusVisible +| | | | | +---useLazyRef +| | | | | +---useLocalStorageState +| | | | | +---useOnMount +| | | | | +---usePreviousProps +| | | | | +---useTimeout +| | | | | \---visuallyHidden +| | | | +---node +| | | | | +---capitalize +| | | | | +---chainPropTypes +| | | | | +---clamp +| | | | | +---ClassNameGenerator +| | | | | +---composeClasses +| | | | | +---createChainedFunction +| | | | | +---debounce +| | | | | +---deepmerge +| | | | | +---deprecatedPropType +| | | | | +---elementAcceptingRef +| | | | | +---elementTypeAcceptingRef +| | | | | +---exactProp +| | | | | +---formatMuiErrorMessage +| | | | | +---generateUtilityClass +| | | | | +---generateUtilityClasses +| | | | | +---getDisplayName +| | | | | +---getScrollbarSize +| | | | | +---getValidReactChildren +| | | | | +---HTMLElementType +| | | | | +---integerPropType +| | | | | +---isMuiElement +| | | | | +---ownerDocument +| | | | | +---ownerWindow +| | | | | +---ponyfillGlobal +| | | | | +---refType +| | | | | +---requirePropFactory +| | | | | +---resolveProps +| | | | | +---scrollLeft +| | | | | +---setRef +| | | | | +---unsupportedProp +| | | | | +---useControlled +| | | | | +---useEnhancedEffect +| | | | | +---useEventCallback +| | | | | +---useForkRef +| | | | | +---useId +| | | | | +---useIsFocusVisible +| | | | | +---useLazyRef +| | | | | +---useLocalStorageState +| | | | | +---useOnMount +| | | | | +---usePreviousProps +| | | | | +---useTimeout +| | | | | \---visuallyHidden +| | | | +---ownerDocument +| | | | +---ownerWindow +| | | | +---ponyfillGlobal +| | | | +---refType +| | | | +---requirePropFactory +| | | | +---resolveProps +| | | | +---scrollLeft +| | | | +---setRef +| | | | +---unsupportedProp +| | | | +---useControlled +| | | | +---useEnhancedEffect +| | | | +---useEventCallback +| | | | +---useForkRef +| | | | +---useId +| | | | +---useIsFocusVisible +| | | | +---useLazyRef +| | | | +---useLocalStorageState +| | | | +---useOnMount +| | | | +---usePreviousProps +| | | | +---useTimeout +| | | | \---visuallyHidden +| | | \---x-date-pickers +| | | +---AdapterDateFns +| | | +---AdapterDateFnsBase +| | | +---AdapterDateFnsJalali +| | | +---AdapterDateFnsJalaliV3 +| | | +---AdapterDateFnsV3 +| | | +---AdapterDayjs +| | | +---AdapterLuxon +| | | +---AdapterMoment +| | | +---AdapterMomentHijri +| | | +---AdapterMomentJalaali +| | | +---DateCalendar +| | | +---DateField +| | | +---DatePicker +| | | +---DateTimeField +| | | +---DateTimePicker +| | | +---dateViewRenderers +| | | +---DayCalendarSkeleton +| | | +---DesktopDatePicker +| | | +---DesktopDateTimePicker +| | | +---DesktopTimePicker +| | | +---DigitalClock +| | | +---hooks +| | | +---icons +| | | +---internals +| | | | +---components +| | | | | +---PickersArrowSwitcher +| | | | | \---PickerViewRoot +| | | | +---constants +| | | | +---demo +| | | | +---hooks +| | | | | +---useDesktopPicker +| | | | | +---useField +| | | | | +---useMobilePicker +| | | | | +---usePicker +| | | | | \---useStaticPicker +| | | | +---models +| | | | | \---props +| | | | \---utils +| | | | \---validation +| | | +---locales +| | | | \---utils +| | | +---LocalizationProvider +| | | +---MobileDatePicker +| | | +---MobileDateTimePicker +| | | +---MobileTimePicker +| | | +---models +| | | +---modern +| | | | +---AdapterDateFns +| | | | +---AdapterDateFnsBase +| | | | +---AdapterDateFnsJalali +| | | | +---AdapterDateFnsJalaliV3 +| | | | +---AdapterDateFnsV3 +| | | | +---AdapterDayjs +| | | | +---AdapterLuxon +| | | | +---AdapterMoment +| | | | +---AdapterMomentHijri +| | | | +---AdapterMomentJalaali +| | | | +---DateCalendar +| | | | +---DateField +| | | | +---DatePicker +| | | | +---DateTimeField +| | | | +---DateTimePicker +| | | | +---dateViewRenderers +| | | | +---DayCalendarSkeleton +| | | | +---DesktopDatePicker +| | | | +---DesktopDateTimePicker +| | | | +---DesktopTimePicker +| | | | +---DigitalClock +| | | | +---hooks +| | | | +---icons +| | | | +---internals +| | | | | +---components +| | | | | | +---PickersArrowSwitcher +| | | | | | \---PickerViewRoot +| | | | | +---constants +| | | | | +---demo +| | | | | +---hooks +| | | | | | +---useDesktopPicker +| | | | | | +---useField +| | | | | | +---useMobilePicker +| | | | | | +---usePicker +| | | | | | \---useStaticPicker +| | | | | +---models +| | | | | | \---props +| | | | | \---utils +| | | | | \---validation +| | | | +---locales +| | | | | \---utils +| | | | +---LocalizationProvider +| | | | +---MobileDatePicker +| | | | +---MobileDateTimePicker +| | | | +---MobileTimePicker +| | | | +---models +| | | | +---MonthCalendar +| | | | +---MultiSectionDigitalClock +| | | | +---PickersActionBar +| | | | +---PickersCalendarHeader +| | | | +---PickersDay +| | | | +---PickersLayout +| | | | +---PickersSectionList +| | | | +---PickersShortcuts +| | | | +---PickersTextField +| | | | | +---PickersFilledInput +| | | | | +---PickersInput +| | | | | +---PickersInputBase +| | | | | \---PickersOutlinedInput +| | | | +---StaticDatePicker +| | | | +---StaticDateTimePicker +| | | | +---StaticTimePicker +| | | | +---themeAugmentation +| | | | +---TimeClock +| | | | +---TimeField +| | | | +---TimePicker +| | | | +---timeViewRenderers +| | | | \---YearCalendar +| | | +---MonthCalendar +| | | +---MultiSectionDigitalClock +| | | +---node +| | | | +---AdapterDateFns +| | | | +---AdapterDateFnsBase +| | | | +---AdapterDateFnsJalali +| | | | +---AdapterDateFnsJalaliV3 +| | | | +---AdapterDateFnsV3 +| | | | +---AdapterDayjs +| | | | +---AdapterLuxon +| | | | +---AdapterMoment +| | | | +---AdapterMomentHijri +| | | | +---AdapterMomentJalaali +| | | | +---DateCalendar +| | | | +---DateField +| | | | +---DatePicker +| | | | +---DateTimeField +| | | | +---DateTimePicker +| | | | +---dateViewRenderers +| | | | +---DayCalendarSkeleton +| | | | +---DesktopDatePicker +| | | | +---DesktopDateTimePicker +| | | | +---DesktopTimePicker +| | | | +---DigitalClock +| | | | +---hooks +| | | | +---icons +| | | | +---internals +| | | | | +---components +| | | | | | +---PickersArrowSwitcher +| | | | | | \---PickerViewRoot +| | | | | +---constants +| | | | | +---demo +| | | | | +---hooks +| | | | | | +---useDesktopPicker +| | | | | | +---useField +| | | | | | +---useMobilePicker +| | | | | | +---usePicker +| | | | | | \---useStaticPicker +| | | | | +---models +| | | | | | \---props +| | | | | \---utils +| | | | | \---validation +| | | | +---locales +| | | | | \---utils +| | | | +---LocalizationProvider +| | | | +---MobileDatePicker +| | | | +---MobileDateTimePicker +| | | | +---MobileTimePicker +| | | | +---models +| | | | +---MonthCalendar +| | | | +---MultiSectionDigitalClock +| | | | +---PickersActionBar +| | | | +---PickersCalendarHeader +| | | | +---PickersDay +| | | | +---PickersLayout +| | | | +---PickersSectionList +| | | | +---PickersShortcuts +| | | | +---PickersTextField +| | | | | +---PickersFilledInput +| | | | | +---PickersInput +| | | | | +---PickersInputBase +| | | | | \---PickersOutlinedInput +| | | | +---StaticDatePicker +| | | | +---StaticDateTimePicker +| | | | +---StaticTimePicker +| | | | +---themeAugmentation +| | | | +---TimeClock +| | | | +---TimeField +| | | | +---TimePicker +| | | | +---timeViewRenderers +| | | | \---YearCalendar +| | | +---PickersActionBar +| | | +---PickersCalendarHeader +| | | +---PickersDay +| | | +---PickersLayout +| | | +---PickersSectionList +| | | +---PickersShortcuts +| | | +---PickersTextField +| | | | +---PickersFilledInput +| | | | +---PickersInput +| | | | +---PickersInputBase +| | | | \---PickersOutlinedInput +| | | +---StaticDatePicker +| | | +---StaticDateTimePicker +| | | +---StaticTimePicker +| | | +---themeAugmentation +| | | +---TimeClock +| | | +---TimeField +| | | +---TimePicker +| | | +---timeViewRenderers +| | | \---YearCalendar +| | +---@napi-rs +| | +---@next +| | | +---env +| | | | \---dist +| | | +---eslint-plugin-next +| | | | \---dist +| | | | +---rules +| | | | \---utils +| | | \---swc-win32-x64-msvc +| | +---@nicolo-ribaudo +| | | \---eslint-scope-5-internals +| | +---@nodelib +| | | +---fs.scandir +| | | | \---out +| | | | +---adapters +| | | | +---providers +| | | | +---types +| | | | \---utils +| | | +---fs.stat +| | | | \---out +| | | | +---adapters +| | | | +---providers +| | | | \---types +| | | \---fs.walk +| | | \---out +| | | +---providers +| | | +---readers +| | | \---types +| | +---@nolyfill +| | | \---is-core-module +| | +---@phosphor-icons +| | | \---react +| | | \---dist +| | | +---csr +| | | +---defs +| | | +---lib +| | | \---ssr +| | +---@pkgjs +| | | \---parseargs +| | | +---examples +| | | \---internal +| | +---@pkgr +| | | \---core +| | | \---lib +| | +---@popperjs +| | | \---core +| | | +---dist +| | | | +---cjs +| | | | +---esm +| | | | | +---dom-utils +| | | | | +---modifiers +| | | | | \---utils +| | | | \---umd +| | | \---lib +| | | +---dom-utils +| | | +---modifiers +| | | \---utils +| | +---@prisma +| | | +---client +| | | | +---generator-build +| | | | +---runtime +| | | | \---scripts +| | | +---config +| | | | \---dist +| | | +---debug +| | | | \---dist +| | | +---engines +| | | | +---dist +| | | | | \---scripts +| | | | | \---.cache +| | | | | \---prisma +| | | | | \---master +| | | | | \---9b628578b3b7cae625e8c927178f15a170e74a9c +| | | | | \---windows +| | | | \---scripts +| | | +---engines-version +| | | +---fetch-engine +| | | | \---dist +| | | \---get-platform +| | | \---dist +| | | \---test-utils +| | +---@rtsao +| | | \---scc +| | +---@rushstack +| | | \---eslint-patch +| | | \---lib +| | | +---eslint-bulk-suppressions +| | | | \---cli +| | | | \---utils +| | | \---exports +| | +---@sinclair +| | | \---typebox +| | | +---compiler +| | | +---errors +| | | +---system +| | | \---value +| | +---@sinonjs +| | | +---commons +| | | | +---lib +| | | | | \---prototypes +| | | | \---types +| | | | \---prototypes +| | | \---fake-timers +| | | \---src +| | +---@swc +| | | +---counter +| | | \---helpers +| | | +---cjs +| | | +---esm +| | | +---scripts +| | | +---src +| | | \---_ +| | | +---index +| | | +---_apply_decorated_descriptor +| | | +---_apply_decs_2203_r +| | | +---_array_like_to_array +| | | +---_array_without_holes +| | | +---_array_with_holes +| | | +---_assert_this_initialized +| | | +---_async_generator +| | | +---_async_generator_delegate +| | | +---_async_iterator +| | | +---_async_to_generator +| | | +---_await_async_generator +| | | +---_await_value +| | | +---_check_private_redeclaration +| | | +---_class_apply_descriptor_destructure +| | | +---_class_apply_descriptor_get +| | | +---_class_apply_descriptor_set +| | | +---_class_apply_descriptor_update +| | | +---_class_call_check +| | | +---_class_check_private_static_access +| | | +---_class_check_private_static_field_descriptor +| | | +---_class_extract_field_descriptor +| | | +---_class_name_tdz_error +| | | +---_class_private_field_destructure +| | | +---_class_private_field_get +| | | +---_class_private_field_init +| | | +---_class_private_field_loose_base +| | | +---_class_private_field_loose_key +| | | +---_class_private_field_set +| | | +---_class_private_field_update +| | | +---_class_private_method_get +| | | +---_class_private_method_init +| | | +---_class_private_method_set +| | | +---_class_static_private_field_destructure +| | | +---_class_static_private_field_spec_get +| | | +---_class_static_private_field_spec_set +| | | +---_class_static_private_field_update +| | | +---_class_static_private_method_get +| | | +---_construct +| | | +---_create_class +| | | +---_create_for_of_iterator_helper_loose +| | | +---_create_super +| | | +---_decorate +| | | +---_defaults +| | | +---_define_enumerable_properties +| | | +---_define_property +| | | +---_dispose +| | | +---_export_star +| | | +---_extends +| | | +---_get +| | | +---_get_prototype_of +| | | +---_inherits +| | | +---_inherits_loose +| | | +---_initializer_define_property +| | | +---_initializer_warning_helper +| | | +---_instanceof +| | | +---_interop_require_default +| | | +---_interop_require_wildcard +| | | +---_is_native_function +| | | +---_is_native_reflect_construct +| | | +---_iterable_to_array +| | | +---_iterable_to_array_limit +| | | +---_iterable_to_array_limit_loose +| | | +---_jsx +| | | +---_new_arrow_check +| | | +---_non_iterable_rest +| | | +---_non_iterable_spread +| | | +---_object_destructuring_empty +| | | +---_object_spread +| | | +---_object_spread_props +| | | +---_object_without_properties +| | | +---_object_without_properties_loose +| | | +---_possible_constructor_return +| | | +---_read_only_error +| | | +---_set +| | | +---_set_prototype_of +| | | +---_skip_first_generator_next +| | | +---_sliced_to_array +| | | +---_sliced_to_array_loose +| | | +---_super_prop_base +| | | +---_tagged_template_literal +| | | +---_tagged_template_literal_loose +| | | +---_throw +| | | +---_to_array +| | | +---_to_consumable_array +| | | +---_to_primitive +| | | +---_to_property_key +| | | +---_ts_decorate +| | | +---_ts_generator +| | | +---_ts_metadata +| | | +---_ts_param +| | | +---_ts_values +| | | +---_type_of +| | | +---_unsupported_iterable_to_array +| | | +---_update +| | | +---_using +| | | +---_wrap_async_generator +| | | +---_wrap_native_super +| | | \---_write_only_error +| | +---@testing-library +| | | +---dom +| | | | +---dist +| | | | | +---@testing-library +| | | | | \---queries +| | | | \---types +| | | +---jest-dom +| | | | +---dist +| | | | | +---chalk +| | | | | | \---source +| | | | | \---dom-accessibility-api +| | | | | \---dist +| | | | | \---polyfills +| | | | \---types +| | | | \---__tests__ +| | | | +---bun +| | | | +---jest +| | | | +---jest-globals +| | | | \---vitest +| | | \---react +| | | +---dist +| | | | \---@testing-library +| | | \---types +| | +---@tootallnate +| | | \---once +| | | \---dist +| | +---@tybys +| | +---@types +| | | +---aria-query +| | | +---babel__core +| | | +---babel__generator +| | | +---babel__template +| | | +---babel__traverse +| | | +---bcryptjs +| | | +---d3-array +| | | +---d3-color +| | | +---d3-ease +| | | +---d3-interpolate +| | | +---d3-path +| | | +---d3-scale +| | | +---d3-shape +| | | +---d3-time +| | | +---d3-timer +| | | +---geojson +| | | +---graceful-fs +| | | +---istanbul-lib-coverage +| | | +---istanbul-lib-report +| | | +---istanbul-reports +| | | +---jest +| | | | +---ansi-styles +| | | | \---pretty-format +| | | | \---build +| | | | \---plugins +| | | | \---lib +| | | +---jsdom +| | | +---json-schema +| | | +---json5 +| | | +---jsonwebtoken +| | | +---mapbox-gl +| | | +---ms +| | | +---node +| | | | +---assert +| | | | +---dns +| | | | +---fs +| | | | +---readline +| | | | +---stream +| | | | \---timers +| | | +---normalize-package-data +| | | +---parse-json +| | | +---prop-types +| | | +---react +| | | | \---ts5.0 +| | | +---react-dom +| | | | \---test-utils +| | | +---react-syntax-highlighter +| | | +---react-transition-group +| | | +---semver +| | | | +---classes +| | | | +---functions +| | | | +---internals +| | | | \---ranges +| | | +---stack-utils +| | | +---tough-cookie +| | | +---yargs +| | | \---yargs-parser +| | +---@typescript-eslint +| | | +---eslint-plugin +| | | | +---dist +| | | | | +---configs +| | | | | +---rules +| | | | | | +---enum-utils +| | | | | | +---naming-convention-utils +| | | | | | \---prefer-optional-chain-utils +| | | | | \---util +| | | | \---docs +| | | | \---rules +| | | +---parser +| | | | \---dist +| | | +---scope-manager +| | | | \---dist +| | | | +---definition +| | | | +---lib +| | | | +---referencer +| | | | +---scope +| | | | \---variable +| | | +---type-utils +| | | | \---dist +| | | +---types +| | | | \---dist +| | | | \---generated +| | | +---typescript-estree +| | | | \---dist +| | | | +---create-program +| | | | +---jsx +| | | | +---parseSettings +| | | | \---ts-estree +| | | +---utils +| | | | \---dist +| | | | +---ast-utils +| | | | | \---eslint-utils +| | | | +---eslint-utils +| | | | +---ts-eslint +| | | | | \---eslint +| | | | \---ts-utils +| | | \---visitor-keys +| | | +---dist +| | | \---eslint-visitor-keys +| | | +---dist +| | | \---lib +| | +---@ungap +| | | \---structured-clone +| | | +---.github +| | | | \---workflows +| | | +---cjs +| | | \---esm +| | +---@unrs +| | | \---resolver-binding-win32-x64-msvc +| | +---@vercel +| | | \---style-guide +| | | +---eslint +| | | | +---rules +| | | | | \---typescript +| | | | \---utils +| | | +---prettier +| | | \---typescript +| | +---@yr +| | | \---monotone-cubic-spline +| | | \---src +| | +---abab +| | | \---lib +| | +---acorn +| | | +---bin +| | | \---dist +| | +---acorn-globals +| | +---acorn-jsx +| | +---acorn-walk +| | | \---dist +| | +---agent-base +| | | +---dist +| | | | \---src +| | | \---src +| | +---ajv +| | | +---dist +| | | +---lib +| | | | +---compile +| | | | +---dot +| | | | +---dotjs +| | | | \---refs +| | | \---scripts +| | +---ansi-escapes +| | +---ansi-regex +| | +---ansi-styles +| | +---anymatch +| | +---apexcharts +| | | +---dist +| | | | \---locales +| | | +---src +| | | | +---assets +| | | | +---charts +| | | | | \---common +| | | | | +---bar +| | | | | +---circle +| | | | | +---line +| | | | | \---treemap +| | | | +---libs +| | | | +---locales +| | | | +---modules +| | | | | +---annotations +| | | | | +---axes +| | | | | +---dimensions +| | | | | +---helpers +| | | | | +---legend +| | | | | +---settings +| | | | | \---tooltip +| | | | +---svgjs +| | | | \---utils +| | | \---types +| | +---argparse +| | | \---lib +| | +---aria-query +| | | \---lib +| | | +---etc +| | | | \---roles +| | | | +---abstract +| | | | +---dpub +| | | | +---graphics +| | | | \---literal +| | | \---util +| | +---array-buffer-byte-length +| | | +---.github +| | | \---test +| | +---array-includes +| | | +---.github +| | | \---test +| | +---array-union +| | +---array.prototype.findlast +| | | +---.github +| | | \---test +| | +---array.prototype.findlastindex +| | | +---.github +| | | \---test +| | +---array.prototype.flat +| | | +---.github +| | | \---test +| | +---array.prototype.flatmap +| | | +---.github +| | | \---test +| | +---array.prototype.tosorted +| | | +---.github +| | | \---test +| | +---arraybuffer.prototype.slice +| | | \---test +| | +---ast-types-flow +| | | \---lib +| | +---async-function +| | | +---.github +| | | \---test +| | +---asynckit +| | | \---lib +| | +---available-typed-arrays +| | | +---.github +| | | \---test +| | +---axe-core +| | | \---locales +| | +---axobject-query +| | | \---lib +| | | +---etc +| | | | \---objects +| | | \---util +| | +---babel-jest +| | | \---build +| | +---babel-plugin-istanbul +| | | +---lib +| | | +---.bin +| | | +---istanbul-lib-instrument +| | | | \---src +| | | \---semver +| | | \---bin +| | +---babel-plugin-jest-hoist +| | | \---build +| | +---babel-plugin-macros +| | | \---dist +| | +---babel-preset-current-node-syntax +| | | +---.github +| | | | \---workflows +| | | \---src +| | +---babel-preset-jest +| | +---balanced-match +| | | \---.github +| | +---bcryptjs +| | | +---bin +| | | \---umd +| | +---brace-expansion +| | | \---.github +| | +---braces +| | | \---lib +| | +---browserslist +| | +---bser +| | +---buffer-equal-constant-time +| | +---buffer-from +| | +---builtin-modules +| | +---busboy +| | | +---.github +| | | | \---workflows +| | | +---bench +| | | +---lib +| | | | \---types +| | | \---test +| | +---call-bind +| | | +---.github +| | | \---test +| | +---call-bind-apply-helpers +| | | +---.github +| | | \---test +| | +---call-bound +| | | +---.github +| | | \---test +| | +---callsites +| | +---camelcase +| | +---caniuse-lite +| | | +---data +| | | | +---features +| | | | \---regions +| | | \---dist +| | | +---lib +| | | \---unpacker +| | +---chalk +| | | \---source +| | +---char-regex +| | +---ci-info +| | +---cjs-module-lexer +| | | \---dist +| | +---clean-regexp +| | | +---lib +| | | \---escape-string-regexp +| | +---client-only +| | +---cliui +| | | +---build +| | | | \---lib +| | | +---emoji-regex +| | | | \---es2015 +| | | +---string-width +| | | \---wrap-ansi +| | +---clsx +| | | \---dist +| | +---co +| | +---collect-v8-coverage +| | +---color-convert +| | +---color-name +| | +---combined-stream +| | | \---lib +| | +---concat-map +| | | +---example +| | | \---test +| | +---convert-source-map +| | +---core-js-compat +| | +---core-util-is +| | | \---lib +| | +---cosmiconfig +| | | \---dist +| | +---create-jest +| | | +---bin +| | | \---build +| | +---cross-spawn +| | | \---lib +| | | \---util +| | +---css.escape +| | +---cssom +| | | \---lib +| | +---cssstyle +| | | +---lib +| | | | +---properties +| | | | \---utils +| | | \---cssom +| | | \---lib +| | +---csstype +| | +---d3-array +| | | +---dist +| | | \---src +| | | \---threshold +| | +---d3-color +| | | +---dist +| | | \---src +| | +---d3-ease +| | | +---dist +| | | \---src +| | +---d3-format +| | | +---dist +| | | +---locale +| | | \---src +| | +---d3-interpolate +| | | +---dist +| | | \---src +| | | \---transform +| | +---d3-path +| | | +---dist +| | | \---src +| | +---d3-scale +| | | +---dist +| | | \---src +| | +---d3-shape +| | | +---dist +| | | \---src +| | | +---curve +| | | +---offset +| | | +---order +| | | \---symbol +| | +---d3-time +| | | +---dist +| | | \---src +| | +---d3-time-format +| | | +---dist +| | | +---locale +| | | \---src +| | +---d3-timer +| | | +---dist +| | | \---src +| | +---damerau-levenshtein +| | | +---scripts +| | | \---test +| | +---data-urls +| | | \---lib +| | +---data-view-buffer +| | | +---.github +| | | \---test +| | +---data-view-byte-length +| | | +---.github +| | | \---test +| | +---data-view-byte-offset +| | | +---.github +| | | \---test +| | +---dayjs +| | | +---esm +| | | | +---locale +| | | | \---plugin +| | | | +---advancedFormat +| | | | +---arraySupport +| | | | +---badMutable +| | | | +---bigIntSupport +| | | | +---buddhistEra +| | | | +---calendar +| | | | +---customParseFormat +| | | | +---dayOfYear +| | | | +---devHelper +| | | | +---duration +| | | | +---isBetween +| | | | +---isLeapYear +| | | | +---isMoment +| | | | +---isoWeek +| | | | +---isoWeeksInYear +| | | | +---isSameOrAfter +| | | | +---isSameOrBefore +| | | | +---isToday +| | | | +---isTomorrow +| | | | +---isYesterday +| | | | +---localeData +| | | | +---localizedFormat +| | | | +---minMax +| | | | +---objectSupport +| | | | +---pluralGetSet +| | | | +---preParsePostFormat +| | | | +---quarterOfYear +| | | | +---relativeTime +| | | | +---timezone +| | | | +---toArray +| | | | +---toObject +| | | | +---updateLocale +| | | | +---utc +| | | | +---weekday +| | | | +---weekOfYear +| | | | \---weekYear +| | | +---locale +| | | \---plugin +| | +---debug +| | | \---src +| | +---decimal.js +| | +---decimal.js-light +| | | \---doc +| | +---dedent +| | | \---dist +| | +---deep-is +| | | +---example +| | | \---test +| | +---deepmerge +| | | \---dist +| | +---define-data-property +| | | +---.github +| | | \---test +| | +---define-properties +| | | \---.github +| | +---delayed-stream +| | | \---lib +| | +---dequal +| | | +---dist +| | | \---lite +| | +---detect-indent +| | +---detect-newline +| | +---diff-sequences +| | | \---build +| | +---dir-glob +| | +---doctrine +| | | \---lib +| | +---dom-accessibility-api +| | | \---dist +| | | \---polyfills +| | +---dom-helpers +| | | +---activeElement +| | | +---addClass +| | | +---addEventListener +| | | +---animate +| | | +---animationFrame +| | | +---attribute +| | | +---camelize +| | | +---camelizeStyle +| | | +---canUseDOM +| | | +---childElements +| | | +---childNodes +| | | +---cjs +| | | +---clear +| | | +---closest +| | | +---collectElements +| | | +---collectSiblings +| | | +---contains +| | | +---css +| | | +---esm +| | | +---filterEventHandler +| | | +---getComputedStyle +| | | +---getScrollAccessor +| | | +---hasClass +| | | +---height +| | | +---hyphenate +| | | +---hyphenateStyle +| | | +---insertAfter +| | | +---isDocument +| | | +---isInput +| | | +---isTransform +| | | +---isVisible +| | | +---isWindow +| | | +---listen +| | | +---matches +| | | +---nextUntil +| | | +---offset +| | | +---offsetParent +| | | +---ownerDocument +| | | +---ownerWindow +| | | +---parents +| | | +---position +| | | +---prepend +| | | +---querySelectorAll +| | | +---remove +| | | +---removeClass +| | | +---removeEventListener +| | | +---scrollbarSize +| | | +---scrollLeft +| | | +---scrollParent +| | | +---scrollTo +| | | +---scrollTop +| | | +---siblings +| | | +---text +| | | +---toggleClass +| | | +---transitionEnd +| | | +---triggerEvent +| | | \---width +| | +---domexception +| | | \---lib +| | +---dunder-proto +| | | +---.github +| | | \---test +| | +---duplexer2 +| | | +---isarray +| | | +---readable-stream +| | | | +---doc +| | | | | \---wg-meetings +| | | | \---lib +| | | | \---internal +| | | | \---streams +| | | \---string_decoder +| | | \---lib +| | +---eastasianwidth +| | +---ecdsa-sig-formatter +| | | \---src +| | +---electron-to-chromium +| | +---emittery +| | +---emoji-regex +| | | \---es2015 +| | +---entities +| | | +---dist +| | | | +---commonjs +| | | | | \---generated +| | | | \---esm +| | | | \---generated +| | | \---src +| | | \---generated +| | +---error-ex +| | +---es-abstract +| | | +---2015 +| | | | \---tables +| | | +---2016 +| | | | \---tables +| | | +---2017 +| | | | \---tables +| | | +---2018 +| | | | \---tables +| | | +---2019 +| | | | \---tables +| | | +---2020 +| | | | +---BigInt +| | | | +---Number +| | | | \---tables +| | | +---2021 +| | | | +---BigInt +| | | | +---Number +| | | | \---tables +| | | +---2022 +| | | | +---BigInt +| | | | +---Number +| | | | \---tables +| | | +---2023 +| | | | +---BigInt +| | | | +---Number +| | | | \---tables +| | | +---2024 +| | | | +---BigInt +| | | | +---Number +| | | | \---tables +| | | +---2025 +| | | | +---BigInt +| | | | +---Number +| | | | \---tables +| | | +---5 +| | | +---helpers +| | | | \---records +| | | \---operations +| | +---es-define-property +| | | +---.github +| | | \---test +| | +---es-errors +| | | +---.github +| | | \---test +| | +---es-iterator-helpers +| | | +---.github +| | | +---aos +| | | +---Iterator +| | | +---Iterator.concat +| | | +---Iterator.from +| | | +---Iterator.prototype +| | | +---Iterator.prototype.constructor +| | | +---Iterator.prototype.drop +| | | +---Iterator.prototype.every +| | | +---Iterator.prototype.filter +| | | +---Iterator.prototype.find +| | | +---Iterator.prototype.flatMap +| | | +---Iterator.prototype.forEach +| | | +---Iterator.prototype.map +| | | +---Iterator.prototype.reduce +| | | +---Iterator.prototype.some +| | | +---Iterator.prototype.take +| | | +---Iterator.prototype.toArray +| | | +---Iterator.zip +| | | +---Iterator.zipKeyed +| | | +---IteratorHelperPrototype +| | | +---test +| | | | \---helpers +| | | \---WrapForValidIteratorPrototype +| | +---es-object-atoms +| | | +---.github +| | | \---test +| | +---es-set-tostringtag +| | | \---test +| | +---es-shim-unscopables +| | | +---.github +| | | \---test +| | +---es-to-primitive +| | | +---.github +| | | +---helpers +| | | \---test +| | +---escalade +| | | +---dist +| | | \---sync +| | +---escape-string-regexp +| | +---escodegen +| | | +---bin +| | | \---source-map +| | | +---dist +| | | \---lib +| | +---eslint +| | | +---bin +| | | +---conf +| | | +---lib +| | | | +---cli-engine +| | | | | \---formatters +| | | | +---config +| | | | +---eslint +| | | | +---linter +| | | | | \---code-path-analysis +| | | | +---rule-tester +| | | | +---rules +| | | | | \---utils +| | | | | +---patterns +| | | | | \---unicode +| | | | +---shared +| | | | \---source-code +| | | | \---token-store +| | | +---messages +| | | +---brace-expansion +| | | +---eslint-scope +| | | | +---dist +| | | | \---lib +| | | +---eslint-visitor-keys +| | | | +---dist +| | | | \---lib +| | | +---globals +| | | +---minimatch +| | | \---type-fest +| | | +---source +| | | \---ts41 +| | +---eslint-config-next +| | | +---@typescript-eslint +| | | | +---parser +| | | | | \---dist +| | | | +---scope-manager +| | | | | \---dist +| | | | | +---definition +| | | | | +---lib +| | | | | +---referencer +| | | | | +---scope +| | | | | \---variable +| | | | +---types +| | | | | \---dist +| | | | | \---generated +| | | | +---typescript-estree +| | | | | \---dist +| | | | | +---create-program +| | | | | +---jsx +| | | | | +---parseSettings +| | | | | \---ts-estree +| | | | \---visitor-keys +| | | | \---dist +| | | +---eslint-visitor-keys +| | | | +---dist +| | | | \---lib +| | | \---minimatch +| | | \---dist +| | | +---cjs +| | | \---mjs +| | +---eslint-config-prettier +| | | \---bin +| | +---eslint-import-resolver-alias +| | +---eslint-import-resolver-node +| | | \---debug +| | | \---src +| | +---eslint-import-resolver-typescript +| | | \---lib +| | +---eslint-module-utils +| | | \---debug +| | | \---src +| | +---eslint-plugin-eslint-comments +| | | +---lib +| | | | +---configs +| | | | +---internal +| | | | +---rules +| | | | \---utils +| | | \---escape-string-regexp +| | +---eslint-plugin-import +| | | +---config +| | | | \---flat +| | | +---docs +| | | | \---rules +| | | +---lib +| | | | +---core +| | | | +---exportMap +| | | | \---rules +| | | +---memo-parser +| | | +---.bin +| | | +---brace-expansion +| | | +---debug +| | | | \---src +| | | +---doctrine +| | | | \---lib +| | | +---minimatch +| | | \---semver +| | | \---bin +| | +---eslint-plugin-jest +| | | +---docs +| | | | \---rules +| | | +---lib +| | | | +---processors +| | | | \---rules +| | | | \---utils +| | | +---@typescript-eslint +| | | | +---scope-manager +| | | | | \---dist +| | | | | +---definition +| | | | | +---lib +| | | | | +---referencer +| | | | | +---scope +| | | | | \---variable +| | | | +---types +| | | | | +---dist +| | | | | | \---generated +| | | | | \---_ts3.4 +| | | | | \---dist +| | | | | \---generated +| | | | +---typescript-estree +| | | | | +---dist +| | | | | | +---create-program +| | | | | | +---jsx +| | | | | | +---parseSettings +| | | | | | \---ts-estree +| | | | | \---_ts3.4 +| | | | | \---dist +| | | | | +---create-program +| | | | | +---jsx +| | | | | +---parseSettings +| | | | | \---ts-estree +| | | | +---utils +| | | | | +---dist +| | | | | | +---ast-utils +| | | | | | | \---eslint-utils +| | | | | | +---eslint-utils +| | | | | | | \---rule-tester +| | | | | | +---ts-eslint +| | | | | | \---ts-eslint-scope +| | | | | \---_ts3.4 +| | | | | \---dist +| | | | | +---ast-utils +| | | | | | \---eslint-utils +| | | | | +---eslint-utils +| | | | | | \---rule-tester +| | | | | +---ts-eslint +| | | | | \---ts-eslint-scope +| | | | \---visitor-keys +| | | | +---dist +| | | | \---_ts3.4 +| | | | \---dist +| | | \---eslint-visitor-keys +| | | +---dist +| | | \---lib +| | +---eslint-plugin-jsx-a11y +| | | +---docs +| | | | \---rules +| | | +---lib +| | | | +---configs +| | | | +---rules +| | | | \---util +| | | | \---implicitRoles +| | | | +---aria-query +| | | | | \---lib +| | | | | +---etc +| | | | | | \---roles +| | | | | | +---abstract +| | | | | | +---dpub +| | | | | | +---graphics +| | | | | | \---literal +| | | | | \---util +| | | | +---brace-expansion +| | | | \---minimatch +| | | +---__mocks__ +| | | \---__tests__ +| | | +---src +| | | | +---rules +| | | | \---util +| | | | \---implicitRoles +| | | \---__util__ +| | | \---helpers +| | +---eslint-plugin-playwright +| | | +---dist +| | | +---globals +| | | \---type-fest +| | | +---source +| | | \---ts41 +| | +---eslint-plugin-react +| | | +---configs +| | | +---lib +| | | | +---rules +| | | | \---util +| | | +---.bin +| | | +---brace-expansion +| | | +---doctrine +| | | | \---lib +| | | +---minimatch +| | | +---resolve +| | | | +---.github +| | | | +---bin +| | | | +---example +| | | | +---lib +| | | | \---test +| | | | +---dotdot +| | | | | \---abc +| | | | +---module_dir +| | | | | +---xmodules +| | | | | | \---aaa +| | | | | +---ymodules +| | | | | | \---aaa +| | | | | \---zmodules +| | | | | \---bbb +| | | | +---node_path +| | | | | +---x +| | | | | | +---aaa +| | | | | | \---ccc +| | | | | \---y +| | | | | +---bbb +| | | | | \---ccc +| | | | +---pathfilter +| | | | | \---deep_ref +| | | | +---precedence +| | | | | +---aaa +| | | | | \---bbb +| | | | +---resolver +| | | | | +---baz +| | | | | +---browser_field +| | | | | +---dot_main +| | | | | +---dot_slash_main +| | | | | +---empty_main +| | | | | +---false_main +| | | | | +---incorrect_main +| | | | | +---invalid_main +| | | | | +---missing_index +| | | | | +---missing_main +| | | | | +---multirepo +| | | | | | \---packages +| | | | | | +---package-a +| | | | | | \---package-b +| | | | | +---nested_symlinks +| | | | | | \---mylib +| | | | | +---null_main +| | | | | +---other_path +| | | | | | \---lib +| | | | | +---quux +| | | | | | \---foo +| | | | | +---same_names +| | | | | | \---foo +| | | | | +---symlinked +| | | | | | +---package +| | | | | | \---_ +| | | | | | \---symlink_target +| | | | | \---without_basedir +| | | | \---shadowed_core +| | | | \---util +| | | \---semver +| | | \---bin +| | +---eslint-plugin-react-hooks +| | | \---cjs +| | +---eslint-plugin-testing-library +| | | +---dist +| | | | +---configs +| | | | +---create-testing-library-rule +| | | | +---node-utils +| | | | +---rules +| | | | \---utils +| | | +---@typescript-eslint +| | | | +---scope-manager +| | | | | \---dist +| | | | | +---definition +| | | | | +---lib +| | | | | +---referencer +| | | | | +---scope +| | | | | \---variable +| | | | +---types +| | | | | +---dist +| | | | | | \---generated +| | | | | \---_ts3.4 +| | | | | \---dist +| | | | | \---generated +| | | | +---typescript-estree +| | | | | +---dist +| | | | | | +---create-program +| | | | | | +---jsx +| | | | | | +---parseSettings +| | | | | | \---ts-estree +| | | | | \---_ts3.4 +| | | | | \---dist +| | | | | +---create-program +| | | | | +---jsx +| | | | | +---parseSettings +| | | | | \---ts-estree +| | | | +---utils +| | | | | +---dist +| | | | | | +---ast-utils +| | | | | | | \---eslint-utils +| | | | | | +---eslint-utils +| | | | | | | \---rule-tester +| | | | | | +---ts-eslint +| | | | | | \---ts-eslint-scope +| | | | | \---_ts3.4 +| | | | | \---dist +| | | | | +---ast-utils +| | | | | | \---eslint-utils +| | | | | +---eslint-utils +| | | | | | \---rule-tester +| | | | | +---ts-eslint +| | | | | \---ts-eslint-scope +| | | | \---visitor-keys +| | | | +---dist +| | | | \---_ts3.4 +| | | | \---dist +| | | \---eslint-visitor-keys +| | | +---dist +| | | \---lib +| | +---eslint-plugin-tsdoc +| | | \---lib +| | | \---tests +| | +---eslint-plugin-unicorn +| | | +---configs +| | | \---rules +| | | +---ast +| | | +---fix +| | | +---shared +| | | \---utils +| | +---eslint-plugin-vitest +| | | \---dist +| | +---eslint-scope +| | | +---lib +| | | \---estraverse +| | +---eslint-visitor-keys +| | | \---lib +| | +---espree +| | | +---dist +| | | +---lib +| | | \---eslint-visitor-keys +| | | +---dist +| | | \---lib +| | +---esprima +| | | +---bin +| | | \---dist +| | +---esquery +| | | \---dist +| | +---esrecurse +| | +---estraverse +| | +---esutils +| | | \---lib +| | +---eventemitter3 +| | | \---umd +| | +---execa +| | | +---lib +| | | \---signal-exit +| | +---exit +| | | +---lib +| | | \---test +| | | \---fixtures +| | +---expect +| | | \---build +| | +---fast-deep-equal +| | | \---es6 +| | +---fast-equals +| | | +---config +| | | | +---rollup +| | | | \---tsconfig +| | | +---dist +| | | | +---cjs +| | | | | \---types +| | | | +---esm +| | | | | \---types +| | | | +---min +| | | | | \---types +| | | | \---umd +| | | | \---types +| | | +---recipes +| | | +---scripts +| | | \---src +| | +---fast-glob +| | | | \---glob-parent +| | | \---out +| | | +---managers +| | | +---providers +| | | | +---filters +| | | | +---matchers +| | | | \---transformers +| | | +---readers +| | | +---types +| | | \---utils +| | +---fast-json-stable-stringify +| | | +---.github +| | | +---benchmark +| | | +---example +| | | \---test +| | +---fast-levenshtein +| | +---fastq +| | | +---.github +| | | | \---workflows +| | | \---test +| | +---fb-watchman +| | +---file-entry-cache +| | +---fill-range +| | +---find-root +| | | \---test +| | +---find-up +| | +---flat-cache +| | | \---src +| | +---flatted +| | | +---cjs +| | | +---esm +| | | +---php +| | | +---python +| | | \---types +| | +---for-each +| | | +---.github +| | | \---test +| | +---foreground-child +| | | \---dist +| | | +---commonjs +| | | \---esm +| | +---form-data +| | | \---lib +| | +---fs.realpath +| | +---function-bind +| | | +---.github +| | | \---test +| | +---function.prototype.name +| | | +---.github +| | | +---helpers +| | | \---test +| | +---functions-have-names +| | | +---.github +| | | \---test +| | +---gensync +| | | \---test +| | +---get-caller-file +| | +---get-intrinsic +| | | +---.github +| | | \---test +| | +---get-package-type +| | +---get-proto +| | | +---.github +| | | \---test +| | +---get-stream +| | +---get-symbol-description +| | | +---.github +| | | \---test +| | +---get-tsconfig +| | | \---dist +| | +---git-hooks-list +| | +---glob +| | | \---dist +| | | +---commonjs +| | | \---esm +| | +---glob-parent +| | +---globals +| | +---globalthis +| | | \---test +| | +---globby +| | +---gopd +| | | +---.github +| | | \---test +| | +---graceful-fs +| | +---graphemer +| | | \---lib +| | +---has-bigints +| | | +---.github +| | | \---test +| | +---has-flag +| | +---has-property-descriptors +| | | +---.github +| | | \---test +| | +---has-proto +| | | +---.github +| | | \---test +| | +---has-symbols +| | | +---.github +| | | \---test +| | | \---shams +| | +---has-tostringtag +| | | +---.github +| | | \---test +| | | \---shams +| | +---hasown +| | | \---.github +| | +---hoist-non-react-statics +| | | +---dist +| | | | \---react-is +| | | | +---cjs +| | | | \---umd +| | | \---src +| | +---hosted-git-info +| | +---html-encoding-sniffer +| | | \---lib +| | +---html-escaper +| | | +---cjs +| | | +---esm +| | | \---test +| | +---html-tokenize +| | | +---bench +| | | +---bin +| | | +---example +| | | \---test +| | | +---attributes +| | | +---cdata +| | | +---comment +| | | +---comment_comment +| | | +---loose_angle_brackets +| | | +---quote +| | | +---script +| | | +---style +| | | +---table +| | | +---tag_comment +| | | \---title +| | +---http-proxy-agent +| | | \---dist +| | +---https-proxy-agent +| | | \---dist +| | +---human-signals +| | | \---build +| | | \---src +| | +---iconv-lite +| | | +---.github +| | | +---.idea +| | | | +---codeStyles +| | | | \---inspectionProfiles +| | | +---encodings +| | | | \---tables +| | | \---lib +| | +---ignore +| | +---import-fresh +| | +---import-local +| | | \---fixtures +| | +---imurmurhash +| | +---indent-string +| | +---inflight +| | +---inherits +| | +---internal-slot +| | | +---.github +| | | \---test +| | +---internmap +| | | +---dist +| | | \---src +| | +---is-array-buffer +| | | +---.github +| | | \---test +| | +---is-arrayish +| | +---is-async-function +| | | \---test +| | +---is-bigint +| | | +---.github +| | | \---test +| | +---is-boolean-object +| | | +---.github +| | | \---test +| | +---is-builtin-module +| | +---is-bun-module +| | | \---dist +| | +---is-callable +| | | +---.github +| | | \---test +| | +---is-core-module +| | | \---test +| | +---is-data-view +| | | +---.github +| | | \---test +| | +---is-date-object +| | | +---.github +| | | \---test +| | +---is-extglob +| | +---is-finalizationregistry +| | | +---.github +| | | \---test +| | +---is-fullwidth-code-point +| | +---is-generator-fn +| | +---is-generator-function +| | | \---test +| | +---is-glob +| | +---is-map +| | | +---.github +| | | \---test +| | +---is-negative-zero +| | | +---.github +| | | \---test +| | +---is-number +| | +---is-number-object +| | | +---.github +| | | \---test +| | +---is-path-inside +| | +---is-plain-obj +| | +---is-potential-custom-element-name +| | +---is-regex +| | | \---test +| | +---is-set +| | | +---.github +| | | \---test +| | +---is-shared-array-buffer +| | | +---.github +| | | \---test +| | +---is-stream +| | +---is-string +| | | +---.github +| | | \---test +| | +---is-symbol +| | | +---.github +| | | \---test +| | +---is-typed-array +| | | +---.github +| | | \---test +| | +---is-weakmap +| | | +---.github +| | | \---test +| | +---is-weakref +| | | +---.github +| | | \---test +| | +---is-weakset +| | | +---.github +| | | \---test +| | +---isarray +| | | \---build +| | +---isexe +| | | \---test +| | +---istanbul-lib-coverage +| | | \---lib +| | +---istanbul-lib-instrument +| | | \---src +| | +---istanbul-lib-report +| | | \---lib +| | +---istanbul-lib-source-maps +| | | +---lib +| | | \---source-map +| | | +---dist +| | | \---lib +| | +---istanbul-reports +| | | \---lib +| | | +---clover +| | | +---cobertura +| | | +---html +| | | | \---assets +| | | | \---vendor +| | | +---html-spa +| | | | +---assets +| | | | \---src +| | | +---json +| | | +---json-summary +| | | +---lcov +| | | +---lcovonly +| | | +---none +| | | +---teamcity +| | | +---text +| | | +---text-lcov +| | | \---text-summary +| | +---iterator.prototype +| | | +---.github +| | | \---test +| | +---jackspeak +| | | \---dist +| | | +---commonjs +| | | \---esm +| | +---jest +| | | +---bin +| | | \---build +| | +---jest-changed-files +| | | \---build +| | +---jest-circus +| | | +---build +| | | | \---legacy-code-todo-rewrite +| | | +---ansi-styles +| | | \---pretty-format +| | | \---build +| | | \---plugins +| | | \---lib +| | +---jest-cli +| | | +---bin +| | | \---build +| | +---jest-config +| | | +---build +| | | +---ansi-styles +| | | +---brace-expansion +| | | +---ci-info +| | | +---glob +| | | +---minimatch +| | | \---pretty-format +| | | \---build +| | | \---plugins +| | | \---lib +| | +---jest-diff +| | | +---build +| | | +---ansi-styles +| | | \---pretty-format +| | | \---build +| | | \---plugins +| | | \---lib +| | +---jest-docblock +| | | \---build +| | +---jest-each +| | | +---build +| | | | \---table +| | | +---ansi-styles +| | | \---pretty-format +| | | \---build +| | | \---plugins +| | | \---lib +| | +---jest-environment-jsdom +| | | \---build +| | +---jest-environment-node +| | | \---build +| | +---jest-get-type +| | | \---build +| | +---jest-haste-map +| | | \---build +| | | +---crawlers +| | | +---lib +| | | \---watchers +| | +---jest-leak-detector +| | | +---build +| | | +---ansi-styles +| | | \---pretty-format +| | | \---build +| | | \---plugins +| | | \---lib +| | +---jest-matcher-utils +| | | +---build +| | | +---ansi-styles +| | | \---pretty-format +| | | \---build +| | | \---plugins +| | | \---lib +| | +---jest-message-util +| | | +---build +| | | +---ansi-styles +| | | \---pretty-format +| | | \---build +| | | \---plugins +| | | \---lib +| | +---jest-mock +| | | \---build +| | +---jest-pnp-resolver +| | +---jest-regex-util +| | | \---build +| | +---jest-resolve +| | | \---build +| | +---jest-resolve-dependencies +| | | \---build +| | +---jest-runner +| | | \---build +| | +---jest-runtime +| | | +---build +| | | +---brace-expansion +| | | +---glob +| | | \---minimatch +| | +---jest-snapshot +| | | +---build +| | | +---ansi-styles +| | | \---pretty-format +| | | \---build +| | | \---plugins +| | | \---lib +| | +---jest-util +| | | +---build +| | | \---ci-info +| | +---jest-validate +| | | +---build +| | | +---ansi-styles +| | | +---camelcase +| | | \---pretty-format +| | | \---build +| | | \---plugins +| | | \---lib +| | +---jest-watcher +| | | \---build +| | | \---lib +| | +---jest-worker +| | | +---build +| | | | +---base +| | | | \---workers +| | | \---supports-color +| | +---jiti +| | | +---dist +| | | \---lib +| | +---jju +| | | \---lib +| | +---js-tokens +| | +---js-yaml +| | | +---bin +| | | +---dist +| | | \---lib +| | | +---schema +| | | \---type +| | +---jsdom +| | | \---lib +| | | \---jsdom +| | | +---browser +| | | | +---parser +| | | | \---resources +| | | +---level2 +| | | +---level3 +| | | \---living +| | | +---aborting +| | | +---attributes +| | | +---constraint-validation +| | | +---crypto +| | | +---cssom +| | | +---custom-elements +| | | +---domparsing +| | | +---events +| | | +---fetch +| | | +---file-api +| | | +---generated +| | | +---helpers +| | | | \---svg +| | | +---hr-time +| | | +---mutation-observer +| | | +---navigator +| | | +---nodes +| | | +---range +| | | +---selection +| | | +---svg +| | | +---traversal +| | | +---websockets +| | | +---webstorage +| | | +---window +| | | \---xhr +| | +---jsesc +| | | +---bin +| | | \---man +| | +---json-buffer +| | | \---test +| | +---json-parse-even-better-errors +| | +---json-schema-traverse +| | | \---spec +| | | \---fixtures +| | +---json-stable-stringify-without-jsonify +| | | +---example +| | | \---test +| | +---json5 +| | | +---dist +| | | \---lib +| | +---jsonwebtoken +| | | \---lib +| | +---jsx-ast-utils +| | | +---.github +| | | +---lib +| | | | \---values +| | | | \---expressions +| | | +---src +| | | | \---values +| | | | \---expressions +| | | \---__tests__ +| | | \---src +| | +---jwa +| | +---jws +| | | \---lib +| | +---keyv +| | | \---src +| | +---kleur +| | +---language-subtag-registry +| | | \---data +| | | \---json +| | +---language-tags +| | | \---lib +| | +---leven +| | +---levn +| | | \---lib +| | +---lines-and-columns +| | | \---build +| | +---locate-path +| | +---lodash +| | | \---fp +| | +---lodash.includes +| | +---lodash.isboolean +| | +---lodash.isinteger +| | +---lodash.isnumber +| | +---lodash.isplainobject +| | +---lodash.isstring +| | +---lodash.merge +| | +---lodash.once +| | +---loose-envify +| | +---lru-cache +| | +---lz-string +| | | +---bin +| | | +---libs +| | | +---reference +| | | +---tests +| | | | \---lib +| | | | \---jasmine-1.3.1 +| | | \---typings +| | +---make-dir +| | +---makeerror +| | | \---lib +| | +---math-intrinsics +| | | +---.github +| | | +---constants +| | | \---test +| | +---merge-stream +| | +---merge2 +| | +---micromatch +| | +---mime-db +| | +---mime-types +| | +---mimic-fn +| | +---min-indent +| | +---minimatch +| | | \---dist +| | | +---commonjs +| | | \---esm +| | +---minimist +| | | +---.github +| | | +---example +| | | \---test +| | +---minipass +| | | \---dist +| | | +---commonjs +| | | \---esm +| | +---ms +| | +---multipipe +| | | \---test +| | +---nanoid +| | | +---async +| | | +---bin +| | | +---non-secure +| | | \---url-alphabet +| | +---napi-postinstall +| | | \---lib +| | +---natural-compare +| | +---next +| | | +---compat +| | | +---dist +| | | | +---api +| | | | +---bin +| | | | +---build +| | | | | +---analysis +| | | | | +---babel +| | | | | | +---loader +| | | | | | \---plugins +| | | | | +---jest +| | | | | | \---__mocks__ +| | | | | +---manifests +| | | | | | \---formatter +| | | | | +---output +| | | | | +---polyfills +| | | | | | +---fetch +| | | | | | \---object.assign +| | | | | +---swc +| | | | | +---templates +| | | | | +---turborepo-access-trace +| | | | | +---webpack +| | | | | | +---alias +| | | | | | +---config +| | | | | | | \---blocks +| | | | | | | +---css +| | | | | | | | \---loaders +| | | | | | | \---images +| | | | | | +---loaders +| | | | | | | +---css-loader +| | | | | | | | \---src +| | | | | | | | +---plugins +| | | | | | | | \---runtime +| | | | | | | +---lightningcss-loader +| | | | | | | | \---src +| | | | | | | +---metadata +| | | | | | | +---next-edge-app-route-loader +| | | | | | | +---next-edge-ssr-loader +| | | | | | | +---next-flight-loader +| | | | | | | +---next-font-loader +| | | | | | | +---next-image-loader +| | | | | | | +---next-route-loader +| | | | | | | +---next-style-loader +| | | | | | | | \---runtime +| | | | | | | +---postcss-loader +| | | | | | | | \---src +| | | | | | | \---resolve-url-loader +| | | | | | | \---lib +| | | | | | \---plugins +| | | | | | +---next-types-plugin +| | | | | | +---terser-webpack-plugin +| | | | | | | \---src +| | | | | | \---wellknown-errors-plugin +| | | | | +---webpack-build +| | | | | \---webpack-config-rules +| | | | +---cli +| | | | +---client +| | | | | +---compat +| | | | | +---components +| | | | | | +---react-dev-overlay +| | | | | | | +---app +| | | | | | | +---internal +| | | | | | | | +---components +| | | | | | | | | +---CodeFrame +| | | | | | | | | +---Dialog +| | | | | | | | | +---hot-linked-text +| | | | | | | | | +---LeftRightDialogHeader +| | | | | | | | | +---Overlay +| | | | | | | | | +---Terminal +| | | | | | | | | +---Toast +| | | | | | | | | \---VersionStalenessInfo +| | | | | | | | +---container +| | | | | | | | | \---RuntimeError +| | | | | | | | +---helpers +| | | | | | | | +---hooks +| | | | | | | | +---icons +| | | | | | | | \---styles +| | | | | | | +---pages +| | | | | | | \---server +| | | | | | \---router-reducer +| | | | | | \---reducers +| | | | | +---dev +| | | | | | \---error-overlay +| | | | | +---legacy +| | | | | +---portal +| | | | | \---tracing +| | | | +---compiled +| | | | | +---@ampproject +| | | | | | \---toolbox-optimizer +| | | | | +---@babel +| | | | | | \---runtime +| | | | | | +---helpers +| | | | | | | \---esm +| | | | | | \---regenerator +| | | | | +---@edge-runtime +| | | | | | +---cookies +| | | | | | +---ponyfill +| | | | | | \---primitives +| | | | | +---@hapi +| | | | | | \---accept +| | | | | +---@mswjs +| | | | | | \---interceptors +| | | | | | \---ClientRequest +| | | | | +---@napi-rs +| | | | | | \---triples +| | | | | +---@next +| | | | | | +---font +| | | | | | | +---dist +| | | | | | | | +---fontkit +| | | | | | | | +---google +| | | | | | | | \---local +| | | | | | | +---google +| | | | | | | \---local +| | | | | | \---react-refresh-utils +| | | | | | \---dist +| | | | | | \---internal +| | | | | +---@opentelemetry +| | | | | | \---api +| | | | | +---@vercel +| | | | | | +---nft +| | | | | | \---og +| | | | | | +---emoji +| | | | | | +---figma +| | | | | | +---language +| | | | | | \---satori +| | | | | +---acorn +| | | | | +---amphtml-validator +| | | | | +---anser +| | | | | +---arg +| | | | | +---assert +| | | | | +---async-retry +| | | | | +---async-sema +| | | | | +---babel +| | | | | +---babel-packages +| | | | | +---browserify-zlib +| | | | | +---browserslist +| | | | | +---buffer +| | | | | +---bytes +| | | | | +---ci-info +| | | | | +---cli-select +| | | | | +---client-only +| | | | | +---commander +| | | | | +---comment-json +| | | | | +---compression +| | | | | +---conf +| | | | | +---constants-browserify +| | | | | +---content-disposition +| | | | | +---content-type +| | | | | +---cookie +| | | | | +---cross-spawn +| | | | | +---crypto-browserify +| | | | | +---css.escape +| | | | | +---cssnano-simple +| | | | | +---data-uri-to-buffer +| | | | | +---debug +| | | | | +---devalue +| | | | | +---domain-browser +| | | | | +---edge-runtime +| | | | | +---events +| | | | | +---find-cache-dir +| | | | | +---find-up +| | | | | +---fresh +| | | | | +---get-orientation +| | | | | +---glob +| | | | | +---gzip-size +| | | | | +---http-proxy +| | | | | +---http-proxy-agent +| | | | | +---https-browserify +| | | | | +---https-proxy-agent +| | | | | +---icss-utils +| | | | | +---ignore-loader +| | | | | +---image-size +| | | | | +---is-animated +| | | | | +---is-docker +| | | | | +---is-wsl +| | | | | +---jest-worker +| | | | | +---json5 +| | | | | +---jsonwebtoken +| | | | | +---loader-runner +| | | | | +---loader-utils2 +| | | | | +---loader-utils3 +| | | | | +---lodash.curry +| | | | | +---lru-cache +| | | | | +---mini-css-extract-plugin +| | | | | | \---hmr +| | | | | +---nanoid +| | | | | +---native-url +| | | | | +---neo-async +| | | | | +---next-server +| | | | | +---node-fetch +| | | | | +---node-html-parser +| | | | | +---ora +| | | | | +---os-browserify +| | | | | +---p-limit +| | | | | +---path-browserify +| | | | | +---path-to-regexp +| | | | | +---picomatch +| | | | | +---platform +| | | | | +---postcss-flexbugs-fixes +| | | | | +---postcss-modules-extract-imports +| | | | | +---postcss-modules-local-by-default +| | | | | +---postcss-modules-scope +| | | | | +---postcss-modules-values +| | | | | +---postcss-plugin-stub-for-cssnano-simple +| | | | | +---postcss-preset-env +| | | | | +---postcss-safe-parser +| | | | | +---postcss-scss +| | | | | +---postcss-value-parser +| | | | | +---process +| | | | | +---punycode +| | | | | +---querystring-es3 +| | | | | +---raw-body +| | | | | +---react +| | | | | | \---cjs +| | | | | +---react-dom +| | | | | | \---cjs +| | | | | +---react-dom-experimental +| | | | | | \---cjs +| | | | | +---react-experimental +| | | | | | \---cjs +| | | | | +---react-is +| | | | | | +---cjs +| | | | | | \---umd +| | | | | +---react-refresh +| | | | | | \---cjs +| | | | | +---react-server-dom-turbopack +| | | | | | \---cjs +| | | | | +---react-server-dom-turbopack-experimental +| | | | | | \---cjs +| | | | | +---react-server-dom-webpack +| | | | | | \---cjs +| | | | | +---react-server-dom-webpack-experimental +| | | | | | \---cjs +| | | | | +---regenerator-runtime +| | | | | +---sass-loader +| | | | | +---scheduler +| | | | | | \---cjs +| | | | | +---scheduler-experimental +| | | | | | \---cjs +| | | | | +---schema-utils2 +| | | | | +---schema-utils3 +| | | | | +---semver +| | | | | +---send +| | | | | +---server-only +| | | | | +---setimmediate +| | | | | +---shell-quote +| | | | | +---source-map +| | | | | +---source-map08 +| | | | | +---stacktrace-parser +| | | | | +---stream-browserify +| | | | | +---stream-http +| | | | | +---string-hash +| | | | | +---string_decoder +| | | | | +---strip-ansi +| | | | | +---superstruct +| | | | | +---tar +| | | | | +---terser +| | | | | +---text-table +| | | | | +---timers-browserify +| | | | | +---tty-browserify +| | | | | +---ua-parser-js +| | | | | +---unistore +| | | | | +---util +| | | | | +---vm-browserify +| | | | | +---watchpack +| | | | | +---web-vitals +| | | | | +---web-vitals-attribution +| | | | | +---webpack +| | | | | +---webpack-sources1 +| | | | | +---webpack-sources3 +| | | | | +---ws +| | | | | \---zod +| | | | +---esm +| | | | | +---api +| | | | | +---build +| | | | | | +---analysis +| | | | | | +---babel +| | | | | | | +---loader +| | | | | | | \---plugins +| | | | | | +---manifests +| | | | | | | \---formatter +| | | | | | +---output +| | | | | | +---polyfills +| | | | | | | +---fetch +| | | | | | | \---object.assign +| | | | | | +---swc +| | | | | | +---templates +| | | | | | +---turborepo-access-trace +| | | | | | +---webpack +| | | | | | | +---alias +| | | | | | | +---config +| | | | | | | | \---blocks +| | | | | | | | +---css +| | | | | | | | | \---loaders +| | | | | | | | \---images +| | | | | | | +---loaders +| | | | | | | | +---css-loader +| | | | | | | | | \---src +| | | | | | | | | +---plugins +| | | | | | | | | \---runtime +| | | | | | | | +---lightningcss-loader +| | | | | | | | | \---src +| | | | | | | | +---metadata +| | | | | | | | +---next-edge-app-route-loader +| | | | | | | | +---next-edge-ssr-loader +| | | | | | | | +---next-flight-loader +| | | | | | | | +---next-font-loader +| | | | | | | | +---next-image-loader +| | | | | | | | +---next-route-loader +| | | | | | | | +---next-style-loader +| | | | | | | | | \---runtime +| | | | | | | | +---postcss-loader +| | | | | | | | | \---src +| | | | | | | | \---resolve-url-loader +| | | | | | | | \---lib +| | | | | | | \---plugins +| | | | | | | +---next-types-plugin +| | | | | | | +---terser-webpack-plugin +| | | | | | | | \---src +| | | | | | | \---wellknown-errors-plugin +| | | | | | +---webpack-build +| | | | | | \---webpack-config-rules +| | | | | +---client +| | | | | | +---compat +| | | | | | +---components +| | | | | | | +---react-dev-overlay +| | | | | | | | +---app +| | | | | | | | +---internal +| | | | | | | | | +---components +| | | | | | | | | | +---CodeFrame +| | | | | | | | | | +---Dialog +| | | | | | | | | | +---hot-linked-text +| | | | | | | | | | +---LeftRightDialogHeader +| | | | | | | | | | +---Overlay +| | | | | | | | | | +---Terminal +| | | | | | | | | | +---Toast +| | | | | | | | | | \---VersionStalenessInfo +| | | | | | | | | +---container +| | | | | | | | | | \---RuntimeError +| | | | | | | | | +---helpers +| | | | | | | | | +---hooks +| | | | | | | | | +---icons +| | | | | | | | | \---styles +| | | | | | | | +---pages +| | | | | | | | \---server +| | | | | | | \---router-reducer +| | | | | | | \---reducers +| | | | | | +---dev +| | | | | | | \---error-overlay +| | | | | | +---legacy +| | | | | | +---portal +| | | | | | \---tracing +| | | | | +---export +| | | | | | +---helpers +| | | | | | \---routes +| | | | | +---lib +| | | | | | +---eslint +| | | | | | +---fs +| | | | | | +---helpers +| | | | | | +---memory +| | | | | | +---metadata +| | | | | | | +---generate +| | | | | | | +---resolvers +| | | | | | | \---types +| | | | | | \---typescript +| | | | | +---pages +| | | | | +---server +| | | | | | +---api-utils +| | | | | | | \---node +| | | | | | +---app-render +| | | | | | | +---rsc +| | | | | | | \---static +| | | | | | +---async-storage +| | | | | | +---base-http +| | | | | | +---dev +| | | | | | | \---turbopack +| | | | | | +---future +| | | | | | | +---helpers +| | | | | | | | \---module-loader +| | | | | | | +---normalizers +| | | | | | | | +---built +| | | | | | | | | +---app +| | | | | | | | | \---pages +| | | | | | | | \---request +| | | | | | | +---route-definitions +| | | | | | | +---route-matcher-managers +| | | | | | | +---route-matcher-providers +| | | | | | | | +---dev +| | | | | | | | | \---helpers +| | | | | | | | | \---file-reader +| | | | | | | | \---helpers +| | | | | | | | \---manifest-loaders +| | | | | | | +---route-matchers +| | | | | | | +---route-matches +| | | | | | | \---route-modules +| | | | | | | +---app-page +| | | | | | | | \---vendored +| | | | | | | | +---contexts +| | | | | | | | +---rsc +| | | | | | | | \---ssr +| | | | | | | +---app-route +| | | | | | | | \---helpers +| | | | | | | +---helpers +| | | | | | | +---pages +| | | | | | | | +---builtin +| | | | | | | | \---vendored +| | | | | | | | \---contexts +| | | | | | | \---pages-api +| | | | | | +---lib +| | | | | | | +---incremental-cache +| | | | | | | +---router-utils +| | | | | | | +---server-ipc +| | | | | | | +---squoosh +| | | | | | | | +---avif +| | | | | | | | +---mozjpeg +| | | | | | | | +---png +| | | | | | | | +---resize +| | | | | | | | \---webp +| | | | | | | \---trace +| | | | | | +---og +| | | | | | +---response-cache +| | | | | | +---stream-utils +| | | | | | +---typescript +| | | | | | | \---rules +| | | | | | \---web +| | | | | | +---exports +| | | | | | +---sandbox +| | | | | | \---spec-extension +| | | | | | \---adapters +| | | | | \---shared +| | | | | \---lib +| | | | | +---i18n +| | | | | +---isomorphic +| | | | | +---lazy-dynamic +| | | | | +---page-path +| | | | | +---router +| | | | | | \---utils +| | | | | \---utils +| | | | +---experimental +| | | | | \---testmode +| | | | | +---playwright +| | | | | \---proxy +| | | | +---export +| | | | | +---helpers +| | | | | \---routes +| | | | +---lib +| | | | | +---eslint +| | | | | +---fs +| | | | | +---helpers +| | | | | +---memory +| | | | | +---metadata +| | | | | | +---generate +| | | | | | +---resolvers +| | | | | | \---types +| | | | | \---typescript +| | | | +---pages +| | | | +---server +| | | | | +---api-utils +| | | | | | \---node +| | | | | +---app-render +| | | | | | +---rsc +| | | | | | \---static +| | | | | +---async-storage +| | | | | +---base-http +| | | | | +---dev +| | | | | | \---turbopack +| | | | | +---future +| | | | | | +---helpers +| | | | | | | \---module-loader +| | | | | | +---normalizers +| | | | | | | +---built +| | | | | | | | +---app +| | | | | | | | \---pages +| | | | | | | \---request +| | | | | | +---route-definitions +| | | | | | +---route-matcher-managers +| | | | | | +---route-matcher-providers +| | | | | | | +---dev +| | | | | | | | \---helpers +| | | | | | | | \---file-reader +| | | | | | | \---helpers +| | | | | | | \---manifest-loaders +| | | | | | +---route-matchers +| | | | | | +---route-matches +| | | | | | \---route-modules +| | | | | | +---app-page +| | | | | | | \---vendored +| | | | | | | +---contexts +| | | | | | | +---rsc +| | | | | | | \---ssr +| | | | | | +---app-route +| | | | | | | \---helpers +| | | | | | +---helpers +| | | | | | +---pages +| | | | | | | +---builtin +| | | | | | | \---vendored +| | | | | | | \---contexts +| | | | | | \---pages-api +| | | | | +---lib +| | | | | | +---incremental-cache +| | | | | | +---router-utils +| | | | | | +---server-ipc +| | | | | | +---squoosh +| | | | | | | +---avif +| | | | | | | +---mozjpeg +| | | | | | | +---png +| | | | | | | +---resize +| | | | | | | +---rotate +| | | | | | | \---webp +| | | | | | \---trace +| | | | | +---og +| | | | | +---response-cache +| | | | | +---stream-utils +| | | | | +---typescript +| | | | | | \---rules +| | | | | \---web +| | | | | +---exports +| | | | | +---sandbox +| | | | | \---spec-extension +| | | | | \---adapters +| | | | +---shared +| | | | | \---lib +| | | | | +---i18n +| | | | | +---isomorphic +| | | | | +---lazy-dynamic +| | | | | +---page-path +| | | | | +---router +| | | | | | \---utils +| | | | | \---utils +| | | | +---src +| | | | | \---compiled +| | | | | \---@ampproject +| | | | | \---toolbox-optimizer +| | | | +---styled-jsx +| | | | | \---types +| | | | +---telemetry +| | | | | \---events +| | | | \---trace +| | | | \---report +| | | +---experimental +| | | | \---testmode +| | | | \---playwright +| | | +---font +| | | | +---google +| | | | \---local +| | | +---image-types +| | | +---legacy +| | | +---navigation-types +| | | | \---compat +| | | \---types +| | +---node-int64 +| | +---node-releases +| | | \---data +| | | +---processed +| | | \---release-schedule +| | +---normalize-package-data +| | | +---lib +| | | +---.bin +| | | \---semver +| | | \---bin +| | +---normalize-path +| | +---npm-run-path +| | +---nwsapi +| | | +---dist +| | | \---src +| | | \---modules +| | +---object-assign +| | +---object-inspect +| | | +---.github +| | | +---example +| | | \---test +| | | \---browser +| | +---object-keys +| | | \---test +| | +---object.assign +| | | +---.github +| | | +---dist +| | | \---test +| | +---object.entries +| | | \---test +| | +---object.fromentries +| | | \---test +| | +---object.groupby +| | | +---.github +| | | \---test +| | +---object.values +| | | \---test +| | +---once +| | +---onetime +| | +---optionator +| | | \---lib +| | +---own-keys +| | | +---.github +| | | \---test +| | +---p-limit +| | +---p-locate +| | +---p-try +| | +---parent-module +| | +---parse-json +| | +---parse5 +| | | \---dist +| | | +---cjs +| | | | +---common +| | | | +---parser +| | | | +---serializer +| | | | +---tokenizer +| | | | \---tree-adapters +| | | +---common +| | | +---parser +| | | +---serializer +| | | +---tokenizer +| | | \---tree-adapters +| | +---path-exists +| | +---path-is-absolute +| | +---path-key +| | +---path-parse +| | +---path-scurry +| | | +---dist +| | | | +---commonjs +| | | | \---esm +| | | \---lru-cache +| | | \---dist +| | | +---commonjs +| | | \---esm +| | +---path-type +| | +---picocolors +| | +---picomatch +| | | \---lib +| | +---pirates +| | | \---lib +| | +---pkg-dir +| | | +---find-up +| | | +---locate-path +| | | +---p-limit +| | | \---p-locate +| | +---pluralize +| | +---possible-typed-array-names +| | | +---.github +| | | \---test +| | +---postcss +| | | \---lib +| | +---prelude-ls +| | | \---lib +| | +---prettier +| | | +---bin +| | | +---internal +| | | \---plugins +| | +---prettier-plugin-packagejson +| | | \---lib +| | | \---esm-proxy +| | +---pretty-format +| | | +---build +| | | | \---plugins +| | | | \---lib +| | | +---ansi-styles +| | | \---react-is +| | | +---cjs +| | | \---umd +| | +---prisma +| | | +---build +| | | | \---public +| | | | +---assets +| | | | +---http +| | | | \---pages +| | | | \---http +| | | +---dist +| | | | \---cli +| | | | \---src +| | | +---preinstall +| | | +---prisma-client +| | | | +---generator-build +| | | | +---runtime +| | | | \---scripts +| | | \---scripts +| | +---process-nextick-args +| | +---prompts +| | | +---dist +| | | | +---dateparts +| | | | +---elements +| | | | \---util +| | | \---lib +| | | +---dateparts +| | | +---elements +| | | \---util +| | +---prop-types +| | | +---lib +| | | \---react-is +| | | +---cjs +| | | \---umd +| | +---psl +| | | +---data +| | | +---dist +| | | \---types +| | +---punycode +| | +---pure-rand +| | | \---lib +| | | +---distribution +| | | | \---internals +| | | +---esm +| | | | +---distribution +| | | | | \---internals +| | | | +---generator +| | | | \---types +| | | | +---distribution +| | | | | \---internals +| | | | \---generator +| | | +---generator +| | | \---types +| | | +---distribution +| | | | \---internals +| | | \---generator +| | +---querystringify +| | +---queue-microtask +| | +---react +| | | +---cjs +| | | \---umd +| | +---react-apexcharts +| | | +---dist +| | | \---types +| | +---react-dom +| | | +---cjs +| | | \---umd +| | +---react-hook-form +| | | \---dist +| | | +---logic +| | | +---types +| | | | \---path +| | | +---utils +| | | +---__tests__ +| | | | +---logic +| | | | +---useFieldArray +| | | | \---useForm +| | | \---__typetest__ +| | | +---path +| | | \---__fixtures__ +| | +---react-is +| | | +---cjs +| | | \---umd +| | +---react-smooth +| | | +---es6 +| | | +---lib +| | | +---src +| | | \---umd +| | +---react-transition-group +| | | +---cjs +| | | | \---utils +| | | +---config +| | | +---CSSTransition +| | | +---dist +| | | +---esm +| | | | \---utils +| | | +---ReplaceTransition +| | | +---SwitchTransition +| | | +---Transition +| | | +---TransitionGroup +| | | \---TransitionGroupContext +| | +---read-pkg +| | | \---type-fest +| | | \---source +| | +---read-pkg-up +| | | +---find-up +| | | +---locate-path +| | | +---p-limit +| | | +---p-locate +| | | \---type-fest +| | | \---source +| | +---readable-stream +| | | \---lib +| | +---recharts +| | | +---es6 +| | | | +---cartesian +| | | | +---chart +| | | | +---component +| | | | +---container +| | | | +---context +| | | | +---numberAxis +| | | | +---polar +| | | | +---shape +| | | | \---util +| | | | +---cursor +| | | | +---payload +| | | | \---tooltip +| | | +---lib +| | | | +---cartesian +| | | | +---chart +| | | | +---component +| | | | +---container +| | | | +---context +| | | | +---numberAxis +| | | | +---polar +| | | | +---shape +| | | | \---util +| | | | +---cursor +| | | | +---payload +| | | | \---tooltip +| | | | \---react-is +| | | | +---cjs +| | | | \---umd +| | | +---types +| | | | +---cartesian +| | | | +---chart +| | | | +---component +| | | | +---container +| | | | +---context +| | | | +---numberAxis +| | | | +---polar +| | | | +---shape +| | | | \---util +| | | | +---cursor +| | | | +---payload +| | | | \---tooltip +| | | \---umd +| | +---recharts-scale +| | | +---es6 +| | | | \---util +| | | +---lib +| | | | \---util +| | | +---src +| | | | \---util +| | | \---umd +| | +---redent +| | +---reflect.getprototypeof +| | | \---test +| | +---regexp-tree +| | | +---bin +| | | \---dist +| | | +---bin +| | | +---compat-transpiler +| | | | +---runtime +| | | | \---transforms +| | | +---generator +| | | +---interpreter +| | | | \---finite-automaton +| | | | +---dfa +| | | | \---nfa +| | | +---optimizer +| | | | \---transforms +| | | +---parser +| | | | +---generated +| | | | \---unicode +| | | +---transform +| | | +---traverse +| | | \---utils +| | +---regexp.prototype.flags +| | | \---test +| | +---regjsparser +| | | +---bin +| | | +---.bin +| | | \---jsesc +| | | +---bin +| | | \---man +| | +---require-directory +| | +---requires-port +| | +---resolve +| | | +---.github +| | | +---bin +| | | +---example +| | | +---lib +| | | \---test +| | | +---dotdot +| | | | \---abc +| | | +---module_dir +| | | | +---xmodules +| | | | | \---aaa +| | | | +---ymodules +| | | | | \---aaa +| | | | \---zmodules +| | | | \---bbb +| | | +---node_path +| | | | +---x +| | | | | +---aaa +| | | | | \---ccc +| | | | \---y +| | | | +---bbb +| | | | \---ccc +| | | +---pathfilter +| | | | \---deep_ref +| | | +---precedence +| | | | +---aaa +| | | | \---bbb +| | | +---resolver +| | | | +---baz +| | | | +---browser_field +| | | | +---dot_main +| | | | +---dot_slash_main +| | | | +---false_main +| | | | +---incorrect_main +| | | | +---invalid_main +| | | | +---multirepo +| | | | | \---packages +| | | | | +---package-a +| | | | | \---package-b +| | | | +---nested_symlinks +| | | | | \---mylib +| | | | +---other_path +| | | | | \---lib +| | | | +---quux +| | | | | \---foo +| | | | +---same_names +| | | | | \---foo +| | | | +---symlinked +| | | | | +---package +| | | | | \---_ +| | | | | \---symlink_target +| | | | \---without_basedir +| | | \---shadowed_core +| | | \---util +| | +---resolve-cwd +| | | \---resolve-from +| | +---resolve-from +| | +---resolve-pkg-maps +| | | \---dist +| | +---resolve.exports +| | | \---dist +| | +---reusify +| | | +---.github +| | | | \---workflows +| | | \---benchmarks +| | +---rimraf +| | | +---brace-expansion +| | | +---glob +| | | \---minimatch +| | +---run-parallel +| | +---safe-array-concat +| | | +---.github +| | | | \---isarray +| | | \---test +| | +---safe-buffer +| | +---safe-push-apply +| | | +---.github +| | | | \---isarray +| | | \---test +| | +---safe-regex-test +| | | +---.github +| | | \---test +| | +---safer-buffer +| | +---saxes +| | +---scheduler +| | | +---cjs +| | | \---umd +| | +---semver +| | | +---bin +| | | +---classes +| | | +---functions +| | | +---internal +| | | \---ranges +| | +---set-function-length +| | | \---.github +| | +---set-function-name +| | | \---.github +| | +---set-proto +| | | +---.github +| | | \---test +| | +---shebang-command +| | +---shebang-regex +| | +---side-channel +| | | +---.github +| | | \---test +| | +---side-channel-list +| | | +---.github +| | | \---test +| | +---side-channel-map +| | | +---.github +| | | \---test +| | +---side-channel-weakmap +| | | +---.github +| | | \---test +| | +---signal-exit +| | | \---dist +| | | +---cjs +| | | \---mjs +| | +---sisteransi +| | | \---src +| | +---slash +| | +---sort-object-keys +| | +---sort-package-json +| | | \---detect-newline +| | +---source-map +| | | +---dist +| | | \---lib +| | +---source-map-js +| | | \---lib +| | +---source-map-support +| | | +---buffer-from +| | | \---source-map +| | | +---dist +| | | \---lib +| | +---spdx-correct +| | +---spdx-exceptions +| | +---spdx-expression-parse +| | +---spdx-license-ids +| | +---sprintf-js +| | | +---demo +| | | +---dist +| | | +---src +| | | \---test +| | +---stable-hash +| | | \---dist +| | +---stack-utils +| | | \---escape-string-regexp +| | +---stop-iteration-iterator +| | | +---.github +| | | \---test +| | +---streamsearch +| | | +---.github +| | | | \---workflows +| | | +---lib +| | | \---test +| | +---string-length +| | +---string-width +| | | +---ansi-regex +| | | \---strip-ansi +| | +---string-width-cjs +| | | \---emoji-regex +| | | \---es2015 +| | +---string.prototype.includes +| | | +---.github +| | | | \---workflows +| | | \---tests +| | +---string.prototype.matchall +| | | +---.github +| | | \---test +| | +---string.prototype.repeat +| | | \---tests +| | +---string.prototype.trim +| | | \---test +| | +---string.prototype.trimend +| | | \---test +| | +---string.prototype.trimstart +| | | \---test +| | +---string_decoder +| | +---strip-ansi +| | +---strip-ansi-cjs +| | +---strip-bom +| | +---strip-final-newline +| | +---strip-indent +| | +---strip-json-comments +| | +---styled-jsx +| | | +---dist +| | | | +---babel +| | | | +---index +| | | | \---webpack +| | | \---lib +| | +---stylis +| | | +---dist +| | | | \---umd +| | | \---src +| | +---supports-color +| | +---supports-preserve-symlinks-flag +| | | +---.github +| | | \---test +| | +---svg.draggable.js +| | | \---dist +| | +---svg.easing.js +| | | \---dist +| | +---svg.filter.js +| | | \---dist +| | +---svg.js +| | | +---.config +| | | +---.github +| | | | \---ISSUE_TEMPLATE +| | | +---bench +| | | | \---tests +| | | +---dist +| | | +---spec +| | | | +---fixtures +| | | | +---lib +| | | | | \---jasmine-2.6.0 +| | | | +---spec +| | | | \---support +| | | \---src +| | +---svg.pathmorphing.js +| | | \---dist +| | +---svg.resize.js +| | | +---dist +| | | \---svg.select.js +| | | \---dist +| | +---svg.select.js +| | | \---dist +| | +---symbol-tree +| | | \---lib +| | +---synckit +| | | \---lib +| | +---test-exclude +| | | +---brace-expansion +| | | +---glob +| | | \---minimatch +| | +---text-table +| | | +---example +| | | \---test +| | +---through +| | | \---test +| | +---through2 +| | +---tiny-invariant +| | | +---dist +| | | | \---esm +| | | \---src +| | +---tinyglobby +| | | +---dist +| | | +---fdir +| | | | \---dist +| | | | +---api +| | | | | \---functions +| | | | \---builder +| | | \---picomatch +| | | \---lib +| | +---tmpl +| | | \---lib +| | +---to-regex-range +| | +---tough-cookie +| | | \---lib +| | +---tr46 +| | | \---lib +| | +---ts-api-utils +| | | \---lib +| | +---tsconfig-paths +| | | +---lib +| | | | \---__tests__ +| | | | \---data +| | | | +---.bin +| | | | +---json5 +| | | | | +---dist +| | | | | \---lib +| | | | \---strip-bom +| | | \---src +| | | \---__tests__ +| | | \---data +| | +---tslib +| | | \---modules +| | +---tsutils +| | | | \---tslib +| | | | +---modules +| | | | \---test +| | | | \---validateModuleExportsMatchCommonJS +| | | +---typeguard +| | | | +---2.8 +| | | | +---2.9 +| | | | +---3.0 +| | | | +---3.2 +| | | | \---next +| | | \---util +| | +---type-check +| | | \---lib +| | +---type-detect +| | +---type-fest +| | | +---source +| | | \---ts41 +| | +---typed-array-buffer +| | | +---.github +| | | \---test +| | +---typed-array-byte-length +| | | +---.github +| | | \---test +| | +---typed-array-byte-offset +| | | +---.github +| | | \---test +| | +---typed-array-length +| | | +---.github +| | | \---test +| | +---typescript +| | | +---bin +| | | \---lib +| | | +---cs +| | | +---de +| | | +---es +| | | +---fr +| | | +---it +| | | +---ja +| | | +---ko +| | | +---pl +| | | +---pt-br +| | | +---ru +| | | +---tr +| | | +---zh-cn +| | | \---zh-tw +| | +---unbox-primitive +| | | +---.github +| | | \---test +| | +---undici-types +| | +---universalify +| | +---unrs-resolver +| | +---update-browserslist-db +| | +---uri-js +| | | \---dist +| | | +---es5 +| | | \---esnext +| | | \---schemes +| | +---url-parse +| | | \---dist +| | +---util-deprecate +| | +---v8-to-istanbul +| | | +---lib +| | | \---convert-source-map +| | +---validate-npm-package-license +| | +---victory-vendor +| | | +---es +| | | +---lib +| | | \---lib-vendor +| | | +---d3-array +| | | | \---src +| | | | \---threshold +| | | +---d3-color +| | | | \---src +| | | +---d3-ease +| | | | \---src +| | | +---d3-format +| | | | \---src +| | | +---d3-interpolate +| | | | \---src +| | | | \---transform +| | | +---d3-path +| | | | \---src +| | | +---d3-scale +| | | | \---src +| | | +---d3-shape +| | | | \---src +| | | | +---curve +| | | | +---offset +| | | | +---order +| | | | \---symbol +| | | +---d3-time +| | | | \---src +| | | +---d3-time-format +| | | | \---src +| | | +---d3-timer +| | | | \---src +| | | +---d3-voronoi +| | | | \---src +| | | \---internmap +| | | \---src +| | +---w3c-xmlserializer +| | | \---lib +| | +---walker +| | | \---lib +| | +---webidl-conversions +| | | \---lib +| | +---whatwg-encoding +| | | \---lib +| | +---whatwg-mimetype +| | | \---lib +| | +---whatwg-url +| | | \---lib +| | +---which +| | | \---bin +| | +---which-boxed-primitive +| | | +---.github +| | | \---test +| | +---which-builtin-type +| | | | \---isarray +| | | \---test +| | +---which-collection +| | | +---.github +| | | \---test +| | +---which-typed-array +| | | +---.github +| | | \---test +| | +---word-wrap +| | +---wrap-ansi +| | | +---ansi-regex +| | | +---ansi-styles +| | | \---strip-ansi +| | +---wrap-ansi-cjs +| | | +---emoji-regex +| | | | \---es2015 +| | | \---string-width +| | +---wrappy +| | +---write-file-atomic +| | | +---lib +| | | \---signal-exit +| | +---ws +| | | \---lib +| | +---xml-name-validator +| | | \---lib +| | +---xmlchars +| | | +---xml +| | | | +---1.0 +| | | | \---1.1 +| | | \---xmlns +| | | \---1.0 +| | +---xtend +| | | \---object-keys +| | | \---test +| | +---y18n +| | | \---build +| | | \---lib +| | | \---platform-shims +| | +---yallist +| | +---yaml +| | | +---browser +| | | | +---dist +| | | | \---types +| | | +---dist +| | | \---types +| | +---yargs +| | | +---build +| | | | \---lib +| | | | +---typings +| | | | \---utils +| | | +---helpers +| | | +---lib +| | | | \---platform-shims +| | | +---locales +| | | +---emoji-regex +| | | | \---es2015 +| | | \---string-width +| | +---yargs-parser +| | | \---build +| | | \---lib +| | +---yocto-queue +| | \---zod +| | \---lib +| | +---benchmarks +| | +---helpers +| | +---locales +| | \---__tests__ +| +---prisma +| +---public +| | \---assets +| +---scripts +| \---src +| +---admin +| +---app +| | +---api +| | | +---admin +| | | | \---usersecho +| | | +---auth +| | | | +---me +| | | | +---signin +| | | | \---signout +| | | +---dados +| | | +---financial +| | | | +---fii-market-data +| | | | +---international-data +| | | | +---market-data +| | | | \---quotes +| | | +---hotmart +| | | | \---webhook +| | | +---ifix +| | | \---market +| | | \---ifix +| | +---auth +| | | +---reset-password +| | | +---sign-in +| | | \---sign-up +| | +---dados-ifix +| | +---dashboard +| | | +---account +| | | +---admin +| | | +---central-agenda +| | | +---central-proventos +| | | +---central-relatorios +| | | +---customers +| | | +---empresa +| | | | \---[ticker] +| | | +---empresa-exterior +| | | | \---[ticker] +| | | +---integrations +| | | +---internacional +| | | | +---dividendos +| | | | +---etfs +| | | | +---projeto-america +| | | | \---stocks +| | | +---overview +| | | +---recursos-exclusivos +| | | | +---analise-de-carteira +| | | | +---dicas-de-investimentos +| | | | +---ebooks +| | | | +---imposto-de-renda +| | | | +---lives-e-aulas +| | | | +---milhas-aereas +| | | | +---planilhas +| | | | \---telegram +| | | +---rentabilidades +| | | \---settings +| | \---errors +| | \---not-found +| +---components +| | +---auth +| | +---core +| | | \---theme-provider +| | \---dashboard +| | +---account +| | +---customer +| | +---integrations +| | +---layout +| | +---overview +| | +---rentabilidades +| | \---settings +| +---contexts +| +---hooks +| +---lib +| | \---auth +| +---styles +| | \---theme +| | \---components +| \---types +| +---.bin +| +---.cache +| | \---prisma +| | \---master +| | +---9b628578b3b7cae625e8c927178f15a170e74a9c +| | | \---windows +| | \---f40f79ec31188888a2e33acda0ecc8fd10a853a9 +| | \---windows +| +---.prisma +| | \---client +| +---.prisma-ZWgP0fnG +| | \---engines +| | \---9b628578b3b7cae625e8c927178f15a170e74a9c +| +---@adobe +| | \---css-tools +| | \---dist +| +---@ampproject +| | \---remapping +| | \---dist +| | \---types +| +---@babel +| | +---code-frame +| | | \---lib +| | +---compat-data +| | | \---data +| | +---core +| | | +---lib +| | | | +---config +| | | | | +---files +| | | | | +---helpers +| | | | | \---validation +| | | | +---errors +| | | | +---gensync-utils +| | | | +---parser +| | | | | \---util +| | | | +---tools +| | | | +---transformation +| | | | | +---file +| | | | | \---util +| | | | \---vendor +| | | | +---.bin +| | | | +---convert-source-map +| | | | \---semver +| | | | \---bin +| | | \---src +| | | \---config +| | | \---files +| | +---eslint-parser +| | | +---lib +| | | | +---convert +| | | | +---utils +| | | | \---worker +| | | +---.bin +| | | \---semver +| | | \---bin +| | +---generator +| | | \---lib +| | | +---generators +| | | \---node +| | +---helper-compilation-targets +| | | +---lib +| | | +---.bin +| | | \---semver +| | | \---bin +| | +---helper-module-imports +| | | \---lib +| | +---helper-module-transforms +| | | \---lib +| | +---helper-plugin-utils +| | | \---lib +| | +---helper-string-parser +| | | \---lib +| | +---helper-validator-identifier +| | | \---lib +| | +---helper-validator-option +| | | \---lib +| | +---helpers +| | | \---lib +| | | \---helpers +| | +---parser +| | | +---bin +| | | +---lib +| | | \---typings +| | +---plugin-syntax-async-generators +| | | \---lib +| | +---plugin-syntax-bigint +| | | \---lib +| | +---plugin-syntax-class-properties +| | | \---lib +| | +---plugin-syntax-class-static-block +| | | \---lib +| | +---plugin-syntax-import-attributes +| | | \---lib +| | +---plugin-syntax-import-meta +| | | \---lib +| | +---plugin-syntax-json-strings +| | | \---lib +| | +---plugin-syntax-jsx +| | | \---lib +| | +---plugin-syntax-logical-assignment-operators +| | | \---lib +| | +---plugin-syntax-nullish-coalescing-operator +| | | \---lib +| | +---plugin-syntax-numeric-separator +| | | \---lib +| | +---plugin-syntax-object-rest-spread +| | | \---lib +| | +---plugin-syntax-optional-catch-binding +| | | \---lib +| | +---plugin-syntax-optional-chaining +| | | \---lib +| | +---plugin-syntax-private-property-in-object +| | | \---lib +| | +---plugin-syntax-top-level-await +| | | \---lib +| | +---plugin-syntax-typescript +| | | \---lib +| | +---runtime +| | | +---helpers +| | | | \---esm +| | | \---regenerator +| | +---template +| | | \---lib +| | +---traverse +| | | \---lib +| | | +---path +| | | | +---inference +| | | | \---lib +| | | \---scope +| | | \---lib +| | \---types +| | \---lib +| | +---asserts +| | | \---generated +| | +---ast-types +| | | \---generated +| | +---builders +| | | +---flow +| | | +---generated +| | | +---react +| | | \---typescript +| | +---clone +| | +---comments +| | +---constants +| | | \---generated +| | +---converters +| | +---definitions +| | +---modifications +| | | +---flow +| | | \---typescript +| | +---retrievers +| | +---traverse +| | +---utils +| | | \---react +| | \---validators +| | +---generated +| | \---react +| +---@bcoe +| | \---v8-coverage +| | +---dist +| | | \---lib +| | | \---_src +| | \---src +| | +---lib +| | \---test +| +---@emnapi +| +---@emotion +| | +---babel-plugin +| | | +---dist +| | | | \---@emotion +| | | | \---memoize +| | | | +---dist +| | | | | \---declarations +| | | | | \---src +| | | | \---src +| | | \---src +| | | \---utils +| | +---cache +| | | +---dist +| | | | \---declarations +| | | | +---src +| | | | \---types +| | | +---src +| | | \---types +| | +---hash +| | | +---dist +| | | | \---declarations +| | | | \---src +| | | \---src +| | +---is-prop-valid +| | | +---dist +| | | | \---declarations +| | | | \---src +| | | | \---@emotion +| | | | \---memoize +| | | | +---dist +| | | | | \---declarations +| | | | | \---src +| | | | \---src +| | | \---src +| | +---memoize +| | | +---dist +| | | | \---declarations +| | | | +---src +| | | | \---types +| | | +---src +| | | \---types +| | +---react +| | | +---dist +| | | | \---declarations +| | | | +---src +| | | | \---types +| | | +---jsx-dev-runtime +| | | | \---dist +| | | +---jsx-runtime +| | | | \---dist +| | | +---src +| | | +---types +| | | \---_isolated-hnrs +| | | \---dist +| | +---serialize +| | | +---dist +| | | | \---declarations +| | | | \---src +| | | | \---@emotion +| | | | \---memoize +| | | | +---dist +| | | | | \---declarations +| | | | | \---src +| | | | \---src +| | | \---src +| | | \---conditions +| | +---server +| | | +---create-instance +| | | | \---dist +| | | +---dist +| | | | \---declarations +| | | | +---src +| | | | | \---create-instance +| | | | \---types +| | | +---src +| | | | \---create-instance +| | | \---types +| | +---sheet +| | | +---dist +| | | | \---declarations +| | | | \---src +| | | \---src +| | | \---conditions +| | +---styled +| | | +---base +| | | | \---dist +| | | +---dist +| | | | \---declarations +| | | | +---src +| | | | \---types +| | | +---src +| | | \---types +| | +---unitless +| | | +---dist +| | | | \---declarations +| | | | \---src +| | | \---src +| | +---use-insertion-effect-with-fallbacks +| | | +---dist +| | | | \---declarations +| | | | \---src +| | | \---src +| | | \---conditions +| | +---utils +| | | +---dist +| | | | \---declarations +| | | | \---src +| | | \---src +| | | \---conditions +| | \---weak-memoize +| | +---dist +| | | \---declarations +| | | +---src +| | | \---types +| | +---src +| | \---types +| +---@eslint +| | +---eslintrc +| | | +---conf +| | | +---dist +| | | +---lib +| | | | +---config-array +| | | | \---shared +| | | +---brace-expansion +| | | +---globals +| | | +---minimatch +| | | \---type-fest +| | | +---source +| | | \---ts41 +| | \---js +| | \---src +| | \---configs +| +---@eslint-community +| | +---eslint-utils +| | | \---eslint-visitor-keys +| | | +---dist +| | | \---lib +| | \---regexpp +| +---@floating-ui +| | +---core +| | | \---dist +| | +---dom +| | | \---dist +| | +---react-dom +| | | \---dist +| | \---utils +| | +---dist +| | \---dom +| +---@fontsource +| | +---inter +| | | +---files +| | | \---scss +| | +---plus-jakarta-sans +| | | +---files +| | | \---scss +| | \---roboto-mono +| | +---files +| | \---scss +| +---@hookform +| | \---resolvers +| | +---ajv +| | | +---dist +| | | \---src +| | | \---__tests__ +| | | +---__fixtures__ +| | | \---__snapshots__ +| | +---arktype +| | | +---dist +| | | \---src +| | | \---__tests__ +| | | +---__fixtures__ +| | | \---__snapshots__ +| | +---class-validator +| | | +---dist +| | | \---src +| | | \---__tests__ +| | | +---__fixtures__ +| | | \---__snapshots__ +| | +---computed-types +| | | +---dist +| | | \---src +| | | \---__tests__ +| | | +---__fixtures__ +| | | \---__snapshots__ +| | +---dist +| | +---effect-ts +| | | +---dist +| | | \---src +| | | \---__tests__ +| | | +---__fixtures__ +| | | \---__snapshots__ +| | +---io-ts +| | | +---dist +| | | \---src +| | | \---__tests__ +| | | +---__fixtures__ +| | | \---__snapshots__ +| | +---joi +| | | +---dist +| | | \---src +| | | \---__tests__ +| | | +---__fixtures__ +| | | \---__snapshots__ +| | +---nope +| | | +---dist +| | | \---src +| | | \---__tests__ +| | | +---__fixtures__ +| | | \---__snapshots__ +| | +---superstruct +| | | +---dist +| | | \---src +| | | \---__tests__ +| | | +---__fixtures__ +| | | \---__snapshots__ +| | +---typanion +| | | +---dist +| | | \---src +| | | \---__tests__ +| | | +---__fixtures__ +| | | \---__snapshots__ +| | +---typebox +| | | +---dist +| | | \---src +| | | \---__tests__ +| | | +---__fixtures__ +| | | \---__snapshots__ +| | +---valibot +| | | +---dist +| | | \---src +| | | \---__tests__ +| | | +---__fixtures__ +| | | \---__snapshots__ +| | +---vest +| | | +---dist +| | | \---src +| | | \---__tests__ +| | | +---__fixtures__ +| | | \---__snapshots__ +| | +---yup +| | | +---dist +| | | \---src +| | | \---__tests__ +| | | +---__fixtures__ +| | | \---__snapshots__ +| | \---zod +| | +---dist +| | \---src +| | \---__tests__ +| | +---__fixtures__ +| | \---__snapshots__ +| +---@humanwhocodes +| | +---config-array +| | | +---brace-expansion +| | | \---minimatch +| | +---module-importer +| | | +---dist +| | | \---src +| | \---object-schema +| | \---src +| +---@ianvs +| | \---prettier-plugin-sort-imports +| | +---lib +| | | \---src +| | | +---natural-sort +| | | +---preprocessors +| | | \---utils +| | \---types +| +---@isaacs +| | \---cliui +| | +---build +| | | \---lib +| | +---ansi-regex +| | \---strip-ansi +| +---@istanbuljs +| | +---load-nyc-config +| | | +---.bin +| | | +---argparse +| | | | \---lib +| | | | +---action +| | | | | +---append +| | | | | \---store +| | | | +---argument +| | | | \---help +| | | +---find-up +| | | +---js-yaml +| | | | +---bin +| | | | +---dist +| | | | \---lib +| | | | \---js-yaml +| | | | +---schema +| | | | \---type +| | | | \---js +| | | +---locate-path +| | | +---p-limit +| | | +---p-locate +| | | \---resolve-from +| | \---schema +| +---@jest +| | +---console +| | | \---build +| | +---core +| | | +---build +| | | | +---cli +| | | | +---lib +| | | | \---plugins +| | | +---ansi-styles +| | | +---ci-info +| | | \---pretty-format +| | | \---build +| | | \---plugins +| | | \---lib +| | +---environment +| | | \---build +| | +---expect +| | | \---build +| | +---expect-utils +| | | \---build +| | +---fake-timers +| | | \---build +| | +---globals +| | | \---build +| | +---reporters +| | | +---assets +| | | +---build +| | | +---brace-expansion +| | | +---glob +| | | \---minimatch +| | +---schemas +| | | \---build +| | +---source-map +| | | \---build +| | +---test-result +| | | \---build +| | +---test-sequencer +| | | \---build +| | +---transform +| | | +---build +| | | \---convert-source-map +| | \---types +| | \---build +| +---@jridgewell +| | +---gen-mapping +| | | +---dist +| | | +---src +| | | \---types +| | +---resolve-uri +| | | \---dist +| | | \---types +| | +---sourcemap-codec +| | | +---dist +| | | +---src +| | | \---types +| | \---trace-mapping +| | +---dist +| | +---src +| | \---types +| +---@microsoft +| | +---tsdoc +| | | +---lib +| | | | +---beta +| | | | | \---__tests__ +| | | | +---configuration +| | | | +---details +| | | | +---emitters +| | | | | \---__tests__ +| | | | +---nodes +| | | | +---parser +| | | | | \---__tests__ +| | | | +---transforms +| | | | \---__tests__ +| | | +---lib-commonjs +| | | | +---beta +| | | | | \---__tests__ +| | | | +---configuration +| | | | +---details +| | | | +---emitters +| | | | | \---__tests__ +| | | | +---nodes +| | | | +---parser +| | | | | \---__tests__ +| | | | +---transforms +| | | | \---__tests__ +| | | \---schemas +| | \---tsdoc-config +| | +---lib +| | | \---__tests__ +| | | \---assets +| | | +---e1 +| | | +---e2 +| | | +---e3 +| | | +---e4 +| | | +---e5 +| | | +---e6 +| | | +---e7 +| | | +---p1 +| | | +---p10 +| | | | \---base1 +| | | +---p11 +| | | | +---base1 +| | | | \---base2 +| | | +---p12 +| | | +---p2 +| | | +---p3 +| | | | +---base1 +| | | | \---base2 +| | | +---p4 +| | | | \---example-lib +| | | | \---dist +| | | +---p5 +| | | | +---base1 +| | | | \---base2 +| | | +---p6 +| | | | \---base1 +| | | +---p7 +| | | +---p8 +| | | | \---base1 +| | | \---p9 +| | | \---base1 +| | \---resolve +| | +---.github +| | | \---workflows +| | +---example +| | +---lib +| | \---test +| | +---dotdot +| | | \---abc +| | +---module_dir +| | | +---xmodules +| | | | \---aaa +| | | +---ymodules +| | | | \---aaa +| | | \---zmodules +| | | \---bbb +| | +---node_path +| | | +---x +| | | | +---aaa +| | | | \---ccc +| | | \---y +| | | +---bbb +| | | \---ccc +| | +---pathfilter +| | | \---deep_ref +| | +---precedence +| | | +---aaa +| | | \---bbb +| | +---resolver +| | | +---baz +| | | +---browser_field +| | | +---dot_main +| | | +---dot_slash_main +| | | +---incorrect_main +| | | +---invalid_main +| | | +---multirepo +| | | | \---packages +| | | | +---package-a +| | | | \---package-b +| | | +---nested_symlinks +| | | | \---mylib +| | | +---other_path +| | | | \---lib +| | | +---quux +| | | | \---foo +| | | +---same_names +| | | | \---foo +| | | +---symlinked +| | | | +---package +| | | | \---_ +| | | | \---symlink_target +| | | \---without_basedir +| | \---shadowed_core +| | \---util +| +---@mui +| | +---base +| | | +---Badge +| | | +---Button +| | | +---ClassNameGenerator +| | | +---ClickAwayListener +| | | +---composeClasses +| | | +---Dropdown +| | | +---FocusTrap +| | | +---FormControl +| | | +---generateUtilityClass +| | | +---generateUtilityClasses +| | | +---Input +| | | +---legacy +| | | | +---Badge +| | | | +---Button +| | | | +---ClassNameGenerator +| | | | +---ClickAwayListener +| | | | +---composeClasses +| | | | +---Dropdown +| | | | +---FocusTrap +| | | | +---FormControl +| | | | +---generateUtilityClass +| | | | +---generateUtilityClasses +| | | | +---Input +| | | | +---Menu +| | | | +---MenuButton +| | | | +---MenuItem +| | | | +---Modal +| | | | +---MultiSelect +| | | | +---NoSsr +| | | | +---Option +| | | | +---OptionGroup +| | | | +---Popper +| | | | +---Portal +| | | | +---Select +| | | | +---Slider +| | | | +---Snackbar +| | | | +---Switch +| | | | +---Tab +| | | | +---TablePagination +| | | | +---TabPanel +| | | | +---Tabs +| | | | +---TabsList +| | | | +---TextareaAutosize +| | | | +---Transitions +| | | | +---Unstable_NumberInput +| | | | +---Unstable_Popup +| | | | +---unstable_useModal +| | | | +---unstable_useNumberInput +| | | | +---useAutocomplete +| | | | +---useBadge +| | | | +---useButton +| | | | +---useCompound +| | | | +---useDropdown +| | | | +---useInput +| | | | +---useList +| | | | +---useMenu +| | | | +---useMenuButton +| | | | +---useMenuItem +| | | | +---useOption +| | | | +---useSelect +| | | | +---useSlider +| | | | +---useSnackbar +| | | | +---useSwitch +| | | | +---useTab +| | | | +---useTabPanel +| | | | +---useTabs +| | | | +---useTabsList +| | | | +---useTransition +| | | | \---utils +| | | +---Menu +| | | +---MenuButton +| | | +---MenuItem +| | | +---Modal +| | | +---modern +| | | | +---Badge +| | | | +---Button +| | | | +---ClassNameGenerator +| | | | +---ClickAwayListener +| | | | +---composeClasses +| | | | +---Dropdown +| | | | +---FocusTrap +| | | | +---FormControl +| | | | +---generateUtilityClass +| | | | +---generateUtilityClasses +| | | | +---Input +| | | | +---Menu +| | | | +---MenuButton +| | | | +---MenuItem +| | | | +---Modal +| | | | +---MultiSelect +| | | | +---NoSsr +| | | | +---Option +| | | | +---OptionGroup +| | | | +---Popper +| | | | +---Portal +| | | | +---Select +| | | | +---Slider +| | | | +---Snackbar +| | | | +---Switch +| | | | +---Tab +| | | | +---TablePagination +| | | | +---TabPanel +| | | | +---Tabs +| | | | +---TabsList +| | | | +---TextareaAutosize +| | | | +---Transitions +| | | | +---Unstable_NumberInput +| | | | +---Unstable_Popup +| | | | +---unstable_useModal +| | | | +---unstable_useNumberInput +| | | | +---useAutocomplete +| | | | +---useBadge +| | | | +---useButton +| | | | +---useCompound +| | | | +---useDropdown +| | | | +---useInput +| | | | +---useList +| | | | +---useMenu +| | | | +---useMenuButton +| | | | +---useMenuItem +| | | | +---useOption +| | | | +---useSelect +| | | | +---useSlider +| | | | +---useSnackbar +| | | | +---useSwitch +| | | | +---useTab +| | | | +---useTabPanel +| | | | +---useTabs +| | | | +---useTabsList +| | | | +---useTransition +| | | | \---utils +| | | +---MultiSelect +| | | +---node +| | | | +---Badge +| | | | +---Button +| | | | +---ClassNameGenerator +| | | | +---ClickAwayListener +| | | | +---composeClasses +| | | | +---Dropdown +| | | | +---FocusTrap +| | | | +---FormControl +| | | | +---generateUtilityClass +| | | | +---generateUtilityClasses +| | | | +---Input +| | | | +---Menu +| | | | +---MenuButton +| | | | +---MenuItem +| | | | +---Modal +| | | | +---MultiSelect +| | | | +---NoSsr +| | | | +---Option +| | | | +---OptionGroup +| | | | +---Popper +| | | | +---Portal +| | | | +---Select +| | | | +---Slider +| | | | +---Snackbar +| | | | +---Switch +| | | | +---Tab +| | | | +---TablePagination +| | | | +---TabPanel +| | | | +---Tabs +| | | | +---TabsList +| | | | +---TextareaAutosize +| | | | +---Transitions +| | | | +---Unstable_NumberInput +| | | | +---Unstable_Popup +| | | | +---unstable_useModal +| | | | +---unstable_useNumberInput +| | | | +---useAutocomplete +| | | | +---useBadge +| | | | +---useButton +| | | | +---useCompound +| | | | +---useDropdown +| | | | +---useInput +| | | | +---useList +| | | | +---useMenu +| | | | +---useMenuButton +| | | | +---useMenuItem +| | | | +---useOption +| | | | +---useSelect +| | | | +---useSlider +| | | | +---useSnackbar +| | | | +---useSwitch +| | | | +---useTab +| | | | +---useTabPanel +| | | | +---useTabs +| | | | +---useTabsList +| | | | +---useTransition +| | | | \---utils +| | | +---NoSsr +| | | +---Option +| | | +---OptionGroup +| | | +---Popper +| | | +---Portal +| | | +---Select +| | | +---Slider +| | | +---Snackbar +| | | +---Switch +| | | +---Tab +| | | +---TablePagination +| | | +---TabPanel +| | | +---Tabs +| | | +---TabsList +| | | +---TextareaAutosize +| | | +---Transitions +| | | +---Unstable_NumberInput +| | | +---Unstable_Popup +| | | +---unstable_useModal +| | | +---unstable_useNumberInput +| | | +---useAutocomplete +| | | +---useBadge +| | | +---useButton +| | | +---useCompound +| | | +---useDropdown +| | | +---useInput +| | | +---useList +| | | +---useMenu +| | | +---useMenuButton +| | | +---useMenuItem +| | | +---useOption +| | | +---useSelect +| | | +---useSlider +| | | +---useSnackbar +| | | +---useSwitch +| | | +---useTab +| | | +---useTabPanel +| | | +---useTabs +| | | +---useTabsList +| | | +---useTransition +| | | \---utils +| | +---core-downloads-tracker +| | +---icons-material +| | | +---esm +| | | | \---utils +| | | \---utils +| | +---lab +| | | +---AdapterDateFns +| | | +---AdapterDayjs +| | | +---AdapterLuxon +| | | +---AdapterMoment +| | | +---Alert +| | | +---AlertTitle +| | | +---Autocomplete +| | | +---AvatarGroup +| | | +---CalendarPicker +| | | +---CalendarPickerSkeleton +| | | +---ClockPicker +| | | +---DatePicker +| | | +---DateRangePicker +| | | +---DateRangePickerDay +| | | +---DateTimePicker +| | | +---DesktopDatePicker +| | | +---DesktopDateRangePicker +| | | +---DesktopDateTimePicker +| | | +---DesktopTimePicker +| | | +---internal +| | | | \---svg-icons +| | | +---legacy +| | | | +---AdapterDateFns +| | | | +---AdapterDayjs +| | | | +---AdapterLuxon +| | | | +---AdapterMoment +| | | | +---Alert +| | | | +---AlertTitle +| | | | +---Autocomplete +| | | | +---AvatarGroup +| | | | +---CalendarPicker +| | | | +---CalendarPickerSkeleton +| | | | +---ClockPicker +| | | | +---DatePicker +| | | | +---DateRangePicker +| | | | +---DateRangePickerDay +| | | | +---DateTimePicker +| | | | +---DesktopDatePicker +| | | | +---DesktopDateRangePicker +| | | | +---DesktopDateTimePicker +| | | | +---DesktopTimePicker +| | | | +---internal +| | | | | \---svg-icons +| | | | +---LoadingButton +| | | | +---LocalizationProvider +| | | | +---Masonry +| | | | +---MobileDatePicker +| | | | +---MobileDateRangePicker +| | | | +---MobileDateTimePicker +| | | | +---MobileTimePicker +| | | | +---MonthPicker +| | | | +---Pagination +| | | | +---PaginationItem +| | | | +---PickersDay +| | | | +---Rating +| | | | +---Skeleton +| | | | +---SpeedDial +| | | | +---SpeedDialAction +| | | | +---SpeedDialIcon +| | | | +---StaticDatePicker +| | | | +---StaticDateRangePicker +| | | | +---StaticDateTimePicker +| | | | +---StaticTimePicker +| | | | +---TabContext +| | | | +---TabList +| | | | +---TabPanel +| | | | +---themeAugmentation +| | | | +---Timeline +| | | | +---TimelineConnector +| | | | +---TimelineContent +| | | | +---TimelineDot +| | | | +---TimelineItem +| | | | +---TimelineOppositeContent +| | | | +---TimelineSeparator +| | | | +---TimePicker +| | | | +---ToggleButton +| | | | +---ToggleButtonGroup +| | | | +---TreeItem +| | | | +---TreeView +| | | | +---useAutocomplete +| | | | \---YearPicker +| | | +---LoadingButton +| | | +---LocalizationProvider +| | | +---Masonry +| | | +---MobileDatePicker +| | | +---MobileDateRangePicker +| | | +---MobileDateTimePicker +| | | +---MobileTimePicker +| | | +---modern +| | | | +---AdapterDateFns +| | | | +---AdapterDayjs +| | | | +---AdapterLuxon +| | | | +---AdapterMoment +| | | | +---Alert +| | | | +---AlertTitle +| | | | +---Autocomplete +| | | | +---AvatarGroup +| | | | +---CalendarPicker +| | | | +---CalendarPickerSkeleton +| | | | +---ClockPicker +| | | | +---DatePicker +| | | | +---DateRangePicker +| | | | +---DateRangePickerDay +| | | | +---DateTimePicker +| | | | +---DesktopDatePicker +| | | | +---DesktopDateRangePicker +| | | | +---DesktopDateTimePicker +| | | | +---DesktopTimePicker +| | | | +---internal +| | | | | \---svg-icons +| | | | +---LoadingButton +| | | | +---LocalizationProvider +| | | | +---Masonry +| | | | +---MobileDatePicker +| | | | +---MobileDateRangePicker +| | | | +---MobileDateTimePicker +| | | | +---MobileTimePicker +| | | | +---MonthPicker +| | | | +---Pagination +| | | | +---PaginationItem +| | | | +---PickersDay +| | | | +---Rating +| | | | +---Skeleton +| | | | +---SpeedDial +| | | | +---SpeedDialAction +| | | | +---SpeedDialIcon +| | | | +---StaticDatePicker +| | | | +---StaticDateRangePicker +| | | | +---StaticDateTimePicker +| | | | +---StaticTimePicker +| | | | +---TabContext +| | | | +---TabList +| | | | +---TabPanel +| | | | +---themeAugmentation +| | | | +---Timeline +| | | | +---TimelineConnector +| | | | +---TimelineContent +| | | | +---TimelineDot +| | | | +---TimelineItem +| | | | +---TimelineOppositeContent +| | | | +---TimelineSeparator +| | | | +---TimePicker +| | | | +---ToggleButton +| | | | +---ToggleButtonGroup +| | | | +---TreeItem +| | | | +---TreeView +| | | | +---useAutocomplete +| | | | \---YearPicker +| | | +---MonthPicker +| | | +---node +| | | | +---AdapterDateFns +| | | | +---AdapterDayjs +| | | | +---AdapterLuxon +| | | | +---AdapterMoment +| | | | +---Alert +| | | | +---AlertTitle +| | | | +---Autocomplete +| | | | +---AvatarGroup +| | | | +---CalendarPicker +| | | | +---CalendarPickerSkeleton +| | | | +---ClockPicker +| | | | +---DatePicker +| | | | +---DateRangePicker +| | | | +---DateRangePickerDay +| | | | +---DateTimePicker +| | | | +---DesktopDatePicker +| | | | +---DesktopDateRangePicker +| | | | +---DesktopDateTimePicker +| | | | +---DesktopTimePicker +| | | | +---internal +| | | | | \---svg-icons +| | | | +---LoadingButton +| | | | +---LocalizationProvider +| | | | +---Masonry +| | | | +---MobileDatePicker +| | | | +---MobileDateRangePicker +| | | | +---MobileDateTimePicker +| | | | +---MobileTimePicker +| | | | +---MonthPicker +| | | | +---Pagination +| | | | +---PaginationItem +| | | | +---PickersDay +| | | | +---Rating +| | | | +---Skeleton +| | | | +---SpeedDial +| | | | +---SpeedDialAction +| | | | +---SpeedDialIcon +| | | | +---StaticDatePicker +| | | | +---StaticDateRangePicker +| | | | +---StaticDateTimePicker +| | | | +---StaticTimePicker +| | | | +---TabContext +| | | | +---TabList +| | | | +---TabPanel +| | | | +---themeAugmentation +| | | | +---Timeline +| | | | +---TimelineConnector +| | | | +---TimelineContent +| | | | +---TimelineDot +| | | | +---TimelineItem +| | | | +---TimelineOppositeContent +| | | | +---TimelineSeparator +| | | | +---TimePicker +| | | | +---ToggleButton +| | | | +---ToggleButtonGroup +| | | | +---TreeItem +| | | | +---TreeView +| | | | +---useAutocomplete +| | | | \---YearPicker +| | | +---Pagination +| | | +---PaginationItem +| | | +---PickersDay +| | | +---Rating +| | | +---Skeleton +| | | +---SpeedDial +| | | +---SpeedDialAction +| | | +---SpeedDialIcon +| | | +---StaticDatePicker +| | | +---StaticDateRangePicker +| | | +---StaticDateTimePicker +| | | +---StaticTimePicker +| | | +---TabContext +| | | +---TabList +| | | +---TabPanel +| | | +---themeAugmentation +| | | +---Timeline +| | | +---TimelineConnector +| | | +---TimelineContent +| | | +---TimelineDot +| | | +---TimelineItem +| | | +---TimelineOppositeContent +| | | +---TimelineSeparator +| | | +---TimePicker +| | | +---ToggleButton +| | | +---ToggleButtonGroup +| | | +---TreeItem +| | | +---TreeView +| | | +---useAutocomplete +| | | \---YearPicker +| | +---material +| | | +---Accordion +| | | +---AccordionActions +| | | +---AccordionDetails +| | | +---AccordionSummary +| | | +---Alert +| | | +---AlertTitle +| | | +---AppBar +| | | +---Autocomplete +| | | +---Avatar +| | | +---AvatarGroup +| | | +---Backdrop +| | | +---Badge +| | | +---BottomNavigation +| | | +---BottomNavigationAction +| | | +---Box +| | | +---Breadcrumbs +| | | +---Button +| | | +---ButtonBase +| | | +---ButtonGroup +| | | +---Card +| | | +---CardActionArea +| | | +---CardActions +| | | +---CardContent +| | | +---CardHeader +| | | +---CardMedia +| | | +---Checkbox +| | | +---Chip +| | | +---CircularProgress +| | | +---className +| | | +---ClickAwayListener +| | | +---Collapse +| | | +---colors +| | | +---Container +| | | +---CssBaseline +| | | +---darkScrollbar +| | | +---Dialog +| | | +---DialogActions +| | | +---DialogContent +| | | +---DialogContentText +| | | +---DialogTitle +| | | +---Divider +| | | +---Drawer +| | | +---Fab +| | | +---Fade +| | | +---FilledInput +| | | +---FormControl +| | | +---FormControlLabel +| | | +---FormGroup +| | | +---FormHelperText +| | | +---FormLabel +| | | +---generateUtilityClass +| | | +---generateUtilityClasses +| | | +---GlobalStyles +| | | +---Grid +| | | +---Grow +| | | +---Hidden +| | | +---Icon +| | | +---IconButton +| | | +---ImageList +| | | +---ImageListItem +| | | +---ImageListItemBar +| | | +---Input +| | | +---InputAdornment +| | | +---InputBase +| | | +---InputLabel +| | | +---internal +| | | | \---svg-icons +| | | +---legacy +| | | | +---Accordion +| | | | +---AccordionActions +| | | | +---AccordionDetails +| | | | +---AccordionSummary +| | | | +---Alert +| | | | +---AlertTitle +| | | | +---AppBar +| | | | +---Autocomplete +| | | | +---Avatar +| | | | +---AvatarGroup +| | | | +---Backdrop +| | | | +---Badge +| | | | +---BottomNavigation +| | | | +---BottomNavigationAction +| | | | +---Box +| | | | +---Breadcrumbs +| | | | +---Button +| | | | +---ButtonBase +| | | | +---ButtonGroup +| | | | +---Card +| | | | +---CardActionArea +| | | | +---CardActions +| | | | +---CardContent +| | | | +---CardHeader +| | | | +---CardMedia +| | | | +---Checkbox +| | | | +---Chip +| | | | +---CircularProgress +| | | | +---className +| | | | +---ClickAwayListener +| | | | +---Collapse +| | | | +---colors +| | | | +---Container +| | | | +---CssBaseline +| | | | +---darkScrollbar +| | | | +---Dialog +| | | | +---DialogActions +| | | | +---DialogContent +| | | | +---DialogContentText +| | | | +---DialogTitle +| | | | +---Divider +| | | | +---Drawer +| | | | +---Fab +| | | | +---Fade +| | | | +---FilledInput +| | | | +---FormControl +| | | | +---FormControlLabel +| | | | +---FormGroup +| | | | +---FormHelperText +| | | | +---FormLabel +| | | | +---generateUtilityClass +| | | | +---generateUtilityClasses +| | | | +---GlobalStyles +| | | | +---Grid +| | | | +---Grow +| | | | +---Hidden +| | | | +---Icon +| | | | +---IconButton +| | | | +---ImageList +| | | | +---ImageListItem +| | | | +---ImageListItemBar +| | | | +---Input +| | | | +---InputAdornment +| | | | +---InputBase +| | | | +---InputLabel +| | | | +---internal +| | | | | \---svg-icons +| | | | +---LinearProgress +| | | | +---Link +| | | | +---List +| | | | +---ListItem +| | | | +---ListItemAvatar +| | | | +---ListItemButton +| | | | +---ListItemIcon +| | | | +---ListItemSecondaryAction +| | | | +---ListItemText +| | | | +---ListSubheader +| | | | +---locale +| | | | +---Menu +| | | | +---MenuItem +| | | | +---MenuList +| | | | +---MobileStepper +| | | | +---Modal +| | | | +---NativeSelect +| | | | +---NoSsr +| | | | +---OutlinedInput +| | | | +---Pagination +| | | | +---PaginationItem +| | | | +---Paper +| | | | +---Popover +| | | | +---Popper +| | | | +---Portal +| | | | +---Radio +| | | | +---RadioGroup +| | | | +---Rating +| | | | +---ScopedCssBaseline +| | | | +---Select +| | | | +---Skeleton +| | | | +---Slide +| | | | +---Slider +| | | | +---Snackbar +| | | | +---SnackbarContent +| | | | +---SpeedDial +| | | | +---SpeedDialAction +| | | | +---SpeedDialIcon +| | | | +---Stack +| | | | +---Step +| | | | +---StepButton +| | | | +---StepConnector +| | | | +---StepContent +| | | | +---StepIcon +| | | | +---StepLabel +| | | | +---Stepper +| | | | +---StyledEngineProvider +| | | | +---styles +| | | | +---SvgIcon +| | | | +---SwipeableDrawer +| | | | +---Switch +| | | | +---Tab +| | | | +---Table +| | | | +---TableBody +| | | | +---TableCell +| | | | +---TableContainer +| | | | +---TableFooter +| | | | +---TableHead +| | | | +---TablePagination +| | | | +---TableRow +| | | | +---TableSortLabel +| | | | +---Tabs +| | | | +---TabScrollButton +| | | | +---TextareaAutosize +| | | | +---TextField +| | | | +---ToggleButton +| | | | +---ToggleButtonGroup +| | | | +---Toolbar +| | | | +---Tooltip +| | | | +---transitions +| | | | +---types +| | | | +---Typography +| | | | +---Unstable_Grid2 +| | | | +---Unstable_TrapFocus +| | | | +---useAutocomplete +| | | | +---useMediaQuery +| | | | +---usePagination +| | | | +---useScrollTrigger +| | | | +---useTouchRipple +| | | | +---utils +| | | | +---zero-styled +| | | | \---Zoom +| | | +---LinearProgress +| | | +---Link +| | | +---List +| | | +---ListItem +| | | +---ListItemAvatar +| | | +---ListItemButton +| | | +---ListItemIcon +| | | +---ListItemSecondaryAction +| | | +---ListItemText +| | | +---ListSubheader +| | | +---locale +| | | +---Menu +| | | +---MenuItem +| | | +---MenuList +| | | +---MobileStepper +| | | +---Modal +| | | +---modern +| | | | +---Accordion +| | | | +---AccordionActions +| | | | +---AccordionDetails +| | | | +---AccordionSummary +| | | | +---Alert +| | | | +---AlertTitle +| | | | +---AppBar +| | | | +---Autocomplete +| | | | +---Avatar +| | | | +---AvatarGroup +| | | | +---Backdrop +| | | | +---Badge +| | | | +---BottomNavigation +| | | | +---BottomNavigationAction +| | | | +---Box +| | | | +---Breadcrumbs +| | | | +---Button +| | | | +---ButtonBase +| | | | +---ButtonGroup +| | | | +---Card +| | | | +---CardActionArea +| | | | +---CardActions +| | | | +---CardContent +| | | | +---CardHeader +| | | | +---CardMedia +| | | | +---Checkbox +| | | | +---Chip +| | | | +---CircularProgress +| | | | +---className +| | | | +---ClickAwayListener +| | | | +---Collapse +| | | | +---colors +| | | | +---Container +| | | | +---CssBaseline +| | | | +---darkScrollbar +| | | | +---Dialog +| | | | +---DialogActions +| | | | +---DialogContent +| | | | +---DialogContentText +| | | | +---DialogTitle +| | | | +---Divider +| | | | +---Drawer +| | | | +---Fab +| | | | +---Fade +| | | | +---FilledInput +| | | | +---FormControl +| | | | +---FormControlLabel +| | | | +---FormGroup +| | | | +---FormHelperText +| | | | +---FormLabel +| | | | +---generateUtilityClass +| | | | +---generateUtilityClasses +| | | | +---GlobalStyles +| | | | +---Grid +| | | | +---Grow +| | | | +---Hidden +| | | | +---Icon +| | | | +---IconButton +| | | | +---ImageList +| | | | +---ImageListItem +| | | | +---ImageListItemBar +| | | | +---Input +| | | | +---InputAdornment +| | | | +---InputBase +| | | | +---InputLabel +| | | | +---internal +| | | | | \---svg-icons +| | | | +---LinearProgress +| | | | +---Link +| | | | +---List +| | | | +---ListItem +| | | | +---ListItemAvatar +| | | | +---ListItemButton +| | | | +---ListItemIcon +| | | | +---ListItemSecondaryAction +| | | | +---ListItemText +| | | | +---ListSubheader +| | | | +---locale +| | | | +---Menu +| | | | +---MenuItem +| | | | +---MenuList +| | | | +---MobileStepper +| | | | +---Modal +| | | | +---NativeSelect +| | | | +---NoSsr +| | | | +---OutlinedInput +| | | | +---Pagination +| | | | +---PaginationItem +| | | | +---Paper +| | | | +---Popover +| | | | +---Popper +| | | | +---Portal +| | | | +---Radio +| | | | +---RadioGroup +| | | | +---Rating +| | | | +---ScopedCssBaseline +| | | | +---Select +| | | | +---Skeleton +| | | | +---Slide +| | | | +---Slider +| | | | +---Snackbar +| | | | +---SnackbarContent +| | | | +---SpeedDial +| | | | +---SpeedDialAction +| | | | +---SpeedDialIcon +| | | | +---Stack +| | | | +---Step +| | | | +---StepButton +| | | | +---StepConnector +| | | | +---StepContent +| | | | +---StepIcon +| | | | +---StepLabel +| | | | +---Stepper +| | | | +---StyledEngineProvider +| | | | +---styles +| | | | +---SvgIcon +| | | | +---SwipeableDrawer +| | | | +---Switch +| | | | +---Tab +| | | | +---Table +| | | | +---TableBody +| | | | +---TableCell +| | | | +---TableContainer +| | | | +---TableFooter +| | | | +---TableHead +| | | | +---TablePagination +| | | | +---TableRow +| | | | +---TableSortLabel +| | | | +---Tabs +| | | | +---TabScrollButton +| | | | +---TextareaAutosize +| | | | +---TextField +| | | | +---ToggleButton +| | | | +---ToggleButtonGroup +| | | | +---Toolbar +| | | | +---Tooltip +| | | | +---transitions +| | | | +---types +| | | | +---Typography +| | | | +---Unstable_Grid2 +| | | | +---Unstable_TrapFocus +| | | | +---useAutocomplete +| | | | +---useMediaQuery +| | | | +---usePagination +| | | | +---useScrollTrigger +| | | | +---useTouchRipple +| | | | +---utils +| | | | +---zero-styled +| | | | \---Zoom +| | | +---NativeSelect +| | | +---node +| | | | +---Accordion +| | | | +---AccordionActions +| | | | +---AccordionDetails +| | | | +---AccordionSummary +| | | | +---Alert +| | | | +---AlertTitle +| | | | +---AppBar +| | | | +---Autocomplete +| | | | +---Avatar +| | | | +---AvatarGroup +| | | | +---Backdrop +| | | | +---Badge +| | | | +---BottomNavigation +| | | | +---BottomNavigationAction +| | | | +---Box +| | | | +---Breadcrumbs +| | | | +---Button +| | | | +---ButtonBase +| | | | +---ButtonGroup +| | | | +---Card +| | | | +---CardActionArea +| | | | +---CardActions +| | | | +---CardContent +| | | | +---CardHeader +| | | | +---CardMedia +| | | | +---Checkbox +| | | | +---Chip +| | | | +---CircularProgress +| | | | +---className +| | | | +---ClickAwayListener +| | | | +---Collapse +| | | | +---colors +| | | | +---Container +| | | | +---CssBaseline +| | | | +---darkScrollbar +| | | | +---Dialog +| | | | +---DialogActions +| | | | +---DialogContent +| | | | +---DialogContentText +| | | | +---DialogTitle +| | | | +---Divider +| | | | +---Drawer +| | | | +---Fab +| | | | +---Fade +| | | | +---FilledInput +| | | | +---FormControl +| | | | +---FormControlLabel +| | | | +---FormGroup +| | | | +---FormHelperText +| | | | +---FormLabel +| | | | +---generateUtilityClass +| | | | +---generateUtilityClasses +| | | | +---GlobalStyles +| | | | +---Grid +| | | | +---Grow +| | | | +---Hidden +| | | | +---Icon +| | | | +---IconButton +| | | | +---ImageList +| | | | +---ImageListItem +| | | | +---ImageListItemBar +| | | | +---Input +| | | | +---InputAdornment +| | | | +---InputBase +| | | | +---InputLabel +| | | | +---internal +| | | | | \---svg-icons +| | | | +---LinearProgress +| | | | +---Link +| | | | +---List +| | | | +---ListItem +| | | | +---ListItemAvatar +| | | | +---ListItemButton +| | | | +---ListItemIcon +| | | | +---ListItemSecondaryAction +| | | | +---ListItemText +| | | | +---ListSubheader +| | | | +---locale +| | | | +---Menu +| | | | +---MenuItem +| | | | +---MenuList +| | | | +---MobileStepper +| | | | +---Modal +| | | | +---NativeSelect +| | | | +---NoSsr +| | | | +---OutlinedInput +| | | | +---Pagination +| | | | +---PaginationItem +| | | | +---Paper +| | | | +---Popover +| | | | +---Popper +| | | | +---Portal +| | | | +---Radio +| | | | +---RadioGroup +| | | | +---Rating +| | | | +---ScopedCssBaseline +| | | | +---Select +| | | | +---Skeleton +| | | | +---Slide +| | | | +---Slider +| | | | +---Snackbar +| | | | +---SnackbarContent +| | | | +---SpeedDial +| | | | +---SpeedDialAction +| | | | +---SpeedDialIcon +| | | | +---Stack +| | | | +---Step +| | | | +---StepButton +| | | | +---StepConnector +| | | | +---StepContent +| | | | +---StepIcon +| | | | +---StepLabel +| | | | +---Stepper +| | | | +---StyledEngineProvider +| | | | +---styles +| | | | +---SvgIcon +| | | | +---SwipeableDrawer +| | | | +---Switch +| | | | +---Tab +| | | | +---Table +| | | | +---TableBody +| | | | +---TableCell +| | | | +---TableContainer +| | | | +---TableFooter +| | | | +---TableHead +| | | | +---TablePagination +| | | | +---TableRow +| | | | +---TableSortLabel +| | | | +---Tabs +| | | | +---TabScrollButton +| | | | +---TextareaAutosize +| | | | +---TextField +| | | | +---ToggleButton +| | | | +---ToggleButtonGroup +| | | | +---Toolbar +| | | | +---Tooltip +| | | | +---transitions +| | | | +---types +| | | | +---Typography +| | | | +---Unstable_Grid2 +| | | | +---Unstable_TrapFocus +| | | | +---useAutocomplete +| | | | +---useMediaQuery +| | | | +---usePagination +| | | | +---useScrollTrigger +| | | | +---useTouchRipple +| | | | +---utils +| | | | +---zero-styled +| | | | \---Zoom +| | | +---NoSsr +| | | +---OutlinedInput +| | | +---Pagination +| | | +---PaginationItem +| | | +---Paper +| | | +---Popover +| | | +---Popper +| | | +---Portal +| | | +---Radio +| | | +---RadioGroup +| | | +---Rating +| | | +---ScopedCssBaseline +| | | +---Select +| | | +---Skeleton +| | | +---Slide +| | | +---Slider +| | | +---Snackbar +| | | +---SnackbarContent +| | | +---SpeedDial +| | | +---SpeedDialAction +| | | +---SpeedDialIcon +| | | +---Stack +| | | +---Step +| | | +---StepButton +| | | +---StepConnector +| | | +---StepContent +| | | +---StepIcon +| | | +---StepLabel +| | | +---Stepper +| | | +---StyledEngineProvider +| | | +---styles +| | | +---SvgIcon +| | | +---SwipeableDrawer +| | | +---Switch +| | | +---Tab +| | | +---Table +| | | +---TableBody +| | | +---TableCell +| | | +---TableContainer +| | | +---TableFooter +| | | +---TableHead +| | | +---TablePagination +| | | +---TableRow +| | | +---TableSortLabel +| | | +---Tabs +| | | +---TabScrollButton +| | | +---TextareaAutosize +| | | +---TextField +| | | +---themeCssVarsAugmentation +| | | +---ToggleButton +| | | +---ToggleButtonGroup +| | | +---Toolbar +| | | +---Tooltip +| | | +---transitions +| | | +---types +| | | +---Typography +| | | +---umd +| | | +---Unstable_Grid2 +| | | +---Unstable_TrapFocus +| | | +---useAutocomplete +| | | +---useMediaQuery +| | | +---usePagination +| | | +---useScrollTrigger +| | | +---useTouchRipple +| | | +---utils +| | | +---zero-styled +| | | \---Zoom +| | +---private-theming +| | | +---defaultTheme +| | | +---legacy +| | | | +---ThemeProvider +| | | | \---useTheme +| | | +---modern +| | | | +---ThemeProvider +| | | | \---useTheme +| | | +---node +| | | | +---ThemeProvider +| | | | \---useTheme +| | | | +---@mui +| | | | | +---types +| | | | | \---utils +| | | | | +---appendOwnerState +| | | | | +---capitalize +| | | | | +---chainPropTypes +| | | | | +---clamp +| | | | | +---ClassNameGenerator +| | | | | +---composeClasses +| | | | | +---createChainedFunction +| | | | | +---debounce +| | | | | +---deepmerge +| | | | | +---deprecatedPropType +| | | | | +---elementAcceptingRef +| | | | | +---elementTypeAcceptingRef +| | | | | +---esm +| | | | | | +---appendOwnerState +| | | | | | +---capitalize +| | | | | | +---chainPropTypes +| | | | | | +---clamp +| | | | | | +---ClassNameGenerator +| | | | | | +---composeClasses +| | | | | | +---createChainedFunction +| | | | | | +---debounce +| | | | | | +---deepmerge +| | | | | | +---deprecatedPropType +| | | | | | +---elementAcceptingRef +| | | | | | +---elementTypeAcceptingRef +| | | | | | +---exactProp +| | | | | | +---extractEventHandlers +| | | | | | +---formatMuiErrorMessage +| | | | | | +---generateUtilityClass +| | | | | | +---generateUtilityClasses +| | | | | | +---getDisplayName +| | | | | | +---getReactElementRef +| | | | | | +---getScrollbarSize +| | | | | | +---getValidReactChildren +| | | | | | +---HTMLElementType +| | | | | | +---integerPropType +| | | | | | +---isHostComponent +| | | | | | +---isMuiElement +| | | | | | +---mergeSlotProps +| | | | | | +---omitEventHandlers +| | | | | | +---ownerDocument +| | | | | | +---ownerWindow +| | | | | | +---ponyfillGlobal +| | | | | | +---refType +| | | | | | +---requirePropFactory +| | | | | | +---resolveComponentProps +| | | | | | +---resolveProps +| | | | | | +---scrollLeft +| | | | | | +---setRef +| | | | | | +---unsupportedProp +| | | | | | +---useControlled +| | | | | | +---useEnhancedEffect +| | | | | | +---useEventCallback +| | | | | | +---useForkRef +| | | | | | +---useId +| | | | | | +---useIsFocusVisible +| | | | | | +---useLazyRef +| | | | | | +---useLocalStorageState +| | | | | | +---useOnMount +| | | | | | +---usePreviousProps +| | | | | | +---useSlotProps +| | | | | | +---useTimeout +| | | | | | \---visuallyHidden +| | | | | +---exactProp +| | | | | +---extractEventHandlers +| | | | | +---formatMuiErrorMessage +| | | | | +---generateUtilityClass +| | | | | +---generateUtilityClasses +| | | | | +---getDisplayName +| | | | | +---getReactElementRef +| | | | | +---getScrollbarSize +| | | | | +---getValidReactChildren +| | | | | +---HTMLElementType +| | | | | +---integerPropType +| | | | | +---isHostComponent +| | | | | +---isMuiElement +| | | | | +---legacy +| | | | | | +---appendOwnerState +| | | | | | +---capitalize +| | | | | | +---chainPropTypes +| | | | | | +---clamp +| | | | | | +---ClassNameGenerator +| | | | | | +---composeClasses +| | | | | | +---createChainedFunction +| | | | | | +---debounce +| | | | | | +---deepmerge +| | | | | | +---deprecatedPropType +| | | | | | +---elementAcceptingRef +| | | | | | +---elementTypeAcceptingRef +| | | | | | +---exactProp +| | | | | | +---extractEventHandlers +| | | | | | +---formatMuiErrorMessage +| | | | | | +---generateUtilityClass +| | | | | | +---generateUtilityClasses +| | | | | | +---getDisplayName +| | | | | | +---getReactElementRef +| | | | | | +---getScrollbarSize +| | | | | | +---getValidReactChildren +| | | | | | +---HTMLElementType +| | | | | | +---integerPropType +| | | | | | +---isHostComponent +| | | | | | +---isMuiElement +| | | | | | +---mergeSlotProps +| | | | | | +---omitEventHandlers +| | | | | | +---ownerDocument +| | | | | | +---ownerWindow +| | | | | | +---ponyfillGlobal +| | | | | | +---refType +| | | | | | +---requirePropFactory +| | | | | | +---resolveComponentProps +| | | | | | +---resolveProps +| | | | | | +---scrollLeft +| | | | | | +---setRef +| | | | | | +---unsupportedProp +| | | | | | +---useControlled +| | | | | | +---useEnhancedEffect +| | | | | | +---useEventCallback +| | | | | | +---useForkRef +| | | | | | +---useId +| | | | | | +---useIsFocusVisible +| | | | | | +---useLazyRef +| | | | | | +---useLocalStorageState +| | | | | | +---useOnMount +| | | | | | +---usePreviousProps +| | | | | | +---useSlotProps +| | | | | | +---useTimeout +| | | | | | \---visuallyHidden +| | | | | +---mergeSlotProps +| | | | | +---modern +| | | | | | +---appendOwnerState +| | | | | | +---capitalize +| | | | | | +---chainPropTypes +| | | | | | +---clamp +| | | | | | +---ClassNameGenerator +| | | | | | +---composeClasses +| | | | | | +---createChainedFunction +| | | | | | +---debounce +| | | | | | +---deepmerge +| | | | | | +---deprecatedPropType +| | | | | | +---elementAcceptingRef +| | | | | | +---elementTypeAcceptingRef +| | | | | | +---exactProp +| | | | | | +---extractEventHandlers +| | | | | | +---formatMuiErrorMessage +| | | | | | +---generateUtilityClass +| | | | | | +---generateUtilityClasses +| | | | | | +---getDisplayName +| | | | | | +---getReactElementRef +| | | | | | +---getScrollbarSize +| | | | | | +---getValidReactChildren +| | | | | | +---HTMLElementType +| | | | | | +---integerPropType +| | | | | | +---isHostComponent +| | | | | | +---isMuiElement +| | | | | | +---mergeSlotProps +| | | | | | +---omitEventHandlers +| | | | | | +---ownerDocument +| | | | | | +---ownerWindow +| | | | | | +---ponyfillGlobal +| | | | | | +---refType +| | | | | | +---requirePropFactory +| | | | | | +---resolveComponentProps +| | | | | | +---resolveProps +| | | | | | +---scrollLeft +| | | | | | +---setRef +| | | | | | +---unsupportedProp +| | | | | | +---useControlled +| | | | | | +---useEnhancedEffect +| | | | | | +---useEventCallback +| | | | | | +---useForkRef +| | | | | | +---useId +| | | | | | +---useIsFocusVisible +| | | | | | +---useLazyRef +| | | | | | +---useLocalStorageState +| | | | | | +---useOnMount +| | | | | | +---usePreviousProps +| | | | | | +---useSlotProps +| | | | | | +---useTimeout +| | | | | | \---visuallyHidden +| | | | | +---omitEventHandlers +| | | | | +---ownerDocument +| | | | | +---ownerWindow +| | | | | +---ponyfillGlobal +| | | | | +---refType +| | | | | +---requirePropFactory +| | | | | +---resolveComponentProps +| | | | | +---resolveProps +| | | | | +---scrollLeft +| | | | | +---setRef +| | | | | +---unsupportedProp +| | | | | +---useControlled +| | | | | +---useEnhancedEffect +| | | | | +---useEventCallback +| | | | | +---useForkRef +| | | | | +---useId +| | | | | +---useIsFocusVisible +| | | | | +---useLazyRef +| | | | | +---useLocalStorageState +| | | | | +---useOnMount +| | | | | +---usePreviousProps +| | | | | +---useSlotProps +| | | | | +---useTimeout +| | | | | \---visuallyHidden +| | | | \---react-is +| | | | \---cjs +| | | +---ThemeProvider +| | | \---useTheme +| | +---styled-engine +| | | +---GlobalStyles +| | | +---legacy +| | | | +---GlobalStyles +| | | | \---StyledEngineProvider +| | | +---modern +| | | | +---GlobalStyles +| | | | \---StyledEngineProvider +| | | +---node +| | | | +---GlobalStyles +| | | | \---StyledEngineProvider +| | | | \---@emotion +| | | | +---cache +| | | | | +---dist +| | | | | | \---declarations +| | | | | | \---src +| | | | | \---src +| | | | | \---conditions +| | | | +---memoize +| | | | | +---dist +| | | | | | \---declarations +| | | | | | \---src +| | | | | \---src +| | | | \---weak-memoize +| | | | +---dist +| | | | | \---declarations +| | | | | \---src +| | | | \---src +| | | \---StyledEngineProvider +| | +---system +| | | +---Box +| | | +---Container +| | | +---createTheme +| | | +---cssVars +| | | +---esm +| | | | +---Box +| | | | +---Container +| | | | +---createTheme +| | | | +---cssVars +| | | | +---GlobalStyles +| | | | +---RtlProvider +| | | | +---Stack +| | | | +---styleFunctionSx +| | | | +---ThemeProvider +| | | | +---Unstable_Grid +| | | | +---useMediaQuery +| | | | \---useThemeProps +| | | +---GlobalStyles +| | | +---legacy +| | | | +---Box +| | | | +---Container +| | | | +---createTheme +| | | | +---cssVars +| | | | +---GlobalStyles +| | | | +---RtlProvider +| | | | +---Stack +| | | | +---styleFunctionSx +| | | | +---ThemeProvider +| | | | +---Unstable_Grid +| | | | +---useMediaQuery +| | | | \---useThemeProps +| | | +---modern +| | | | +---Box +| | | | +---Container +| | | | +---createTheme +| | | | +---cssVars +| | | | +---GlobalStyles +| | | | +---RtlProvider +| | | | +---Stack +| | | | +---styleFunctionSx +| | | | +---ThemeProvider +| | | | +---Unstable_Grid +| | | | +---useMediaQuery +| | | | \---useThemeProps +| | | +---RtlProvider +| | | +---Stack +| | | +---styleFunctionSx +| | | +---ThemeProvider +| | | +---Unstable_Grid +| | | +---useMediaQuery +| | | \---useThemeProps +| | +---types +| | | \---esm +| | +---utils +| | | +---capitalize +| | | +---chainPropTypes +| | | +---clamp +| | | +---ClassNameGenerator +| | | +---composeClasses +| | | +---createChainedFunction +| | | +---debounce +| | | +---deepmerge +| | | +---deprecatedPropType +| | | +---elementAcceptingRef +| | | +---elementTypeAcceptingRef +| | | +---exactProp +| | | +---formatMuiErrorMessage +| | | +---generateUtilityClass +| | | +---generateUtilityClasses +| | | +---getDisplayName +| | | +---getScrollbarSize +| | | +---getValidReactChildren +| | | +---HTMLElementType +| | | +---integerPropType +| | | +---isMuiElement +| | | +---legacy +| | | | +---capitalize +| | | | +---chainPropTypes +| | | | +---clamp +| | | | +---ClassNameGenerator +| | | | +---composeClasses +| | | | +---createChainedFunction +| | | | +---debounce +| | | | +---deepmerge +| | | | +---deprecatedPropType +| | | | +---elementAcceptingRef +| | | | +---elementTypeAcceptingRef +| | | | +---exactProp +| | | | +---formatMuiErrorMessage +| | | | +---generateUtilityClass +| | | | +---generateUtilityClasses +| | | | +---getDisplayName +| | | | +---getScrollbarSize +| | | | +---getValidReactChildren +| | | | +---HTMLElementType +| | | | +---integerPropType +| | | | +---isMuiElement +| | | | +---ownerDocument +| | | | +---ownerWindow +| | | | +---ponyfillGlobal +| | | | +---refType +| | | | +---requirePropFactory +| | | | +---resolveProps +| | | | +---scrollLeft +| | | | +---setRef +| | | | +---unsupportedProp +| | | | +---useControlled +| | | | +---useEnhancedEffect +| | | | +---useEventCallback +| | | | +---useForkRef +| | | | +---useId +| | | | +---useIsFocusVisible +| | | | +---useLazyRef +| | | | +---useLocalStorageState +| | | | +---useOnMount +| | | | +---usePreviousProps +| | | | +---useTimeout +| | | | \---visuallyHidden +| | | +---modern +| | | | +---capitalize +| | | | +---chainPropTypes +| | | | +---clamp +| | | | +---ClassNameGenerator +| | | | +---composeClasses +| | | | +---createChainedFunction +| | | | +---debounce +| | | | +---deepmerge +| | | | +---deprecatedPropType +| | | | +---elementAcceptingRef +| | | | +---elementTypeAcceptingRef +| | | | +---exactProp +| | | | +---formatMuiErrorMessage +| | | | +---generateUtilityClass +| | | | +---generateUtilityClasses +| | | | +---getDisplayName +| | | | +---getScrollbarSize +| | | | +---getValidReactChildren +| | | | +---HTMLElementType +| | | | +---integerPropType +| | | | +---isMuiElement +| | | | +---ownerDocument +| | | | +---ownerWindow +| | | | +---ponyfillGlobal +| | | | +---refType +| | | | +---requirePropFactory +| | | | +---resolveProps +| | | | +---scrollLeft +| | | | +---setRef +| | | | +---unsupportedProp +| | | | +---useControlled +| | | | +---useEnhancedEffect +| | | | +---useEventCallback +| | | | +---useForkRef +| | | | +---useId +| | | | +---useIsFocusVisible +| | | | +---useLazyRef +| | | | +---useLocalStorageState +| | | | +---useOnMount +| | | | +---usePreviousProps +| | | | +---useTimeout +| | | | \---visuallyHidden +| | | +---node +| | | | +---capitalize +| | | | +---chainPropTypes +| | | | +---clamp +| | | | +---ClassNameGenerator +| | | | +---composeClasses +| | | | +---createChainedFunction +| | | | +---debounce +| | | | +---deepmerge +| | | | +---deprecatedPropType +| | | | +---elementAcceptingRef +| | | | +---elementTypeAcceptingRef +| | | | +---exactProp +| | | | +---formatMuiErrorMessage +| | | | +---generateUtilityClass +| | | | +---generateUtilityClasses +| | | | +---getDisplayName +| | | | +---getScrollbarSize +| | | | +---getValidReactChildren +| | | | +---HTMLElementType +| | | | +---integerPropType +| | | | +---isMuiElement +| | | | +---ownerDocument +| | | | +---ownerWindow +| | | | +---ponyfillGlobal +| | | | +---refType +| | | | +---requirePropFactory +| | | | +---resolveProps +| | | | +---scrollLeft +| | | | +---setRef +| | | | +---unsupportedProp +| | | | +---useControlled +| | | | +---useEnhancedEffect +| | | | +---useEventCallback +| | | | +---useForkRef +| | | | +---useId +| | | | +---useIsFocusVisible +| | | | +---useLazyRef +| | | | +---useLocalStorageState +| | | | +---useOnMount +| | | | +---usePreviousProps +| | | | +---useTimeout +| | | | \---visuallyHidden +| | | +---ownerDocument +| | | +---ownerWindow +| | | +---ponyfillGlobal +| | | +---refType +| | | +---requirePropFactory +| | | +---resolveProps +| | | +---scrollLeft +| | | +---setRef +| | | +---unsupportedProp +| | | +---useControlled +| | | +---useEnhancedEffect +| | | +---useEventCallback +| | | +---useForkRef +| | | +---useId +| | | +---useIsFocusVisible +| | | +---useLazyRef +| | | +---useLocalStorageState +| | | +---useOnMount +| | | +---usePreviousProps +| | | +---useTimeout +| | | \---visuallyHidden +| | \---x-date-pickers +| | +---AdapterDateFns +| | +---AdapterDateFnsBase +| | +---AdapterDateFnsJalali +| | +---AdapterDateFnsJalaliV3 +| | +---AdapterDateFnsV3 +| | +---AdapterDayjs +| | +---AdapterLuxon +| | +---AdapterMoment +| | +---AdapterMomentHijri +| | +---AdapterMomentJalaali +| | +---DateCalendar +| | +---DateField +| | +---DatePicker +| | +---DateTimeField +| | +---DateTimePicker +| | +---dateViewRenderers +| | +---DayCalendarSkeleton +| | +---DesktopDatePicker +| | +---DesktopDateTimePicker +| | +---DesktopTimePicker +| | +---DigitalClock +| | +---hooks +| | +---icons +| | +---internals +| | | +---components +| | | | +---PickersArrowSwitcher +| | | | \---PickerViewRoot +| | | +---constants +| | | +---demo +| | | +---hooks +| | | | +---useDesktopPicker +| | | | +---useField +| | | | +---useMobilePicker +| | | | +---usePicker +| | | | \---useStaticPicker +| | | +---models +| | | | \---props +| | | \---utils +| | | \---validation +| | +---locales +| | | \---utils +| | +---LocalizationProvider +| | +---MobileDatePicker +| | +---MobileDateTimePicker +| | +---MobileTimePicker +| | +---models +| | +---modern +| | | +---AdapterDateFns +| | | +---AdapterDateFnsBase +| | | +---AdapterDateFnsJalali +| | | +---AdapterDateFnsJalaliV3 +| | | +---AdapterDateFnsV3 +| | | +---AdapterDayjs +| | | +---AdapterLuxon +| | | +---AdapterMoment +| | | +---AdapterMomentHijri +| | | +---AdapterMomentJalaali +| | | +---DateCalendar +| | | +---DateField +| | | +---DatePicker +| | | +---DateTimeField +| | | +---DateTimePicker +| | | +---dateViewRenderers +| | | +---DayCalendarSkeleton +| | | +---DesktopDatePicker +| | | +---DesktopDateTimePicker +| | | +---DesktopTimePicker +| | | +---DigitalClock +| | | +---hooks +| | | +---icons +| | | +---internals +| | | | +---components +| | | | | +---PickersArrowSwitcher +| | | | | \---PickerViewRoot +| | | | +---constants +| | | | +---demo +| | | | +---hooks +| | | | | +---useDesktopPicker +| | | | | +---useField +| | | | | +---useMobilePicker +| | | | | +---usePicker +| | | | | \---useStaticPicker +| | | | +---models +| | | | | \---props +| | | | \---utils +| | | | \---validation +| | | +---locales +| | | | \---utils +| | | +---LocalizationProvider +| | | +---MobileDatePicker +| | | +---MobileDateTimePicker +| | | +---MobileTimePicker +| | | +---models +| | | +---MonthCalendar +| | | +---MultiSectionDigitalClock +| | | +---PickersActionBar +| | | +---PickersCalendarHeader +| | | +---PickersDay +| | | +---PickersLayout +| | | +---PickersSectionList +| | | +---PickersShortcuts +| | | +---PickersTextField +| | | | +---PickersFilledInput +| | | | +---PickersInput +| | | | +---PickersInputBase +| | | | \---PickersOutlinedInput +| | | +---StaticDatePicker +| | | +---StaticDateTimePicker +| | | +---StaticTimePicker +| | | +---themeAugmentation +| | | +---TimeClock +| | | +---TimeField +| | | +---TimePicker +| | | +---timeViewRenderers +| | | \---YearCalendar +| | +---MonthCalendar +| | +---MultiSectionDigitalClock +| | +---node +| | | +---AdapterDateFns +| | | +---AdapterDateFnsBase +| | | +---AdapterDateFnsJalali +| | | +---AdapterDateFnsJalaliV3 +| | | +---AdapterDateFnsV3 +| | | +---AdapterDayjs +| | | +---AdapterLuxon +| | | +---AdapterMoment +| | | +---AdapterMomentHijri +| | | +---AdapterMomentJalaali +| | | +---DateCalendar +| | | +---DateField +| | | +---DatePicker +| | | +---DateTimeField +| | | +---DateTimePicker +| | | +---dateViewRenderers +| | | +---DayCalendarSkeleton +| | | +---DesktopDatePicker +| | | +---DesktopDateTimePicker +| | | +---DesktopTimePicker +| | | +---DigitalClock +| | | +---hooks +| | | +---icons +| | | +---internals +| | | | +---components +| | | | | +---PickersArrowSwitcher +| | | | | \---PickerViewRoot +| | | | +---constants +| | | | +---demo +| | | | +---hooks +| | | | | +---useDesktopPicker +| | | | | +---useField +| | | | | +---useMobilePicker +| | | | | +---usePicker +| | | | | \---useStaticPicker +| | | | +---models +| | | | | \---props +| | | | \---utils +| | | | \---validation +| | | +---locales +| | | | \---utils +| | | +---LocalizationProvider +| | | +---MobileDatePicker +| | | +---MobileDateTimePicker +| | | +---MobileTimePicker +| | | +---models +| | | +---MonthCalendar +| | | +---MultiSectionDigitalClock +| | | +---PickersActionBar +| | | +---PickersCalendarHeader +| | | +---PickersDay +| | | +---PickersLayout +| | | +---PickersSectionList +| | | +---PickersShortcuts +| | | +---PickersTextField +| | | | +---PickersFilledInput +| | | | +---PickersInput +| | | | +---PickersInputBase +| | | | \---PickersOutlinedInput +| | | +---StaticDatePicker +| | | +---StaticDateTimePicker +| | | +---StaticTimePicker +| | | +---themeAugmentation +| | | +---TimeClock +| | | +---TimeField +| | | +---TimePicker +| | | +---timeViewRenderers +| | | \---YearCalendar +| | +---PickersActionBar +| | +---PickersCalendarHeader +| | +---PickersDay +| | +---PickersLayout +| | +---PickersSectionList +| | +---PickersShortcuts +| | +---PickersTextField +| | | +---PickersFilledInput +| | | +---PickersInput +| | | +---PickersInputBase +| | | \---PickersOutlinedInput +| | +---StaticDatePicker +| | +---StaticDateTimePicker +| | +---StaticTimePicker +| | +---themeAugmentation +| | +---TimeClock +| | +---TimeField +| | +---TimePicker +| | +---timeViewRenderers +| | \---YearCalendar +| +---@napi-rs +| +---@next +| | +---env +| | | \---dist +| | +---eslint-plugin-next +| | | \---dist +| | | +---rules +| | | \---utils +| | \---swc-win32-x64-msvc +| +---@nicolo-ribaudo +| | \---eslint-scope-5-internals +| +---@nodelib +| | +---fs.scandir +| | | \---out +| | | +---adapters +| | | +---providers +| | | +---types +| | | \---utils +| | +---fs.stat +| | | \---out +| | | +---adapters +| | | +---providers +| | | \---types +| | \---fs.walk +| | \---out +| | +---providers +| | +---readers +| | \---types +| +---@nolyfill +| | \---is-core-module +| +---@phosphor-icons +| | \---react +| | \---dist +| | +---csr +| | +---defs +| | +---lib +| | \---ssr +| +---@pkgjs +| | \---parseargs +| | +---examples +| | \---internal +| +---@pkgr +| | \---core +| | \---lib +| +---@popperjs +| | \---core +| | +---dist +| | | +---cjs +| | | +---esm +| | | | +---dom-utils +| | | | +---modifiers +| | | | \---utils +| | | \---umd +| | \---lib +| | +---dom-utils +| | +---modifiers +| | \---utils +| +---@prisma +| | +---client +| | | +---generator-build +| | | +---runtime +| | | \---scripts +| | +---config +| | | \---dist +| | +---debug +| | | \---dist +| | +---engines +| | | +---dist +| | | | \---scripts +| | | | \---.cache +| | | | \---prisma +| | | | \---master +| | | | +---9b628578b3b7cae625e8c927178f15a170e74a9c +| | | | | \---windows +| | | | \---f40f79ec31188888a2e33acda0ecc8fd10a853a9 +| | | | \---windows +| | | \---scripts +| | +---engines-version +| | +---fetch-engine +| | | \---dist +| | \---get-platform +| | \---dist +| | \---test-utils +| +---@puppeteer +| | \---browsers +| | +---lib +| | | +---cjs +| | | | +---browser-data +| | | | \---generated +| | | \---esm +| | | +---browser-data +| | | \---generated +| | \---src +| | +---browser-data +| | +---generated +| | \---templates +| +---@rtsao +| | \---scc +| +---@rushstack +| | \---eslint-patch +| | \---lib +| | +---eslint-bulk-suppressions +| | | \---cli +| | | \---utils +| | \---exports +| +---@sinclair +| | \---typebox +| | +---compiler +| | +---errors +| | +---system +| | \---value +| +---@sinonjs +| | +---commons +| | | +---lib +| | | | \---prototypes +| | | \---types +| | | \---prototypes +| | \---fake-timers +| | \---src +| +---@swc +| | +---counter +| | \---helpers +| | +---cjs +| | +---esm +| | +---scripts +| | +---src +| | \---_ +| | +---index +| | +---_apply_decorated_descriptor +| | +---_apply_decs_2203_r +| | +---_array_like_to_array +| | +---_array_without_holes +| | +---_array_with_holes +| | +---_assert_this_initialized +| | +---_async_generator +| | +---_async_generator_delegate +| | +---_async_iterator +| | +---_async_to_generator +| | +---_await_async_generator +| | +---_await_value +| | +---_check_private_redeclaration +| | +---_class_apply_descriptor_destructure +| | +---_class_apply_descriptor_get +| | +---_class_apply_descriptor_set +| | +---_class_apply_descriptor_update +| | +---_class_call_check +| | +---_class_check_private_static_access +| | +---_class_check_private_static_field_descriptor +| | +---_class_extract_field_descriptor +| | +---_class_name_tdz_error +| | +---_class_private_field_destructure +| | +---_class_private_field_get +| | +---_class_private_field_init +| | +---_class_private_field_loose_base +| | +---_class_private_field_loose_key +| | +---_class_private_field_set +| | +---_class_private_field_update +| | +---_class_private_method_get +| | +---_class_private_method_init +| | +---_class_private_method_set +| | +---_class_static_private_field_destructure +| | +---_class_static_private_field_spec_get +| | +---_class_static_private_field_spec_set +| | +---_class_static_private_field_update +| | +---_class_static_private_method_get +| | +---_construct +| | +---_create_class +| | +---_create_for_of_iterator_helper_loose +| | +---_create_super +| | +---_decorate +| | +---_defaults +| | +---_define_enumerable_properties +| | +---_define_property +| | +---_dispose +| | +---_export_star +| | +---_extends +| | +---_get +| | +---_get_prototype_of +| | +---_inherits +| | +---_inherits_loose +| | +---_initializer_define_property +| | +---_initializer_warning_helper +| | +---_instanceof +| | +---_interop_require_default +| | +---_interop_require_wildcard +| | +---_is_native_function +| | +---_is_native_reflect_construct +| | +---_iterable_to_array +| | +---_iterable_to_array_limit +| | +---_iterable_to_array_limit_loose +| | +---_jsx +| | +---_new_arrow_check +| | +---_non_iterable_rest +| | +---_non_iterable_spread +| | +---_object_destructuring_empty +| | +---_object_spread +| | +---_object_spread_props +| | +---_object_without_properties +| | +---_object_without_properties_loose +| | +---_possible_constructor_return +| | +---_read_only_error +| | +---_set +| | +---_set_prototype_of +| | +---_skip_first_generator_next +| | +---_sliced_to_array +| | +---_sliced_to_array_loose +| | +---_super_prop_base +| | +---_tagged_template_literal +| | +---_tagged_template_literal_loose +| | +---_throw +| | +---_to_array +| | +---_to_consumable_array +| | +---_to_primitive +| | +---_to_property_key +| | +---_ts_decorate +| | +---_ts_generator +| | +---_ts_metadata +| | +---_ts_param +| | +---_ts_values +| | +---_type_of +| | +---_unsupported_iterable_to_array +| | +---_update +| | +---_using +| | +---_wrap_async_generator +| | +---_wrap_native_super +| | \---_write_only_error +| +---@tanstack +| | +---query-core +| | | +---build +| | | | +---legacy +| | | | \---modern +| | | \---src +| | \---react-query +| | +---build +| | | +---codemods +| | | | \---src +| | | | +---utils +| | | | | \---transformers +| | | | +---v4 +| | | | | \---utils +| | | | | \---replacers +| | | | \---v5 +| | | | +---is-loading +| | | | +---keep-previous-data +| | | | | \---utils +| | | | +---remove-overloads +| | | | | +---transformers +| | | | | \---utils +| | | | +---rename-hydrate +| | | | \---rename-properties +| | | +---legacy +| | | +---modern +| | | \---query-codemods +| | \---src +| +---@testing-library +| | +---jest-dom +| | | +---dist +| | | | +---chalk +| | | | | \---source +| | | | \---dom-accessibility-api +| | | | \---dist +| | | | \---polyfills +| | | \---types +| | | \---__tests__ +| | | +---bun +| | | +---jest +| | | +---jest-globals +| | | \---vitest +| | \---react +| | +---dist +| | | \---@testing-library +| | \---types +| +---@tootallnate +| | +---once +| | | \---dist +| | \---quickjs-emscripten +| | +---c +| | \---dist +| | \---generated +| +---@tybys +| +---@types +| | +---babel__core +| | +---babel__generator +| | +---babel__template +| | +---babel__traverse +| | +---d3-array +| | +---d3-color +| | +---d3-ease +| | +---d3-interpolate +| | +---d3-path +| | +---d3-scale +| | +---d3-shape +| | +---d3-time +| | +---d3-timer +| | +---geojson +| | +---graceful-fs +| | +---istanbul-lib-coverage +| | +---istanbul-lib-report +| | +---istanbul-reports +| | +---jest +| | | +---ansi-styles +| | | \---pretty-format +| | | \---build +| | | \---plugins +| | | \---lib +| | +---jsdom +| | +---json-schema +| | +---json5 +| | +---mapbox-gl +| | +---node +| | | +---assert +| | | +---dns +| | | +---fs +| | | +---readline +| | | +---stream +| | | \---timers +| | +---nodemailer +| | | \---lib +| | | +---addressparser +| | | +---base64 +| | | +---dkim +| | | +---fetch +| | | +---json-transport +| | | +---mail-composer +| | | +---mailer +| | | +---mime-funcs +| | | +---mime-node +| | | +---qp +| | | +---sendmail-transport +| | | +---ses-transport +| | | +---shared +| | | +---smtp-connection +| | | +---smtp-pool +| | | +---smtp-transport +| | | +---stream-transport +| | | +---well-known +| | | \---xoauth2 +| | +---normalize-package-data +| | +---parse-json +| | +---prop-types +| | +---react +| | | \---ts5.0 +| | +---react-dom +| | | \---test-utils +| | +---react-syntax-highlighter +| | +---react-transition-group +| | +---semver +| | | +---classes +| | | +---functions +| | | +---internals +| | | \---ranges +| | +---stack-utils +| | +---tough-cookie +| | +---yargs +| | +---yargs-parser +| | \---yauzl +| +---@typescript-eslint +| | +---eslint-plugin +| | | +---dist +| | | | +---configs +| | | | +---rules +| | | | | +---enum-utils +| | | | | +---naming-convention-utils +| | | | | \---prefer-optional-chain-utils +| | | | \---util +| | | \---docs +| | | \---rules +| | +---parser +| | | \---dist +| | +---scope-manager +| | | \---dist +| | | +---definition +| | | +---lib +| | | +---referencer +| | | +---scope +| | | \---variable +| | +---type-utils +| | | \---dist +| | +---types +| | | \---dist +| | | \---generated +| | +---typescript-estree +| | | \---dist +| | | +---create-program +| | | +---jsx +| | | +---parseSettings +| | | \---ts-estree +| | +---utils +| | | \---dist +| | | +---ast-utils +| | | | \---eslint-utils +| | | +---eslint-utils +| | | +---ts-eslint +| | | | \---eslint +| | | \---ts-utils +| | \---visitor-keys +| | +---dist +| | \---eslint-visitor-keys +| | +---dist +| | \---lib +| +---@ungap +| | \---structured-clone +| | +---.github +| | | \---workflows +| | +---cjs +| | \---esm +| +---@unrs +| | \---resolver-binding-win32-x64-msvc +| +---@vercel +| | \---style-guide +| | +---eslint +| | | +---rules +| | | | \---typescript +| | | \---utils +| | +---prettier +| | \---typescript +| +---@yr +| | \---monotone-cubic-spline +| | \---src +| +---abab +| | \---lib +| +---acorn +| | +---bin +| | \---dist +| +---acorn-globals +| +---acorn-jsx +| +---acorn-walk +| | \---dist +| +---adler-32 +| | \---types +| +---agent-base +| | +---dist +| | | \---src +| | \---src +| +---ajv +| | +---dist +| | +---lib +| | | +---compile +| | | +---dot +| | | +---dotjs +| | | \---refs +| | \---scripts +| +---ansi-escapes +| +---ansi-regex +| +---ansi-styles +| +---anymatch +| +---apexcharts +| | +---dist +| | | \---locales +| | +---src +| | | +---assets +| | | +---charts +| | | | \---common +| | | | +---bar +| | | | +---circle +| | | | +---line +| | | | \---treemap +| | | +---libs +| | | +---locales +| | | +---modules +| | | | +---annotations +| | | | +---axes +| | | | +---dimensions +| | | | +---helpers +| | | | +---legend +| | | | +---settings +| | | | \---tooltip +| | | +---svgjs +| | | \---utils +| | \---types +| +---argparse +| | \---lib +| +---aria-query +| | \---lib +| | +---etc +| | | \---roles +| | | +---abstract +| | | +---dpub +| | | +---graphics +| | | \---literal +| | \---util +| +---array-buffer-byte-length +| | +---.github +| | \---test +| +---array-includes +| | +---.github +| | \---test +| +---array-union +| +---array.prototype.findlast +| | +---.github +| | \---test +| +---array.prototype.findlastindex +| | +---.github +| | \---test +| +---array.prototype.flat +| | +---.github +| | \---test +| +---array.prototype.flatmap +| | +---.github +| | \---test +| +---array.prototype.tosorted +| | +---.github +| | \---test +| +---arraybuffer.prototype.slice +| | \---test +| +---ast-types +| | +---.github +| | | \---workflows +| | +---def +| | +---gen +| | \---lib +| +---ast-types-flow +| | \---lib +| +---async-function +| | +---.github +| | \---test +| +---asynckit +| | \---lib +| +---attr-accept +| | \---dist +| | \---es +| +---autoprefixer +| | +---bin +| | +---data +| | \---lib +| | \---hacks +| +---available-typed-arrays +| | +---.github +| | \---test +| +---axe-core +| | \---locales +| +---axobject-query +| | \---lib +| | +---etc +| | | \---objects +| | \---util +| +---b4a +| | \---lib +| +---babel-jest +| | \---build +| +---babel-plugin-istanbul +| | +---lib +| | +---.bin +| | +---istanbul-lib-instrument +| | | \---src +| | \---semver +| | \---bin +| +---babel-plugin-jest-hoist +| | \---build +| +---babel-plugin-macros +| | \---dist +| +---babel-preset-current-node-syntax +| | +---.github +| | | \---workflows +| | \---src +| +---babel-preset-jest +| +---balanced-match +| | \---.github +| +---bare-events +| | \---lib +| +---bare-fs +| | +---lib +| | \---prebuilds +| | +---android-arm +| | +---android-arm64 +| | +---android-ia32 +| | +---android-x64 +| | +---darwin-arm64 +| | +---darwin-x64 +| | +---ios-arm64 +| | +---ios-arm64-simulator +| | +---ios-x64-simulator +| | +---linux-arm64 +| | +---linux-x64 +| | +---win32-arm64 +| | \---win32-x64 +| +---bare-os +| | +---lib +| | \---prebuilds +| | +---android-arm +| | +---android-arm64 +| | +---android-ia32 +| | +---android-x64 +| | +---darwin-arm64 +| | +---darwin-x64 +| | +---ios-arm64 +| | +---ios-arm64-simulator +| | +---ios-x64-simulator +| | +---linux-arm64 +| | +---linux-x64 +| | +---win32-arm64 +| | \---win32-x64 +| +---bare-path +| | \---lib +| +---bare-stream +| +---basic-ftp +| | \---dist +| +---bcryptjs +| | +---bin +| | \---umd +| +---brace-expansion +| | \---.github +| +---braces +| | \---lib +| +---browserslist +| +---bser +| +---buffer-crc32 +| +---buffer-equal-constant-time +| +---buffer-from +| +---builtin-modules +| +---busboy +| | +---.github +| | | \---workflows +| | +---bench +| | +---lib +| | | \---types +| | \---test +| +---call-bind +| | +---.github +| | \---test +| +---call-bind-apply-helpers +| | +---.github +| | \---test +| +---call-bound +| | +---.github +| | \---test +| +---callsites +| +---camelcase +| +---caniuse-lite +| | +---data +| | | +---features +| | | \---regions +| | \---dist +| | +---lib +| | \---unpacker +| +---cfb +| | +---dist +| | \---types +| +---chalk +| | \---source +| +---char-regex +| +---chromium-bidi +| | +---lib +| | | +---cjs +| | | | +---bidiMapper +| | | | | \---modules +| | | | | +---bluetooth +| | | | | +---browser +| | | | | +---cdp +| | | | | +---context +| | | | | +---emulation +| | | | | +---input +| | | | | +---log +| | | | | +---network +| | | | | +---permissions +| | | | | +---script +| | | | | +---session +| | | | | +---storage +| | | | | \---webExtension +| | | | +---bidiTab +| | | | +---cdp +| | | | +---protocol +| | | | | \---generated +| | | | +---protocol-parser +| | | | | \---generated +| | | | \---utils +| | | +---esm +| | | | +---bidiMapper +| | | | | \---modules +| | | | | +---bluetooth +| | | | | +---browser +| | | | | +---cdp +| | | | | +---context +| | | | | +---emulation +| | | | | +---input +| | | | | +---log +| | | | | +---network +| | | | | +---permissions +| | | | | +---script +| | | | | +---session +| | | | | +---storage +| | | | | \---webExtension +| | | | +---bidiServer +| | | | +---bidiTab +| | | | +---cdp +| | | | +---protocol +| | | | | \---generated +| | | | +---protocol-parser +| | | | | \---generated +| | | | \---utils +| | | \---iife +| | \---zod +| | +---src +| | | +---v3 +| | | | +---benchmarks +| | | | +---helpers +| | | | +---locales +| | | | \---tests +| | | +---v4 +| | | | +---classic +| | | | | \---tests +| | | | +---core +| | | | | \---tests +| | | | | \---locales +| | | | +---locales +| | | | \---mini +| | | | \---tests +| | | \---v4-mini +| | +---v3 +| | | +---helpers +| | | \---locales +| | +---v4 +| | | +---classic +| | | +---core +| | | +---locales +| | | \---mini +| | \---v4-mini +| +---ci-info +| +---cjs-module-lexer +| | \---dist +| +---clean-regexp +| | +---lib +| | \---escape-string-regexp +| +---client-only +| +---cliui +| | +---build +| | | \---lib +| | +---emoji-regex +| | | \---es2015 +| | +---string-width +| | \---wrap-ansi +| +---clsx +| | \---dist +| +---co +| +---codepage +| | +---bits +| | +---dist +| | \---types +| +---collect-v8-coverage +| +---color-convert +| +---color-name +| +---combined-stream +| | \---lib +| +---concat-map +| | +---example +| | \---test +| +---convert-source-map +| +---core-js-compat +| +---core-util-is +| | \---lib +| +---cosmiconfig +| | \---dist +| +---crc-32 +| | +---bin +| | \---types +| +---create-jest +| | +---bin +| | \---build +| +---cross-spawn +| | \---lib +| | \---util +| +---css.escape +| +---cssom +| | \---lib +| +---cssstyle +| | +---lib +| | | +---properties +| | | \---utils +| | \---cssom +| | \---lib +| +---csstype +| +---d3-array +| | +---dist +| | \---src +| | \---threshold +| +---d3-color +| | +---dist +| | \---src +| +---d3-ease +| | +---dist +| | \---src +| +---d3-format +| | +---dist +| | +---locale +| | \---src +| +---d3-interpolate +| | +---dist +| | \---src +| | \---transform +| +---d3-path +| | +---dist +| | \---src +| +---d3-scale +| | +---dist +| | \---src +| +---d3-shape +| | +---dist +| | \---src +| | +---curve +| | +---offset +| | +---order +| | \---symbol +| +---d3-time +| | +---dist +| | \---src +| +---d3-time-format +| | +---dist +| | +---locale +| | \---src +| +---d3-timer +| | +---dist +| | \---src +| +---damerau-levenshtein +| | +---scripts +| | \---test +| +---data-uri-to-buffer +| | \---dist +| +---data-urls +| | \---lib +| +---data-view-buffer +| | +---.github +| | \---test +| +---data-view-byte-length +| | +---.github +| | \---test +| +---data-view-byte-offset +| | +---.github +| | \---test +| +---dayjs +| | +---esm +| | | +---locale +| | | \---plugin +| | | +---advancedFormat +| | | +---arraySupport +| | | +---badMutable +| | | +---bigIntSupport +| | | +---buddhistEra +| | | +---calendar +| | | +---customParseFormat +| | | +---dayOfYear +| | | +---devHelper +| | | +---duration +| | | +---isBetween +| | | +---isLeapYear +| | | +---isMoment +| | | +---isoWeek +| | | +---isoWeeksInYear +| | | +---isSameOrAfter +| | | +---isSameOrBefore +| | | +---isToday +| | | +---isTomorrow +| | | +---isYesterday +| | | +---localeData +| | | +---localizedFormat +| | | +---minMax +| | | +---objectSupport +| | | +---pluralGetSet +| | | +---preParsePostFormat +| | | +---quarterOfYear +| | | +---relativeTime +| | | +---timezone +| | | +---toArray +| | | +---toObject +| | | +---updateLocale +| | | +---utc +| | | +---weekday +| | | +---weekOfYear +| | | \---weekYear +| | +---locale +| | \---plugin +| +---debug +| | \---src +| +---decimal.js +| +---decimal.js-light +| | \---doc +| +---dedent +| | \---dist +| +---deep-is +| | +---example +| | \---test +| +---deepmerge +| | \---dist +| +---define-data-property +| | +---.github +| | \---test +| +---define-properties +| | \---.github +| +---degenerator +| | \---dist +| +---delayed-stream +| | \---lib +| +---dequal +| | +---dist +| | \---lite +| +---detect-indent +| +---detect-newline +| +---devtools-protocol +| | +---json +| | +---pdl +| | \---types +| +---diff-sequences +| | \---build +| +---dir-glob +| +---doctrine +| | \---lib +| +---dom-helpers +| | +---activeElement +| | +---addClass +| | +---addEventListener +| | +---animate +| | +---animationFrame +| | +---attribute +| | +---camelize +| | +---camelizeStyle +| | +---canUseDOM +| | +---childElements +| | +---childNodes +| | +---cjs +| | +---clear +| | +---closest +| | +---collectElements +| | +---collectSiblings +| | +---contains +| | +---css +| | +---esm +| | +---filterEventHandler +| | +---getComputedStyle +| | +---getScrollAccessor +| | +---hasClass +| | +---height +| | +---hyphenate +| | +---hyphenateStyle +| | +---insertAfter +| | +---isDocument +| | +---isInput +| | +---isTransform +| | +---isVisible +| | +---isWindow +| | +---listen +| | +---matches +| | +---nextUntil +| | +---offset +| | +---offsetParent +| | +---ownerDocument +| | +---ownerWindow +| | +---parents +| | +---position +| | +---prepend +| | +---querySelectorAll +| | +---remove +| | +---removeClass +| | +---removeEventListener +| | +---scrollbarSize +| | +---scrollLeft +| | +---scrollParent +| | +---scrollTo +| | +---scrollTop +| | +---siblings +| | +---text +| | +---toggleClass +| | +---transitionEnd +| | +---triggerEvent +| | \---width +| +---domexception +| | \---lib +| +---dotenv +| | \---lib +| +---dunder-proto +| | +---.github +| | \---test +| +---duplexer2 +| | +---isarray +| | +---readable-stream +| | | +---doc +| | | | \---wg-meetings +| | | \---lib +| | | \---internal +| | | \---streams +| | \---string_decoder +| | \---lib +| +---eastasianwidth +| +---ecdsa-sig-formatter +| | \---src +| +---electron-to-chromium +| +---emittery +| +---emoji-regex +| | \---es2015 +| +---end-of-stream +| +---entities +| | +---dist +| | | +---commonjs +| | | | \---generated +| | | \---esm +| | | \---generated +| | \---src +| | \---generated +| +---env-paths +| +---error-ex +| +---es-abstract +| | +---2015 +| | | \---tables +| | +---2016 +| | | \---tables +| | +---2017 +| | | \---tables +| | +---2018 +| | | \---tables +| | +---2019 +| | | \---tables +| | +---2020 +| | | +---BigInt +| | | +---Number +| | | \---tables +| | +---2021 +| | | +---BigInt +| | | +---Number +| | | \---tables +| | +---2022 +| | | +---BigInt +| | | +---Number +| | | \---tables +| | +---2023 +| | | +---BigInt +| | | +---Number +| | | \---tables +| | +---2024 +| | | +---BigInt +| | | +---Number +| | | \---tables +| | +---2025 +| | | +---BigInt +| | | +---Number +| | | \---tables +| | +---5 +| | +---helpers +| | | \---records +| | \---operations +| +---es-define-property +| | +---.github +| | \---test +| +---es-errors +| | +---.github +| | \---test +| +---es-iterator-helpers +| | +---.github +| | +---aos +| | +---Iterator +| | +---Iterator.concat +| | +---Iterator.from +| | +---Iterator.prototype +| | +---Iterator.prototype.constructor +| | +---Iterator.prototype.drop +| | +---Iterator.prototype.every +| | +---Iterator.prototype.filter +| | +---Iterator.prototype.find +| | +---Iterator.prototype.flatMap +| | +---Iterator.prototype.forEach +| | +---Iterator.prototype.map +| | +---Iterator.prototype.reduce +| | +---Iterator.prototype.some +| | +---Iterator.prototype.take +| | +---Iterator.prototype.toArray +| | +---Iterator.zip +| | +---Iterator.zipKeyed +| | +---IteratorHelperPrototype +| | +---test +| | | \---helpers +| | \---WrapForValidIteratorPrototype +| +---es-object-atoms +| | +---.github +| | \---test +| +---es-set-tostringtag +| | \---test +| +---es-shim-unscopables +| | +---.github +| | \---test +| +---es-to-primitive +| | +---.github +| | +---helpers +| | \---test +| +---escalade +| | +---dist +| | \---sync +| +---escape-string-regexp +| +---escodegen +| | +---bin +| | \---source-map +| | +---dist +| | \---lib +| +---eslint +| | +---bin +| | +---conf +| | +---lib +| | | +---cli-engine +| | | | \---formatters +| | | +---config +| | | +---eslint +| | | +---linter +| | | | \---code-path-analysis +| | | +---rule-tester +| | | +---rules +| | | | \---utils +| | | | +---patterns +| | | | \---unicode +| | | +---shared +| | | \---source-code +| | | \---token-store +| | +---messages +| | +---brace-expansion +| | +---eslint-scope +| | | +---dist +| | | \---lib +| | +---eslint-visitor-keys +| | | +---dist +| | | \---lib +| | +---globals +| | +---minimatch +| | \---type-fest +| | +---source +| | \---ts41 +| +---eslint-config-next +| | +---@typescript-eslint +| | | +---parser +| | | | \---dist +| | | +---scope-manager +| | | | \---dist +| | | | +---definition +| | | | +---lib +| | | | +---referencer +| | | | +---scope +| | | | \---variable +| | | +---types +| | | | \---dist +| | | | \---generated +| | | +---typescript-estree +| | | | \---dist +| | | | +---create-program +| | | | +---jsx +| | | | +---parseSettings +| | | | \---ts-estree +| | | \---visitor-keys +| | | \---dist +| | +---eslint-visitor-keys +| | | +---dist +| | | \---lib +| | \---minimatch +| | \---dist +| | +---cjs +| | \---mjs +| +---eslint-config-prettier +| | \---bin +| +---eslint-import-resolver-alias +| +---eslint-import-resolver-node +| | \---debug +| | \---src +| +---eslint-import-resolver-typescript +| | \---lib +| +---eslint-module-utils +| | \---debug +| | \---src +| +---eslint-plugin-eslint-comments +| | +---lib +| | | +---configs +| | | +---internal +| | | +---rules +| | | \---utils +| | \---escape-string-regexp +| +---eslint-plugin-import +| | +---config +| | | \---flat +| | +---docs +| | | \---rules +| | +---lib +| | | +---core +| | | +---exportMap +| | | \---rules +| | +---memo-parser +| | +---.bin +| | +---brace-expansion +| | +---debug +| | | \---src +| | +---doctrine +| | | \---lib +| | +---minimatch +| | \---semver +| | \---bin +| +---eslint-plugin-jest +| | +---docs +| | | \---rules +| | +---lib +| | | +---processors +| | | \---rules +| | | \---utils +| | +---@typescript-eslint +| | | +---scope-manager +| | | | \---dist +| | | | +---definition +| | | | +---lib +| | | | +---referencer +| | | | +---scope +| | | | \---variable +| | | +---types +| | | | +---dist +| | | | | \---generated +| | | | \---_ts3.4 +| | | | \---dist +| | | | \---generated +| | | +---typescript-estree +| | | | +---dist +| | | | | +---create-program +| | | | | +---jsx +| | | | | +---parseSettings +| | | | | \---ts-estree +| | | | \---_ts3.4 +| | | | \---dist +| | | | +---create-program +| | | | +---jsx +| | | | +---parseSettings +| | | | \---ts-estree +| | | +---utils +| | | | +---dist +| | | | | +---ast-utils +| | | | | | \---eslint-utils +| | | | | +---eslint-utils +| | | | | | \---rule-tester +| | | | | +---ts-eslint +| | | | | \---ts-eslint-scope +| | | | \---_ts3.4 +| | | | \---dist +| | | | +---ast-utils +| | | | | \---eslint-utils +| | | | +---eslint-utils +| | | | | \---rule-tester +| | | | +---ts-eslint +| | | | \---ts-eslint-scope +| | | \---visitor-keys +| | | +---dist +| | | \---_ts3.4 +| | | \---dist +| | \---eslint-visitor-keys +| | +---dist +| | \---lib +| +---eslint-plugin-jsx-a11y +| | +---docs +| | | \---rules +| | +---lib +| | | +---configs +| | | +---rules +| | | \---util +| | | \---implicitRoles +| | | +---aria-query +| | | | \---lib +| | | | +---etc +| | | | | \---roles +| | | | | +---abstract +| | | | | +---dpub +| | | | | +---graphics +| | | | | \---literal +| | | | \---util +| | | +---brace-expansion +| | | \---minimatch +| | +---__mocks__ +| | \---__tests__ +| | +---src +| | | +---rules +| | | \---util +| | | \---implicitRoles +| | \---__util__ +| | \---helpers +| +---eslint-plugin-playwright +| | +---dist +| | +---globals +| | \---type-fest +| | +---source +| | \---ts41 +| +---eslint-plugin-react +| | +---configs +| | +---lib +| | | +---rules +| | | \---util +| | +---.bin +| | +---brace-expansion +| | +---doctrine +| | | \---lib +| | +---minimatch +| | +---resolve +| | | +---.github +| | | +---bin +| | | +---example +| | | +---lib +| | | \---test +| | | +---dotdot +| | | | \---abc +| | | +---module_dir +| | | | +---xmodules +| | | | | \---aaa +| | | | +---ymodules +| | | | | \---aaa +| | | | \---zmodules +| | | | \---bbb +| | | +---node_path +| | | | +---x +| | | | | +---aaa +| | | | | \---ccc +| | | | \---y +| | | | +---bbb +| | | | \---ccc +| | | +---pathfilter +| | | | \---deep_ref +| | | +---precedence +| | | | +---aaa +| | | | \---bbb +| | | +---resolver +| | | | +---baz +| | | | +---browser_field +| | | | +---dot_main +| | | | +---dot_slash_main +| | | | +---empty_main +| | | | +---false_main +| | | | +---incorrect_main +| | | | +---invalid_main +| | | | +---missing_index +| | | | +---missing_main +| | | | +---multirepo +| | | | | \---packages +| | | | | +---package-a +| | | | | \---package-b +| | | | +---nested_symlinks +| | | | | \---mylib +| | | | +---null_main +| | | | +---other_path +| | | | | \---lib +| | | | +---quux +| | | | | \---foo +| | | | +---same_names +| | | | | \---foo +| | | | +---symlinked +| | | | | +---package +| | | | | \---_ +| | | | | \---symlink_target +| | | | \---without_basedir +| | | \---shadowed_core +| | | \---util +| | \---semver +| | \---bin +| +---eslint-plugin-react-hooks +| | \---cjs +| +---eslint-plugin-testing-library +| | +---dist +| | | +---configs +| | | +---create-testing-library-rule +| | | +---node-utils +| | | +---rules +| | | \---utils +| | +---@typescript-eslint +| | | +---scope-manager +| | | | \---dist +| | | | +---definition +| | | | +---lib +| | | | +---referencer +| | | | +---scope +| | | | \---variable +| | | +---types +| | | | +---dist +| | | | | \---generated +| | | | \---_ts3.4 +| | | | \---dist +| | | | \---generated +| | | +---typescript-estree +| | | | +---dist +| | | | | +---create-program +| | | | | +---jsx +| | | | | +---parseSettings +| | | | | \---ts-estree +| | | | \---_ts3.4 +| | | | \---dist +| | | | +---create-program +| | | | +---jsx +| | | | +---parseSettings +| | | | \---ts-estree +| | | +---utils +| | | | +---dist +| | | | | +---ast-utils +| | | | | | \---eslint-utils +| | | | | +---eslint-utils +| | | | | | \---rule-tester +| | | | | +---ts-eslint +| | | | | \---ts-eslint-scope +| | | | \---_ts3.4 +| | | | \---dist +| | | | +---ast-utils +| | | | | \---eslint-utils +| | | | +---eslint-utils +| | | | | \---rule-tester +| | | | +---ts-eslint +| | | | \---ts-eslint-scope +| | | \---visitor-keys +| | | +---dist +| | | \---_ts3.4 +| | | \---dist +| | \---eslint-visitor-keys +| | +---dist +| | \---lib +| +---eslint-plugin-tsdoc +| | \---lib +| | \---tests +| +---eslint-plugin-unicorn +| | +---configs +| | \---rules +| | +---ast +| | +---fix +| | +---shared +| | \---utils +| +---eslint-plugin-vitest +| | \---dist +| +---eslint-scope +| | +---lib +| | \---estraverse +| +---eslint-visitor-keys +| | \---lib +| +---espree +| | +---dist +| | +---lib +| | \---eslint-visitor-keys +| | +---dist +| | \---lib +| +---esprima +| | +---bin +| | \---dist +| +---esquery +| | \---dist +| +---esrecurse +| +---estraverse +| +---esutils +| | \---lib +| +---eventemitter3 +| | \---umd +| +---execa +| | +---lib +| | \---signal-exit +| +---exit +| | +---lib +| | \---test +| | \---fixtures +| +---expect +| | \---build +| +---extract-zip +| | \---get-stream +| +---fast-deep-equal +| | \---es6 +| +---fast-equals +| | +---config +| | | +---rollup +| | | \---tsconfig +| | +---dist +| | | +---cjs +| | | | \---types +| | | +---esm +| | | | \---types +| | | +---min +| | | | \---types +| | | \---umd +| | | \---types +| | +---recipes +| | +---scripts +| | \---src +| +---fast-fifo +| +---fast-glob +| | | \---glob-parent +| | \---out +| | +---managers +| | +---providers +| | | +---filters +| | | +---matchers +| | | \---transformers +| | +---readers +| | +---types +| | \---utils +| +---fast-json-stable-stringify +| | +---.github +| | +---benchmark +| | +---example +| | \---test +| +---fast-levenshtein +| +---fastq +| | +---.github +| | | \---workflows +| | \---test +| +---fb-watchman +| +---fd-slicer +| | \---test +| +---file-entry-cache +| +---file-saver +| | +---dist +| | \---src +| +---file-selector +| | +---dist +| | | +---bundles +| | | \---es2015 +| | \---src +| +---fill-range +| +---find-root +| | \---test +| +---find-up +| +---flat-cache +| | \---src +| +---flatted +| | +---cjs +| | +---esm +| | +---php +| | +---python +| | \---types +| +---for-each +| | +---.github +| | \---test +| +---foreground-child +| | \---dist +| | +---commonjs +| | \---esm +| +---form-data +| | \---lib +| +---frac +| | +---dist +| | \---types +| +---fraction.js +| +---fs.realpath +| +---function-bind +| | +---.github +| | \---test +| +---function.prototype.name +| | +---.github +| | +---helpers +| | \---test +| +---functions-have-names +| | +---.github +| | \---test +| +---gensync +| | \---test +| +---get-caller-file +| +---get-intrinsic +| | +---.github +| | \---test +| +---get-package-type +| +---get-proto +| | +---.github +| | \---test +| +---get-stream +| +---get-symbol-description +| | +---.github +| | \---test +| +---get-tsconfig +| | \---dist +| +---get-uri +| | \---dist +| +---git-hooks-list +| +---glob +| | \---dist +| | +---commonjs +| | \---esm +| +---glob-parent +| +---globals +| +---globalthis +| | \---test +| +---globby +| +---gopd +| | +---.github +| | \---test +| +---graceful-fs +| +---graphemer +| | \---lib +| +---has-bigints +| | +---.github +| | \---test +| +---has-flag +| +---has-property-descriptors +| | +---.github +| | \---test +| +---has-proto +| | +---.github +| | \---test +| +---has-symbols +| | +---.github +| | \---test +| | \---shams +| +---has-tostringtag +| | +---.github +| | \---test +| | \---shams +| +---hasown +| | \---.github +| +---hoist-non-react-statics +| | +---dist +| | | \---react-is +| | | +---cjs +| | | \---umd +| | \---src +| +---hosted-git-info +| +---html-encoding-sniffer +| | \---lib +| +---html-escaper +| | +---cjs +| | +---esm +| | \---test +| +---html-tokenize +| | +---bench +| | +---bin +| | +---example +| | \---test +| | +---attributes +| | +---cdata +| | +---comment +| | +---comment_comment +| | +---loose_angle_brackets +| | +---quote +| | +---script +| | +---style +| | +---table +| | +---tag_comment +| | \---title +| +---http-proxy-agent +| | \---dist +| +---https-proxy-agent +| | \---dist +| +---human-signals +| | \---build +| | \---src +| +---iconv-lite +| | +---.github +| | +---.idea +| | | +---codeStyles +| | | \---inspectionProfiles +| | +---encodings +| | | \---tables +| | \---lib +| +---ignore +| +---import-fresh +| +---import-local +| | \---fixtures +| +---imurmurhash +| +---indent-string +| +---inflight +| +---inherits +| +---internal-slot +| | +---.github +| | \---test +| +---internmap +| | +---dist +| | \---src +| +---ip-address +| | +---dist +| | | +---v4 +| | | \---v6 +| | | \---sprintf-js +| | | +---dist +| | | \---src +| | \---src +| | +---v4 +| | \---v6 +| +---is-array-buffer +| | +---.github +| | \---test +| +---is-arrayish +| +---is-async-function +| | \---test +| +---is-bigint +| | +---.github +| | \---test +| +---is-boolean-object +| | +---.github +| | \---test +| +---is-builtin-module +| +---is-bun-module +| | \---dist +| +---is-callable +| | +---.github +| | \---test +| +---is-core-module +| | \---test +| +---is-data-view +| | +---.github +| | \---test +| +---is-date-object +| | +---.github +| | \---test +| +---is-extglob +| +---is-finalizationregistry +| | +---.github +| | \---test +| +---is-fullwidth-code-point +| +---is-generator-fn +| +---is-generator-function +| | \---test +| +---is-glob +| +---is-map +| | +---.github +| | \---test +| +---is-negative-zero +| | +---.github +| | \---test +| +---is-number +| +---is-number-object +| | +---.github +| | \---test +| +---is-path-inside +| +---is-plain-obj +| +---is-potential-custom-element-name +| +---is-regex +| | \---test +| +---is-set +| | +---.github +| | \---test +| +---is-shared-array-buffer +| | +---.github +| | \---test +| +---is-stream +| +---is-string +| | +---.github +| | \---test +| +---is-symbol +| | +---.github +| | \---test +| +---is-typed-array +| | +---.github +| | \---test +| +---is-weakmap +| | +---.github +| | \---test +| +---is-weakref +| | +---.github +| | \---test +| +---is-weakset +| | +---.github +| | \---test +| +---isarray +| | \---build +| +---isexe +| | \---test +| +---istanbul-lib-coverage +| | \---lib +| +---istanbul-lib-instrument +| | \---src +| +---istanbul-lib-report +| | \---lib +| +---istanbul-lib-source-maps +| | +---lib +| | \---source-map +| | +---dist +| | \---lib +| +---istanbul-reports +| | \---lib +| | +---clover +| | +---cobertura +| | +---html +| | | \---assets +| | | \---vendor +| | +---html-spa +| | | +---assets +| | | \---src +| | +---json +| | +---json-summary +| | +---lcov +| | +---lcovonly +| | +---none +| | +---teamcity +| | +---text +| | +---text-lcov +| | \---text-summary +| +---iterator.prototype +| | +---.github +| | \---test +| +---jackspeak +| | \---dist +| | +---commonjs +| | \---esm +| +---jest +| | +---bin +| | \---build +| +---jest-changed-files +| | \---build +| +---jest-circus +| | +---build +| | | \---legacy-code-todo-rewrite +| | +---ansi-styles +| | \---pretty-format +| | \---build +| | \---plugins +| | \---lib +| +---jest-cli +| | +---bin +| | \---build +| +---jest-config +| | +---build +| | +---ansi-styles +| | +---brace-expansion +| | +---ci-info +| | +---glob +| | +---minimatch +| | \---pretty-format +| | \---build +| | \---plugins +| | \---lib +| +---jest-diff +| | +---build +| | +---ansi-styles +| | \---pretty-format +| | \---build +| | \---plugins +| | \---lib +| +---jest-docblock +| | \---build +| +---jest-each +| | +---build +| | | \---table +| | +---ansi-styles +| | \---pretty-format +| | \---build +| | \---plugins +| | \---lib +| +---jest-environment-jsdom +| | \---build +| +---jest-environment-node +| | \---build +| +---jest-get-type +| | \---build +| +---jest-haste-map +| | \---build +| | +---crawlers +| | +---lib +| | \---watchers +| +---jest-leak-detector +| | +---build +| | +---ansi-styles +| | \---pretty-format +| | \---build +| | \---plugins +| | \---lib +| +---jest-matcher-utils +| | +---build +| | +---ansi-styles +| | \---pretty-format +| | \---build +| | \---plugins +| | \---lib +| +---jest-message-util +| | +---build +| | +---ansi-styles +| | \---pretty-format +| | \---build +| | \---plugins +| | \---lib +| +---jest-mock +| | \---build +| +---jest-pnp-resolver +| +---jest-regex-util +| | \---build +| +---jest-resolve +| | \---build +| +---jest-resolve-dependencies +| | \---build +| +---jest-runner +| | \---build +| +---jest-runtime +| | +---build +| | +---brace-expansion +| | +---glob +| | \---minimatch +| +---jest-snapshot +| | +---build +| | +---ansi-styles +| | \---pretty-format +| | \---build +| | \---plugins +| | \---lib +| +---jest-util +| | +---build +| | \---ci-info +| +---jest-validate +| | +---build +| | +---ansi-styles +| | +---camelcase +| | \---pretty-format +| | \---build +| | \---plugins +| | \---lib +| +---jest-watcher +| | \---build +| | \---lib +| +---jest-worker +| | +---build +| | | +---base +| | | \---workers +| | \---supports-color +| +---jiti +| | +---dist +| | \---lib +| +---jju +| | \---lib +| +---js-tokens +| +---js-yaml +| | +---bin +| | +---dist +| | \---lib +| | +---schema +| | \---type +| +---jsbn +| | \---test +| +---jsdom +| | \---lib +| | \---jsdom +| | +---browser +| | | +---parser +| | | \---resources +| | +---level2 +| | +---level3 +| | \---living +| | +---aborting +| | +---attributes +| | +---constraint-validation +| | +---crypto +| | +---cssom +| | +---custom-elements +| | +---domparsing +| | +---events +| | +---fetch +| | +---file-api +| | +---generated +| | +---helpers +| | | \---svg +| | +---hr-time +| | +---mutation-observer +| | +---navigator +| | +---nodes +| | +---range +| | +---selection +| | +---svg +| | +---traversal +| | +---websockets +| | +---webstorage +| | +---window +| | \---xhr +| +---jsesc +| | +---bin +| | \---man +| +---json-buffer +| | \---test +| +---json-parse-even-better-errors +| +---json-schema-traverse +| | \---spec +| | \---fixtures +| +---json-stable-stringify-without-jsonify +| | +---example +| | \---test +| +---json5 +| | +---dist +| | \---lib +| +---jsonwebtoken +| | \---lib +| +---jsx-ast-utils +| | +---.github +| | +---lib +| | | \---values +| | | \---expressions +| | +---src +| | | \---values +| | | \---expressions +| | \---__tests__ +| | \---src +| +---jwa +| +---jws +| | \---lib +| +---keyv +| | \---src +| +---kleur +| +---language-subtag-registry +| | \---data +| | \---json +| +---language-tags +| | \---lib +| +---leven +| +---levn +| | \---lib +| +---lines-and-columns +| | \---build +| +---locate-path +| +---lodash +| | \---fp +| +---lodash.includes +| +---lodash.isboolean +| +---lodash.isinteger +| +---lodash.isnumber +| +---lodash.isplainobject +| +---lodash.isstring +| +---lodash.merge +| +---lodash.once +| +---loose-envify +| +---lru-cache +| +---lucide-react +| | \---dist +| | +---cjs +| | +---esm +| | | +---icons +| | | \---shared +| | | \---src +| | \---umd +| +---make-dir +| +---makeerror +| | \---lib +| +---math-intrinsics +| | +---.github +| | +---constants +| | \---test +| +---merge-stream +| +---merge2 +| +---micromatch +| +---mime-db +| +---mime-types +| +---mimic-fn +| +---min-indent +| +---minimatch +| | \---dist +| | +---commonjs +| | \---esm +| +---minimist +| | +---.github +| | +---example +| | \---test +| +---minipass +| | \---dist +| | +---commonjs +| | \---esm +| +---mitt +| | \---dist +| +---ms +| +---multipipe +| | \---test +| +---nanoid +| | +---async +| | +---bin +| | +---non-secure +| | \---url-alphabet +| +---napi-postinstall +| | \---lib +| +---natural-compare +| +---netmask +| | +---example +| | +---lib +| | +---test +| | \---tests +| +---next +| | +---compat +| | +---dist +| | | +---api +| | | +---bin +| | | +---build +| | | | +---analysis +| | | | +---babel +| | | | | +---loader +| | | | | \---plugins +| | | | +---jest +| | | | | \---__mocks__ +| | | | +---manifests +| | | | | \---formatter +| | | | +---output +| | | | +---polyfills +| | | | | +---fetch +| | | | | \---object.assign +| | | | +---swc +| | | | +---templates +| | | | +---turborepo-access-trace +| | | | +---webpack +| | | | | +---alias +| | | | | +---config +| | | | | | \---blocks +| | | | | | +---css +| | | | | | | \---loaders +| | | | | | \---images +| | | | | +---loaders +| | | | | | +---css-loader +| | | | | | | \---src +| | | | | | | +---plugins +| | | | | | | \---runtime +| | | | | | +---lightningcss-loader +| | | | | | | \---src +| | | | | | +---metadata +| | | | | | +---next-edge-app-route-loader +| | | | | | +---next-edge-ssr-loader +| | | | | | +---next-flight-loader +| | | | | | +---next-font-loader +| | | | | | +---next-image-loader +| | | | | | +---next-route-loader +| | | | | | +---next-style-loader +| | | | | | | \---runtime +| | | | | | +---postcss-loader +| | | | | | | \---src +| | | | | | \---resolve-url-loader +| | | | | | \---lib +| | | | | \---plugins +| | | | | +---next-types-plugin +| | | | | +---terser-webpack-plugin +| | | | | | \---src +| | | | | \---wellknown-errors-plugin +| | | | +---webpack-build +| | | | \---webpack-config-rules +| | | +---cli +| | | +---client +| | | | +---compat +| | | | +---components +| | | | | +---react-dev-overlay +| | | | | | +---app +| | | | | | +---internal +| | | | | | | +---components +| | | | | | | | +---CodeFrame +| | | | | | | | +---Dialog +| | | | | | | | +---hot-linked-text +| | | | | | | | +---LeftRightDialogHeader +| | | | | | | | +---Overlay +| | | | | | | | +---Terminal +| | | | | | | | +---Toast +| | | | | | | | \---VersionStalenessInfo +| | | | | | | +---container +| | | | | | | | \---RuntimeError +| | | | | | | +---helpers +| | | | | | | +---hooks +| | | | | | | +---icons +| | | | | | | \---styles +| | | | | | +---pages +| | | | | | \---server +| | | | | \---router-reducer +| | | | | \---reducers +| | | | +---dev +| | | | | \---error-overlay +| | | | +---legacy +| | | | +---portal +| | | | \---tracing +| | | +---compiled +| | | | +---@ampproject +| | | | | \---toolbox-optimizer +| | | | +---@babel +| | | | | \---runtime +| | | | | +---helpers +| | | | | | \---esm +| | | | | \---regenerator +| | | | +---@edge-runtime +| | | | | +---cookies +| | | | | +---ponyfill +| | | | | \---primitives +| | | | +---@hapi +| | | | | \---accept +| | | | +---@mswjs +| | | | | \---interceptors +| | | | | \---ClientRequest +| | | | +---@napi-rs +| | | | | \---triples +| | | | +---@next +| | | | | +---font +| | | | | | +---dist +| | | | | | | +---fontkit +| | | | | | | +---google +| | | | | | | \---local +| | | | | | +---google +| | | | | | \---local +| | | | | \---react-refresh-utils +| | | | | \---dist +| | | | | \---internal +| | | | +---@opentelemetry +| | | | | \---api +| | | | +---@vercel +| | | | | +---nft +| | | | | \---og +| | | | | +---emoji +| | | | | +---figma +| | | | | +---language +| | | | | \---satori +| | | | +---acorn +| | | | +---amphtml-validator +| | | | +---anser +| | | | +---arg +| | | | +---assert +| | | | +---async-retry +| | | | +---async-sema +| | | | +---babel +| | | | +---babel-packages +| | | | +---browserify-zlib +| | | | +---browserslist +| | | | +---buffer +| | | | +---bytes +| | | | +---ci-info +| | | | +---cli-select +| | | | +---client-only +| | | | +---commander +| | | | +---comment-json +| | | | +---compression +| | | | +---conf +| | | | +---constants-browserify +| | | | +---content-disposition +| | | | +---content-type +| | | | +---cookie +| | | | +---cross-spawn +| | | | +---crypto-browserify +| | | | +---css.escape +| | | | +---cssnano-simple +| | | | +---data-uri-to-buffer +| | | | +---debug +| | | | +---devalue +| | | | +---domain-browser +| | | | +---edge-runtime +| | | | +---events +| | | | +---find-cache-dir +| | | | +---find-up +| | | | +---fresh +| | | | +---get-orientation +| | | | +---glob +| | | | +---gzip-size +| | | | +---http-proxy +| | | | +---http-proxy-agent +| | | | +---https-browserify +| | | | +---https-proxy-agent +| | | | +---icss-utils +| | | | +---ignore-loader +| | | | +---image-size +| | | | +---is-animated +| | | | +---is-docker +| | | | +---is-wsl +| | | | +---jest-worker +| | | | +---json5 +| | | | +---jsonwebtoken +| | | | +---loader-runner +| | | | +---loader-utils2 +| | | | +---loader-utils3 +| | | | +---lodash.curry +| | | | +---lru-cache +| | | | +---mini-css-extract-plugin +| | | | | \---hmr +| | | | +---nanoid +| | | | +---native-url +| | | | +---neo-async +| | | | +---next-server +| | | | +---node-fetch +| | | | +---node-html-parser +| | | | +---ora +| | | | +---os-browserify +| | | | +---p-limit +| | | | +---path-browserify +| | | | +---path-to-regexp +| | | | +---picomatch +| | | | +---platform +| | | | +---postcss-flexbugs-fixes +| | | | +---postcss-modules-extract-imports +| | | | +---postcss-modules-local-by-default +| | | | +---postcss-modules-scope +| | | | +---postcss-modules-values +| | | | +---postcss-plugin-stub-for-cssnano-simple +| | | | +---postcss-preset-env +| | | | +---postcss-safe-parser +| | | | +---postcss-scss +| | | | +---postcss-value-parser +| | | | +---process +| | | | +---punycode +| | | | +---querystring-es3 +| | | | +---raw-body +| | | | +---react +| | | | | \---cjs +| | | | +---react-dom +| | | | | \---cjs +| | | | +---react-dom-experimental +| | | | | \---cjs +| | | | +---react-experimental +| | | | | \---cjs +| | | | +---react-is +| | | | | +---cjs +| | | | | \---umd +| | | | +---react-refresh +| | | | | \---cjs +| | | | +---react-server-dom-turbopack +| | | | | \---cjs +| | | | +---react-server-dom-turbopack-experimental +| | | | | \---cjs +| | | | +---react-server-dom-webpack +| | | | | \---cjs +| | | | +---react-server-dom-webpack-experimental +| | | | | \---cjs +| | | | +---regenerator-runtime +| | | | +---sass-loader +| | | | +---scheduler +| | | | | \---cjs +| | | | +---scheduler-experimental +| | | | | \---cjs +| | | | +---schema-utils2 +| | | | +---schema-utils3 +| | | | +---semver +| | | | +---send +| | | | +---server-only +| | | | +---setimmediate +| | | | +---shell-quote +| | | | +---source-map +| | | | +---source-map08 +| | | | +---stacktrace-parser +| | | | +---stream-browserify +| | | | +---stream-http +| | | | +---string-hash +| | | | +---string_decoder +| | | | +---strip-ansi +| | | | +---superstruct +| | | | +---tar +| | | | +---terser +| | | | +---text-table +| | | | +---timers-browserify +| | | | +---tty-browserify +| | | | +---ua-parser-js +| | | | +---unistore +| | | | +---util +| | | | +---vm-browserify +| | | | +---watchpack +| | | | +---web-vitals +| | | | +---web-vitals-attribution +| | | | +---webpack +| | | | +---webpack-sources1 +| | | | +---webpack-sources3 +| | | | +---ws +| | | | \---zod +| | | +---esm +| | | | +---api +| | | | +---build +| | | | | +---analysis +| | | | | +---babel +| | | | | | +---loader +| | | | | | \---plugins +| | | | | +---manifests +| | | | | | \---formatter +| | | | | +---output +| | | | | +---polyfills +| | | | | | +---fetch +| | | | | | \---object.assign +| | | | | +---swc +| | | | | +---templates +| | | | | +---turborepo-access-trace +| | | | | +---webpack +| | | | | | +---alias +| | | | | | +---config +| | | | | | | \---blocks +| | | | | | | +---css +| | | | | | | | \---loaders +| | | | | | | \---images +| | | | | | +---loaders +| | | | | | | +---css-loader +| | | | | | | | \---src +| | | | | | | | +---plugins +| | | | | | | | \---runtime +| | | | | | | +---lightningcss-loader +| | | | | | | | \---src +| | | | | | | +---metadata +| | | | | | | +---next-edge-app-route-loader +| | | | | | | +---next-edge-ssr-loader +| | | | | | | +---next-flight-loader +| | | | | | | +---next-font-loader +| | | | | | | +---next-image-loader +| | | | | | | +---next-route-loader +| | | | | | | +---next-style-loader +| | | | | | | | \---runtime +| | | | | | | +---postcss-loader +| | | | | | | | \---src +| | | | | | | \---resolve-url-loader +| | | | | | | \---lib +| | | | | | \---plugins +| | | | | | +---next-types-plugin +| | | | | | +---terser-webpack-plugin +| | | | | | | \---src +| | | | | | \---wellknown-errors-plugin +| | | | | +---webpack-build +| | | | | \---webpack-config-rules +| | | | +---client +| | | | | +---compat +| | | | | +---components +| | | | | | +---react-dev-overlay +| | | | | | | +---app +| | | | | | | +---internal +| | | | | | | | +---components +| | | | | | | | | +---CodeFrame +| | | | | | | | | +---Dialog +| | | | | | | | | +---hot-linked-text +| | | | | | | | | +---LeftRightDialogHeader +| | | | | | | | | +---Overlay +| | | | | | | | | +---Terminal +| | | | | | | | | +---Toast +| | | | | | | | | \---VersionStalenessInfo +| | | | | | | | +---container +| | | | | | | | | \---RuntimeError +| | | | | | | | +---helpers +| | | | | | | | +---hooks +| | | | | | | | +---icons +| | | | | | | | \---styles +| | | | | | | +---pages +| | | | | | | \---server +| | | | | | \---router-reducer +| | | | | | \---reducers +| | | | | +---dev +| | | | | | \---error-overlay +| | | | | +---legacy +| | | | | +---portal +| | | | | \---tracing +| | | | +---export +| | | | | +---helpers +| | | | | \---routes +| | | | +---lib +| | | | | +---eslint +| | | | | +---fs +| | | | | +---helpers +| | | | | +---memory +| | | | | +---metadata +| | | | | | +---generate +| | | | | | +---resolvers +| | | | | | \---types +| | | | | \---typescript +| | | | +---pages +| | | | +---server +| | | | | +---api-utils +| | | | | | \---node +| | | | | +---app-render +| | | | | | +---rsc +| | | | | | \---static +| | | | | +---async-storage +| | | | | +---base-http +| | | | | +---dev +| | | | | | \---turbopack +| | | | | +---future +| | | | | | +---helpers +| | | | | | | \---module-loader +| | | | | | +---normalizers +| | | | | | | +---built +| | | | | | | | +---app +| | | | | | | | \---pages +| | | | | | | \---request +| | | | | | +---route-definitions +| | | | | | +---route-matcher-managers +| | | | | | +---route-matcher-providers +| | | | | | | +---dev +| | | | | | | | \---helpers +| | | | | | | | \---file-reader +| | | | | | | \---helpers +| | | | | | | \---manifest-loaders +| | | | | | +---route-matchers +| | | | | | +---route-matches +| | | | | | \---route-modules +| | | | | | +---app-page +| | | | | | | \---vendored +| | | | | | | +---contexts +| | | | | | | +---rsc +| | | | | | | \---ssr +| | | | | | +---app-route +| | | | | | | \---helpers +| | | | | | +---helpers +| | | | | | +---pages +| | | | | | | +---builtin +| | | | | | | \---vendored +| | | | | | | \---contexts +| | | | | | \---pages-api +| | | | | +---lib +| | | | | | +---incremental-cache +| | | | | | +---router-utils +| | | | | | +---server-ipc +| | | | | | +---squoosh +| | | | | | | +---avif +| | | | | | | +---mozjpeg +| | | | | | | +---png +| | | | | | | +---resize +| | | | | | | \---webp +| | | | | | \---trace +| | | | | +---og +| | | | | +---response-cache +| | | | | +---stream-utils +| | | | | +---typescript +| | | | | | \---rules +| | | | | \---web +| | | | | +---exports +| | | | | +---sandbox +| | | | | \---spec-extension +| | | | | \---adapters +| | | | \---shared +| | | | \---lib +| | | | +---i18n +| | | | +---isomorphic +| | | | +---lazy-dynamic +| | | | +---page-path +| | | | +---router +| | | | | \---utils +| | | | \---utils +| | | +---experimental +| | | | \---testmode +| | | | +---playwright +| | | | \---proxy +| | | +---export +| | | | +---helpers +| | | | \---routes +| | | +---lib +| | | | +---eslint +| | | | +---fs +| | | | +---helpers +| | | | +---memory +| | | | +---metadata +| | | | | +---generate +| | | | | +---resolvers +| | | | | \---types +| | | | \---typescript +| | | +---pages +| | | +---server +| | | | +---api-utils +| | | | | \---node +| | | | +---app-render +| | | | | +---rsc +| | | | | \---static +| | | | +---async-storage +| | | | +---base-http +| | | | +---dev +| | | | | \---turbopack +| | | | +---future +| | | | | +---helpers +| | | | | | \---module-loader +| | | | | +---normalizers +| | | | | | +---built +| | | | | | | +---app +| | | | | | | \---pages +| | | | | | \---request +| | | | | +---route-definitions +| | | | | +---route-matcher-managers +| | | | | +---route-matcher-providers +| | | | | | +---dev +| | | | | | | \---helpers +| | | | | | | \---file-reader +| | | | | | \---helpers +| | | | | | \---manifest-loaders +| | | | | +---route-matchers +| | | | | +---route-matches +| | | | | \---route-modules +| | | | | +---app-page +| | | | | | \---vendored +| | | | | | +---contexts +| | | | | | +---rsc +| | | | | | \---ssr +| | | | | +---app-route +| | | | | | \---helpers +| | | | | +---helpers +| | | | | +---pages +| | | | | | +---builtin +| | | | | | \---vendored +| | | | | | \---contexts +| | | | | \---pages-api +| | | | +---lib +| | | | | +---incremental-cache +| | | | | +---router-utils +| | | | | +---server-ipc +| | | | | +---squoosh +| | | | | | +---avif +| | | | | | +---mozjpeg +| | | | | | +---png +| | | | | | +---resize +| | | | | | +---rotate +| | | | | | \---webp +| | | | | \---trace +| | | | +---og +| | | | +---response-cache +| | | | +---stream-utils +| | | | +---typescript +| | | | | \---rules +| | | | \---web +| | | | +---exports +| | | | +---sandbox +| | | | \---spec-extension +| | | | \---adapters +| | | +---shared +| | | | \---lib +| | | | +---i18n +| | | | +---isomorphic +| | | | +---lazy-dynamic +| | | | +---page-path +| | | | +---router +| | | | | \---utils +| | | | \---utils +| | | +---src +| | | | \---compiled +| | | | \---@ampproject +| | | | \---toolbox-optimizer +| | | +---styled-jsx +| | | | \---types +| | | +---telemetry +| | | | \---events +| | | \---trace +| | | \---report +| | +---experimental +| | | \---testmode +| | | \---playwright +| | +---font +| | | +---google +| | | \---local +| | +---image-types +| | +---legacy +| | +---navigation-types +| | | \---compat +| | | \---postcss +| | | \---lib +| | \---types +| +---node-int64 +| +---node-releases +| | \---data +| | +---processed +| | \---release-schedule +| +---nodemailer +| | \---lib +| | +---addressparser +| | +---base64 +| | +---dkim +| | +---fetch +| | +---json-transport +| | +---mail-composer +| | +---mailer +| | +---mime-funcs +| | +---mime-node +| | +---punycode +| | +---qp +| | +---sendmail-transport +| | +---ses-transport +| | +---shared +| | +---smtp-connection +| | +---smtp-pool +| | +---smtp-transport +| | +---stream-transport +| | +---well-known +| | \---xoauth2 +| +---normalize-package-data +| | +---lib +| | +---.bin +| | \---semver +| | \---bin +| +---normalize-path +| +---normalize-range +| +---npm-run-path +| +---nwsapi +| | +---dist +| | \---src +| | \---modules +| +---object-assign +| +---object-inspect +| | +---.github +| | +---example +| | \---test +| | \---browser +| +---object-keys +| | \---test +| +---object.assign +| | +---.github +| | +---dist +| | \---test +| +---object.entries +| | \---test +| +---object.fromentries +| | \---test +| +---object.groupby +| | +---.github +| | \---test +| +---object.values +| | \---test +| +---once +| +---onetime +| +---optionator +| | \---lib +| +---own-keys +| | +---.github +| | \---test +| +---p-limit +| +---p-locate +| +---p-try +| +---pac-proxy-agent +| | +---dist +| | +---agent-base +| | | \---dist +| | +---http-proxy-agent +| | | \---dist +| | \---https-proxy-agent +| | \---dist +| +---pac-resolver +| | \---dist +| +---parent-module +| +---parse-json +| +---parse5 +| | \---dist +| | +---cjs +| | | +---common +| | | +---parser +| | | +---serializer +| | | +---tokenizer +| | | \---tree-adapters +| | +---common +| | +---parser +| | +---serializer +| | +---tokenizer +| | \---tree-adapters +| +---path-exists +| +---path-is-absolute +| +---path-key +| +---path-parse +| +---path-scurry +| | +---dist +| | | +---commonjs +| | | \---esm +| | \---lru-cache +| | \---dist +| | +---commonjs +| | \---esm +| +---path-type +| +---pend +| +---picocolors +| +---picomatch +| | \---lib +| +---pirates +| | \---lib +| +---pkg-dir +| | +---find-up +| | +---locate-path +| | +---p-limit +| | \---p-locate +| +---pluralize +| +---possible-typed-array-names +| | +---.github +| | \---test +| +---postcss +| | \---lib +| +---postcss-value-parser +| | \---lib +| +---prelude-ls +| | \---lib +| +---prettier +| | +---bin +| | +---internal +| | \---plugins +| +---prettier-plugin-packagejson +| | \---lib +| | \---esm-proxy +| +---prisma +| | +---build +| | | \---public +| | | +---assets +| | | +---http +| | | \---pages +| | | \---http +| | +---dist +| | | \---cli +| | | \---src +| | +---engines +| | | \---9b628578b3b7cae625e8c927178f15a170e74a9c +| | +---preinstall +| | +---prisma-client +| | | +---generator-build +| | | +---runtime +| | | \---scripts +| | \---scripts +| +---process-nextick-args +| +---progress +| | \---lib +| +---prompts +| | +---dist +| | | +---dateparts +| | | +---elements +| | | \---util +| | \---lib +| | +---dateparts +| | +---elements +| | \---util +| +---prop-types +| | +---lib +| | \---react-is +| | +---cjs +| | \---umd +| +---proxy-agent +| | +---dist +| | +---agent-base +| | | \---dist +| | +---http-proxy-agent +| | | \---dist +| | +---https-proxy-agent +| | | \---dist +| | \---lru-cache +| +---proxy-from-env +| +---psl +| | +---data +| | +---dist +| | \---types +| +---pump +| | \---.github +| +---punycode +| +---puppeteer +| | +---lib +| | | +---cjs +| | | | \---puppeteer +| | | | \---node +| | | \---esm +| | | \---puppeteer +| | | \---node +| | | \---cosmiconfig +| | | \---dist +| | \---src +| | \---node +| +---puppeteer-core +| | +---lib +| | | +---cjs +| | | | +---puppeteer +| | | | | +---api +| | | | | | \---locators +| | | | | +---bidi +| | | | | | \---core +| | | | | +---cdp +| | | | | +---common +| | | | | +---generated +| | | | | +---injected +| | | | | +---node +| | | | | | \---util +| | | | | \---util +| | | | \---third_party +| | | | +---mitt +| | | | +---parsel-js +| | | | \---rxjs +| | | +---es5-iife +| | | \---esm +| | | +---puppeteer +| | | | +---api +| | | | | \---locators +| | | | +---bidi +| | | | | \---core +| | | | +---cdp +| | | | +---common +| | | | +---generated +| | | | +---injected +| | | | +---node +| | | | | \---util +| | | | \---util +| | | \---third_party +| | | +---mitt +| | | +---parsel-js +| | | \---rxjs +| | \---src +| | +---api +| | | \---locators +| | +---bidi +| | | \---core +| | +---cdp +| | +---common +| | +---generated +| | +---injected +| | +---node +| | | \---util +| | +---templates +| | \---util +| +---pure-rand +| | \---lib +| | +---distribution +| | | \---internals +| | +---esm +| | | +---distribution +| | | | \---internals +| | | +---generator +| | | \---types +| | | +---distribution +| | | | \---internals +| | | \---generator +| | +---generator +| | \---types +| | +---distribution +| | | \---internals +| | \---generator +| +---querystringify +| +---queue-microtask +| +---react +| | +---cjs +| | \---umd +| +---react-apexcharts +| | +---dist +| | \---types +| +---react-dom +| | +---cjs +| | \---umd +| +---react-dropzone +| | +---.husky +| | +---dist +| | | \---es +| | | \---utils +| | +---examples +| | | +---accept +| | | +---basic +| | | +---class-component +| | | +---events +| | | +---file-dialog +| | | +---forms +| | | +---maxFiles +| | | +---no-jsx +| | | +---pintura +| | | +---plugins +| | | +---previews +| | | +---styling +| | | \---validator +| | +---src +| | | +---utils +| | | \---__snapshots__ +| | \---typings +| | \---tests +| +---react-hook-form +| | \---dist +| | +---logic +| | +---types +| | | \---path +| | +---utils +| | +---__tests__ +| | | +---logic +| | | +---useFieldArray +| | | \---useForm +| | \---__typetest__ +| | +---path +| | \---__fixtures__ +| +---react-is +| | +---cjs +| | \---umd +| +---react-smooth +| | +---es6 +| | +---lib +| | +---src +| | \---umd +| +---react-transition-group +| | +---cjs +| | | \---utils +| | +---config +| | +---CSSTransition +| | +---dist +| | +---esm +| | | \---utils +| | +---ReplaceTransition +| | +---SwitchTransition +| | +---Transition +| | +---TransitionGroup +| | \---TransitionGroupContext +| +---read-pkg +| | \---type-fest +| | \---source +| +---read-pkg-up +| | +---find-up +| | +---locate-path +| | +---p-limit +| | +---p-locate +| | \---type-fest +| | \---source +| +---readable-stream +| | \---lib +| +---recharts +| | +---es6 +| | | +---cartesian +| | | +---chart +| | | +---component +| | | +---container +| | | +---context +| | | +---numberAxis +| | | +---polar +| | | +---shape +| | | \---util +| | | +---cursor +| | | +---payload +| | | \---tooltip +| | +---lib +| | | +---cartesian +| | | +---chart +| | | +---component +| | | +---container +| | | +---context +| | | +---numberAxis +| | | +---polar +| | | +---shape +| | | \---util +| | | +---cursor +| | | +---payload +| | | \---tooltip +| | | \---react-is +| | | +---cjs +| | | \---umd +| | +---types +| | | +---cartesian +| | | +---chart +| | | +---component +| | | +---container +| | | +---context +| | | +---numberAxis +| | | +---polar +| | | +---shape +| | | \---util +| | | +---cursor +| | | +---payload +| | | \---tooltip +| | \---umd +| +---recharts-scale +| | +---es6 +| | | \---util +| | +---lib +| | | \---util +| | +---src +| | | \---util +| | \---umd +| +---redent +| +---reflect.getprototypeof +| | \---test +| +---regexp-tree +| | +---bin +| | \---dist +| | +---bin +| | +---compat-transpiler +| | | +---runtime +| | | \---transforms +| | +---generator +| | +---interpreter +| | | \---finite-automaton +| | | +---dfa +| | | \---nfa +| | +---optimizer +| | | \---transforms +| | +---parser +| | | +---generated +| | | \---unicode +| | +---transform +| | +---traverse +| | \---utils +| +---regexp.prototype.flags +| | \---test +| +---regjsparser +| | +---bin +| | +---.bin +| | \---jsesc +| | +---bin +| | \---man +| +---require-directory +| +---requires-port +| +---resolve +| | +---.github +| | +---bin +| | +---example +| | +---lib +| | \---test +| | +---dotdot +| | | \---abc +| | +---module_dir +| | | +---xmodules +| | | | \---aaa +| | | +---ymodules +| | | | \---aaa +| | | \---zmodules +| | | \---bbb +| | +---node_path +| | | +---x +| | | | +---aaa +| | | | \---ccc +| | | \---y +| | | +---bbb +| | | \---ccc +| | +---pathfilter +| | | \---deep_ref +| | +---precedence +| | | +---aaa +| | | \---bbb +| | +---resolver +| | | +---baz +| | | +---browser_field +| | | +---dot_main +| | | +---dot_slash_main +| | | +---false_main +| | | +---incorrect_main +| | | +---invalid_main +| | | +---multirepo +| | | | \---packages +| | | | +---package-a +| | | | \---package-b +| | | +---nested_symlinks +| | | | \---mylib +| | | +---other_path +| | | | \---lib +| | | +---quux +| | | | \---foo +| | | +---same_names +| | | | \---foo +| | | +---symlinked +| | | | +---package +| | | | \---_ +| | | | \---symlink_target +| | | \---without_basedir +| | \---shadowed_core +| | \---util +| +---resolve-cwd +| | \---resolve-from +| +---resolve-from +| +---resolve-pkg-maps +| | \---dist +| +---resolve.exports +| | \---dist +| +---reusify +| | +---.github +| | | \---workflows +| | \---benchmarks +| +---rimraf +| | +---brace-expansion +| | +---glob +| | \---minimatch +| +---run-parallel +| +---safe-array-concat +| | +---.github +| | | \---isarray +| | \---test +| +---safe-buffer +| +---safe-push-apply +| | +---.github +| | | \---isarray +| | \---test +| +---safe-regex-test +| | +---.github +| | \---test +| +---safer-buffer +| +---saxes +| +---scheduler +| | +---cjs +| | \---umd +| +---semver +| | +---bin +| | +---classes +| | +---functions +| | +---internal +| | \---ranges +| +---set-function-length +| | \---.github +| +---set-function-name +| | \---.github +| +---set-proto +| | +---.github +| | \---test +| +---shebang-command +| +---shebang-regex +| +---side-channel +| | +---.github +| | \---test +| +---side-channel-list +| | +---.github +| | \---test +| +---side-channel-map +| | +---.github +| | \---test +| +---side-channel-weakmap +| | +---.github +| | \---test +| +---signal-exit +| | \---dist +| | +---cjs +| | \---mjs +| +---sisteransi +| | \---src +| +---slash +| +---smart-buffer +| | +---build +| | +---docs +| | \---typings +| +---socks +| | +---build +| | | +---client +| | | \---common +| | +---docs +| | | \---examples +| | | +---javascript +| | | \---typescript +| | \---typings +| | +---client +| | \---common +| +---socks-proxy-agent +| | +---dist +| | \---agent-base +| | \---dist +| +---sort-object-keys +| +---sort-package-json +| | \---detect-newline +| +---source-map +| | +---dist +| | \---lib +| +---source-map-js +| | \---lib +| +---source-map-support +| | +---buffer-from +| | \---source-map +| | +---dist +| | \---lib +| +---spdx-correct +| +---spdx-exceptions +| +---spdx-expression-parse +| +---spdx-license-ids +| +---sprintf-js +| | +---demo +| | +---dist +| | +---src +| | \---test +| +---ssf +| | \---types +| +---stable-hash +| | \---dist +| +---stack-utils +| | \---escape-string-regexp +| +---stop-iteration-iterator +| | +---.github +| | \---test +| +---streamsearch +| | +---.github +| | | \---workflows +| | +---lib +| | \---test +| +---streamx +| +---string-length +| +---string-width +| | +---ansi-regex +| | \---strip-ansi +| +---string-width-cjs +| | \---emoji-regex +| | \---es2015 +| +---string.prototype.includes +| | +---.github +| | | \---workflows +| | \---tests +| +---string.prototype.matchall +| | +---.github +| | \---test +| +---string.prototype.repeat +| | \---tests +| +---string.prototype.trim +| | \---test +| +---string.prototype.trimend +| | \---test +| +---string.prototype.trimstart +| | \---test +| +---string_decoder +| +---strip-ansi +| +---strip-ansi-cjs +| +---strip-bom +| +---strip-final-newline +| +---strip-indent +| +---strip-json-comments +| +---styled-jsx +| | +---dist +| | | +---babel +| | | +---index +| | | \---webpack +| | \---lib +| +---stylis +| | +---dist +| | | \---umd +| | \---src +| +---supports-color +| +---supports-preserve-symlinks-flag +| | +---.github +| | \---test +| +---svg.draggable.js +| | \---dist +| +---svg.easing.js +| | \---dist +| +---svg.filter.js +| | \---dist +| +---svg.js +| | +---.config +| | +---.github +| | | \---ISSUE_TEMPLATE +| | +---bench +| | | \---tests +| | +---dist +| | +---spec +| | | +---fixtures +| | | +---lib +| | | | \---jasmine-2.6.0 +| | | +---spec +| | | \---support +| | \---src +| +---svg.pathmorphing.js +| | \---dist +| +---svg.resize.js +| | +---dist +| | \---svg.select.js +| | \---dist +| +---svg.select.js +| | \---dist +| +---symbol-tree +| | \---lib +| +---synckit +| | \---lib +| +---tailwindcss +| | \---dist +| +---tar-fs +| +---tar-stream +| +---test-exclude +| | +---brace-expansion +| | +---glob +| | \---minimatch +| +---text-decoder +| | \---lib +| +---text-table +| | +---example +| | \---test +| +---through +| | \---test +| +---through2 +| +---tiny-invariant +| | +---dist +| | | \---esm +| | \---src +| +---tinyglobby +| | +---dist +| | +---fdir +| | | \---dist +| | | +---api +| | | | \---functions +| | | \---builder +| | \---picomatch +| | \---lib +| +---tmpl +| | \---lib +| +---to-regex-range +| +---tough-cookie +| | \---lib +| +---tr46 +| | \---lib +| +---ts-api-utils +| | \---lib +| +---tsconfig-paths +| | +---lib +| | | \---__tests__ +| | | \---data +| | | +---.bin +| | | +---json5 +| | | | +---dist +| | | | \---lib +| | | \---strip-bom +| | \---src +| | \---__tests__ +| | \---data +| +---tslib +| | \---modules +| +---tsutils +| | | \---tslib +| | | +---modules +| | | \---test +| | | \---validateModuleExportsMatchCommonJS +| | +---typeguard +| | | +---2.8 +| | | +---2.9 +| | | +---3.0 +| | | +---3.2 +| | | \---next +| | \---util +| +---type-check +| | \---lib +| +---type-detect +| +---type-fest +| | +---source +| | \---ts41 +| +---typed-array-buffer +| | +---.github +| | \---test +| +---typed-array-byte-length +| | +---.github +| | \---test +| +---typed-array-byte-offset +| | +---.github +| | \---test +| +---typed-array-length +| | +---.github +| | \---test +| +---typed-query-selector +| +---typescript +| | +---bin +| | \---lib +| | +---cs +| | +---de +| | +---es +| | +---fr +| | +---it +| | +---ja +| | +---ko +| | +---pl +| | +---pt-br +| | +---ru +| | +---tr +| | +---zh-cn +| | \---zh-tw +| +---unbox-primitive +| | +---.github +| | \---test +| +---undici-types +| +---universalify +| +---unrs-resolver +| +---update-browserslist-db +| +---uri-js +| | \---dist +| | +---es5 +| | \---esnext +| | \---schemes +| +---url-parse +| | \---dist +| +---util-deprecate +| +---v8-to-istanbul +| | +---lib +| | \---convert-source-map +| +---validate-npm-package-license +| +---victory-vendor +| | +---es +| | +---lib +| | \---lib-vendor +| | +---d3-array +| | | \---src +| | | \---threshold +| | +---d3-color +| | | \---src +| | +---d3-ease +| | | \---src +| | +---d3-format +| | | \---src +| | +---d3-interpolate +| | | \---src +| | | \---transform +| | +---d3-path +| | | \---src +| | +---d3-scale +| | | \---src +| | +---d3-shape +| | | \---src +| | | +---curve +| | | +---offset +| | | +---order +| | | \---symbol +| | +---d3-time +| | | \---src +| | +---d3-time-format +| | | \---src +| | +---d3-timer +| | | \---src +| | +---d3-voronoi +| | | \---src +| | \---internmap +| | \---src +| +---w3c-xmlserializer +| | \---lib +| +---walker +| | \---lib +| +---webidl-conversions +| | \---lib +| +---whatwg-encoding +| | \---lib +| +---whatwg-mimetype +| | \---lib +| +---whatwg-url +| | \---lib +| +---which +| | \---bin +| +---which-boxed-primitive +| | +---.github +| | \---test +| +---which-builtin-type +| | | \---isarray +| | \---test +| +---which-collection +| | +---.github +| | \---test +| +---which-typed-array +| | +---.github +| | \---test +| +---wmf +| | \---dist +| +---word +| +---word-wrap +| +---wrap-ansi +| | +---ansi-regex +| | +---ansi-styles +| | \---strip-ansi +| +---wrap-ansi-cjs +| | +---emoji-regex +| | | \---es2015 +| | \---string-width +| +---wrappy +| +---write-file-atomic +| | +---lib +| | \---signal-exit +| +---ws +| | \---lib +| +---xlsx +| | +---bin +| | +---dist +| | \---types +| +---xml-name-validator +| | \---lib +| +---xmlchars +| | +---xml +| | | +---1.0 +| | | \---1.1 +| | \---xmlns +| | \---1.0 +| +---xtend +| | \---object-keys +| | \---test +| +---y18n +| | \---build +| | \---lib +| | \---platform-shims +| +---yallist +| +---yaml +| | +---browser +| | | +---dist +| | | \---types +| | +---dist +| | \---types +| +---yargs +| | +---build +| | | \---lib +| | | +---typings +| | | \---utils +| | +---helpers +| | +---lib +| | | \---platform-shims +| | +---locales +| | +---emoji-regex +| | | \---es2015 +| | \---string-width +| +---yargs-parser +| | \---build +| | \---lib +| +---yauzl +| +---yocto-queue +| \---zod +| \---lib +| +---benchmarks +| +---helpers +| +---locales +| \---__tests__ ++---prisma +| +---public +| | \---templates +| +---src +| | +---app +| | | \---api +| | | +---admin +| | | | \---carteiras +| | | \---carteiras +| | | \---upload +| | \---components +| | \---dashboard +| | \---carteiras +| \---uploads +| \---carteiras ++---public +| +---assets +| +---images +| | +---avatars +| | +---logos +| | \---products +| +---pdfs +| \---templates ++---scripts ++---src +| +---admin +| +---app +| | +---api +| | | +---admin +| | | | +---bulk-import +| | | | +---carteiras +| | | | +---instagram-cadastros +| | | | +---integracoes +| | | | | \---hotmart +| | | | +---renovacoes +| | | | \---users +| | | | \---[id] +| | | +---auth +| | | | +---change-password +| | | | +---login +| | | | +---me +| | | | \---reset-password +| | | | \---[token] +| | | +---carteiras +| | | | +---minhas +| | | | +---status-usuario +| | | | +---upload +| | | | \---[id] +| | | | \---download-pdf +| | | +---check-admin +| | | +---dados +| | | +---financial +| | | | +---fii-market-data +| | | | +---international-data +| | | | +---market-data +| | | | \---quotes +| | | +---hotmart +| | | | \---webhook +| | | +---ifix +| | | +---instagram-cadastro +| | | +---market +| | | | \---ifix +| | | +---relatorio-semanal +| | | +---user +| | | | +---me +| | | | \---profile +| | | \---webhooks +| | | +---eduzz +| | | | \---[token] +| | | +---hotmart +| | | | \---[id] +| | | \---kiwify +| | | \---[token] +| | +---auth +| | | +---change-password +| | | +---reset-password +| | | | \---[token] +| | | +---sign-in +| | | \---sign-up +| | +---dados-ifix +| | +---dashboard +| | | +---account +| | | +---admin +| | | | +---analises-trimesestrais +| | | | +---carteiras +| | | | +---import-usuarios +| | | | +---instagram-cadastros +| | | | +---integracoes +| | | | | +---eduzz +| | | | | +---hotmart +| | | | | \---kiwify +| | | | +---relatorio-semanal +| | | | +---renovacoes +| | | | \---usuarios +| | | | +---novo +| | | | \---[id] +| | | +---ativo +| | | | \---[ticker] +| | | +---central-agenda +| | | +---central-proventos +| | | +---central-relatorios +| | | +---customers +| | | +---empresa +| | | | \---[ticker] +| | | +---empresa-exterior +| | | | \---[ticker] +| | | +---gerenciamento +| | | +---integrations +| | | +---internacional +| | | | +---dividendos +| | | | +---etfs +| | | | +---projeto-america +| | | | \---stocks +| | | +---MicroCapsPageV2 +| | | +---overview +| | | +---recursos-exclusivos +| | | | +---analise-de-carteira +| | | | +---dicas-de-investimentos +| | | | +---ebooks +| | | | +---imposto-de-renda +| | | | +---lives-e-aulas +| | | | +---milhas-aereas +| | | | +---planilhas +| | | | +---reserva-emergencia +| | | | \---telegram +| | | +---relatorio-semanal +| | | +---rentabilidades +| | | \---settings +| | +---errors +| | | \---not-found +| | \---teste +| +---assets +| | \---scss +| | \---misc +| +---common +| | +---colors +| | \---validators +| +---components +| | +---Alert +| | +---auth +| | +---CodeBlock +| | +---core +| | | \---theme-provider +| | +---dashboard +| | | +---account +| | | +---carteiras +| | | +---customer +| | | +---integrations +| | | +---layout +| | | +---overview +| | | +---rentabilidades +| | | \---settings +| | +---DisplayMode +| | +---Map +| | +---micro-caps +| | +---Paper +| | +---Portlet +| | +---PortletContent +| | +---PortletFooter +| | +---PortletHeader +| | +---PortletLabel +| | +---PortletToolbar +| | +---SearchInput +| | +---Status +| | \---Task +| +---config +| +---contexts +| +---data +| +---helpers +| +---HOCs +| | \---authGuard +| +---hooks +| | \---micro-caps +| +---icons +| | +---Facebook +| | \---Google +| +---layouts +| | \---Dashboard +| | \---components +| | +---Footer +| | +---Sidebar +| | \---Topbar +| | \---components +| | \---NotificationList +| +---lib +| | \---auth +| +---services +| | +---auth +| | +---notification +| | +---order +| | +---product +| | \---user +| +---styles +| | \---theme +| | \---components +| +---theme +| | \---overrides +| +---types +| +---utils +| | \---micro-caps +| \---views +| +---Account +| | \---components +| | +---AccountDetails +| | \---AccountProfile +| +---Dashboard +| | \---components +| | +---Budget +| | +---DevicesChart +| | +---OrdersTable +| | +---ProductList +| | +---Profit +| | +---Progress +| | +---SalesChart +| | \---Users +| +---Icons +| +---NotFound +| +---ProductList +| | \---components +| | +---ProductCard +| | \---ProductsToolbar +| +---Settings +| | \---components +| | +---Notifications +| | \---Password +| +---SignIn +| +---SignUp +| +---Typography +| +---UnderDevelopment +| \---UserList +| \---components +| +---UsersTable +| \---UsersToolbar ++---uploads +| \---carteiras +\---utils diff --git a/et --hard HEAD~1 b/et --hard HEAD~1 new file mode 100644 index 000000000..932070301 --- /dev/null +++ b/et --hard HEAD~1 @@ -0,0 +1,369 @@ +e35e9aff (HEAD -> main, origin/main, origin/HEAD) Atualização da carteira de opções +0e1f2f3a Merge pull request #310 from Jacbdias/codex/ajustar-pagina-de-gerenciamento-de-ativos +73165427 Refactor CF Renda Turbinada management +09adb755 Adjust CF Renda Turbinada management data model +0427d2f6 Merge pull request #309 from Jacbdias/codex/fix-displayed-price-ceiling-for-etfs-psyxb9 +3a41ea63 Merge branch 'main' into codex/fix-displayed-price-ceiling-for-etfs-psyxb9 +09a1f20b Update page.tsx +1559cec7 Update page.tsx +30218922 Restore price ceiling display for non-ETF assets +7acd0607 Update page.tsx +e322e390 Update page.tsx +ba4bbff9 Merge pull request #308 from Jacbdias/codex/fix-displayed-price-ceiling-for-etfs +a7988e8a Hide ETF price ceiling on Projeto América +3ed5f5e3 Merge pull request #307 from Jacbdias/codex/remove-preco-teto-do-ativo +e7bcf110 Fix clearing price ceiling on asset management +c1b81110 Update page.tsx +26f82542 Update page.tsx +2da28250 Merge pull request #306 from Jacbdias/codex/fix-responsiveness-for-usuarios-page +6c479d0c Improve admin user management responsiveness +273794b1 Merge pull request #305 from Jacbdias/codex/corrigir-identificacao-de-ativos-para-alup11,-klbn11,-sanb11 +e1a18a0d Fix FII detection for Brazilian units +a7e7b932 Update page.tsx +080ba77b Update page.tsx +3ad57f38 Update route.ts +758681e0 Update page.tsx +912a0e64 Update page.tsx +cce5adf3 Merge pull request #304 from Jacbdias/codex/find-account-lockout-error-threshold +6c933ce1 Show password reset guidance after repeated login failures +ecf5bf1d Merge pull request #303 from Jacbdias/codex/fix-incorrect-purchase-values-on-management-page-y1olqd +dab7e241 Fix Kiwify purchase amount reconciliation +f66c79f9 Merge pull request #302 from Jacbdias/codex/fix-incorrect-purchase-values-on-management-page +9b303174 Fix Kiwify amount normalization +e5c12646 Merge pull request #301 from Jacbdias/codex/permitir-admin-excluir-perguntas +8482f337 Allow admins to delete central duvidas questions +6c78cf2e Update page.tsx +b186fc74 Update page.tsx +30ce0991 Update page.tsx +3b2c29cd Update page.tsx +eceaeaf1 Update page.tsx +ebb4e263 Update auth-access-context.tsx +1619a89c Update page.tsx +d541e08e Update page.tsx +af6c0566 Update page.tsx +a0a8a933 Merge pull request #300 from Jacbdias/codex/ajustar-pagina-da-carteira-recomendada +96d9be76 feat: redesign renda turbinada options dashboard +c97c0b84 Merge pull request #299 from Jacbdias/codex/permitir-botao-entrar-na-live-em-andamento +8541bf8d Keep live access button visible during ongoing sessions +d2a938c1 Merge pull request #298 from Jacbdias/codex/fix-percentage-display-for-fii-assets +bc204a71 Corrige cálculo de percentual de carteira para FIIs +ba6112f2 Merge pull request #297 from Jacbdias/codex/add-fii-icon-support-to-dashboard +6d29e5cf Use FII logos from local directory +95b71787 Descrição das alterações que você fez +ad502043 Merge pull request #296 from Jacbdias/codex/remove-calendario-de-lives +dc2e41dc Remove live calendar section from Bastidores da Bolsa +cc9bb5e3 Add files via upload +45a9b6c1 Delete public/logos-fiis/Logotipo minimalista moderno preto e verde.png +823eb720 Add files via upload +f7ed46a8 Delete public/logos-fiis/DEVA11.png +af273226 Add files via upload +e77f46c2 Delete public/logos-fiis/DEVA11.png +9d975db1 Merge pull request #295 from Jacbdias/codex/fix-incorrect-purchase-value-in-api +0ed7cf60 Fix Kiwify webhook amount parsing +36589d88 Merge pull request #294 from Jacbdias/codex/add-pdf-upload-and-extraction-features-dusca0 +c0976016 Merge branch 'main' into codex/add-pdf-upload-and-extraction-features-dusca0 +c7e7fe91 fix: pass operator list to svg extraction +f65f4846 Merge pull request #293 from Jacbdias/codex/add-pdf-upload-and-extraction-features-63az8y +9a1df57d Merge branch 'main' into codex/add-pdf-upload-and-extraction-features-63az8y +bc267ec4 Merge pull request #292 from Jacbdias/codex/check-card-access-by-user-plan-jbqzj9 +5db0a747 fix: extract embedded images from pdf imports +64ac5f4e Restrict admin dashboard cards by access permissions +efb405b9 Merge pull request #291 from Jacbdias/codex/add-pdf-upload-and-extraction-features +79bc7b61 feat: auto-fill technical analysis from PDF +cd845c0a Merge pull request #290 from Jacbdias/codex/check-card-access-by-user-plan +d2c9986c Filter exclusive resources by user permissions +fb3f4cdd Update AdminAnaliseTecnicaManager.tsx +ea6fb470 Update TechnicalAnalysisSection.tsx +3f343130 Update AdminAnaliseTecnicaManager.tsx +2ebfa92a Update AdminAnaliseTecnicaManager.tsx +7d5663c7 Merge pull request #289 from Jacbdias/codex/fix-user-plan-customization-saving-issue-7rq08e +92732179 Merge branch 'main' into codex/fix-user-plan-customization-saving-issue-7rq08e +364e04a9 Avoid custom permission helper name collisions +e012bc66 Merge pull request #287 from Jacbdias/codex/fix-user-plan-customization-saving-issue-bhkq5o +54ccaceb Normalize custom permissions for end-user access +4bf394e8 Merge pull request #286 from Jacbdias/codex/fix-user-plan-customization-saving-issue +780ef627 Allow manual analyst users to retain custom permissions +943a0827 Merge pull request #285 from Jacbdias/codex/fix-referenceerror-for-purchasesmap +cbf6c982 Fix purchases map initialization in admin users route +2ac5addf Merge pull request #284 from Jacbdias/codex/fix-question-count-display-issue +772897e3 Fix admin question counters using stats +268eaa49 Merge pull request #283 from Jacbdias/codex/improve-button-positioning-on-dashboard +901d0c94 Improve responder button layout on admin central duvidas +b807c07a Update AnalisesTrimesestrais.tsx +4bade9fd Update TechnicalAnalysisSection.tsx +f2fddf7d Merge pull request #282 from Jacbdias/codex/add-show-more-button-for-live-calendar +b0ec3a19 Refine live calendar and show more toggle +a1e6eb2b Add expandable live calendar list +48093f17 Update page.tsx +42d9d173 Update page.tsx +784f6927 Update page.tsx +3cfc8678 Merge pull request #280 from Jacbdias/codex/ajustar-layout-de-titulos-grandes-no-mobile +5f6d7c39 Fix mobile live card layout JSX structure +818a65d6 Fix mobile layout of live cards +fe6403f0 Improve mobile layout for live cards +2f489637 Update page.tsx +ccef5a3b Update page.tsx +a5cb88a4 Update page.tsx +d3d951e5 Update page.tsx +6fbfb945 Update page.tsx +1cceed7c Merge pull request #279 from Jacbdias/codex/corrigir-erro-ao-anexar-arquivos-nas-mentorias +06c851b3 Redirecionar download do react-dropzone +ad941116 Corrigir importação do utilitário de upload +a30c00b2 Refatorar upload de materiais das mentorias +d8cb94ae Merge pull request #278 from Jacbdias/codex/add-section-for-recorded-mentorships-txaz9o +36f439e1 Add migration for CF Renda Turbinada mentorias +8f7af77b Merge pull request #277 from Jacbdias/codex/add-section-for-recorded-mentorships +440295c7 Fix mentorship API util import paths +fb88bf19 Add recorded mentorships section and API support +5fbfab69 Merge pull request #276 from Jacbdias/codex/corrigir-erro-de-sintaxe-em-page.tsx-9e62h3 +52d78f9d Merge branch 'main' into codex/corrigir-erro-de-sintaxe-em-page.tsx-9e62h3 +c3cbdc1b Fix inline material preview for CF Renda Turbinada lessons +0336f50c Merge pull request #275 from Jacbdias/codex/corrigir-erro-de-sintaxe-em-page.tsx +ab2729a3 Fix lesson materials list markup +15dc0e08 Update page.tsx +2d1a22f2 Update page.tsx +2538088c Update next.config.js +b91b34a6 Update route.ts +764c4724 Update route.ts +864ed818 chore: atualizar dependências e resolver conflitos +3b00effe Merge pull request #274 from Jacbdias/codex/corrigir-erro-ao-anexar-pdf-nas-aulas +c2cead40 Handle file uploads without relying on File constructor +1972ab38 Merge pull request #273 from Jacbdias/codex/adicionar-anexos-e-editor-de-texto-as-aulas-a70t4a +0df2ba75 Merge branch 'main' into codex/adicionar-anexos-e-editor-de-texto-as-aulas-a70t4a +2ac206c6 Handle uploads without relying on File constructor +5d359d32 Merge pull request #272 from Jacbdias/codex/adicionar-anexos-e-editor-de-texto-as-aulas-53kmc1 +314f572f Add migration for CF Renda Turbinada lesson materials +9bc8830e Merge pull request #271 from Jacbdias/codex/adicionar-anexos-e-editor-de-texto-as-aulas +f9f99ab9 Add lesson materials support to CF Renda Turbinada classes +93e11fdf Merge pull request #270 from Jacbdias/codex/fix-data-loading-issue-on-settings-page +26740325 Compute DY fallback from local proventos data +96191da4 Update page.tsx +10faceb8 Merge pull request #269 from Jacbdias/codex/fix-page-load-error-on-/dashboard/settings-ca33oo +3f9436ee Merge branch 'main' into codex/fix-page-load-error-on-/dashboard/settings-ca33oo +a28858a5 Add compatibility financial APIs to remove console errors +a8f1dc1e Merge pull request #268 from Jacbdias/codex/fix-page-load-error-on-/dashboard/settings +42455f11 Add portfolio API endpoints and share default carteira data +69f6b635 Merge pull request #267 from Jacbdias/codex/corrigir-carregamento-de-dados-da-pagina-/dashboard/settings +bcaa0628 Ensure settings dashboard falls back to seeded data +4112cbe1 Merge pull request #266 from Jacbdias/codex/create-server-side-fetcher-for-market-data +db33de37 Merge branch 'main' into codex/create-server-side-fetcher-for-market-data +582226e0 Fix server market data module directive +31ca434d Merge pull request #265 from Jacbdias/codex/extract-static-values-to-utility-module +332e8220 refactor: centralize market data fetching +185b2eb0 Merge pull request #264 from Jacbdias/codex/implementar-melhorias-na-api-de-proventos +336fe1b2 Refactor IFIX fallbacks into shared utilities +ad35a037 Improve proventos-based DY fallback +8510e94a Merge pull request #263 from Jacbdias/codex/corrigir-permissoes-do-plano-cf-renda-turbinada +5783baf1 (origin/codex/corrigir-permissoes-do-plano-cf-renda-turbinada) Add CF Renda Turbinada permission options to admin user editor +9106cb90 Merge pull request #262 from Jacbdias/codex/add-function-to-reorder-classes +f5a5a92b (origin/codex/add-function-to-reorder-classes) feat: allow reorganizing CF Renda Turbinada lessons +09513d14 Merge pull request #261 from Jacbdias/codex/fix-email-sending-ratelimit-error +a4ba1330 (origin/codex/fix-email-sending-ratelimit-error) Handle SMTP rate limits with retries +7196c0dc Merge pull request #260 from Jacbdias/codex/add-user-profile-for-analista-re3oqt +eb17d666 (origin/codex/add-user-profile-for-analista-re3oqt) Add missing central agenda styles module +a44ee45f Update central-duvidas.module.css +7d6add70 Merge pull request #259 from Jacbdias/codex/add-user-profile-for-analista-2hre6a +8605d90d (origin/codex/add-user-profile-for-analista-2hre6a) Sanitize Analista admin permissions +a23effc6 Merge pull request #258 from Jacbdias/codex/add-user-profile-for-analista-d0hd4j +e1be0bab (origin/codex/add-user-profile-for-analista-d0hd4j) Allow configuring Analista admin access +943c6d25 Update page.tsx +aca90448 Update central-duvidas.module.css +b7e3522a Update central-duvidas.module.css +a104f156 Merge pull request #257 from Jacbdias/codex/add-user-profile-for-analista +6a3d9ff0 (origin/codex/add-user-profile-for-analista) Add Analista role with configurable access and central support +560d2aa4 Update page.tsx +2fcc126d Merge pull request #256 from Jacbdias/codex/ajustar-links-clicaveis-em-respostas +fcfba2e2 Merge pull request #255 from Jacbdias/codex/alterar-nome-do-administrador-para-fatos-da-bolsa-0ektxr +162b0f30 (origin/codex/ajustar-links-clicaveis-em-respostas) Render Central Dúvidas links as anchors +4aeaad05 (origin/codex/alterar-nome-do-administrador-para-fatos-da-bolsa-0ektxr) Ajusta autoria das notificações de respostas +0e3b6539 Merge pull request #254 from Jacbdias/codex/alterar-nome-do-administrador-para-fatos-da-bolsa +b88963ba (origin/codex/alterar-nome-do-administrador-para-fatos-da-bolsa) Atualiza exibição de nome nas respostas administrativas +66af9106 Merge pull request #253 from Jacbdias/codex/ajustar-venimento-usuarios-plano-renda-turbinada-wapvlq +4354df6f (origin/codex/ajustar-venimento-usuarios-plano-renda-turbinada-wapvlq) Add lifetime toggle to admin user expiration form +78bfe9d2 Merge pull request #252 from Jacbdias/codex/ajustar-venimento-usuarios-plano-renda-turbinada +211d4536 (origin/codex/ajustar-venimento-usuarios-plano-renda-turbinada) Ensure Renda Turbinada plan grants lifetime access +5ff81dd4 Merge pull request #251 from Jacbdias/codex/implementar-sistema-de-notificacoes +89d19be1 (origin/codex/implementar-sistema-de-notificacoes) Add admin broadcast notifications UI +3232d357 Merge pull request #250 from Jacbdias/codex/adicionar-acesso-ao-telegram-no-menu-lateral-pvhcvf +3372d27b (origin/codex/adicionar-acesso-ao-telegram-no-menu-lateral-pvhcvf) Rename Telegram menu entry for Renda Turbinada +e46a8561 Merge pull request #249 from Jacbdias/codex/adicionar-acesso-ao-telegram-no-menu-lateral +40dab218 (origin/codex/adicionar-acesso-ao-telegram-no-menu-lateral) Allow Renda Turbinada to access Telegram resource +3ce331f6 Merge pull request #248 from Jacbdias/codex/ajuste-responsivo-para-mobile-na-pagina-aulas-qneue6 +91b64fc8 (origin/codex/ajuste-responsivo-para-mobile-na-pagina-aulas-qneue6) Merge branch 'main' into codex/ajuste-responsivo-para-mobile-na-pagina-aulas-qneue6 +7c553846 Expand lesson videos on mobile +263b3dc5 Merge pull request #247 from Jacbdias/codex/ajuste-responsivo-para-mobile-na-pagina-aulas +6cc465ba (origin/codex/ajuste-responsivo-para-mobile-na-pagina-aulas) Adjust mobile padding for CF Renda Turbinada lessons +3f1bb4ae Merge pull request #246 from Jacbdias/codex/corrigir-erro-na-importacao-de-usuarios-gk8t5g +d2a86abf (origin/codex/corrigir-erro-na-importacao-de-usuarios-gk8t5g) Normalize plan and status in admin bulk import +271018a9 Merge pull request #245 from Jacbdias/codex/corrigir-erro-na-importacao-de-usuarios +3d550b26 (origin/codex/corrigir-erro-na-importacao-de-usuarios) Fix bulk import route admin auth +c7eacd34 Update page.tsx +cb4ffae0 Update page.tsx +98098946 Merge pull request #244 from Jacbdias/codex/modificar-exibicao-do-dashboard-para-renda-turbinada +513aa0c5 (origin/codex/modificar-exibicao-do-dashboard-para-renda-turbinada) Oculta Instagram no Telegram para plano Renda Turbinada +2fa5c2a5 Update page.tsx +40c01e8d Merge pull request #243 from Jacbdias/codex/add-lock-icon-and-release-date +d84f64e2 Update page.tsx +f147050e (origin/codex/add-lock-icon-and-release-date) Restrict carteira recomendada page to admins +7264ebd7 Merge pull request #242 from Jacbdias/codex/add-card-for-telegram-access-instructions +5881c7c1 (origin/codex/add-card-for-telegram-access-instructions) Include telegram card for Renda Turbinada +d0f310cf Update route.ts +9365523d Merge pull request #241 from Jacbdias/codex/new-task +d178c467 (origin/codex/new-task) Improve SMTP config compatibility +8934274a Merge pull request #240 from Jacbdias/codex/fix-email-resend-error-on-user-page +9430bf9e (origin/codex/fix-email-resend-error-on-user-page) Support SMTP password fallback +0df2a86d Merge pull request #239 from Jacbdias/codex/fix-access-instructions-email-error-kphirl +2bd0dc11 (origin/codex/fix-access-instructions-email-error-kphirl) Merge branch 'main' into codex/fix-access-instructions-email-error-kphirl +bd168c62 Handle numeric SMTP_SECURE values +1f3b4681 Merge pull request #238 from Jacbdias/codex/fix-access-instructions-email-error +9dda0fcb (origin/codex/fix-access-instructions-email-error) Improve SMTP configuration validation +77ef450c Merge pull request #237 from Jacbdias/codex/implement-access-for-cf-renda-turbinada +301f9396 (origin/codex/implement-access-for-cf-renda-turbinada) Map Hotmart Renda Turbinada product to plan +ef8f4696 Merge pull request #235 from Jacbdias/codex/fix-exclusive-resources-on-cf-renda-turbinada +f324838f Merge pull request #236 from Jacbdias/codex/corrigir-erro-ao-mudar-plano-de-usuario +99b8acdd (origin/codex/fix-exclusive-resources-on-cf-renda-turbinada) Merge branch 'main' into codex/fix-exclusive-resources-on-cf-renda-turbinada +c491dbb6 (origin/codex/corrigir-erro-ao-mudar-plano-de-usuario) fix: normalizar permissões personalizadas ao atualizar usuário +485f0637 Merge pull request #234 from Jacbdias/codex/corrigir-permissoes-para-conteudo-cf-renda-passiva-lk8odh +8bbce2e0 Restrict CF Renda Turbinada resources to support +3bc921ce (origin/codex/corrigir-permissoes-para-conteudo-cf-renda-passiva-lk8odh) Restrict Turbinada exclusive resources to support +30d7ead4 Merge pull request #233 from Jacbdias/codex/corrigir-permissoes-para-conteudo-cf-renda-passiva-24vbtx +cd240e2d (origin/codex/corrigir-permissoes-para-conteudo-cf-renda-passiva-24vbtx) Merge branch 'main' into codex/corrigir-permissoes-para-conteudo-cf-renda-passiva-24vbtx +98193de7 Restrict Turbinada access to exclusive content +5e516bcd Merge pull request #232 from Jacbdias/codex/corrigir-permissoes-para-conteudo-cf-renda-passiva +357ea1bb (origin/codex/corrigir-permissoes-para-conteudo-cf-renda-passiva) Restrict Renda Passiva access and restore Bastidores icon +2283151a Merge pull request #231 from Jacbdias/codex/add-bastidores-da-bolsa-page +850a9f64 (origin/codex/add-bastidores-da-bolsa-page) Merge branch 'main' into codex/add-bastidores-da-bolsa-page +3a97aa2a feat: add Bastidores da Bolsa dashboard area with attachments +955a0550 Merge pull request #230 from Jacbdias/codex/remove-view-permission-for-reports +22eb6697 (origin/codex/remove-view-permission-for-reports) Remove weekly report and exclusive resources from Renda Turbinada +890da807 Merge pull request #229 from Jacbdias/codex/investigar-conteudo-bloqueado-no-dashboard-qtcb9s +0e246cec (origin/codex/investigar-conteudo-bloqueado-no-dashboard-qtcb9s) Remove Próximos passos card from CF Renda Turbinada lessons +b29836ed Merge pull request #228 from Jacbdias/codex/investigar-conteudo-bloqueado-no-dashboard +bb527626 (origin/codex/investigar-conteudo-bloqueado-no-dashboard) Expose scheduled CF Renda Turbinada modules as locked +d0cd6108 Update page.tsx +0e7cec33 Update page.tsx +4c11a915 Update page.tsx +c42aa4d8 Update page.tsx +af365eca Merge pull request #226 from Jacbdias/codex/add-live-event-calendar-to-dashboard +172a55de (origin/codex/add-live-event-calendar-to-dashboard) Add CF Renda Turbinada live events API +76d8b83b Update page.tsx +f121eac7 Update page.tsx +5ac111e9 Update page.tsx +30084a96 Update page.tsx +b92724ae Update page.tsx +0398c6d7 Update page.tsx +797d836c Update page.tsx +4cfedc81 Update page.tsx +f2a555d6 Merge pull request #225 from Jacbdias/codex/add-video-insertion-to-aula-module +f088b0d1 (origin/codex/add-video-insertion-to-aula-module) Embed lesson videos in CF Renda Turbinada modules +9e0d51ac Merge pull request #224 from Jacbdias/codex/implement-database-persistence-for-cfrendaturbinada +0651e021 (origin/codex/implement-database-persistence-for-cfrendaturbinada) feat: persist cf renda turbinada content +b60e0a25 Merge pull request #223 from Jacbdias/codex/add-content-visibility-options-for-admins-58ei9p +5f9ce054 (origin/codex/add-content-visibility-options-for-admins-58ei9p) Merge branch 'main' into codex/add-content-visibility-options-for-admins-58ei9p +54d71f3a Fix edit cancellation hook order +551c68c1 Merge pull request #222 from Jacbdias/codex/add-content-visibility-options-for-admins-hmsq3w +1db43007 (origin/codex/add-content-visibility-options-for-admins-hmsq3w) Merge branch 'main' into codex/add-content-visibility-options-for-admins-hmsq3w +a659e893 Add admin option to remove course modules +067e49f5 Merge pull request #221 from Jacbdias/codex/add-content-visibility-options-for-admins-636z8g +9864bfd5 (origin/codex/add-content-visibility-options-for-admins-636z8g) Merge branch 'main' into codex/add-content-visibility-options-for-admins-636z8g +ac94761f Fix duplicate lesson render return +cca9aa0d Add admin editing and deletion controls for lessons +5a4f7033 Update page.tsx +d748a7f9 Merge pull request #220 from Jacbdias/codex/add-content-visibility-options-for-admins-g3z1vf +29c97209 (origin/codex/add-content-visibility-options-for-admins-g3z1vf) feat: add admin controls to CF Renda Turbinada aulas +46085663 (origin/codex/add-content-visibility-options-for-admins) Merge pull request #219 from Jacbdias/codex/corrigir-dados-de-dy-12m-no-dashboard-h7kl68 +8922ddc7 (origin/codex/corrigir-dados-de-dy-12m-no-dashboard-h7kl68) Merge branch 'main' into codex/corrigir-dados-de-dy-12m-no-dashboard-h7kl68 +5a40de58 Refine dividend yield fallback heuristics +4914f2ed Merge pull request #218 from Jacbdias/codex/corrigir-dados-de-dy-12m-no-dashboard +6008e8a5 (origin/codex/corrigir-dados-de-dy-12m-no-dashboard) Fix dividend yield fetch fallback on settings dashboard +10059125 Update route.ts +77ae7925 Update route.ts +28495aa1 Update route.ts +d8ff214b Update route.ts +4ceeb701 Update route.ts +78f892d7 Merge pull request #217 from Jacbdias/codex/aumentar-velocidade-de-carregamento-da-pagina +e5c42235 (origin/codex/aumentar-velocidade-de-carregamento-da-pagina) Optimize DY fetching on settings dashboard +d678f4fd Merge pull request #216 from Jacbdias/codex/investigate-missing-dy-12m-data +3b3dff4d (origin/codex/investigate-missing-dy-12m-data) Align settings FII DY calculation with asset detail logic +87731f4d Create useHGBrasilAcoes +0cbf80d5 Merge pull request #215 from Jacbdias/codex/corrigir-erro-apresentado +1f69457b (origin/codex/corrigir-erro-apresentado) Merge branch 'main' into codex/corrigir-erro-apresentado +ce7a034f Separate type import for Vies override +0d20d7f6 Update page.tsx +a48bbb0f Update page.tsx +07d2a668 Merge pull request #214 from Jacbdias/codex/fix-invalid-summarydetail-module-for-brapi +9d91a3ee (origin/codex/fix-invalid-summarydetail-module-for-brapi) Atualizar integrações de DY para usar endpoints válidos da BRAPI +658e32f1 Merge pull request #213 from Jacbdias/codex/add-rate-limiting-for-api-requests-ix98c2 +c2e0993a (origin/codex/add-rate-limiting-for-api-requests-ix98c2) Merge branch 'main' into codex/add-rate-limiting-for-api-requests-ix98c2 +2edf2804 Improve rate limited fetch retries +acb1df1e Merge pull request #212 from Jacbdias/codex/add-rate-limiting-for-api-requests +7c9056ee (origin/codex/add-rate-limiting-for-api-requests) Add rate limiting to dividend yield fetches +c0ad97ba Merge pull request #211 from Jacbdias/codex/corrigir-dados-de-dy +d63a9e93 (origin/codex/corrigir-dados-de-dy) Expand DY parsing to hooks and company page +e285577a Merge pull request #210 from Jacbdias/codex/separar-etfs-em-tabela-distinta +65c65020 (origin/codex/separar-etfs-em-tabela-distinta) Separar ETFs em tabela dedicada no Projeto América +f7fcf1b8 Merge pull request #209 from Jacbdias/codex/organize-logos-in-a-folder-48fg3f +dcf4f3f1 (origin/codex/organize-logos-in-a-folder-48fg3f) Merge branch 'main' into codex/organize-logos-in-a-folder-48fg3f +132d70d9 Show local logos for closed FII positions +80e96238 Merge pull request #208 from Jacbdias/codex/organize-logos-in-a-folder +5d26d757 (origin/codex/organize-logos-in-a-folder) Fix FII logo path to keep ticker digits +7fb6cb2c Update page.tsx +f96c1138 Delete public/assets/BCRI11.png +fcb6912d Delete public/assets/BCRI11.png +fd79d0b4 Delete public/assets/BCIA11.png +ea652d5b Delete public/assets/ALZR11.png +150e1f19 Delete public/assets/AFHI11.png +486da430 Merge pull request #207 from Jacbdias/codex/align-card-layout-to-customers-page +68d0018e (origin/codex/align-card-layout-to-customers-page) Align settings dashboard cards with customers layout +39a78c67 Merge pull request #206 from Jacbdias/codex/fix-deployment-error-due-to-duplicate-imports +4b16b82c (origin/codex/fix-deployment-error-due-to-duplicate-imports) Deduplicate vies utility imports +671f28c4 Merge pull request #205 from Jacbdias/codex/adicionar-novo-formato-de-analise-de-vies-933loh +98987b15 (origin/codex/adicionar-novo-formato-de-analise-de-vies-933loh) Fix duplicate viés helper imports +82c29ba7 Merge pull request #204 from Jacbdias/codex/adicionar-novo-formato-de-analise-de-vies-mkxdip +1b11ecfa (origin/codex/adicionar-novo-formato-de-analise-de-vies-mkxdip) Fix viés override desync on asset detail +93ed1f6e Merge pull request #203 from Jacbdias/codex/fix-missing-asset-composition-chart +079a92c6 (origin/codex/fix-missing-asset-composition-chart) Add closed positions and composition sections to settings +805f5e0a Merge pull request #202 from Jacbdias/codex/adicionar-novo-formato-de-analise-de-vies +43af3ef1 (origin/codex/adicionar-novo-formato-de-analise-de-vies) Align asset detail bias display with global override logic +7a5fc921 Merge pull request #201 from Jacbdias/codex/update-text-color-and-background-g1yaex +6a7b15a4 (origin/codex/update-text-color-and-background-g1yaex) Ajusta cores do viés 'Manter' para cinza +c0ffff9f Merge pull request #200 from Jacbdias/codex/corrigir-error-apresentado +876d64ec (origin/codex/corrigir-error-apresentado) Fix hook order in DataStore provider +2608765a Merge pull request #199 from Jacbdias/codex/add-manual-bias-field-and-api +39ec9b7b (origin/codex/add-manual-bias-field-and-api) Finalize manual bias override support +b101938e Merge pull request #198 from Jacbdias/codex/find-etf-identification-in-page.tsx +ef4a46f8 (origin/codex/find-etf-identification-in-page.tsx) Fix ETF detection when BDR mapping exists +c4e38bac Merge pull request #197 from Jacbdias/codex/remover-card-de-analise-de-vies-para-etf-nfoikk +44ad7afc (origin/codex/remover-card-de-analise-de-vies-para-etf-nfoikk) Merge branch 'main' into codex/remover-card-de-analise-de-vies-para-etf-nfoikk +6b73845f Fix ETF bias card visibility +c5898153 Merge pull request #196 from Jacbdias/codex/remover-card-de-analise-de-vies-para-etf +0fb3e8da (origin/codex/remover-card-de-analise-de-vies-para-etf) Hide bias analysis card for ETFs +dcc4aade Merge pull request #195 from Jacbdias/codex/add-inline-text-editing-tools-94845z +fde6cce9 (origin/codex/add-inline-text-editing-tools-94845z) Merge branch 'main' into codex/add-inline-text-editing-tools-94845z +d8df0541 Fix image resize slider interaction +94e50ac1 Merge pull request #194 from Jacbdias/codex/add-inline-text-editing-tools +15965ed4 (origin/codex/add-inline-text-editing-tools) Enhance rich text editor usability +bdf9347c Merge pull request #193 from Jacbdias/codex/add-bdr-ceiling-price-to-dashboard/ativo-qrpwy9 +3ff358b2 (origin/codex/add-bdr-ceiling-price-to-dashboard/ativo-qrpwy9) Ajustar exibição de Dividend Yield zerado para FIIs +ae1435a4 Merge pull request #192 from Jacbdias/codex/add-bdr-ceiling-price-to-dashboard/ativo-zbg455 +29cb1c95 (origin/codex/add-bdr-ceiling-price-to-dashboard/ativo-zbg455) Remover fonte das métricas de valuation +12520d2b Merge pull request #191 from Jacbdias/codex/add-bdr-ceiling-price-to-dashboard/ativo-pxwmfd +97b16358 (origin/codex/add-bdr-ceiling-price-to-dashboard/ativo-pxwmfd) Mover percentual da carteira para os dados da posição +0a0f057f Merge pull request #190 from Jacbdias/codex/add-bdr-ceiling-price-to-dashboard/ativo +3701859f (origin/codex/add-bdr-ceiling-price-to-dashboard/ativo) Exibir preço teto do BDR no painel do ativo +2475845a Update page.tsx +c53f67a6 Update useAtivoDetalhes.ts +98956845 Merge pull request #189 from Jacbdias/codex/investigar-dados-bdr-ausentes-no-dashboard-u05g9p +35c27182 (origin/codex/investigar-dados-bdr-ausentes-no-dashboard-u05g9p) Melhora exibição de dados de BDR +240aed95 Merge pull request #188 from Jacbdias/codex/investigar-dados-bdr-ausentes-no-dashboard-ubq6om +41335571 (origin/codex/investigar-dados-bdr-ausentes-no-dashboard-ubq6om) Merge branch 'main' into codex/investigar-dados-bdr-ausentes-no-dashboard-ubq6om +3e748cb1 Adiciona mapeamento de BDR para STX +d723f3dc Merge pull request #187 from Jacbdias/codex/investigar-dados-bdr-ausentes-no-dashboard +a782f023 (origin/codex/investigar-dados-bdr-ausentes-no-dashboard) Atualiza mapeamento de BDRs com novos tickers +181dad89 Update page.tsx +6598389b Update page.tsx +48411e0e Update page.tsx +74fd713f Update page.tsx +4111d8b6 Update page.tsx +9edfed8f Update page.tsx +f264a718 \ No newline at end of file diff --git a/fix-password.js b/fix-password.js new file mode 100644 index 000000000..84d13f8ed --- /dev/null +++ b/fix-password.js @@ -0,0 +1,27 @@ +const { PrismaClient } = require('@prisma/client'); +const bcrypt = require('bcrypt'); + +const prisma = new PrismaClient(); + +async function fixPassword() { + try { + const hashedPassword = await bcrypt.hash('admin123', 10); + + await prisma.user.updateMany({ + where: { + email: { in: ['admin@fatosdobolsa.com', 'admin@sistema.local'] } + }, + data: { password: hashedPassword } + }); + + console.log('✅ Senhas atualizadas!'); + console.log('🔑 Use: admin123'); + + } catch (error) { + console.error('⌠Erro:', error); + } finally { + await prisma.$disconnect(); + } +} + +fixPassword(); \ No newline at end of file diff --git a/git b/git new file mode 100644 index 000000000..e69de29bb diff --git a/hooks/optimized/optimized/useAtivoCompleto.ts b/hooks/optimized/optimized/useAtivoCompleto.ts new file mode 100644 index 000000000..8d1c8b69c --- /dev/null +++ b/hooks/optimized/optimized/useAtivoCompleto.ts @@ -0,0 +1 @@ + diff --git a/hooks/optimized/useAtivoCompleto.js b/hooks/optimized/useAtivoCompleto.js new file mode 100644 index 000000000..8d1c8b69c --- /dev/null +++ b/hooks/optimized/useAtivoCompleto.js @@ -0,0 +1 @@ + diff --git a/hooks/useDataStore.jsdir b/hooks/useDataStore.jsdir new file mode 100644 index 000000000..e69de29bb diff --git a/hooks/usePerformanceCalculator.js b/hooks/usePerformanceCalculator.js new file mode 100644 index 000000000..1769535c6 --- /dev/null +++ b/hooks/usePerformanceCalculator.js @@ -0,0 +1,176 @@ +/** + * 🎯 HOOK CUSTOMIZADO PARA CÃLCULO DE PERFORMANCE + * Gerencia estado, cache e integração com Total Return Calculator + */ + +import { useState, useEffect, useMemo } from 'react'; +import { calcularPerformanceCarteira } from '../utils/totalReturnCalculator'; + +export const usePerformanceCalculator = (dadosCarteira, cotacoesAtuais = {}, valorPorAtivo = 1000) => { + const [loading, setLoading] = useState(false); + const [erro, setErro] = useState(null); + const [ultimaAtualizacao, setUltimaAtualizacao] = useState(null); + + // 🔄 CALCULAR PERFORMANCE COM CACHE INTELIGENTE + const metricas = useMemo(() => { + if (!dadosCarteira || dadosCarteira.length === 0) { + return { + valorInicial: 0, + valorFinal: 0, + rentabilidadeTotal: 0, + rentabilidadeAnualizada: 0, + totalProventos: 0, + quantidadeAtivos: 0, + ativosComCalculos: [], + melhorAtivo: null, + piorAtivo: null, + metodo: 'Total Return Contínuo', + loading: false, + erro: null + }; + } + + console.log('🎯 Hook: Recalculando performance da carteira...'); + setLoading(true); + setErro(null); + + try { + const resultado = calcularPerformanceCarteira(dadosCarteira, cotacoesAtuais, valorPorAtivo); + + setUltimaAtualizacao(new Date()); + setLoading(false); + + return { + ...resultado, + loading: false, + erro: null, + ultimaAtualizacao: new Date() + }; + + } catch (error) { + console.error('⌠Hook: Erro ao calcular performance:', error); + setErro(error.message); + setLoading(false); + + return { + valorInicial: 0, + valorFinal: 0, + rentabilidadeTotal: 0, + rentabilidadeAnualizada: 0, + totalProventos: 0, + quantidadeAtivos: 0, + ativosComCalculos: [], + melhorAtivo: null, + piorAtivo: null, + metodo: 'Total Return Contínuo', + loading: false, + erro: error.message + }; + } + }, [dadosCarteira, cotacoesAtuais, valorPorAtivo]); + + // 🚀 FUNÇÃO PARA FORÇAR RECÃLCULO + const recalcular = () => { + console.log('🔄 Hook: Forçando recálculo...'); + setUltimaAtualizacao(new Date()); + }; + + // 📊 FUNÇÕES DE FORMATAÇÃO + const formatCurrency = (value, moeda = 'BRL') => { + const currency = moeda === 'USD' ? 'USD' : 'BRL'; + const locale = moeda === 'USD' ? 'en-US' : 'pt-BR'; + return new Intl.NumberFormat(locale, { + style: 'currency', + currency: currency, + minimumFractionDigits: 2 + }).format(value); + }; + + const formatPercentage = (value) => { + const signal = value >= 0 ? '+' : ''; + return signal + value.toFixed(1) + '%'; + }; + + // 📈 MÉTRICAS FORMATADAS PARA EXIBIÇÃO + const metricasFormatadas = useMemo(() => ({ + ...metricas, + valorInicialFormatado: formatCurrency(metricas.valorInicial), + valorFinalFormatado: formatCurrency(metricas.valorFinal), + rentabilidadeTotalFormatada: formatPercentage(metricas.rentabilidadeTotal), + rentabilidadeAnualizadaFormatada: formatPercentage(metricas.rentabilidadeAnualizada), + totalProventosFormatado: formatCurrency(metricas.totalProventos), + ganhoTotalFormatado: formatCurrency(metricas.valorFinal - metricas.valorInicial), + + // Formatação dos ativos individuais + ativosComCalculosFormatados: metricas.ativosComCalculos?.map(ativo => ({ + ...ativo, + valorInicialFormatado: formatCurrency(ativo.valorInicial), + valorFinalFormatado: formatCurrency(ativo.valorFinal), + rentabilidadeTotalFormatada: formatPercentage(ativo.rentabilidadeTotal), + rentabilidadeAnualizadaFormatada: formatPercentage(ativo.rentabilidadeAnualizada), + totalProventosFormatado: formatCurrency(ativo.totalProventos), + cotaFinalFormatada: formatCurrency(ativo.cotaFinal) + })) || [] + }), [metricas]); + + // 🎯 ESTATÃSTICAS ADICIONAIS + const estatisticas = useMemo(() => { + if (!metricas.ativosComCalculos || metricas.ativosComCalculos.length === 0) { + return { + ativosPositivos: 0, + ativosNegativos: 0, + percentualPositivos: 0, + mediaRentabilidade: 0, + medianRentabilidade: 0, + volatilidade: 0 + }; + } + + const rentabilidades = metricas.ativosComCalculos.map(a => a.rentabilidadeTotal); + const ativosPositivos = rentabilidades.filter(r => r > 0).length; + const ativosNegativos = rentabilidades.filter(r => r < 0).length; + + const media = rentabilidades.reduce((acc, r) => acc + r, 0) / rentabilidades.length; + + const rentabilidadesOrdenadas = [...rentabilidades].sort((a, b) => a - b); + const mediana = rentabilidadesOrdenadas.length % 2 === 0 + ? (rentabilidadesOrdenadas[rentabilidadesOrdenadas.length / 2 - 1] + rentabilidadesOrdenadas[rentabilidadesOrdenadas.length / 2]) / 2 + : rentabilidadesOrdenadas[Math.floor(rentabilidadesOrdenadas.length / 2)]; + + // Volatilidade (desvio padrão) + const variancia = rentabilidades.reduce((acc, r) => acc + Math.pow(r - media, 2), 0) / rentabilidades.length; + const volatilidade = Math.sqrt(variancia); + + return { + ativosPositivos, + ativosNegativos, + percentualPositivos: (ativosPositivos / rentabilidades.length) * 100, + mediaRentabilidade: media, + medianRentabilidade: mediana, + volatilidade: volatilidade, + sharpeRatio: volatilidade > 0 ? media / volatilidade : 0 // Simplificado + }; + }, [metricas]); + + return { + // Métricas principais + metricas: metricasFormatadas, + + // Estados + loading: loading || metricas.loading, + erro: erro || metricas.erro, + ultimaAtualizacao, + + // Estatísticas adicionais + estatisticas, + + // Funções utilitárias + recalcular, + formatCurrency, + formatPercentage, + + // Informações do método + metodo: 'Total Return Contínuo com Reinvestimento', + versao: '2.0.0' + }; +}; \ No newline at end of file diff --git a/ismobile_usage.txt b/ismobile_usage.txt new file mode 100644 index 000000000..30e84da86 --- /dev/null +++ b/ismobile_usage.txt @@ -0,0 +1,15 @@ +src\app\dashboard\empresa\[ticker]\page.tsx:const isMobile = () => { +src\app\dashboard\empresa\[ticker]\page.tsx: if (isMobile() && typeof window !== 'undefined') { +src\app\dashboard\empresa\[ticker]\page.tsx: const tentarAPI = !isMobile() || +src\app\dashboard\empresa\[ticker]\page.tsx: const isGoodConnection = !isMobile() || +src\app\dashboard\empresa\[ticker]\page.tsx: logMobile(`â±ï¸ Timeout de API (${isMobile() ? '2s' : '8s'})`); +src\app\dashboard\empresa\[ticker]\page.tsx: }, isMobile() ? 2000 : 8000); +src\app\dashboard\empresa\[ticker]\page.tsx: 'User-Agent': isMobile() ? 'Portfolio-Mobile/1.0' : 'Portfolio-Desktop/1.0', +src\app\dashboard\empresa\[ticker]\page.tsx: 'Cache-Control': isMobile() ? 'max-age=3600' : 'no-cache' +src\app\dashboard\empresa\[ticker]\page.tsx: if (!isMobile()) return null; +src\layouts\Dashboard\index.jsx: const isMobile = ['xs', 'sm', 'md'].includes(props.width); +src\layouts\Dashboard\index.jsx: isOpen: !isMobile +src\layouts\Dashboard\index.jsx: const isMobile = ['xs', 'sm', 'md'].includes(width); +src\layouts\Dashboard\index.jsx: const shiftTopbar = isOpen && !isMobile; +src\layouts\Dashboard\index.jsx: const shiftContent = isOpen && !isMobile; +src\layouts\Dashboard\index.jsx: variant={isMobile ? 'temporary' : 'persistent'} diff --git a/item.id) b/item.id) new file mode 100644 index 000000000..e69de29bb diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 000000000..d58a056f5 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,17 @@ +const nextJest = require('next/jest'); + +const createJestConfig = nextJest({ + dir: './' +}); + +const customJestConfig = { + testEnvironment: 'node', + testMatch: ['**/src/**/*.test.{js,jsx,ts,tsx}'], + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + '^canvas$': '/tests/__mocks__/canvasMock.js' + }, + setupFiles: ['/jest.setup.js'] +}; + +module.exports = createJestConfig(customJestConfig); diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 000000000..6f487400f --- /dev/null +++ b/jest.setup.js @@ -0,0 +1 @@ +jest.mock('canvas', () => ({})); diff --git a/migration_manual.sql b/migration_manual.sql new file mode 100644 index 000000000..682594f3c --- /dev/null +++ b/migration_manual.sql @@ -0,0 +1,32 @@ +-- Migration Manual: Adicionar campos de pagamento e tabela de migração +-- Execute este SQL diretamente no seu banco de dados PostgreSQL + +-- 1. Adicionar novos campos na tabela Purchase +ALTER TABLE "Purchase" ADD COLUMN IF NOT EXISTS "paymentMethod" TEXT; +ALTER TABLE "Purchase" ADD COLUMN IF NOT EXISTS "installments" INTEGER NOT NULL DEFAULT 1; +ALTER TABLE "Purchase" ADD COLUMN IF NOT EXISTS "platform" TEXT; + +-- 2. Criar tabela PlanMigration +CREATE TABLE IF NOT EXISTS "plan_migrations" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "planOriginal" TEXT NOT NULL, + "planNovo" TEXT NOT NULL, + "valorOriginal" DOUBLE PRECISION NOT NULL, + "valorNovo" DOUBLE PRECISION NOT NULL, + "mesesRestantes" INTEGER NOT NULL, + "valorDesconto" DOUBLE PRECISION NOT NULL, + "valorFinal" DOUBLE PRECISION NOT NULL, + "status" TEXT NOT NULL DEFAULT 'PENDING', + "observacoes" TEXT, + "processadoPor" TEXT NOT NULL, + "dataCriacao" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "dataProcessamento" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "plan_migrations_pkey" PRIMARY KEY ("id") +); + +-- 3. Exibir confirmação +SELECT 'Migration aplicada com sucesso!' as status; diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 000000000..4f11a03dc --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/next.config.js b/next.config.js new file mode 100644 index 000000000..c564dcc14 --- /dev/null +++ b/next.config.js @@ -0,0 +1,77 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + eslint: { + ignoreDuringBuilds: true, + dirs: ['src'], + }, + typescript: { + ignoreBuildErrors: true, + }, + + // ✅ ADICIONAR ISSO - Force new bundle generation + generateBuildId: async () => { + return `build-${Date.now()}` + }, + + experimental: { + forceSwcTransforms: true, + serverComponentsExternalPackages: ['sharp'], + serverActions: { + bodySizeLimit: '12mb', + }, + }, + + images: { + domains: [ + 'raw.githubusercontent.com', + 'brapi.dev', + ], + }, + + api: { + bodyParser: { + sizeLimit: '12mb', + }, + responseLimit: false, + }, + + serverRuntimeConfig: { + maxFileSize: '12mb' + }, + + async headers() { + return [ + { + source: '/api/(auth|user|admin|carteiras|agenda|reports|relatorios|notifications|answers|questions|webhooks|hotmart|instagram-cadastro|meus-ativos|report-files|generate-pdf|cron|faq|check-admin)/:path*', + headers: [ + { + key: 'Cache-Control', + value: 'no-store, must-revalidate', + }, + ], + }, + { + source: '/api/carteiras/upload', + headers: [ + { + key: 'Access-Control-Allow-Origin', + value: '*' + }, + { + key: 'Access-Control-Allow-Methods', + value: 'POST, OPTIONS' + }, + { + key: 'Access-Control-Allow-Headers', + value: 'Content-Type, Authorization' + }, + { + key: 'Cache-Control', + value: 'no-store, must-revalidate' + } + ] + } + ]; + }, +} +module.exports = nextConfig; diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 000000000..836e4e756 --- /dev/null +++ b/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const config = {}; + +export default config; diff --git a/notepad b/notepad new file mode 100644 index 000000000..e69de29bb diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..95db75da5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,16148 @@ +{ + "name": "fatos-da-bolsa-hub", + "version": "4.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fatos-da-bolsa-hub", + "version": "4.0.0", + "hasInstallScript": true, + "dependencies": { + "@aws-sdk/client-s3": "^3.x", + "@emotion/cache": "11.11.0", + "@emotion/react": "11.11.4", + "@emotion/server": "11.11.0", + "@emotion/styled": "11.11.5", + "@fontsource/inter": "5.0.18", + "@fontsource/plus-jakarta-sans": "5.0.20", + "@fontsource/roboto-mono": "5.0.18", + "@hookform/resolvers": "3.6.0", + "@mui/icons-material": "^7.1.2", + "@mui/lab": "5.0.0-alpha.170", + "@mui/material": "5.15.20", + "@mui/system": "5.15.20", + "@mui/utils": "5.15.20", + "@mui/x-date-pickers": "7.7.1", + "@phosphor-icons/react": "2.1.6", + "@prisma/client": "^6.10.1", + "@tanstack/react-query": "^5.83.0", + "@upstash/redis": "^1.35.3", + "apexcharts": "3.49.2", + "bcryptjs": "^3.0.2", + "dayjs": "1.11.11", + "dotenv": "^17.0.0", + "file-saver": "^2.0.5", + "jsonwebtoken": "^9.0.2", + "lucide-react": "^0.525.0", + "next": "14.2.4", + "nodemailer": "^7.0.4", + "openai": "^5.12.0", + "pdfjs-dist": "^3.11.174", + "puppeteer": "^24.11.2", + "react": "18.3.1", + "react-apexcharts": "1.4.1", + "react-dom": "18.3.1", + "react-dropzone": "^14.3.8", + "react-hook-form": "7.52.0", + "recharts": "2.12.7", + "xlsx": "^0.18.5", + "zod": "3.23.8" + }, + "devDependencies": { + "@ianvs/prettier-plugin-sort-imports": "4.2.1", + "@testing-library/jest-dom": "6.4.6", + "@testing-library/react": "16.0.0", + "@types/jest": "29.5.12", + "@types/mapbox-gl": "3.1.0", + "@types/node": "20.14.9", + "@types/nodemailer": "^6.4.17", + "@types/react": "18.3.3", + "@types/react-dom": "18.3.0", + "@types/react-syntax-highlighter": "15.5.13", + "@vercel/style-guide": "6.0.0", + "autoprefixer": "^10.4.21", + "eslint": "8.57.0", + "eslint-config-next": "14.2.4", + "eslint-config-prettier": "9.1.0", + "jest": "29.7.0", + "jest-environment-jsdom": "29.7.0", + "postcss": "^8.5.6", + "prettier": "3.3.2", + "prisma": "^6.10.1", + "tailwindcss": "^4.1.11", + "typescript": "5.5.2" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.3.tgz", + "integrity": "sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.925.0.tgz", + "integrity": "sha512-imAul+6pyJYH4cbxPz1OiFXxrKKTRqVzlT2e0M6zbPHmUcJsF5E+b+4qvHQChU8wFGtIWJHH/JChF2ibfTnXdA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/credential-provider-node": "3.925.0", + "@aws-sdk/middleware-bucket-endpoint": "3.922.0", + "@aws-sdk/middleware-expect-continue": "3.922.0", + "@aws-sdk/middleware-flexible-checksums": "3.922.0", + "@aws-sdk/middleware-host-header": "3.922.0", + "@aws-sdk/middleware-location-constraint": "3.922.0", + "@aws-sdk/middleware-logger": "3.922.0", + "@aws-sdk/middleware-recursion-detection": "3.922.0", + "@aws-sdk/middleware-sdk-s3": "3.922.0", + "@aws-sdk/middleware-ssec": "3.922.0", + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/region-config-resolver": "3.925.0", + "@aws-sdk/signature-v4-multi-region": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@aws-sdk/util-user-agent-browser": "3.922.0", + "@aws-sdk/util-user-agent-node": "3.922.0", + "@aws-sdk/xml-builder": "3.921.0", + "@smithy/config-resolver": "^4.4.2", + "@smithy/core": "^3.17.2", + "@smithy/eventstream-serde-browser": "^4.2.4", + "@smithy/eventstream-serde-config-resolver": "^4.3.4", + "@smithy/eventstream-serde-node": "^4.2.4", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/hash-blob-browser": "^4.2.5", + "@smithy/hash-node": "^4.2.4", + "@smithy/hash-stream-node": "^4.2.4", + "@smithy/invalid-dependency": "^4.2.4", + "@smithy/md5-js": "^4.2.4", + "@smithy/middleware-content-length": "^4.2.4", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-retry": "^4.4.6", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.5", + "@smithy/util-defaults-mode-node": "^4.2.8", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/util-stream": "^4.5.5", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.4", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.925.0.tgz", + "integrity": "sha512-ixC9CyXe/mBo1X+bzOxIIzsdBYzM+klWoHUYzwnPMrXhpDrMjj8D24R/FPqrDnhoYYXiyS4BApRLpeymsFJq2Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/middleware-host-header": "3.922.0", + "@aws-sdk/middleware-logger": "3.922.0", + "@aws-sdk/middleware-recursion-detection": "3.922.0", + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/region-config-resolver": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@aws-sdk/util-user-agent-browser": "3.922.0", + "@aws-sdk/util-user-agent-node": "3.922.0", + "@smithy/config-resolver": "^4.4.2", + "@smithy/core": "^3.17.2", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/hash-node": "^4.2.4", + "@smithy/invalid-dependency": "^4.2.4", + "@smithy/middleware-content-length": "^4.2.4", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-retry": "^4.4.6", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.5", + "@smithy/util-defaults-mode-node": "^4.2.8", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.922.0.tgz", + "integrity": "sha512-EvfP4cqJfpO3L2v5vkIlTkMesPtRwWlMfsaW6Tpfm7iYfBOuTi6jx60pMDMTyJNVfh6cGmXwh/kj1jQdR+w99Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@aws-sdk/xml-builder": "3.921.0", + "@smithy/core": "^3.17.2", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/signature-v4": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.922.0.tgz", + "integrity": "sha512-WikGQpKkROJSK3D3E7odPjZ8tU7WJp5/TgGdRuZw3izsHUeH48xMv6IznafpRTmvHcjAbDQj4U3CJZNAzOK/OQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.922.0.tgz", + "integrity": "sha512-i72DgHMK7ydAEqdzU0Duqh60Q8W59EZmRJ73y0Y5oFmNOqnYsAI+UXyOoCsubp+Dkr6+yOwAn1gPt1XGE9Aowg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-stream": "^4.5.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.925.0.tgz", + "integrity": "sha512-TOs/UkKWwXrSPolRTChpDUQjczw6KqbbanF0EzjUm3sp/AS1ThOQCKuTTdaOBZXkCIJdvRmZjF3adccE3rAoXg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/credential-provider-env": "3.922.0", + "@aws-sdk/credential-provider-http": "3.922.0", + "@aws-sdk/credential-provider-process": "3.922.0", + "@aws-sdk/credential-provider-sso": "3.925.0", + "@aws-sdk/credential-provider-web-identity": "3.925.0", + "@aws-sdk/nested-clients": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@smithy/credential-provider-imds": "^4.2.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.925.0.tgz", + "integrity": "sha512-+T9mnnTY73MLkVxsk5RtzE4fv7GnMhR7iXhL/yTusf1zLfA09uxlA9VCz6tWxm5rHcO4ZN0x4hnqqDhM+DB5KQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.922.0", + "@aws-sdk/credential-provider-http": "3.922.0", + "@aws-sdk/credential-provider-ini": "3.925.0", + "@aws-sdk/credential-provider-process": "3.922.0", + "@aws-sdk/credential-provider-sso": "3.925.0", + "@aws-sdk/credential-provider-web-identity": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@smithy/credential-provider-imds": "^4.2.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.922.0.tgz", + "integrity": "sha512-1DZOYezT6okslpvMW7oA2q+y17CJd4fxjNFH0jtThfswdh9CtG62+wxenqO+NExttq0UMaKisrkZiVrYQBTShw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.925.0.tgz", + "integrity": "sha512-aZlUC6LRsOMDvIu0ifF62mTjL3KGzclWu5XBBN8eLDAYTdhqMxv3HyrqWoiHnGZnZGaVU+II+qsVoeBnGOwHow==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.925.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/token-providers": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.925.0.tgz", + "integrity": "sha512-dR34s8Sfd1wJBzIuvRFO2FCnLmYD8iwPWrdXWI2ZypFt1EQR8jeQ20mnS+UOCoR5Z0tY6wJqEgTXKl4KuZ+DUg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/nested-clients": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.922.0.tgz", + "integrity": "sha512-Dpr2YeOaLFqt3q1hocwBesynE3x8/dXZqXZRuzSX/9/VQcwYBFChHAm4mTAl4zuvArtDbLrwzWSxmOWYZGtq5w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "@smithy/util-config-provider": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.922.0.tgz", + "integrity": "sha512-xmnLWMtmHJHJBupSWMUEW1gyxuRIeQ1Ov2xa8Tqq77fPr4Ft2AluEwiDMaZIMHoAvpxWKEEt9Si59Li7GIA+bQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.922.0.tgz", + "integrity": "sha512-G363np7YcJhf+gBucskdv8cOTbs2TRwocEzRupuqDIooGDlLBlfJrvwehdgtWR8l53yjJR3zcHvGrVPTe2h8Nw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-stream": "^4.5.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.922.0.tgz", + "integrity": "sha512-HPquFgBnq/KqKRVkiuCt97PmWbKtxQ5iUNLEc6FIviqOoZTmaYG3EDsIbuFBz9C4RHJU4FKLmHL2bL3FEId6AA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.922.0.tgz", + "integrity": "sha512-T4iqd7WQ2DDjCH/0s50mnhdoX+IJns83ZE+3zj9IDlpU0N2aq8R91IG890qTfYkUEdP9yRm0xir/CNed+v6Dew==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.922.0.tgz", + "integrity": "sha512-AkvYO6b80FBm5/kk2E636zNNcNgjztNNUxpqVx+huyGn9ZqGTzS4kLqW2hO6CBe5APzVtPCtiQsXL24nzuOlAg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.922.0.tgz", + "integrity": "sha512-TtSCEDonV/9R0VhVlCpxZbp/9sxQvTTRKzIf8LxW3uXpby6Wl8IxEciBJlxmSkoqxh542WRcko7NYODlvL/gDA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@aws/lambda-invoke-store": "^0.1.1", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.922.0.tgz", + "integrity": "sha512-ygg8lME1oFAbsH42ed2wtGqfHLoT5irgx6VC4X98j79fV1qXEwwwbqMsAiMQ/HJehpjqAFRVsHox3MHLN48Z5A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/core": "^3.17.2", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/signature-v4": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-stream": "^4.5.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.922.0.tgz", + "integrity": "sha512-eHvSJZTSRJO+/tjjGD6ocnPc8q9o3m26+qbwQTu/4V6yOJQ1q+xkDZNqwJQphL+CodYaQ7uljp8g1Ji/AN3D9w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.922.0.tgz", + "integrity": "sha512-N4Qx/9KP3oVQBJOrSghhz8iZFtUC2NNeSZt88hpPhbqAEAtuX8aD8OzVcpnAtrwWqy82Yd2YTxlkqMGkgqnBsQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@smithy/core": "^3.17.2", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.925.0.tgz", + "integrity": "sha512-Fc8QhH+1YzGQb5aWQUX6gRnKSzUZ9p3p/muqXIgYBL8RSd5O6hSPhDTyrOWE247zFlOjVlAlEnoTMJKarH0cIA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/middleware-host-header": "3.922.0", + "@aws-sdk/middleware-logger": "3.922.0", + "@aws-sdk/middleware-recursion-detection": "3.922.0", + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/region-config-resolver": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@aws-sdk/util-user-agent-browser": "3.922.0", + "@aws-sdk/util-user-agent-node": "3.922.0", + "@smithy/config-resolver": "^4.4.2", + "@smithy/core": "^3.17.2", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/hash-node": "^4.2.4", + "@smithy/invalid-dependency": "^4.2.4", + "@smithy/middleware-content-length": "^4.2.4", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-retry": "^4.4.6", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.5", + "@smithy/util-defaults-mode-node": "^4.2.8", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.925.0.tgz", + "integrity": "sha512-FOthcdF9oDb1pfQBRCfWPZhJZT5wqpvdAS5aJzB1WDZ+6EuaAhLzLH/fW1slDunIqq1PSQGG3uSnVglVVOvPHQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/config-resolver": "^4.4.2", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.922.0.tgz", + "integrity": "sha512-mmsgEEL5pE+A7gFYiJMDBCLVciaXq4EFI5iAP7bPpnHvOplnNOYxVy2IreKMllGvrfjVyLnwxzZYlo5zZ65FWg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/protocol-http": "^5.3.4", + "@smithy/signature-v4": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.925.0.tgz", + "integrity": "sha512-F4Oibka1W5YYDeL+rGt/Hg3NLjOzrJdmuZOE0OFQt/U6dnJwYmYi2gFqduvZnZcD1agNm37mh7/GUq1zvKS6ig==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/nested-clients": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.922.0.tgz", + "integrity": "sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.893.0.tgz", + "integrity": "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.922.0.tgz", + "integrity": "sha512-4ZdQCSuNMY8HMlR1YN4MRDdXuKd+uQTeKIr5/pIM+g3TjInZoj8imvXudjcrFGA63UF3t92YVTkBq88mg58RXQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-endpoints": "^3.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", + "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.922.0.tgz", + "integrity": "sha512-qOJAERZ3Plj1st7M4Q5henl5FRpE30uLm6L9edZqZXGR6c7ry9jzexWamWVpQ4H4xVAVmiO9dIEBAfbq4mduOA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.922.0.tgz", + "integrity": "sha512-NrPe/Rsr5kcGunkog0eBV+bY0inkRELsD2SacC4lQZvZiXf8VJ2Y7j+Yq1tB+h+FPLsdt3v9wItIvDf/laAm0Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.921.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.921.0.tgz", + "integrity": "sha512-LVHg0jgjyicKKvpNIEMXIMr1EBViESxcPkqfOlT+X1FkmUMTNZEEVF18tOJg4m4hV5vxtkWcqtr4IEeWa1C41Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.1.1.tgz", + "integrity": "sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.7.tgz", + "integrity": "sha512-xgu/ySj2mTiUFmdE9yCMfBxLp4DHd5DwmbbD05YAuICfodYT3VvRxbrh81LGQ/8UpSdtMdfKMn3KouYDX59DGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.7.tgz", + "integrity": "sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.27.7", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.7", + "@babel/types": "^7.27.7", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/eslint-parser": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.27.5.tgz", + "integrity": "sha512-HLkYQfRICudzcOtjGwkPvGc5nF1b4ljLZh1IRDj50lRZ718NAKVgQpIAUX8bfg6u/yuSKY3L7E0YzIV+OxrB8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0", + "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/@babel/eslint-parser/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.7.tgz", + "integrity": "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.7.tgz", + "integrity": "sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.5", + "@babel/parser": "^7.27.7", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.7.tgz", + "integrity": "sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emnapi/core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", + "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz", + "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", + "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/is-prop-valid/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz", + "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/serialize/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/server": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/server/-/server-11.11.0.tgz", + "integrity": "sha512-6q89fj2z8VBTx9w93kJ5n51hsmtYuFPtZgnc1L8VzRx9ti4EU6EyvF6Nn1H1x3vcCQCF7u2dB2lY4AYJwUW4PA==", + "license": "MIT", + "dependencies": { + "@emotion/utils": "^1.2.1", + "html-tokenize": "^2.0.0", + "multipipe": "^1.0.2", + "through": "^2.3.8" + }, + "peerDependencies": { + "@emotion/css": "^11.0.0-rc.0" + }, + "peerDependenciesMeta": { + "@emotion/css": { + "optional": true + } + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.11.5", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.5.tgz", + "integrity": "sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/is-prop-valid": "^1.2.2", + "@emotion/serialize": "^1.1.4", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==", + "license": "MIT" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz", + "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz", + "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.2", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.4.tgz", + "integrity": "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.2" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@fontsource/inter": { + "version": "5.0.18", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.0.18.tgz", + "integrity": "sha512-YCsoYPTcs713sI7tLtxaPrIhXAXvEetGg5Ry02ivA8qUOb3fQHojbK/X9HLD5OOKvFUNR2Ynkwb1kR1hVKQHpw==", + "license": "OFL-1.1" + }, + "node_modules/@fontsource/plus-jakarta-sans": { + "version": "5.0.20", + "resolved": "https://registry.npmjs.org/@fontsource/plus-jakarta-sans/-/plus-jakarta-sans-5.0.20.tgz", + "integrity": "sha512-a887FrG31RWqliRg1jXZM8tjzqee2NlO3Vuc5uGdws2EFvOg46P9VMaeS5eM3jihFq8Kj0zUQD0GYQqcxdkfCA==", + "license": "OFL-1.1" + }, + "node_modules/@fontsource/roboto-mono": { + "version": "5.0.18", + "resolved": "https://registry.npmjs.org/@fontsource/roboto-mono/-/roboto-mono-5.0.18.tgz", + "integrity": "sha512-hKuwk/cy3i6fWPzazT5xjgWq4YNqZWDHVbJh2Wwj3KYvWGi2v3ToBw/4LKQ+ggEkPLcIG6VU8GpCT3Xtf+mbbA==", + "license": "Apache-2.0" + }, + "node_modules/@hookform/resolvers": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.6.0.tgz", + "integrity": "sha512-UBcpyOX3+RR+dNnqBd0lchXpoL8p4xC21XP8H6Meb8uve5Br1GCnmg0PcBoKKqPKgGu9GHQ/oygcmPrQhetwqw==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@ianvs/prettier-plugin-sort-imports": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@ianvs/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.2.1.tgz", + "integrity": "sha512-NKN1LVFWUDGDGr3vt+6Ey3qPeN/163uR1pOPAlkWpgvAqgxQ6kSdUf1F0it8aHUtKRUzEGcK38Wxd07O61d7+Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/core": "^7.24.0", + "@babel/generator": "^7.23.6", + "@babel/parser": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0", + "semver": "^7.5.2" + }, + "peerDependencies": { + "@vue/compiler-sfc": "2.7.x || 3.x", + "prettier": "2 || 3" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/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/@jest/core/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.9.tgz", + "integrity": "sha512-xpz6C/vXOegF9VEtlMBlkNNIjHrLhKaFBsO4lmQGr00x5BHp7p+oliR6i7LwIcM5cZU2VjLSwm2R+/zj5IjPWg==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.1.tgz", + "integrity": "sha512-mBLKRHc7Ffw/hObYb9+cunuGNjshQk+vZdwZBJoqiysK/mW3Jq0UXosq8aIhMnLevANhR9yoYfdUEOHg6M9y0g==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.26", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.26.tgz", + "integrity": "sha512-Z9rjt4BUVEbLFpw0qjCklVxxf421wrmcbP4w+LmBUxYCyJTYYSclgJD0YsCgGqQCtCIPiz7kjbYYJiAKhjJ3kA==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@microsoft/tsdoc-config": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz", + "integrity": "sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "ajv": "~6.12.6", + "jju": "~1.4.0", + "resolve": "~1.19.0" + } + }, + "node_modules/@microsoft/tsdoc-config/node_modules/resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@mui/base": { + "version": "5.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", + "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==", + "deprecated": "This package has been replaced by @base-ui-components/react", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@floating-ui/react-dom": "^2.0.8", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", + "@popperjs/core": "^2.11.8", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.17.1.tgz", + "integrity": "sha512-OcZj+cs6EfUD39IoPBOgN61zf1XFVY+imsGoBDwXeSq2UHJZE3N59zzBOVjclck91Ne3e9gudONOeILvHCIhUA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.1.2.tgz", + "integrity": "sha512-slqJByDub7Y1UcokrM17BoMBMvn8n7daXFXVoTv0MEH5k3sHjmsH8ql/Mt3s9vQ20cORDr83UZ448TEGcbrXtw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^7.1.2", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/lab": { + "version": "5.0.0-alpha.170", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.170.tgz", + "integrity": "sha512-0bDVECGmrNjd3+bLdcLiwYZ0O4HP5j5WSQm5DV6iA/Z9kr8O6AnvZ1bv9ImQbbX7Gj3pX4o43EKwCutj3EQxQg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/base": "5.0.0-beta.40", + "@mui/system": "^5.15.15", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material": ">=5.15.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "5.15.20", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.20.tgz", + "integrity": "sha512-tVq3l4qoXx/NxUgIx/x3lZiPn/5xDbdTE8VrLczNpfblLYZzlrbxA7kb9mI8NoBF6+w9WE9IrxWnKK5KlPI2bg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/base": "5.0.0-beta.40", + "@mui/core-downloads-tracker": "^5.15.20", + "@mui/system": "^5.15.20", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.20", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^18.2.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.17.1.tgz", + "integrity": "sha512-XMxU0NTYcKqdsG8LRmSoxERPXwMbp16sIXPcLVgLGII/bVNagX0xaheWAwFv8+zDK7tI3ajllkuD3GZZE++ICQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/utils": "^5.17.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming/node_modules/@mui/types": { + "version": "7.2.24", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz", + "integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming/node_modules/@mui/utils": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.17.1.tgz", + "integrity": "sha512-jEZ8FTqInt2WzxDV8bhImWBqeQRD99c/id/fq83H0ER9tFl+sfZlaAoCdznGvbSQQ9ividMxqSV2c7cC1vBcQg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/types": "~7.2.15", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming/node_modules/react-is": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", + "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", + "license": "MIT" + }, + "node_modules/@mui/styled-engine": { + "version": "5.16.14", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.14.tgz", + "integrity": "sha512-UAiMPZABZ7p8mUW4akDV6O7N3+4DatStpXMZwPlt+H/dA0lt67qawN021MNND+4QTpjaiMYxbhKZeQcyWCbuKw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@emotion/cache": "^11.13.5", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine/node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@mui/styled-engine/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@mui/styled-engine/node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, + "node_modules/@mui/system": { + "version": "5.15.20", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.20.tgz", + "integrity": "sha512-LoMq4IlAAhxzL2VNUDBTQxAb4chnBe8JvRINVNDiMtHE2PiPOoHlhOPutSxEbaL5mkECPVWSv6p8JEV+uykwIA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/private-theming": "^5.15.20", + "@mui/styled-engine": "^5.15.14", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.20", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.3.tgz", + "integrity": "sha512-2UCEiK29vtiZTeLdS2d4GndBKacVyxGvReznGXGr+CzW/YhjIX+OHUdCIczZjzcRAgKBGmE9zCIgoV9FleuyRQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.1" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "5.15.20", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.20.tgz", + "integrity": "sha512-mAbYx0sovrnpAu1zHc3MDIhPqL8RPVC5W5xcO1b7PiSCJPtckIZmBkp8hefamAvUiAV8gpfMOM6Zb+eSisbI2A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@types/prop-types": "^15.7.11", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-date-pickers": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.7.1.tgz", + "integrity": "sha512-p7/TY8QcdQd6RelNqzW5q89GeUFctvZnDHTfQVEC0l0nAy7ArE6u21uNF8QWGrijZoJXCM+OlIRzlZADaUPpWA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@mui/base": "^5.0.0-beta.40", + "@mui/system": "^5.15.20", + "@mui/utils": "^5.15.20", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14", + "date-fns": "^2.25.0 || ^3.2.0", + "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz", + "integrity": "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.9.0" + } + }, + "node_modules/@next/env": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.4.tgz", + "integrity": "sha512-3EtkY5VDkuV2+lNmKlbkibIJxcO4oIHEhBWne6PaAp+76J9KoSsGvNikp6ivzAT8dhhBMYrm6op2pS1ApG0Hzg==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.4.tgz", + "integrity": "sha512-svSFxW9f3xDaZA3idQmlFw7SusOuWTpDTAeBlO3AEPDltrraV+lqs7mAc6A27YdnpQVVIA3sODqUAAHdWhVWsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "10.3.10" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.4.tgz", + "integrity": "sha512-AH3mO4JlFUqsYcwFUHb1wAKlebHU/Hv2u2kb1pAuRanDZ7pD/A/KPD98RHZmwsJpdHQwfEc/06mgpSzwrJYnNg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.4.tgz", + "integrity": "sha512-QVadW73sWIO6E2VroyUjuAxhWLZWEpiFqHdZdoQ/AMpN9YWGuHV8t2rChr0ahy+irKX5mlDU7OY68k3n4tAZTg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.4.tgz", + "integrity": "sha512-KT6GUrb3oyCfcfJ+WliXuJnD6pCpZiosx2X3k66HLR+DMoilRb76LpWPGb4tZprawTtcnyrv75ElD6VncVamUQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.4.tgz", + "integrity": "sha512-Alv8/XGSs/ytwQcbCHwze1HmiIkIVhDHYLjczSVrf0Wi2MvKn/blt7+S6FJitj3yTlMwMxII1gIJ9WepI4aZ/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.4.tgz", + "integrity": "sha512-ze0ShQDBPCqxLImzw4sCdfnB3lRmN3qGMB2GWDRlq5Wqy4G36pxtNOo2usu/Nm9+V2Rh/QQnrRc2l94kYFXO6Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.4.tgz", + "integrity": "sha512-8dwC0UJoc6fC7PX70csdaznVMNr16hQrTDAMPvLPloazlcaWfdPogq+UpZX6Drqb1OBlwowz8iG7WR0Tzk/diQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.4.tgz", + "integrity": "sha512-jxyg67NbEWkDyvM+O8UDbPAyYRZqGLQDTPwvrBBeOSyVWW/jFQkQKQ70JDqDSYg1ZDdl+E3nkbFbq8xM8E9x8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.4.tgz", + "integrity": "sha512-twrmN753hjXRdcrZmZttb/m5xaCBFa48Dt3FbeEItpJArxriYDunWxJn+QFXdJ3hPkm4u7CKxncVvnmgQMY1ag==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.4.tgz", + "integrity": "sha512-tkLrjBzqFTP8DVrAAQmZelEahfR9OxWpFR++vAI9FBhCiIxtwHwBHC23SBHCTURBtwB4kc/x44imVOnkKGNVGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "5.1.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@phosphor-icons/react": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.6.tgz", + "integrity": "sha512-F963SJvCTk0Qm5SRTSHXP8bCIYgMAbSVZ73f5DoxjP2iG/yAzRdySzbs9kVPETYxvr0zwTY4DUTqUjB3vr8sVw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">= 16.8", + "react-dom": ">= 16.8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", + "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@prisma/client": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.10.1.tgz", + "integrity": "sha512-Re4pMlcUsQsUTAYMK7EJ4Bw2kg3WfZAAlr8GjORJaK4VOP6LxRQUQ1TuLnxcF42XqGkWQ36q5CQF1yVadANQ6w==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.10.1.tgz", + "integrity": "sha512-kz4/bnqrOrzWo8KzYguN0cden4CzLJJ+2VSpKtF8utHS3l1JS0Lhv6BLwpOX6X9yNreTbZQZwewb+/BMPDCIYQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jiti": "2.4.2" + } + }, + "node_modules/@prisma/debug": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.10.1.tgz", + "integrity": "sha512-k2YT53cWxv9OLjW4zSYTZ6Z7j0gPfCzcr2Mj99qsuvlxr8WAKSZ2NcSR0zLf/mP4oxnYG842IMj3utTgcd7CaA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.10.1.tgz", + "integrity": "sha512-Q07P5rS2iPwk2IQr/rUQJ42tHjpPyFcbiH7PXZlV81Ryr9NYIgdxcUrwgVOWVm5T7ap02C0dNd1dpnNcSWig8A==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.10.1", + "@prisma/engines-version": "6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c", + "@prisma/fetch-engine": "6.10.1", + "@prisma/get-platform": "6.10.1" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c.tgz", + "integrity": "sha512-ZJFTsEqapiTYVzXya6TUKYDFnSWCNegfUiG5ik9fleQva5Sk3DNyyUi7X1+0ZxWFHwHDr6BZV5Vm+iwP+LlciA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.10.1.tgz", + "integrity": "sha512-clmbG/Jgmrc/n6Y77QcBmAUlq9LrwI9Dbgy4pq5jeEARBpRCWJDJ7PWW1P8p0LfFU0i5fsyO7FqRzRB8mkdS4g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.10.1", + "@prisma/engines-version": "6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c", + "@prisma/get-platform": "6.10.1" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.10.1.tgz", + "integrity": "sha512-4CY5ndKylcsce9Mv+VWp5obbR2/86SHOLVV053pwIkhVtT9C9A83yqiqI/5kJM9T1v1u1qco/bYjDKycmei9HA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.10.1" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz", + "integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.1", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.2", + "tar-fs": "^3.0.8", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz", + "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.4.tgz", + "integrity": "sha512-Z4DUr/AkgyFf1bOThW2HwzREagee0sB5ycl+hDiSZOfRLW8ZgrOjDi6g8mHH19yyU5E2A/64W3z6SMIf5XiUSQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.0.tgz", + "integrity": "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.1.tgz", + "integrity": "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.2.tgz", + "integrity": "sha512-4Jys0ni2tB2VZzgslbEgszZyMdTkPOFGA8g+So/NjR8oy6Qwaq4eSwsrRI+NMtb0Dq4kqCzGUu/nGUx7OM/xfw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.4", + "@smithy/types": "^4.8.1", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.17.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.17.2.tgz", + "integrity": "sha512-n3g4Nl1Te+qGPDbNFAYf+smkRVB+JhFsGy9uJXXZQEufoP4u0r+WLh6KvTDolCswaagysDc/afS1yvb2jnj1gQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-stream": "^4.5.5", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.4.tgz", + "integrity": "sha512-YVNMjhdz2pVto5bRdux7GMs0x1m0Afz3OcQy/4Yf9DH4fWOtroGH7uLvs7ZmDyoBJzLdegtIPpXrpJOZWvUXdw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.4.tgz", + "integrity": "sha512-aV8blR9RBDKrOlZVgjOdmOibTC2sBXNiT7WA558b4MPdsLTV6sbyc1WIE9QiIuYMJjYtnPLciefoqSW8Gi+MZQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.8.1", + "@smithy/util-hex-encoding": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.4.tgz", + "integrity": "sha512-d5T7ZS3J/r8P/PDjgmCcutmNxnSRvPH1U6iHeXjzI50sMr78GLmFcrczLw33Ap92oEKqa4CLrkAPeSSOqvGdUA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.4.tgz", + "integrity": "sha512-lxfDT0UuSc1HqltOGsTEAlZ6H29gpfDSdEPTapD5G63RbnYToZ+ezjzdonCCH90j5tRRCw3aLXVbiZaBW3VRVg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.4.tgz", + "integrity": "sha512-TPhiGByWnYyzcpU/K3pO5V7QgtXYpE0NaJPEZBCa1Y5jlw5SjqzMSbFiLb+ZkJhqoQc0ImGyVINqnq1ze0ZRcQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.4.tgz", + "integrity": "sha512-GNI/IXaY/XBB1SkGBFmbW033uWA0tj085eCxYih0eccUe/PFR7+UBQv9HNDk2fD9TJu7UVsCWsH99TkpEPSOzQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.2.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.5.tgz", + "integrity": "sha512-mg83SM3FLI8Sa2ooTJbsh5MFfyMTyNRwxqpKHmE0ICRIa66Aodv80DMsTQI02xBLVJ0hckwqTRr5IGAbbWuFLQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.4", + "@smithy/querystring-builder": "^4.2.4", + "@smithy/types": "^4.8.1", + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.5.tgz", + "integrity": "sha512-kCdgjD2J50qAqycYx0imbkA9tPtyQr1i5GwbK/EOUkpBmJGSkJe4mRJm+0F65TUSvvui1HZ5FFGFCND7l8/3WQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.2.0", + "@smithy/chunked-blob-reader-native": "^4.2.1", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.4.tgz", + "integrity": "sha512-kKU0gVhx/ppVMntvUOZE7WRMFW86HuaxLwvqileBEjL7PoILI8/djoILw3gPQloGVE6O0oOzqafxeNi2KbnUJw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.4.tgz", + "integrity": "sha512-amuh2IJiyRfO5MV0X/YFlZMD6banjvjAwKdeJiYGUbId608x+oSNwv3vlyW2Gt6AGAgl3EYAuyYLGRX/xU8npQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.4.tgz", + "integrity": "sha512-z6aDLGiHzsMhbS2MjetlIWopWz//K+mCoPXjW6aLr0mypF+Y7qdEh5TyJ20Onf9FbWHiWl4eC+rITdizpnXqOw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.4.tgz", + "integrity": "sha512-h7kzNWZuMe5bPnZwKxhVbY1gan5+TZ2c9JcVTHCygB14buVGOZxLl+oGfpY2p2Xm48SFqEWdghpvbBdmaz3ncQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.4.tgz", + "integrity": "sha512-hJRZuFS9UsElX4DJSJfoX4M1qXRH+VFiLMUnhsWvtOOUWRNvvOfDaUSdlNbjwv1IkpVjj/Rd/O59Jl3nhAcxow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.6.tgz", + "integrity": "sha512-PXehXofGMFpDqr933rxD8RGOcZ0QBAWtuzTgYRAHAL2BnKawHDEdf/TnGpcmfPJGwonhginaaeJIKluEojiF/w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.17.2", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-middleware": "^4.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.6.tgz", + "integrity": "sha512-OhLx131znrEDxZPAvH/OYufR9d1nB2CQADyYFN4C3V/NQS7Mg4V6uvxHC/Dr96ZQW8IlHJTJ+vAhKt6oxWRndA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/service-error-classification": "^4.2.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.4.tgz", + "integrity": "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.4.tgz", + "integrity": "sha512-Gy3TKCOnm9JwpFooldwAboazw+EFYlC+Bb+1QBsSi5xI0W5lX81j/P5+CXvD/9ZjtYKRgxq+kkqd/KOHflzvgA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.4.tgz", + "integrity": "sha512-3X3w7qzmo4XNNdPKNS4nbJcGSwiEMsNsRSunMA92S4DJLLIrH5g1AyuOA2XKM9PAPi8mIWfqC+fnfKNsI4KvHw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.4.tgz", + "integrity": "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/querystring-builder": "^4.2.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.4.tgz", + "integrity": "sha512-g2DHo08IhxV5GdY3Cpt/jr0mkTlAD39EJKN27Jb5N8Fb5qt8KG39wVKTXiTRCmHHou7lbXR8nKVU14/aRUf86w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.4.tgz", + "integrity": "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.4.tgz", + "integrity": "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "@smithy/util-uri-escape": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.4.tgz", + "integrity": "sha512-aHb5cqXZocdzEkZ/CvhVjdw5l4r1aU/9iMEyoKzH4eXMowT6M0YjBpp7W/+XjkBnY8Xh0kVd55GKjnPKlCwinQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.4.tgz", + "integrity": "sha512-fdWuhEx4+jHLGeew9/IvqVU/fxT/ot70tpRGuOLxE3HzZOyKeTQfYeV1oaBXpzi93WOk668hjMuuagJ2/Qs7ng==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.3.4.tgz", + "integrity": "sha512-y5ozxeQ9omVjbnJo9dtTsdXj9BEvGx2X8xvRgKnV+/7wLBuYJQL6dOa/qMY6omyHi7yjt1OA97jZLoVRYi8lxA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.4.tgz", + "integrity": "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.2.tgz", + "integrity": "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.17.2", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "@smithy/util-stream": "^4.5.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.8.1.tgz", + "integrity": "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.4.tgz", + "integrity": "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.5.tgz", + "integrity": "sha512-GwaGjv/QLuL/QHQaqhf/maM7+MnRFQQs7Bsl6FlaeK6lm6U7mV5AAnVabw68cIoMl5FQFyKK62u7RWRzWL25OQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.8.tgz", + "integrity": "sha512-gIoTf9V/nFSIZ0TtgDNLd+Ws59AJvijmMDYrOozoMHPJaG9cMRdqNO50jZTlbM6ydzQYY8L/mQ4tKSw/TB+s6g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.2", + "@smithy/credential-provider-imds": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.4.tgz", + "integrity": "sha512-f+nBDhgYRCmUEDKEQb6q0aCcOTXRDqH5wWaFHJxt4anB4pKHlgGoYP3xtioKXH64e37ANUkzWf6p4Mnv1M5/Vg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.4.tgz", + "integrity": "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.4.tgz", + "integrity": "sha512-yQncJmj4dtv/isTXxRb4AamZHy4QFr4ew8GxS6XLWt7sCIxkPxPzINWd7WLISEFPsIan14zrKgvyAF+/yzfwoA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.2.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.5.tgz", + "integrity": "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/types": "^4.8.1", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.4.tgz", + "integrity": "sha512-roKXtXIC6fopFvVOju8VYHtguc/jAcMlK8IlDOHsrQn0ayMkHynjm/D2DCMRf7MJFXzjHhlzg2edr3QPEakchQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", + "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.83.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz", + "integrity": "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.83.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz", + "integrity": "sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.83.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.6.tgz", + "integrity": "sha512-8qpnGVincVDLEcQXWaHOf6zmlbwTKc6Us6PPu4CRnPXCzo2OGBS5cwgMMOWdxDpEz1mkbvXHpEy99M5Yvt682w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "@babel/runtime": "^7.9.2", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@jest/globals": ">= 28", + "@types/bun": "latest", + "@types/jest": ">= 28", + "jest": ">= 28", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "@jest/globals": { + "optional": true + }, + "@types/bun": { + "optional": true + }, + "@types/jest": { + "optional": true + }, + "jest": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.0.0.tgz", + "integrity": "sha512-guuxUKRWQ+FgNX0h0NS0FIq3Q3uLtWVpBzcLOggmfMoUpgBnzBzvLLd4fbm6yS8ydJd94cIfY4yP9qUQjM2KwQ==", + "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", + "@types/react-dom": "^18.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", + "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/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/@types/jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mapbox-gl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-3.1.0.tgz", + "integrity": "sha512-hI6cQDjw1bkJw7MC/eHMqq5TWUamLwsujnUUeiIX2KDRjxRNSYMjnHz07+LATz9I9XIsKumOtUz4gRYnZOJ/FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/node": { + "version": "20.14.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz", + "integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/nodemailer": { + "version": "6.4.17", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", + "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-syntax-highlighter": { + "version": "15.5.13", + "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", + "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.9.2.tgz", + "integrity": "sha512-tS+lqTU3N0kkthU+rYp0spAYq15DU8ld9kXkaKg9sbQqJNF+WPMuNHZQGCgdxrUOEO0j22RKMwRVhF1HTl+X8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.9.2.tgz", + "integrity": "sha512-MffGiZULa/KmkNjHeuuflLVqfhqLv1vZLm8lWIyeADvlElJ/GLSOkoUX+5jf4/EGtfwrNFcEaB8BRas03KT0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.9.2.tgz", + "integrity": "sha512-dzJYK5rohS1sYl1DHdJ3mwfwClJj5BClQnQSyAgEfggbUwA9RlROQSSbKBLqrGfsiC/VyrDPtbO8hh56fnkbsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.9.2.tgz", + "integrity": "sha512-gaIMWK+CWtXcg9gUyznkdV54LzQ90S3X3dn8zlh+QR5Xy7Y+Efqw4Rs4im61K1juy4YNb67vmJsCDAGOnIeffQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.9.2.tgz", + "integrity": "sha512-S7QpkMbVoVJb0xwHFwujnwCAEDe/596xqY603rpi/ioTn9VDgBHnCCxh+UFrr5yxuMH+dliHfjwCZJXOPJGPnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.9.2.tgz", + "integrity": "sha512-+XPUMCuCCI80I46nCDFbGum0ZODP5NWGiwS3Pj8fOgsG5/ctz+/zzuBlq/WmGa+EjWZdue6CF0aWWNv84sE1uw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.9.2.tgz", + "integrity": "sha512-sqvUyAd1JUpwbz33Ce2tuTLJKM+ucSsYpPGl2vuFwZnEIg0CmdxiZ01MHQ3j6ExuRqEDUCy8yvkDKvjYFPb8Zg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.9.2.tgz", + "integrity": "sha512-UYA0MA8ajkEDCFRQdng/FVx3F6szBvk3EPnkTTQuuO9lV1kPGuTB+V9TmbDxy5ikaEgyWKxa4CI3ySjklZ9lFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.9.2.tgz", + "integrity": "sha512-P/CO3ODU9YJIHFqAkHbquKtFst0COxdphc8TKGL5yCX75GOiVpGqd1d15ahpqu8xXVsqP4MGFP2C3LRZnnL5MA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.9.2.tgz", + "integrity": "sha512-uKStFlOELBxBum2s1hODPtgJhY4NxYJE9pAeyBgNEzHgTqTiVBPjfTlPFJkfxyTjQEuxZbbJlJnMCrRgD7ubzw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.9.2.tgz", + "integrity": "sha512-LkbNnZlhINfY9gK30AHs26IIVEZ9PEl9qOScYdmY2o81imJYI4IMnJiW0vJVtXaDHvBvxeAgEy5CflwJFIl3tQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.9.2.tgz", + "integrity": "sha512-vI+e6FzLyZHSLFNomPi+nT+qUWN4YSj8pFtQZSFTtmgFoxqB6NyjxSjAxEC1m93qn6hUXhIsh8WMp+fGgxCoRg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.9.2.tgz", + "integrity": "sha512-sSO4AlAYhSM2RAzBsRpahcJB1msc6uYLAtP6pesPbZtptF8OU/CbCPhSRW6cnYOGuVmEmWVW5xVboAqCnWTeHQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.9.2.tgz", + "integrity": "sha512-jkSkwch0uPFva20Mdu8orbQjv2A3G88NExTN2oPTI1AJ+7mZfYW3cDCTyoH6OnctBKbBVeJCEqh0U02lTkqD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.9.2.tgz", + "integrity": "sha512-Uk64NoiTpQbkpl+bXsbeyOPRpUoMdcUqa+hDC1KhMW7aN1lfW8PBlBH4mJ3n3Y47dYE8qi0XTxy1mBACruYBaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.9.2.tgz", + "integrity": "sha512-EpBGwkcjDicjR/ybC0g8wO5adPNdVuMrNalVgYcWi+gYtC1XYNuxe3rufcO7dA76OHGeVabcO6cSkPJKVcbCXQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.9.2.tgz", + "integrity": "sha512-EdFbGn7o1SxGmN6aZw9wAkehZJetFPao0VGZ9OMBwKx6TkvDuj6cNeLimF/Psi6ts9lMOe+Dt6z19fZQ9Ye2fw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.9.2.tgz", + "integrity": "sha512-JY9hi1p7AG+5c/dMU8o2kWemM8I6VZxfGwn1GCtf3c5i+IKcMo2NQ8OjZ4Z3/itvY/Si3K10jOBQn7qsD/whUA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.9.2.tgz", + "integrity": "sha512-ryoo+EB19lMxAd80ln9BVf8pdOAxLb97amrQ3SFN9OCRn/5M5wvwDgAe4i8ZjhpbiHoDeP8yavcTEnpKBo7lZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@upstash/redis": { + "version": "1.35.3", + "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.35.3.tgz", + "integrity": "sha512-hSjv66NOuahW3MisRGlSgoszU2uONAY2l5Qo3Sae8OT3/Tng9K+2/cBRuyPBX8egwEGcNNCF9+r0V6grNnhL+w==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/@vercel/style-guide": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@vercel/style-guide/-/style-guide-6.0.0.tgz", + "integrity": "sha512-tu0wFINGz91EPwaT5VjSqUwbvCY9pvLach7SPG4XyfJKPU9Vku2TFa6+AyzJ4oroGbo9fK+TQhIFHrnFl0nCdg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@babel/core": "^7.24.0", + "@babel/eslint-parser": "^7.23.10", + "@rushstack/eslint-patch": "^1.7.2", + "@typescript-eslint/eslint-plugin": "^7.1.1", + "@typescript-eslint/parser": "^7.1.1", + "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-alias": "^1.1.2", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-jest": "^27.9.0", + "eslint-plugin-jsx-a11y": "^6.8.0", + "eslint-plugin-playwright": "^1.5.2", + "eslint-plugin-react": "^7.34.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-testing-library": "^6.2.0", + "eslint-plugin-tsdoc": "^0.2.17", + "eslint-plugin-unicorn": "^51.0.1", + "eslint-plugin-vitest": "^0.3.22", + "prettier-plugin-packagejson": "^2.4.12" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "@next/eslint-plugin-next": ">=12.3.0 <15.0.0-0", + "eslint": ">=8.48.0 <9", + "prettier": ">=3.0.0 <4", + "typescript": ">=4.8.0 <6" + }, + "peerDependenciesMeta": { + "@next/eslint-plugin-next": { + "optional": true + }, + "eslint": { + "optional": true + }, + "prettier": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@yr/monotone-cubic-spline": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", + "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==", + "license": "MIT" + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/apexcharts": { + "version": "3.49.2", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.49.2.tgz", + "integrity": "sha512-vBB8KgwfD9rSObA7s4kY2rU6DeaN67gTR3JN7r32ztgKVf8lKkdFQ6iUhk6oIHrV7W8PoHhr5EwKymn0z5Fz6A==", + "license": "MIT", + "dependencies": { + "@yr/monotone-cubic-spline": "^1.0.3", + "svg.draggable.js": "^2.2.2", + "svg.easing.js": "^2.0.0", + "svg.filter.js": "^2.0.2", + "svg.pathmorphing.js": "^0.1.3", + "svg.resize.js": "^1.4.3", + "svg.select.js": "^3.0.1" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/are-we-there-yet/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/are-we-there-yet/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "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/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/attr-accept": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", + "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", + "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "license": "Apache-2.0" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/bare-fs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.6.tgz", + "integrity": "sha512-25RsLF33BqooOEFNdMcEhMpJy8EoR88zSMrnOQOaM3USnOK2VmaJ1uaQEwPA6AQjrv1lXChScosN6CzbwbO9OQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", + "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", + "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/bowser": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", + "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-0.1.2.tgz", + "integrity": "sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg==", + "license": "MIT" + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001726", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", + "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chromium-bidi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz", + "integrity": "sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/chromium-bidi/node_modules/zod": { + "version": "3.25.74", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.74.tgz", + "integrity": "sha512-J8poo92VuhKjNknViHRAIuuN6li/EwFbAC8OedzI8uxpEPGiXHGQu9wemIAioIpqgfB4SySaJhdk0mH5Y4ICBg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/ci-info": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz", + "integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clean-regexp/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.43.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.43.0.tgz", + "integrity": "sha512-2GML2ZsCc5LR7hZYz4AXmjQw8zuy2T//2QntwdnpuYI7jteT6GVYJL7F6C2C57R7gSYrcqVW3lAALefdbhBLDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dayjs": { + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", + "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, + "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-indent": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.1.tgz", + "integrity": "sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1464554", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1464554.tgz", + "integrity": "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw==", + "license": "BSD-3-Clause" + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dotenv": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.0.0.tgz", + "integrity": "sha512-A0BJ5lrpJVSfnMMXjmeO0xUnoxqsBHWCoqqTnGwGYVdnctqXXUEhJOO7LxmgxJon9tEZFGpe0xPRX0h2v3AANQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.177", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.177.tgz", + "integrity": "sha512-7EH2G59nLsEMj97fpDuvVcYi6lwTcM1xuWw3PssD8xzboAW7zj7iB3COEEEATUfjLHrs5uKBLQT03V/8URx06g==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.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/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-next": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.4.tgz", + "integrity": "sha512-Qr0wMgG9m6m4uYy2jrYJmyuNlYZzPRQq5Kvb9IDlYwn+7yq6W6sfMNFgb+9guM1KYwuIo6TIaiFhZJ6SnQ/Efw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "14.2.4", + "@rushstack/eslint-patch": "^1.3.3", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/parser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", + "integrity": "sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/scope-manager": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz", + "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz", + "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz", + "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", + "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-next/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-next/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-alias": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-alias/-/eslint-import-resolver-alias-1.1.2.tgz", + "integrity": "sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + }, + "peerDependencies": { + "eslint-plugin-import": ">=1.4.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-eslint-comments": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz", + "integrity": "sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5", + "ignore": "^5.0.5" + }, + "engines": { + "node": ">=6.5.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-eslint-comments/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "27.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.9.0.tgz", + "integrity": "sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^5.10.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0 || ^6.0.0 || ^7.0.0", + "eslint": "^7.0.0 || ^8.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-playwright": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-1.8.3.tgz", + "integrity": "sha512-h87JPFHkz8a6oPhn8GRGGhSQoAJjx0AkOv1jME6NoMk2FpEsfvfJJNaQDxLSqSALkCr0IJXPGTnp6SIRVu5Nqg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "examples" + ], + "dependencies": { + "globals": "^13.23.0" + }, + "engines": { + "node": ">=16.6.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0", + "eslint-plugin-jest": ">=25" + }, + "peerDependenciesMeta": { + "eslint-plugin-jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-playwright/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-playwright/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-testing-library": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-6.5.0.tgz", + "integrity": "sha512-Ls5TUfLm5/snocMAOlofSOJxNN0aKqwTlco7CrNtMjkTdQlkpSMaeTCDHCuXfzrI97xcx2rSCNeKeJjtpkNC1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0", + "npm": ">=6" + }, + "peerDependencies": { + "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-tsdoc": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.2.17.tgz", + "integrity": "sha512-xRmVi7Zx44lOBuYqG8vzTXuL6IdGOeF9nHX17bjJ8+VE6fsxpdGem0/SBTmAwgYMKYB1WBkqRJVQ+n8GK041pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "0.16.2" + } + }, + "node_modules/eslint-plugin-unicorn": { + "version": "51.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-51.0.1.tgz", + "integrity": "sha512-MuR/+9VuB0fydoI0nIn2RDA5WISRn4AsJyNSaNKLVwie9/ONvQhxOBbkfSICBPnzKrB77Fh6CZZXjgTt/4Latw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "@eslint-community/eslint-utils": "^4.4.0", + "@eslint/eslintrc": "^2.1.4", + "ci-info": "^4.0.0", + "clean-regexp": "^1.0.0", + "core-js-compat": "^3.34.0", + "esquery": "^1.5.0", + "indent-string": "^4.0.0", + "is-builtin-module": "^3.2.1", + "jsesc": "^3.0.2", + "pluralize": "^8.0.0", + "read-pkg-up": "^7.0.1", + "regexp-tree": "^0.1.27", + "regjsparser": "^0.10.0", + "semver": "^7.5.4", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" + }, + "peerDependencies": { + "eslint": ">=8.56.0" + } + }, + "node_modules/eslint-plugin-vitest": { + "version": "0.3.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-vitest/-/eslint-plugin-vitest-0.3.26.tgz", + "integrity": "sha512-oxe5JSPgRjco8caVLTh7Ti8PxpwJdhSV0hTQAmkFcNcmy/9DnqLB/oNVRA11RmVRP//2+jIIT6JuBEcpW3obYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^7.1.1" + }, + "engines": { + "node": "^18.0.0 || >= 20.0.0" + }, + "peerDependencies": { + "eslint": ">=8.0.0", + "vitest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-scope/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", + "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", + "license": "MIT" + }, + "node_modules/file-selector": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", + "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==", + "license": "MIT", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/get-uri": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", + "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/git-hooks-list": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/git-hooks-list/-/git-hooks-list-4.1.1.tgz", + "integrity": "sha512-cmP497iLq54AZnv4YRAEMnEyQ1eIn4tGKbmswqwmFV4GBnAqE8NLtWxxdXa++AalfgL5EBH4IxTPyquEuGY/jA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/fisker/git-hooks-list?sponsor=1" + } + }, + "node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-tokenize": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-tokenize/-/html-tokenize-2.0.1.tgz", + "integrity": "sha512-QY6S+hZ0f5m1WT8WffYN+Hg+xm/w5I8XeUcAq/ZYP5wVC8xbKi4Whhru3FtrAebD5EhBW8rmFzkDI6eCAuFe2w==", + "license": "MIT", + "dependencies": { + "buffer-from": "~0.1.1", + "inherits": "~2.0.1", + "minimist": "~1.2.5", + "readable-stream": "~1.0.27-1", + "through2": "~0.4.1" + }, + "bin": { + "html-tokenize": "bin/cmd.js" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "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/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "devOptional": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/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/jest-circus/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/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/jest-config/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-config/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-config/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/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/jest-diff/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/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/jest-each/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/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/jest-leak-detector/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/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/jest-matcher-utils/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/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/jest-message-util/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/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/jest-snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/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/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.525.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz", + "integrity": "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multipipe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-1.0.2.tgz", + "integrity": "sha512-6uiC9OvY71vzSGX8lZvSqscE7ft9nPupJ8fMjrCNRAUy2LREUW42UL+V/NTrogr6rFgRydUrCX4ZitfpSNkSCQ==", + "license": "MIT", + "dependencies": { + "duplexer2": "^0.1.2", + "object-assign": "^4.1.0" + } + }, + "node_modules/nan": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", + "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", + "license": "MIT", + "optional": true + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.2.5.tgz", + "integrity": "sha512-kmsgUvCRIJohHjbZ3V8avP0I1Pekw329MVAMDzVxsrkjgdnqiwvMX5XwR+hWV66vsAtZ+iM+fVnq8RTQawUmCQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/next": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.4.tgz", + "integrity": "sha512-R8/V7vugY+822rsQGQCjoLhMuC9oFj9SOi4Cl4b2wjDrseD0LRZ10W7R6Czo4w9ZznVSshKjuIomsRjvm9EKJQ==", + "license": "MIT", + "dependencies": { + "@next/env": "14.2.4", + "@swc/helpers": "0.5.5", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.2.4", + "@next/swc-darwin-x64": "14.2.4", + "@next/swc-linux-arm64-gnu": "14.2.4", + "@next/swc-linux-arm64-musl": "14.2.4", + "@next/swc-linux-x64-gnu": "14.2.4", + "@next/swc-linux-x64-musl": "14.2.4", + "@next/swc-win32-arm64-msvc": "14.2.4", + "@next/swc-win32-ia32-msvc": "14.2.4", + "@next/swc-win32-x64-msvc": "14.2.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT", + "optional": true + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemailer": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.4.tgz", + "integrity": "sha512-9O00Vh89/Ld2EcVCqJ/etd7u20UhME0f/NToPfArwPEe1Don1zy4mAIz6ariRr7mJ2RDxtaDzN0WJVdVXPtZaw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/nwsapi": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openai": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-5.12.0.tgz", + "integrity": "sha512-vUdt02xiWgOHiYUmW0Hj1Qu9OKAiVQu5Bd547ktVCiMKC1BkB5L3ImeEnCyq3WpRKR6ZTaPgekzqdozwdPs7Lg==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/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==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/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==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/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/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path2d-polyfill": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", + "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pdfjs-dist": { + "version": "3.11.174", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz", + "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "canvas": "^2.11.2", + "path2d-polyfill": "^2.0.1" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-packagejson": { + "version": "2.5.17", + "resolved": "https://registry.npmjs.org/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.17.tgz", + "integrity": "sha512-1WYvhTix+4EMYZQYSjAxb6+KTCULINuHUTBcxYa2ipoUS9Y2zJVjE3kuZ5I7ZWIFqyK8xpwYIunXqN5eiT7Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "sort-package-json": "3.3.1", + "synckit": "0.11.8" + }, + "peerDependencies": { + "prettier": ">= 1.16.0" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + } + } + }, + "node_modules/prisma": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.10.1.tgz", + "integrity": "sha512-khhlC/G49E4+uyA3T3H5PRBut486HD2bDqE2+rvkU0pwk9IAqGFacLFUyIx9Uw+W2eCtf6XGwsp+/strUwMNPw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.10.1", + "@prisma/engines": "6.10.1" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/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==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/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==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.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/puppeteer": { + "version": "24.11.2", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.11.2.tgz", + "integrity": "sha512-HopdRZWHa5zk0HSwd8hU+GlahQ3fmesTAqMIDHVY9HasCvppcYuHYXyjml0nlm+nbwVCqAQWV+dSmiNCrZGTGQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.10.5", + "chromium-bidi": "5.1.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1464554", + "puppeteer-core": "24.11.2", + "typed-query-selector": "^2.12.0" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.11.2", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.11.2.tgz", + "integrity": "sha512-c49WifNb8hix+gQH17TldmD6TC/Md2HBaTJLHexIUq4sZvo2pyHY/Pp25qFQjibksBu/SJRYUY7JsoaepNbiRA==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.10.5", + "chromium-bidi": "5.1.0", + "debug": "^4.4.1", + "devtools-protocol": "0.0.1464554", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer/node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-apexcharts": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-1.4.1.tgz", + "integrity": "sha512-G14nVaD64Bnbgy8tYxkjuXEUp/7h30Q0U33xc3AwtGFijJB9nHqOt1a6eG0WBn055RgRg+NwqbKGtqPxy15d0Q==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "apexcharts": "^3.41.0", + "react": ">=0.13" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-dropzone": { + "version": "14.3.8", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz", + "integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==", + "license": "MIT", + "dependencies": { + "attr-accept": "^2.2.4", + "file-selector": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.52.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.52.0.tgz", + "integrity": "sha512-mJX506Xc6mirzLsmXUJyqlAI3Kj9Ph2RhplYhUVffeOQSnubK2uVqBFOBJmvKikvbFV91pxVXmDiR+QMF19x6A==", + "license": "MIT", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/recharts": { + "version": "2.12.7", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.7.tgz", + "integrity": "sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^16.10.2", + "react-smooth": "^4.0.0", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regjsparser": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz", + "integrity": "sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "devOptional": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "devOptional": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "optional": true + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz", + "integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==", + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/sort-object-keys": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sort-object-keys/-/sort-object-keys-1.1.3.tgz", + "integrity": "sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sort-package-json": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-3.3.1.tgz", + "integrity": "sha512-awjhQR2Iy5UN3NuguAK5+RezcEuUg9Ra4O8y2Aj+DlJa7MywyHaipAPf9bu4qqFj0hsYHHoT9sS3aV7Ucu728g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-indent": "^7.0.1", + "detect-newline": "^4.0.1", + "git-hooks-list": "^4.0.0", + "is-plain-obj": "^4.1.0", + "semver": "^7.7.1", + "sort-object-keys": "^1.1.3", + "tinyglobby": "^0.2.12" + }, + "bin": { + "sort-package-json": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/sort-package-json/node_modules/detect-newline": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-4.0.1.tgz", + "integrity": "sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/streamx": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "license": "MIT" + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg.draggable.js": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz", + "integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==", + "license": "MIT", + "dependencies": { + "svg.js": "^2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.easing.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz", + "integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==", + "license": "MIT", + "dependencies": { + "svg.js": ">=2.3.x" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.filter.js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz", + "integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==", + "license": "MIT", + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz", + "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==", + "license": "MIT" + }, + "node_modules/svg.pathmorphing.js": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz", + "integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==", + "license": "MIT", + "dependencies": { + "svg.js": "^2.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz", + "integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==", + "license": "MIT", + "dependencies": { + "svg.js": "^2.6.5", + "svg.select.js": "^2.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js/node_modules/svg.select.js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz", + "integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==", + "license": "MIT", + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.select.js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz", + "integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==", + "license": "MIT", + "dependencies": { + "svg.js": "^2.6.5" + }, + "engines": { + "node": ">= 0.8.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/synckit": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", + "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.4" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", + "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "optional": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz", + "integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, + "node_modules/through2": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz", + "integrity": "sha512-45Llu+EwHKtAZYTPPVn3XZHBgakWMN3rokhEv5hu596XP+cNgplMg+Gj+1nmAvj+L0K7+N49zBKx5rah5u0QIQ==", + "license": "MIT", + "dependencies": { + "readable-stream": "~1.0.17", + "xtend": "~2.1.1" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz", + "integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unrs-resolver": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.9.2.tgz", + "integrity": "sha512-VUyWiTNQD7itdiMuJy+EuLEErLj3uwX/EpHQF8EOf33Dq3Ju6VW1GXm+swk6+1h7a49uv9fKZ+dft9jU7esdLA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.2.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.9.2", + "@unrs/resolver-binding-android-arm64": "1.9.2", + "@unrs/resolver-binding-darwin-arm64": "1.9.2", + "@unrs/resolver-binding-darwin-x64": "1.9.2", + "@unrs/resolver-binding-freebsd-x64": "1.9.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.9.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.9.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.9.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.9.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.9.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.9.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.9.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.9.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.9.2", + "@unrs/resolver-binding-linux-x64-musl": "1.9.2", + "@unrs/resolver-binding-wasm32-wasi": "1.9.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.9.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.9.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.9.2" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wide-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "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/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "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/xtend": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha512-vMNKzr2rHP9Dp/e1NQFnLQlwlhp9L/LfvnsVdHxN1f+uggyVI3i08uD14GPvCToPkdsRfyPqIyYGmIk58V98ZQ==", + "dependencies": { + "object-keys": "~0.4.0" + }, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/xtend/node_modules/object-keys": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha512-ncrLw+X55z7bkl5PnUvHwFK9FcGuFYo9gtjws2XtSzL+aZ8tm830P60WJ0dSmFVaSalWieW5MD7kEdnXda9yJw==", + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json index aa6137060..c66155947 100644 --- a/package.json +++ b/package.json @@ -1,47 +1,85 @@ { - "name": "react-material-dashboard", - "version": "0.1.1", - "private": true, - "dependencies": { - "@material-ui/core": "^3.9.2", - "@material-ui/icons": "^3.0.2", - "@material-ui/styles": "^3.0.0-alpha.10", - "chart.js": "^2.7.3", - "classnames": "^2.2.6", - "history": "^4.7.2", - "moment": "^2.24.0", - "node-sass": "^4.11.0", - "prop-types": "^15.7.1", - "react": "^16.8.1", - "react-chartjs-2": "^2.7.4", - "react-dom": "^16.8.1", - "react-perfect-scrollbar": "^1.4.4", - "react-router-config": "^5.0.0", - "react-router-dom": "^4.3.1", - "react-scripts": "2.1.5", - "recompose": "^0.30.0", - "underscore": "^1.9.1", - "validate.js": "^0.12.0" - }, + "name": "fatos-da-bolsa-hub", + "version": "4.0.0", + "author": "Fatos da Bolsa", + "licence": "MIT", + "homepage": "https://fatosdabolsa.com.br", "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject" + "dev": "next dev", + "build": "prisma generate && next build", + "postinstall": "prisma generate", + "start": "next start", + "lint": "next lint", + "lint:fix": "next lint --fix", + "typecheck": "tsc --noEmit", + "format:write": "prettier --write \"**/*.{js,jsx,mjs,ts,tsx,mdx}\" --cache", + "format:check": "prettier --check \"**/*.{js,jsx,mjs,ts,tsx,mdx}\" --cache", + "test": "jest" }, - "eslintConfig": { - "extends": "react-app" + "dependencies": { + "@aws-sdk/client-s3": "^3.x", + "@emotion/cache": "11.11.0", + "@emotion/react": "11.11.4", + "@emotion/server": "11.11.0", + "@emotion/styled": "11.11.5", + "@fontsource/inter": "5.0.18", + "@fontsource/plus-jakarta-sans": "5.0.20", + "@fontsource/roboto-mono": "5.0.18", + "@hookform/resolvers": "3.6.0", + "@mui/icons-material": "^7.1.2", + "@mui/lab": "5.0.0-alpha.170", + "@mui/material": "5.15.20", + "@mui/system": "5.15.20", + "@mui/utils": "5.15.20", + "@mui/x-date-pickers": "7.7.1", + "@phosphor-icons/react": "2.1.6", + "@prisma/client": "^6.10.1", + "@tanstack/react-query": "^5.83.0", + "@upstash/redis": "^1.35.3", + "apexcharts": "3.49.2", + "next-auth": "^4.24.7", + "bcryptjs": "^3.0.2", + "dayjs": "1.11.11", + "dotenv": "^17.0.0", + "file-saver": "^2.0.5", + "jsonwebtoken": "^9.0.2", + "lucide-react": "^0.525.0", + "next": "14.2.4", + "nodemailer": "^7.0.4", + "openai": "^5.12.0", + "pdfjs-dist": "^3.11.174", + "puppeteer": "^24.11.2", + "react": "18.3.1", + "react-apexcharts": "1.4.1", + "react-dom": "18.3.1", + "react-dropzone": "^14.3.8", + "react-hook-form": "7.52.0", + "recharts": "2.12.7", + "xlsx": "^0.18.5", + "zod": "3.23.8" }, - "browserslist": [ - ">0.2%", - "not dead", - "not ie <= 11", - "not op_mini all" - ], "devDependencies": { - "eslint-plugin-prettier": "^3.0.1", - "eslint-plugin-react": "^7.12.4", - "prettier-eslint": "^8.8.2", - "prettier-eslint-cli": "^4.7.1" + "@ianvs/prettier-plugin-sort-imports": "4.2.1", + "@testing-library/jest-dom": "6.4.6", + "@testing-library/react": "16.0.0", + "@types/jest": "29.5.12", + "@types/mapbox-gl": "3.1.0", + "@types/node": "20.14.9", + "@types/nodemailer": "^6.4.17", + "@types/react": "18.3.3", + "@types/react-dom": "18.3.0", + "@types/react-syntax-highlighter": "15.5.13", + "@vercel/style-guide": "6.0.0", + "autoprefixer": "^10.4.21", + "eslint": "8.57.0", + "eslint-config-next": "14.2.4", + "eslint-config-prettier": "9.1.0", + "jest": "29.7.0", + "jest-environment-jsdom": "29.7.0", + "postcss": "^8.5.6", + "prettier": "3.3.2", + "prisma": "^6.10.1", + "tailwindcss": "^4.1.11", + "typescript": "5.5.2" } -} \ No newline at end of file +} diff --git a/populate-users.js b/populate-users.js new file mode 100644 index 000000000..6623c5b3a --- /dev/null +++ b/populate-users.js @@ -0,0 +1,174 @@ +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); + +async function populateUsers() { + try { + console.log('🚀 Criando usuários de exemplo...'); + + // Usuário Admin + const admin = await prisma.user.create({ + data: { + firstName: 'Admin', + lastName: 'Sistema', + email: 'admin@fatosdobolsa.com', + password: 'admin123', + plan: 'ADMIN', + status: 'ACTIVE', + customPermissions: JSON.stringify(['small-caps', 'dividendos', 'recursos-dicas']) + } + }); + + // Usuários VIP + const userVip = await prisma.user.create({ + data: { + firstName: 'João', + lastName: 'Silva', + email: 'joao.silva@email.com', + password: 'senha123', + plan: 'VIP', + status: 'ACTIVE', + expirationDate: new Date('2025-12-31'), + customPermissions: JSON.stringify(['internacional-etfs', 'recursos-analise']) + } + }); + + // Usuário LITE + const userLite = await prisma.user.create({ + data: { + firstName: 'Maria', + lastName: 'Santos', + email: 'maria.santos@email.com', + password: 'senha123', + plan: 'LITE', + status: 'ACTIVE', + expirationDate: new Date('2025-06-30'), + customPermissions: JSON.stringify(['dividendos']) + } + }); + + // Usuário FIIS + const userFiis = await prisma.user.create({ + data: { + firstName: 'Pedro', + lastName: 'Costa', + email: 'pedro.costa@email.com', + password: 'senha123', + plan: 'FIIS', + status: 'ACTIVE', + expirationDate: new Date('2025-09-15'), + customPermissions: JSON.stringify(['fundos-imobiliarios', 'recursos-planilhas']) + } + }); + + // Usuário Projeto América + const userAmerica = await prisma.user.create({ + data: { + firstName: 'Ana', + lastName: 'Lima', + email: 'ana.lima@email.com', + password: 'senha123', + plan: 'AMERICA', + status: 'ACTIVE', + expirationDate: new Date('2026-01-01'), + customPermissions: JSON.stringify(['projeto-america', 'internacional-stocks']) + } + }); + + // Usuário Renda Passiva + const userRenda = await prisma.user.create({ + data: { + firstName: 'Carlos', + lastName: 'Oliveira', + email: 'carlos.oliveira@email.com', + password: 'senha123', + plan: 'RENDA_PASSIVA', + status: 'ACTIVE', + expirationDate: new Date('2025-08-20'), + customPermissions: JSON.stringify(['dividendos', 'recursos-ebooks']) + } + }); + + // Usuário com status PENDING + const userPending = await prisma.user.create({ + data: { + firstName: 'Lucas', + lastName: 'Fernandes', + email: 'lucas.fernandes@email.com', + password: 'senha123', + plan: 'LITE', + status: 'PENDING', + customPermissions: JSON.stringify([]) + } + }); + + // Usuário expirado + const userExpired = await prisma.user.create({ + data: { + firstName: 'Patricia', + lastName: 'Rocha', + email: 'patricia.rocha@email.com', + password: 'senha123', + plan: 'VIP', + status: 'INACTIVE', + expirationDate: new Date('2024-12-31'), // Já expirado + customPermissions: JSON.stringify(['small-caps']) + } + }); + + // Criar algumas compras de exemplo + const purchases = [ + { + userId: userVip.id, + amount: 497.00, + productName: 'Close Friends VIP - Anual', + status: 'COMPLETED', + hotmartTransactionId: 'HP123456789' + }, + { + userId: userLite.id, + amount: 197.00, + productName: 'Close Friends LITE - Semestral', + status: 'COMPLETED', + hotmartTransactionId: 'HP987654321' + }, + { + userId: userFiis.id, + amount: 297.00, + productName: 'Projeto FIIs - Anual', + status: 'COMPLETED', + hotmartTransactionId: 'HP456789123' + }, + { + userId: userAmerica.id, + amount: 397.00, + productName: 'Projeto América - Anual', + status: 'COMPLETED', + hotmartTransactionId: 'HP789123456' + } + ]; + + for (const purchase of purchases) { + await prisma.purchase.create({ data: purchase }); + } + + console.log('✅ Usuários e compras criados com sucesso!'); + console.log(`👥 Total: 8 usuários`); + console.log(`💰 Total: ${purchases.length} compras`); + console.log(''); + console.log('👑 Admin: admin@fatosdobolsa.com'); + console.log('📊 VIP: joao.silva@email.com'); + console.log('â­ LITE: maria.santos@email.com'); + console.log('🢠FIIS: pedro.costa@email.com'); + console.log('🇺🇸 AMERICA: ana.lima@email.com'); + console.log('💰 RENDA_PASSIVA: carlos.oliveira@email.com'); + console.log('â³ PENDING: lucas.fernandes@email.com'); + console.log('🔴 EXPIRADO: patricia.rocha@email.com'); + + } catch (error) { + console.error('⌠Erro ao criar usuários:', error); + } finally { + await prisma.$disconnect(); + } +} + +populateUsers(); \ No newline at end of file diff --git a/prettier.config.mjs b/prettier.config.mjs new file mode 100644 index 000000000..3f8fb297c --- /dev/null +++ b/prettier.config.mjs @@ -0,0 +1,34 @@ +/** @type {import('prettier').Config} */ +const config = { + endOfLine: 'lf', + semi: true, + singleQuote: true, + tabWidth: 2, + trailingComma: 'es5', + printWidth: 120, + importOrder: [ + '^node:$', + '', + '^(react/(.*)$)|^(react$)', + '^(next/(.*)$)|^(next$)', + '', + '', + '^@/types$', + '^@/types/(.*)$', + '^@/config$', + '^@/config/(.*)$', + '^@/paths$', + '^@/data/(.*)$', + '^@/lib/(.*)$', + '^@/actions/(.*)$', + '^@/contexts/(.*)$', + '^@/hooks/(.*)$', + '^@/components/(.*)$', + '^@/styles/(.*)$', + '', + '^[./]', + ], + plugins: ['@ianvs/prettier-plugin-sort-imports'], +}; + +export default config; diff --git a/prisma/dev.db b/prisma/dev.db new file mode 100644 index 000000000..58912befe Binary files /dev/null and b/prisma/dev.db differ diff --git a/prisma/fix.js b/prisma/fix.js new file mode 100644 index 000000000..4d8d9cae6 --- /dev/null +++ b/prisma/fix.js @@ -0,0 +1,16 @@ +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); +async function main() { + const admin = await prisma.user.create({ + data: { + email: 'admin@fatosdobolsa.com', + firstName: 'Admin', + lastName: 'Sistema', + password: '$2a$10$N9qo8uLOickgx2ZMRZoMye1IVQ9VqKJoOB9gw3zVdG3d0F3kGqP2.', + plan: 'ADMIN', + status: 'ACTIVE' + } + }); + console.log('Admin criado!', admin.email); +} +main().catch(console.error).finally(() = diff --git a/prisma/migrations/20240709123456_add-user-origin/migration.sql b/prisma/migrations/20240709123456_add-user-origin/migration.sql new file mode 100644 index 000000000..734ccc7d2 --- /dev/null +++ b/prisma/migrations/20240709123456_add-user-origin/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "User" ADD COLUMN "origin" TEXT NOT NULL DEFAULT 'MANUAL'; diff --git a/prisma/migrations/20240710120000_add-reports/migration.sql b/prisma/migrations/20240710120000_add-reports/migration.sql new file mode 100644 index 000000000..8c684153c --- /dev/null +++ b/prisma/migrations/20240710120000_add-reports/migration.sql @@ -0,0 +1,15 @@ +-- CreateTable +CREATE TABLE "reports" ( + "id" TEXT NOT NULL, + "canvaId" TEXT NOT NULL, + "name" TEXT, + "iframeUrl" TEXT, + "status" TEXT NOT NULL DEFAULT 'pending', + "progress" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "reports_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "reports_status_idx" ON "reports"("status"); diff --git a/prisma/migrations/20240715150000_add-user-entitlements/migration.sql b/prisma/migrations/20240715150000_add-user-entitlements/migration.sql new file mode 100644 index 000000000..0087e2042 --- /dev/null +++ b/prisma/migrations/20240715150000_add-user-entitlements/migration.sql @@ -0,0 +1,18 @@ +-- CreateTable +CREATE TABLE "UserEntitlement" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "code" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'ACTIVE', + "expiresAt" TIMESTAMP, + "origin" TEXT, + "createdAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "UserEntitlement_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "UserEntitlement_userId_code_key" ON "UserEntitlement"("userId", "code"); + +-- AddForeignKey +ALTER TABLE "UserEntitlement" ADD CONSTRAINT "UserEntitlement_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250101120000_add_vies_override/migration.sql b/prisma/migrations/20250101120000_add_vies_override/migration.sql new file mode 100644 index 000000000..83a783976 --- /dev/null +++ b/prisma/migrations/20250101120000_add_vies_override/migration.sql @@ -0,0 +1,13 @@ +-- CreateEnum +CREATE TYPE "ViesOverride" AS ENUM ('AUTO', 'MANTER'); + +-- AlterTable +ALTER TABLE "user_micro_caps" ADD COLUMN "viesOverride" "ViesOverride" DEFAULT 'AUTO'; +ALTER TABLE "user_small_caps" ADD COLUMN "viesOverride" "ViesOverride" DEFAULT 'AUTO'; +ALTER TABLE "user_dividendos" ADD COLUMN "viesOverride" "ViesOverride" DEFAULT 'AUTO'; +ALTER TABLE "user_fiis" ADD COLUMN "viesOverride" "ViesOverride" DEFAULT 'AUTO'; +ALTER TABLE "user_renda_turbinada" ADD COLUMN "viesOverride" "ViesOverride" DEFAULT 'AUTO'; +ALTER TABLE "user_dividendos_internacional" ADD COLUMN "viesOverride" "ViesOverride" DEFAULT 'AUTO'; +ALTER TABLE "user_etfs" ADD COLUMN "viesOverride" "ViesOverride" DEFAULT 'AUTO'; +ALTER TABLE "user_projeto_america" ADD COLUMN "viesOverride" "ViesOverride" DEFAULT 'AUTO'; +ALTER TABLE "user_exterior_stocks" ADD COLUMN "viesOverride" "ViesOverride" DEFAULT 'AUTO'; diff --git a/prisma/migrations/20250204100000_add_cf_renda_turbinada_live_events/migration.sql b/prisma/migrations/20250204100000_add_cf_renda_turbinada_live_events/migration.sql new file mode 100644 index 000000000..5e0dc5c71 --- /dev/null +++ b/prisma/migrations/20250204100000_add_cf_renda_turbinada_live_events/migration.sql @@ -0,0 +1,15 @@ +-- CreateTable +CREATE TABLE "live_events" ( + "id" SERIAL PRIMARY KEY, + "title" TEXT NOT NULL, + "description" TEXT, + "event_date" TIMESTAMP(3) NOT NULL, + "duration_minutes" INTEGER NOT NULL DEFAULT 60, + "meeting_url" TEXT, + "is_cancelled" BOOLEAN NOT NULL DEFAULT FALSE, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Index to quickly fetch upcoming events by date +CREATE INDEX "live_events_event_date_idx" ON "live_events" ("event_date"); diff --git a/prisma/migrations/20250205103000_add_cf_renda_turbinada_lesson_materials/migration.sql b/prisma/migrations/20250205103000_add_cf_renda_turbinada_lesson_materials/migration.sql new file mode 100644 index 000000000..0423170ac --- /dev/null +++ b/prisma/migrations/20250205103000_add_cf_renda_turbinada_lesson_materials/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE "lesson_materials" ( + "id" SERIAL PRIMARY KEY, + "lessonId" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "fileUrl" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Index to speed up material lookups by lesson +CREATE INDEX "lesson_materials_lessonId_idx" ON "lesson_materials" ("lessonId"); + +-- AddForeignKey +ALTER TABLE "lesson_materials" + ADD CONSTRAINT "lesson_materials_lessonId_fkey" + FOREIGN KEY ("lessonId") REFERENCES "lessons"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250206153000_add_cf_renda_turbinada_mentorias/migration.sql b/prisma/migrations/20250206153000_add_cf_renda_turbinada_mentorias/migration.sql new file mode 100644 index 000000000..cf8d36010 --- /dev/null +++ b/prisma/migrations/20250206153000_add_cf_renda_turbinada_mentorias/migration.sql @@ -0,0 +1,33 @@ +-- CreateTable +CREATE TABLE "mentorias" ( + "id" SERIAL PRIMARY KEY, + "title" TEXT NOT NULL, + "description" TEXT, + "content" TEXT, + "video_url" TEXT, + "recorded_at" TIMESTAMP(3) NOT NULL, + "duration_minutes" INTEGER, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Index to quickly sort or filter mentorias by recorded date +CREATE INDEX "mentorias_recorded_at_idx" ON "mentorias" ("recorded_at" DESC); + +-- CreateTable +CREATE TABLE "mentoria_materials" ( + "id" SERIAL PRIMARY KEY, + "mentoria_id" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "file_url" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Index to speed up lookups of materials for a given mentoria +CREATE INDEX "mentoria_materials_mentoria_id_idx" ON "mentoria_materials" ("mentoria_id"); + +-- AddForeignKey +ALTER TABLE "mentoria_materials" + ADD CONSTRAINT "mentoria_materials_mentoria_id_fkey" + FOREIGN KEY ("mentoria_id") REFERENCES "mentorias"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250210123000_add_cf_renda_turbinada_mentoria_sort_order/migration.sql b/prisma/migrations/20250210123000_add_cf_renda_turbinada_mentoria_sort_order/migration.sql new file mode 100644 index 000000000..6c2dda268 --- /dev/null +++ b/prisma/migrations/20250210123000_add_cf_renda_turbinada_mentoria_sort_order/migration.sql @@ -0,0 +1,18 @@ +-- Add sort order column to mentorias to support manual reordering +ALTER TABLE "mentorias" ADD COLUMN "sort_order" INTEGER; + +-- Initialize existing mentorias with an explicit order based on current schedule +WITH ordered AS ( + SELECT + id, + ROW_NUMBER() OVER (ORDER BY COALESCE(release_date, recorded_at) DESC, id DESC) AS position + FROM "mentorias" +) +UPDATE "mentorias" AS m +SET "sort_order" = o.position +FROM ordered AS o +WHERE m.id = o.id; + +-- Index to speed up ordering queries for mentorias +CREATE INDEX IF NOT EXISTS "mentorias_sort_order_idx" + ON "mentorias" ("sort_order" ASC, "release_date" DESC, "recorded_at" DESC); diff --git a/prisma/migrations/20250309000000_update_provento_sincronizacao/migration.sql b/prisma/migrations/20250309000000_update_provento_sincronizacao/migration.sql new file mode 100644 index 000000000..e0821f547 --- /dev/null +++ b/prisma/migrations/20250309000000_update_provento_sincronizacao/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "proventos_sincronizacao" ALTER COLUMN "ticker" DROP NOT NULL; +ALTER TABLE "proventos_sincronizacao" ADD COLUMN "totalErros" INTEGER NOT NULL DEFAULT 0; diff --git a/prisma/migrations/20250310000000_use_data_pagamento_key/migration.sql b/prisma/migrations/20250310000000_use_data_pagamento_key/migration.sql new file mode 100644 index 000000000..1081f4e22 --- /dev/null +++ b/prisma/migrations/20250310000000_use_data_pagamento_key/migration.sql @@ -0,0 +1,5 @@ +-- DropIndex +DROP INDEX "unique_provento_ticker_dataCom_tipo"; + +-- CreateIndex +CREATE UNIQUE INDEX "unique_provento_ticker_dataPagamento_tipo" ON "proventos"("ticker", "dataPagamento", "tipo"); diff --git a/prisma/migrations/20250312090000_add_user_phone/migration.sql b/prisma/migrations/20250312090000_add_user_phone/migration.sql new file mode 100644 index 000000000..b70c9ddb1 --- /dev/null +++ b/prisma/migrations/20250312090000_add_user_phone/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "User" ADD COLUMN "phone" TEXT; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 000000000..2fe25d87c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1 @@ +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 000000000..a69522be9 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,1234 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +enum ViesOverride { + AUTO + MANTER +} + +enum ProventoOrigem { + BRAPI + CSV + MANUAL +} + +enum ProventoSincronizacaoStatus { + EM_PROGRESSO + CONCLUIDO + ERRO +} + +model User { + id String @id @default(cuid()) + firstName String + lastName String + email String @unique + phone String? + password String? + avatar String? + plan String @default("LITE") + status String @default("ACTIVE") + origin String @default("MANUAL") + hotmartCustomerId String? + expirationDate DateTime? + customPermissions String? @default("[]") + passwordCreatedAt DateTime? + mustChangePassword Boolean @default(true) + loginAttempts Int @default(0) + lockedUntil DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + lastLogin DateTime? + loginLogs LoginLog[] + resetTokens PasswordResetToken[] + purchases Purchase[] + analisesTrimestrais AnaliseTrimestreData[] @relation("AnalisesTrimestrais") + adminAnswers Answer[] @relation("AdminAnswers") + carteirasAnalista CarteiraAnalise[] @relation("CarteirasAnalista") + carteiras CarteiraAnalise[] @relation("CarteirasUsuario") + instagramCadastro InstagramCadastro? + notifications Notification[] @relation("UserNotifications") + proventoUploads ProventoUpload[] @relation("ProventoUploads") + questionsClosed Question[] @relation("QuestionsClosed") + userQuestions Question[] @relation("UserQuestions") + responseTemplates ResponseTemplate[] @relation("ResponseTemplates") + entitlements UserEntitlement[] + dividendos UserDividendos[] + dividendosInternacional UserDividendosInternacional[] + etfs UserEtfs[] + exteriorStocks UserExteriorStocks[] + fiis UserFiis[] + rendaTurbinada UserRendaTurbinada[] + microCaps UserMicroCaps[] + projetoAmerica UserProjetoAmerica[] + smallCaps UserSmallCaps[] + comments Comment[] + analisesTecnicas AnaliseTecnica[] + lessonProgress LessonProgress[] + lessonProgressTurma2 LessonProgressTurma2[] + lessonProgressTurma3 LessonProgressTurma3[] +} + +model Purchase { + id String @id @default(cuid()) + userId String + amount Float + productName String? + status String @default("COMPLETED") + hotmartTransactionId String? + paymentMethod String? + installments Int @default(1) + platform String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + user User @relation(fields: [userId], references: [id]) +} + +model PlanMigration { + id String @id @default(cuid()) + userId String + planOriginal String + planNovo String + valorOriginal Float + valorNovo Float + mesesRestantes Int + valorDesconto Float + valorFinal Float + status String @default("PENDING") + observacoes String? + processadoPor String + dataCriacao DateTime @default(now()) + dataProcessamento DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("plan_migrations") +} + +model PasswordResetToken { + id String @id @default(cuid()) + userId String + token String @unique + expiresAt DateTime + used Boolean @default(false) + createdAt DateTime @default(now()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model LoginLog { + id String @id @default(cuid()) + userId String? + email String + success Boolean + ip String? + userAgent String? + createdAt DateTime @default(now()) + user User? @relation(fields: [userId], references: [id]) +} + +model CarteiraAnalise { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + nomeArquivo String + arquivoUrl String + valorTotal Float? + quantidadeAtivos Int? + status String @default("PENDENTE") + dataEnvio DateTime @default(now()) + dataAnalise DateTime? + userId String + analistaId String? + feedback String? + recomendacoes String? + pontuacao Float? + riscoBeneficio String? + diversificacao Float? + questionario String? + avaliacaoAdaptacao Int? + avaliacaoDiversificacao Int? + avaliacaoQualidade Int? + dadosEstruturados String? + ativos AtivoCarteira[] + analista User? @relation("CarteirasAnalista", fields: [analistaId], references: [id]) + user User @relation("CarteirasUsuario", fields: [userId], references: [id], onDelete: Cascade) + + @@map("carteiras_analise") +} + +model AtivoCarteira { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + codigo String + quantidade Float + precoMedio Float + valorTotal Float + tipo String + setor String? + carteiraId String + carteira CarteiraAnalise @relation(fields: [carteiraId], references: [id], onDelete: Cascade) + + @@map("ativos_carteira") +} + +model InstagramCadastro { + id String @id @default(cuid()) + userId String @unique + instagram String + previousInstagram String? + isUpdated Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("instagram_cadastros") +} + +model CfRendaTurbinadaCarteiraOperacao { + id String @id @default(cuid()) + ordem Int + ativoBase String + codigoOpcao String + strike Float? + premio Float? + premioCustom String? + rendaPercentual Float? + garantia Float? + precoReferencia Float? + vencimento String + tipoOperacao String + status String + observacao String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([tipoOperacao]) + @@map("cf_renda_turbinada_carteira_operacoes") +} + +model AnaliseTecnica { + id String @id @default(cuid()) + ticker String + slug String @unique + titulo String + resumo String? + conteudo String + imagemCapa String? + fonte String? + publicado Boolean @default(true) + destaque Boolean @default(false) + autorId String? + autor User? @relation(fields: [autorId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([ticker]) + @@map("analises_tecnicas") +} + +model HotmartIntegration { + id String @id @default(cuid()) + name String + token String @unique + plan String + status String @default("ACTIVE") + totalSales Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("hotmart_integrations") +} + +model UserEntitlement { + id String @id @default(cuid()) + userId String + code String + status String @default("ACTIVE") + expiresAt DateTime? + origin String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId, code]) +} + +model RelatorioSemanal { + id String @id @default(cuid()) + date String? + weekOf String? + macro Json @default("[]") + proventos Json @default("[]") + dividendos Json @default("[]") + smallCaps Json @default("[]") + microCaps Json @default("[]") + exterior Json @default("[]") + authorId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + autor String? + dataPublicacao String? + exteriorDividendos Json @default("[]") + exteriorETFs Json @default("[]") + exteriorProjetoAmerica Json @default("[]") + exteriorStocks Json @default("[]") + semana String? + titulo String? + status String @default("draft") + + @@map("relatorio_semanal") +} + +model UserMicroCaps { + id String @id @default(cuid()) + userId String + ticker String + setor String + dataEntrada String + precoEntrada Float + precoTeto Float? + precoTetoBDR Float? + viesOverride ViesOverride? @default(AUTO) + posicaoEncerrada Boolean @default(false) + dataSaida String? + precoSaida Float? + motivoEncerramento String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + editadoEm DateTime @default(now()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId, ticker]) + @@map("user_micro_caps") +} + +model UserSmallCaps { + id String @id @default(cuid()) + userId String + ticker String + setor String + dataEntrada String + precoEntrada Float + precoTeto Float? + viesOverride ViesOverride? @default(AUTO) + posicaoEncerrada Boolean @default(false) + dataSaida String? + precoSaida Float? + motivoEncerramento String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + editadoEm DateTime @default(now()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId, ticker]) + @@map("user_small_caps") +} + +model UserDividendos { + id String @id @default(cuid()) + userId String + ticker String + setor String + dataEntrada String + precoEntrada Float + precoTeto Float? + viesOverride ViesOverride? @default(AUTO) + posicaoEncerrada Boolean @default(false) + dataSaida String? + precoSaida Float? + motivoEncerramento String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + editadoEm DateTime @default(now()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId, ticker]) + @@map("user_dividendos") +} + +model UserFiis { + id String @id @default(cuid()) + userId String + ticker String + setor String + dataEntrada String + precoEntrada Float + precoTeto Float? + viesOverride ViesOverride? @default(AUTO) + posicaoEncerrada Boolean @default(false) + dataSaida String? + precoSaida Float? + motivoEncerramento String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + editadoEm DateTime @default(now()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId, ticker]) + @@map("user_fiis") +} + +model UserRendaTurbinada { + id String @id @default(cuid()) + userId String + ticker String + setor String + dataEntrada String + precoEntrada Float + precoTeto Float? + viesOverride ViesOverride? @default(AUTO) + strike Float? + premioEntrada Float? + vencimento String? + tipoOperacao String? + posicaoEncerrada Boolean @default(false) + dataSaida String? + precoSaida Float? + motivoEncerramento String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + editadoEm DateTime @default(now()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("user_renda_turbinada") +} + +model UserDividendosInternacional { + id String @id @default(cuid()) + userId String + ticker String + setor String + dataEntrada String + precoEntrada Float + precoTeto Float? + precoTetoBDR Float? + viesOverride ViesOverride? @default(AUTO) + posicaoEncerrada Boolean @default(false) + dataSaida String? + precoSaida Float? + motivoEncerramento String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + editadoEm DateTime @default(now()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId, ticker]) + @@map("user_dividendos_internacional") +} + +model UserEtfs { + id String @id @default(cuid()) + userId String + ticker String + setor String + dataEntrada String + precoEntrada Float + viesOverride ViesOverride? @default(AUTO) + posicaoEncerrada Boolean @default(false) + dataSaida String? + precoSaida Float? + motivoEncerramento String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + editadoEm DateTime @default(now()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId, ticker]) + @@map("user_etfs") +} + +model UserProjetoAmerica { + id String @id @default(cuid()) + userId String + ticker String + setor String + dataEntrada String + precoEntrada Float + precoTeto Float? + precoTetoBDR Float? + viesOverride ViesOverride? @default(AUTO) + posicaoEncerrada Boolean @default(false) + dataSaida String? + precoSaida Float? + motivoEncerramento String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + editadoEm DateTime @default(now()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId, ticker]) + @@map("user_projeto_america") +} + +model UserExteriorStocks { + id String @id @default(cuid()) + userId String + ticker String + setor String + dataEntrada String + precoEntrada Float + precoTeto Float? + precoTetoBDR Float? + viesOverride ViesOverride? @default(AUTO) + posicaoEncerrada Boolean @default(false) + dataSaida String? + precoSaida Float? + motivoEncerramento String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + editadoEm DateTime @default(now()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId, ticker]) + @@map("user_exterior_stocks") +} + +model Provento { + id String @id @default(cuid()) + ticker String + valor Float + tipo String + data String + dataObj DateTime + dataCom String? + dataPagamento String? + dataFormatada String + valorFormatado String + dividendYield Float? + nomeEmpresa String? + setor String? + fonte String? + observacoes String? + origem ProventoOrigem @default(CSV) + dataUltimaAtualizacao DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([ticker, dataObj, valor, tipo], name: "unique_provento") + @@unique([ticker, dataPagamento, tipo], name: "unique_provento_ticker_dataPagamento_tipo") + @@index([ticker]) + @@index([dataObj]) + @@index([ticker, dataObj]) + @@index([tipo]) + @@index([createdAt]) + @@map("proventos") +} + +model ProventoSincronizacao { + id Int @id @default(autoincrement()) + ticker String? + dataInicio DateTime + dataFim DateTime? + status ProventoSincronizacaoStatus + totalProcessados Int @default(0) + totalNovos Int @default(0) + totalAtualizados Int @default(0) + totalErros Int @default(0) + erros Json? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([ticker]) + @@map("proventos_sincronizacao") +} + +model ProventoEstatistica { + id String @id @default(cuid()) + ticker String @unique + nomeEmpresa String? + setor String? + totalProventos Int @default(0) + valorTotal Float @default(0) + valorMedio Float @default(0) + ultimoProvento DateTime? + primeiroProvento DateTime? + totalDividendos Int @default(0) + totalJCP Int @default(0) + totalBonus Int @default(0) + valorDividendos Float @default(0) + valorJCP Float @default(0) + valorBonus Float @default(0) + dividendYieldMedio Float? + ultimaAtualizacao DateTime @default(now()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([ticker]) + @@index([ultimaAtualizacao]) + @@map("proventos_estatisticas") +} + +model ProventoUpload { + id String @id @default(cuid()) + nomeArquivo String + tamanhoArquivo Int + totalLinhas Int + linhasProcessadas Int + linhasComErro Int + tempoProcessamento Int? + proventosCriados Int @default(0) + proventosAtualizados Int @default(0) + formatoDetectado String? + errosDetalhados String? + userId String? + iniciadoEm DateTime @default(now()) + finalizadoEm DateTime? + createdAt DateTime @default(now()) + user User? @relation("ProventoUploads", fields: [userId], references: [id]) + + @@index([userId]) + @@index([createdAt]) + @@map("proventos_uploads") +} + +model EventoCorporativo { + id String @id @default(cuid()) + ticker String + tipo_evento String + titulo String + data_evento DateTime + descricao String + status String + prioridade String? + url_externo String? + observacoes String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("eventos_corporativos") +} + +model Relatorio { + id String @id @default(cuid()) + ticker String + nome String + tipo String + dataReferencia String + linkCanva String? + linkExterno String? + tipoVisualizacao String @default("iframe") + arquivoPdf String? + nomeArquivoPdf String? + tamanhoArquivo Int? + tipoPdf String? + hashArquivo String? + solicitarReupload Boolean @default(false) + dataUpload DateTime @default(now()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + reportFiles ReportFile[] + + @@index([ticker]) + @@index([tipo]) + @@index([dataUpload]) + @@index([ticker, tipo]) + @@index([ticker, dataReferencia]) + @@map("relatorios") +} + +model ReportFile { + reportId String + pdfUrl String? + status String + createdAt DateTime @default(now()) + report Relatorio @relation(fields: [reportId], references: [id], onDelete: Cascade) + + @@id([reportId, createdAt]) + @@index([reportId]) + @@map("report_files") +} + +model AnaliseTrimestreData { + id String @id @default(cuid()) + ticker String + empresa String + trimestre String? + dataPublicacao DateTime @default(now()) + autor String? + categoria String @default("resultado_trimestral") + titulo String + resumoExecutivo String? + analiseCompleta String? + metricas Json @default("{}") + pontosFavoraveis String? + pontosAtencao String? + recomendacao String @default("MANTER") + precoAlvo Float? + risco String @default("MÉDIO") + linkResultado String? + linkConferencia String? + status String @default("draft") + visualizacoes Int @default(0) + userId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + badges Json? + dadosPeriodo Json? + fonteDados String? @default("manual") + historicoComparativo Json? + ultimaAtualizacaoBrapi DateTime? + nota Float? + user User? @relation("AnalisesTrimestrais", fields: [userId], references: [id]) + + @@unique([ticker, trimestre, titulo], name: "unique_analise_trimestre") + @@index([ticker]) + @@index([trimestre]) + @@index([status]) + @@index([categoria]) + @@index([dataPublicacao]) + @@index([ticker, trimestre]) + @@index([ticker, status]) + @@index([userId]) + @@map("analises_trimestrais") +} + +model Question { + id String @id @default(cuid()) + title String + content String + category QuestionCategory @default(GERAL) // deprecated + priority QuestionPriority @default(NORMAL) + status QuestionStatus @default(NOVA) + userId String + readByAdmin Boolean @default(false) + closedAt DateTime? + closedBy String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + answers Answer[] + comments Comment[] + closer User? @relation("QuestionsClosed", fields: [closedBy], references: [id]) + user User @relation("UserQuestions", fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([status]) + @@index([createdAt]) + @@index([userId, status]) + @@map("questions") +} + +model Answer { + id String @id @default(cuid()) + content String + questionId String + adminId String + isOfficial Boolean @default(true) + readByUser Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + faqOrder Int @default(0) + faqTitle String? + isFaq Boolean @default(false) + admin User @relation("AdminAnswers", fields: [adminId], references: [id]) + question Question @relation(fields: [questionId], references: [id], onDelete: Cascade) + comments Comment[] + + @@index([questionId]) + @@index([adminId]) + @@index([createdAt]) + @@index([isFaq]) + @@index([faqOrder]) + @@map("answers") +} + +model Comment { + id String @id @default(cuid()) + content String + questionId String + answerId String? + authorId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + question Question @relation(fields: [questionId], references: [id], onDelete: Cascade) + answer Answer? @relation(fields: [answerId], references: [id], onDelete: Cascade) + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + + @@index([questionId]) + @@index([answerId]) + @@index([authorId]) + @@map("comments") +} + +model ResponseTemplate { + id String @id @default(cuid()) + title String + content String + category QuestionCategory @default(GERAL) + isActive Boolean @default(true) + usageCount Int @default(0) + createdBy String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + creator User @relation("ResponseTemplates", fields: [createdBy], references: [id]) + + @@index([category]) + @@index([isActive]) + @@index([createdBy]) + @@index([category, isActive]) + @@map("response_templates") +} + +model Notification { + id String @id @default(cuid()) + userId String + title String + message String + type String @default("info") + category String @default("general") + read Boolean @default(false) + actionUrl String? + metadata Json? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + user User @relation("UserNotifications", fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([read]) + @@index([createdAt]) + @@index([userId, read]) + @@index([category]) + @@index([type]) + @@map("notifications") +} + +enum QuestionStatus { + NOVA + RESPONDIDA + FECHADA +} + +enum QuestionPriority { + BAIXA + NORMAL + URGENTE +} + +enum QuestionCategory { + SMALL_CAPS + MICRO_CAPS + DIVIDENDOS + FIIS + INTERNACIONAL_ETFS + INTERNACIONAL_STOCKS + INTERNACIONAL_DIVIDENDOS + PROJETO_AMERICA + GERAL + TECNICO + FISCAL +} + +model AtivoInformacao { + id String @id @default(cuid()) + codigo String @unique // ITUB4, PETR4, etc. + nome String // Itaú Unibanco, Petrobras, etc. + setor String // Bancário, Petróleo, etc. + subsetor String? // Bancos Múltiplos, Exploração, etc. + tipo String // ACAO, FII, ETF, etc. + + // Análise Padrão + qualidade Int @default(7) // 0-10 + risco String @default("MEDIO") // BAIXO, MEDIO, ALTO + recomendacao String @default("NEUTRO") // COMPRA, VENDA, NEUTRO, MANTER + fundamentosResumo String? // Resumo dos fundamentos + pontosFortes String? // Pontos positivos (JSON array como string) + pontosFracos String? // Pontos negativos (JSON array como string) + observacoes String? // Observações gerais + + // Categorização + segmento String? // Large Cap, Small Cap, etc. + governanca String? // N1, N2, NM, etc. + dividend_yield Float? // Último dividend yield conhecido + + // Metadata + ativo Boolean @default(true) + ultimaRevisao DateTime @default(now()) + criadoPor String? // ID do admin que criou + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("ativos_informacao") +} + +model Report { + id String @id @default(cuid()) + canvaId String + name String? + iframeUrl String? + status String @default("pending") + progress Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([status]) + @@map("reports") +} + +model CfRendaTurbinadaModule { + id String @id @default(cuid()) + title String + highlight String? + releaseDate DateTime + isHidden Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + sortOrder Int? + lessons CfRendaTurbinadaLesson[] + + @@map("modules") +} + +model CfRendaTurbinadaLesson { + id Int @id @default(autoincrement()) + moduleId String + title String + duration String + description String? + videoUrl String? + content String? + sortOrder Int? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + module CfRendaTurbinadaModule @relation(fields: [moduleId], references: [id], onDelete: Cascade) + materials CfRendaTurbinadaLessonMaterial[] + progress LessonProgress[] + + @@map("lessons") +} + +model LessonProgress { + id Int @id @default(autoincrement()) + userId String @map("user_id") + lessonId Int @map("lesson_id") + completedAt DateTime @default(now()) @map("completed_at") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + lesson CfRendaTurbinadaLesson @relation(fields: [lessonId], references: [id], onDelete: Cascade) + + @@unique([userId, lessonId]) + @@index([userId]) + @@index([lessonId]) + @@map("lesson_progress") +} + +model CfRendaTurbinadaLessonMaterial { + id Int @id @default(autoincrement()) + lessonId Int + title String + fileUrl String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + lesson CfRendaTurbinadaLesson @relation(fields: [lessonId], references: [id], onDelete: Cascade) + + @@map("lesson_materials") +} + +model CfRendaTurbinadaTurma2Module { + id String @id @default(cuid()) + title String + highlight String? + releaseDate DateTime + isHidden Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + sortOrder Int? + lessons CfRendaTurbinadaTurma2Lesson[] + + @@map("modules_turma2") +} + +model CfRendaTurbinadaTurma2Lesson { + id Int @id @default(autoincrement()) + moduleId String + title String + duration String + description String? + videoUrl String? + content String? + sortOrder Int? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + module CfRendaTurbinadaTurma2Module @relation(fields: [moduleId], references: [id], onDelete: Cascade) + materials CfRendaTurbinadaTurma2LessonMaterial[] + progress LessonProgressTurma2[] + + @@map("lessons_turma2") +} + +model CfRendaTurbinadaTurma2LessonMaterial { + id Int @id @default(autoincrement()) + lessonId Int + title String + fileUrl String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + lesson CfRendaTurbinadaTurma2Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade) + + @@map("lesson_materials_turma2") +} + +model LessonProgressTurma2 { + id Int @id @default(autoincrement()) + userId String @map("user_id") + lessonId Int @map("lesson_id") + completedAt DateTime @default(now()) @map("completed_at") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + lesson CfRendaTurbinadaTurma2Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade) + + @@unique([userId, lessonId]) + @@index([userId]) + @@index([lessonId]) + @@map("lesson_progress_turma2") +} + +model CfRendaTurbinadaTurma3Module { + id String @id @default(cuid()) + title String + highlight String? + releaseDate DateTime + isHidden Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + sortOrder Int? + lessons CfRendaTurbinadaTurma3Lesson[] + + @@map("modules_turma3") +} + +model CfRendaTurbinadaTurma3Lesson { + id Int @id @default(autoincrement()) + moduleId String + title String + duration String + description String? + videoUrl String? + content String? + sortOrder Int? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + module CfRendaTurbinadaTurma3Module @relation(fields: [moduleId], references: [id], onDelete: Cascade) + materials CfRendaTurbinadaTurma3LessonMaterial[] + progress LessonProgressTurma3[] + + @@map("lessons_turma3") +} + +model CfRendaTurbinadaTurma3LessonMaterial { + id Int @id @default(autoincrement()) + lessonId Int + title String + fileUrl String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + lesson CfRendaTurbinadaTurma3Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade) + + @@map("lesson_materials_turma3") +} + +model LessonProgressTurma3 { + id Int @id @default(autoincrement()) + userId String @map("user_id") + lessonId Int @map("lesson_id") + completedAt DateTime @default(now()) @map("completed_at") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + lesson CfRendaTurbinadaTurma3Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade) + + @@unique([userId, lessonId]) + @@index([userId]) + @@index([lessonId]) + @@map("lesson_progress_turma3") +} + +model CfRendaTurbinadaLiveEvent { + id Int @id @default(autoincrement()) + title String + description String? @map("description") + eventDate DateTime @map("event_date") + durationMinutes Int @default(60) @map("duration_minutes") + meetingUrl String? @map("meeting_url") + isCancelled Boolean @default(false) @map("is_cancelled") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@map("live_events") +} + +model CfRendaTurbinadaTurma2LiveEvent { + id Int @id @default(autoincrement()) + title String + description String? @map("description") + eventDate DateTime @map("event_date") + durationMinutes Int @default(60) @map("duration_minutes") + meetingUrl String? @map("meeting_url") + isCancelled Boolean @default(false) @map("is_cancelled") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@map("live_events_turma2") +} + +model CfRendaTurbinadaTurma3LiveEvent { + id Int @id @default(autoincrement()) + title String + description String? @map("description") + eventDate DateTime @map("event_date") + durationMinutes Int @default(60) @map("duration_minutes") + meetingUrl String? @map("meeting_url") + isCancelled Boolean @default(false) @map("is_cancelled") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@map("live_events_turma3") +} + +model CfRendaTurbinadaMentoria { + id Int @id @default(autoincrement()) + title String + description String? + content String? + videoUrl String? @map("video_url") + recordedAt DateTime @map("recorded_at") + releaseDate DateTime? @map("release_date") + durationMinutes Int? @map("duration_minutes") + sortOrder Int? @map("sort_order") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + materials CfRendaTurbinadaMentoriaMaterial[] + + @@map("mentorias") +} + +model CfRendaTurbinadaMentoriaMaterial { + id Int @id @default(autoincrement()) + mentoriaId Int @map("mentoria_id") + title String + fileUrl String @map("file_url") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + mentoria CfRendaTurbinadaMentoria @relation(fields: [mentoriaId], references: [id], onDelete: Cascade) + + @@map("mentoria_materials") +} + +model CfRendaTurbinadaTurma2Mentoria { + id Int @id @default(autoincrement()) + title String + description String? + content String? + videoUrl String? @map("video_url") + recordedAt DateTime @map("recorded_at") + releaseDate DateTime? @map("release_date") + durationMinutes Int? @map("duration_minutes") + sortOrder Int? @map("sort_order") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + materials CfRendaTurbinadaTurma2MentoriaMaterial[] + + @@map("mentorias_turma2") +} + +model CfRendaTurbinadaTurma2MentoriaMaterial { + id Int @id @default(autoincrement()) + mentoriaId Int @map("mentoria_id") + title String + fileUrl String @map("file_url") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + mentoria CfRendaTurbinadaTurma2Mentoria @relation(fields: [mentoriaId], references: [id], onDelete: Cascade) + + @@map("mentoria_materials_turma2") +} + +model CfRendaTurbinadaTurma3Mentoria { + id Int @id @default(autoincrement()) + title String + description String? + content String? + videoUrl String? @map("video_url") + recordedAt DateTime @map("recorded_at") + releaseDate DateTime? @map("release_date") + durationMinutes Int? @map("duration_minutes") + sortOrder Int? @map("sort_order") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + materials CfRendaTurbinadaTurma3MentoriaMaterial[] + + @@map("mentorias_turma3") +} + +model CfRendaTurbinadaTurma3MentoriaMaterial { + id Int @id @default(autoincrement()) + mentoriaId Int @map("mentoria_id") + title String + fileUrl String @map("file_url") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + mentoria CfRendaTurbinadaTurma3Mentoria @relation(fields: [mentoriaId], references: [id], onDelete: Cascade) + + @@map("mentoria_materials_turma3") +} + +model BastidoresDaBolsaModule { + id String @id @default(cuid()) + title String + highlight String? + releaseDate DateTime + isHidden Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + sortOrder Int? + lessons BastidoresDaBolsaLesson[] + + @@map("bastidores_modules") +} + +model BastidoresDaBolsaLesson { + id Int @id @default(autoincrement()) + moduleId String + title String + duration String + description String? + videoUrl String? + content String? + sortOrder Int? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + module BastidoresDaBolsaModule @relation(fields: [moduleId], references: [id], onDelete: Cascade) + materials BastidoresDaBolsaLessonMaterial[] + + @@map("bastidores_lessons") +} + +model BastidoresDaBolsaLessonMaterial { + id Int @id @default(autoincrement()) + lessonId Int + title String + fileUrl String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + lesson BastidoresDaBolsaLesson @relation(fields: [lessonId], references: [id], onDelete: Cascade) + + @@map("bastidores_lesson_materials") +} + +model BastidoresDaBolsaLiveEvent { + id Int @id @default(autoincrement()) + title String + description String? @map("description") + eventDate DateTime @map("event_date") + durationMinutes Int @default(60) @map("duration_minutes") + meetingUrl String? @map("meeting_url") + isCancelled Boolean @default(false) @map("is_cancelled") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@map("bastidores_live_events") +} diff --git a/prisma/seed-simple.js b/prisma/seed-simple.js new file mode 100644 index 000000000..b3840c8a5 --- /dev/null +++ b/prisma/seed-simple.js @@ -0,0 +1,40 @@ +require('dotenv').config({ path: '.env.local' }); + +const { PrismaClient } = require('@prisma/client'); + +// Se ainda der erro de DATABASE_URL, usar URL direta temporariamente +const prisma = new PrismaClient({ + datasources: { + db: { + url: process.env.DATABASE_URL || "postgresql://neondb_owner:npg_aOq2Kmpjiv9Q@ep-noisy-wind-a8b1knv2-pooler.eastus2.azure.neon.tech/neondb?sslmode=require&channel_binding=require" + } + } +}); + +async function main() { + console.log('ðŸ›¡ï¸ Criando admin...'); + + // Senha já hasheada para "Admin123!" + const hashedPassword = '$2a$10$N9qo8uLOickgx2ZMRZoMye1IVQ9VqKJoOB9gw3zVdG3d0F3kGqP2.'; + + const admin = await prisma.user.upsert({ + where: { email: 'admin@fatosdobolsa.com' }, + update: {}, + create: { + email: 'admin@fatosdobolsa.com', + firstName: 'Admin', + lastName: 'Sistema', + password: hashedPassword, + plan: 'ADMIN', + status: 'ACTIVE' + } + }); + + console.log('✅ Admin criado:', admin.email); + console.log('📧 Email: admin@fatosdobolsa.com'); + console.log('🔑 Senha: Admin123!'); +} + +main() + .catch(console.error) + .finally(() => prisma.$disconnect()); \ No newline at end of file diff --git a/prisma/seed.js b/prisma/seed.js new file mode 100644 index 000000000..a9b13f6bf --- /dev/null +++ b/prisma/seed.js @@ -0,0 +1,29 @@ +const { PrismaClient } = require('@prisma/client'); +const bcrypt = require('bcryptjs'); + +const prisma = new PrismaClient(); + +async function main() { + console.log('ðŸ›¡ï¸ Criando admin...'); + + const hashedPassword = await bcrypt.hash('Admin123!', 10); + + const admin = await prisma.user.upsert({ + where: { email: 'admin@fatosdobolsa.com' }, + update: {}, + create: { + email: 'admin@fatosdobolsa.com', + firstName: 'Admin', + lastName: 'Sistema', + password: hashedPassword, + plan: 'ADMIN', + status: 'ACTIVE' + } + }); + + console.log('✅ Admin criado:', admin.email); +} + +main() + .catch(console.error) + .finally(() => prisma.$disconnect()); \ No newline at end of file diff --git a/public/assets/auth-widgets.png b/public/assets/auth-widgets.png new file mode 100644 index 000000000..46ae88d3d Binary files /dev/null and b/public/assets/auth-widgets.png differ diff --git a/public/assets/avatar-1.png b/public/assets/avatar-1.png new file mode 100644 index 000000000..a0cbb823d Binary files /dev/null and b/public/assets/avatar-1.png differ diff --git a/public/assets/avatar-10.png b/public/assets/avatar-10.png new file mode 100644 index 000000000..8ca22945b Binary files /dev/null and b/public/assets/avatar-10.png differ diff --git a/public/assets/avatar-11.png b/public/assets/avatar-11.png new file mode 100644 index 000000000..de9e4ad63 Binary files /dev/null and b/public/assets/avatar-11.png differ diff --git a/public/assets/avatar-2.png b/public/assets/avatar-2.png new file mode 100644 index 000000000..deb078ec6 Binary files /dev/null and b/public/assets/avatar-2.png differ diff --git a/public/assets/avatar-3.png b/public/assets/avatar-3.png new file mode 100644 index 000000000..43fe3587b Binary files /dev/null and b/public/assets/avatar-3.png differ diff --git a/public/assets/avatar-36.png b/public/assets/avatar-36.png new file mode 100644 index 000000000..a749867ae Binary files /dev/null and b/public/assets/avatar-36.png differ diff --git a/public/assets/avatar-4.png b/public/assets/avatar-4.png new file mode 100644 index 000000000..bfe926c2d Binary files /dev/null and b/public/assets/avatar-4.png differ diff --git a/public/assets/avatar-5.png b/public/assets/avatar-5.png new file mode 100644 index 000000000..624c12c25 Binary files /dev/null and b/public/assets/avatar-5.png differ diff --git a/public/assets/avatar-6.png b/public/assets/avatar-6.png new file mode 100644 index 000000000..44c15978a Binary files /dev/null and b/public/assets/avatar-6.png differ diff --git a/public/assets/avatar-7.png b/public/assets/avatar-7.png new file mode 100644 index 000000000..f67f21bb5 Binary files /dev/null and b/public/assets/avatar-7.png differ diff --git a/public/assets/avatar-8.png b/public/assets/avatar-8.png new file mode 100644 index 000000000..b49479568 Binary files /dev/null and b/public/assets/avatar-8.png differ diff --git a/public/assets/avatar-9.png b/public/assets/avatar-9.png new file mode 100644 index 000000000..f24404c2e Binary files /dev/null and b/public/assets/avatar-9.png differ diff --git a/public/assets/avatar.png b/public/assets/avatar.png new file mode 100644 index 000000000..12c16e77f Binary files /dev/null and b/public/assets/avatar.png differ diff --git a/public/assets/error-401.png b/public/assets/error-401.png new file mode 100644 index 000000000..f51381a63 Binary files /dev/null and b/public/assets/error-401.png differ diff --git a/public/assets/error-404.png b/public/assets/error-404.png new file mode 100644 index 000000000..301010c93 Binary files /dev/null and b/public/assets/error-404.png differ diff --git a/public/assets/error-500.png b/public/assets/error-500.png new file mode 100644 index 000000000..f951ebf60 Binary files /dev/null and b/public/assets/error-500.png differ diff --git a/public/assets/fatos-da-bolsa-pro.png b/public/assets/fatos-da-bolsa-pro.png new file mode 100644 index 000000000..4a3bc347b Binary files /dev/null and b/public/assets/fatos-da-bolsa-pro.png differ diff --git a/public/assets/logo--dark.svg b/public/assets/logo--dark.svg new file mode 100644 index 000000000..114a3a4af --- /dev/null +++ b/public/assets/logo--dark.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/assets/logo-dropbox.png b/public/assets/logo-dropbox.png new file mode 100644 index 000000000..27b50314d Binary files /dev/null and b/public/assets/logo-dropbox.png differ diff --git a/public/assets/logo-emblem--dark.svg b/public/assets/logo-emblem--dark.svg new file mode 100644 index 000000000..e65f428e0 --- /dev/null +++ b/public/assets/logo-emblem--dark.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/public/assets/logo-emblem.svg b/public/assets/logo-emblem.svg new file mode 100644 index 000000000..84a6d0287 --- /dev/null +++ b/public/assets/logo-emblem.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/public/assets/logo-github.png b/public/assets/logo-github.png new file mode 100644 index 000000000..cf13db0f8 Binary files /dev/null and b/public/assets/logo-github.png differ diff --git a/public/assets/logo-lyft.png b/public/assets/logo-lyft.png new file mode 100644 index 000000000..142ec7460 Binary files /dev/null and b/public/assets/logo-lyft.png differ diff --git a/public/assets/logo-medium.png b/public/assets/logo-medium.png new file mode 100644 index 000000000..b4b103bf8 Binary files /dev/null and b/public/assets/logo-medium.png differ diff --git a/public/assets/logo-slack.png b/public/assets/logo-slack.png new file mode 100644 index 000000000..5bfd2d941 Binary files /dev/null and b/public/assets/logo-slack.png differ diff --git a/public/assets/logo-squarespace.png b/public/assets/logo-squarespace.png new file mode 100644 index 000000000..bd9809214 Binary files /dev/null and b/public/assets/logo-squarespace.png differ diff --git a/public/assets/logo.svg b/public/assets/logo.svg new file mode 100644 index 000000000..2fd10e88d --- /dev/null +++ b/public/assets/logo.svg @@ -0,0 +1 @@ + diff --git a/public/assets/product-1.png b/public/assets/product-1.png new file mode 100644 index 000000000..a2c527171 Binary files /dev/null and b/public/assets/product-1.png differ diff --git a/public/assets/product-2.png b/public/assets/product-2.png new file mode 100644 index 000000000..dde7db4ff Binary files /dev/null and b/public/assets/product-2.png differ diff --git a/public/assets/product-3.png b/public/assets/product-3.png new file mode 100644 index 000000000..9229198eb Binary files /dev/null and b/public/assets/product-3.png differ diff --git a/public/assets/product-4.png b/public/assets/product-4.png new file mode 100644 index 000000000..0b99ad4ab Binary files /dev/null and b/public/assets/product-4.png differ diff --git a/public/assets/product-5.png b/public/assets/product-5.png new file mode 100644 index 000000000..631be137b Binary files /dev/null and b/public/assets/product-5.png differ diff --git a/public/assets/thumbnail.png b/public/assets/thumbnail.png new file mode 100644 index 000000000..4b952467d Binary files /dev/null and b/public/assets/thumbnail.png differ diff --git a/public/favicon.ico b/public/favicon.ico index 56c61fb73..19fd3f70b 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html old mode 100755 new mode 100644 diff --git a/public/logos-fiis/AFHI11.png b/public/logos-fiis/AFHI11.png new file mode 100644 index 000000000..d8edd7850 Binary files /dev/null and b/public/logos-fiis/AFHI11.png differ diff --git a/public/logos-fiis/ALZR11.png b/public/logos-fiis/ALZR11.png new file mode 100644 index 000000000..1b15f960c Binary files /dev/null and b/public/logos-fiis/ALZR11.png differ diff --git a/public/logos-fiis/BCIA11.png b/public/logos-fiis/BCIA11.png new file mode 100644 index 000000000..1d3b8b360 Binary files /dev/null and b/public/logos-fiis/BCIA11.png differ diff --git a/public/logos-fiis/BCRI11.png b/public/logos-fiis/BCRI11.png new file mode 100644 index 000000000..0cf266c84 Binary files /dev/null and b/public/logos-fiis/BCRI11.png differ diff --git a/public/logos-fiis/BRCO11.png b/public/logos-fiis/BRCO11.png new file mode 100644 index 000000000..3fd0ed442 Binary files /dev/null and b/public/logos-fiis/BRCO11.png differ diff --git a/public/logos-fiis/BTLG11.png b/public/logos-fiis/BTLG11.png new file mode 100644 index 000000000..a3eed9db0 Binary files /dev/null and b/public/logos-fiis/BTLG11.png differ diff --git a/public/logos-fiis/DEVA11 (2).png b/public/logos-fiis/DEVA11 (2).png new file mode 100644 index 000000000..e5dd5be96 Binary files /dev/null and b/public/logos-fiis/DEVA11 (2).png differ diff --git a/public/logos-fiis/DEVA11.png b/public/logos-fiis/DEVA11.png new file mode 100644 index 000000000..4bfe5a29d Binary files /dev/null and b/public/logos-fiis/DEVA11.png differ diff --git a/public/logos-fiis/HGBS11.png b/public/logos-fiis/HGBS11.png new file mode 100644 index 000000000..de567b83f Binary files /dev/null and b/public/logos-fiis/HGBS11.png differ diff --git a/public/logos-fiis/HGLG11.png b/public/logos-fiis/HGLG11.png new file mode 100644 index 000000000..33f48efa2 Binary files /dev/null and b/public/logos-fiis/HGLG11.png differ diff --git a/public/logos-fiis/HGRU11.png b/public/logos-fiis/HGRU11.png new file mode 100644 index 000000000..a01aeba94 Binary files /dev/null and b/public/logos-fiis/HGRU11.png differ diff --git a/public/logos-fiis/HSML11.png b/public/logos-fiis/HSML11.png new file mode 100644 index 000000000..a078ad7c3 Binary files /dev/null and b/public/logos-fiis/HSML11.png differ diff --git a/public/logos-fiis/IRDM11.png b/public/logos-fiis/IRDM11.png new file mode 100644 index 000000000..9ee8a480c Binary files /dev/null and b/public/logos-fiis/IRDM11.png differ diff --git a/public/logos-fiis/KISU11.png b/public/logos-fiis/KISU11.png new file mode 100644 index 000000000..5c0d95263 Binary files /dev/null and b/public/logos-fiis/KISU11.png differ diff --git a/public/logos-fiis/KNHF11.png b/public/logos-fiis/KNHF11.png new file mode 100644 index 000000000..95b660d26 Binary files /dev/null and b/public/logos-fiis/KNHF11.png differ diff --git a/public/logos-fiis/KNRI11.png b/public/logos-fiis/KNRI11.png new file mode 100644 index 000000000..fb8b77eac Binary files /dev/null and b/public/logos-fiis/KNRI11.png differ diff --git a/public/logos-fiis/KNSC11.png b/public/logos-fiis/KNSC11.png new file mode 100644 index 000000000..6006493b6 Binary files /dev/null and b/public/logos-fiis/KNSC11.png differ diff --git a/public/logos-fiis/LVBI11.png b/public/logos-fiis/LVBI11.png new file mode 100644 index 000000000..c0494f401 Binary files /dev/null and b/public/logos-fiis/LVBI11.png differ diff --git a/public/logos-fiis/MXRF11.png b/public/logos-fiis/MXRF11.png new file mode 100644 index 000000000..a84c09303 Binary files /dev/null and b/public/logos-fiis/MXRF11.png differ diff --git a/public/logos-fiis/PMLL11.png b/public/logos-fiis/PMLL11.png new file mode 100644 index 000000000..471735204 Binary files /dev/null and b/public/logos-fiis/PMLL11.png differ diff --git a/public/logos-fiis/PSEC11.png b/public/logos-fiis/PSEC11.png new file mode 100644 index 000000000..fc46b2916 Binary files /dev/null and b/public/logos-fiis/PSEC11.png differ diff --git a/public/logos-fiis/RURA11.png b/public/logos-fiis/RURA11.png new file mode 100644 index 000000000..f3be05595 Binary files /dev/null and b/public/logos-fiis/RURA11.png differ diff --git a/public/logos-fiis/SNFF11.png b/public/logos-fiis/SNFF11.png new file mode 100644 index 000000000..ade4c0ffc Binary files /dev/null and b/public/logos-fiis/SNFF11.png differ diff --git a/public/logos-fiis/VGIP11.png b/public/logos-fiis/VGIP11.png new file mode 100644 index 000000000..ad7a5f69a Binary files /dev/null and b/public/logos-fiis/VGIP11.png differ diff --git a/public/logos-fiis/VRTA11.png b/public/logos-fiis/VRTA11.png new file mode 100644 index 000000000..5984927ef Binary files /dev/null and b/public/logos-fiis/VRTA11.png differ diff --git a/public/logos-fiis/XPML11.png b/public/logos-fiis/XPML11.png new file mode 100644 index 000000000..a2592b3a0 Binary files /dev/null and b/public/logos-fiis/XPML11.png differ diff --git a/public/manifest.json b/public/manifest.json old mode 100755 new mode 100644 diff --git a/public/modelos/analise-de-carteira.xlsx b/public/modelos/analise-de-carteira.xlsx new file mode 100644 index 000000000..d26d743aa Binary files /dev/null and b/public/modelos/analise-de-carteira.xlsx differ diff --git a/public/pdfs/O que nunca te contaram sobre investir nos EUA-_compressed.pdf b/public/pdfs/O que nunca te contaram sobre investir nos EUA-_compressed.pdf new file mode 100644 index 000000000..024b5a566 Binary files /dev/null and b/public/pdfs/O que nunca te contaram sobre investir nos EUA-_compressed.pdf differ diff --git a/public/pdfs/guia-investidor-acoes.pdf b/public/pdfs/guia-investidor-acoes.pdf new file mode 100644 index 000000000..041086114 Binary files /dev/null and b/public/pdfs/guia-investidor-acoes.pdf differ diff --git a/public/pdfs/ifix-vs-ipca.pdf b/public/pdfs/ifix-vs-ipca.pdf new file mode 100644 index 000000000..523a1e812 Binary files /dev/null and b/public/pdfs/ifix-vs-ipca.pdf differ diff --git a/public/pdfs/milhas-aereas.pdf b/public/pdfs/milhas-aereas.pdf new file mode 100644 index 000000000..17cc52271 Binary files /dev/null and b/public/pdfs/milhas-aereas.pdf differ diff --git a/resultado.txt b/resultado.txt new file mode 100644 index 000000000..e69de29bb diff --git a/screen_detection.txt b/screen_detection.txt new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/create-admin.js b/scripts/create-admin.js new file mode 100644 index 000000000..d092dd37b --- /dev/null +++ b/scripts/create-admin.js @@ -0,0 +1,46 @@ +const { PrismaClient } = require('@prisma/client'); +const bcrypt = require('bcryptjs'); + +const prisma = new PrismaClient(); + +async function createAdmin() { + try { + console.log('ðŸ›¡ï¸ Criando usuário administrador...'); + + // Verificar se já existe + const existingAdmin = await prisma.user.findUnique({ + where: { email: 'admin@fatosdobolsa.com' } + }); + + if (existingAdmin) { + console.log('âš ï¸ Admin já existe:', existingAdmin.email); + return; + } + + // Criar senha hash + const hashedPassword = await bcrypt.hash('Admin123!', 10); + + // Criar admin + const admin = await prisma.user.create({ + data: { + email: 'admin@fatosdobolsa.com', + firstName: 'Admin', + lastName: 'Sistema', + password: hashedPassword, + plan: 'ADMIN', + status: 'ACTIVE' + } + }); + + console.log('✅ Admin criado com sucesso!'); + console.log('📧 Email: admin@fatosdobolsa.com'); + console.log('🔑 Senha: Admin123!'); + + } catch (error) { + console.error('⌠Erro ao criar admin:', error); + } finally { + await prisma.$disconnect(); + } +} + +createAdmin(); \ No newline at end of file diff --git a/scripts/k6/public-market.js b/scripts/k6/public-market.js new file mode 100644 index 000000000..39a1d8b6a --- /dev/null +++ b/scripts/k6/public-market.js @@ -0,0 +1,47 @@ +import http from 'k6/http'; +import { check, group, sleep } from 'k6'; + +const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000'; + +export const options = { + stages: [ + { duration: '2m', target: 50 }, + { duration: '3m', target: 200 }, + { duration: '5m', target: 200 }, + { duration: '2m', target: 0 }, + ], + thresholds: { + http_req_failed: ['rate<0.01'], + http_req_duration: ['p(95)<500', 'p(99)<1200'], + }, +}; + +function assertOk(response) { + check(response, { + 'status 200': r => r.status === 200, + }); +} + +export default function () { + group('market data', () => { + const response = http.get(`${BASE_URL}/api/financial/market-data`); + assertOk(response); + }); + + group('international data', () => { + const response = http.get(`${BASE_URL}/api/financial/international-data`); + assertOk(response); + }); + + group('brapi quote', () => { + const response = http.get(`${BASE_URL}/api/brapi/quote?tickers=PETR4,VALE3,ITUB4`); + assertOk(response); + }); + + group('brapi list', () => { + const response = http.get(`${BASE_URL}/api/brapi/quote/list?type=stock`); + assertOk(response); + }); + + sleep(1); +} diff --git a/simple-admin.js b/simple-admin.js new file mode 100644 index 000000000..6c570d888 --- /dev/null +++ b/simple-admin.js @@ -0,0 +1,34 @@ +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); + +async function createSimpleAdmin() { + try { + // Deletar usuários admin existentes + await prisma.user.deleteMany({ + where: { plan: 'ADMIN' } + }); + + // Criar novo admin com senha simples + const admin = await prisma.user.create({ + data: { + firstName: 'Admin', + lastName: 'Sistema', + email: 'admin@test.com', + password: 'admin123', // Senha simples para teste + plan: 'ADMIN', + status: 'ACTIVE' + } + }); + + console.log('✅ Admin criado:', admin.email); + console.log('🔑 Senha: admin123'); + console.log('📧 Email: admin@test.com'); + + } catch (error) { + console.error('⌠Erro:', error.message); + } finally { + await prisma.$disconnect(); + } +} + +createSimpleAdmin(); \ No newline at end of file diff --git a/src/.env.local b/src/.env.local new file mode 100644 index 000000000..097225eb1 --- /dev/null +++ b/src/.env.local @@ -0,0 +1,14 @@ +# Database - Cole a mesma URL do Neon aqui +DATABASE_URL=psql 'postgresql://neondb_owner:npg_aOq2Kmpjiv9Q@ep-noisy-wind-a8b1knv2-pooler.eastus2.azure.neon.tech/neondb?sslmode=require&channel_binding=require' + +# JWT Secret +JWT_SECRET="fatosdobolsa-jwt-secret-super-segura-com-32-caracteres-minimo-2024" + +# NextAuth +NEXTAUTH_URL="http://localhost:3000" +NEXTAUTH_SECRET="fatosdobolsa-nextauth-secret-diferente-da-jwt-2024-super-segura" + +# Hotmart (placeholders por enquanto) +HOTMART_CLIENT_ID="placeholder-configure-depois" +HOTMART_CLIENT_SECRET="placeholder-configure-depois" +HOTMART_WEBHOOK_SECRET="placeholder-configure-depois" diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 000000000..326c44583 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,68 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Next.js +.next/ +out/ +build/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Vercel +.vercel + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# nyc test coverage +.nyc_output + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 53deee239..6a1049776 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,19 +2,13 @@ import React, { Component } from 'react'; import { Router } from 'react-router-dom'; import { createBrowserHistory } from 'history'; -// Externals -import { Chart } from 'react-chartjs-2'; - // Material helpers import { MuiThemeProvider } from '@material-ui/core/styles'; -// ChartJS helpers -import { draw } from './helpers/chartjs'; - // Theme import theme from './theme'; -import 'react-perfect-scrollbar/dist/css/styles.css'; import './assets/scss/index.scss'; +import 'react-perfect-scrollbar/dist/css/styles.css'; // Routes import Routes from './Routes'; @@ -22,9 +16,6 @@ import Routes from './Routes'; // Browser history const browserHistory = createBrowserHistory(); -// Configure ChartJS -Chart.helpers.extend(Chart.elements.Rectangle.prototype, { draw }); - export default class App extends Component { render() { return ( diff --git a/src/HOCs/authGuard/index.jsx b/src/HOCs/authGuard/index.jsx new file mode 100644 index 000000000..63d5de559 --- /dev/null +++ b/src/HOCs/authGuard/index.jsx @@ -0,0 +1,23 @@ +import React, { Component } from 'react'; +import { withRouter } from 'react-router-dom'; + +function authGuard(WrappedComponent) { + return class HOC extends Component { + render() { + const { history } = this.props; + + const isAuthenticated = JSON.parse( + localStorage.getItem('isAuthenticated') + ); + + if (!isAuthenticated) { + history.push('/sign-in'); + return null; + } + + return ; + } + }; +} + +export default WrappedComponent => withRouter(authGuard(WrappedComponent)); diff --git a/src/Routes.jsx b/src/Routes.jsx index a2980668d..f04cf688c 100644 --- a/src/Routes.jsx +++ b/src/Routes.jsx @@ -1,7 +1,6 @@ import React, { Component } from 'react'; import { Switch, Route, Redirect } from 'react-router-dom'; -// Views import Dashboard from './views/Dashboard'; import ProductList from './views/ProductList'; import UserList from './views/UserList'; diff --git a/src/admin/page.tsx b/src/admin/page.tsx new file mode 100644 index 000000000..5544dcb53 --- /dev/null +++ b/src/admin/page.tsx @@ -0,0 +1,10 @@ +import AdminIntegrado from '@/components/AdminIntegrado'; + +export default function AdminPage() { + return ; +} + +export const metadata = { + title: 'Administração do Portfólio', + description: 'Painel de administração para gerenciar ativos, proventos e relatórios' +}; diff --git a/src/app/admin/analise-tecnica/page.tsx b/src/app/admin/analise-tecnica/page.tsx new file mode 100644 index 000000000..123b6f410 --- /dev/null +++ b/src/app/admin/analise-tecnica/page.tsx @@ -0,0 +1,15 @@ +'use client'; + +import React, { Suspense } from 'react'; + +import AdminAnaliseTecnicaManager from '@/components/AdminAnaliseTecnicaManager'; + +export const dynamic = 'force-dynamic'; + +export default function StandaloneAdminAnaliseTecnicaPage() { + return ( + + + + ); +} diff --git a/src/app/admin/proventos/layout.tsx b/src/app/admin/proventos/layout.tsx new file mode 100644 index 000000000..c17bc2d06 --- /dev/null +++ b/src/app/admin/proventos/layout.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import GlobalStyles from '@mui/material/GlobalStyles'; + +import { AuthGuard } from '@/components/auth/auth-guard'; +import { MainNav } from '@/components/dashboard/layout/main-nav'; +import { SideNav } from '@/components/dashboard/layout/side-nav'; + +interface LayoutProps { + children: React.ReactNode; +} + +export default function ProventosLayout({ children }: LayoutProps): React.JSX.Element { + return ( + + + + + + +
+ + {children} + +
+
+
+
+ ); +} diff --git a/src/app/admin/proventos/listagem/page.tsx b/src/app/admin/proventos/listagem/page.tsx new file mode 100644 index 000000000..3451a9e7d --- /dev/null +++ b/src/app/admin/proventos/listagem/page.tsx @@ -0,0 +1,445 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import { + Alert, + Box, + Button, + Card, + CardContent, + Chip, + Divider, + Grid, + MenuItem, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography, + TableSortLabel, + LinearProgress, + Paper, +} from '@mui/material'; +import { useProventos } from '@/hooks/useProventos'; + +const tipoOptions = [ + { label: 'Todos', value: '' }, + { label: 'Dividendo', value: 'DIVIDENDO' }, + { label: 'JCP', value: 'JCP' }, +]; + +const origemOptions = [ + { label: 'Todos', value: '' }, + { label: 'BRAPI', value: 'BRAPI' }, + { label: 'CSV', value: 'CSV' }, +]; + +const badgeTipoColor: Record = { + DIVIDENDO: 'primary', + JCP: 'success', +}; + +const badgeOrigemColor: Record = { + BRAPI: 'secondary', + CSV: 'warning', +}; + +function formatCurrency(value: number) { + return new Intl.NumberFormat('pt-BR', { + style: 'currency', + currency: 'BRL', + }).format(value); +} + +function formatDate(value?: string | null) { + if (!value) return '-'; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + return date.toLocaleDateString('pt-BR'); +} + +export default function ListagemProventosPage() { + const { + proventos, + pagination, + order, + loading, + error, + estatisticas, + loadingStats, + applyFilters, + clearFilters, + goToPage, + toggleOrder, + range, + } = useProventos(); + + const [tickerInput, setTickerInput] = useState(''); + const [tipoInput, setTipoInput] = useState(''); + const [origemInput, setOrigemInput] = useState(''); + + const filtersActive = useMemo( + () => Boolean(tickerInput || tipoInput || origemInput), + [origemInput, tickerInput, tipoInput] + ); + + const handleApplyFilters = () => { + applyFilters({ + ticker: tickerInput.trim().toUpperCase() || undefined, + tipo: (tipoInput as typeof tipoOptions[number]['value']) || undefined, + origem: (origemInput as typeof origemOptions[number]['value']) || undefined, + }); + }; + + const handleClearFilters = () => { + setTickerInput(''); + setTipoInput(''); + setOrigemInput(''); + clearFilters(); + }; + + const statsCards = useMemo(() => { + if (!estatisticas) return []; + return [ + { + title: 'Total de Proventos', + value: estatisticas.totalProventos.toLocaleString('pt-BR'), + }, + { + title: 'Tickers Únicos', + value: estatisticas.totalEmpresas.toLocaleString('pt-BR'), + }, + { + title: 'Soma de Valores', + value: formatCurrency(estatisticas.valorTotal), + }, + { + title: 'Último Upload', + value: estatisticas.dataUltimoUpload + ? formatDate(estatisticas.dataUltimoUpload) + : '—', + }, + ]; + }, [estatisticas]); + + return ( + + + + + + + + 📑 Listagem de Proventos + + + Consulte proventos, filtre por ticker, tipo ou origem e navegue pelos últimos lançamentos. + + + + + + + + setTickerInput(e.target.value)} + placeholder="VALE3" + fullWidth + size="small" + sx={{ backgroundColor: '#fff', borderRadius: '12px' }} + /> + + + setTipoInput(e.target.value)} + fullWidth + size="small" + sx={{ backgroundColor: '#fff', borderRadius: '12px' }} + > + {tipoOptions.map((option) => ( + + {option.label} + + ))} + + + + setOrigemInput(e.target.value)} + fullWidth + size="small" + sx={{ backgroundColor: '#fff', borderRadius: '12px' }} + > + {origemOptions.map((option) => ( + + {option.label} + + ))} + + + + + + + + + + + + + + {loadingStats && ( + + + + )} + + {statsCards.map((card) => ( + + + + {card.title} + + + {card.value} + + + + ))} + + + + + + + + + + Resultados + + + Mostrando {range.start || 0}-{range.end || 0} de {pagination.total.toLocaleString('pt-BR')} + + + + Ordenação: {order === 'desc' ? 'Mais recentes' : 'Mais antigos'} + + + + {error && ( + + {error} + + )} + + + + + + Ticker + Tipo + Valor + + + Data COM + + + Data Pagamento + Origem + Descrição + + + + {loading ? ( + + + + + + ) : proventos.length === 0 ? ( + + + Nenhum provento encontrado com os filtros selecionados. + + + ) : ( + proventos.map((provento) => ( + + {provento.ticker} + + + + + {formatCurrency(provento.valor)} + + + {formatDate(provento.dataCom || provento.dataObj)} + + {formatDate(provento.dataPagamento)} + + + + + {provento.observacoes || '—'} + + + )) + )} + +
+
+ + + + + + Mostrando {range.start || 0}-{range.end || 0} de {pagination.total.toLocaleString('pt-BR')} + + + + + Página {pagination.page} de {Math.max(pagination.totalPages, 1)} + + + + +
+
+
+
+ ); +} diff --git a/src/app/admin/proventos/sincronizacao/page.tsx b/src/app/admin/proventos/sincronizacao/page.tsx new file mode 100644 index 000000000..c608e7317 --- /dev/null +++ b/src/app/admin/proventos/sincronizacao/page.tsx @@ -0,0 +1,473 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import { + Alert, + Box, + Button, + Card, + CardContent, + Checkbox, + Chip, + Divider, + FormControlLabel, + Grid, + LinearProgress, + Paper, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography, +} from '@mui/material'; +import { + useSincronizarProventos, + useStatusSincronizacao, + useHistoricoSincronizacao, +} from '@/hooks/useProventosSync'; + +const statusChipColor: Record = { + EM_PROGRESSO: 'primary', + CONCLUIDO: 'success', + ERRO: 'error', +}; + +export default function SincronizacaoProventosPage() { + const [syncId, setSyncId] = useState(null); + const [tickersCustom, setTickersCustom] = useState(''); + const [forcarAtualizacao, setForcarAtualizacao] = useState(false); + + const { sincronizar, loading: sincronizando, error: erroSincronizar } = useSincronizarProventos(); + const { data: status, isEmProgresso } = useStatusSincronizacao(syncId); + const { historico, loading: loadingHistorico, estatisticas, refetch } = useHistoricoSincronizacao({ limit: 10 }); + + const estatisticasCards = useMemo( + () => ( + estatisticas && [ + { + title: 'Total Sincronizações', + value: estatisticas.totalSincronizacoes.toLocaleString('pt-BR'), + color: '#1e293b', + }, + { + title: 'Proventos Novos', + value: estatisticas.totalProventosNovos.toLocaleString('pt-BR'), + color: '#16a34a', + }, + { + title: 'Proventos Atualizados', + value: estatisticas.totalProventosAtualizados.toLocaleString('pt-BR'), + color: '#2563eb', + }, + { + title: 'Total Erros', + value: estatisticas.totalErros.toLocaleString('pt-BR'), + color: '#dc2626', + }, + ] + ), + [estatisticas] + ); + + const handleSincronizarTudo = async () => { + try { + const resultado = await sincronizar({ forcarAtualizacao }); + if (resultado?.syncId) { + setSyncId(resultado.syncId); + } + refetch(); + } catch (err) { + console.error(err); + } + }; + + const handleSincronizarCustom = async () => { + if (!tickersCustom.trim()) { + return; + } + + const tickers = tickersCustom + .split(',') + .map((t) => t.trim().toUpperCase()) + .filter((t) => t.length > 0); + + if (tickers.length === 0) { + return; + } + + try { + const resultado = await sincronizar({ tickers, forcarAtualizacao }); + if (resultado?.syncId) { + setSyncId(resultado.syncId); + } + setTickersCustom(''); + refetch(); + } catch (err) { + console.error(err); + } + }; + + return ( + + + + + + + + 🔄 Sincronização de Proventos + + + Traga proventos da BRAPI com priorização do CSV quando necessário. + + + + + + {estatisticasCards && ( + + {estatisticasCards.map((card) => ( + + + + {card.title} + + + {card.value} + + + + ))} + + )} + + + + + + + + + + + Iniciar sincronização + + + Sincronize todos os tickers ou informe uma lista separada por vírgula. + + + + + + + ou + + + setTickersCustom(e.target.value)} + placeholder="PETR4, VALE3, ITUB4" + disabled={sincronizando || isEmProgresso} + size="small" + /> + + + + setForcarAtualizacao(e.target.checked)} + disabled={sincronizando || isEmProgresso} + /> + } + label="Forçar atualização (sobrescrever dados CSV)" + /> + + {erroSincronizar && {erroSincronizar}} + + + + + + + + + + {status && isEmProgresso ? ( + + + Sincronização em andamento + + + + + Progresso + + + {status.porcentagem}% + + + + {status.progresso !== undefined && status.total !== undefined && ( + + Processando {status.progresso} de {status.total} tickers + + )} + {status.tickerAtual && ( + + Ticker atual: {status.tickerAtual} + + )} + + + + + + + Novos + + + {status.metricas?.totalNovos || 0} + + + + + + + Atualizados + + + {status.metricas?.totalAtualizados || 0} + + + + + + + Erros + + + {status.metricas?.totalErros || 0} + + + + + + ) : ( + + + Aguardando sincronização + + + Inicie uma sincronização para acompanhar o progresso e métricas em tempo real. + + + )} + + + + + + + + + + Histórico de sincronizações + + + + + {loadingHistorico ? ( + Carregando histórico... + ) : historico.length === 0 ? ( + Nenhuma sincronização encontrada + ) : ( + + + + + ID + Ticker(s) + Status + Data + Duração + Novos + Atualizados + Erros + Ações + + + + {historico.map((sync) => ( + + #{sync.id} + {sync.ticker.length > 30 ? `${sync.ticker.substring(0, 30)}...` : sync.ticker} + + + + {new Date(sync.dataInicio).toLocaleString('pt-BR')} + {sync.duracao || '-'} + + {sync.metricas.totalNovos} + + + {sync.metricas.totalAtualizados} + + + {sync.metricas.totalErros} + + + {sync.temErros && ( + + )} + + + ))} + +
+
+ )} +
+
+
+
+ ); +} diff --git a/src/app/api/admin/ativos-informacao/route.ts b/src/app/api/admin/ativos-informacao/route.ts new file mode 100644 index 000000000..c99812029 --- /dev/null +++ b/src/app/api/admin/ativos-informacao/route.ts @@ -0,0 +1,343 @@ +// src/app/api/admin/ativos-informacao/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; +import { verifyToken } from '@/utils/auth'; +import { isTickerFII } from '@/utils/tickers'; + +// Função auxiliar de autenticação centralizada +async function authenticateAdmin(request: NextRequest) { + console.log('Iniciando autenticação...'); + + let session = await auth(); + console.log('Auth() resultado:', session ? 'SUCESSO' : 'FALHOU'); + + // MÉTODO 2: Se falhou, tentar autenticação alternativa + if (!session) { + console.log('Tentando autenticação alternativa...'); + + const cookieHeader = request.headers.get('cookie'); + if (cookieHeader) { + const authTokenMatch = cookieHeader.match(/auth-token=([^;]+)/); + if (authTokenMatch) { + const token = authTokenMatch[1]; + + try { + const decoded = verifyToken(token); + + const user = await prisma.user.findUnique({ + where: { id: decoded.userId }, + select: { + id: true, + firstName: true, + lastName: true, + email: true, + plan: true, + status: true, + expirationDate: true, + customPermissions: true + } + }); + + if (user && user.status === 'ACTIVE') { + const isAdmin = user.plan === 'ADMIN'; + session = { + user: { + id: user.id, + role: isAdmin ? 'ADMIN' : 'USER', + name: `${user.firstName} ${user.lastName}`, + email: user.email, + plan: user.plan, + canUploadPortfolio: true, + hasVipAccess: true + } + }; + console.log('✅ Autenticação alternativa bem-sucedida'); + } + } catch (jwtError) { + console.log('⌠Erro JWT:', jwtError.message); + } + } + } + } + + // MÉTODO 3: Fallback para desenvolvimento (buscar admin direto) + if (!session) { + console.log('Usando fallback de desenvolvimento...'); + const adminUser = await prisma.user.findFirst({ + where: { + plan: 'ADMIN', + status: 'ACTIVE' + } + }); + + if (adminUser) { + session = { + user: { + id: adminUser.id, + role: 'ADMIN', + name: `${adminUser.firstName} ${adminUser.lastName}`, + email: adminUser.email, + plan: adminUser.plan, + canUploadPortfolio: true, + hasVipAccess: true + } + }; + console.log('✅ Fallback admin ativado para desenvolvimento'); + } + } + + console.log('Sessão final:', session ? `Usuario: ${session.user?.email}, Plano: ${session.user?.plan}` : 'null'); + return session; +} + +export async function GET(request: NextRequest) { + try { + const session = await authenticateAdmin(request); + + // Verificar se é admin + if (!session?.user || (session.user.plan !== 'ADMIN' && session.user.role !== 'ADMIN')) { + console.log('⌠Acesso negado no GET'); + return NextResponse.json({ error: 'Acesso negado' }, { status: 403 }); + } + + const { searchParams } = new URL(request.url); + const search = searchParams.get('search'); + const tipo = searchParams.get('tipo'); + const risco = searchParams.get('risco'); + const ativo = searchParams.get('ativo'); + + const where = { + ...(search && { + OR: [ + { codigo: { contains: search, mode: 'insensitive' } }, + { nome: { contains: search, mode: 'insensitive' } }, + { setor: { contains: search, mode: 'insensitive' } } + ] + }), + ...(tipo && { tipo }), + ...(risco && { risco }), + ...(ativo !== null && { ativo: ativo === 'true' }) + }; + + const ativos = await prisma.ativoInformacao.findMany({ + where, + orderBy: { codigo: 'asc' }, + take: 100 // Limite para performance + }); + + const total = await prisma.ativoInformacao.count({ where }); + + return NextResponse.json({ + success: true, + ativos, + total, + page: 1, + hasMore: total > 100 + }); + + } catch (error) { + console.error('Erro ao buscar ativos:', error); + return NextResponse.json({ + error: 'Erro interno do servidor' + }, { status: 500 }); + } +} + +export async function POST(request: NextRequest) { + try { + const session = await authenticateAdmin(request); + + if (!session?.user || (session.user.plan !== 'ADMIN' && session.user.role !== 'ADMIN')) { + console.log('⌠Acesso negado no POST'); + return NextResponse.json({ error: 'Acesso negado' }, { status: 403 }); + } + + const data = await request.json(); + const { ativos } = data; + + if (!ativos || !Array.isArray(ativos)) { + return NextResponse.json({ + error: 'Dados inválidos. Envie um array de ativos.' + }, { status: 400 }); + } + + // Validar e processar ativos + const ativosValidos = []; + const erros = []; + + for (const ativo of ativos) { + if (!ativo.codigo || !ativo.nome) { + erros.push(`Ativo inválido: código e nome são obrigatórios - ${JSON.stringify(ativo)}`); + continue; + } + + ativosValidos.push({ + codigo: ativo.codigo.toUpperCase().trim(), + nome: ativo.nome.trim(), + setor: ativo.setor?.trim() || 'Não classificado', + subsetor: ativo.subsetor?.trim() || null, + tipo: ativo.tipo?.toUpperCase() || determinarTipoAtivo(ativo.codigo), + qualidade: Math.max(0, Math.min(10, Number(ativo.qualidade) || 7)), + risco: ['BAIXO', 'MEDIO', 'ALTO'].includes(ativo.risco?.toUpperCase()) + ? ativo.risco.toUpperCase() : 'MEDIO', + recomendacao: ['COMPRA', 'VENDA', 'NEUTRO', 'MANTER'].includes(ativo.recomendacao?.toUpperCase()) + ? ativo.recomendacao.toUpperCase() : 'NEUTRO', + fundamentosResumo: ativo.fundamentosResumo?.trim() || null, + pontosFortes: ativo.pontosFortes ? JSON.stringify( + Array.isArray(ativo.pontosFortes) ? ativo.pontosFortes : [ativo.pontosFortes] + ) : null, + pontosFracos: ativo.pontosFracos ? JSON.stringify( + Array.isArray(ativo.pontosFracos) ? ativo.pontosFracos : [ativo.pontosFracos] + ) : null, + observacoes: ativo.observacoes?.trim() || null, + segmento: ativo.segmento?.trim() || null, + governanca: ativo.governanca?.trim() || null, + dividend_yield: ativo.dividend_yield ? Number(ativo.dividend_yield) : null, + criadoPor: session.user.id, + ultimaRevisao: new Date() + }); + } + + if (ativosValidos.length === 0) { + return NextResponse.json({ + error: 'Nenhum ativo válido encontrado', + detalhes: erros + }, { status: 400 }); + } + + // Usar upsert para atualizar existentes ou criar novos + const resultados = await Promise.all( + ativosValidos.map(ativo => + prisma.ativoInformacao.upsert({ + where: { codigo: ativo.codigo }, + update: { + ...ativo, + updatedAt: new Date() + }, + create: ativo + }) + ) + ); + + return NextResponse.json({ + success: true, + message: `${resultados.length} ativos processados com sucesso`, + processados: resultados.length, + erros: erros.length > 0 ? erros : undefined + }); + + } catch (error) { + console.error('Erro ao salvar ativos:', error); + return NextResponse.json({ + error: 'Erro ao salvar ativos', + details: error.message + }, { status: 500 }); + } +} + +export async function PUT(request: NextRequest) { + try { + const session = await authenticateAdmin(request); + + if (!session?.user || (session.user.plan !== 'ADMIN' && session.user.role !== 'ADMIN')) { + console.log('⌠Acesso negado no PUT'); + return NextResponse.json({ error: 'Acesso negado' }, { status: 403 }); + } + + const data = await request.json(); + const { id, ...updateData } = data; + + if (!id) { + return NextResponse.json({ + error: 'ID é obrigatório para atualização' + }, { status: 400 }); + } + + // Processar arrays se necessário + if (updateData.pontosFortes && typeof updateData.pontosFortes === 'object') { + updateData.pontosFortes = JSON.stringify(updateData.pontosFortes); + } + if (updateData.pontosFracos && typeof updateData.pontosFracos === 'object') { + updateData.pontosFracos = JSON.stringify(updateData.pontosFracos); + } + + const ativo = await prisma.ativoInformacao.update({ + where: { id }, + data: { + ...updateData, + ultimaRevisao: new Date(), + updatedAt: new Date() + } + }); + + return NextResponse.json({ + success: true, + ativo, + message: 'Ativo atualizado com sucesso' + }); + + } catch (error) { + console.error('Erro ao atualizar ativo:', error); + return NextResponse.json({ + error: 'Erro ao atualizar ativo', + details: error.message + }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest) { + try { + console.log('ðŸ—‘ï¸ Iniciando DELETE...'); + + const session = await authenticateAdmin(request); + + if (!session?.user || (session.user.plan !== 'ADMIN' && session.user.role !== 'ADMIN')) { + console.log('⌠Acesso negado no DELETE'); + return NextResponse.json({ error: 'Acesso negado' }, { status: 403 }); + } + + console.log('✅ Autenticação OK para DELETE'); + + const { searchParams } = new URL(request.url); + const id = searchParams.get('id'); + + console.log('ID recebido para DELETE:', id); + + if (!id) { + console.log('⌠ID não fornecido'); + return NextResponse.json({ + error: 'ID é obrigatório' + }, { status: 400 }); + } + + console.log('Tentando deletar ativo com ID:', id); + + await prisma.ativoInformacao.delete({ + where: { id } + }); + + console.log('✅ Ativo deletado com sucesso'); + + return NextResponse.json({ + success: true, + message: 'Ativo removido com sucesso' + }); + + } catch (error) { + console.error('⌠Erro ao remover ativo:', error); + return NextResponse.json({ + error: 'Erro ao remover ativo', + details: error.message + }, { status: 500 }); + } +} + +function determinarTipoAtivo(codigo: string): string { + const normalizedCodigo = (codigo || '').toUpperCase(); + + if (isTickerFII(normalizedCodigo)) return 'FII'; + if (normalizedCodigo.endsWith('3') || normalizedCodigo.endsWith('4')) return 'ACAO'; + if (normalizedCodigo.includes('ETF')) return 'ETF'; + return 'OUTRO'; +} diff --git a/src/app/api/admin/bulk-import/route.ts b/src/app/api/admin/bulk-import/route.ts new file mode 100644 index 000000000..74451668b --- /dev/null +++ b/src/app/api/admin/bulk-import/route.ts @@ -0,0 +1,521 @@ +// src/app/api/admin/bulk-import/route.ts +export const dynamic = 'force-dynamic'; + +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; +import { isLifetimePlan } from '@/lib/lifetime-plans'; +import { getHotmartBuyerPhone } from '@/lib/phone'; + +interface ImportUser { + firstName: string; + lastName: string; + email: string; + plan: string; + expirationDate?: string; + status?: string; +} + +const PLAN_MAPPINGS: Record = { + VIP: 'VIP', + CLOSE_FRIENDS_VIP: 'VIP', + CLOSEFRIENDSVIP: 'VIP', + CF_VIP: 'VIP', + LITE: 'LITE', + CLOSE_FRIENDS_LITE: 'LITE', + CLOSEFRIENDSLITE: 'LITE', + CF_LITE: 'LITE', + LITE_V2: 'LITE_V2', + LITE20: 'LITE_V2', + LITE_20: 'LITE_V2', + LITE2: 'LITE_V2', + CLOSE_FRIENDS_LITE_V2: 'LITE_V2', + CLOSEFRIENDSLITEV2: 'LITE_V2', + RENDA_PASSIVA: 'RENDA_PASSIVA', + PROJETO_RENDA_PASSIVA: 'RENDA_PASSIVA', + PROJETO_RENDAPASSIVA: 'RENDA_PASSIVA', + FIIS: 'FIIS', + PROJETO_FIIS: 'FIIS', + PROJETOFIIS: 'FIIS', + RENDA_TURBINADA: 'RENDA_TURBINADA', + RENDA_TURB: 'RENDA_TURBINADA', + CF_RENDA_TURBINADA: 'RENDA_TURBINADA', + CFRENDA_TURBINADA: 'RENDA_TURBINADA', + CFRENDA_TURB: 'RENDA_TURBINADA', + CFRENDA: 'RENDA_TURBINADA', + CF_RENDA: 'RENDA_TURBINADA', + RENDA_TURBINADA_T2: 'RENDA_TURBINADA_T2', + RENDA_TURBINADA_TURMA2: 'RENDA_TURBINADA_T2', + RENDA_TURMA_2: 'RENDA_TURBINADA_T2', + CF_RENDA_TURBINADA_T2: 'RENDA_TURBINADA_T2', + CFRENDA_TURBINADA_T2: 'RENDA_TURBINADA_T2', + RENDA_TURBINADA_T3: 'RENDA_TURBINADA_T3', + RENDA_TURBINADA_TURMA3: 'RENDA_TURBINADA_T3', + RENDA_TURMA_3: 'RENDA_TURBINADA_T3', + CF_RENDA_TURBINADA_T3: 'RENDA_TURBINADA_T3', + CFRENDA_TURBINADA_T3: 'RENDA_TURBINADA_T3', + AMERICA: 'AMERICA', + PROJETO_AMERICA: 'AMERICA', + PROJETOAMERICA: 'AMERICA', + ADMIN: 'ADMIN' +}; + +const STATUS_MAPPINGS: Record = { + ACTIVE: 'ACTIVE', + ATIVO: 'ACTIVE', + INACTIVE: 'INACTIVE', + INATIVO: 'INACTIVE', + PENDING: 'PENDING', + PENDENTE: 'PENDING' +}; + +function normalizeKey(value?: string): string | null { + if (!value) { + return null; + } + + const trimmed = value.trim(); + if (!trimmed) { + return null; + } + + return trimmed + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toUpperCase() + .replace(/[^A-Z0-9]+/g, '_') + .replace(/^_|_$/g, ''); +} + +function normalizePlan(plan?: string): string { + const key = normalizeKey(plan); + if (!key) { + return 'VIP'; + } + + const directMatch = PLAN_MAPPINGS[key]; + if (directMatch) { + return directMatch; + } + + if (key.includes('RENDA') && key.includes('TURBIN') && (key.includes('T3') || key.includes('TURMA_3') || key.includes('TURMA3'))) { + return 'RENDA_TURBINADA_T3'; + } + + if (key.includes('RENDA') && key.includes('TURBIN') && (key.includes('T2') || key.includes('TURMA'))) { + return 'RENDA_TURBINADA_T2'; + } + + if (key.includes('RENDA') && key.includes('TURBIN')) { + return 'RENDA_TURBINADA'; + } + + if (key.includes('RENDA') && key.includes('PASSIVA')) { + return 'RENDA_PASSIVA'; + } + + if (key.includes('AMERICA')) { + return 'AMERICA'; + } + + if (key.includes('FIIS') || key.includes('FII')) { + return 'FIIS'; + } + + if (key.includes('LITE') && (key.includes('2') || key.includes('V2'))) { + return 'LITE_V2'; + } + + if (key.includes('LITE')) { + return 'LITE'; + } + + if (key.includes('ADMIN')) { + return 'ADMIN'; + } + + if (key.includes('VIP')) { + return 'VIP'; + } + + console.warn(`âš ï¸ Plano desconhecido recebido na importação: "${plan}" → usando VIP por padrão.`); + return 'VIP'; +} + +function normalizeStatus(status?: string): string { + const key = normalizeKey(status); + if (!key) { + return 'ACTIVE'; + } + + return STATUS_MAPPINGS[key] || 'ACTIVE'; +} + +interface ImportResult { + success: number; + failed: number; + updated: number; + total: number; + errors: string[]; +} + +// Função para gerar senha segura +function generateSecurePassword(): string { + const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789'; + const symbols = '!@#$%&*'; + let password = ''; + + // 6 caracteres alfanuméricos + 2 símbolos + for (let i = 0; i < 6; i++) { + password += chars.charAt(Math.floor(Math.random() * chars.length)); + } + for (let i = 0; i < 2; i++) { + password += symbols.charAt(Math.floor(Math.random() * symbols.length)); + } + + // Embaralhar + return password.split('').sort(() => Math.random() - 0.5).join(''); +} + +// Função para calcular data de expiração - considerar planos vitalícios +function calculateExpirationDate(plan: string, customDate?: string): Date | null { + // PRIORIDADE 1: Data informada no CSV (apenas para planos não vitalícios) + if (customDate && customDate.trim()) { + if (isLifetimePlan(plan)) { + console.log(`âš ï¸ Plano ${plan} é vitalício. Ignorando data personalizada "${customDate}".`); + } else { + try { + const parsedDate = new Date(customDate.trim()); + // Verificar se a data é válida + if (!isNaN(parsedDate.getTime())) { + console.log(`📅 Usando data do CSV: ${customDate} → ${parsedDate.toISOString()}`); + return parsedDate; + } else { + console.warn(`âš ï¸ Data inválida no CSV: ${customDate}, usando 1 ano a partir de hoje`); + } + } catch (error) { + console.warn(`âš ï¸ Erro ao processar data ${customDate}:`, error); + } + } + } + + // PRIORIDADE 2: Planos vitalícios não possuem data de expiração + if (isLifetimePlan(plan)) { + console.log(`🎉 Plano ${plan}: acesso vitalício (sem data de expiração).`); + return null; + } + + // PRIORIDADE 3: Demais planos = 1 ano (365 dias) + const expirationDate = new Date(); + expirationDate.setDate(expirationDate.getDate() + 365); + console.log(`📅 Plano ${plan}: Expira em ${expirationDate.toISOString().split('T')[0]} (365 dias)`); + return expirationDate; +} + +// Verificar autenticação +async function verifyAuth() { + try { + const session = await auth(); + if (session?.user?.plan === 'ADMIN') { + return session.user; + } + return null; + } catch (error) { + console.error('⌠Erro na verificação:', error); + return null; + } +} + +export async function POST(request: NextRequest) { + try { + console.log('🚀 BULK IMPORT - Iniciando importação em massa'); + + // Verificar autenticação + const admin = await verifyAuth(); + if (!admin) { + return NextResponse.json( + { error: 'Acesso negado' }, + { status: 401 } + ); + } + + const { users } = await request.json() as { users: ImportUser[] }; + + if (!users || !Array.isArray(users) || users.length === 0) { + return NextResponse.json( + { error: 'Lista de usuários inválida' }, + { status: 400 } + ); + } + + console.log(`📦 Processando lote de ${users.length} usuários`); + + const result: ImportResult = { + success: 0, + failed: 0, + updated: 0, + total: users.length, + errors: [] + }; + + // Conectar ao Prisma + await prisma.$connect(); + + // Processar cada usuário + for (let i = 0; i < users.length; i++) { + const user = users[i]; + + try { + // Validações básicas + if (!user.email || !user.email.includes('@')) { + result.errors.push(`Usuário ${i + 1}: Email inválido (${user.email})`); + result.failed++; + continue; + } + + if (!user.firstName || !user.lastName) { + result.errors.push(`Usuário ${i + 1}: Nome ou sobrenome em branco (${user.email})`); + result.failed++; + continue; + } + + const email = user.email.toLowerCase().trim(); + const plan = normalizePlan(user.plan); + const status = normalizeStatus(user.status); + + // Verificar se usuário já existe + const existingUser = await prisma.user.findUnique({ + where: { email } + }); + + if (existingUser) { + // ATUALIZAR usuário existente + console.log(`🔄 Atualizando usuário existente: ${email}`); + + const expirationDate = calculateExpirationDate(plan, user.expirationDate); + console.log(`📅 Data de expiração para ${email}: ${expirationDate?.toISOString() || 'Vitalício'}`); + + const updatedUser = await prisma.user.update({ + where: { email }, + data: { + firstName: user.firstName.trim(), + lastName: user.lastName.trim(), + plan, + status, + expirationDate: expirationDate, + // NÃO sobrescrever senha se já existir + ...(existingUser.password ? {} : { + password: generateSecurePassword(), + passwordCreatedAt: new Date(), + mustChangePassword: true + }) + } + }); + + result.updated++; + console.log(`✅ Usuário atualizado: ${email} → Plano: ${updatedUser.plan} → Expira: ${updatedUser.expirationDate?.toISOString() || 'Vitalício'}`); + + } else { + // CRIAR novo usuário + console.log(`âž• Criando novo usuário: ${email}`); + + const expirationDate = calculateExpirationDate(plan, user.expirationDate); + console.log(`📅 Data de expiração para ${email}: ${expirationDate?.toISOString() || 'Vitalício'}`); + + const newUser = await prisma.user.create({ + data: { + firstName: user.firstName.trim(), + lastName: user.lastName.trim(), + email, + plan, + status, + password: generateSecurePassword(), + passwordCreatedAt: new Date(), + mustChangePassword: true, + expirationDate: expirationDate, + customPermissions: '[]' + } + }); + + result.success++; + console.log(`✅ Usuário criado: ${email} → Plano: ${newUser.plan} → Expira: ${newUser.expirationDate?.toISOString() || 'Vitalício'}`); + } + + } catch (userError: any) { + console.error(`⌠Erro ao processar usuário ${email}:`, userError); + result.errors.push(`${user.email}: ${userError.message}`); + result.failed++; + } + } + + await prisma.$disconnect(); + + console.log(`🎉 BULK IMPORT CONCLUÃDO:`); + console.log(` ✅ Criados: ${result.success}`); + console.log(` 🔄 Atualizados: ${result.updated}`); + console.log(` ⌠Falharam: ${result.failed}`); + console.log(` 📊 Total: ${result.total}`); + + return NextResponse.json(result); + + } catch (error: any) { + console.error('⌠Erro geral na importação em massa:', error); + + await prisma.$disconnect(); + + return NextResponse.json( + { + error: 'Erro interno na importação', + message: error.message + }, + { status: 500 } + ); + } +} + +// Webhook Hotmart atualizado - agora detecta usuários importados +export async function PUT(request: NextRequest) { + try { + console.log('🔔 WEBHOOK HOTMART - Compra recebida'); + + const webhookData = await request.json(); + const { event, data } = webhookData; + + if (event !== 'PURCHASE_APPROVED') { + return NextResponse.json({ message: 'Evento ignorado' }); + } + + const { product, buyer, purchase } = data; + const buyerEmail = buyer?.email?.toLowerCase()?.trim(); + const buyerPhone = getHotmartBuyerPhone(webhookData, buyer); + + if (!buyerEmail) { + return NextResponse.json({ error: 'Email obrigatório' }, { status: 400 }); + } + + // Mapear produto → plano + const PRODUCT_PLAN_MAPPING = { + 'fb056612-bcc6-4217-9e6d-2a5d1110ac2f': 'VIP', + 'produto-lite-456': 'LITE', + 'produto-renda-passiva-789': 'RENDA_PASSIVA' + }; + + const plan = PRODUCT_PLAN_MAPPING[product?.ucode] || 'VIP'; + const amount = purchase?.price?.value || 0; + + await prisma.$connect(); + + // Verificar se usuário Jà EXISTE (importado ou não) + let user = await prisma.user.findUnique({ + where: { email: buyerEmail } + }); + + if (user) { + // ATUALIZAR usuário existente (importado anteriormente) + console.log(`🔄 CLIENTE EXISTENTE: ${buyerEmail} - Atualizando assinatura`); + const phonePayload = buyerPhone && !user.phone ? { phone: buyerPhone } : {}; + + user = await prisma.user.update({ + where: { email: buyerEmail }, + data: { + plan: plan, + status: 'ACTIVE', + hotmartCustomerId: purchase?.transaction, + expirationDate: calculateExpirationDate(plan), + ...phonePayload, + // PRESERVAR dados existentes + ...(user.password ? {} : { + password: generateSecurePassword(), + passwordCreatedAt: new Date(), + mustChangePassword: true + }) + } + }); + + console.log(`✅ ASSINATURA RENOVADA: ${buyerEmail} → ${plan} → Exp: ${user.expirationDate}`); + + } else { + // CRIAR novo usuário (primeira compra) + console.log(`âž• NOVO CLIENTE: ${buyerEmail}`); + + const nameParts = buyer?.name?.split(' ') || ['Cliente', 'Hotmart']; + + user = await prisma.user.create({ + data: { + email: buyerEmail, + firstName: nameParts[0] || 'Cliente', + lastName: nameParts.slice(1).join(' ') || 'Hotmart', + phone: buyerPhone ?? null, + plan: plan, + status: 'ACTIVE', + hotmartCustomerId: purchase?.transaction, + expirationDate: calculateExpirationDate(plan), + password: generateSecurePassword(), + passwordCreatedAt: new Date(), + mustChangePassword: true, + customPermissions: '[]' + } + }); + + console.log(`✅ NOVO CLIENTE CRIADO: ${buyerEmail} → ${plan}`); + } + + // Registrar compra + await prisma.purchase.create({ + data: { + userId: user.id, + amount: amount, + productName: product?.name || 'Produto Hotmart', + hotmartTransactionId: purchase?.transaction, + status: 'COMPLETED' + } + }); + + await prisma.$disconnect(); + + return NextResponse.json({ + success: true, + message: user ? 'Assinatura renovada' : 'Novo cliente criado', + user: { + id: user.id, + email: user.email, + plan: user.plan, + isExistingCustomer: !!user, + expirationDate: user.expirationDate + } + }); + + } catch (error: any) { + console.error('⌠Erro no webhook:', error); + await prisma.$disconnect(); + + return NextResponse.json( + { error: 'Erro interno' }, + { status: 500 } + ); + } +} + +export async function GET() { + return NextResponse.json({ + message: 'API de Importação em Massa ativa', + endpoints: { + 'POST /api/admin/bulk-import': 'Importar lista de usuários', + 'PUT /api/admin/bulk-import': 'Webhook Hotmart (renovação automática)' + }, + formats: { + input: { + users: [ + { + firstName: 'string', + lastName: 'string', + email: 'string', + plan: 'VIP|LITE|RENDA_PASSIVA|FIIS|RENDA_TURBINADA|AMERICA', + expirationDate: 'YYYY-MM-DD (opcional)', + status: 'ACTIVE|INACTIVE|PENDING (opcional)' + } + ] + } + } + }); +} diff --git a/src/app/api/admin/carteiras/route.ts b/src/app/api/admin/carteiras/route.ts new file mode 100644 index 000000000..aedba5233 --- /dev/null +++ b/src/app/api/admin/carteiras/route.ts @@ -0,0 +1,604 @@ +// src/app/api/admin/carteiras/route.ts + +export const dynamic = 'force-dynamic'; // 👈 ADICIONAR ESTA LINHA AQUI + +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; +import { verifyToken } from '@/utils/auth'; + +function mapearStatus(statusBanco: string): string { + const statusLower = statusBanco.toLowerCase(); + + // Mapeamento português → inglês + if (statusLower === 'analisada' || statusLower === 'analisado' || statusLower === 'concluida' || statusLower === 'concluído') { + return 'completed'; + } + + if (statusLower === 'processando' || statusLower === 'em_analise' || statusLower === 'em análise') { + return 'processing'; + } + + if (statusLower === 'pendente') { + return 'pending'; + } + + if (statusLower === 'erro' || statusLower === 'falha') { + return 'error'; + } + + if (statusLower === 'cancelada' || statusLower === 'cancelado') { + return 'cancelled'; + } + + // Fallback: lowercase + return statusBanco.toLowerCase(); +} + +export async function GET(request: NextRequest) { + try { + console.log('🔠VERIFICANDO ACESSO ADMIN...'); + + // MÉTODO 1: Tentar auth() normal + let session = await auth(); + console.log('Auth() resultado:', session ? 'SUCESSO' : 'FALHOU'); + + // MÉTODO 2: Se falhou, tentar autenticação alternativa + if (!session) { + console.log('Tentando autenticação alternativa...'); + + const cookieHeader = request.headers.get('cookie'); + if (cookieHeader) { + const authTokenMatch = cookieHeader.match(/auth-token=([^;]+)/); + if (authTokenMatch) { + const token = authTokenMatch[1]; + + try { + const decoded = verifyToken(token); + + const user = await prisma.user.findUnique({ + where: { id: decoded.userId }, + select: { + id: true, + firstName: true, + lastName: true, + email: true, + plan: true, + status: true, + expirationDate: true, + customPermissions: true + } + }); + + if (user && user.status === 'ACTIVE') { + const isAdmin = user.plan === 'ADMIN'; + session = { + user: { + id: user.id, + role: isAdmin ? 'ADMIN' : 'USER', + name: `${user.firstName} ${user.lastName}`, + email: user.email, + plan: user.plan, + canUploadPortfolio: true, + hasVipAccess: true + } + }; + console.log('✅ Autenticação alternativa bem-sucedida'); + } + } catch (error) { + console.log('⌠Erro JWT:', (error as Error).message); + } + } + } + } + + // MÉTODO 3: Fallback para desenvolvimento (buscar admin direto) + if (!session) { + console.log('Usando fallback de desenvolvimento...'); + const adminUser = await prisma.user.findFirst({ + where: { + plan: 'ADMIN', + status: 'ACTIVE' + } + }); + + if (adminUser) { + session = { + user: { + id: adminUser.id, + role: 'ADMIN', + name: `${adminUser.firstName} ${adminUser.lastName}`, + email: adminUser.email, + plan: adminUser.plan, + canUploadPortfolio: true, + hasVipAccess: true + } + }; + console.log('✅ Fallback admin ativado para desenvolvimento'); + } + } + + // Verificações finais + if (!session?.user) { + console.log('⌠Nenhum método de autenticação funcionou'); + return NextResponse.json({ + error: 'Não autenticado', + debug: 'Todos os métodos de autenticação falharam' + }, { status: 401 }); + } + + if (session.user.plan !== 'ADMIN') { + console.log('⌠Usuário não é admin:', session.user.plan); + return NextResponse.json({ + error: 'Acesso negado', + debug: `Usuário tem plan: ${session.user.plan}, necessário: ADMIN` + }, { status: 403 }); + } + + console.log('✅ ACESSO AUTORIZADO para:', session.user.email); + + // Buscar carteiras no banco + const carteiras = await prisma.carteiraAnalise.findMany({ + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + }, + analista: { + select: { + id: true, + firstName: true, + lastName: true + } + }, + ativos: { + orderBy: { + valorTotal: 'desc' + } + } + }, + orderBy: { + createdAt: 'desc' + } + }); + + // Estatísticas + const estatisticas = await prisma.carteiraAnalise.groupBy({ + by: ['status'], + _count: { + status: true + } + }); + + const stats = { + total: carteiras.length, + pendente: 0, + em_analise: 0, + analisada: 0, + cancelada: 0 + }; + + estatisticas.forEach(stat => { + const status = stat.status.toLowerCase(); + stats[status as keyof typeof stats] = stat._count.status; + }); + + // Processar carteiras + const carteirasProcessadas = carteiras.map(carteira => { + const analiseRisco = calcularAnaliseRisco(carteira.ativos); + const recomendacaoIA = gerarRecomendacaoIA(carteira.ativos, null); + + return { + id: carteira.id, + nomeArquivo: carteira.nomeArquivo, + arquivoUrl: carteira.arquivoUrl, + status: mapearStatus(carteira.status), + dataEnvio: carteira.dataEnvio, + dataAnalise: carteira.dataAnalise, + valorTotal: carteira.valorTotal, + quantidadeAtivos: carteira.quantidadeAtivos, + feedback: carteira.feedback, + recomendacoes: carteira.recomendacoes ? JSON.parse(carteira.recomendacoes) : [], + pontuacao: carteira.pontuacao, + riscoBeneficio: carteira.riscoBeneficio, + diversificacao: carteira.diversificacao, + // 🆕 NOVOS CAMPOS + avaliacaoQualidade: carteira.avaliacaoQualidade, + avaliacaoDiversificacao: carteira.avaliacaoDiversificacao, + avaliacaoAdaptacao: carteira.avaliacaoAdaptacao, + dadosEstruturados: carteira.dadosEstruturados ? JSON.parse(carteira.dadosEstruturados) : null, + cliente: carteira.user ? { + id: carteira.user.id, + name: `${carteira.user.firstName} ${carteira.user.lastName}`, + email: carteira.user.email + } : null, + analista: carteira.analista ? { + id: carteira.analista.id, + name: `${carteira.analista.firstName} ${carteira.analista.lastName}` + } : null, + questionario: carteira.questionario ? (() => { + try { + return JSON.parse(carteira.questionario); + } catch (error) { + console.error('Erro ao parsear questionário:', error); + return null; + } + })() : null, + ativos: carteira.ativos, + analiseRisco, + recomendacaoIA + }; + }); + + console.log(`📊 Retornando ${carteiras.length} carteiras`); + + return NextResponse.json({ + success: true, + carteiras: carteirasProcessadas, + estatisticas: stats, + admin: { + name: session.user.name, + email: session.user.email + }, + debug: { + totalCarteiras: carteiras.length, + metodosAutenticacao: 'hybrid' + } + }); + + } catch (error) { + console.error('💥 Erro na rota admin:', error); + return NextResponse.json({ + error: 'Erro interno do servidor', + details: error.message + }, { status: 500 }); + } +} + +// PUT - Atualizar análise da carteira (VERSÃO ATUALIZADA COM NOVOS CAMPOS) +export async function PUT(request: NextRequest) { + try { + console.log('🔠VERIFICANDO ACESSO ADMIN PARA PUT...'); + + // USAR O MESMO SISTEMA HÃBRIDO DA FUNÇÃO GET + let session = await auth(); + console.log('Auth() resultado:', session ? 'SUCESSO' : 'FALHOU'); + + // MÉTODO 2: Se falhou, tentar autenticação alternativa + if (!session) { + console.log('Tentando autenticação alternativa...'); + + const cookieHeader = request.headers.get('cookie'); + if (cookieHeader) { + const authTokenMatch = cookieHeader.match(/auth-token=([^;]+)/); + if (authTokenMatch) { + const token = authTokenMatch[1]; + + try { + const decoded = verifyToken(token); + + const user = await prisma.user.findUnique({ + where: { id: decoded.userId }, + select: { + id: true, + firstName: true, + lastName: true, + email: true, + plan: true, + status: true, + expirationDate: true, + customPermissions: true + } + }); + + if (user && user.status === 'ACTIVE') { + const isAdmin = user.plan === 'ADMIN'; + session = { + user: { + id: user.id, + role: isAdmin ? 'ADMIN' : 'USER', + name: `${user.firstName} ${user.lastName}`, + email: user.email, + plan: user.plan, + canUploadPortfolio: true, + hasVipAccess: true + } + }; + console.log('✅ Autenticação alternativa bem-sucedida'); + } + } catch (error) { + console.log('⌠Erro JWT:', (error as Error).message); + } + } + } + } + + // MÉTODO 3: Fallback para desenvolvimento + if (!session) { + console.log('Usando fallback de desenvolvimento...'); + const adminUser = await prisma.user.findFirst({ + where: { + plan: 'ADMIN', + status: 'ACTIVE' + } + }); + + if (adminUser) { + session = { + user: { + id: adminUser.id, + role: 'ADMIN', + name: `${adminUser.firstName} ${adminUser.lastName}`, + email: adminUser.email, + plan: adminUser.plan, + canUploadPortfolio: true, + hasVipAccess: true + } + }; + console.log('✅ Fallback admin ativado para desenvolvimento'); + } + } + + // Verificações finais + if (!session?.user) { + console.log('⌠PUT: Nenhum método de autenticação funcionou'); + return NextResponse.json({ + error: 'Acesso negado', + debug: 'Autenticação falhou na rota PUT' + }, { status: 403 }); + } + + if (session.user.plan !== 'ADMIN') { + console.log('⌠PUT: Usuário não é admin:', session.user.plan); + return NextResponse.json({ + error: 'Acesso negado', + debug: `Usuário tem plan: ${session.user.plan}, necessário: ADMIN` + }, { status: 403 }); + } + + console.log('✅ PUT: ACESSO AUTORIZADO para:', session.user.email); + + const body = await request.json(); + const { + id, // 🆕 MUDANÇA: usar 'id' ao invés de 'carteiraId' + carteiraId, // Manter compatibilidade + status, + feedback, + recomendacoes, + pontuacao, + riscoBeneficio, + diversificacao, + // 🆕 NOVOS CAMPOS + avaliacaoQualidade, + avaliacaoDiversificacao, + avaliacaoAdaptacao, + dadosEstruturados, + dataAnalise + } = body; + + // Usar 'id' ou 'carteiraId' para compatibilidade + const carteiraIdFinal = id || carteiraId; + + console.log('📠Dados recebidos:', { + carteiraIdFinal, + feedback, + pontuacao, + recomendacoes, + avaliacaoQualidade, + avaliacaoDiversificacao, + avaliacaoAdaptacao + }); + + if (!carteiraIdFinal) { + return NextResponse.json({ error: 'ID da carteira é obrigatório' }, { status: 400 }); + } + + const updateData: any = {}; + + if (status) { + updateData.status = status.toUpperCase(); + + if (status.toUpperCase() === 'EM_ANALISE') { + updateData.analistaId = session.user.id; + } + + if (status.toUpperCase() === 'ANALISADA') { + updateData.dataAnalise = new Date(); + updateData.analistaId = session.user.id; + } + } + + // Campos originais + if (feedback) updateData.feedback = feedback; + if (recomendacoes) updateData.recomendacoes = JSON.stringify(recomendacoes); + if (pontuacao !== undefined) updateData.pontuacao = pontuacao; + if (riscoBeneficio) updateData.riscoBeneficio = riscoBeneficio; + if (diversificacao !== undefined) updateData.diversificacao = diversificacao; + + // 🆕 NOVOS CAMPOS + if (avaliacaoQualidade !== undefined) updateData.avaliacaoQualidade = avaliacaoQualidade; + if (avaliacaoDiversificacao !== undefined) updateData.avaliacaoDiversificacao = avaliacaoDiversificacao; + if (avaliacaoAdaptacao !== undefined) updateData.avaliacaoAdaptacao = avaliacaoAdaptacao; + if (dadosEstruturados) updateData.dadosEstruturados = JSON.stringify(dadosEstruturados); + if (dataAnalise) updateData.dataAnalise = new Date(dataAnalise); + + console.log('💾 Atualizando carteira:', carteiraIdFinal, 'com dados:', updateData); + + const carteiraAtualizada = await prisma.carteiraAnalise.update({ + where: { id: carteiraIdFinal }, + data: updateData, + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + }, + analista: { + select: { + id: true, + firstName: true, + lastName: true + } + }, + ativos: true + } + }); + + console.log('✅ Carteira atualizada com sucesso:', carteiraAtualizada.id); + + return NextResponse.json({ + success: true, + message: 'Análise salva com sucesso!', + carteira: { + id: carteiraAtualizada.id, + nomeArquivo: carteiraAtualizada.nomeArquivo, + status: mapearStatus(carteiraAtualizada.status), + dataEnvio: carteiraAtualizada.dataEnvio, + dataAnalise: carteiraAtualizada.dataAnalise, + valorTotal: carteiraAtualizada.valorTotal, + quantidadeAtivos: carteiraAtualizada.quantidadeAtivos, + feedback: carteiraAtualizada.feedback, + recomendacoes: carteiraAtualizada.recomendacoes ? JSON.parse(carteiraAtualizada.recomendacoes) : [], + pontuacao: carteiraAtualizada.pontuacao, + riscoBeneficio: carteiraAtualizada.riscoBeneficio, + diversificacao: carteiraAtualizada.diversificacao, + // 🆕 RETORNAR OS NOVOS CAMPOS + avaliacaoQualidade: carteiraAtualizada.avaliacaoQualidade, + avaliacaoDiversificacao: carteiraAtualizada.avaliacaoDiversificacao, + avaliacaoAdaptacao: carteiraAtualizada.avaliacaoAdaptacao, + dadosEstruturados: carteiraAtualizada.dadosEstruturados ? JSON.parse(carteiraAtualizada.dadosEstruturados) : null, + cliente: carteiraAtualizada.user ? { + id: carteiraAtualizada.user.id, + name: `${carteiraAtualizada.user.firstName} ${carteiraAtualizada.user.lastName}`, + email: carteiraAtualizada.user.email + } : null, + analista: carteiraAtualizada.analista ? { + id: carteiraAtualizada.analista.id, + name: `${carteiraAtualizada.analista.firstName} ${carteiraAtualizada.analista.lastName}` + } : null, + questionario: null + } + }); + + } catch (error) { + console.error('💥 Erro ao atualizar carteira:', error); + return NextResponse.json({ + error: 'Erro interno do servidor', + details: error.message + }, { status: 500 }); + } +} + +function calcularAnaliseRisco(ativos: any[]) { + if (!ativos.length) return null; + + const totalCarteira = ativos.reduce((sum, ativo) => sum + ativo.valorTotal, 0); + + // Concentração por ativo + const concentracoes = ativos.map(ativo => ({ + codigo: ativo.codigo, + percentual: (ativo.valorTotal / totalCarteira) * 100 + })); + + // Verificar concentração excessiva + const concentracaoMaxima = Math.max(...concentracoes.map(c => c.percentual)); + const concentracaoTop3 = concentracoes + .sort((a, b) => b.percentual - a.percentual) + .slice(0, 3) + .reduce((sum, c) => sum + c.percentual, 0); + + // Análise por tipo + const distribuicaoTipo = ativos.reduce((acc, ativo) => { + acc[ativo.tipo] = (acc[ativo.tipo] || 0) + ativo.valorTotal; + return acc; + }, {} as Record); + + const percentualPorTipo = Object.entries(distribuicaoTipo).map(([tipo, valor]) => ({ + tipo, + percentual: (valor / totalCarteira) * 100 + })); + + // Score de risco (0-10) + let scoreRisco = 5; // Neutro + + // Penalizar concentração excessiva + if (concentracaoMaxima > 20) scoreRisco += 2; + if (concentracaoTop3 > 60) scoreRisco += 1; + + // Recompensar diversificação + if (ativos.length > 15) scoreRisco -= 1; + if (percentualPorTipo.length > 2) scoreRisco -= 0.5; + + return { + scoreRisco: Math.max(0, Math.min(10, scoreRisco)), + concentracaoMaxima, + concentracaoTop3, + distribuicaoTipo: percentualPorTipo, + numeroAtivos: ativos.length, + recomendacoes: gerarRecomendacoesRisco(concentracaoMaxima, concentracaoTop3, ativos.length) + }; +} + +function gerarRecomendacoesRisco(concentracaoMax: number, concentracaoTop3: number, numeroAtivos: number) { + const recomendacoes = []; + + if (concentracaoMax > 25) { + recomendacoes.push(`Ativo mais concentrado representa ${concentracaoMax.toFixed(1)}% da carteira. Considere reduzir para máximo 20%.`); + } + + if (concentracaoTop3 > 70) { + recomendacoes.push(`Top 3 ativos representam ${concentracaoTop3.toFixed(1)}% da carteira. Aumente a diversificação.`); + } + + if (numeroAtivos < 8) { + recomendacoes.push('Carteira com poucos ativos. Considere adicionar mais posições para melhor diversificação.'); + } + + if (numeroAtivos > 30) { + recomendacoes.push('Carteira muito pulverizada. Considere concentrar em suas melhores convicções.'); + } + + return recomendacoes; +} + +function gerarRecomendacaoIA(ativos: any[], questionario: any) { + if (!ativos.length) return null; + + const totalCarteira = ativos.reduce((sum, ativo) => sum + ativo.valorTotal, 0); + const distribuicaoTipo = ativos.reduce((acc, ativo) => { + acc[ativo.tipo] = (acc[ativo.tipo] || 0) + ativo.valorTotal; + return acc; + }, {} as Record); + + const percentualAcoes = ((distribuicaoTipo.ACAO || 0) / totalCarteira) * 100; + const percentualFIIs = ((distribuicaoTipo.FII || 0) / totalCarteira) * 100; + + const recomendacoes = []; + + // Baseado no perfil de risco + if (questionario?.toleranciaOscilacao === 'Conservadora' && percentualAcoes > 60) { + recomendacoes.push('Perfil conservador com alta exposição em ações. Considere aumentar renda fixa.'); + } + + if (questionario?.objetivoCarteira === 'Renda passiva' && percentualFIIs < 30) { + recomendacoes.push('Objetivo de renda passiva com baixa exposição em FIIs. Considere aumentar a posição.'); + } + + return { + resumo: `Carteira com ${ativos.length} ativos, R$ ${totalCarteira.toLocaleString('pt-BR', { minimumFractionDigits: 2 })} investidos.`, + recomendacoes, + scoreGeral: Math.random() * 10 // Substituir por análise real + }; +} + +async function notificarCliente(carteira: any) { + // Implementar notificação para o cliente + console.log(`Análise finalizada para cliente ${carteira.user.email}: ${carteira.id}`); +} \ No newline at end of file diff --git a/src/app/api/admin/instagram-cadastros/route.ts b/src/app/api/admin/instagram-cadastros/route.ts new file mode 100644 index 000000000..014a879b2 --- /dev/null +++ b/src/app/api/admin/instagram-cadastros/route.ts @@ -0,0 +1,90 @@ +// src/app/api/admin/instagram-cadastros/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +export async function GET(req: NextRequest) { + try { + console.log('🔠API Admin chamada...'); + const { searchParams } = new URL(req.url); + const page = Math.max(1, Number(searchParams.get('page') ?? 1)); + const rawLimit = Number(searchParams.get('limit') ?? 50); + const limit = Math.min(100, Math.max(20, Number.isFinite(rawLimit) ? rawLimit : 50)); + + // Buscar todos os cadastros sem verificação (TEMPORÃRIO) + const cadastros = await prisma.instagramCadastro.findMany({ + select: { + id: true, + instagram: true, + previousInstagram: true, + isUpdated: true, + createdAt: true, + updatedAt: true, + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true, + plan: true, + status: true, + createdAt: true + } + } + }, + orderBy: { createdAt: 'desc' }, + skip: (page - 1) * limit, + take: limit + }); + const total = await prisma.instagramCadastro.count(); + + console.log('✅ Cadastros encontrados:', cadastros.length); + + return NextResponse.json({ + success: true, + data: cadastros, + total, + pagination: { + page, + limit, + totalPages: Math.ceil(total / limit) + } + }); + + } catch (error) { + console.error('💥 Erro ao buscar cadastros:', error); + return NextResponse.json( + { error: 'Erro interno do servidor' }, + { status: 500 } + ); + } +} + +export async function DELETE(req: NextRequest) { + try { + const { searchParams } = new URL(req.url); + const cadastroId = searchParams.get('id'); + + if (!cadastroId) { + return NextResponse.json( + { error: 'ID do cadastro é obrigatório' }, + { status: 400 } + ); + } + + await prisma.instagramCadastro.delete({ + where: { id: cadastroId } + }); + + return NextResponse.json({ + success: true, + message: 'Cadastro removido com sucesso' + }); + + } catch (error) { + console.error('Erro ao remover cadastro:', error); + return NextResponse.json( + { error: 'Erro interno do servidor' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/admin/integracoes/hotmart/route.ts b/src/app/api/admin/integracoes/hotmart/route.ts new file mode 100644 index 000000000..da48c3b6c --- /dev/null +++ b/src/app/api/admin/integracoes/hotmart/route.ts @@ -0,0 +1,183 @@ +export const dynamic = 'force-dynamic'; + +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { HOTMART_INTEGRATIONS } from '@/data/integrations'; + +// Verificar autenticação admin +async function verifyAuth(request: NextRequest) { + const userEmail = request.headers.get('x-user-email'); + return userEmail === 'admin@fatosdobolsa.com'; +} + +// GET - Listar todas as integrações +export async function GET(request: NextRequest) { + try { + if (!(await verifyAuth(request))) { + return NextResponse.json({ error: 'Acesso negado' }, { status: 401 }); + } + + await prisma.$connect(); + + const origin = new URL(request.url).origin; + + const integrations = HOTMART_INTEGRATIONS.map(integration => ({ + ...integration, + webhookUrl: `${origin}/api/webhooks/hotmart/${integration.id}`, + })); + + await prisma.$disconnect(); + + return NextResponse.json({ + success: true, + integrations, + total: integrations.length + }); + + } catch (error: any) { + console.error('⌠Erro ao buscar integrações:', error); + await prisma.$disconnect(); + + return NextResponse.json( + { error: 'Erro interno do servidor' }, + { status: 500 } + ); + } +} + +// POST - Criar nova integração +export async function POST(request: NextRequest) { + try { + if (!(await verifyAuth(request))) { + return NextResponse.json({ error: 'Acesso negado' }, { status: 401 }); + } + + const { productName, productId, plan } = await request.json(); + + if (!productName || !productId || !plan) { + return NextResponse.json( + { error: 'Dados obrigatórios: productName, productId, plan' }, + { status: 400 } + ); + } + + await prisma.$connect(); + + // Gerar ID único para a integração + const integrationId = Math.random().toString().slice(2, 8); + const webhookUrl = `${new URL(request.url).origin}/api/webhooks/hotmart/${integrationId}`; + + // Por enquanto, apenas retornar a integração criada + // Mais tarde implementaremos salvamento no banco + const newIntegration = { + id: integrationId, + productName, + productId, + plan, + webhookUrl, + status: 'ACTIVE', + createdAt: new Date().toISOString().split('T')[0], + totalSales: 0 + }; + + await prisma.$disconnect(); + + console.log('✅ Nova integração criada:', newIntegration); + + return NextResponse.json({ + success: true, + integration: newIntegration, + message: 'Integração criada com sucesso' + }); + + } catch (error: any) { + console.error('⌠Erro ao criar integração:', error); + await prisma.$disconnect(); + + return NextResponse.json( + { error: 'Erro interno do servidor' }, + { status: 500 } + ); + } +} + +// PUT - Atualizar integração existente +export async function PUT(request: NextRequest) { + try { + if (!(await verifyAuth(request))) { + return NextResponse.json({ error: 'Acesso negado' }, { status: 401 }); + } + + const { id, productName, productId, plan, status } = await request.json(); + + if (!id) { + return NextResponse.json( + { error: 'ID da integração é obrigatório' }, + { status: 400 } + ); + } + + // Por enquanto, apenas retornar sucesso + // Mais tarde implementaremos atualização no banco + const updatedIntegration = { + id, + productName, + productId, + plan, + status, + updatedAt: new Date().toISOString() + }; + + console.log('✅ Integração atualizada:', updatedIntegration); + + return NextResponse.json({ + success: true, + integration: updatedIntegration, + message: 'Integração atualizada com sucesso' + }); + + } catch (error: any) { + console.error('⌠Erro ao atualizar integração:', error); + + return NextResponse.json( + { error: 'Erro interno do servidor' }, + { status: 500 } + ); + } +} + +// DELETE - Excluir integração +export async function DELETE(request: NextRequest) { + try { + if (!(await verifyAuth(request))) { + return NextResponse.json({ error: 'Acesso negado' }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const id = searchParams.get('id'); + + if (!id) { + return NextResponse.json( + { error: 'ID da integração é obrigatório' }, + { status: 400 } + ); + } + + // Por enquanto, apenas retornar sucesso + // Mais tarde implementaremos exclusão no banco + console.log('✅ Integração excluída:', id); + + return NextResponse.json({ + success: true, + message: 'Integração excluída com sucesso' + }); + + } catch (error: any) { + console.error('⌠Erro ao excluir integração:', error); + + return NextResponse.json( + { error: 'Erro interno do servidor' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/admin/integracoes/stats/route.ts b/src/app/api/admin/integracoes/stats/route.ts new file mode 100644 index 000000000..a00dd1b39 --- /dev/null +++ b/src/app/api/admin/integracoes/stats/route.ts @@ -0,0 +1,201 @@ +export const dynamic = 'force-dynamic'; + +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; +import { + PLATFORM_METADATA, + PLATFORM_SLUGS, + getPublicIntegrationsByPlatform, + type PublicPlatformIntegrationConfig, + type PlatformSlug, +} from '@/data/integrations'; + +interface IntegrationSummary { + id: string; + productName: string; + plan: string; + status: 'ACTIVE' | 'INACTIVE'; + totalSales: number; + webhookUrl: string; + productId?: string; +} + +interface PlatformPayload { + slug: PlatformSlug; + name: string; + emoji: string; + color: string; + description: string; + status: 'active' | 'inactive' | 'coming-soon'; + integrations: IntegrationSummary[]; + totals: { + totalIntegrations: number; + activeIntegrations: number; + totalSales: number; + }; +} + +function buildWebhookUrl(origin: string, slug: PlatformSlug, integration: PublicPlatformIntegrationConfig): string { + if (slug === 'hotmart') { + return `${origin}/api/webhooks/hotmart/${integration.id}`; + } + + return `${origin}/api/webhooks/${slug}/{token}`; +} + +function normaliseIntegrations( + origin: string, + slug: PlatformSlug, + integrations: readonly PublicPlatformIntegrationConfig[], + salesByProductName: Map, +): IntegrationSummary[] { + return integrations.map(integration => ({ + id: integration.id, + productName: integration.productName, + plan: integration.plan, + status: integration.status, + totalSales: salesByProductName.get(integration.productName) ?? 0, + webhookUrl: buildWebhookUrl(origin, slug, integration), + ...('productId' in integration ? { productId: integration.productId } : {}), + })); +} + +function detectPlatformByProductName(productName?: string | null): PlatformSlug | null { + if (!productName) return null; + const normalized = productName.toLowerCase(); + + if (normalized.includes('hotmart')) return 'hotmart'; + if (normalized.includes('kiwify')) return 'kiwify'; + if (normalized.includes('eduzz')) return 'eduzz'; + + return null; +} + +function detectPlatformByOrigin(origin?: string | null): PlatformSlug | null { + if (!origin) return null; + const normalized = origin.toLowerCase(); + + if (normalized.includes('hotmart')) return 'hotmart'; + if (normalized.includes('kiwify')) return 'kiwify'; + if (normalized.includes('eduzz')) return 'eduzz'; + + return null; +} + +function computePlatformPayload( + origin: string, + slug: PlatformSlug, + salesByProductName: Map, + salesByPlatform: Record, +): PlatformPayload { + const meta = PLATFORM_METADATA[slug]; + const integrations = getPublicIntegrationsByPlatform(slug); + const summaries = normaliseIntegrations(origin, slug, integrations, salesByProductName); + const activeIntegrations = integrations.filter(integration => integration.status === 'ACTIVE').length; + const totalSales = salesByPlatform[slug] ?? 0; + + return { + slug, + name: meta.name, + emoji: meta.emoji, + color: meta.color, + description: meta.description, + status: activeIntegrations > 0 ? 'active' : meta.status, + integrations: summaries, + totals: { + totalIntegrations: integrations.length, + activeIntegrations, + totalSales, + }, + }; +} + +async function getSalesAggregates() { + const purchases = await prisma.purchase.findMany({ + where: { + status: 'COMPLETED', + }, + select: { + productName: true, + user: { + select: { + origin: true, + }, + }, + }, + }); + + const salesByProductName = new Map(); + const salesByPlatform: Record = { + hotmart: 0, + kiwify: 0, + eduzz: 0, + }; + + for (const purchase of purchases) { + if (purchase.productName) { + const key = purchase.productName.trim(); + salesByProductName.set(key, (salesByProductName.get(key) ?? 0) + 1); + } + + const platformFromName = detectPlatformByProductName(purchase.productName); + const platformFromOrigin = detectPlatformByOrigin(purchase.user?.origin ?? null); + const platform = platformFromName ?? platformFromOrigin; + + if (platform) { + salesByPlatform[platform] += 1; + } + } + + return { salesByProductName, salesByPlatform }; +} + +async function verifyAuth() { + try { + const session = await auth(); + if (session?.user?.plan === 'ADMIN') { + return session.user; + } + } catch (error) { + console.error('⌠Erro ao verificar autenticação das integrações:', error); + } + return null; +} + +export async function GET(request: NextRequest) { + const admin = await verifyAuth(); + + if (!admin) { + return NextResponse.json( + { + success: false, + error: 'Acesso negado. É necessário ser administrador para visualizar as integrações.', + }, + { status: 401 }, + ); + } + + const origin = new URL(request.url).origin; + const { salesByProductName, salesByPlatform } = await getSalesAggregates(); + const platforms = PLATFORM_SLUGS.map(slug => + computePlatformPayload(origin, slug, salesByProductName, salesByPlatform), + ); + + const totals = platforms.reduce( + (acc, platform) => { + acc.totalIntegrations += platform.totals.totalIntegrations; + acc.activeIntegrations += platform.totals.activeIntegrations; + acc.totalSales += platform.totals.totalSales; + return acc; + }, + { totalIntegrations: 0, activeIntegrations: 0, totalSales: 0 }, + ); + + return NextResponse.json({ + success: true, + platforms, + totals, + generatedAt: new Date().toISOString(), + }); +} diff --git a/src/app/api/admin/migracao-plano/calcular/route.ts b/src/app/api/admin/migracao-plano/calcular/route.ts new file mode 100644 index 000000000..e825b0e6c --- /dev/null +++ b/src/app/api/admin/migracao-plano/calcular/route.ts @@ -0,0 +1,204 @@ +export const dynamic = 'force-dynamic'; + +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; +import { getPaymentMethodDisplayName, formatInstallments } from '@/lib/payment-normalization'; + +// Valores dos planos +const VALOR_LITE = 511.86; +const VALOR_VIP = 1297.00; +const MESES_ANO = 12; + +/** + * Calcula meses entre duas datas + */ +function calcularMesesEntreDatas(dataInicio: Date, dataFim: Date): number { + const anos = dataFim.getFullYear() - dataInicio.getFullYear(); + const meses = dataFim.getMonth() - dataInicio.getMonth(); + return anos * 12 + meses; +} + +/** + * GET /api/admin/migracao-plano/calcular?email=cliente@email.com + * Busca cliente e calcula valor da migração + */ +export async function GET(request: NextRequest) { + try { + // Verificar autenticação + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json( + { error: 'Não autenticado' }, + { status: 401 } + ); + } + + // Verificar se é admin + const sessionUser = await prisma.user.findUnique({ + where: { email: session.user.email! }, + select: { customPermissions: true } + }); + + const permissions = JSON.parse(sessionUser?.customPermissions || '[]'); + if (!permissions.includes('admin')) { + return NextResponse.json( + { error: 'Acesso negado. Apenas administradores.' }, + { status: 403 } + ); + } + + // Extrair parâmetros + const searchParams = request.nextUrl.searchParams; + const email = searchParams.get('email'); + + if (!email) { + return NextResponse.json( + { error: 'E-mail é obrigatório' }, + { status: 400 } + ); + } + + // Buscar cliente + const cliente = await prisma.user.findUnique({ + where: { email: email.toLowerCase().trim() }, + include: { + purchases: { + where: { + status: 'COMPLETED', + amount: { gt: 0 } + }, + orderBy: { + createdAt: 'desc' + }, + take: 1 + } + } + }); + + if (!cliente) { + return NextResponse.json( + { error: 'Cliente não encontrado' }, + { status: 404 } + ); + } + + // Verificar se está no plano LITE + if (cliente.plan !== 'LITE' && cliente.plan !== 'LITE_V2') { + return NextResponse.json( + { error: `Cliente está no plano ${cliente.plan}. Apenas clientes do plano LITE podem migrar.` }, + { status: 400 } + ); + } + + // Verificar se o plano está ativo + if (cliente.status !== 'ACTIVE') { + return NextResponse.json( + { error: 'Plano do cliente não está ativo' }, + { status: 400 } + ); + } + + // Verificar se tem data de expiração + if (!cliente.expirationDate) { + return NextResponse.json( + { error: 'Cliente não tem data de expiração definida' }, + { status: 400 } + ); + } + + // Buscar compra original + const compraOriginal = cliente.purchases[0]; + if (!compraOriginal) { + return NextResponse.json( + { error: 'Não foi encontrada compra do cliente' }, + { status: 404 } + ); + } + + // Calcular meses utilizados e restantes + const dataAtual = new Date(); + const dataContratacao = cliente.createdAt; + const dataExpiracao = new Date(cliente.expirationDate); + + // Garantir que a data de expiração não passou + if (dataExpiracao <= dataAtual) { + return NextResponse.json( + { error: 'O plano do cliente já expirou' }, + { status: 400 } + ); + } + + const mesesUtilizados = Math.max(0, calcularMesesEntreDatas(dataContratacao, dataAtual)); + const mesesRestantes = Math.max(0, calcularMesesEntreDatas(dataAtual, dataExpiracao)); + + // Calcular diferença mensal entre planos + const diferencaMensal = (VALOR_VIP - VALOR_LITE) / MESES_ANO; + + // Calcular desconto (proporcional aos meses restantes) + const valorDesconto = diferencaMensal * mesesRestantes; + + // Calcular valor final da migração + const valorFinalMigracao = Math.max(0, VALOR_VIP - valorDesconto); + + // Preparar resposta + const response = { + cliente: { + id: cliente.id, + nome: `${cliente.firstName} ${cliente.lastName}`, + email: cliente.email, + planoAtual: cliente.plan, + dataContratacao: dataContratacao.toISOString(), + dataExpiracao: dataExpiracao.toISOString(), + status: cliente.status, + origin: cliente.origin + }, + compraOriginal: { + id: compraOriginal.id, + valor: compraOriginal.amount, + valorFormatado: `R$ ${compraOriginal.amount.toFixed(2)}`, + formaPagamento: getPaymentMethodDisplayName(compraOriginal.paymentMethod), + formaPagamentoCodigo: compraOriginal.paymentMethod, + numeroParcelas: compraOriginal.installments, + parcelamentoFormatado: formatInstallments(compraOriginal.installments, compraOriginal.amount), + plataforma: compraOriginal.platform, + dataCompra: compraOriginal.createdAt.toISOString(), + produtoNome: compraOriginal.productName + }, + calculo: { + valorLite: VALOR_LITE, + valorLiteFormatado: `R$ ${VALOR_LITE.toFixed(2)}`, + valorVip: VALOR_VIP, + valorVipFormatado: `R$ ${VALOR_VIP.toFixed(2)}`, + mesesUtilizados, + mesesRestantes, + diferencaMensal, + diferencaMensalFormatada: `R$ ${diferencaMensal.toFixed(2)}`, + valorDesconto, + valorDescontoFormatado: `R$ ${valorDesconto.toFixed(2)}`, + valorFinalMigracao, + valorFinalMigracaoFormatado: `R$ ${valorFinalMigracao.toFixed(2)}` + }, + observacoes: [ + compraOriginal.installments > 1 + ? `As ${compraOriginal.installments} parcelas do CF LITE (${formatInstallments(compraOriginal.installments, compraOriginal.amount)}) continuarão sendo cobradas` + : 'O pagamento do CF LITE foi à vista', + 'O plano VIP terá validade de 1 ano a partir da data da compra', + `O desconto de R$ ${valorDesconto.toFixed(2)} será aplicado no valor do VIP` + ] + }; + + return NextResponse.json(response); + + } catch (error: any) { + console.error('⌠Erro ao calcular migração:', error); + return NextResponse.json( + { + error: 'Erro interno ao calcular migração', + message: error.message + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/admin/migracao-plano/processar/route.ts b/src/app/api/admin/migracao-plano/processar/route.ts new file mode 100644 index 000000000..7efb93b79 --- /dev/null +++ b/src/app/api/admin/migracao-plano/processar/route.ts @@ -0,0 +1,151 @@ +export const dynamic = 'force-dynamic'; + +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +/** + * POST /api/admin/migracao-plano/processar + * Processa a migração do cliente de LITE para VIP + */ +export async function POST(request: NextRequest) { + try { + // Verificar autenticação + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json( + { error: 'Não autenticado' }, + { status: 401 } + ); + } + + // Verificar se é admin + const sessionUser = await prisma.user.findUnique({ + where: { email: session.user.email! }, + select: { id: true, customPermissions: true } + }); + + if (!sessionUser) { + return NextResponse.json( + { error: 'Usuário não encontrado' }, + { status: 404 } + ); + } + + const permissions = JSON.parse(sessionUser.customPermissions || '[]'); + if (!permissions.includes('admin')) { + return NextResponse.json( + { error: 'Acesso negado. Apenas administradores.' }, + { status: 403 } + ); + } + + // Extrair dados do body + const body = await request.json(); + const { userId, valorFinal, mesesRestantes, valorDesconto, observacoes } = body; + + if (!userId) { + return NextResponse.json( + { error: 'userId é obrigatório' }, + { status: 400 } + ); + } + + // Buscar cliente + const cliente = await prisma.user.findUnique({ + where: { id: userId } + }); + + if (!cliente) { + return NextResponse.json( + { error: 'Cliente não encontrado' }, + { status: 404 } + ); + } + + // Verificar se está no plano LITE + if (cliente.plan !== 'LITE' && cliente.plan !== 'LITE_V2') { + return NextResponse.json( + { error: `Cliente está no plano ${cliente.plan}. Apenas clientes do plano LITE podem migrar.` }, + { status: 400 } + ); + } + + // Verificar se o plano está ativo + if (cliente.status !== 'ACTIVE') { + return NextResponse.json( + { error: 'Plano do cliente não está ativo' }, + { status: 400 } + ); + } + + const VALOR_LITE = 511.86; + const VALOR_VIP = 1297.00; + + // Iniciar transação + const result = await prisma.$transaction(async (tx) => { + // Registrar migração + const migracao = await tx.planMigration.create({ + data: { + userId: cliente.id, + planOriginal: cliente.plan, + planNovo: 'VIP', + valorOriginal: VALOR_LITE, + valorNovo: VALOR_VIP, + mesesRestantes: mesesRestantes || 0, + valorDesconto: valorDesconto || 0, + valorFinal: valorFinal || VALOR_VIP, + status: 'COMPLETED', + observacoes: observacoes || null, + processadoPor: sessionUser.id, + dataProcessamento: new Date() + } + }); + + // Atualizar plano do cliente para VIP + // Nova data de expiração: 1 ano a partir de agora + const novaDataExpiracao = new Date(); + novaDataExpiracao.setFullYear(novaDataExpiracao.getFullYear() + 1); + + const clienteAtualizado = await tx.user.update({ + where: { id: cliente.id }, + data: { + plan: 'VIP', + expirationDate: novaDataExpiracao + } + }); + + return { migracao, clienteAtualizado }; + }); + + return NextResponse.json({ + success: true, + message: 'Migração processada com sucesso', + migracao: { + id: result.migracao.id, + planOriginal: result.migracao.planOriginal, + planNovo: result.migracao.planNovo, + valorFinal: result.migracao.valorFinal, + valorFinalFormatado: `R$ ${result.migracao.valorFinal.toFixed(2)}`, + dataProcessamento: result.migracao.dataProcessamento + }, + cliente: { + id: result.clienteAtualizado.id, + email: result.clienteAtualizado.email, + planoNovo: result.clienteAtualizado.plan, + novaDataExpiracao: result.clienteAtualizado.expirationDate + } + }); + + } catch (error: any) { + console.error('⌠Erro ao processar migração:', error); + return NextResponse.json( + { + error: 'Erro interno ao processar migração', + message: error.message + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/admin/renovacoes/route.ts b/src/app/api/admin/renovacoes/route.ts new file mode 100644 index 000000000..83ceabc2d --- /dev/null +++ b/src/app/api/admin/renovacoes/route.ts @@ -0,0 +1,326 @@ +export const dynamic = 'force-dynamic'; + +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +// Verificar autenticação admin +async function verifyAuth(request: NextRequest) { + const userEmail = request.headers.get('x-user-email'); + return userEmail === 'admin@fatosdobolsa.com'; +} + +// GET - Dados do dashboard de renovações +export async function GET(request: NextRequest) { + try { + if (!(await verifyAuth(request))) { + return NextResponse.json({ error: 'Acesso negado' }, { status: 401 }); + } + + await prisma.$connect(); + + // Calcular datas importantes + const now = new Date(); + const in7Days = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); + const in15Days = new Date(now.getTime() + 15 * 24 * 60 * 60 * 1000); + const in30Days = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); + const endOfLastMonth = new Date(now.getFullYear(), now.getMonth(), 0); + + // 1. Estatísticas gerais de usuários + const totalUsers = await prisma.user.count(); + const activeUsers = await prisma.user.count({ + where: { + status: 'ACTIVE', + OR: [ + { expirationDate: null }, // Vitalícios + { expirationDate: { gte: now } } // Não expirados + ] + } + }); + const inactiveUsers = totalUsers - activeUsers; + + // 2. Usuários expirando + const expiringIn7Days = await prisma.user.count({ + where: { + status: 'ACTIVE', + expirationDate: { + gte: now, + lte: in7Days + } + } + }); + + const expiringIn15Days = await prisma.user.count({ + where: { + status: 'ACTIVE', + expirationDate: { + gte: now, + lte: in15Days + } + } + }); + + const expiringIn30Days = await prisma.user.count({ + where: { + status: 'ACTIVE', + expirationDate: { + gte: now, + lte: in30Days + } + } + }); + + // 3. Renovações (purchase com mesmo email) + const renewalsThisMonth = await prisma.purchase.count({ + where: { + createdAt: { gte: startOfMonth }, + user: { + purchases: { + some: { + createdAt: { lt: startOfMonth } + } + } + } + } + }); + + const renewalsLastMonth = await prisma.purchase.count({ + where: { + createdAt: { + gte: lastMonth, + lte: endOfLastMonth + }, + user: { + purchases: { + some: { + createdAt: { lt: lastMonth } + } + } + } + } + }); + + // 4. Cálculo de churn e retenção + const totalRenewalsExpected = await prisma.user.count({ + where: { + expirationDate: { + gte: lastMonth, + lte: endOfLastMonth + } + } + }); + + const churnRate = totalRenewalsExpected > 0 ? + ((totalRenewalsExpected - renewalsLastMonth) / totalRenewalsExpected) * 100 : 0; + const retentionRate = 100 - churnRate; + + // 5. Usuários expirando com detalhes + const usersExpiring = await prisma.user.findMany({ + where: { + status: 'ACTIVE', + expirationDate: { + gte: now, + lte: in30Days + } + }, + include: { + purchases: { + orderBy: { createdAt: 'desc' }, + take: 1 + } + }, + orderBy: { expirationDate: 'asc' } + }); + + // Formatar usuários expirando + const formattedUsersExpiring = usersExpiring.map(user => { + const daysUntilExpiry = user.expirationDate ? + Math.ceil((new Date(user.expirationDate).getTime() - now.getTime()) / (1000 * 60 * 60 * 24)) : 0; + + return { + id: user.id, + name: `${user.firstName} ${user.lastName}`, + email: user.email, + plan: user.plan, + expirationDate: user.expirationDate?.toISOString(), + daysUntilExpiry, + lastRenewal: user.purchases[0]?.createdAt?.toISOString(), + totalPurchases: user.purchases.reduce((sum, p) => sum + p.amount, 0), + status: user.status, + emailSent: false // Implementar lógica de controle de email + }; + }); + + // 6. Dados mensais para gráficos (últimos 7 meses) + const monthlyData = []; + for (let i = 6; i >= 0; i--) { + const monthStart = new Date(now.getFullYear(), now.getMonth() - i, 1); + const monthEnd = new Date(now.getFullYear(), now.getMonth() - i + 1, 0); + + const monthRenewals = await prisma.purchase.count({ + where: { + createdAt: { gte: monthStart, lte: monthEnd }, + user: { + purchases: { + some: { + createdAt: { lt: monthStart } + } + } + } + } + }); + + const monthNewUsers = await prisma.user.count({ + where: { + createdAt: { gte: monthStart, lte: monthEnd } + } + }); + + const monthCancellations = await prisma.user.count({ + where: { + updatedAt: { gte: monthStart, lte: monthEnd }, + status: 'INACTIVE' + } + }); + + const monthRevenue = await prisma.purchase.aggregate({ + where: { + createdAt: { gte: monthStart, lte: monthEnd } + }, + _sum: { amount: true } + }); + + monthlyData.push({ + month: monthStart.toLocaleDateString('pt-BR', { month: 'short', year: 'numeric' }), + renovacoes: monthRenewals, + novosUsuarios: monthNewUsers, + cancelamentos: monthCancellations, + receita: monthRevenue._sum.amount || 0 + }); + } + + await prisma.$disconnect(); + + const stats = { + totalUsers, + activeUsers, + inactiveUsers, + expiringIn7Days, + expiringIn15Days, + expiringIn30Days, + renewalsThisMonth, + renewalsLastMonth, + churnRate: Math.round(churnRate * 10) / 10, + retentionRate: Math.round(retentionRate * 10) / 10 + }; + + console.log('✅ Dados de renovação carregados:', { + stats, + usersExpiringCount: formattedUsersExpiring.length, + monthlyDataPoints: monthlyData.length + }); + + return NextResponse.json({ + success: true, + stats, + usersExpiring: formattedUsersExpiring, + monthlyData, + timestamp: new Date().toISOString() + }); + + } catch (error: any) { + console.error('⌠Erro ao carregar dados de renovação:', error); + await prisma.$disconnect(); + + return NextResponse.json( + { error: 'Erro interno do servidor' }, + { status: 500 } + ); + } +} + +// POST - Enviar emails de lembrete +export async function POST(request: NextRequest) { + try { + if (!(await verifyAuth(request))) { + return NextResponse.json({ error: 'Acesso negado' }, { status: 401 }); + } + + const { action, userId, userIds, emailType, subject, message } = await request.json(); + + if (action === 'sendReminderEmail') { + // Enviar email individual + if (!userId) { + return NextResponse.json({ error: 'userId é obrigatório' }, { status: 400 }); + } + + await prisma.$connect(); + + const user = await prisma.user.findUnique({ + where: { id: userId } + }); + + if (!user) { + return NextResponse.json({ error: 'Usuário não encontrado' }, { status: 404 }); + } + + // Aqui você implementaria o envio real do email + // Exemplo com serviço de email (Resend, SendGrid, etc.) + console.log(`📧 Enviando email de lembrete para: ${user.email}`); + + // Simular envio de email + const emailSent = true; // Substituir por lógica real de envio + + await prisma.$disconnect(); + + return NextResponse.json({ + success: true, + message: 'Email de lembrete enviado com sucesso', + emailSent, + recipient: user.email + }); + } + + if (action === 'sendBulkEmails') { + // Enviar emails em lote + if (!userIds || !Array.isArray(userIds)) { + return NextResponse.json({ error: 'userIds é obrigatório' }, { status: 400 }); + } + + await prisma.$connect(); + + const users = await prisma.user.findMany({ + where: { id: { in: userIds } } + }); + + console.log(`📧 Enviando emails em lote para ${users.length} usuários`); + + // Simular envio em lote + const emailResults = users.map(user => ({ + userId: user.id, + email: user.email, + sent: true // Substituir por lógica real + })); + + await prisma.$disconnect(); + + return NextResponse.json({ + success: true, + message: `${emailResults.length} emails enviados com sucesso`, + results: emailResults + }); + } + + return NextResponse.json({ error: 'Ação não reconhecida' }, { status: 400 }); + + } catch (error: any) { + console.error('⌠Erro ao processar emails:', error); + await prisma.$disconnect(); + + return NextResponse.json( + { error: 'Erro interno do servidor' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/admin/users/[id]/resend-instructions/route.ts b/src/app/api/admin/users/[id]/resend-instructions/route.ts new file mode 100644 index 000000000..aeaee2e11 --- /dev/null +++ b/src/app/api/admin/users/[id]/resend-instructions/route.ts @@ -0,0 +1,83 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { auth } from '@/lib/auth'; +import { gerarSenhaSegura, hashPassword } from '@/lib/auth/password'; +import { enviarEmailCredenciais } from '@/lib/auth/email'; +import { prisma } from '@/lib/prisma'; + +async function verifyAuth() { + try { + const session = await auth(); + + if (session?.user?.plan === 'ADMIN') { + return session.user; + } + + return null; + } catch (error) { + console.error('⌠Erro na verificação:', error); + return null; + } +} + +export async function POST(_request: NextRequest, { params }: { params: { id: string } }) { + try { + const admin = await verifyAuth(); + + if (!admin) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const userId = params.id; + + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user) { + return NextResponse.json({ error: 'Usuário não encontrado' }, { status: 404 }); + } + + const tempPassword = gerarSenhaSegura(); + const hashedPassword = await hashPassword(tempPassword); + + await prisma.user.update({ + where: { id: userId }, + data: { + password: hashedPassword, + mustChangePassword: true, + }, + }); + + try { + await enviarEmailCredenciais( + user.email, + user.firstName || 'usuário', + tempPassword, + user.plan || undefined + ); + } catch (emailError: any) { + console.error('⌠Erro ao enviar email:', emailError); + return NextResponse.json( + { + success: false, + error: 'Erro ao enviar email com instruções', + details: emailError?.message ?? null, + }, + { status: 500 }, + ); + } + + return NextResponse.json({ + success: true, + message: 'Instruções de acesso reenviadas com sucesso. Uma nova senha temporária foi enviada ao usuário.', + }); + } catch (error) { + console.error('⌠Erro ao reenviar instruções:', error); + return NextResponse.json( + { + error: 'Erro interno do servidor', + }, + { status: 500 }, + ); + } +} diff --git a/src/app/api/admin/users/[id]/route.ts b/src/app/api/admin/users/[id]/route.ts new file mode 100644 index 000000000..9062d345f --- /dev/null +++ b/src/app/api/admin/users/[id]/route.ts @@ -0,0 +1,270 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; +import { getActiveEntitlementCodes } from '@/lib/entitlements'; +import { isLifetimePlan } from '@/lib/lifetime-plans'; +import { normalizeCustomPermissions, serializeCustomPermissions } from '@/lib/custom-permissions'; + +async function verifyAuth() { + try { + const session = await auth(); + + if (session?.user?.plan === 'ADMIN') { + return session.user; + } + + return null; + } catch (error) { + console.error('⌠Erro na verificação:', error); + return null; + } +} + +// GET - Buscar dados do usuário + compras +export async function GET(request: NextRequest, { params }: { params: { id: string } }) { + try { + const admin = await verifyAuth(); + if (!admin) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const userId = params.id; + console.log('🔠Buscando usuário com ID:', userId); + + const user = await prisma.user.findUnique({ + where: { id: userId }, + include: { + entitlements: true, + }, + }); + + if (!user) { + console.log('⌠Usuário não encontrado:', userId); + return NextResponse.json({ error: 'Usuário não encontrado' }, { status: 404 }); + } + + console.log('✅ Usuário encontrado:', user.email); + + // Buscar também as compras do usuário + const purchases = await prisma.purchase.findMany({ + where: { userId: userId }, + orderBy: { createdAt: 'desc' } + }); + + // Buscar dados das integrações (Hotmart/Kiwify/Eduzz) para telefone + let hotmartData: any = null; + let kiwifyData: any = null; + let eduzzData: any = null; + + try { + hotmartData = await (prisma as any).hotmartData?.findUnique?.({ where: { userId } }); + } catch (error: any) { + console.warn('âš ï¸ Erro ao buscar dados Hotmart para telefone:', error.message); + } + + try { + kiwifyData = await (prisma as any).kiwifyData?.findUnique?.({ where: { userId } }); + } catch (error: any) { + console.warn('âš ï¸ Erro ao buscar dados Kiwify para telefone:', error.message); + } + + try { + eduzzData = await (prisma as any).eduzzData?.findUnique?.({ where: { userId } }); + } catch (error: any) { + console.warn('âš ï¸ Erro ao buscar dados Eduzz para telefone:', error.message); + } + + const formattedPurchases = purchases.map(purchase => ({ + id: purchase.id, + amount: purchase.amount, + date: purchase.createdAt.toISOString(), + product: purchase.productName || 'Produto não especificado', + status: purchase.status, + hotmartTransactionId: purchase.hotmartTransactionId + })); + + // Formatar os dados do usuário conforme esperado pelo frontend + const activeEntitlements = getActiveEntitlementCodes(user.entitlements ?? []); + + const formattedUser = { + id: user.id, + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + avatar: user.avatar || undefined, + plan: user.plan, + status: user.status, + hotmartCustomerId: user.hotmartCustomerId || undefined, + phone: user.phone || hotmartData?.phone || kiwifyData?.phone || eduzzData?.phone, + hotmartPhone: hotmartData?.phone, + kiwifyPhone: kiwifyData?.phone, + eduzzPhone: eduzzData?.phone, + createdAt: user.createdAt.toISOString(), + lastLogin: user.lastLogin?.toISOString(), + totalPurchases: user.totalPurchases || 0, + purchaseCount: user.purchaseCount || 0, + expirationDate: user.expirationDate?.toISOString(), + customPermissions: normalizeCustomPermissions(user.customPermissions), + origin: user.origin, + entitlements: user.entitlements.map(entitlement => ({ + id: entitlement.id, + code: entitlement.code, + status: entitlement.status, + expiresAt: entitlement.expiresAt?.toISOString(), + origin: entitlement.origin, + createdAt: entitlement.createdAt.toISOString(), + updatedAt: entitlement.updatedAt.toISOString(), + })), + activeEntitlements, + }; + + console.log(`✅ Encontradas ${purchases.length} compras para o usuário`); + + return NextResponse.json({ + user: formattedUser, + purchases: formattedPurchases + }); + + } catch (error) { + console.error('⌠Erro ao buscar usuário:', error); + return NextResponse.json( + { error: 'Erro interno do servidor' }, + { status: 500 } + ); + } +} + +// PUT - Atualizar dados do usuário +export async function PUT(request: NextRequest, { params }: { params: { id: string } }) { + try { + const admin = await verifyAuth(); + if (!admin) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const userId = params.id; + const userData = await request.json(); + + console.log('🔄 Atualizando usuário:', userId); + + // Verificar se o usuário existe + const existingUser = await prisma.user.findUnique({ + where: { id: userId } + }); + + if (!existingUser) { + return NextResponse.json({ error: 'Usuário não encontrado' }, { status: 404 }); + } + + // Preparar dados para atualização + const updateData: any = { + firstName: userData.firstName, + lastName: userData.lastName, + email: userData.email, + plan: userData.plan, + status: userData.status, + customPermissions: serializeCustomPermissions(userData.customPermissions) + }; + + // Garantir acesso vitalício para planos específicos + if (isLifetimePlan(userData.plan)) { + updateData.expirationDate = null; + } else if (userData.expirationDate) { + updateData.expirationDate = new Date(userData.expirationDate); + } else if (userData.expirationDate === null || userData.expirationDate === '') { + updateData.expirationDate = null; + } + + const updatedUser = await prisma.user.update({ + where: { id: userId }, + data: updateData, + include: { + entitlements: true, + }, + }); + + console.log('✅ Usuário atualizado:', updatedUser.email); + + // Formatar resposta + const activeEntitlements = getActiveEntitlementCodes(updatedUser.entitlements ?? []); + + const formattedUser = { + id: updatedUser.id, + firstName: updatedUser.firstName, + lastName: updatedUser.lastName, + email: updatedUser.email, + avatar: updatedUser.avatar || undefined, + plan: updatedUser.plan, + status: updatedUser.status, + hotmartCustomerId: updatedUser.hotmartCustomerId || undefined, + createdAt: updatedUser.createdAt.toISOString(), + lastLogin: updatedUser.lastLogin?.toISOString(), + totalPurchases: updatedUser.totalPurchases || 0, + purchaseCount: updatedUser.purchaseCount || 0, + expirationDate: updatedUser.expirationDate?.toISOString(), + customPermissions: normalizeCustomPermissions(updatedUser.customPermissions), + origin: updatedUser.origin, + entitlements: updatedUser.entitlements.map(entitlement => ({ + id: entitlement.id, + code: entitlement.code, + status: entitlement.status, + expiresAt: entitlement.expiresAt?.toISOString(), + origin: entitlement.origin, + createdAt: entitlement.createdAt.toISOString(), + updatedAt: entitlement.updatedAt.toISOString(), + })), + activeEntitlements, + }; + + return NextResponse.json({ user: formattedUser }); + + } catch (error) { + console.error('⌠Erro ao atualizar usuário:', error); + return NextResponse.json( + { error: 'Erro interno do servidor' }, + { status: 500 } + ); + } +} + +// DELETE - Excluir usuário +export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) { + try { + const admin = await verifyAuth(); + if (!admin) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const userId = params.id; + console.log('ðŸ—‘ï¸ Excluindo usuário:', userId); + + // Verificar se o usuário existe + const existingUser = await prisma.user.findUnique({ + where: { id: userId } + }); + + if (!existingUser) { + return NextResponse.json({ error: 'Usuário não encontrado' }, { status: 404 }); + } + + // Excluir relacionamentos primeiro (se necessário) + await prisma.purchase.deleteMany({ where: { userId } }); + await prisma.userEntitlement.deleteMany({ where: { userId } }); + + // Excluir usuário + await prisma.user.delete({ + where: { id: userId } + }); + + console.log('✅ Usuário excluído:', existingUser.email); + + return NextResponse.json({ message: 'Usuário excluído com sucesso' }); + + } catch (error) { + console.error('⌠Erro ao excluir usuário:', error); + return NextResponse.json( + { error: 'Erro interno do servidor' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts new file mode 100644 index 000000000..9d01e79bf --- /dev/null +++ b/src/app/api/admin/users/route.ts @@ -0,0 +1,595 @@ +export const dynamic = 'force-dynamic'; + +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { enviarEmailCredenciais } from '@/lib/auth/email'; +import { hashPassword, gerarSenhaSegura } from '@/lib/auth/password'; // ✅ ADICIONADO +import { auth } from '@/lib/auth'; +import { isLifetimePlan } from '@/lib/lifetime-plans'; +import { normalizeCustomPermissions, serializeCustomPermissions } from '@/lib/custom-permissions'; + +// Função de autenticação +async function verifyAuth() { + try { + const session = await auth(); + if (session?.user?.plan === 'ADMIN') { + return session.user; + } + return null; + } catch (error) { + console.error('⌠Erro na verificação:', error); + return null; + } +} + +export async function GET(request: NextRequest) { + console.log('🚀 GET /api/admin/users - COM INTEGRAÇÃO HOTMART'); + + try { + const { searchParams } = new URL(request.url); + const headers = { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache, no-store, must-revalidate' + }; + + // Verificar autenticação + const admin = await verifyAuth(); + if (!admin) { + return NextResponse.json( + { + error: 'Acesso negado. Você precisa ser administrador.', + code: 'UNAUTHORIZED' + }, + { status: 401, headers } + ); + } + + console.log('✅ Admin autorizado, conectando ao banco...'); + + // Testar conexão básica + try { + await prisma.$connect(); + console.log('✅ Conectado ao banco'); + + } catch (connectionError: any) { + console.error('⌠Erro de conexão:', connectionError); + return NextResponse.json( + { + error: 'Erro de conexão com o banco', + code: 'DATABASE_CONNECTION_ERROR', + details: connectionError.message + }, + { status: 503, headers } + ); + } + + console.log('🔄 Buscando usuários com dados do Hotmart...'); + + // Estratégia: tentar diferentes abordagens até uma funcionar + let users; + let page = 1; + let limit = 50; + let totalUsers = 0; + const hotmartDataMap = new Map(); + const kiwifyDataMap = new Map(); + const eduzzDataMap = new Map(); + const purchasesMap = new Map(); + + try { + totalUsers = await prisma.user.count(); + const limitParam = searchParams.get('limit'); + page = Math.max(1, Number(searchParams.get('page') ?? 1)); + const rawLimit = limitParam ? Number(limitParam) : undefined; + limit = limitParam + ? Math.min(100, Math.max(10, Number.isFinite(rawLimit) ? rawLimit : 50)) + : Math.max(1, totalUsers); + const offset = (page - 1) * limit; + + // 1. Primeiro, buscar usuários básicos + console.log('📊 Buscando usuários...'); + users = await prisma.user.findMany({ + select: { + id: true, + firstName: true, + lastName: true, + email: true, + avatar: true, + plan: true, + status: true, + origin: true, + createdAt: true, + lastLogin: true, + hotmartCustomerId: true, + phone: true + }, + orderBy: { createdAt: 'desc' }, + skip: offset, + take: limit + }); + console.log(`✅ Encontrados ${users.length} usuários`); + + // 2. Tentar buscar dados do Hotmart separadamente + console.log('🔄 Buscando dados do Hotmart...'); + try { + const hotmartData = await prisma.hotmartData.findMany({ + select: { + userId: true, + phone: true, + status: true, + productId: true, + productName: true, + purchaseDate: true, + expirationDate: true + }, + where: { + userId: { in: users.map(user => user.id) } + } + }); + console.log(`✅ Encontrados ${hotmartData.length} registros Hotmart`); + + // Criar mapa para associar dados do Hotmart aos usuários + hotmartData.forEach(data => { + hotmartDataMap.set(data.userId, data); + }); + + } catch (hotmartError) { + console.warn('âš ï¸ Erro ao buscar HotmartData (pode não existir ainda):', hotmartError.message); + } + + // 2b. Buscar dados da Kiwify + try { + const kiwifyData = await (prisma as any).kiwifyData?.findMany?.({ + select: { userId: true, phone: true }, + where: { + userId: { in: users.map(user => user.id) } + } + }); + if (Array.isArray(kiwifyData)) { + console.log(`✅ Encontrados ${kiwifyData.length} registros Kiwify`); + kiwifyData.forEach((data: any) => { + if (data?.userId) { + kiwifyDataMap.set(data.userId, data); + } + }); + } + } catch (kiwifyError: any) { + console.warn('âš ï¸ Erro ao buscar dados da Kiwify (pode não existir ainda):', kiwifyError.message); + } + + // 2c. Buscar dados da Eduzz + try { + const eduzzData = await (prisma as any).eduzzData?.findMany?.({ + select: { userId: true, phone: true }, + where: { + userId: { in: users.map(user => user.id) } + } + }); + if (Array.isArray(eduzzData)) { + console.log(`✅ Encontrados ${eduzzData.length} registros Eduzz`); + eduzzData.forEach((data: any) => { + if (data?.userId) { + eduzzDataMap.set(data.userId, data); + } + }); + } + } catch (eduzzError: any) { + console.warn('âš ï¸ Erro ao buscar dados da Eduzz (pode não existir ainda):', eduzzError.message); + } + + // 3. Tentar buscar purchases separadamente + console.log('🔄 Buscando purchases...'); + try { + const purchases = await prisma.purchase.findMany({ + select: { userId: true, status: true, amount: true }, + where: { + userId: { in: users.map(user => user.id) } + } + }); + console.log(`✅ Encontradas ${purchases.length} purchases`); + + // Agrupar purchases por usuário + purchases.forEach(purchase => { + if (!purchasesMap.has(purchase.userId)) { + purchasesMap.set(purchase.userId, []); + } + purchasesMap.get(purchase.userId).push(purchase); + }); + + } catch (purchaseError) { + console.warn('âš ï¸ Erro ao buscar Purchases (pode não existir ainda):', purchaseError.message); + } + + } catch (queryError: any) { + console.error('⌠Erro na query de usuários:', queryError); + return NextResponse.json( + { + error: 'Erro ao buscar usuários no banco', + code: 'QUERY_ERROR', + details: queryError.message + }, + { status: 500, headers } + ); + } + + // Formatar dados incluindo informações do Hotmart + console.log('🔄 Formatando dados com integração Hotmart...'); + const formattedUsers = users.map((user, index) => { + try { + console.log(`Formatando usuário ${index + 1}:`, user.email); + + // Pegar dados do Hotmart para este usuário + const hotmartInfo = hotmartDataMap.get(user.id); + const kiwifyInfo = kiwifyDataMap.get(user.id); + const eduzzInfo = eduzzDataMap.get(user.id); + + // Calcular total de compras + const userPurchases = purchasesMap?.get(user.id) || []; + const totalPurchases = userPurchases + .filter(p => p.status === 'COMPLETED') + .reduce((sum, p) => sum + (p.amount || 0), 0); + + return { + id: user.id, + firstName: user.firstName || '', + lastName: user.lastName || '', + email: user.email, + avatar: user.avatar || null, + plan: user.plan, + status: user.status, + origin: user.origin, + + // 📞 Contatos das plataformas + phone: user.phone || hotmartInfo?.phone || kiwifyInfo?.phone || eduzzInfo?.phone || null, + hotmartPhone: hotmartInfo?.phone || null, + kiwifyPhone: kiwifyInfo?.phone || null, + eduzzPhone: eduzzInfo?.phone || null, + + // 🔥 DADOS DO HOTMART (essenciais para liberação de membros) + hotmartCustomerId: hotmartInfo?.hotmartCustomerId || user.hotmartCustomerId || null, + + createdAt: user.createdAt?.toISOString() ?? null, + lastLogin: user.lastLogin?.toISOString() || null, + totalPurchases, + purchaseCount: userPurchases.length, + + // 🔥 DATA DE EXPIRAÇÃO (crítica para controle de acesso) + expirationDate: hotmartInfo?.expirationDate?.toISOString() || null, + + // 🔥 STATUS DO HOTMART (importante para validação) + hotmartStatus: hotmartInfo?.status || null, + hotmartProductId: hotmartInfo?.productId || null, + hotmartProductName: hotmartInfo?.productName || null, + hotmartPurchaseDate: hotmartInfo?.purchaseDate?.toISOString() || null + }; + } catch (formatError) { + console.error(`⌠Erro ao formatar usuário ${user.email}:`, formatError); + // Retornar dados básicos em caso de erro + return { + id: user.id, + firstName: user.firstName || 'N/A', + lastName: user.lastName || 'N/A', + email: user.email, + avatar: null, + plan: user.plan, + status: user.status, + origin: user.origin, + hotmartCustomerId: user.hotmartCustomerId || null, + createdAt: user.createdAt?.toISOString() ?? null, + lastLogin: null, + totalPurchases: 0, + purchaseCount: 0, + expirationDate: null, + hotmartStatus: null, + hotmartProductId: null, + hotmartProductName: null, + hotmartPurchaseDate: null + }; + } + }); + + // Estatísticas com dados do Hotmart + const stats = { + totalUsers, + activeUsers: users.filter(u => u.status === 'ACTIVE').length, + expiredUsers: formattedUsers.filter(u => { + if (!u.expirationDate) return false; + return new Date(u.expirationDate) < new Date(); + }).length, + hotmartIntegrated: formattedUsers.filter(u => u.hotmartCustomerId).length + }; + + const response = { + success: true, + users: formattedUsers, + total: totalUsers, + pagination: { + page, + limit, + totalPages: Math.ceil(totalUsers / limit) + }, + timestamp: new Date().toISOString(), + admin: { + id: admin.id, + email: admin.email + }, + source: 'database_with_hotmart', + stats, + hotmartIntegration: { + enabled: true, + usersWithHotmart: stats.hotmartIntegrated, + message: 'Integração Hotmart ativa para controle de membros' + } + }; + + console.log(`✅ SUCESSO! Retornando ${formattedUsers.length} usuários com dados Hotmart`); + console.log(`📊 Stats: ${stats.hotmartIntegrated} usuários com integração Hotmart`); + + return NextResponse.json(response, { + status: 200, + headers + }); + + } catch (error: any) { + console.error('⌠Erro inesperado geral:', error); + + return NextResponse.json( + { + error: 'Erro interno do servidor', + code: 'INTERNAL_ERROR', + details: process.env.NODE_ENV === 'development' ? { + message: error.message, + stack: error.stack + } : undefined + }, + { + status: 500, + headers: { + 'Content-Type': 'application/json' + } + } + ); + } finally { + try { + await prisma.$disconnect(); + console.log('🔌 Desconectado do banco'); + } catch (disconnectError) { + console.error('⌠Erro ao desconectar:', disconnectError); + } + } +} + +export async function POST(request: NextRequest) { + console.log('🚀 POST /api/admin/users - CRIANDO COM HASH SEGURO'); + + try { + const headers = { + 'Content-Type': 'application/json' + }; + + // Verificar autenticação + const admin = await verifyAuth(); + if (!admin) { + return NextResponse.json( + { + error: 'Acesso negado para criação de usuários', + code: 'UNAUTHORIZED' + }, + { status: 403, headers } + ); + } + + // Parse do JSON + let requestData; + try { + requestData = await request.json(); + } catch (parseError) { + return NextResponse.json( + { + error: 'Dados JSON inválidos', + code: 'INVALID_JSON' + }, + { status: 400, headers } + ); + } + + const { + firstName, + lastName, + email, + plan, + expirationDate, + hotmartCustomerId, + customPermissions: customPermissionsInput + } = requestData; + console.log('📠Criando usuário com senha segura:', { firstName, lastName, email, plan }); + + // Validações + if (!firstName || !lastName || !email || !plan) { + return NextResponse.json({ + error: 'Campos obrigatórios: firstName, lastName, email, plan', + code: 'MISSING_REQUIRED_FIELDS' + }, { status: 400, headers }); + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return NextResponse.json({ + error: 'Email inválido', + code: 'INVALID_EMAIL' + }, { status: 400, headers }); + } + + const normalizedCustomPermissions = normalizeCustomPermissions(customPermissionsInput); + const effectiveCustomPermissions = normalizedCustomPermissions; + + // Conectar ao banco + await prisma.$connect(); + console.log('✅ Conectado ao banco para criação'); + + // Verificar se usuário já existe + const existingUser = await prisma.user.findUnique({ + where: { email: email.toLowerCase() } + }); + + if (existingUser) { + return NextResponse.json({ + error: 'Usuário já existe com este email', + code: 'USER_ALREADY_EXISTS' + }, { status: 400, headers }); + } + + // Normalizar data de expiração considerando planos vitalícios + let normalizedExpirationDate: Date | null = null; + if (expirationDate) { + const parsed = new Date(expirationDate); + if (isNaN(parsed.getTime())) { + console.warn(`âš ï¸ Data de expiração inválida recebida (${expirationDate}). Ignorando.`); + } else { + normalizedExpirationDate = parsed; + } + } + + if (isLifetimePlan(plan)) { + if (normalizedExpirationDate) { + console.log(`âš ï¸ Plano ${plan} é vitalício. Ignorando data personalizada de expiração.`); + } + normalizedExpirationDate = null; + } + + // ✅ GERAR SENHA SEGURA + const tempPassword = gerarSenhaSegura(); + console.log(`🔑 Senha segura gerada: ${tempPassword}`); + + // ✅ HASH DA SENHA + const hashedPassword = await hashPassword(tempPassword); + console.log('🔒 Senha hasheada com sucesso'); + + // Criar usuário no banco + const novoUsuario = await prisma.user.create({ + data: { + email: email.toLowerCase(), + firstName, + lastName, + password: hashedPassword, // ✅ USANDO HASH SEGURO + plan: plan as any, + status: 'ACTIVE', + origin: 'MANUAL', + mustChangePassword: true, // ✅ FORÇA MUDANÇA NO PRIMEIRO ACESSO + hotmartCustomerId: hotmartCustomerId || null, + expirationDate: normalizedExpirationDate, + customPermissions: serializeCustomPermissions(effectiveCustomPermissions) + } + }); + + // Se há dados do Hotmart, criar registro separado + if (normalizedExpirationDate || hotmartCustomerId) { + try { + console.log('🔄 Criando dados do Hotmart...'); + await prisma.hotmartData.create({ + data: { + userId: novoUsuario.id, + hotmartCustomerId: hotmartCustomerId || `MANUAL_${Date.now()}`, + productId: 'MANUAL_CREATION', + productName: `Plano ${plan}`, + purchaseDate: new Date(), + expirationDate: normalizedExpirationDate, + status: 'ACTIVE' + } + }); + console.log('✅ Dados do Hotmart criados'); + } catch (hotmartError) { + console.warn('âš ï¸ Erro ao criar dados Hotmart (usuário já foi criado):', hotmartError); + } + } + + console.log('✅ Usuário criado com senha segura:', novoUsuario.email); + + // 📧 ENVIAR EMAIL COM CREDENCIAIS + let emailSent = false; + let emailError = null; + + try { + console.log(`📧 Enviando email de credenciais para ${email}...`); + await enviarEmailCredenciais(email, firstName, tempPassword, plan); + emailSent = true; + console.log('✅ Email enviado com sucesso!'); + } catch (error: any) { + emailError = error.message; + console.error('⌠Erro ao enviar email:', error); + // Não falha a criação do usuário por causa do email + } + + return NextResponse.json({ + success: true, + message: emailSent + ? 'Usuário criado com sucesso! Email com credenciais enviado.' + : 'Usuário criado com senha segura, mas houve erro no envio do email.', + user: { + id: novoUsuario.id, + email: novoUsuario.email, + firstName: novoUsuario.firstName, + lastName: novoUsuario.lastName, + plan: novoUsuario.plan, + status: novoUsuario.status, + hotmartCustomerId: novoUsuario.hotmartCustomerId, + origin: novoUsuario.origin, + customPermissions: effectiveCustomPermissions + }, + email: { + sent: emailSent, + error: emailError + }, + // ✅ Só retorna a senha se o email falhou (para segurança) + tempPassword: emailSent ? undefined : tempPassword, + security: { + passwordHashed: true, + passwordSecure: true, + message: 'Senha gerada e armazenada com hash seguro' + }, + hotmartIntegration: { + enabled: true, + message: 'Usuário preparado para validação via Hotmart' + } + }, { + status: 201, + headers + }); + + } catch (error: any) { + console.error('⌠Erro ao criar usuário:', error); + + let errorMessage = 'Erro interno ao criar usuário'; + let errorCode = 'CREATION_ERROR'; + + if (error.code === 'P2002') { + errorMessage = 'Email já está em uso'; + errorCode = 'EMAIL_ALREADY_EXISTS'; + } + + return NextResponse.json({ + error: errorMessage, + code: errorCode, + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }, { + status: 500, + headers + }); + } finally { + try { + await prisma.$disconnect(); + } catch (disconnectError) { + console.error('⌠Erro ao desconectar:', disconnectError); + } + } +} + +export async function OPTIONS(request: NextRequest) { + return new NextResponse(null, { + status: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-User-Email', + }, + }); +} diff --git a/src/app/api/admin/users/test-page.tsx b/src/app/api/admin/users/test-page.tsx new file mode 100644 index 000000000..a97622b89 --- /dev/null +++ b/src/app/api/admin/users/test-page.tsx @@ -0,0 +1,26 @@ +'use client'; + +import * as React from 'react'; +import { Box, Typography } from '@mui/material'; +import { useUser } from '@/hooks/use-user'; + +export default function TestAdminPage() { + const { user } = useUser(); + + return ( + + + ðŸ›¡ï¸ Teste Admin + + + Usuário: {user?.firstName} {user?.lastName} + + + Email: {user?.email} + + + Plano: {(user as any)?.plan} + + + ); +} \ No newline at end of file diff --git a/src/app/api/agenda/estatisticas/route.ts b/src/app/api/agenda/estatisticas/route.ts new file mode 100644 index 000000000..77ec4f2b6 --- /dev/null +++ b/src/app/api/agenda/estatisticas/route.ts @@ -0,0 +1,39 @@ +import { NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +export async function GET() { + try { + const totalEventos = await prisma.eventoCorporativo.count(); + + const limit = 500; + const eventos = await prisma.eventoCorporativo.findMany({ + take: limit, + orderBy: { data_evento: 'desc' } + }); + + const tickersUnicos = await prisma.eventoCorporativo.groupBy({ + by: ['ticker'], + _count: { ticker: true } + }); + + const ultimoUpload = await prisma.eventoCorporativo.findFirst({ + orderBy: { createdAt: 'desc' }, + select: { createdAt: true } + }); + + return NextResponse.json({ + totalEventos, + totalTickers: tickersUnicos.length, + dataUltimoUpload: ultimoUpload?.createdAt || null, + limit, + eventos + }); + + } catch (error) { + console.error('Erro ao carregar estatísticas:', error); + return NextResponse.json( + { error: 'Erro interno do servidor' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/agenda/excluir-evento/route.ts b/src/app/api/agenda/excluir-evento/route.ts new file mode 100644 index 000000000..57beddbe4 --- /dev/null +++ b/src/app/api/agenda/excluir-evento/route.ts @@ -0,0 +1,36 @@ +export async function DELETE(request: Request) { + try { + const { eventoIndex } = await request.json(); + + // Buscar todos eventos ordenados para encontrar o correto pelo índice + const eventos = await prisma.eventoCorporativo.findMany({ + take: 1000, + orderBy: { createdAt: 'desc' } + }); + + if (eventoIndex < 0 || eventoIndex >= eventos.length) { + return NextResponse.json( + { error: 'Ãndice de evento inválido' }, + { status: 400 } + ); + } + + const eventoParaExcluir = eventos[eventoIndex]; + + await prisma.eventoCorporativo.delete({ + where: { id: eventoParaExcluir.id } + }); + + return NextResponse.json({ + success: true, + eventoExcluido: eventoParaExcluir.titulo + }); + + } catch (error) { + console.error('Erro ao excluir evento:', error); + return NextResponse.json( + { error: 'Erro ao excluir evento' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/agenda/excluir-eventos/route.ts b/src/app/api/agenda/excluir-eventos/route.ts new file mode 100644 index 000000000..461b1638e --- /dev/null +++ b/src/app/api/agenda/excluir-eventos/route.ts @@ -0,0 +1,45 @@ +// ========================================== +// ðŸ—‘ï¸ DELETE /api/agenda/excluir-eventos +// ========================================== + +export async function DELETE(request: Request) { + try { + const { indices } = await request.json(); + + if (!Array.isArray(indices) || indices.length === 0) { + return NextResponse.json( + { error: 'Ãndices inválidos' }, + { status: 400 } + ); + } + + // Buscar eventos ordenados + const eventos = await prisma.eventoCorporativo.findMany({ + take: 1000, + orderBy: { createdAt: 'desc' } + }); + + // Obter IDs dos eventos a serem excluídos + const idsParaExcluir = indices + .filter(index => index >= 0 && index < eventos.length) + .map(index => eventos[index].id); + + const resultado = await prisma.eventoCorporativo.deleteMany({ + where: { + id: { in: idsParaExcluir } + } + }); + + return NextResponse.json({ + success: true, + eventosExcluidos: resultado.count + }); + + } catch (error) { + console.error('Erro ao excluir eventos:', error); + return NextResponse.json( + { error: 'Erro ao excluir eventos' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/agenda/excluir-por-ticker/route.ts b/src/app/api/agenda/excluir-por-ticker/route.ts new file mode 100644 index 000000000..f33d73807 --- /dev/null +++ b/src/app/api/agenda/excluir-por-ticker/route.ts @@ -0,0 +1,35 @@ +// ========================================== +// ðŸ—‘ï¸ DELETE /api/agenda/excluir-por-ticker +// ========================================== + +export async function DELETE(request: Request) { + try { + const { ticker } = await request.json(); + + if (!ticker) { + return NextResponse.json( + { error: 'Ticker não informado' }, + { status: 400 } + ); + } + + const resultado = await prisma.eventoCorporativo.deleteMany({ + where: { + ticker: ticker.toUpperCase() + } + }); + + return NextResponse.json({ + success: true, + eventosExcluidos: resultado.count, + ticker + }); + + } catch (error) { + console.error('Erro ao excluir eventos do ticker:', error); + return NextResponse.json( + { error: 'Erro ao excluir eventos do ticker' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/agenda/exportar/route.ts b/src/app/api/agenda/exportar/route.ts new file mode 100644 index 000000000..d7524bd17 --- /dev/null +++ b/src/app/api/agenda/exportar/route.ts @@ -0,0 +1,61 @@ +// ========================================== +// 📥 GET /api/agenda/exportar +// ========================================== + +// ✅ Adicionar estas importações no topo do arquivo +import { NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +// ✅ Inicializar o Prisma + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const rawLimit = Number.parseInt(searchParams.get('limit') || '1000', 10); + const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? rawLimit : 1000; + + const eventos = await prisma.eventoCorporativo.findMany({ + take: limit, + orderBy: [ + { ticker: 'asc' }, + { data_evento: 'desc' } + ] + }); + const total = await prisma.eventoCorporativo.count(); + + // Gerar CSV + const header = 'ticker,tipo_evento,titulo,data_evento,descricao,status,prioridade,url_externo,observacoes\n'; + + const csvContent = eventos + .map(evento => [ + evento.ticker, + evento.tipo_evento, + `"${evento.titulo.replace(/"/g, '""')}"`, // Escapar aspas + evento.data_evento.toISOString().split('T')[0], + `"${evento.descricao.replace(/"/g, '""')}"`, + evento.status, + evento.prioridade || '', + evento.url_externo || '', + evento.observacoes ? `"${evento.observacoes.replace(/"/g, '""')}"` : '' + ].join(',')) + .join('\n'); + + const csvData = header + csvContent; + + return new Response(csvData, { + headers: { + 'Content-Type': 'text/csv; charset=utf-8', + 'Content-Disposition': `attachment; filename="agenda_corporativa_${new Date().toISOString().split('T')[0]}.csv"`, + 'X-Total-Count': total.toString(), + 'X-Limit': limit.toString() + } + }); + + } catch (error) { + console.error('Erro ao exportar dados:', error); + return NextResponse.json( + { error: 'Erro ao exportar dados' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/agenda/limpar-todos/route.ts b/src/app/api/agenda/limpar-todos/route.ts new file mode 100644 index 000000000..49e966219 --- /dev/null +++ b/src/app/api/agenda/limpar-todos/route.ts @@ -0,0 +1,64 @@ +// src/app/api/agenda/limpar-todos/route.ts +import { NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +export async function DELETE() { + console.log('ðŸ—‘ï¸ [LIMPAR-TODOS] Iniciando...'); + + try { + // Testar conexão com banco + console.log('📡 [LIMPAR-TODOS] Testando conexão com banco...'); + + // Verificar quantos eventos existem + const totalAntes = await prisma.eventoCorporativo.count(); + console.log(`📊 [LIMPAR-TODOS] Eventos encontrados: ${totalAntes}`); + + if (totalAntes === 0) { + console.log('â„¹ï¸ [LIMPAR-TODOS] Nenhum evento para remover'); + return NextResponse.json({ + success: true, + message: 'Nenhum evento para remover', + eventosExcluidos: 0 + }); + } + + // Executar a remoção + console.log('🔥 [LIMPAR-TODOS] Iniciando deleteMany...'); + const resultado = await prisma.eventoCorporativo.deleteMany({}); + console.log(`✅ [LIMPAR-TODOS] DeleteMany executado. Count: ${resultado.count}`); + + const resposta = { + success: true, + message: `${resultado.count} eventos removidos com sucesso`, + eventosExcluidos: resultado.count, + totalAnterior: totalAntes + }; + + console.log('📋 [LIMPAR-TODOS] Resposta a ser enviada:', resposta); + + return NextResponse.json(resposta); + + } catch (error) { + console.error('⌠[LIMPAR-TODOS] ERRO CAPTURADO:'); + console.error('- Tipo:', typeof error); + console.error('- Nome:', error?.name); + console.error('- Mensagem:', error?.message); + console.error('- Stack:', error?.stack); + console.error('- Objeto completo:', error); + + const errorResponse = { + success: false, + error: 'Erro interno do servidor', + message: error?.message || 'Erro desconhecido', + details: process.env.NODE_ENV === 'development' ? { + name: error?.name, + message: error?.message, + stack: error?.stack + } : undefined + }; + + console.log('📋 [LIMPAR-TODOS] Error response:', errorResponse); + + return NextResponse.json(errorResponse, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/agenda/por-ticker/[ticker]/route.ts b/src/app/api/agenda/por-ticker/[ticker]/route.ts new file mode 100644 index 000000000..9bb9fa6e3 --- /dev/null +++ b/src/app/api/agenda/por-ticker/[ticker]/route.ts @@ -0,0 +1,40 @@ +// ========================================== +// 📋 GET /api/agenda/por-ticker/[ticker] +// ========================================== + +export async function GET( + request: Request, + { params }: { params: { ticker: string } } +) { + try { + const { ticker } = params; + + if (!ticker) { + return NextResponse.json( + { error: 'Ticker não informado' }, + { status: 400 } + ); + } + + const eventos = await prisma.eventoCorporativo.findMany({ + where: { + ticker: ticker.toUpperCase() + }, + take: 100, + orderBy: { data_evento: 'desc' } + }); + + return NextResponse.json({ + ticker: ticker.toUpperCase(), + totalEventos: eventos.length, + eventos + }); + + } catch (error) { + console.error(`Erro ao carregar eventos para ${params.ticker}:`, error); + return NextResponse.json( + { error: 'Erro ao carregar eventos' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/agenda/upload/route.ts b/src/app/api/agenda/upload/route.ts new file mode 100644 index 000000000..2e22d5932 --- /dev/null +++ b/src/app/api/agenda/upload/route.ts @@ -0,0 +1,45 @@ +import { NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +export async function POST(request: Request) { + try { + const { eventos, metadata } = await request.json(); + + if (!eventos || !Array.isArray(eventos) || eventos.length === 0) { + return NextResponse.json( + { error: 'Dados de eventos inválidos' }, + { status: 400 } + ); + } + + const eventosParaInserir = eventos.map((evento) => ({ + ticker: evento.ticker.toUpperCase(), + tipo_evento: evento.tipo_evento, + titulo: evento.titulo, + data_evento: new Date(evento.data_evento), + descricao: evento.descricao, + status: evento.status, + prioridade: evento.prioridade || null, + url_externo: evento.url_externo || null, + observacoes: evento.observacoes || null + })); + + const resultado = await prisma.eventoCorporativo.createMany({ + data: eventosParaInserir, + skipDuplicates: true + }); + + return NextResponse.json({ + success: true, + eventosInseridos: resultado.count, + metadata + }); + + } catch (error) { + console.error('Erro no upload:', error); + return NextResponse.json( + { error: 'Erro ao processar upload' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/analises-tecnicas/[id]/route.ts b/src/app/api/analises-tecnicas/[id]/route.ts new file mode 100644 index 000000000..223d695a3 --- /dev/null +++ b/src/app/api/analises-tecnicas/[id]/route.ts @@ -0,0 +1,172 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; +import { ensureSlug, slugify } from '@/utils/slugify'; + +export async function GET( + _request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const { id } = params; + + const analise = await prisma.analiseTecnica.findUnique({ + where: { + id, + }, + include: { + autor: { + select: { + id: true, + firstName: true, + lastName: true, + avatar: true, + email: true, + }, + }, + }, + }); + + if (!analise) { + return NextResponse.json({ error: 'Análise técnica não encontrada' }, { status: 404 }); + } + + return NextResponse.json({ analise }); + } catch (error) { + console.error('Erro ao buscar análise técnica:', error); + return NextResponse.json( + { + error: 'Erro interno do servidor', + details: error instanceof Error ? error.message : 'Erro desconhecido', + }, + { status: 500 } + ); + } +} + +export async function PUT( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const session = await auth(); + + if (!session || session.user.role !== 'ADMIN') { + return NextResponse.json({ error: 'Acesso não autorizado' }, { status: 401 }); + } + + const { id } = params; + const body = await request.json(); + + const { + ticker, + titulo, + resumo, + conteudo, + imagemCapa, + publicado, + destaque, + fonte, + slug: providedSlug, + } = body; + + const current = await prisma.analiseTecnica.findUnique({ where: { id } }); + + if (!current) { + return NextResponse.json({ error: 'Análise técnica não encontrada' }, { status: 404 }); + } + + let slug = current.slug; + + if (typeof providedSlug === 'string' && providedSlug.trim().length > 0) { + slug = slugify(providedSlug); + } else if (typeof titulo === 'string' && titulo.trim().length > 0) { + slug = slugify(titulo); + } + + if (!slug) { + const baseSlugSource = + current.slug && current.slug.trim().length > 0 + ? current.slug + : current.titulo || current.ticker || current.id; + slug = ensureSlug(baseSlugSource, Date.now()); + } else if (slug !== current.slug) { + const slugExists = await prisma.analiseTecnica.findUnique({ where: { slug } }); + if (slugExists && slugExists.id !== current.id) { + slug = ensureSlug(slug, Date.now()); + } + } + + const nextAutorId = + typeof current.autorId === 'string' && current.autorId.trim().length > 0 + ? current.autorId + : session.user.id ?? null; + + const analiseAtualizada = await prisma.analiseTecnica.update({ + where: { id }, + data: { + ticker: ticker ? String(ticker).toUpperCase() : current.ticker, + titulo: titulo ?? current.titulo, + resumo: resumo !== undefined ? resumo : current.resumo, + conteudo: conteudo ?? current.conteudo, + imagemCapa: imagemCapa !== undefined ? imagemCapa : current.imagemCapa, + publicado: typeof publicado === 'boolean' ? publicado : current.publicado, + destaque: typeof destaque === 'boolean' ? destaque : current.destaque, + fonte: fonte !== undefined ? fonte : current.fonte, + slug, + ...(nextAutorId ? { autorId: nextAutorId } : {}), + }, + include: { + autor: { + select: { + id: true, + firstName: true, + lastName: true, + avatar: true, + email: true, + }, + }, + }, + }); + + return NextResponse.json({ analise: analiseAtualizada }); + } catch (error) { + console.error('Erro ao atualizar análise técnica:', error); + return NextResponse.json( + { + error: 'Erro interno do servidor', + details: error instanceof Error ? error.message : 'Erro desconhecido', + }, + { status: 500 } + ); + } +} + +export async function DELETE( + _request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const session = await auth(); + + if (!session || session.user.role !== 'ADMIN') { + return NextResponse.json({ error: 'Acesso não autorizado' }, { status: 401 }); + } + + const { id } = params; + + await prisma.analiseTecnica.delete({ where: { id } }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao excluir análise técnica:', error); + return NextResponse.json( + { + error: 'Erro interno do servidor', + details: error instanceof Error ? error.message : 'Erro desconhecido', + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/analises-tecnicas/route.ts b/src/app/api/analises-tecnicas/route.ts new file mode 100644 index 000000000..44fd3fa98 --- /dev/null +++ b/src/app/api/analises-tecnicas/route.ts @@ -0,0 +1,156 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; +import { ensureSlug, slugify } from '@/utils/slugify'; + +function buildUniqueSlug(base: string): string { + const baseSlug = slugify(base); + if (!baseSlug) { + return `analise-${Date.now()}`; + } + + return ensureSlug(baseSlug, Date.now()); +} + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const ticker = searchParams.get('ticker'); + const slug = searchParams.get('slug'); + const publicado = searchParams.get('publicado'); + const destaque = searchParams.get('destaque'); + const limitParam = searchParams.get('limit'); + + const where: any = {}; + + if (ticker) { + where.ticker = ticker.toUpperCase(); + } + + if (slug) { + where.slug = slug; + } + + if (publicado !== null) { + where.publicado = publicado === 'true'; + } + + if (destaque !== null) { + where.destaque = destaque === 'true'; + } + + const take = limitParam ? Number.parseInt(limitParam, 10) || undefined : undefined; + + const analises = await prisma.analiseTecnica.findMany({ + where, + take, + include: { + autor: { + select: { + id: true, + firstName: true, + lastName: true, + avatar: true, + email: true, + }, + }, + }, + orderBy: [ + { destaque: 'desc' }, + { createdAt: 'desc' }, + ], + }); + + return NextResponse.json({ analises }); + } catch (error) { + console.error('Erro ao buscar análises técnicas:', error); + return NextResponse.json( + { + error: 'Erro interno do servidor', + details: error instanceof Error ? error.message : 'Erro desconhecido', + }, + { status: 500 } + ); + } +} + +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session || session.user.role !== 'ADMIN') { + return NextResponse.json({ error: 'Acesso não autorizado' }, { status: 401 }); + } + + const body = await request.json(); + const { + ticker, + titulo, + resumo, + conteudo, + imagemCapa, + publicado = true, + destaque = false, + fonte, + slug: providedSlug, + } = body; + + if (!ticker || !titulo || !conteudo) { + return NextResponse.json( + { error: 'Campos obrigatórios: ticker, titulo e conteudo' }, + { status: 400 } + ); + } + + const normalizedTicker = String(ticker).toUpperCase(); + + let slug = providedSlug ? slugify(String(providedSlug)) : slugify(String(titulo)); + if (!slug) { + slug = buildUniqueSlug(String(titulo) || normalizedTicker); + } else { + const exists = await prisma.analiseTecnica.findUnique({ where: { slug } }); + if (exists) { + slug = ensureSlug(slug, Date.now()); + } + } + + const analise = await prisma.analiseTecnica.create({ + data: { + ticker: normalizedTicker, + titulo, + resumo: resumo ?? null, + conteudo, + imagemCapa: imagemCapa ?? null, + publicado, + destaque, + fonte: fonte ?? null, + slug, + autorId: session.user.id, + }, + include: { + autor: { + select: { + id: true, + firstName: true, + lastName: true, + avatar: true, + email: true, + }, + }, + }, + }); + + return NextResponse.json({ analise }, { status: 201 }); + } catch (error) { + console.error('Erro ao criar análise técnica:', error); + + return NextResponse.json( + { + error: 'Erro interno do servidor', + details: error instanceof Error ? error.message : 'Erro desconhecido', + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/analises-trimestrais/[id]/route.ts b/src/app/api/analises-trimestrais/[id]/route.ts new file mode 100644 index 000000000..0f3fd3fa5 --- /dev/null +++ b/src/app/api/analises-trimestrais/[id]/route.ts @@ -0,0 +1,205 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +// PUT /api/analises-trimestrais/[id] - Atualizar análise +export async function PUT( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const { id } = params; + const body = await request.json(); + + console.log('âœï¸ Atualizando análise:', id); + + // Verificar se a análise existe + const analiseExistente = await prisma.analiseTrimestreData.findUnique({ + where: { id } + }); + + if (!analiseExistente) { + return NextResponse.json( + { error: 'Análise não encontrada' }, + { status: 404 } + ); + } + + // Garantir que dataPublicacao seja uma data válida + const dataPublicacao = body.dataPublicacao ? new Date(body.dataPublicacao) : analiseExistente.dataPublicacao; + + const otherMetrics = body.other_metrics ?? body.otherMetrics ?? body.metricas?.other_metrics ?? body.metricas?.otherMetrics; + let metricasUpdate: any; + + if (body.metricas !== undefined) { + if (body.metricas && typeof body.metricas === 'object') { + metricasUpdate = { ...body.metricas }; + if (otherMetrics !== undefined) { + metricasUpdate.other_metrics = otherMetrics; + } + } else { + metricasUpdate = body.metricas; + } + } else if (otherMetrics !== undefined) { + const metricasExistentes = analiseExistente.metricas && typeof analiseExistente.metricas === 'object' + ? { ...(analiseExistente.metricas as Record) } + : {}; + metricasExistentes.other_metrics = otherMetrics; + metricasUpdate = metricasExistentes; + } + + // Preparar dados para atualização + const updateData = { + ...(body.ticker && { ticker: body.ticker.toUpperCase() }), + ...(body.empresa && { empresa: body.empresa }), + ...(body.trimestre !== undefined && { trimestre: body.trimestre }), + dataPublicacao, + ...(body.autor !== undefined && { autor: body.autor }), + ...(body.categoria && { categoria: body.categoria }), + ...(body.titulo && { titulo: body.titulo }), + ...(body.resumoExecutivo !== undefined && { resumoExecutivo: body.resumoExecutivo }), + ...(body.analiseCompleta !== undefined && { analiseCompleta: body.analiseCompleta }), + ...(metricasUpdate !== undefined && { metricas: metricasUpdate }), + ...(body.pontosFavoraveis !== undefined && { pontosFavoraveis: body.pontosFavoraveis }), + ...(body.pontosAtencao !== undefined && { pontosAtencao: body.pontosAtencao }), + ...(body.recomendacao && { recomendacao: body.recomendacao }), + ...(body.precoAlvo !== undefined && { precoAlvo: body.precoAlvo }), + ...(body.risco && { risco: body.risco }), + ...(body.nota !== undefined && { nota: body.nota }), // ↠ADICIONAR ESTA LINHA + ...(body.linkResultado !== undefined && { linkResultado: body.linkResultado }), + ...(body.linkConferencia !== undefined && { linkConferencia: body.linkConferencia }), + ...(body.status && { status: body.status }), + ...(body.userId !== undefined && { userId: body.userId }) + }; + + const analiseAtualizada = await prisma.analiseTrimestreData.update({ + where: { id }, + data: updateData, + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + avatar: true + } + } + } + }); + + console.log('✅ Análise atualizada com sucesso:', id); + + return NextResponse.json(analiseAtualizada, { status: 200 }); + + } catch (error) { + console.error('⌠Erro ao atualizar análise:', error); + + if (error instanceof Error) { + if (error.message.includes('unique constraint')) { + return NextResponse.json( + { error: 'Já existe uma análise com estes dados (ticker + trimestre + título)' }, + { status: 409 } + ); + } + } + + return NextResponse.json( + { error: 'Erro interno do servidor', details: error instanceof Error ? error.message : 'Erro desconhecido' }, + { status: 500 } + ); + } +} + +// DELETE /api/analises-trimestrais/[id] - Deletar análise +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const { id } = params; + + console.log('ðŸ—‘ï¸ Deletando análise:', id); + + // Verificar se a análise existe + const analiseExistente = await prisma.analiseTrimestreData.findUnique({ + where: { id } + }); + + if (!analiseExistente) { + return NextResponse.json( + { error: 'Análise não encontrada' }, + { status: 404 } + ); + } + + // Deletar a análise + await prisma.analiseTrimestreData.delete({ + where: { id } + }); + + console.log('✅ Análise deletada com sucesso:', id); + + return NextResponse.json( + { message: 'Análise deletada com sucesso' }, + { status: 200 } + ); + + } catch (error) { + console.error('⌠Erro ao deletar análise:', error); + + return NextResponse.json( + { error: 'Erro interno do servidor', details: error instanceof Error ? error.message : 'Erro desconhecido' }, + { status: 500 } + ); + } +} + +// GET /api/analises-trimestrais/[id] - Buscar análise específica +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const { id } = params; + + console.log('🔠Buscando análise específica:', id); + + const analise = await prisma.analiseTrimestreData.findUnique({ + where: { id }, + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + avatar: true + } + } + } + }); + + if (!analise) { + return NextResponse.json( + { error: 'Análise não encontrada' }, + { status: 404 } + ); + } + + console.log('✅ Análise encontrada:', analise.ticker); + + // Incrementar visualizações + await prisma.analiseTrimestreData.update({ + where: { id }, + data: { visualizacoes: { increment: 1 } } + }); + + return NextResponse.json(analise, { status: 200 }); + + } catch (error) { + console.error('⌠Erro ao buscar análise:', error); + + return NextResponse.json( + { error: 'Erro interno do servidor', details: error instanceof Error ? error.message : 'Erro desconhecido' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/analises-trimestrais/analisar-pdf/route.ts b/src/app/api/analises-trimestrais/analisar-pdf/route.ts new file mode 100644 index 000000000..ec2563323 --- /dev/null +++ b/src/app/api/analises-trimestrais/analisar-pdf/route.ts @@ -0,0 +1,291 @@ +import { NextRequest, NextResponse } from 'next/server'; +import OpenAI from 'openai'; +import { DadosExtraidosPDF, MetricExtractionItem } from '@/types/quarterly-metrics'; +import { + buildMetricCollectionsFromPDF, + normalizeConfidenceLevel, + normalizeDeltaBasisValue, + normalizeNumericString, + normalizeUnitString, + slugifyMetric, + stripOriginalMetric +} from '@/utils/quarterly-metrics'; + +// Configurar OpenAI +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY +}); + +// Prompt para análises trimestrais +const PROMPT_ANALISE_TRIMESTRAL = ` +Você é um analista financeiro especialista em análise fundamentalista de empresas brasileiras. + +Analise o texto do relatório trimestral fornecido e retorne APENAS um JSON válido seguindo a estrutura abaixo. Capture **todas** as métricas financeiras e operacionais relevantes encontradas, incluindo números, margens, indicadores de produtividade, eficiência e alavancagem. + +{ + "empresa": "Nome da empresa", + "ticker": "Código da ação (ex: VALE3)", + "trimestre": "Período (ex: 3T24)", + "dataReferencia": "Data de referência", + + "metricas": [ + { + "raw_label": "Rótulo exatamente como encontrado no PDF", + "normalized_label": "Rótulo padronizado (ex: Receita Líquida)", + "slug": "versão em kebab-case do normalized_label ou raw_label", + "value": "valor textual com contexto (ex: R$ 1,25 bi)", + "unit": "unidade padronizada (ex: R$ bi, %, p.p., toneladas)", + "delta_value": "valor da variação quando existir (número ou string)", + "delta_unit": "unidade da variação (%, p.p., etc)", + "delta_basis": "base da variação (yoy, qoq, pp, abs)", + "value_num": "valor numérico puro após conversões (usar number, não string)", + "parse_confidence": "high | medium | low", + "evidence": "trecho curto do PDF ou explicação da métrica" + } + ], + "metricas_map": { + "slug": { + "raw_label": "...", + "normalized_label": "...", + "slug": "...", + "value": "...", + "unit": "...", + "delta_value": "...", + "delta_unit": "...", + "delta_basis": "...", + "value_num": "...", + "parse_confidence": "high | medium | low", + "evidence": "..." + } + }, + + "relatorioGerado": { + "titulo": "Título profissional da análise", + "resumoExecutivo": "Resumo de 2-3 parágrafos sobre os principais resultados", + "destaques": [ + "Lista de 4-6 pontos positivos principais" + ], + "pontosAtencao": [ + "Lista de 4-6 pontos de atenção ou riscos" + ], + "conclusao": "Conclusão detalhada em 3-4 parágrafos sobre a situação da empresa, perspectivas e posicionamento competitivo", + "recomendacao": "COMPRA" | "MANTER" | "VENDA", + "precoAlvo": número (preço alvo em R$) + } +} + +REGRAS DE EXTRAÇÃO E CONVERSÃO: +- Converta números formatados no padrão PT-BR para números puros: vírgula como decimal, ponto como separador de milhar. +- Interprete sufixos: "mi" = milhões (multiplique por 1.000.000), "bi" = bilhões (multiplique por 1.000.000.000), "mil" = mil (multiplique por 1.000). Aplique o mesmo raciocínio para unidades em inglês quando aparecerem (million, billion, thousand). +- Se o valor vier com unidade monetária (R$, US$) e sufixo, aplique os multiplicadores mantendo o valor final em value_num. +- Remova ruídos do texto (asteriscos, notas de rodapé, símbolos irrelevantes) antes de calcular value_num, mas preserve-os em evidence quando forem úteis para contexto. +- Sempre gere slug em kebab-case consistente. +- Quando houver mais de uma variação (YoY, QoQ, p.p.), registre cada uma separadamente se possível. +- Preserve evidências citando o trecho ou número bruto do PDF utilizado para gerar cada métrica. +- Inclua métricas específicas do setor (produção, reservas, NIM, inadimplência, consumo, etc.) sempre que aparecerem. +- parse_confidence deve refletir a confiança na interpretação numérica (high, medium, low). +- Se não encontrar alguma informação, use null explicitamente. + +Texto do relatório: +`; + +export async function POST(request: NextRequest) { + try { + console.log('🤖 Iniciando análise com OpenAI...'); + + const { textoPDF } = await request.json(); + + if (!textoPDF) { + return NextResponse.json( + { error: 'Texto do PDF é obrigatório' }, + { status: 400 } + ); + } + + if (!process.env.OPENAI_API_KEY) { + console.error('⌠Chave da OpenAI não configurada'); + return NextResponse.json( + { error: 'Serviço de IA temporariamente indisponível' }, + { status: 500 } + ); + } + + console.log('📊 Enviando texto para OpenAI...'); + console.log(`📄 Tamanho do texto: ${textoPDF.length} caracteres`); + + // Truncar texto se muito grande (OpenAI tem limite de tokens) + let textoParaAnalise = textoPDF; + if (textoPDF.length > 15000) { + console.log('âœ‚ï¸ Truncando texto devido ao tamanho...'); + textoParaAnalise = textoPDF.substring(0, 15000) + "\n\n[TEXTO TRUNCADO DEVIDO AO TAMANHO]"; + } + + const completion = await openai.chat.completions.create({ + model: "gpt-4o", + messages: [ + { + role: "system", + content: "Você é um analista financeiro especialista em análise fundamentalista. Retorne APENAS um JSON válido, sem comentários adicionais." + }, + { + role: "user", + content: PROMPT_ANALISE_TRIMESTRAL + textoParaAnalise + } + ], + max_tokens: 4000, + temperature: 0.3, + response_format: { type: "json_object" } + }); + + const respostaIA = completion.choices[0].message.content; + console.log('✅ Resposta recebida da OpenAI'); + + if (!respostaIA) { + throw new Error('Resposta vazia da OpenAI'); + } + + let analiseJSON; + try { + analiseJSON = JSON.parse(respostaIA); + } catch (parseError) { + console.error('⌠Erro ao fazer parse do JSON da OpenAI:', parseError); + throw new Error('Resposta da IA em formato inválido'); + } + + console.log('📋 Análise parseada com sucesso'); + + const metricasListRaw = Array.isArray(analiseJSON.metricas) + ? analiseJSON.metricas + : Array.isArray(analiseJSON.metricasExtraidas) + ? analiseJSON.metricasExtraidas + : []; + + const sanitizeMetric = (item: any, index: number): MetricExtractionItem => { + const rawLabelSource = typeof item.raw_label === 'string' && item.raw_label.trim() + ? item.raw_label.trim() + : typeof item.label === 'string' && item.label.trim() + ? item.label.trim() + : `Métrica ${index + 1}`; + + const normalizedLabelSource = typeof item.normalized_label === 'string' && item.normalized_label.trim() + ? item.normalized_label.trim() + : rawLabelSource; + + const slugCandidate = typeof item.slug === 'string' && item.slug.trim() + ? item.slug.trim() + : slugifyMetric(normalizedLabelSource) || slugifyMetric(rawLabelSource) || `metrica-${index + 1}`; + + let valueNum: number | null = null; + if (typeof item.value_num === 'number' && Number.isFinite(item.value_num)) { + valueNum = item.value_num; + } else if (typeof item.valueNum === 'number' && Number.isFinite(item.valueNum)) { + valueNum = item.valueNum; + } else if (typeof item.value_num === 'string') { + const parsed = parseFloat(normalizeNumericString(item.value_num)); + valueNum = Number.isFinite(parsed) ? parsed : null; + } + + const unit = normalizeUnitString(item.unit ?? item.unidade ?? null); + const deltaUnit = normalizeUnitString(item.delta_unit ?? item.deltaUnit ?? null); + const deltaBasis = normalizeDeltaBasisValue(item.delta_basis ?? item.deltaBasis ?? null); + + return { + raw_label: rawLabelSource, + normalized_label: normalizedLabelSource, + slug: slugCandidate, + value: item.value ?? item.valorFormatado ?? item.formattedValue ?? null, + unit: unit, + delta_value: item.delta_value ?? item.deltaValue ?? null, + delta_unit: deltaUnit, + delta_basis: deltaBasis, + value_num: valueNum, + parse_confidence: normalizeConfidenceLevel(item.parse_confidence ?? item.parseConfidence ?? null), + evidence: item.evidence ?? (typeof item.value === 'string' ? item.value : null) + }; + }; + + const metricasList: MetricExtractionItem[] = metricasListRaw + .filter((item: any) => item && typeof item === 'object') + .map((item: any, index: number) => sanitizeMetric(item, index)); + + const metricasMapEntries: Array<[string, MetricExtractionItem]> = []; + if (analiseJSON.metricas_map && typeof analiseJSON.metricas_map === 'object') { + Object.entries(analiseJSON.metricas_map).forEach(([slug, value], mapIndex) => { + if (value && typeof value === 'object') { + metricasMapEntries.push([ + slug, + sanitizeMetric({ slug, ...value }, metricasList.length + mapIndex) + ]); + } + }); + } + + metricasList.forEach(item => { + metricasMapEntries.push([item.slug, item]); + }); + + const metricasMap: Record = Object.fromEntries(metricasMapEntries); + + const dadosExtraidos: DadosExtraidosPDF = { + empresa: analiseJSON.empresa, + ticker: analiseJSON.ticker, + trimestre: analiseJSON.trimestre, + dataReferencia: analiseJSON.dataReferencia, + dadosFinanceiros: analiseJSON.dadosFinanceiros, + metricas: metricasList, + metricasExtraidas: metricasList, + metricasMap + }; + + const collections = buildMetricCollectionsFromPDF(dadosExtraidos); + + const normalizedMetricMap = Object.fromEntries( + Object.entries(collections.metricMap).map(([slug, metric]) => [slug, stripOriginalMetric(metric)]) + ); + + const resultado = { + ...dadosExtraidos, + metricasNormalizadas: collections.normalizedMetrics, + metricasMap: normalizedMetricMap, + relatorioGerado: analiseJSON.relatorioGerado, + contextoMercado: `Análise gerada via OpenAI GPT-4o em ${new Date().toLocaleString('pt-BR')}`, + outlook: 'Baseado em dados do relatório trimestral processado', + canonicalMetrics: collections.canonicalMetrics, + otherMetrics: collections.dynamicEntries, + success: true + }; + + console.log('🎯 Análise OpenAI finalizada com sucesso'); + return NextResponse.json(resultado, { status: 200 }); + + } catch (error) { + console.error('⌠Erro na análise OpenAI:', error); + + let errorMessage = 'Erro interno do servidor'; + let statusCode = 500; + + if (error instanceof Error) { + if (error.message.includes('API key')) { + errorMessage = 'Erro de autenticação com IA'; + statusCode = 401; + } else if (error.message.includes('quota') || error.message.includes('rate limit')) { + errorMessage = 'Limite de uso da IA atingido. Tente novamente em alguns minutos.'; + statusCode = 429; + } else if (error.message.includes('timeout')) { + errorMessage = 'Timeout na análise da IA. Tente novamente.'; + statusCode = 408; + } else { + errorMessage = error.message; + } + } + + return NextResponse.json( + { + error: errorMessage, + success: false, + details: error instanceof Error ? error.message : 'Erro desconhecido' + }, + { status: statusCode } + ); + } +} \ No newline at end of file diff --git a/src/app/api/analises-trimestrais/route.ts b/src/app/api/analises-trimestrais/route.ts new file mode 100644 index 000000000..5c6676a55 --- /dev/null +++ b/src/app/api/analises-trimestrais/route.ts @@ -0,0 +1,141 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +// GET /api/analises-trimestrais - Listar análises +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const status = searchParams.get('status'); + const ticker = searchParams.get('ticker'); + const userId = searchParams.get('userId'); + const trimestre = searchParams.get('trimestre'); + + console.log('🔠Buscando análises com filtros:', { status, ticker, userId, trimestre }); + + const analises = await prisma.analiseTrimestreData.findMany({ + where: { + ...(status && { status: status }), + ...(ticker && { ticker: { contains: ticker, mode: 'insensitive' } }), + ...(userId && { userId: userId }), + ...(trimestre && { trimestre: { contains: trimestre, mode: 'insensitive' } }) + }, + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + avatar: true + } + } + }, + orderBy: [ + { dataPublicacao: 'desc' }, + { createdAt: 'desc' } + ], + take: 100 + }); + + console.log(`✅ Encontradas ${analises.length} análises`); + + return NextResponse.json(analises, { status: 200 }); + + } catch (error) { + console.error('⌠Erro ao buscar análises:', error); + return NextResponse.json( + { error: 'Erro interno do servidor', details: error instanceof Error ? error.message : 'Erro desconhecido' }, + { status: 500 } + ); + } +} + +// POST /api/analises-trimestrais - Criar nova análise +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + + console.log('📠Criando nova análise:', { + ticker: body.ticker, + titulo: body.titulo, + autor: body.autor + }); + + // Validações básicas + if (!body.ticker || !body.empresa || !body.titulo) { + return NextResponse.json( + { error: 'Campos obrigatórios: ticker, empresa, titulo' }, + { status: 400 } + ); + } + + // Garantir que dataPublicacao seja uma data válida + const dataPublicacao = body.dataPublicacao ? new Date(body.dataPublicacao) : new Date(); + + const otherMetrics = body.other_metrics ?? body.otherMetrics ?? body.metricas?.other_metrics ?? body.metricas?.otherMetrics; + const metricas = body.metricas && typeof body.metricas === 'object' + ? { ...body.metricas } + : {}; + + if (otherMetrics !== undefined) { + (metricas as any).other_metrics = otherMetrics; + } + + // Preparar dados para o Prisma + const analiseData = { + ticker: body.ticker.toUpperCase(), + empresa: body.empresa, + trimestre: body.trimestre || null, + dataPublicacao: dataPublicacao, + autor: body.autor || null, + categoria: body.categoria || 'resultado_trimestral', + titulo: body.titulo, + resumoExecutivo: body.resumoExecutivo || null, + analiseCompleta: body.analiseCompleta || null, + metricas: metricas, + pontosFavoraveis: body.pontosFavoraveis || null, + pontosAtencao: body.pontosAtencao || null, + recomendacao: body.recomendacao || 'MANTER', + nota: body.nota || null, // ↠ADICIONAR ESTA LINHA + linkResultado: body.linkResultado || null, + linkConferencia: body.linkConferencia || null, + status: body.status || 'draft', + userId: body.userId || null + }; + + const novaAnalise = await prisma.analiseTrimestreData.create({ + data: analiseData, + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + avatar: true + } + } + } + }); + + console.log('✅ Análise criada com sucesso:', novaAnalise.id); + + return NextResponse.json(novaAnalise, { status: 201 }); + + } catch (error) { + console.error('⌠Erro ao criar análise:', error); + + // Tratamento de erros específicos do Prisma + if (error instanceof Error) { + if (error.message.includes('unique constraint')) { + return NextResponse.json( + { error: 'Já existe uma análise com estes dados (ticker + trimestre + título)' }, + { status: 409 } + ); + } + } + + return NextResponse.json( + { error: 'Erro interno do servidor', details: error instanceof Error ? error.message : 'Erro desconhecido' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/answers/[id]/faq/route.ts b/src/app/api/answers/[id]/faq/route.ts new file mode 100644 index 000000000..fe39436cb --- /dev/null +++ b/src/app/api/answers/[id]/faq/route.ts @@ -0,0 +1,259 @@ +// src/app/api/answers/[id]/faq/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +// PUT - Marcar/desmarcar resposta como FAQ +export async function PUT( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + console.log('🚀 [FAQ-EDIT] Editando FAQ ID:', params.id); + + // USAR AUTENTICAÇÃO POR COOKIE (mesmo padrão da criação que funciona) + let user; + + try { + // Verificar se existe user no request (middleware) + user = (request as any).user; + + if (!user) { + // Buscar usuário admin para teste (mesmo padrão da criação) + const testUser = await prisma.user.findFirst({ + where: { + plan: 'ADMIN', + status: 'ACTIVE' + } + }); + + if (testUser) { + console.log('âš ï¸ [TEMP] Usando usuário admin para teste:', testUser.email); + user = testUser; + } + } + + } catch (error) { + console.log('⌠[AUTH] Erro na verificação de autenticação:', error.message); + } + + if (!user) { + console.log('⌠[AUTH] Usuário não encontrado'); + return NextResponse.json({ + error: 'Authentication required' + }, { status: 401 }); + } + + if (user.plan !== 'ADMIN' && user.plan !== 'ANALISTA') { + console.log('⌠[AUTH] Acesso negado - Plano:', user.plan); + return NextResponse.json({ + error: 'Admin access required' + }, { status: 403 }); + } + + console.log('✅ [AUTH] Admin autenticado:', user.email); + + const answer = await prisma.answer.findUnique({ + where: { id: params.id }, + include: { + question: { + select: { + id: true, + title: true, + content: true + } + } + } + }); + + if (!answer) { + console.log('⌠[NOT-FOUND] Resposta não encontrada:', params.id); + return NextResponse.json({ error: 'Answer not found' }, { status: 404 }); + } + + console.log('🔠[FOUND] Resposta encontrada:', answer.id); + + const body = await request.json(); + const { isFaq, faqTitle, faqOrder = 0, content } = body; + + console.log('📠[DATA] Dados recebidos:', { isFaq, faqTitle, faqOrder, content }); + + // Validações + if (isFaq !== undefined && typeof isFaq !== 'boolean') { + return NextResponse.json({ + error: 'isFaq must be a boolean' + }, { status: 400 }); + } + + if (faqTitle !== undefined && faqTitle !== null && typeof faqTitle !== 'string') { + return NextResponse.json({ + error: 'faqTitle must be a string' + }, { status: 400 }); + } + + if (typeof faqOrder !== 'number' || faqOrder < 0) { + return NextResponse.json({ + error: 'faqOrder must be a non-negative number' + }, { status: 400 }); + } + + if (content !== undefined && typeof content !== 'string') { + return NextResponse.json({ + error: 'content must be a string' + }, { status: 400 }); + } + + // Se está marcando como FAQ, gerar um título se não fornecido + let finalFaqTitle = faqTitle; + const finalIsFaq = isFaq !== undefined ? isFaq : answer.isFaq; + + if (finalIsFaq && !finalFaqTitle) { + finalFaqTitle = answer.question.title; + } + + // Verificar se já existe FAQ com mesmo título (se marcando como FAQ) + if (finalIsFaq && finalFaqTitle) { + const existingFaq = await prisma.answer.findFirst({ + where: { + isFaq: true, + faqTitle: finalFaqTitle.trim(), + id: { not: params.id } + } + }); + + if (existingFaq) { + console.log('⌠[DUPLICATE] FAQ com mesmo título já existe'); + return NextResponse.json({ + error: 'A FAQ with this title already exists' + }, { status: 400 }); + } + } + + const updateData: any = { + isFaq: finalIsFaq, + faqTitle: finalIsFaq && finalFaqTitle ? finalFaqTitle.trim() : null, + faqOrder: finalIsFaq ? faqOrder : 0 + }; + + if (content !== undefined) { + updateData.content = content; + } + + const updatedAnswer = await prisma.answer.update({ + where: { id: params.id }, + data: updateData, + include: { + question: { + select: { + id: true, + title: true, + content: true + } + }, + admin: { + select: { + id: true, + firstName: true, + lastName: true + } + } + } + }); + + console.log('✅ [SUCCESS] FAQ atualizada:', updatedAnswer.id); + + return NextResponse.json({ + answer: updatedAnswer, + message: finalIsFaq ? 'Resposta adicionada ao FAQ' : 'Resposta removida do FAQ' + }); + + } catch (error) { + console.error('💥 [ERROR] Error updating FAQ status:', error); + + // Tratamento específico de erros do Prisma + if (error && typeof error === 'object' && 'code' in error) { + const prismaError = error as any; + + if (prismaError.code === 'P2025') { + return NextResponse.json({ + error: 'Answer not found' + }, { status: 404 }); + } + } + + return NextResponse.json({ + error: 'Internal server error', + details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined + }, { status: 500 }); + } finally { + await prisma.$disconnect(); + } +} + +// DELETE - Remover FAQ +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + console.log('ðŸ—‘ï¸ [FAQ-DELETE] Removendo FAQ ID:', params.id); + + let user; + + try { + user = (request as any).user; + + if (!user) { + const testUser = await prisma.user.findFirst({ + where: { + plan: 'ADMIN', + status: 'ACTIVE' + } + }); + + if (testUser) { + console.log('âš ï¸ [TEMP] Usando usuário admin para teste:', testUser.email); + user = testUser; + } + } + } catch (error) { + console.log('⌠[AUTH] Erro na verificação de autenticação:', (error as Error).message); + } + + if (!user) { + console.log('⌠[AUTH] Usuário não encontrado'); + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + if (user.plan !== 'ADMIN' && user.plan !== 'ANALISTA') { + console.log('⌠[AUTH] Acesso negado - Plano:', user.plan); + return NextResponse.json({ error: 'Admin access required' }, { status: 403 }); + } + + const answer = await prisma.answer.findUnique({ + where: { id: params.id } + }); + + if (!answer) { + console.log('⌠[NOT-FOUND] Resposta não encontrada:', params.id); + return NextResponse.json({ error: 'Answer not found' }, { status: 404 }); + } + + await prisma.answer.update({ + where: { id: params.id }, + data: { + isFaq: false, + faqTitle: null, + faqOrder: 0 + } + }); + + console.log('✅ [SUCCESS] FAQ removida:', params.id); + + return NextResponse.json({ message: 'FAQ removed successfully' }); + } catch (error) { + console.error('💥 [ERROR] Error deleting FAQ:', error); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } finally { + await prisma.$disconnect(); + } +} diff --git a/src/app/api/ativo/[ticker]/route.ts b/src/app/api/ativo/[ticker]/route.ts new file mode 100644 index 000000000..f042fc7a7 --- /dev/null +++ b/src/app/api/ativo/[ticker]/route.ts @@ -0,0 +1,72 @@ +// app/api/ativo/[ticker]/completo/route.ts +import { CacheService } from '@/lib/redis'; + +export const maxDuration = 5; + +export async function GET( + request: Request, + { params }: { params: { ticker: string } } +) { + const ticker = params.ticker.toUpperCase(); + const cacheKey = CacheService.getCacheKey('ativo', ticker); + + // Headers de cache para CDN + const headers = new Headers(); + headers.set('Cache-Control', 'public, s-maxage=300, stale-while-revalidate=600'); + + try { + const { data, metadata } = await CacheService.withCache( + cacheKey, + async () => { + const [hgBrasilRes, yahooRes, brapiRes] = await Promise.allSettled([ + fetchHGBrasilData(ticker), + fetchYahooData(ticker), + fetchBRAPIData(ticker) + ]); + + const dadosConsolidados = { + ticker, + timestamp: new Date().toISOString(), + + preco: { + valor: extractPrice(brapiRes, yahooRes, hgBrasilRes), + variacao: extractVariation(brapiRes, yahooRes, hgBrasilRes), + fonte: getPriceSource(brapiRes, yahooRes, hgBrasilRes) + }, + + multiplos: { + pl: extractPL(hgBrasilRes, brapiRes, yahooRes), + pvp: extractPVP(hgBrasilRes, brapiRes, yahooRes), + dividendYield: extractDY(brapiRes, hgBrasilRes, yahooRes), + fontes: { + pl: getPLSource(hgBrasilRes, brapiRes, yahooRes), + pvp: getPVPSource(hgBrasilRes, brapiRes, yahooRes), + dy: getDYSource(brapiRes, hgBrasilRes, yahooRes) + } + }, + + qualidadeData: calculateDataQuality(hgBrasilRes, yahooRes, brapiRes) + }; + + return dadosConsolidados; + }, + { ttlSeconds: 300, tags: [`ativo:${ticker}`] } + ); + + return Response.json({ + success: true, + data, + cached: metadata.cached, + source: metadata.cached ? metadata.backend : 'api' + }, { headers }); + + } catch (error) { + console.error(`Erro ao buscar dados de ${ticker}:`, error); + + return Response.json({ + success: false, + error: 'Erro ao buscar dados do ativo', + ticker + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/auth/change-password/route.ts b/src/app/api/auth/change-password/route.ts new file mode 100644 index 000000000..e1e876b43 --- /dev/null +++ b/src/app/api/auth/change-password/route.ts @@ -0,0 +1,92 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { verifyPassword, hashPassword } from '@/lib/auth/password'; +import { auth } from '@/lib/auth'; + +export async function POST(request: NextRequest) { + try { + const { currentPassword, newPassword, confirmPassword } = await request.json(); + + console.log('🔠Solicitação de alteração de senha'); + + // Validar dados + if (!currentPassword || !newPassword || !confirmPassword) { + return NextResponse.json({ + error: 'Todos os campos são obrigatórios' + }, { status: 400 }); + } + + if (newPassword !== confirmPassword) { + return NextResponse.json({ + error: 'Nova senha e confirmação não coincidem' + }, { status: 400 }); + } + + if (newPassword.length < 6) { + return NextResponse.json({ + error: 'Nova senha deve ter pelo menos 6 caracteres' + }, { status: 400 }); + } + + // Recuperar usuário da sessão + const session = await auth(); + + if (!session?.user?.email) { + return NextResponse.json({ + error: 'Usuário não autenticado' + }, { status: 401 }); + } + + // Buscar usuário + const user = await prisma.user.findUnique({ + where: { email: session.user.email.toLowerCase() } + }); + + if (!user) { + return NextResponse.json({ + error: 'Usuário não encontrado' + }, { status: 404 }); + } + + // Verificar senha atual + const senhaValida = await verifyPassword(currentPassword, user.password || ''); + + if (!senhaValida) { + return NextResponse.json({ + error: 'Senha atual incorreta' + }, { status: 400 }); + } + + console.log('✅ Senha atual confirmada para:', user.email); + + // Hash da nova senha + const novaSenhaHash = await hashPassword(newPassword); + + // Atualizar senha do usuário + await prisma.user.update({ + where: { id: user.id }, + data: { + password: novaSenhaHash, + passwordCreatedAt: new Date(), + mustChangePassword: false, // 🎯 IMPORTANTE: Remover obrigação + loginAttempts: 0, // Resetar tentativas + lockedUntil: null // Desbloquear conta + } + }); + + console.log('🎉 Senha alterada com sucesso para:', user.email); + + return NextResponse.json({ + message: 'Senha alterada com sucesso', + mustChangePassword: false + }); + + } catch (error) { + console.error('⌠Erro ao alterar senha:', error); + return NextResponse.json({ + error: 'Erro interno do servidor' + }, { status: 500 }); + } finally { + await prisma.$disconnect(); + } +} \ No newline at end of file diff --git a/src/app/api/auth/check/route.ts b/src/app/api/auth/check/route.ts new file mode 100644 index 000000000..4aae076a8 --- /dev/null +++ b/src/app/api/auth/check/route.ts @@ -0,0 +1,58 @@ +// app/api/auth/check/route.ts +export const dynamic = 'force-dynamic'; +import { NextRequest, NextResponse } from 'next/server'; +import { auth } from '@/lib/auth'; +import { CacheService } from '@/lib/redis'; +import { logApiMetric } from '@/lib/metrics'; +import { consumeRateLimit, getClientRateLimitKey } from '@/lib/rateLimiter'; + +export const maxDuration = 3; + +export async function GET(request: NextRequest) { + const startedAt = Date.now(); + + const { allowed, retryAfterMs } = consumeRateLimit({ + key: `auth-check:${getClientRateLimitKey(request)}`, + limit: 6, + windowMs: 5 * 60 * 1000 + }); + + if (!allowed) { + return NextResponse.json( + { error: 'Too many auth checks, slow down' }, + { status: 429, headers: { 'Retry-After': Math.ceil(retryAfterMs / 1000).toString() } } + ); + } + + try { + const session = await auth(); + + if (!session?.user?.id) { // ✅ Melhor checar também o user.id + return NextResponse.json({ authenticated: false }, { status: 401 }); + } + + const cacheKey = CacheService.getCacheKey('auth-check', session.user.id); + + const { data, metadata } = await CacheService.withCache( + cacheKey, + async () => ({ authenticated: true }), + { + ttlSeconds: 60, + tags: [`session:${session.user.id}`], // ✅ CORRIGIDO: adicionado [ + } + ); + + logApiMetric({ + route: '/api/auth/check', + cacheHit: metadata.cached, + cacheBackend: metadata.backend, + durationMs: Date.now() - startedAt, + status: 200, + }); + + return NextResponse.json(data); + } catch (error) { + console.error('Erro na verificação de auth:', error); + return NextResponse.json({ authenticated: false }, { status: 401 }); + } +} diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts new file mode 100644 index 000000000..4b4cf8c8e --- /dev/null +++ b/src/app/api/auth/login/route.ts @@ -0,0 +1,157 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { prisma } from '@/lib/prisma'; +import { verifyPassword } from '@/lib/auth/password'; +import { enforceRateLimit } from '@/lib/rate-limit'; +import jwt from 'jsonwebtoken'; + +const loginSchema = z.object({ + email: z.string().email(), + password: z.string().min(1), +}); + +export async function POST(request: NextRequest) { + try { + const rateLimitKey = `login:${request.ip ?? 'unknown'}`; + const rateLimit = await enforceRateLimit(rateLimitKey, 10, 60_000); + if (!rateLimit.allowed) { + return NextResponse.json( + { error: 'Muitas tentativas de login. Tente novamente em instantes.' }, + { + status: 429, + headers: rateLimit.retryAfter + ? { 'Retry-After': rateLimit.retryAfter.toString() } + : undefined, + } + ); + } + + const payload = await request.json(); + const parsed = loginSchema.safeParse(payload); + + if (!parsed.success) { + return NextResponse.json( + { error: 'Payload inválido', issues: parsed.error.issues }, + { status: 400 } + ); + } + + const { email, password } = parsed.data; + const ip = request.ip || 'unknown'; + const userAgent = request.headers.get('user-agent') || ''; + + // Buscar usuário + const user = await prisma.user.findUnique({ + where: { email: email.toLowerCase() } + }); + + // Log da tentativa + await prisma.loginLog.create({ + data: { + userId: user?.id || null, + email: email.toLowerCase(), + success: false, // Será atualizado se login der certo + ip, + userAgent + } + }); + + if (!user) { + return NextResponse.json({ error: 'Email ou senha inválidos' }, { status: 401 }); + } + + // Verificar senha + const senhaValida = await verifyPassword(password, user.password || ''); + + if (!senhaValida) { + // Incrementar tentativas + const tentativas = (user.loginAttempts || 0) + 1; + const solicitarRedefinicao = tentativas >= 5; + + await prisma.user.update({ + where: { id: user.id }, + data: { + loginAttempts: tentativas, + lockedUntil: null + } + }); + + if (solicitarRedefinicao) { + return NextResponse.json({ + error: 'Muitas tentativas inválidas. Clique em "Esqueci minha senha" para redefinir sua senha.' + }, { status: 401 }); + } + + return NextResponse.json({ error: 'Email ou senha inválidos' }, { status: 401 }); + } + + // Verificar se usuário está ativo + if (user.status !== 'ACTIVE') { + return NextResponse.json({ error: 'Conta inativa' }, { status: 403 }); + } + + // Verificar se acesso expirou + if (user.expirationDate && user.expirationDate < new Date()) { + return NextResponse.json({ error: 'Acesso expirado' }, { status: 403 }); + } + + // Login bem-sucedido - resetar tentativas e atualizar último login + await prisma.user.update({ + where: { id: user.id }, + data: { + loginAttempts: 0, + lockedUntil: null, + lastLogin: new Date() + } + }); + + // Atualizar log para sucesso + await prisma.loginLog.updateMany({ + where: { + email: email.toLowerCase(), + success: false, + createdAt: { gte: new Date(Date.now() - 5000) } // últimos 5 segundos + }, + data: { success: true } + }); + + // Gerar JWT + const token = jwt.sign( + { + userId: user.id, + email: user.email, + plan: user.plan + }, + process.env.JWT_SECRET!, + { expiresIn: '7d' } + ); + + const response = NextResponse.json({ + message: 'Login realizado com sucesso', + user: { + id: user.id, + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + plan: user.plan, + mustChangePassword: user.mustChangePassword + } + }); + + // Definir cookie + response.cookies.set('auth-token', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 7 * 24 * 60 * 60 // 7 dias + }); + + return response; + + } catch (error) { + console.error('⌠Erro no login:', error); + return NextResponse.json({ error: 'Erro interno' }, { status: 500 }); + } finally { + await prisma.$disconnect(); + } +} \ No newline at end of file diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts new file mode 100644 index 000000000..6b43f2043 --- /dev/null +++ b/src/app/api/auth/logout/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from 'next/server'; + +export async function POST() { + const response = NextResponse.json({ success: true }); + + response.cookies.set('auth-token', '', { + maxAge: 0, + path: '/', + httpOnly: true, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + }); + + return response; +} diff --git a/src/app/api/auth/me/route.ts b/src/app/api/auth/me/route.ts new file mode 100644 index 000000000..b0bd62e2b --- /dev/null +++ b/src/app/api/auth/me/route.ts @@ -0,0 +1,51 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { auth } from '@/auth'; +import { prisma } from '@/lib/prisma'; +import { CacheService } from '@/lib/cache-service'; + +export const maxDuration = 3; + +export async function GET(request: NextRequest) { + try { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const cacheKey = `auth:me:${session.user.id}`; + + const user = await CacheService.withCache({ + key: cacheKey, + ttlSeconds: 60, + tags: ['auth', `user:${session.user.id}`], + fn: async () => { + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { + id: true, + firstName: true, + lastName: true, + email: true, + avatar: true, + plan: true, + status: true, + expirationDate: true, + customPermissions: true, + origin: true, + }, + }); + + if (!user) { + throw new Error('User not found'); + } + + return user; + }, + }); + + return NextResponse.json({ user }); + } catch (error) { + console.error('Erro em /api/auth/me:', error); + return NextResponse.json({ error: 'Internal error' }, { status: 500 }); + } +} diff --git a/src/app/api/auth/reset-password/[token]/route.ts b/src/app/api/auth/reset-password/[token]/route.ts new file mode 100644 index 000000000..b0d6c05f2 --- /dev/null +++ b/src/app/api/auth/reset-password/[token]/route.ts @@ -0,0 +1,136 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { hashPassword } from '@/lib/auth/password'; + +export async function POST( + request: NextRequest, + { params }: { params: { token: string } } +) { + try { + const { password, confirmPassword } = await request.json(); + const token = params.token; + + console.log('🔠Confirmando reset com token:', token); + + // Validar dados + if (!password || !confirmPassword) { + return NextResponse.json({ + error: 'Senha e confirmação são obrigatórias' + }, { status: 400 }); + } + + if (password !== confirmPassword) { + return NextResponse.json({ + error: 'Senhas não coincidem' + }, { status: 400 }); + } + + if (password.length < 6) { + return NextResponse.json({ + error: 'Senha deve ter pelo menos 6 caracteres' + }, { status: 400 }); + } + + // Buscar token válido + const resetToken = await prisma.passwordResetToken.findUnique({ + where: { token }, + include: { user: true } + }); + + if (!resetToken) { + return NextResponse.json({ + error: 'Token inválido ou expirado' + }, { status: 400 }); + } + + // Verificar se token não foi usado + if (resetToken.used) { + return NextResponse.json({ + error: 'Token já foi utilizado' + }, { status: 400 }); + } + + // Verificar se token não expirou + if (resetToken.expiresAt < new Date()) { + return NextResponse.json({ + error: 'Token expirado' + }, { status: 400 }); + } + + console.log('✅ Token válido para usuário:', resetToken.user.email); + + // Hash da nova senha + const senhaHash = await hashPassword(password); + + // Atualizar senha do usuário + await prisma.user.update({ + where: { id: resetToken.userId }, + data: { + password: senhaHash, + passwordCreatedAt: new Date(), + mustChangePassword: false, + loginAttempts: 0, + lockedUntil: null + } + }); + + // Marcar token como usado + await prisma.passwordResetToken.update({ + where: { id: resetToken.id }, + data: { used: true } + }); + + console.log('🎉 Senha redefinida com sucesso para:', resetToken.user.email); + + return NextResponse.json({ + message: 'Senha redefinida com sucesso' + }); + + } catch (error) { + console.error('⌠Erro ao redefinir senha:', error); + return NextResponse.json({ + error: 'Erro interno do servidor' + }, { status: 500 }); + } finally { + await prisma.$disconnect(); + } +} + +// GET - Verificar se token é válido +export async function GET( + request: NextRequest, + { params }: { params: { token: string } } +) { + try { + const token = params.token; + + const resetToken = await prisma.passwordResetToken.findUnique({ + where: { token }, + include: { user: { select: { email: true, firstName: true } } } + }); + + if (!resetToken || resetToken.used || resetToken.expiresAt < new Date()) { + return NextResponse.json({ + valid: false, + error: 'Token inválido ou expirado' + }); + } + + return NextResponse.json({ + valid: true, + user: { + email: resetToken.user.email, + firstName: resetToken.user.firstName + } + }); + + } catch (error) { + console.error('⌠Erro ao verificar token:', error); + return NextResponse.json({ + valid: false, + error: 'Erro interno do servidor' + }); + } finally { + await prisma.$disconnect(); + } +} \ No newline at end of file diff --git a/src/app/api/auth/reset-password/route.ts b/src/app/api/auth/reset-password/route.ts new file mode 100644 index 000000000..915bbffd1 --- /dev/null +++ b/src/app/api/auth/reset-password/route.ts @@ -0,0 +1,68 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { gerarTokenReset } from '@/lib/auth/password'; +import { enviarEmailResetSenha } from '@/lib/auth/email'; + +export async function POST(request: NextRequest) { + try { + const { email } = await request.json(); + console.log('🔑 Solicitação de reset para:', email); + + // Validar email + if (!email) { + return NextResponse.json({ + error: 'Email é obrigatório' + }, { status: 400 }); + } + + // Buscar usuário + const user = await prisma.user.findUnique({ + where: { email: email.toLowerCase() } + }); + + if (!user) { + // Por segurança, não revelar se email existe ou não + console.log('⌠Email não encontrado:', email); + return NextResponse.json({ + message: 'Se o email existir, você receberá instruções para redefinir a senha' + }); + } + + console.log('✅ Usuário encontrado:', user.firstName, user.lastName); + + // Gerar token de reset + const { token, expiresAt } = gerarTokenReset(); + console.log('🎫 Token gerado:', token); + + // Salvar token no banco + await prisma.passwordResetToken.create({ + data: { + userId: user.id, + token, + expiresAt, + used: false + } + }); + + // Enviar email + try { + await enviarEmailResetSenha(user.email, user.firstName, token); + console.log('📧 Email de reset enviado para:', user.email); + } catch (emailError) { + console.error('⌠Erro ao enviar email:', emailError); + // Não falhar a operação por erro de email + } + + return NextResponse.json({ + message: 'Se o email existir, você receberá instruções para redefinir a senha' + }); + + } catch (error) { + console.error('⌠Erro no reset de senha:', error); + return NextResponse.json({ + error: 'Erro interno do servidor' + }, { status: 500 }); + } finally { + await prisma.$disconnect(); + } +} \ No newline at end of file diff --git a/src/app/api/b3/investments/route.ts b/src/app/api/b3/investments/route.ts new file mode 100644 index 000000000..3f83ae123 --- /dev/null +++ b/src/app/api/b3/investments/route.ts @@ -0,0 +1,82 @@ +import { NextResponse } from 'next/server'; +import { DEFAULT_CARTEIRAS_DATA, type CarteiraAtivoBase } from '@/data/carteiras-default'; + +export const dynamic = 'force-dynamic'; +export const revalidate = 0; +export const runtime = 'nodejs'; + +interface CarteiraResumo { + chave: keyof typeof DEFAULT_CARTEIRAS_DATA; + nome: string; + totalAtivos: number; + tickers: string[]; + setores: string[]; +} + +const NOMES_CARTEIRAS: Record = { + smallCaps: 'Small Caps', + microCaps: 'Micro Caps', + dividendos: 'Dividendos', + fiis: 'FIIs', + cfRendaTurbinada: 'CF Renda Turbinada', + dividendosInternacional: 'Dividendos Internacional', + etfs: 'ETFs', + projetoAmerica: 'Projeto América', + exteriorStocks: 'Exterior Stocks', +}; + +function resumirCarteira( + chave: keyof typeof DEFAULT_CARTEIRAS_DATA, + ativos: CarteiraAtivoBase[] +): CarteiraResumo { + const tickers = ativos.map(ativo => ativo.ticker); + const setores = Array.from(new Set(ativos.map(ativo => ativo.setor))).filter(Boolean); + + return { + chave, + nome: NOMES_CARTEIRAS[chave], + totalAtivos: ativos.length, + tickers, + setores, + }; +} + +export async function GET() { + const timestamp = new Date().toISOString(); + const resumos = (Object.keys(DEFAULT_CARTEIRAS_DATA) as (keyof typeof DEFAULT_CARTEIRAS_DATA)[]) + .map(chave => { + const ativos = DEFAULT_CARTEIRAS_DATA[chave] as CarteiraAtivoBase[]; + return resumirCarteira(chave, ativos); + }); + + const totais = resumos.reduce( + (acc, resumo) => { + acc.totalAtivos += resumo.totalAtivos; + resumo.tickers.forEach(ticker => acc.tickers.add(ticker)); + resumo.setores.forEach(setor => acc.setores.add(setor)); + return acc; + }, + { totalAtivos: 0, tickers: new Set(), setores: new Set() } + ); + + const body = { + success: true, + timestamp, + totalCategorias: resumos.length, + totais: { + totalAtivos: totais.totalAtivos, + totalTickers: totais.tickers.size, + totalSetores: totais.setores.size, + }, + categorias: resumos, + }; + + return NextResponse.json(body, { + status: 200, + headers: { + 'Cache-Control': 'no-store, max-age=0', + 'CDN-Cache-Control': 'no-store', + 'Vercel-CDN-Cache-Control': 'no-store', + }, + }); +} diff --git a/src/app/api/b3/market-data/route.ts b/src/app/api/b3/market-data/route.ts new file mode 100644 index 000000000..c850aa2c2 --- /dev/null +++ b/src/app/api/b3/market-data/route.ts @@ -0,0 +1,118 @@ +import { NextResponse } from 'next/server'; +import { getMarketSnapshot, type CombinedQuote } from '@/server/market-data'; + +export const dynamic = 'force-dynamic'; +export const revalidate = 0; +export const fetchCache = 'force-no-store'; +export const runtime = 'nodejs'; + +const CACHE_TTL_MS = 2 * 60 * 1000; + +const FALLBACK_DATA = { + ibovespa: { value: '136.431', trend: 'down' as const, diff: -0.26 }, + indiceSmall: { value: '2.237,86', trend: 'up' as const, diff: 1.56 }, + carteiraHoje: { value: '88.7%', trend: 'up' as const, diff: 88.7 }, + dividendYield: { value: '7.4%', trend: 'up' as const, diff: 7.4 }, +}; + +type MarketMetric = { + value: string; + trend: 'up' | 'down'; + diff: number; +}; + +function formatNumber(value: number, fractionDigits = 2): string { + return new Intl.NumberFormat('pt-BR', { + minimumFractionDigits: fractionDigits, + maximumFractionDigits: fractionDigits, + }).format(value); +} + +function buildMetric(quote: CombinedQuote | undefined, fallback: MarketMetric): MarketMetric { + if (!quote || typeof quote.price !== 'number') { + return fallback; + } + + const diff = typeof quote.changePercent === 'number' + ? Number(quote.changePercent.toFixed(2)) + : fallback.diff; + const trend = diff >= 0 ? 'up' : 'down'; + + const value = quote.price >= 1000 + ? formatNumber(quote.price, 0) + : formatNumber(quote.price, 2); + + return { + value, + trend, + diff, + }; +} + +export async function GET() { + const startedAt = Date.now(); + + try { + const snapshot = await getMarketSnapshot([ + { symbol: 'IBOV', key: 'IBOV' }, + { symbol: 'SMLL', key: 'SMLL' }, + ], { + cacheTtlMs: CACHE_TTL_MS, + }); + + const ibovespa = buildMetric(snapshot.quotes.IBOV, FALLBACK_DATA.ibovespa); + const indiceSmall = buildMetric(snapshot.quotes.SMLL, FALLBACK_DATA.indiceSmall); + + const responseBody = { + success: true, + data: { + ibovespa, + indiceSmall, + carteiraHoje: FALLBACK_DATA.carteiraHoje, + dividendYield: FALLBACK_DATA.dividendYield, + }, + metadata: { + timestamp: new Date().toISOString(), + processingTime: Date.now() - startedAt, + cache: { + fromCache: snapshot.fromCache, + cachedAt: snapshot.cachedAt, + cacheTtlMs: snapshot.cacheTtlMs, + expiresAt: snapshot.cachedAt + snapshot.cacheTtlMs, + }, + requested: snapshot.metadata.requested, + }, + }; + + return NextResponse.json(responseBody, { + status: 200, + headers: { + 'Cache-Control': 'no-store, max-age=0', + 'CDN-Cache-Control': 'no-store', + 'Vercel-CDN-Cache-Control': 'no-store', + }, + }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Erro desconhecido'; + + return NextResponse.json({ + success: false, + error: message, + data: { + ibovespa: FALLBACK_DATA.ibovespa, + indiceSmall: FALLBACK_DATA.indiceSmall, + carteiraHoje: FALLBACK_DATA.carteiraHoje, + dividendYield: FALLBACK_DATA.dividendYield, + }, + metadata: { + timestamp: new Date().toISOString(), + fallback: true, + }, + }, { + status: 200, + headers: { + 'Cache-Control': 'no-store', + }, + }); + } +} diff --git a/src/app/api/bastidores-da-bolsa/lessons/[lessonId]/materials/route.ts b/src/app/api/bastidores-da-bolsa/lessons/[lessonId]/materials/route.ts new file mode 100644 index 000000000..f8cb20987 --- /dev/null +++ b/src/app/api/bastidores-da-bolsa/lessons/[lessonId]/materials/route.ts @@ -0,0 +1,117 @@ +import { mkdir, writeFile } from 'fs/promises'; +import { NextRequest, NextResponse } from 'next/server'; +import path from 'path'; +import { randomUUID } from 'crypto'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeMaterial } from '../../../utils'; + +const ALLOWED_MIME_TYPES = new Set([ + 'application/pdf', + 'image/jpeg', + 'image/png', + 'image/webp', + 'image/gif' +]); + +const MAX_FILE_SIZE_BYTES = 25 * 1024 * 1024; // 25MB + +export async function POST(request: NextRequest, { params }: { params: { lessonId: string } }) { + const lessonId = Number(params.lessonId); + + if (!params.lessonId || Number.isNaN(lessonId)) { + return NextResponse.json({ error: 'Lesson id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const lesson = await prisma.bastidoresDaBolsaLesson.findUnique({ + where: { id: lessonId } + }); + + if (!lesson) { + return NextResponse.json({ error: 'Lesson not found' }, { status: 404 }); + } + + const formData = await request.formData(); + + const file = formData.get('file'); + const title = formData.get('title'); + + type FileFormDataEntry = Blob & { name?: string; type?: string }; + + const isFileFormDataEntry = (value: unknown): value is FileFormDataEntry => { + return ( + typeof value === 'object' && + value !== null && + typeof (value as Blob).arrayBuffer === 'function' && + typeof (value as Blob).size === 'number' + ); + }; + + if (!isFileFormDataEntry(file)) { + return NextResponse.json({ error: 'Arquivo é obrigatório' }, { status: 400 }); + } + + if (file.size <= 0) { + return NextResponse.json({ error: 'Arquivo vazio' }, { status: 400 }); + } + + if (file.size > MAX_FILE_SIZE_BYTES) { + return NextResponse.json({ error: 'Arquivo maior que 25MB' }, { status: 400 }); + } + + if (file.type && !ALLOWED_MIME_TYPES.has(file.type)) { + return NextResponse.json({ error: 'Formato de arquivo não suportado' }, { status: 400 }); + } + + const bytes = await file.arrayBuffer(); + const buffer = Buffer.from(bytes); + + const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'bastidores-da-bolsa'); + await mkdir(uploadDir, { recursive: true }); + + const originalName = typeof file.name === 'string' && file.name.trim().length > 0 ? file.name : 'material'; + const originalExtension = path.extname(originalName) || ''; + const filename = `${randomUUID()}${originalExtension}`; + const filePath = path.join(uploadDir, filename); + + await writeFile(filePath, buffer); + + const material = await prisma.bastidoresDaBolsaLessonMaterial.create({ + data: { + lessonId, + title: typeof title === 'string' && title.trim().length > 0 ? title.trim() : originalName, + fileUrl: `/uploads/bastidores-da-bolsa/${filename}` + } + }); + + return NextResponse.json({ material: serializeMaterial(material) }, { status: 201 }); + } catch (error) { + console.error('Erro ao anexar material em Bastidores da Bolsa:', error); + return NextResponse.json({ error: 'Erro ao anexar material' }, { status: 500 }); + } +} diff --git a/src/app/api/bastidores-da-bolsa/lessons/[lessonId]/route.ts b/src/app/api/bastidores-da-bolsa/lessons/[lessonId]/route.ts new file mode 100644 index 000000000..8c342e754 --- /dev/null +++ b/src/app/api/bastidores-da-bolsa/lessons/[lessonId]/route.ts @@ -0,0 +1,200 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeLesson } from '../../utils'; + +export async function PATCH(request: NextRequest, { params }: { params: { lessonId: string } }) { + const lessonId = Number(params.lessonId); + + if (!params.lessonId || Number.isNaN(lessonId)) { + return NextResponse.json({ error: 'Lesson id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const lesson = await prisma.bastidoresDaBolsaLesson.findUnique({ + where: { id: lessonId }, + include: { + materials: { + orderBy: { + createdAt: 'asc' + } + } + } + }); + + if (!lesson) { + return NextResponse.json({ error: 'Lesson not found' }, { status: 404 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, duration, description, videoUrl, content } = body as { + title?: unknown; + duration?: unknown; + description?: unknown; + videoUrl?: unknown; + content?: unknown; + }; + + const data: { + title?: string; + duration?: string; + description?: string | null; + videoUrl?: string | null; + content?: string | null; + } = {}; + + if (typeof title !== 'undefined') { + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título inválido' }, { status: 400 }); + } + + data.title = title.trim(); + } + + if (typeof duration !== 'undefined') { + if (typeof duration !== 'string' || !duration.trim()) { + return NextResponse.json({ error: 'Duração inválida' }, { status: 400 }); + } + + data.duration = duration.trim(); + } + + if (typeof description !== 'undefined') { + if (description !== null && typeof description !== 'string') { + return NextResponse.json({ error: 'Descrição inválida' }, { status: 400 }); + } + + if (typeof description === 'string') { + const trimmedDescription = description.trim(); + data.description = trimmedDescription.length > 0 ? trimmedDescription : null; + } else { + data.description = null; + } + } + + if (typeof videoUrl !== 'undefined') { + if (videoUrl !== null && typeof videoUrl !== 'string') { + return NextResponse.json({ error: 'Link de vídeo inválido' }, { status: 400 }); + } + + if (typeof videoUrl === 'string') { + const trimmedVideoUrl = videoUrl.trim(); + data.videoUrl = trimmedVideoUrl.length > 0 ? trimmedVideoUrl : null; + } else { + data.videoUrl = null; + } + } + + if (typeof content !== 'undefined') { + if (content !== null && typeof content !== 'string') { + return NextResponse.json({ error: 'Conteúdo inválido' }, { status: 400 }); + } + + if (typeof content === 'string') { + const trimmedContent = content.trim(); + data.content = trimmedContent.length > 0 ? trimmedContent : null; + } else { + data.content = null; + } + } + + if (Object.keys(data).length === 0) { + return NextResponse.json({ error: 'Nenhum campo para atualizar' }, { status: 400 }); + } + + const updated = await prisma.bastidoresDaBolsaLesson.update({ + where: { id: lessonId }, + data, + include: { + materials: { + orderBy: { + createdAt: 'asc' + } + } + } + }); + + return NextResponse.json({ lesson: serializeLesson(updated) }); + } catch (error) { + console.error('Erro ao atualizar aula de Bastidores da Bolsa:', error); + return NextResponse.json({ error: 'Erro ao atualizar aula' }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest, { params }: { params: { lessonId: string } }) { + const lessonId = Number(params.lessonId); + + if (!params.lessonId || Number.isNaN(lessonId)) { + return NextResponse.json({ error: 'Lesson id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const lesson = await prisma.bastidoresDaBolsaLesson.findUnique({ + where: { id: lessonId } + }); + + if (!lesson) { + return NextResponse.json({ error: 'Lesson not found' }, { status: 404 }); + } + + await prisma.bastidoresDaBolsaLesson.delete({ + where: { id: lessonId } + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao excluir aula de Bastidores da Bolsa:', error); + return NextResponse.json({ error: 'Erro ao excluir aula' }, { status: 500 }); + } +} diff --git a/src/app/api/bastidores-da-bolsa/live-events/[liveId]/route.ts b/src/app/api/bastidores-da-bolsa/live-events/[liveId]/route.ts new file mode 100644 index 000000000..a4b9d30ca --- /dev/null +++ b/src/app/api/bastidores-da-bolsa/live-events/[liveId]/route.ts @@ -0,0 +1,222 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeLiveEvent } from '../../utils'; + +function parseDurationMinutes(value: unknown): number | null { + if (typeof value === 'number' && Number.isFinite(value)) { + const rounded = Math.round(value); + return rounded > 0 ? rounded : null; + } + + if (typeof value === 'string') { + const trimmed = value.trim(); + if (!trimmed) { + return null; + } + + const parsed = Number.parseInt(trimmed, 10); + if (!Number.isFinite(parsed) || Number.isNaN(parsed)) { + return null; + } + + return parsed > 0 ? parsed : null; + } + + return null; +} + +export async function PATCH(request: NextRequest, { params }: { params: { liveId: string } }) { + const liveId = Number(params.liveId); + + if (!params.liveId || Number.isNaN(liveId)) { + return NextResponse.json({ error: 'Live id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const event = await prisma.bastidoresDaBolsaLiveEvent.findUnique({ + where: { id: liveId } + }); + + if (!event) { + return NextResponse.json({ error: 'Live not found' }, { status: 404 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, description, eventDate, durationMinutes, meetingUrl, isCancelled } = body as { + title?: unknown; + description?: unknown; + eventDate?: unknown; + durationMinutes?: unknown; + meetingUrl?: unknown; + isCancelled?: unknown; + }; + + const data: { + title?: string; + description?: string | null; + eventDate?: Date; + durationMinutes?: number; + meetingUrl?: string | null; + isCancelled?: boolean; + } = {}; + + if (typeof title !== 'undefined') { + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título inválido' }, { status: 400 }); + } + + data.title = title.trim(); + } + + if (typeof description !== 'undefined') { + if (description !== null && typeof description !== 'string') { + return NextResponse.json({ error: 'Descrição inválida' }, { status: 400 }); + } + + if (typeof description === 'string') { + const trimmedDescription = description.trim(); + data.description = trimmedDescription.length > 0 ? trimmedDescription : null; + } else { + data.description = null; + } + } + + if (typeof eventDate !== 'undefined') { + if (typeof eventDate !== 'string' || !eventDate.trim()) { + return NextResponse.json({ error: 'Data inválida' }, { status: 400 }); + } + + const parsedDate = new Date(eventDate); + + if (Number.isNaN(parsedDate.getTime())) { + return NextResponse.json({ error: 'Data inválida' }, { status: 400 }); + } + + data.eventDate = parsedDate; + } + + if (typeof durationMinutes !== 'undefined') { + const parsedDuration = parseDurationMinutes(durationMinutes); + + if (parsedDuration === null) { + return NextResponse.json({ error: 'Duração inválida' }, { status: 400 }); + } + + data.durationMinutes = parsedDuration; + } + + if (typeof meetingUrl !== 'undefined') { + if (meetingUrl !== null && typeof meetingUrl !== 'string') { + return NextResponse.json({ error: 'Link inválido' }, { status: 400 }); + } + + if (typeof meetingUrl === 'string') { + const trimmedUrl = meetingUrl.trim(); + data.meetingUrl = trimmedUrl.length > 0 ? trimmedUrl : null; + } else { + data.meetingUrl = null; + } + } + + if (typeof isCancelled !== 'undefined') { + if (typeof isCancelled !== 'boolean') { + return NextResponse.json({ error: 'Valor inválido para cancelamento' }, { status: 400 }); + } + + data.isCancelled = isCancelled; + } + + if (Object.keys(data).length === 0) { + return NextResponse.json({ error: 'Nenhum campo para atualizar' }, { status: 400 }); + } + + const updated = await prisma.bastidoresDaBolsaLiveEvent.update({ + where: { id: liveId }, + data + }); + + return NextResponse.json({ event: serializeLiveEvent(updated) }); + } catch (error) { + console.error('Erro ao atualizar live de Bastidores da Bolsa:', error); + return NextResponse.json({ error: 'Erro ao atualizar live' }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest, { params }: { params: { liveId: string } }) { + const liveId = Number(params.liveId); + + if (!params.liveId || Number.isNaN(liveId)) { + return NextResponse.json({ error: 'Live id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const event = await prisma.bastidoresDaBolsaLiveEvent.findUnique({ + where: { id: liveId } + }); + + if (!event) { + return NextResponse.json({ error: 'Live not found' }, { status: 404 }); + } + + await prisma.bastidoresDaBolsaLiveEvent.delete({ + where: { id: liveId } + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao excluir live de Bastidores da Bolsa:', error); + return NextResponse.json({ error: 'Erro ao excluir live' }, { status: 500 }); + } +} diff --git a/src/app/api/bastidores-da-bolsa/live-events/route.ts b/src/app/api/bastidores-da-bolsa/live-events/route.ts new file mode 100644 index 000000000..d3a2bdd6b --- /dev/null +++ b/src/app/api/bastidores-da-bolsa/live-events/route.ts @@ -0,0 +1,170 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeLiveEvent } from '../utils'; + +function parseDurationMinutes(value: unknown): number | null { + if (typeof value === 'number' && Number.isFinite(value)) { + const rounded = Math.round(value); + return rounded > 0 ? rounded : null; + } + + if (typeof value === 'string') { + const trimmed = value.trim(); + if (!trimmed) { + return null; + } + + const parsed = Number.parseInt(trimmed, 10); + if (!Number.isFinite(parsed) || Number.isNaN(parsed)) { + return null; + } + + return parsed > 0 ? parsed : null; + } + + return null; +} + +export async function GET() { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const events = await prisma.bastidoresDaBolsaLiveEvent.findMany({ + orderBy: [ + { eventDate: 'asc' }, + { createdAt: 'asc' } + ] + }); + + return NextResponse.json({ events: events.map(serializeLiveEvent) }); + } catch (error) { + console.error('Erro ao listar lives de Bastidores da Bolsa:', error); + return NextResponse.json({ error: 'Erro ao listar lives' }, { status: 500 }); + } +} + +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, description, eventDate, durationMinutes, meetingUrl } = body as { + title?: unknown; + description?: unknown; + eventDate?: unknown; + durationMinutes?: unknown; + meetingUrl?: unknown; + }; + + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título é obrigatório' }, { status: 400 }); + } + + if (typeof eventDate !== 'string' || !eventDate.trim()) { + return NextResponse.json({ error: 'Data e horário são obrigatórios' }, { status: 400 }); + } + + const parsedDate = new Date(eventDate); + + if (Number.isNaN(parsedDate.getTime())) { + return NextResponse.json({ error: 'Data e horário inválidos' }, { status: 400 }); + } + + let normalizedDuration = 60; + + if (typeof durationMinutes !== 'undefined') { + const parsedDuration = parseDurationMinutes(durationMinutes); + + if (parsedDuration === null) { + return NextResponse.json({ error: 'Duração inválida' }, { status: 400 }); + } + + normalizedDuration = parsedDuration; + } + + let normalizedDescription: string | null = null; + + if (typeof description !== 'undefined') { + if (description !== null && typeof description !== 'string') { + return NextResponse.json({ error: 'Descrição inválida' }, { status: 400 }); + } + + if (typeof description === 'string') { + const trimmedDescription = description.trim(); + normalizedDescription = trimmedDescription.length > 0 ? trimmedDescription : null; + } + } + + let normalizedMeetingUrl: string | null = null; + + if (typeof meetingUrl !== 'undefined') { + if (meetingUrl !== null && typeof meetingUrl !== 'string') { + return NextResponse.json({ error: 'Link de reunião inválido' }, { status: 400 }); + } + + if (typeof meetingUrl === 'string') { + const trimmedUrl = meetingUrl.trim(); + normalizedMeetingUrl = trimmedUrl.length > 0 ? trimmedUrl : null; + } + } + + const event = await prisma.bastidoresDaBolsaLiveEvent.create({ + data: { + title: title.trim(), + description: normalizedDescription, + eventDate: parsedDate, + durationMinutes: normalizedDuration, + meetingUrl: normalizedMeetingUrl + } + }); + + return NextResponse.json({ event: serializeLiveEvent(event) }, { status: 201 }); + } catch (error) { + console.error('Erro ao criar live de Bastidores da Bolsa:', error); + return NextResponse.json({ error: 'Erro ao criar live' }, { status: 500 }); + } +} diff --git a/src/app/api/bastidores-da-bolsa/materials/[materialId]/route.ts b/src/app/api/bastidores-da-bolsa/materials/[materialId]/route.ts new file mode 100644 index 000000000..e441744d8 --- /dev/null +++ b/src/app/api/bastidores-da-bolsa/materials/[materialId]/route.ts @@ -0,0 +1,65 @@ +import { unlink } from 'fs/promises'; +import path from 'path'; +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +export async function DELETE(request: NextRequest, { params }: { params: { materialId: string } }) { + const materialId = Number(params.materialId); + + if (!params.materialId || Number.isNaN(materialId)) { + return NextResponse.json({ error: 'Material id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const material = await prisma.bastidoresDaBolsaLessonMaterial.findUnique({ + where: { id: materialId } + }); + + if (!material) { + return NextResponse.json({ error: 'Material not found' }, { status: 404 }); + } + + await prisma.bastidoresDaBolsaLessonMaterial.delete({ + where: { id: materialId } + }); + + if (material.fileUrl.startsWith('/uploads/')) { + const filePath = path.join(process.cwd(), 'public', material.fileUrl.replace(/^\//, '')); + + try { + await unlink(filePath); + } catch (error) { + console.warn('Não foi possível remover arquivo de material de Bastidores da Bolsa:', error); + } + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao remover material de Bastidores da Bolsa:', error); + return NextResponse.json({ error: 'Erro ao remover material' }, { status: 500 }); + } +} diff --git a/src/app/api/bastidores-da-bolsa/modules/[id]/lessons/route.ts b/src/app/api/bastidores-da-bolsa/modules/[id]/lessons/route.ts new file mode 100644 index 000000000..41e363d3e --- /dev/null +++ b/src/app/api/bastidores-da-bolsa/modules/[id]/lessons/route.ts @@ -0,0 +1,109 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeLesson } from '../../../utils'; + +export async function POST(request: NextRequest, { params }: { params: { id: string } }) { + const { id: moduleId } = params; + + if (!moduleId || typeof moduleId !== 'string') { + return NextResponse.json({ error: 'Module id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const module = await prisma.bastidoresDaBolsaModule.findUnique({ + where: { id: moduleId } + }); + + if (!module) { + return NextResponse.json({ error: 'Module not found' }, { status: 404 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, duration, description, videoUrl, content } = body as { + title?: unknown; + duration?: unknown; + description?: unknown; + videoUrl?: unknown; + content?: unknown; + }; + + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título é obrigatório' }, { status: 400 }); + } + + if (typeof duration !== 'string' || !duration.trim()) { + return NextResponse.json({ error: 'Duração é obrigatória' }, { status: 400 }); + } + + const trimmedTitle = title.trim(); + const trimmedDuration = duration.trim(); + const trimmedDescription = typeof description === 'string' ? description.trim() : undefined; + const trimmedVideoUrl = typeof videoUrl === 'string' ? videoUrl.trim() : undefined; + const trimmedContent = typeof content === 'string' ? content.trim() : undefined; + + const aggregate = await prisma.bastidoresDaBolsaLesson.aggregate({ + where: { moduleId }, + _max: { sortOrder: true } + }); + + const nextSortOrder = (aggregate._max.sortOrder ?? 0) + 1; + + const lesson = await prisma.bastidoresDaBolsaLesson.create({ + data: { + moduleId, + title: trimmedTitle, + duration: trimmedDuration, + description: + trimmedDescription && trimmedDescription.length > 0 ? trimmedDescription : null, + videoUrl: trimmedVideoUrl && trimmedVideoUrl.length > 0 ? trimmedVideoUrl : null, + content: trimmedContent && trimmedContent.length > 0 ? trimmedContent : null, + sortOrder: nextSortOrder + }, + include: { + materials: { + orderBy: { + createdAt: 'asc' + } + } + } + }); + + return NextResponse.json({ lesson: serializeLesson(lesson) }, { status: 201 }); + } catch (error) { + console.error('Erro ao criar aula de Bastidores da Bolsa:', error); + return NextResponse.json({ error: 'Erro ao criar aula' }, { status: 500 }); + } +} diff --git a/src/app/api/bastidores-da-bolsa/modules/[id]/route.ts b/src/app/api/bastidores-da-bolsa/modules/[id]/route.ts new file mode 100644 index 000000000..6e7783ee0 --- /dev/null +++ b/src/app/api/bastidores-da-bolsa/modules/[id]/route.ts @@ -0,0 +1,202 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeModule } from '../../utils'; + +export async function PATCH(request: NextRequest, { params }: { params: { id: string } }) { + const { id } = params; + + if (!id || typeof id !== 'string') { + return NextResponse.json({ error: 'Module id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const module = await prisma.bastidoresDaBolsaModule.findUnique({ + where: { id }, + include: { + lessons: { + orderBy: [ + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ], + include: { + materials: { + orderBy: { + createdAt: 'asc' + } + } + } + } + } + }); + + if (!module) { + return NextResponse.json({ error: 'Module not found' }, { status: 404 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, highlight, releaseDate, isHidden } = body as { + title?: unknown; + highlight?: unknown; + releaseDate?: unknown; + isHidden?: unknown; + }; + + const data: { + title?: string; + highlight?: string | null; + releaseDate?: Date; + isHidden?: boolean; + } = {}; + + if (typeof title !== 'undefined') { + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título inválido' }, { status: 400 }); + } + + data.title = title.trim(); + } + + if (typeof highlight !== 'undefined') { + if (highlight !== null && typeof highlight !== 'string') { + return NextResponse.json({ error: 'Destaque inválido' }, { status: 400 }); + } + + if (typeof highlight === 'string') { + const trimmedHighlight = highlight.trim(); + data.highlight = trimmedHighlight.length > 0 ? trimmedHighlight : null; + } else { + data.highlight = null; + } + } + + if (typeof releaseDate !== 'undefined') { + if (typeof releaseDate !== 'string' || !releaseDate.trim()) { + return NextResponse.json({ error: 'Data de liberação inválida' }, { status: 400 }); + } + + const parsedReleaseDate = new Date(releaseDate); + + if (Number.isNaN(parsedReleaseDate.getTime())) { + return NextResponse.json({ error: 'Data de liberação inválida' }, { status: 400 }); + } + + data.releaseDate = parsedReleaseDate; + } + + if (typeof isHidden !== 'undefined') { + if (typeof isHidden !== 'boolean') { + return NextResponse.json({ error: 'Valor inválido para visibilidade' }, { status: 400 }); + } + + data.isHidden = isHidden; + } + + if (Object.keys(data).length === 0) { + return NextResponse.json({ error: 'Nenhum campo para atualizar' }, { status: 400 }); + } + + const updated = await prisma.bastidoresDaBolsaModule.update({ + where: { id }, + data, + include: { + lessons: { + orderBy: [ + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ], + include: { + materials: { + orderBy: { + createdAt: 'asc' + } + } + } + } + } + }); + + return NextResponse.json({ module: serializeModule(updated) }); + } catch (error) { + console.error('Erro ao atualizar módulo de Bastidores da Bolsa:', error); + return NextResponse.json({ error: 'Erro ao atualizar módulo' }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) { + const { id } = params; + + if (!id || typeof id !== 'string') { + return NextResponse.json({ error: 'Module id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const existingModule = await prisma.bastidoresDaBolsaModule.findUnique({ + where: { id } + }); + + if (!existingModule) { + return NextResponse.json({ error: 'Module not found' }, { status: 404 }); + } + + await prisma.bastidoresDaBolsaModule.delete({ + where: { id } + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao excluir módulo de Bastidores da Bolsa:', error); + return NextResponse.json({ error: 'Erro ao excluir módulo' }, { status: 500 }); + } +} diff --git a/src/app/api/bastidores-da-bolsa/modules/route.ts b/src/app/api/bastidores-da-bolsa/modules/route.ts new file mode 100644 index 000000000..67c422019 --- /dev/null +++ b/src/app/api/bastidores-da-bolsa/modules/route.ts @@ -0,0 +1,176 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeModule } from '../utils'; + +export async function GET(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const isAdmin = user.plan === 'ADMIN'; + const now = new Date(); + + const modules = await prisma.bastidoresDaBolsaModule.findMany({ + where: isAdmin + ? undefined + : { + isHidden: false + }, + orderBy: [ + { releaseDate: 'asc' }, + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ], + include: { + lessons: { + orderBy: [ + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ], + include: { + materials: { + orderBy: { + createdAt: 'asc' + } + } + } + } + } + }); + + const serializedModules = modules.map(module => { + const baseModule = serializeModule(module); + const releaseTimestamp = + module.releaseDate instanceof Date ? module.releaseDate.getTime() : Number.NaN; + const isReleased = !Number.isNaN(releaseTimestamp) && releaseTimestamp <= now.getTime(); + + if (isAdmin || isReleased) { + return { + ...baseModule, + isReleased + }; + } + + return { + ...baseModule, + isReleased, + lessons: [] + }; + }); + + return NextResponse.json({ modules: serializedModules }); + } catch (error) { + console.error('Erro ao listar módulos de Bastidores da Bolsa:', error); + return NextResponse.json({ error: 'Erro ao listar módulos' }, { status: 500 }); + } +} + +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const isAdmin = user.plan === 'ADMIN'; + + if (!isAdmin) { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, highlight, releaseDate } = body as { + title?: unknown; + highlight?: unknown; + releaseDate?: unknown; + }; + + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título é obrigatório' }, { status: 400 }); + } + + if (typeof releaseDate !== 'string' || !releaseDate.trim()) { + return NextResponse.json({ error: 'Data de liberação é obrigatória' }, { status: 400 }); + } + + const parsedReleaseDate = new Date(releaseDate); + + if (Number.isNaN(parsedReleaseDate.getTime())) { + return NextResponse.json({ error: 'Data de liberação inválida' }, { status: 400 }); + } + + const trimmedTitle = title.trim(); + const trimmedHighlight = typeof highlight === 'string' ? highlight.trim() : undefined; + + const sortOrderAggregate = await prisma.bastidoresDaBolsaModule.aggregate({ + _max: { sortOrder: true } + }); + + const nextSortOrder = (sortOrderAggregate._max.sortOrder ?? 0) + 1; + + const module = await prisma.bastidoresDaBolsaModule.create({ + data: { + title: trimmedTitle, + highlight: trimmedHighlight && trimmedHighlight.length > 0 ? trimmedHighlight : null, + releaseDate: parsedReleaseDate, + sortOrder: nextSortOrder + }, + include: { + lessons: { + orderBy: [ + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ], + include: { + materials: { + orderBy: { + createdAt: 'asc' + } + } + } + } + } + }); + + return NextResponse.json({ module: serializeModule(module) }, { status: 201 }); + } catch (error) { + console.error('Erro ao criar módulo de Bastidores da Bolsa:', error); + return NextResponse.json({ error: 'Erro ao criar módulo' }, { status: 500 }); + } +} diff --git a/src/app/api/bastidores-da-bolsa/utils.ts b/src/app/api/bastidores-da-bolsa/utils.ts new file mode 100644 index 000000000..58aa8425e --- /dev/null +++ b/src/app/api/bastidores-da-bolsa/utils.ts @@ -0,0 +1,61 @@ +import { + BastidoresDaBolsaLesson, + BastidoresDaBolsaLessonMaterial, + BastidoresDaBolsaLiveEvent, + BastidoresDaBolsaModule +} from '@prisma/client'; + +type ModuleWithRelations = BastidoresDaBolsaModule & { + lessons: (BastidoresDaBolsaLesson & { materials: BastidoresDaBolsaLessonMaterial[] })[]; +}; + +type LessonWithMaterials = BastidoresDaBolsaLesson & { + materials: BastidoresDaBolsaLessonMaterial[]; +}; + +export function serializeMaterial(material: BastidoresDaBolsaLessonMaterial) { + return { + id: material.id, + title: material.title, + fileUrl: material.fileUrl, + createdAt: material.createdAt.toISOString(), + updatedAt: material.updatedAt.toISOString() + }; +} + +export function serializeLesson(lesson: LessonWithMaterials) { + return { + id: lesson.id, + title: lesson.title, + duration: lesson.duration, + description: lesson.description ?? undefined, + videoUrl: lesson.videoUrl ?? undefined, + content: lesson.content ?? undefined, + sortOrder: lesson.sortOrder ?? undefined, + materials: lesson.materials.map(serializeMaterial) + }; +} + +export function serializeModule(module: ModuleWithRelations) { + return { + id: module.id, + title: module.title, + highlight: module.highlight ?? '', + releaseDate: module.releaseDate.toISOString(), + isHidden: module.isHidden, + sortOrder: module.sortOrder ?? undefined, + lessons: module.lessons.map(serializeLesson) + }; +} + +export function serializeLiveEvent(event: BastidoresDaBolsaLiveEvent) { + return { + id: event.id, + title: event.title, + description: event.description ?? undefined, + eventDate: event.eventDate.toISOString(), + durationMinutes: event.durationMinutes, + meetingUrl: event.meetingUrl ?? undefined, + isCancelled: event.isCancelled + }; +} diff --git a/src/app/api/brapi/inflation/route.ts b/src/app/api/brapi/inflation/route.ts new file mode 100644 index 000000000..f69fd73aa --- /dev/null +++ b/src/app/api/brapi/inflation/route.ts @@ -0,0 +1,70 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { CacheService } from '@/lib/cache-service'; +import { BRAPI_CONFIG } from '@/lib/brapi-config'; +import { buildCacheHeaders } from '@/server/cache-headers'; + +export const dynamic = 'force-dynamic'; +export const runtime = 'nodejs'; +export const maxDuration = 5; + +const INFLATION_TTL_SECONDS = 3600; +const CACHE_HEADERS = buildCacheHeaders({ ttl: INFLATION_TTL_SECONDS, swr: INFLATION_TTL_SECONDS * 2 }); + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const forwardedParams = new URLSearchParams(searchParams); + forwardedParams.set('token', BRAPI_CONFIG.TOKEN); + + const cacheKey = CacheService.getCacheKey( + 'brapi', + 'inflation', + forwardedParams.toString() || 'default' + ); + + const { data, metadata } = await CacheService.withCache( + cacheKey, + async () => { + const url = `${BRAPI_CONFIG.BASE_URL}/v2/inflation?${forwardedParams.toString()}`; + const response = await fetch(url, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'User-Agent': 'MembrosFatosdaBolsa-API' + } + }); + + if (!response.ok) { + throw new Error(`Brapi HTTP ${response.status}: ${response.statusText}`); + } + + return response.json(); + }, + { + ttlSeconds: INFLATION_TTL_SECONDS, + tags: ['brapi', 'inflation'] + } + ); + + return NextResponse.json(data, { + status: 200, + headers: { + ...CACHE_HEADERS, + 'X-Cache-Hit': String(metadata.cached) + } + }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Erro desconhecido'; + + return NextResponse.json( + { error: `Falha ao buscar dados de inflação: ${message}` }, + { + status: 500, + headers: { + 'Cache-Control': 'no-store', + 'X-Cache-Hit': 'false' + } + } + ); + } +} diff --git a/src/app/api/brapi/prime-rate/route.ts b/src/app/api/brapi/prime-rate/route.ts new file mode 100644 index 000000000..4a644c116 --- /dev/null +++ b/src/app/api/brapi/prime-rate/route.ts @@ -0,0 +1,70 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { CacheService } from '@/lib/cache-service'; +import { BRAPI_CONFIG } from '@/lib/brapi-config'; +import { buildCacheHeaders } from '@/server/cache-headers'; + +export const dynamic = 'force-dynamic'; +export const runtime = 'nodejs'; +export const maxDuration = 5; + +const PRIME_RATE_TTL_SECONDS = 3600; +const CACHE_HEADERS = buildCacheHeaders({ ttl: PRIME_RATE_TTL_SECONDS, swr: PRIME_RATE_TTL_SECONDS * 2 }); + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const forwardedParams = new URLSearchParams(searchParams); + forwardedParams.set('token', BRAPI_CONFIG.TOKEN); + + const cacheKey = CacheService.getCacheKey( + 'brapi', + 'prime-rate', + forwardedParams.toString() || 'default' + ); + + const { data, metadata } = await CacheService.withCache( + cacheKey, + async () => { + const url = `${BRAPI_CONFIG.BASE_URL}/v2/prime-rate?${forwardedParams.toString()}`; + const response = await fetch(url, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'User-Agent': 'MembrosFatosdaBolsa-API' + } + }); + + if (!response.ok) { + throw new Error(`Brapi HTTP ${response.status}: ${response.statusText}`); + } + + return response.json(); + }, + { + ttlSeconds: PRIME_RATE_TTL_SECONDS, + tags: ['brapi', 'prime-rate'] + } + ); + + return NextResponse.json(data, { + status: 200, + headers: { + ...CACHE_HEADERS, + 'X-Cache-Hit': String(metadata.cached) + } + }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Erro desconhecido'; + + return NextResponse.json( + { error: `Falha ao buscar dados da taxa prime: ${message}` }, + { + status: 500, + headers: { + 'Cache-Control': 'no-store', + 'X-Cache-Hit': 'false' + } + } + ); + } +} diff --git a/src/app/api/brapi/quote/list/route.ts b/src/app/api/brapi/quote/list/route.ts new file mode 100644 index 000000000..ff387aba7 --- /dev/null +++ b/src/app/api/brapi/quote/list/route.ts @@ -0,0 +1,78 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { CacheService } from '@/lib/cache-service'; +import { BRAPI_CONFIG } from '@/lib/brapi-config'; +import { buildCacheHeaders } from '@/server/cache-headers'; + +export const dynamic = 'force-dynamic'; +export const runtime = 'nodejs'; +export const maxDuration = 5; + +const LIST_TTL_SECONDS = 3600; +const CACHE_HEADERS = buildCacheHeaders({ ttl: LIST_TTL_SECONDS, swr: LIST_TTL_SECONDS * 2 }); + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const search = searchParams.get('search') ?? ''; + const type = searchParams.get('type') ?? ''; + + const forwardedParams = new URLSearchParams(); + + if (search) { + forwardedParams.set('search', search); + } + + if (type) { + forwardedParams.set('type', type); + } + + forwardedParams.set('token', BRAPI_CONFIG.TOKEN); + + const cacheKey = CacheService.getCacheKey('brapi', 'quote', 'list', search || 'all', type || 'all'); + + const { data, metadata } = await CacheService.withCache( + cacheKey, + async () => { + const url = `${BRAPI_CONFIG.BASE_URL}${BRAPI_CONFIG.ENDPOINTS.QUOTE_LIST}?${forwardedParams.toString()}`; + const response = await fetch(url, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'User-Agent': 'MembrosFatosdaBolsa-API' + } + }); + + if (!response.ok) { + throw new Error(`Brapi HTTP ${response.status}: ${response.statusText}`); + } + + return response.json(); + }, + { + ttlSeconds: LIST_TTL_SECONDS, + tags: ['brapi', 'list'] + } + ); + + return NextResponse.json(data, { + status: 200, + headers: { + ...CACHE_HEADERS, + 'X-Cache-Hit': String(metadata.cached) + } + }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Erro desconhecido'; + + return NextResponse.json( + { error: `Falha ao buscar lista de ativos: ${message}` }, + { + status: 500, + headers: { + 'Cache-Control': 'no-store', + 'X-Cache-Hit': 'false' + } + } + ); + } +} diff --git a/src/app/api/brapi/quote/route.ts b/src/app/api/brapi/quote/route.ts new file mode 100644 index 000000000..c10df6d68 --- /dev/null +++ b/src/app/api/brapi/quote/route.ts @@ -0,0 +1,94 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { CacheService } from '@/lib/cache-service'; +import { BRAPI_CONFIG } from '@/lib/brapi-config'; +import { buildCacheHeaders } from '@/server/cache-headers'; + +export const dynamic = 'force-dynamic'; +export const runtime = 'nodejs'; +export const maxDuration = 5; + +const QUOTE_TTL_SECONDS = 300; +const CACHE_HEADERS = buildCacheHeaders({ ttl: QUOTE_TTL_SECONDS, swr: QUOTE_TTL_SECONDS * 2 }); + +function parseTickers(searchParams: URLSearchParams): string[] { + const tickersParam = searchParams.get('tickers') ?? searchParams.get('symbols') ?? ''; + return tickersParam + .split(',') + .map(ticker => ticker.trim()) + .filter(Boolean); +} + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const tickers = parseTickers(searchParams); + + if (tickers.length === 0) { + return NextResponse.json( + { error: 'Parâmetro "tickers" é obrigatório.' }, + { status: 400, headers: { 'Cache-Control': 'no-store', 'X-Cache-Hit': 'false' } } + ); + } + + const forwardedParams = new URLSearchParams(); + + searchParams.forEach((value, key) => { + if (key === 'tickers' || key === 'symbols') return; + forwardedParams.set(key, value); + }); + + forwardedParams.set('token', BRAPI_CONFIG.TOKEN); + + const cacheKey = CacheService.getCacheKey( + 'brapi', + 'quote', + tickers.join(','), + forwardedParams.toString() + ); + + const { data, metadata } = await CacheService.withCache( + cacheKey, + async () => { + const url = `${BRAPI_CONFIG.BASE_URL}${BRAPI_CONFIG.ENDPOINTS.QUOTE}/${tickers.join(',')}?${forwardedParams.toString()}`; + const response = await fetch(url, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'User-Agent': 'MembrosFatosdaBolsa-API' + } + }); + + if (!response.ok) { + throw new Error(`Brapi HTTP ${response.status}: ${response.statusText}`); + } + + return response.json(); + }, + { + ttlSeconds: QUOTE_TTL_SECONDS, + tags: ['brapi', 'quotes', ...tickers] + } + ); + + return NextResponse.json(data, { + status: 200, + headers: { + ...CACHE_HEADERS, + 'X-Cache-Hit': String(metadata.cached) + } + }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Erro desconhecido'; + + return NextResponse.json( + { error: `Falha ao buscar cotações: ${message}` }, + { + status: 500, + headers: { + 'Cache-Control': 'no-store', + 'X-Cache-Hit': 'false' + } + } + ); + } +} diff --git a/src/app/api/carteiras/[id]/ativos-info/route.ts b/src/app/api/carteiras/[id]/ativos-info/route.ts new file mode 100644 index 000000000..28a29eb8f --- /dev/null +++ b/src/app/api/carteiras/[id]/ativos-info/route.ts @@ -0,0 +1,238 @@ +// src/app/api/carteiras/[id]/ativos-info/route.ts + +export const dynamic = 'force-dynamic'; + +import { NextRequest, NextResponse } from 'next/server'; +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +interface RouteParams { + params: { id: string } +} + +export async function GET( + request: NextRequest, + { params }: RouteParams +) { + console.log('📊 Buscando ativos da carteira:', params.id); + + try { + const session = await auth(); + + if (!session?.user?.plan || session.user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Acesso negado' }, { status: 403 }); + } + + // 1. BUSCAR CARTEIRA + const carteira = await prisma.carteiraAnalise.findUnique({ + where: { id: params.id }, + include: { + user: { + select: { firstName: true, lastName: true, email: true } + } + } + }); + + if (!carteira) { + return NextResponse.json({ error: 'Carteira não encontrada' }, { status: 404 }); + } + + console.log('✅ Carteira encontrada:', carteira.nomeArquivo); + + // 2. EXTRAIR E DEBUGAR DADOS DO EXCEL + let dadosExcel = null; + if (carteira.dadosEstruturados) { + try { + dadosExcel = JSON.parse(carteira.dadosEstruturados); + + // DEBUG COMPLETO dos dados salvos + console.log('📋 ESTRUTURA COMPLETA dos dados salvos:', JSON.stringify(dadosExcel, null, 2)); + console.log('📋 Chaves principais:', Object.keys(dadosExcel)); + + if (dadosExcel.acoes) { + console.log('📈 AÇÕES encontradas:', dadosExcel.acoes.length); + dadosExcel.acoes.forEach((acao, idx) => { + console.log(` ${idx + 1}. ${acao.codigo} - ${acao.nome} - R$ ${acao.valorAtual}`); + }); + } else { + console.log('⌠AÇÕES: campo não existe ou está vazio'); + } + + if (dadosExcel.fiis) { + console.log('ðŸ˜ï¸ FIIs encontrados:', dadosExcel.fiis.length); + dadosExcel.fiis.forEach((fii, idx) => { + console.log(` ${idx + 1}. ${fii.codigo} - ${fii.nome} - R$ ${fii.valorAtual}`); + }); + } else { + console.log('⌠FIIs: campo não existe ou está vazio'); + } + + if (dadosExcel.exterior) { + console.log('🌎 EXTERIOR encontrados:', dadosExcel.exterior.length); + } + + if (dadosExcel.rendaFixa) { + console.log('💰 RENDA FIXA encontrados:', dadosExcel.rendaFixa.length); + } + + if (dadosExcel.cripto) { + console.log('â‚¿ CRIPTO encontrados:', dadosExcel.cripto.length); + } + + } catch (error) { + console.error('⌠Erro ao parsear dados:', error); + return NextResponse.json({ error: 'Dados corrompidos' }, { status: 500 }); + } + } + + if (!dadosExcel) { + return NextResponse.json({ error: 'Dados do Excel não encontrados' }, { status: 404 }); + } + + // 3. CONSOLIDAR TODOS OS ATIVOS (não apenas ações e FIIs) + const ativosDoExcel = []; + + // Ações + if (dadosExcel.acoes && Array.isArray(dadosExcel.acoes)) { + dadosExcel.acoes.forEach(acao => { + ativosDoExcel.push({ + codigo: acao.codigo, + nome: acao.nome, + tipo: 'ACAO', + quantidade: acao.quantidade, + valorAtual: acao.valorAtual, + pesoCarteira: acao.pesoCarteira + }); + }); + } + + // FIIs + if (dadosExcel.fiis && Array.isArray(dadosExcel.fiis)) { + dadosExcel.fiis.forEach(fii => { + ativosDoExcel.push({ + codigo: fii.codigo, + nome: fii.nome, + tipo: 'FII', + quantidade: fii.quantidade, + valorAtual: fii.valorAtual, + pesoCarteira: fii.pesoCarteira + }); + }); + } + + // Renda Fixa + if (dadosExcel.rendaFixa && Array.isArray(dadosExcel.rendaFixa)) { + dadosExcel.rendaFixa.forEach(rf => { + ativosDoExcel.push({ + codigo: rf.produto, + nome: rf.banco, + tipo: 'RENDA_FIXA', + quantidade: 1, // Renda fixa não tem quantidade + valorAtual: rf.valorAtual, + pesoCarteira: rf.pesoCarteira, + vencimento: rf.vencimento + }); + }); + } + + // Exterior + if (dadosExcel.exterior && Array.isArray(dadosExcel.exterior)) { + dadosExcel.exterior.forEach(ext => { + ativosDoExcel.push({ + codigo: ext.codigo, + nome: ext.nome, + tipo: 'EXTERIOR', + quantidade: ext.quantidade, + valorAtual: ext.valorAtual, + pesoCarteira: ext.pesoCarteira + }); + }); + } + + // Cripto + if (dadosExcel.cripto && Array.isArray(dadosExcel.cripto)) { + dadosExcel.cripto.forEach(cripto => { + ativosDoExcel.push({ + codigo: cripto.criptomoeda, + nome: cripto.exchange, + tipo: 'CRIPTO', + quantidade: cripto.quantidade, + valorAtual: cripto.valorAtual, + pesoCarteira: cripto.pesoCarteira + }); + }); + } + + console.log('📊 Ativos extraídos:', ativosDoExcel.length); + + // 4. BUSCAR INFORMAÇÕES NO SISTEMA + const codigosParaBuscar = ativosDoExcel.map(ativo => ativo.codigo); + + const informacoesDoSistema = await prisma.ativoInformacao.findMany({ + where: { + codigo: { in: codigosParaBuscar } + } + }); + + console.log('📋 Informações encontradas:', informacoesDoSistema.length); + + // 5. COMBINAR DADOS + const ativosCombinados = ativosDoExcel.map(ativoExcel => { + const infoSistema = informacoesDoSistema.find(info => info.codigo === ativoExcel.codigo); + + return { + codigo: ativoExcel.codigo, + nome: ativoExcel.nome, + tipo: ativoExcel.tipo, + quantidade: ativoExcel.quantidade, + valorAtual: ativoExcel.valorAtual, + pesoCarteira: ativoExcel.pesoCarteira, + valorTotal: ativoExcel.valorAtual, + temInformacao: !!infoSistema, + informacao: infoSistema ? { + qualidade: infoSistema.qualidade, + risco: infoSistema.risco, + recomendacao: infoSistema.recomendacao, + setor: infoSistema.setor, + nome: infoSistema.nome, + fundamentosResumo: infoSistema.fundamentosResumo, + pontosFortes: infoSistema.pontosFortes ? JSON.parse(infoSistema.pontosFortes) : [], + pontosFracos: infoSistema.pontosFracos ? JSON.parse(infoSistema.pontosFracos) : [], + observacoes: infoSistema.observacoes, + dividend_yield: infoSistema.dividend_yield + } : null + }; + }); + + // 6. CALCULAR ESTATÃSTICAS + const totalAtivos = ativosCombinados.length; + const ativosComInfo = ativosCombinados.filter(a => a.temInformacao).length; + const cobertura = totalAtivos > 0 ? Math.round((ativosComInfo / totalAtivos) * 100) : 0; + + console.log('✅ Dados combinados:', { totalAtivos, ativosComInfo, cobertura }); + + return NextResponse.json({ + success: true, + ativos: ativosCombinados, + resumo: { + totalAtivos, + valorTotal: carteira.valorTotal, + comInformacao: ativosComInfo, + semInformacao: totalAtivos - ativosComInfo, + cobertura + }, + carteira: { + id: carteira.id, + nomeArquivo: carteira.nomeArquivo, + usuario: carteira.user + } + }); + + } catch (error) { + console.error('Erro ao buscar ativos:', error); + return NextResponse.json({ + error: 'Erro interno do servidor', + details: error.message + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/carteiras/[id]/download-pdf/route.js b/src/app/api/carteiras/[id]/download-pdf/route.js new file mode 100644 index 000000000..08cf355a0 --- /dev/null +++ b/src/app/api/carteiras/[id]/download-pdf/route.js @@ -0,0 +1,758 @@ +// src/app/api/carteiras/[id]/download-pdf/route.js +import puppeteer from 'puppeteer'; +import path from 'path'; +import fs from 'fs'; +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +export async function GET(request, { params }) { + const { id } = params; + + try { + // Verificar autenticação + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Não autorizado' }, { status: 401 }); + } + + // Buscar dados da carteira REAIS do Prisma + const carteira = await buscarCarteiraPorId(id, session.user.id); + + if (!carteira) { + return NextResponse.json({ error: 'Carteira não encontrada' }, { status: 404 }); + } + + // Verificar se a carteira pertence ao usuário + if (carteira.userId !== session.user.id) { + return NextResponse.json({ error: 'Não autorizado' }, { status: 403 }); + } + + // Gerar PDF + const pdfBuffer = await gerarPDF(carteira); + + // Retornar PDF + return new NextResponse(pdfBuffer, { + status: 200, + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment; filename="Analise-Carteira-${carteira.nomeArquivo}-${new Date().toISOString().split('T')[0]}.pdf"`, + }, + }); + + } catch (error) { + console.error('Erro ao gerar PDF:', error); + return NextResponse.json({ error: 'Erro interno do servidor' }, { status: 500 }); + } +} + +async function gerarPDF(carteira) { + let browser; + + try { + // Configurar Puppeteer + browser = await puppeteer.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox'] + }); + + const page = await browser.newPage(); + + // Configurar viewport + await page.setViewport({ width: 1200, height: 800 }); + + // Gerar HTML do relatório + const htmlContent = gerarHTMLRelatorio(carteira); + + // Carregar HTML + await page.setContent(htmlContent, { waitUntil: 'networkidle0' }); + + // Gerar PDF + const pdfBuffer = await page.pdf({ + format: 'A4', + printBackground: true, + margin: { + top: '20mm', + right: '15mm', + bottom: '20mm', + left: '15mm' + } + }); + + return pdfBuffer; + + } finally { + if (browser) { + await browser.close(); + } + } +} + +function gerarHTMLRelatorio(carteira) { + const logoPath = path.join(process.cwd(), 'public/assets/avatar.png'); + const logoBase64 = fs.existsSync(logoPath) + ? `data:image/png;base64,${fs.readFileSync(logoPath, 'base64')}` + : ''; + + return ` + + + + + + Análise de Carteira - ${carteira.nomeArquivo} + + + + +
+
+ ${logoBase64 ? `` : ''} +
+

Fatos da Bolsa

+

Análise Profissional de Carteiras

+
+
+
+

Relatório de Análise

+

Gerado em: ${new Date().toLocaleDateString('pt-BR')}

+

Arquivo: ${carteira.nomeArquivo}

+
+
+ + +
+

📊 Resumo Executivo

+
+
+
+
Valor Total
+
+ ${carteira.valorTotal ? `R$ ${carteira.valorTotal.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}` : 'N/A'} +
+
+
+
Quantidade de Ativos
+
${carteira.quantidadeAtivos || carteira.ativos?.length || 'N/A'}
+
+
+
Data de Envio
+
${new Date(carteira.dataEnvio).toLocaleDateString('pt-BR')}
+
+ ${carteira.pontuacao ? ` +
+
Pontuação Geral
+
${carteira.pontuacao.toFixed(1)}/10
+
+ ` : ''} +
+ + + ${carteira.cliente ? ` +
+
CLIENTE
+
${carteira.cliente.name}
+
${carteira.cliente.email}
+
+ ` : ''} +
+
+ + + ${carteira.dadosEstruturados?.avaliacoes ? ` +
+

📈 Avaliação Detalhada

+
+
+
Qualidade dos Ativos
+
${carteira.dadosEstruturados.avaliacoes.qualidade}%
+
+
+
+
+
+
Diversificação
+
${carteira.dadosEstruturados.avaliacoes.diversificacao}%
+
+
+
+
+
+
Adaptação ao Perfil
+
${carteira.dadosEstruturados.avaliacoes.adaptacao}%
+
+
+
+
+
+
+ ` : ''} + + + ${carteira.feedback ? ` +
+

💬 Análise Geral da Carteira

+ +
+ ` : ''} + + + ${carteira.dadosEstruturados?.recomendacoesDetalhadas?.length > 0 ? ` +
+

💡 Recomendações Personalizadas

+ ${carteira.dadosEstruturados.recomendacoesDetalhadas.map(rec => ` +
+
+ + ${(rec.prioridade || 'baixa').charAt(0).toUpperCase() + (rec.prioridade || 'baixa').slice(1)} Prioridade + + ${rec.categoria || ''} +
+
${rec.titulo || ''}
+
${rec.descricao || ''}
+ ${rec.impacto ? ` +
+ 💥 Impacto Esperado: + ${rec.impacto} +
+ ` : ''} +
+ `).join('')} +
+ ` : ''} + + + ${carteira.dadosEstruturados?.ativosAnalise?.length > 0 ? ` +
+

🢠Análise Individual de Ativos

+ ${carteira.dadosEstruturados.ativosAnalise.map(ativo => ` +
+
+
${ativo.codigo || ''}
+
+
${'â­'.repeat(Math.floor((ativo.nota || 0) / 2))}
+
${ativo.nota || 0}/10
+
+
+
${ativo.comentario || ''}
+
+ `).join('')} +
+ ` : ''} + + + ${carteira.estatisticas?.distribuicaoTipo ? ` +
+

📈 Distribuição da Carteira

+
+
+ ${carteira.estatisticas.distribuicaoTipo.map(dist => ` +
+
${dist.tipo}
+
${dist.percentual.toFixed(1)}%
+
+ R$ ${dist.valor.toLocaleString('pt-BR', { minimumFractionDigits: 2 })} +
+
+ `).join('')} +
+
+
+ ` : ''} + + + ${carteira.ativos && carteira.ativos.length > 0 ? ` +
+

💼 Ativos da Carteira

+
+ + + + + + + + + + + + ${carteira.ativos.slice(0, 20).map((ativo, index) => ` + + + + + + + + `).join('')} + ${carteira.ativos.length > 20 ? ` + + + + ` : ''} + +
CódigoTipoQuantidadePreço MédioValor Total
${ativo.codigo}${formatarTipoAtivo(ativo.tipo)}${ativo.quantidade.toLocaleString('pt-BR', { minimumFractionDigits: 0 })}R$ ${ativo.precoMedio.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}R$ ${ativo.valorTotal.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
+ ... e mais ${carteira.ativos.length - 20} ativos +
+
+
+ ` : ''} + + +
+

Fatos da Bolsa - Análise Profissional de Carteiras

+

Este relatório foi gerado automaticamente em ${new Date().toLocaleDateString('pt-BR')} às ${new Date().toLocaleTimeString('pt-BR')}

+

Para dúvidas ou suporte, entre em contato conosco

+
+ + + `; +} + +// Função REAL - busca dados do Prisma +async function buscarCarteiraPorId(id, userId) { + try { + const carteira = await prisma.carteiraAnalise.findUnique({ + where: { + id: id, + userId: userId // Garantir que é do usuário correto + }, + include: { + analista: { + select: { + firstName: true, + lastName: true + } + }, + ativos: true, + user: { + select: { + firstName: true, + lastName: true, + email: true + } + } + } + }); + + if (!carteira) return null; + + // Processar dados estruturados (se existirem) + let dadosEstruturados = null; + if (carteira.dadosEstruturados) { + try { + dadosEstruturados = JSON.parse(carteira.dadosEstruturados); + } catch (e) { + console.error('Erro ao parsear dadosEstruturados:', e); + } + } + + // Processar questionário (se existir) + let questionarioData = null; + if (carteira.questionario) { + try { + questionarioData = JSON.parse(carteira.questionario); + } catch (e) { + console.error('Erro ao parsear questionario:', e); + } + } + + // Calcular estatísticas básicas dos ativos + const estatisticas = calcularEstatisticas(carteira.ativos); + + // Retornar dados formatados + return { + id: carteira.id, + nomeArquivo: carteira.nomeArquivo, + dataEnvio: carteira.dataEnvio, + dataAnalise: carteira.dataAnalise, + valorTotal: carteira.valorTotal, + quantidadeAtivos: carteira.quantidadeAtivos, + status: carteira.status, + pontuacao: carteira.pontuacao, + feedback: carteira.feedback, + userId: carteira.userId, + + // Analista + analista: carteira.analista ? { + name: `${carteira.analista.firstName} ${carteira.analista.lastName}` + } : null, + + // Cliente + cliente: { + name: `${carteira.user.firstName} ${carteira.user.lastName}`, + email: carteira.user.email + }, + + // Dados estruturados da análise + dadosEstruturados: dadosEstruturados ? { + avaliacoes: { + qualidade: carteira.avaliacaoQualidade || dadosEstruturados.avaliacoes?.qualidade || 0, + diversificacao: carteira.avaliacaoDiversificacao || dadosEstruturados.avaliacoes?.diversificacao || 0, + adaptacao: carteira.avaliacaoAdaptacao || dadosEstruturados.avaliacoes?.adaptacao || 0 + }, + recomendacoesDetalhadas: dadosEstruturados.recomendacoesDetalhadas || [], + ativosAnalise: dadosEstruturados.ativosAnalise || [] + } : null, + + // Estatísticas dos ativos + estatisticas, + + // Ativos da carteira + ativos: carteira.ativos, + + // Questionário + questionario: questionarioData + }; + + } catch (error) { + console.error('Erro ao buscar carteira:', error); + throw error; + } +} + +// Função para calcular estatísticas dos ativos +function calcularEstatisticas(ativos) { + if (!ativos || ativos.length === 0) return null; + + const totalValue = ativos.reduce((sum, ativo) => sum + ativo.valorTotal, 0); + + // Agrupar por tipo + const distribuicaoTipo = ativos.reduce((acc, ativo) => { + const tipo = ativo.tipo; + if (!acc[tipo]) { + acc[tipo] = { valorTotal: 0, quantidade: 0 }; + } + acc[tipo].valorTotal += ativo.valorTotal; + acc[tipo].quantidade += 1; + return acc; + }, {}); + + // Converter para array com percentuais + const distribuicaoArray = Object.entries(distribuicaoTipo).map(([tipo, data]) => ({ + tipo: formatarTipoAtivo(tipo), + valor: data.valorTotal, + percentual: (data.valorTotal / totalValue) * 100, + quantidade: data.quantidade + })); + + return { + valorTotal: totalValue, + quantidadeAtivos: ativos.length, + distribuicaoTipo: distribuicaoArray + }; +} + +// Função para formatar tipos de ativos +function formatarTipoAtivo(tipo) { + const tipos = { + 'ACAO': 'Ações', + 'FII': 'FIIs', + 'ETF': 'ETFs', + 'OUTRO': 'Outros' + }; + return tipos[tipo] || tipo; +} \ No newline at end of file diff --git a/src/app/api/carteiras/[id]/route.ts b/src/app/api/carteiras/[id]/route.ts new file mode 100644 index 000000000..19ef8d8bd --- /dev/null +++ b/src/app/api/carteiras/[id]/route.ts @@ -0,0 +1,156 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; +import { verifyToken } from '@/utils/auth'; + +// Mesma função de autenticação que funciona na API de ativos +async function authenticateAdmin(request: NextRequest) { + console.log('Iniciando autenticação para delete carteira...'); + + let session = await auth(); + console.log('Auth() resultado:', session ? 'SUCESSO' : 'FALHOU'); + + if (!session) { + console.log('Tentando autenticação alternativa...'); + + const cookieHeader = request.headers.get('cookie'); + if (cookieHeader) { + const authTokenMatch = cookieHeader.match(/auth-token=([^;]+)/); + if (authTokenMatch) { + const token = authTokenMatch[1]; + + try { + const decoded = verifyToken(token); + + const user = await prisma.user.findUnique({ + where: { id: decoded.userId }, + select: { + id: true, + firstName: true, + lastName: true, + email: true, + plan: true, + status: true, + expirationDate: true, + customPermissions: true + } + }); + + if (user && user.status === 'ACTIVE') { + const isAdmin = user.plan === 'ADMIN'; + session = { + user: { + id: user.id, + role: isAdmin ? 'ADMIN' : 'USER', + name: `${user.firstName} ${user.lastName}`, + email: user.email, + plan: user.plan, + canUploadPortfolio: true, + hasVipAccess: true + } + }; + console.log('✅ Autenticação alternativa bem-sucedida'); + } + } catch (error) { + console.log('⌠Erro JWT:', (error as Error).message); + } + } + } + } + + if (!session) { + console.log('Usando fallback de desenvolvimento...'); + const adminUser = await prisma.user.findFirst({ + where: { + plan: 'ADMIN', + status: 'ACTIVE' + } + }); + + if (adminUser) { + session = { + user: { + id: adminUser.id, + role: 'ADMIN', + name: `${adminUser.firstName} ${adminUser.lastName}`, + email: adminUser.email, + plan: adminUser.plan, + canUploadPortfolio: true, + hasVipAccess: true + } + }; + console.log('✅ Fallback admin ativado para desenvolvimento'); + } + } + + return session; +} + +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + console.log('DELETE carteira ID:', params.id); + + const session = await authenticateAdmin(request); + + if (!session?.user || (session.user.plan !== 'ADMIN' && session.user.role !== 'ADMIN')) { + console.log('⌠Acesso negado no DELETE carteira'); + return NextResponse.json({ error: 'Acesso negado' }, { status: 403 }); + } + + console.log('✅ Autenticação OK para DELETE carteira'); + + // Buscar carteira com dados do usuário + const carteira = await prisma.carteiraAnalise.findUnique({ + where: { id: params.id }, + include: { + user: { + select: { + firstName: true, + lastName: true, + email: true + } + }, + ativos: true // Incluir ativos para contagem + } + }); + + if (!carteira) { + return NextResponse.json({ + error: 'Carteira não encontrada' + }, { status: 404 }); + } + + console.log(`Carteira encontrada: ${carteira.nomeArquivo} (${carteira.ativos.length} ativos)`); + + // Como tem onDelete: Cascade, só precisa deletar a carteira + // Os ativos serão deletados automaticamente + await prisma.carteiraAnalise.delete({ + where: { id: params.id } + }); + + console.log(`Carteira ${params.id} deletada por ${session.user.email}`); + + return NextResponse.json({ + success: true, + message: `Carteira de ${carteira.user.firstName} ${carteira.user.lastName} removida com sucesso`, + clienteEmail: carteira.user.email, + details: { + carteiraId: params.id, + ativosRemovidos: carteira.ativos.length, + adminEmail: session.user.email + } + }); + + } catch (error) { + console.error('Erro ao deletar carteira:', error); + + return NextResponse.json({ + error: 'Erro interno do servidor', + details: process.env.NODE_ENV === 'development' ? + (error instanceof Error ? error.message : String(error)) : undefined + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/carteiras/minhas/route.ts b/src/app/api/carteiras/minhas/route.ts new file mode 100644 index 000000000..40bfb2db9 --- /dev/null +++ b/src/app/api/carteiras/minhas/route.ts @@ -0,0 +1,163 @@ +export const dynamic = 'force-dynamic'; + +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; // ADICIONAR ESTA LINHA +import { CacheService } from '@/lib/redis'; +import { logApiMetric } from '@/lib/metrics'; + +export const maxDuration = 5; + +export async function GET(request: NextRequest) { + const startedAt = Date.now(); + + try { + // CORRIGIDO: Usar a função auth() real + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ + error: 'Usuário não autenticado' + }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const page = parseInt(searchParams.get('page') || '1'); + const limit = parseInt(searchParams.get('limit') || '10'); + const status = searchParams.get('status'); + const skip = (page - 1) * limit; + + // Filtros + const where: any = { + userId: session.user.id // CORRIGIDO: Usar session.user.id + }; + + if (status && status !== 'todos') { + where.status = status.toUpperCase(); + } + + const cacheKey = CacheService.getCacheKey( + 'minhas-carteiras', + session.user.id, + `page:${page}`, + `limit:${limit}`, + status || 'todos' + ); + + const { data, metadata } = await CacheService.withCache( + cacheKey, + async () => { + const [carteiras, total] = await Promise.all([ + prisma.carteiraAnalise.findMany({ + where, + include: { + ativos: { + orderBy: { + valorTotal: 'desc' + } + }, + analista: { + select: { + id: true, + firstName: true, + lastName: true + } + } + }, + orderBy: { + createdAt: 'desc' + }, + skip, + take: limit + }), + prisma.carteiraAnalise.count({ where }) + ]); + + + // Processar carteiras + const carteirasProcessadas = carteiras.map(carteira => { + const totalCarteira = carteira.ativos.reduce((sum, ativo) => sum + ativo.valorTotal, 0); + + // Distribuição por tipo + const distribuicaoTipo = carteira.ativos.reduce((acc, ativo) => { + acc[ativo.tipo] = (acc[ativo.tipo] || 0) + ativo.valorTotal; + return acc; + }, {} as Record); + + const distribuicaoPercentual = Object.entries(distribuicaoTipo).map(([tipo, valor]) => ({ + tipo, + valor, + percentual: totalCarteira > 0 ? (valor / totalCarteira) * 100 : 0 + })); + + // PARSE DOS DADOS ESTRUTURADOS + let dadosEstruturados = null; + if (carteira.dadosEstruturados) { + try { + dadosEstruturados = typeof carteira.dadosEstruturados === 'string' + ? JSON.parse(carteira.dadosEstruturados) + : carteira.dadosEstruturados; + } catch (error) { + console.error('Erro ao parsear dadosEstruturados:', error); + } + } + + return { + id: carteira.id, + nomeArquivo: carteira.nomeArquivo, + status: carteira.status.toLowerCase(), + dataEnvio: carteira.dataEnvio, + dataAnalise: carteira.dataAnalise, + valorTotal: carteira.valorTotal, + quantidadeAtivos: carteira.quantidadeAtivos, + feedback: carteira.feedback, + recomendacoes: carteira.recomendacoes ? JSON.parse(carteira.recomendacoes) : [], + pontuacao: carteira.pontuacao, + // INCLUIR DADOS ESTRUTURADOS + dadosEstruturados: dadosEstruturados, + // CAMPOS INDIVIDUAIS PARA COMPATIBILIDADE + avaliacaoQualidade: dadosEstruturados?.avaliacoes?.qualidade, + avaliacaoDiversificacao: dadosEstruturados?.avaliacoes?.diversificacao, + avaliacaoAdaptacao: dadosEstruturados?.avaliacoes?.adaptacao, + analista: carteira.analista ? { + id: carteira.analista.id, + name: `${carteira.analista.firstName} ${carteira.analista.lastName}` + } : null, + ativos: carteira.ativos, + estatisticas: { + distribuicaoTipo: distribuicaoPercentual + } + }; + }); + + return { + carteiras: carteirasProcessadas, + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit) + } + }; + }, + { ttlSeconds: 600, tags: [`carteiras:${session.user.id}`] } + ); + + logApiMetric({ + route: '/api/carteiras/minhas', + cacheHit: metadata.cached, + cacheBackend: metadata.backend, + durationMs: Date.now() - startedAt, + status: 200, + extra: { status, page, limit }, + }); + + return NextResponse.json(data); + + } catch (error) { + console.error('Erro ao buscar carteiras:', error); + return NextResponse.json({ + error: 'Erro interno do servidor' + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/carteiras/modelo-excel/route.ts b/src/app/api/carteiras/modelo-excel/route.ts new file mode 100644 index 000000000..3c13c6101 --- /dev/null +++ b/src/app/api/carteiras/modelo-excel/route.ts @@ -0,0 +1,34 @@ +// src/app/api/carteiras/modelo-excel/route.ts + +export const dynamic = 'force-dynamic'; +export const runtime = 'nodejs'; + +import { NextResponse } from 'next/server'; +import { promises as fs } from 'fs'; +import { join } from 'path'; + +const TEMPLATE_PATH = join(process.cwd(), 'public', 'modelos', 'analise-de-carteira.xlsx'); + +export async function GET() { + try { + const fileBuffer = await fs.readFile(TEMPLATE_PATH); + + return new NextResponse(fileBuffer, { + headers: { + 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'Content-Disposition': 'attachment; filename="Modelo-Carteira-Completo.xlsx"' + } + }); + } catch (error) { + console.error('Erro ao carregar modelo Excel:', error); + const details = error instanceof Error ? error.message : 'Erro desconhecido'; + + return NextResponse.json( + { + error: 'Erro ao carregar modelo Excel', + details + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/carteiras/route.ts b/src/app/api/carteiras/route.ts new file mode 100644 index 000000000..6c0763dce --- /dev/null +++ b/src/app/api/carteiras/route.ts @@ -0,0 +1,172 @@ +// src/app/api/carteiras/minhas/route.ts + +export const dynamic = 'force-dynamic'; + +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; // ADICIONAR ESTA LINHA +import { CacheService } from '@/lib/redis'; +import { logApiMetric } from '@/lib/metrics'; + +export const maxDuration = 5; + +export async function GET(request: NextRequest) { + const startedAt = Date.now(); + + try { + // CORRIGIDO: Usar a função auth() real + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ + error: 'Usuário não autenticado' + }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const page = parseInt(searchParams.get('page') || '1'); + const limit = parseInt(searchParams.get('limit') || '10'); + const status = searchParams.get('status'); + + const skip = (page - 1) * limit; + + // Filtros + const where: any = { + userId: session.user.id // CORRIGIDO: Usar session.user.id + }; + + if (status && status !== 'todos') { + where.status = status.toUpperCase(); + } + + const cacheKey = CacheService.getCacheKey( + 'carteiras', + session.user.id, + `page:${page}`, + `limit:${limit}`, + status || 'todos' + ); + + const { data, metadata } = await CacheService.withCache( + cacheKey, + async () => { + const [carteiras, total] = await Promise.all([ + prisma.carteiraAnalise.findMany({ + where, + include: { + ativos: { + orderBy: { + valorTotal: 'desc' + } + }, + analista: { + select: { + id: true, + name: true + } + }, + questionario: true + }, + orderBy: { + createdAt: 'desc' + }, + skip, + take: limit + }), + prisma.carteiraAnalise.count({ where }) + ]); + + const carteirasComEstatisticas = carteiras.map(carteira => { + const estatisticasAtivos = calcularEstatisticasAtivos(carteira.ativos); + + return { + id: carteira.id, + nomeArquivo: carteira.nomeArquivo, + status: carteira.status.toLowerCase(), + dataEnvio: carteira.dataEnvio, + dataAnalise: carteira.dataAnalise, + valorTotal: carteira.valorTotal, + quantidadeAtivos: carteira.quantidadeAtivos, + feedback: carteira.feedback, + recomendacoes: carteira.recomendacoes, + pontuacao: carteira.pontuacao, + riscoBeneficio: carteira.riscoBeneficio, + diversificacao: carteira.diversificacao, + analista: carteira.analista, + questionario: carteira.questionario, + ativos: carteira.ativos, + estatisticas: estatisticasAtivos + }; + }); + + return { + carteiras: carteirasComEstatisticas, + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit) + } + }; + }, + { ttlSeconds: 600, tags: [`carteiras:${session.user.id}`] } + ); + + logApiMetric({ + route: '/api/carteiras', + cacheHit: metadata.cached, + cacheBackend: metadata.backend, + durationMs: Date.now() - startedAt, + status: 200, + extra: { status, page, limit }, + }); + + return NextResponse.json(data); + + } catch (error) { + console.error('Erro ao buscar carteiras:', error); + return NextResponse.json({ + error: 'Erro interno do servidor' + }, { status: 500 }); + } +} + +function calcularEstatisticasAtivos(ativos: any[]) { + if (!ativos.length) return null; + + const totalCarteira = ativos.reduce((sum, ativo) => sum + ativo.valorTotal, 0); + + // Distribuição por tipo + const distribuicaoTipo = ativos.reduce((acc, ativo) => { + acc[ativo.tipo] = (acc[ativo.tipo] || 0) + ativo.valorTotal; + return acc; + }, {} as Record); + + // Converter para percentual + const distribuicaoPercentual = Object.entries(distribuicaoTipo).map(([tipo, valor]) => ({ + tipo, + valor, + percentual: (valor / totalCarteira) * 100 + })); + + // Top 5 ativos + const topAtivos = ativos + .sort((a, b) => b.valorTotal - a.valorTotal) + .slice(0, 5) + .map(ativo => ({ + codigo: ativo.codigo, + valorTotal: ativo.valorTotal, + percentual: (ativo.valorTotal / totalCarteira) * 100, + tipo: ativo.tipo + })); + + // Concentração (soma dos top 5) + const concentracaoTop5 = topAtivos.reduce((sum, ativo) => sum + ativo.percentual, 0); + + return { + distribuicaoTipo: distribuicaoPercentual, + topAtivos, + concentracaoTop5, + numeroSetores: new Set(ativos.filter(a => a.setor).map(a => a.setor)).size + }; +} \ No newline at end of file diff --git a/src/app/api/carteiras/status-usuario/route.ts b/src/app/api/carteiras/status-usuario/route.ts new file mode 100644 index 000000000..d56e1baab --- /dev/null +++ b/src/app/api/carteiras/status-usuario/route.ts @@ -0,0 +1,62 @@ +// src/app/api/carteiras/status-usuario/route.ts +export const dynamic = 'force-dynamic'; +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +export async function GET(request: NextRequest) { + try { + // CORRIGIDO: Usar a função auth() real + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ + error: 'Usuário não autenticado' + }, { status: 401 }); + } + + console.log('VERIFICANDO: Status para usuário:', session.user.id); + + // Verificar se o usuário já enviou uma carteira + const carteiraExistente = await prisma.carteiraAnalise.findFirst({ + where: { + userId: session.user.id + }, + select: { + id: true, + status: true, + dataEnvio: true, + nomeArquivo: true, + createdAt: true + }, + orderBy: { + createdAt: 'desc' // Pegar a mais recente + } + }); + + const jaEnviou = !!carteiraExistente; + + console.log('RESULTADO: Verificação de status:', { + userId: session.user.id, + jaEnviou, + carteira: carteiraExistente + }); + + return NextResponse.json({ + jaEnviou, + carteira: carteiraExistente ? { + id: carteiraExistente.id, + status: carteiraExistente.status.toLowerCase(), + dataEnvio: carteiraExistente.dataEnvio, + nomeArquivo: carteiraExistente.nomeArquivo + } : null + }); + + } catch (error) { + console.error('ERRO: Erro ao verificar status do usuário:', error); + return NextResponse.json( + { error: 'Erro interno do servidor' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/carteiras/upload-validado/parseNumeroFlex.test.ts b/src/app/api/carteiras/upload-validado/parseNumeroFlex.test.ts new file mode 100644 index 000000000..5555b83e2 --- /dev/null +++ b/src/app/api/carteiras/upload-validado/parseNumeroFlex.test.ts @@ -0,0 +1,23 @@ +import { parseNumeroFlex } from './parseNumeroFlex'; + +describe('parseNumeroFlex', () => { + it('interprets thousand separators using dots', () => { + expect(parseNumeroFlex('1.234')).toBe(1234); + }); + + it('interprets thousand separators with trailing zeros', () => { + expect(parseNumeroFlex('2.000')).toBe(2000); + }); + + it('preserves decimal commas when both separators exist', () => { + expect(parseNumeroFlex('1.234,56')).toBeCloseTo(1234.56); + }); + + it('keeps decimal dots when no thousands separators are present', () => { + expect(parseNumeroFlex('1234.56')).toBeCloseTo(1234.56); + }); + + it('keeps decimal numbers below one untouched', () => { + expect(parseNumeroFlex('0.123')).toBeCloseTo(0.123); + }); +}); diff --git a/src/app/api/carteiras/upload-validado/parseNumeroFlex.ts b/src/app/api/carteiras/upload-validado/parseNumeroFlex.ts new file mode 100644 index 000000000..1a1d5e270 --- /dev/null +++ b/src/app/api/carteiras/upload-validado/parseNumeroFlex.ts @@ -0,0 +1,51 @@ +export function parseNumeroFlex(valor: unknown): number { + if (typeof valor === 'number') { + return Number.isFinite(valor) ? valor : 0; + } + + if (typeof valor === 'string') { + const trimmed = valor.trim(); + if (!trimmed) { + return 0; + } + + let somenteNumeros = trimmed + .replace(/[^0-9,.-]/g, '') + .replace(/(?!^)-/g, ''); + + if (!somenteNumeros) { + return 0; + } + + const virgulas = (somenteNumeros.match(/,/g) || []).length; + const pontos = (somenteNumeros.match(/\./g) || []).length; + + if (virgulas && pontos) { + if (somenteNumeros.lastIndexOf(',') > somenteNumeros.lastIndexOf('.')) { + somenteNumeros = somenteNumeros.replace(/\./g, '').replace(',', '.'); + } else { + somenteNumeros = somenteNumeros.replace(/,/g, ''); + } + } else if (virgulas) { + somenteNumeros = somenteNumeros.replace(/,/g, '.'); + } else if (pontos) { + const partes = somenteNumeros.split('.'); + const ultimaParte = partes[partes.length - 1]; + const primeiraParte = partes[0]; + const temMultiplosPontos = partes.length > 2; + const primeiraParteEhZero = primeiraParte.replace(/^-/, '') === '0'; + const tratarComoMilhar = + temMultiplosPontos || + (ultimaParte.length === 3 && !primeiraParteEhZero && primeiraParte.replace(/^-/, '').length <= 3); + + if (tratarComoMilhar) { + somenteNumeros = somenteNumeros.replace(/\./g, ''); + } + } + + const numero = parseFloat(somenteNumeros); + return Number.isFinite(numero) ? numero : 0; + } + + return 0; +} diff --git a/src/app/api/carteiras/upload-validado/route.test.ts b/src/app/api/carteiras/upload-validado/route.test.ts new file mode 100644 index 000000000..4804aeda5 --- /dev/null +++ b/src/app/api/carteiras/upload-validado/route.test.ts @@ -0,0 +1,33 @@ +import { normalizarLinha } from './route'; + +describe('normalizarLinha - upload validado', () => { + it('usa dados do resumo da linha validada para valores numéricos', () => { + const linhaValidada = { + sheetName: 'Ações', + categoria: 'Ações', + values: ['PETR4', 'Petrobras PN', '10', '27,50', 'R$ 275,00', '12,5%'], + resumo: { + quantidade: 10, + precoMedio: 27.5, + valorInvestido: 275, + valorAtual: 300, + moeda: 'BRL' + } + }; + + const resultado = normalizarLinha(linhaValidada); + + expect(resultado).not.toBeNull(); + expect(resultado).toMatchObject({ + categoria: 'acoes', + ticker: 'PETR4', + nome: 'Petrobras PN', + quantidade: 10, + precoMedio: 27.5, + valorInvestido: 275, + valorAtual: 300, + cotacaoAtual: 27.5, + moeda: 'BRL' + }); + }); +}); diff --git a/src/app/api/carteiras/upload-validado/route.ts b/src/app/api/carteiras/upload-validado/route.ts new file mode 100644 index 000000000..b60972491 --- /dev/null +++ b/src/app/api/carteiras/upload-validado/route.ts @@ -0,0 +1,953 @@ +// src/app/api/carteiras/upload-validado/route.ts + +export const dynamic = 'force-dynamic'; +export const runtime = 'nodejs'; + +import { NextRequest, NextResponse } from 'next/server'; +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; +import { parseNumeroFlex } from './parseNumeroFlex'; + +type CategoriaChave = 'acoes' | 'fiis' | 'rendaFixa' | 'exterior' | 'cripto' | 'reserva'; + +interface LinhaNormalizada { + categoria: CategoriaChave; + categoriaOriginal: string; + labelCategoria: string; + ticker?: string; + nome?: string; + quantidade: number; + precoMedio: number; + cotacaoAtual: number; + valorInvestido: number; + valorAtual: number; + banco?: string; + vencimento?: string; + exchange?: string; + observacoes?: string; + moeda?: string; + origem?: string; + taxaCambio?: number; + pesoCarteira?: number; +} + +const categoriaDisplayMap: Record = { + acoes: 'Ações', + fiis: 'FIIs', + rendaFixa: 'Renda Fixa', + exterior: 'Exterior', + cripto: 'Cripto', + reserva: 'Reserva' +}; + +const categoriaSinonimos: Record = { + acoes: [ + 'acoes', 'acao', 'acoesbrasil', 'acoesb3', 'stock', 'stocks', 'equities', + 'brasil', 'b3', 'acaoordinaria', 'acaopreferencial' + ], + fiis: [ + 'fiis', 'fii', 'fundosimobiliarios', 'fundosimobiliario', 'fundoinvestimentoimobiliario', + 'fundodeinvestimentoimobiliario', 'fundos', 'fundosimob', 'fundosimobiliarios' + ], + rendaFixa: [ + 'rendafixa', 'fixa', 'titulo', 'titulos', 'titulospublicos', 'titulosprivados', 'tesouro', 'tesourodireto', + 'cdb', 'lci', 'lca', 'debenture', 'debentures', 'cri', 'cra', 'lc', 'lf', 'bonds', 'previdencia', + 'previdenciaprivada', 'fundoprevidenciario', 'fundoprevidencia' + ], + exterior: [ + 'exterior', 'internacional', 'bdr', 'bdrs', 'acoesinternacionais', 'stocksus', 'usa', 'global', 'internacionais' + ], + cripto: [ + 'cripto', 'criptomoeda', 'criptomoedas', 'crypto', 'bitcoin', 'ethereum', 'ativosdigitais', 'moedadigital' + ], + reserva: [ + 'reserva', 'reservaemergencia', 'reservadeemergencia', 'reservaemergencial', 'reservaoportunidade', + 'caixa', 'poupanca', 'poupancaemergencia', 'emergencia', 'liquidez' + ] +}; + +const categoriasChave = Object.keys(categoriaDisplayMap) as CategoriaChave[]; + +function normalizarTextoChave(valor?: string | null): string { + if (!valor) { + return ''; + } + + return valor + .toString() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/[^a-z0-9]/gi, '') + .toLowerCase(); +} + +function obterValorPorChaveFlex(obj: any, chave: string): T | undefined { + if (!obj || typeof obj !== 'object') { + return undefined; + } + + if (Object.prototype.hasOwnProperty.call(obj, chave)) { + return obj[chave] as T; + } + + const chaveMinuscula = chave.toLowerCase(); + const chaveEncontrada = Object.keys(obj).find(key => key.toLowerCase() === chaveMinuscula); + + if (chaveEncontrada) { + return obj[chaveEncontrada] as T; + } + + return undefined; +} + +function extrairTexto(valor: any): string | undefined { + if (valor === undefined || valor === null) { + return undefined; + } + + if (typeof valor === 'string') { + const trimmed = valor.trim(); + return trimmed.length ? trimmed : undefined; + } + + if (typeof valor === 'number' && Number.isFinite(valor)) { + return valor.toString(); + } + + return undefined; +} + +function extrairPrimeiroTexto(fonte: any, chaves: string[]): string | undefined { + for (const chave of chaves) { + const valor = obterValorPorChaveFlex(fonte, chave); + const texto = extrairTexto(valor); + if (texto) { + return texto; + } + } + + return undefined; +} + +function extrairMoedaDoTexto(valor: unknown): string | undefined { + const texto = extrairTexto(valor); + + if (!texto) { + return undefined; + } + + const moeda = texto.replace(/[0-9\s.,-]/g, '').trim(); + + return moeda || undefined; +} + +function identificarCategoria(possivelCategoria?: string | null, fallback?: string | null): CategoriaChave | null { + const candidatos = [possivelCategoria, fallback].filter(Boolean) as string[]; + + for (const candidato of candidatos) { + const textoNormalizado = normalizarTextoChave(candidato); + if (!textoNormalizado) { + continue; + } + + for (const chave of categoriasChave) { + if (textoNormalizado === normalizarTextoChave(chave)) { + return chave; + } + + if (categoriaSinonimos[chave].includes(textoNormalizado)) { + return chave; + } + } + } + + return null; +} + +function extrairLinhasGenericas(portfolioData: any): { linhas: any[]; metadata: Record } { + if (!portfolioData) { + return { linhas: [], metadata: { formato: 'vazio' } }; + } + + if (Array.isArray(portfolioData)) { + return { linhas: portfolioData, metadata: { formato: 'array' } }; + } + + if (Array.isArray(portfolioData?.linhas)) { + const { linhas, ...resto } = portfolioData; + return { linhas, metadata: { formato: 'linhas', ...resto } }; + } + + if (Array.isArray(portfolioData?.rows)) { + const { rows, ...resto } = portfolioData; + return { linhas: rows, metadata: { formato: 'rows', ...resto } }; + } + + if (typeof portfolioData === 'object') { + const linhas: any[] = []; + const abasOriginais: string[] = []; + + const processarSheet = (sheetName: string, sheetValue: unknown) => { + if (!abasOriginais.includes(sheetName)) { + abasOriginais.push(sheetName); + } + + const dadosSheet = obterValorPorChaveFlex(sheetValue, 'data'); + if (Array.isArray(dadosSheet)) { + dadosSheet.forEach(row => { + if (row && typeof row === 'object') { + linhas.push({ ...row, sheetName }); + } + }); + } + }; + + if (portfolioData?.groups && typeof portfolioData.groups === 'object') { + Object.entries(portfolioData.groups).forEach(([sheetName, sheetValue]) => { + processarSheet(sheetName, sheetValue); + }); + + return { linhas, metadata: { formato: 'abas', abasOriginais } }; + } + + Object.entries(portfolioData).forEach(([sheetName, sheetValue]) => { + processarSheet(sheetName, sheetValue); + }); + + return { linhas, metadata: { formato: 'abas', abasOriginais } }; + } + + return { linhas: [], metadata: { formato: 'desconhecido' } }; +} + +export function normalizarLinha(row: any): LinhaNormalizada | null { + if (!row || typeof row !== 'object') { + return null; + } + + const sheetName = extrairTexto(obterValorPorChaveFlex(row, 'sheetName')); + const categoriaOriginal = + extrairPrimeiroTexto(row, ['categoria', 'Category', 'tipoCategoria', 'tipo', 'grupo', 'setor', 'aba']) || + sheetName || + ''; + + const categoria = identificarCategoria(categoriaOriginal, sheetName); + + if (!categoria) { + return null; + } + + const labelCategoria = categoriaDisplayMap[categoria]; + const valores = Array.isArray(obterValorPorChaveFlex(row, 'values')) + ? (obterValorPorChaveFlex(row, 'values') as unknown[]) + : undefined; + + if (valores && valores.length) { + if (categoria === 'acoes' || categoria === 'fiis') { + const ticker = + extrairTexto(valores[0]) || extrairPrimeiroTexto(row, ['codigo', 'ticker', 'ativo', 'sigla', 'bdr']); + const nome = + extrairTexto(valores[1]) || extrairPrimeiroTexto(row, ['nome', 'descricao', 'description', 'company']); + const resumo = obterValorPorChaveFlex(row, 'resumo'); + const obterNumeroDoResumo = (chaves: string[]): number | undefined => { + if (!resumo || typeof resumo !== 'object') { + return undefined; + } + + for (const chave of chaves) { + const valorResumo = obterValorPorChaveFlex(resumo, chave); + if (valorResumo !== undefined && valorResumo !== null) { + return parseNumeroFlex(valorResumo); + } + } + + return undefined; + }; + + const quantidadeResumo = obterNumeroDoResumo(['quantidade', 'qtde', 'qtd', 'cotas']); + const precoMedioResumo = obterNumeroDoResumo(['precoMedio', 'preco_medio', 'pm']); + const valorInvestidoResumo = obterNumeroDoResumo(['valorInvestido', 'totalInvestido']); + const valorAtualResumo = obterNumeroDoResumo(['valorAtual', 'totalAtual']); + const cotacaoResumo = obterNumeroDoResumo(['cotacaoAtual', 'cotacao', 'precoAtual', 'valorAtualUnitario']); + + const quantidade = + quantidadeResumo ?? parseNumeroFlex(valores[2] ?? extrairPrimeiroTexto(row, ['quantidade', 'qtde', 'qtd'])); + const precoMedio = + precoMedioResumo ?? parseNumeroFlex(valores[3] ?? extrairPrimeiroTexto(row, ['precoMedio', 'pm', 'preco_medio'])); + let cotacaoAtual = + cotacaoResumo ?? parseNumeroFlex(extrairPrimeiroTexto(row, ['cotacaoAtual', 'cotacao', 'precoAtual'])); + let valorInvestido = + valorInvestidoResumo ?? parseNumeroFlex(extrairPrimeiroTexto(row, ['valorInvestido', 'totalInvestido']) ?? valores[4]); + let valorAtual = + valorAtualResumo ?? parseNumeroFlex(extrairPrimeiroTexto(row, ['valorAtual', 'totalAtual'])); + + if (cotacaoAtual <= 0) { + cotacaoAtual = precoMedio > 0 + ? precoMedio + : (valorAtual > 0 && quantidade > 0 ? valorAtual / quantidade : 0); + } + + if (valorInvestido <= 0 && quantidade > 0 && precoMedio > 0) { + valorInvestido = quantidade * precoMedio; + } + + if (valorAtual <= 0 && quantidade > 0) { + const baseCotacao = cotacaoAtual > 0 ? cotacaoAtual : precoMedio; + valorAtual = baseCotacao > 0 ? quantidade * baseCotacao : valorInvestido; + } + + const moeda = + extrairPrimeiroTexto(resumo, ['moeda', 'currency']) ?? + extrairMoedaDoTexto(valores[4]) ?? + extrairPrimeiroTexto(row, ['moeda', 'currency']); + + return { + categoria, + categoriaOriginal, + labelCategoria, + ticker: ticker?.toUpperCase(), + nome: nome || ticker, + quantidade, + precoMedio, + cotacaoAtual, + valorInvestido, + valorAtual, + origem: sheetName, + moeda + }; + } + + if (categoria === 'rendaFixa') { + const produto = + extrairTexto(valores[0]) || extrairPrimeiroTexto(row, ['produto', 'ativo', 'descricao', 'nome']); + const banco = extrairTexto(valores[1]) || extrairPrimeiroTexto(row, ['banco', 'instituicao']); + const valorInvestido = parseNumeroFlex(valores[2] ?? extrairPrimeiroTexto(row, ['valorInvestido', 'valor'])); + const valorAtual = parseNumeroFlex(valores[3] ?? extrairPrimeiroTexto(row, ['valorAtual', 'saldo'])); + const vencimento = extrairTexto(valores[4]) || extrairPrimeiroTexto(row, ['vencimento', 'dataVencimento']); + + return { + categoria, + categoriaOriginal, + labelCategoria, + ticker: produto, + nome: produto, + quantidade: 0, + precoMedio: 0, + cotacaoAtual: 0, + valorInvestido, + valorAtual, + banco, + vencimento, + origem: sheetName + }; + } + + if (categoria === 'exterior') { + const ticker = + extrairTexto(valores[0]) || extrairPrimeiroTexto(row, ['codigo', 'ticker', 'ativo', 'sigla']); + const nome = + extrairTexto(valores[1]) || extrairPrimeiroTexto(row, ['nome', 'descricao', 'company', 'description']); + const quantidade = parseNumeroFlex(valores[2] ?? extrairPrimeiroTexto(row, ['quantidade', 'qtde'])); + const precoMedio = parseNumeroFlex(valores[3] ?? extrairPrimeiroTexto(row, ['precoMedio', 'pm'])); + const cotacaoAtual = parseNumeroFlex(valores[4] ?? extrairPrimeiroTexto(row, ['cotacao', 'cotacaoAtual'])); + const valorInvestido = parseNumeroFlex( + valores[5] ?? extrairPrimeiroTexto(row, ['valorInvestido', 'totalInvestido']) + ); + const valorAtual = parseNumeroFlex(valores[6] ?? extrairPrimeiroTexto(row, ['valorAtual', 'totalAtual'])); + const taxaCambio = parseNumeroFlex( + extrairPrimeiroTexto(row, ['taxaCambio', 'cambio', 'exchangeRate', 'taxa_de_cambio']) + ); + + return { + categoria, + categoriaOriginal, + labelCategoria, + ticker: ticker?.toUpperCase(), + nome: nome || ticker, + quantidade, + precoMedio, + cotacaoAtual, + valorInvestido, + valorAtual, + taxaCambio, + origem: sheetName + }; + } + + if (categoria === 'cripto') { + const nome = + extrairTexto(valores[0]) || extrairPrimeiroTexto(row, ['criptomoeda', 'ativo', 'descricao', 'nome']); + const quantidade = parseNumeroFlex(valores[1] ?? extrairPrimeiroTexto(row, ['quantidade', 'qtde'])); + const precoMedio = parseNumeroFlex(valores[2] ?? extrairPrimeiroTexto(row, ['precoMedio', 'pm'])); + const valorAtual = parseNumeroFlex(valores[3] ?? extrairPrimeiroTexto(row, ['valorAtual', 'totalAtual'])); + const exchange = extrairTexto(valores[4]) || extrairPrimeiroTexto(row, ['exchange', 'corretora']); + + return { + categoria, + categoriaOriginal, + labelCategoria, + ticker: nome?.toUpperCase(), + nome, + quantidade, + precoMedio, + cotacaoAtual: 0, + valorInvestido: quantidade * precoMedio, + valorAtual, + exchange, + origem: sheetName + }; + } + + if (categoria === 'reserva') { + const nome = + extrairTexto(valores[0]) || extrairPrimeiroTexto(row, ['onde', 'instituicao', 'descricao', 'nome']); + const banco = extrairTexto(valores[1]) || extrairPrimeiroTexto(row, ['banco', 'instituicao']); + const valor = parseNumeroFlex(valores[2] ?? extrairPrimeiroTexto(row, ['valor', 'saldo'])); + const observacoes = extrairTexto(valores[3]) || extrairPrimeiroTexto(row, ['observacoes', 'descricao']); + + return { + categoria, + categoriaOriginal, + labelCategoria, + nome, + quantidade: 0, + precoMedio: 0, + cotacaoAtual: 0, + valorInvestido: valor, + valorAtual: valor, + banco, + observacoes, + origem: sheetName + }; + } + } + + const ticker = extrairPrimeiroTexto(row, ['codigo', 'ticker', 'ativo', 'sigla', 'bdr']); + const nome = + extrairPrimeiroTexto(row, ['nome', 'descricao', 'description', 'company', 'produto', 'onde']) || ticker; + const quantidade = parseNumeroFlex( + extrairPrimeiroTexto(row, ['quantidade', 'qtde', 'qtd', 'cotas', 'unidades', 'shares']) + ); + const precoMedio = parseNumeroFlex( + extrairPrimeiroTexto(row, ['precoMedio', 'preco_medio', 'pm', 'averagePrice', 'price']) + ); + const cotacaoAtual = parseNumeroFlex( + extrairPrimeiroTexto(row, ['cotacaoAtual', 'cotacao', 'precoAtual', 'valorAtualUnitario']) + ); + const valorInvestido = parseNumeroFlex( + extrairPrimeiroTexto(row, ['valorInvestido', 'valor_investido', 'totalInvestido', 'valorAplicado', 'investimento']) + ); + const valorAtual = parseNumeroFlex( + extrairPrimeiroTexto(row, ['valorAtual', 'valor_atual', 'totalAtual', 'saldoAtual', 'saldo', 'valor']) + ); + const banco = extrairPrimeiroTexto(row, ['banco', 'instituicao', 'instituicaoFinanceira', 'corretora']); + const vencimento = extrairPrimeiroTexto(row, ['vencimento', 'dataVencimento', 'maturity']); + const exchange = extrairPrimeiroTexto(row, ['exchange', 'corretora', 'plataforma', 'broker']); + const observacoes = extrairPrimeiroTexto(row, ['observacoes', 'observacao', 'comentarios', 'notas']); + const moeda = extrairPrimeiroTexto(row, ['moeda', 'currency']); + const taxaCambio = parseNumeroFlex( + extrairPrimeiroTexto(row, ['taxaCambio', 'cambio', 'exchangeRate', 'taxa_de_cambio']) + ); + + return { + categoria, + categoriaOriginal, + labelCategoria, + ticker: ticker?.toUpperCase(), + nome, + quantidade, + precoMedio, + cotacaoAtual, + valorInvestido, + valorAtual, + banco, + vencimento, + exchange, + observacoes, + moeda, + origem: sheetName, + taxaCambio + }; +} + +export async function POST(request: NextRequest) { + console.log('==================== DEBUG UPLOAD VALIDADO START ===================='); + console.log('🚀 STEP 1: Iniciando upload com dados validados'); + + try { + // STEP 2: VERIFICAR AUTENTICAÇÃO + console.log('🔠STEP 2: Verificando autenticação...'); + const session = await auth(); + + if (!session?.user?.id) { + console.error('⌠STEP 2: Usuário não autenticado'); + return NextResponse.json({ + error: 'Não autorizado', + details: 'Sessão inválida ou expirada' + }, { status: 401 }); + } + + console.log(`✅ STEP 2: Usuário autenticado: ${session.user.id}`); + + // STEP 3: VERIFICAR CARTEIRAS EXISTENTES + console.log('📋 STEP 3: Verificando carteiras existentes...'); + const carteiraExistente = await prisma.carteiraAnalise.findFirst({ + where: { userId: session.user.id } + }); + + if (carteiraExistente) { + console.log('🚫 STEP 3: Upload rejeitado - usuário já enviou carteira'); + return NextResponse.json({ + error: 'Você já enviou uma carteira para análise.', + carteiraExistente: { + id: carteiraExistente.id, + status: carteiraExistente.status.toLowerCase(), + nomeArquivo: carteiraExistente.nomeArquivo + } + }, { status: 400 }); + } + + console.log('✅ STEP 3: Usuário pode enviar nova carteira'); + + // STEP 4: PROCESSAR FORM DATA + console.log('📤 STEP 4: Processando FormData...'); + const formData = await request.formData(); + + const dadosValidadosStr = formData.get('dadosValidados'); + const questionarioStr = formData.get('questionario'); + + if (!dadosValidadosStr) { + console.error('⌠STEP 4: Dados validados não encontrados'); + return NextResponse.json({ + error: 'Dados validados não enviados', + details: 'Campo "dadosValidados" não encontrado' + }, { status: 400 }); + } + + console.log('✅ STEP 4: Dados validados recebidos'); + + // STEP 5: PROCESSAR DADOS VALIDADOS + console.log('🔠STEP 5: Processando dados validados...'); + + let dadosValidados, questionario; + + try { + dadosValidados = JSON.parse(dadosValidadosStr.toString()); + questionario = questionarioStr ? JSON.parse(questionarioStr.toString()) : null; + console.log('✅ STEP 5: JSON válido processado'); + } catch (error) { + console.error('⌠STEP 5: Erro ao fazer parse do JSON:', error); + return NextResponse.json({ + error: 'Dados inválidos', + details: 'Erro ao processar dados JSON' + }, { status: 400 }); + } + + // STEP 6: CONVERTER DADOS VALIDADOS PARA ESTRUTURA DO BANCO + console.log('🔄 STEP 6: Convertendo dados para estrutura do banco...'); + + const { linhas: linhasExtraidas, metadata: metadataEntrada } = extrairLinhasGenericas(dadosValidados.portfolioData); + console.log(`â„¹ï¸ STEP 6: Linhas recebidas para processamento: ${linhasExtraidas.length}`); + + const linhasNormalizadas = linhasExtraidas + .map(normalizarLinha) + .filter((linha): linha is LinhaNormalizada => !!linha); + + if (!linhasNormalizadas.length) { + console.error('⌠STEP 6: Nenhuma linha válida encontrada no formato enviado'); + return NextResponse.json({ + error: 'Formato de dados inválido', + details: 'Não foi possível identificar ativos na estrutura enviada', + recebidos: linhasExtraidas.length + }, { status: 400 }); + } + + const dadosCarteira = { + abas: [] as string[], + acoes: [] as any[], + fiis: [] as any[], + rendaFixa: [] as any[], + exterior: [] as any[], + cripto: [] as any[], + reserva: [] as any[], + processedAt: new Date().toISOString(), + validatedInFrontend: true, + linhasProcessadas: [] as LinhaNormalizada[], + resumoCategorias: {} as Record, + metadadosEntrada: { + ...metadataEntrada, + totalLinhasRecebidas: linhasExtraidas.length + }, + resumoGeral: { + valorTotal: 0, + quantidadeAtivos: 0, + linhasProcessadas: 0, + categorias: [] as string[] + } + }; + + const categoriasEncontradas = new Set(); + const resumoCategoriasAcumulado: Partial> = {}; + const linhasEnriquecidas: LinhaNormalizada[] = []; + + let valorTotal = 0; + let quantidadeAtivos = 0; + + const registrarResumo = (categoria: CategoriaChave, valor: number) => { + if (!resumoCategoriasAcumulado[categoria]) { + resumoCategoriasAcumulado[categoria] = { valor: 0, ativos: 0 }; + } + + if (valor > 0) { + resumoCategoriasAcumulado[categoria]!.valor += valor; + } + + resumoCategoriasAcumulado[categoria]!.ativos += 1; + }; + + linhasNormalizadas.forEach(linha => { + categoriasEncontradas.add(linha.categoria); + + const enriched: LinhaNormalizada = { ...linha }; + + switch (linha.categoria) { + case 'acoes': + case 'fiis': { + const quantidade = linha.quantidade > 0 ? linha.quantidade : 0; + const precoMedio = linha.precoMedio > 0 + ? linha.precoMedio + : (linha.valorInvestido > 0 && quantidade > 0 ? linha.valorInvestido / quantidade : 0); + const cotacaoAtual = linha.cotacaoAtual > 0 + ? linha.cotacaoAtual + : (linha.valorAtual > 0 && quantidade > 0 ? linha.valorAtual / quantidade : precoMedio); + const valorInvestido = linha.valorInvestido > 0 + ? linha.valorInvestido + : (quantidade > 0 ? precoMedio * quantidade : 0); + const valorAtual = linha.valorAtual > 0 + ? linha.valorAtual + : (quantidade > 0 ? cotacaoAtual * quantidade : valorInvestido); + + if ((linha.ticker || linha.nome) && (valorInvestido > 0 || valorAtual > 0)) { + const tipo = linha.categoria === 'acoes' ? 'ACAO' : 'FII'; + const registro = { + codigo: (linha.ticker || linha.nome || '').toUpperCase(), + nome: linha.nome || linha.ticker || '', + tipo, + quantidade, + precoMedio, + cotacaoAtual, + valorInvestido, + valorAtual, + rentabilidade: valorInvestido > 0 ? ((valorAtual - valorInvestido) / valorInvestido) * 100 : 0, + pesoCarteira: 0 + }; + + if (linha.categoria === 'acoes') { + dadosCarteira.acoes.push(registro); + } else { + dadosCarteira.fiis.push(registro); + } + + valorTotal += valorAtual; + quantidadeAtivos += 1; + registrarResumo(linha.categoria, valorAtual); + + enriched.quantidade = quantidade; + enriched.precoMedio = precoMedio; + enriched.cotacaoAtual = cotacaoAtual; + enriched.valorInvestido = valorInvestido; + enriched.valorAtual = valorAtual; + } + + break; + } + + case 'rendaFixa': { + const valorInvestido = linha.valorInvestido > 0 ? linha.valorInvestido : linha.valorAtual; + const valorAtual = linha.valorAtual > 0 ? linha.valorAtual : valorInvestido; + + if ((valorInvestido > 0 || valorAtual > 0) && (linha.nome || linha.ticker)) { + dadosCarteira.rendaFixa.push({ + produto: linha.nome || linha.ticker || 'Renda Fixa', + banco: linha.banco || '', + tipo: 'RENDA_FIXA', + valorInvestido, + valorAtual, + vencimento: linha.vencimento || '', + rentabilidade: valorInvestido > 0 ? ((valorAtual - valorInvestido) / valorInvestido) * 100 : 0, + pesoCarteira: 0 + }); + + valorTotal += valorAtual; + quantidadeAtivos += 1; + registrarResumo('rendaFixa', valorAtual); + + enriched.valorInvestido = valorInvestido; + enriched.valorAtual = valorAtual; + } + + break; + } + + case 'exterior': { + const quantidade = linha.quantidade > 0 ? linha.quantidade : 0; + const precoMedio = linha.precoMedio > 0 + ? linha.precoMedio + : (linha.valorInvestido > 0 && quantidade > 0 ? linha.valorInvestido / quantidade : 0); + const cotacaoAtual = linha.cotacaoAtual > 0 + ? linha.cotacaoAtual + : (linha.valorAtual > 0 && quantidade > 0 ? linha.valorAtual / quantidade : precoMedio); + const valorInvestido = linha.valorInvestido > 0 + ? linha.valorInvestido + : (quantidade > 0 ? precoMedio * quantidade : 0); + const valorAtual = linha.valorAtual > 0 + ? linha.valorAtual + : (quantidade > 0 ? cotacaoAtual * quantidade : valorInvestido); + + if ((linha.ticker || linha.nome) && (valorInvestido > 0 || valorAtual > 0)) { + const moedaNormalizada = normalizarTextoChave(linha.moeda); + const taxaCambio = linha.taxaCambio && linha.taxaCambio > 0 + ? linha.taxaCambio + : (moedaNormalizada === 'usd' ? 5.5 : 1); + + dadosCarteira.exterior.push({ + codigo: (linha.ticker || linha.nome || '').toUpperCase(), + nome: linha.nome || linha.ticker || '', + tipo: 'EXTERIOR', + quantidade, + precoMedioUSD: precoMedio, + cotacaoUSD: cotacaoAtual, + taxaCambio, + valorInvestido, + valorAtual, + rentabilidade: valorInvestido > 0 ? ((valorAtual - valorInvestido) / valorInvestido) * 100 : 0, + pesoCarteira: 0, + moeda: linha.moeda || undefined + }); + + valorTotal += valorAtual; + quantidadeAtivos += 1; + registrarResumo('exterior', valorAtual); + + enriched.quantidade = quantidade; + enriched.precoMedio = precoMedio; + enriched.cotacaoAtual = cotacaoAtual; + enriched.valorInvestido = valorInvestido; + enriched.valorAtual = valorAtual; + enriched.taxaCambio = taxaCambio; + } + + break; + } + + case 'cripto': { + const quantidade = linha.quantidade > 0 ? linha.quantidade : 0; + const precoMedio = linha.precoMedio > 0 + ? linha.precoMedio + : (linha.valorInvestido > 0 && quantidade > 0 ? linha.valorInvestido / quantidade : 0); + const valorInvestido = linha.valorInvestido > 0 ? linha.valorInvestido : quantidade * precoMedio; + const valorAtual = linha.valorAtual > 0 ? linha.valorAtual : valorInvestido; + + if ((linha.nome || linha.ticker) && (valorInvestido > 0 || valorAtual > 0)) { + dadosCarteira.cripto.push({ + criptomoeda: linha.nome || linha.ticker || '', + tipo: 'CRIPTO', + quantidade, + precoMedio, + valorAtual, + exchange: linha.exchange || '', + rentabilidade: valorInvestido > 0 ? ((valorAtual - valorInvestido) / valorInvestido) * 100 : 0, + pesoCarteira: 0 + }); + + valorTotal += valorAtual; + quantidadeAtivos += 1; + registrarResumo('cripto', valorAtual); + + enriched.quantidade = quantidade; + enriched.precoMedio = precoMedio; + enriched.valorInvestido = valorInvestido; + enriched.valorAtual = valorAtual; + } + + break; + } + + case 'reserva': { + const valorReserva = linha.valorAtual > 0 ? linha.valorAtual : linha.valorInvestido; + + if (valorReserva > 0 && (linha.nome || linha.banco)) { + dadosCarteira.reserva.push({ + onde: linha.nome || linha.banco || 'Reserva', + banco: linha.banco || '', + tipo: 'RESERVA', + valor: valorReserva, + observacoes: linha.observacoes || '' + }); + + registrarResumo('reserva', valorReserva); + + enriched.valorInvestido = valorReserva; + enriched.valorAtual = valorReserva; + } + + break; + } + } + + linhasEnriquecidas.push(enriched); + }); + + dadosCarteira.abas = Array.from(categoriasEncontradas).map(categoria => categoriaDisplayMap[categoria]); + + if (valorTotal > 0) { + dadosCarteira.acoes = dadosCarteira.acoes.map(ativo => ({ + ...ativo, + pesoCarteira: (ativo.valorAtual / valorTotal) * 100 + })); + + dadosCarteira.fiis = dadosCarteira.fiis.map(ativo => ({ + ...ativo, + pesoCarteira: (ativo.valorAtual / valorTotal) * 100 + })); + + dadosCarteira.exterior = dadosCarteira.exterior.map(ativo => ({ + ...ativo, + pesoCarteira: (ativo.valorAtual / valorTotal) * 100 + })); + + dadosCarteira.rendaFixa = dadosCarteira.rendaFixa.map(item => ({ + ...item, + pesoCarteira: (item.valorAtual / valorTotal) * 100 + })); + + dadosCarteira.cripto = dadosCarteira.cripto.map(item => ({ + ...item, + pesoCarteira: (item.valorAtual / valorTotal) * 100 + })); + + linhasEnriquecidas.forEach(linha => { + linha.pesoCarteira = linha.categoria === 'reserva' ? 0 : (linha.valorAtual > 0 ? (linha.valorAtual / valorTotal) * 100 : 0); + }); + } else { + linhasEnriquecidas.forEach(linha => { + linha.pesoCarteira = 0; + }); + } + + const resumoCategorias = (Object.entries(resumoCategoriasAcumulado) as [CategoriaChave, { valor: number; ativos: number }][]) + .reduce((acc, [categoria, info]) => { + if (!info) { + return acc; + } + + const valorCategoria = info.valor; + const percentual = categoria === 'reserva' || valorTotal <= 0 + ? 0 + : (valorCategoria / valorTotal) * 100; + + acc[categoria] = { + label: categoriaDisplayMap[categoria], + valorTotal: Number(valorCategoria.toFixed(2)), + percentual: Number(percentual.toFixed(2)), + quantidadeAtivos: info.ativos + }; + + return acc; + }, {} as Record); + + dadosCarteira.resumoCategorias = resumoCategorias; + dadosCarteira.linhasProcessadas = linhasEnriquecidas; + dadosCarteira.resumoGeral = { + valorTotal, + quantidadeAtivos, + linhasProcessadas: linhasEnriquecidas.length, + categorias: dadosCarteira.abas + }; + + console.log('✅ STEP 6: Estrutura reorganizada com sucesso'); + console.log(`📊 Resumo geral: ${quantidadeAtivos} ativos, valor total R$ ${valorTotal.toFixed(2)}`); + console.log('📂 Categorias encontradas:', dadosCarteira.abas); + console.log(`📈 Ações: ${dadosCarteira.acoes.length}`); + console.log(`ðŸ˜ï¸ FIIs: ${dadosCarteira.fiis.length}`); + console.log(`💰 Renda Fixa: ${dadosCarteira.rendaFixa.length}`); + console.log(`🌎 Exterior: ${dadosCarteira.exterior.length}`); + console.log(`â‚¿ Cripto: ${dadosCarteira.cripto.length}`); + console.log(`🦠Reserva: ${dadosCarteira.reserva.length}`); + + // STEP 8: SALVAR NO BANCO + const abasProcessadas = dadosCarteira.abas.length; + const linhasProcessadas = linhasEnriquecidas.length; + + console.log('ðŸ—„ï¸ STEP 8: Salvando no banco...'); + console.log('📠Dados que serão salvos:', { + userId: session.user.id, + nomeArquivo: dadosValidados.fileName, + valorTotal, + quantidadeAtivos, + linhasProcessadas, + abasProcessadas, + status: 'PENDENTE' + }); + + try { + const novaCarteira = await prisma.carteiraAnalise.create({ + data: { + userId: session.user.id, + nomeArquivo: dadosValidados.fileName, + arquivoUrl: `validated/${dadosValidados.fileName}`, // Indica que veio validado + valorTotal: valorTotal, + quantidadeAtivos: quantidadeAtivos, + status: 'PENDENTE', + dadosEstruturados: JSON.stringify({ + ...dadosCarteira, + questionario: questionario, + validatedData: true, + originalData: dadosValidados.portfolioData, // Dados originais da validação + processedAt: dadosValidados.processedAt + }) + } + }); + + console.log('✅ STEP 8: Carteira salva com sucesso:', novaCarteira.id); + console.log('==================== DEBUG UPLOAD VALIDADO SUCCESS ===================='); + + return NextResponse.json({ + success: true, + message: 'Dados validados enviados com sucesso!', + carteiraId: novaCarteira.id, + resumo: { + valorTotal: valorTotal, + quantidadeAtivos: quantidadeAtivos, + linhasProcessadas, + abasProcessadas, + dadosValidados: true, + questionarioIncluido: !!questionario, + nomeArquivo: dadosValidados.fileName, + categorias: dadosCarteira.abas, + distribuicao: dadosCarteira.resumoCategorias, + formatoEntrada: dadosCarteira.metadadosEntrada?.formato || null + } + }); + + } catch (dbError) { + console.error('⌠STEP 8: ERRO no banco:', dbError); + console.error('⌠Mensagem completa:', dbError.message); + console.log('==================== DEBUG UPLOAD VALIDADO FAILED ===================='); + + return NextResponse.json({ + error: 'Erro ao salvar no banco de dados', + details: dbError.message, + step: 'database_save' + }, { status: 500 }); + } + + } catch (globalError) { + console.error('💥 ERRO GLOBAL:', globalError); + console.log('==================== DEBUG UPLOAD VALIDADO ERROR ===================='); + return NextResponse.json({ + error: 'Erro interno do servidor', + details: globalError.message + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/carteiras/upload/route.ts b/src/app/api/carteiras/upload/route.ts new file mode 100644 index 000000000..3a1c37717 --- /dev/null +++ b/src/app/api/carteiras/upload/route.ts @@ -0,0 +1,545 @@ +// src/app/api/carteiras/upload/route.ts + +export const dynamic = 'force-dynamic'; +export const runtime = 'nodejs'; + +import { NextRequest, NextResponse } from 'next/server'; +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; +import * as XLSX from 'xlsx'; + +export async function POST(request: NextRequest) { + console.log('==================== DEBUG UPLOAD START ===================='); + console.log('🚀 STEP 1: Iniciando upload de carteira'); + + try { + // STEP 2: VERIFICAR AUTENTICAÇÃO + console.log('🔠STEP 2: Verificando autenticação...'); + const session = await auth(); + + if (!session?.user?.id) { + console.error('⌠STEP 2: Usuário não autenticado'); + return NextResponse.json({ + error: 'Não autorizado', + details: 'Sessão inválida ou expirada' + }, { status: 401 }); + } + + console.log(`✅ STEP 2: Usuário autenticado: ${session.user.id}`); + + // STEP 3: VERIFICAR CARTEIRAS EXISTENTES + console.log('📋 STEP 3: Verificando carteiras existentes...'); + const carteiraExistente = await prisma.carteiraAnalise.findFirst({ + where: { userId: session.user.id } + }); + + if (carteiraExistente) { + console.log('🚫 STEP 3: Upload rejeitado - usuário já enviou carteira'); + return NextResponse.json({ + error: 'Você já enviou uma carteira para análise.', + carteiraExistente: { + id: carteiraExistente.id, + status: carteiraExistente.status.toLowerCase(), + nomeArquivo: carteiraExistente.nomeArquivo + } + }, { status: 400 }); + } + + console.log('✅ STEP 3: Usuário pode enviar nova carteira'); + + // STEP 4: PROCESSAR FORM DATA + console.log('📤 STEP 4: Processando FormData...'); + const formData = await request.formData(); + + const file: File | null = formData.get('arquivo') as unknown as File; + const questionarioData = formData.get('questionario'); + + if (!file) { + console.error('⌠STEP 4: Nenhum arquivo encontrado'); + return NextResponse.json({ + error: 'Nenhum arquivo enviado', + details: 'Campo "arquivo" não encontrado' + }, { status: 400 }); + } + + console.log(`✅ STEP 4: Arquivo recebido: ${file.name} (${file.size} bytes)`); + + // STEP 5: VALIDAR ARQUIVO + console.log('🔠STEP 5: Validando arquivo...'); + + if (!file.name.endsWith('.xlsx') && !file.name.endsWith('.xls')) { + return NextResponse.json({ + error: 'Formato de arquivo inválido. Use apenas .xlsx ou .xls' + }, { status: 400 }); + } + + if (file.size > 10 * 1024 * 1024) { + return NextResponse.json({ + error: 'Arquivo muito grande. Máximo 10MB' + }, { status: 400 }); + } + + console.log('✅ STEP 5: Arquivo válido'); + + // STEP 6: PROCESSAR QUESTIONÃRIO + let questionario = null; + if (questionarioData) { + try { + const questionarioStr = typeof questionarioData === 'string' + ? questionarioData + : questionarioData.toString(); + questionario = JSON.parse(questionarioStr); + console.log('✅ STEP 6: Questionário processado'); + } catch (error) { + console.error('⌠STEP 6: Erro no questionário:', error); + } + } + + // STEP 7: PROCESSAR EXCEL + console.log('📊 STEP 7: Processando planilha Excel...'); + const bytes = await file.arrayBuffer(); + const buffer = Buffer.from(bytes); + + const workbook = XLSX.read(buffer, { + type: 'buffer', + cellStyles: true, + cellFormulas: true, + cellDates: true + }); + + console.log('✅ STEP 7: Excel processado. Abas:', workbook.SheetNames); + + // STEP 8: EXTRAIR DADOS DO EXCEL CORRETAMENTE + console.log('🔄 STEP 8: Localizando aba principal...'); + + const requiredHeaders = ['ativo', 'categoria', 'quantidade', 'precoMedio']; + + const normalizeText = (value: unknown) => { + if (value === undefined || value === null) return ''; + return String(value) + .trim() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/[^a-zA-Z0-9]/g, '') + .toLowerCase(); + }; + + const headerAlias: Record = { + ativo: 'ativo', + ticker: 'ativo', + codigo: 'ativo', + codigoticker: 'ativo', + papel: 'ativo', + categoria: 'categoria', + classificacao: 'categoria', + classe: 'categoria', + segmento: 'categoria', + setor: 'categoria', + qtde: 'quantidade', + quantidade: 'quantidade', + qtd: 'quantidade', + posicao: 'quantidade', + preco: 'precoMedio', + precomedio: 'precoMedio', + precomedioaplicado: 'precoMedio', + pm: 'precoMedio', + precoteto: 'precoMedio', + precopago: 'precoMedio', + precocompra: 'precoMedio', + cotacao: 'precoAtual', + cotacaoatual: 'precoAtual', + precoatual: 'precoAtual', + precounitario: 'precoAtual', + valorinvestido: 'valorInvestido', + valoraplicado: 'valorInvestido', + valortotal: 'valorInvestido', + custo: 'valorInvestido', + valoratual: 'valorAtual', + valormercado: 'valorAtual', + nome: 'nome', + descricao: 'nome', + empresa: 'nome', + ativoexterior: 'ativo', + simbolo: 'ativo' + }; + + const localizarAbaPrincipal = () => { + for (const sheetName of workbook.SheetNames) { + const worksheet = workbook.Sheets[sheetName]; + const linhas = XLSX.utils.sheet_to_json(worksheet, { header: 1, raw: true, defval: null }); + + for (let i = 0; i < linhas.length; i++) { + const linha = linhas[i]; + if (!linha || linha.length === 0) continue; + + const headersEncontrados = new Set(); + linha.forEach(celula => { + const normalizado = normalizeText(celula); + if (normalizado && headerAlias[normalizado]) { + headersEncontrados.add(headerAlias[normalizado]); + } + }); + + const contemCabecalhos = requiredHeaders.every(header => headersEncontrados.has(header)); + + if (contemCabecalhos) { + return { sheetName, headerRowIndex: i, linhas }; + } + } + } + + return null; + }; + + const resultadoLocalizacao = localizarAbaPrincipal(); + + if (!resultadoLocalizacao) { + console.error('⌠STEP 8: Nenhuma aba com cabeçalhos válidos encontrada.'); + return NextResponse.json({ + error: 'Não foi possível identificar a aba principal da planilha.', + details: 'Certifique-se de que a planilha contenha colunas ATIVO, CATEGORIA, QTDE e PREÇO MÉDIO.' + }, { status: 400 }); + } + + const { sheetName: abaPrincipal, headerRowIndex, linhas: linhasPlanilha } = resultadoLocalizacao; + console.log(`✅ STEP 8: Aba principal encontrada: ${abaPrincipal} (linha de cabeçalho ${headerRowIndex + 1})`); + + const headerRow = linhasPlanilha[headerRowIndex]; + const headerIndexMap: Record = {}; + headerRow.forEach((celula, index) => { + const chaveNormalizada = normalizeText(celula); + if (!chaveNormalizada) return; + const chavePadrao = headerAlias[chaveNormalizada]; + if (chavePadrao && headerIndexMap[chavePadrao] === undefined) { + headerIndexMap[chavePadrao] = index; + } + }); + + const possuiCabecalhosObrigatorios = requiredHeaders.every(header => headerIndexMap[header] !== undefined); + + if (!possuiCabecalhosObrigatorios) { + console.error('⌠STEP 8: Cabeçalhos obrigatórios ausentes na aba principal.'); + return NextResponse.json({ + error: 'Cabeçalhos obrigatórios ausentes na planilha.', + details: 'As colunas ATIVO, CATEGORIA, QTDE e PREÇO MÉDIO são obrigatórias.' + }, { status: 400 }); + } + + const extrairValorNumerico = (valor: unknown): number | null => { + if (valor === undefined || valor === null) return null; + if (typeof valor === 'number' && !Number.isNaN(valor)) return valor; + + const valorStr = String(valor) + .replace(/\./g, '') + .replace(/,/g, '.') + .replace(/[^0-9.-]/g, '') + .trim(); + + if (!valorStr) return null; + + const numero = parseFloat(valorStr); + return Number.isFinite(numero) ? numero : null; + }; + + const obterValor = (linha: any[], chave: string): any => { + const indice = headerIndexMap[chave]; + if (indice === undefined) return null; + return linha[indice]; + }; + + const dadosCarteira = { + abas: [], + acoes: [], + fiis: [], + rendaFixa: [], + exterior: [], + cripto: [], + reserva: [], + outros: [], + processedAt: new Date().toISOString() + }; + + const categoriasEncontradas = new Set(); + + const normalizarCategoria = (categoria: string | null | undefined) => { + if (!categoria) { + return { chave: 'outros', nome: 'Outros' as const }; + } + + const valorNormalizado = normalizeText(categoria); + + const eh = (valor: string) => valorNormalizado === valor; + const contem = (trecho: string) => valorNormalizado.includes(trecho); + + if ( + eh('acao') || + eh('acoes') || + contem('acaobrasil') || + contem('rendavariavel') || + eh('stock') || + eh('stocks') || + eh('equity') + ) { + return { chave: 'acoes' as const, nome: 'Ações' }; + } + + if (contem('fii') || contem('fundodeinvestimentoimobiliario') || contem('fundoimobiliario') || contem('fiagro')) { + return { chave: 'fiis' as const, nome: 'FIIs' }; + } + + if ( + contem('tesouro') || + contem('rendafixa') || + contem('cdb') || + contem('lci') || + contem('lca') || + contem('debenture') || + contem('cri') || + contem('cra') || + contem('prefixado') || + contem('posfixado') || + contem('ipca') + ) { + return { chave: 'rendaFixa' as const, nome: 'Renda Fixa' }; + } + + if ( + contem('reit') || + contem('bdr') || + contem('adr') || + contem('exterior') || + contem('internacional') || + contem('global') || + contem('usa') || + contem('nasdaq') || + contem('nyse') || + contem('stockexchange') || + contem('etf') + ) { + return { chave: 'exterior' as const, nome: 'Exterior' }; + } + + if (contem('cripto') || contem('bitcoin') || contem('ethereum') || contem('defi')) { + return { chave: 'cripto' as const, nome: 'Cripto' }; + } + + if (contem('reserva') || contem('poupanca') || contem('emergencia')) { + return { chave: 'reserva' as const, nome: 'Reserva' }; + } + + if (contem('outro')) { + return { chave: 'outros' as const, nome: 'Outros' }; + } + + console.warn(`âš ï¸ Categoria não reconhecida: ${categoria}. Será classificada como Outros.`); + return { chave: 'outros' as const, nome: 'Outros' }; + }; + + const linhasDados = linhasPlanilha.slice(headerRowIndex + 1); + + for (const linha of linhasDados) { + if (!linha) continue; + + const ativo = obterValor(linha, 'ativo'); + const categoriaRaw = obterValor(linha, 'categoria'); + const precoMedio = extrairValorNumerico(obterValor(linha, 'precoMedio')); + + if (!ativo || !categoriaRaw) continue; + if (!precoMedio || precoMedio <= 0) continue; + + const valorInvestidoInformado = extrairValorNumerico(obterValor(linha, 'valorInvestido')); + let quantidade = extrairValorNumerico(obterValor(linha, 'quantidade')); + + if ((!quantidade || quantidade <= 0) && valorInvestidoInformado && valorInvestidoInformado > 0) { + quantidade = valorInvestidoInformado / precoMedio; + } + + if (!quantidade || quantidade <= 0) continue; + + const precoAtual = extrairValorNumerico(obterValor(linha, 'precoAtual')); + const valorAtualInformado = extrairValorNumerico(obterValor(linha, 'valorAtual')); + const nome = obterValor(linha, 'nome') ? String(obterValor(linha, 'nome')).trim() : ''; + + const ativoStr = String(ativo).trim(); + const valorInvestido = valorInvestidoInformado ?? quantidade * precoMedio; + const valorAtual = valorAtualInformado ?? (precoAtual ? quantidade * precoAtual : valorInvestido); + const rentabilidade = valorInvestido > 0 ? ((valorAtual - valorInvestido) / valorInvestido) * 100 : 0; + + const categoriaNormalizada = normalizarCategoria(String(categoriaRaw)); + categoriasEncontradas.add(categoriaNormalizada.nome); + + const itemBase = { + codigo: ativoStr.toUpperCase(), + codigoOriginal: ativoStr, + nome, + quantidade, + precoMedio, + cotacaoAtual: precoAtual ?? precoMedio, + valorInvestido, + valorAtual, + rentabilidade, + pesoCarteira: 0, + categoriaOriginal: String(categoriaRaw).trim() + }; + + if (categoriaNormalizada.chave === 'acoes') { + dadosCarteira.acoes.push({ + ...itemBase, + tipo: 'ACAO' + }); + } else if (categoriaNormalizada.chave === 'fiis') { + dadosCarteira.fiis.push({ + ...itemBase, + tipo: 'FII' + }); + } else if (categoriaNormalizada.chave === 'rendaFixa') { + dadosCarteira.rendaFixa.push({ + ...itemBase, + produto: itemBase.codigo, + banco: itemBase.nome, + tipo: 'RENDA_FIXA' + }); + } else if (categoriaNormalizada.chave === 'exterior') { + dadosCarteira.exterior.push({ + ...itemBase, + tipo: 'EXTERIOR' + }); + } else if (categoriaNormalizada.chave === 'cripto') { + dadosCarteira.cripto.push({ + ...itemBase, + criptomoeda: itemBase.codigo, + exchange: itemBase.nome, + tipo: 'CRIPTO' + }); + } else if (categoriaNormalizada.chave === 'reserva') { + dadosCarteira.reserva.push({ + ...itemBase, + onde: itemBase.codigoOriginal, + banco: itemBase.nome, + valor: itemBase.valorAtual, + tipo: 'RESERVA' + }); + } else { + dadosCarteira.outros.push({ + ...itemBase, + tipo: 'OUTROS' + }); + } + } + + const abasSet = new Set(); + abasSet.add(abaPrincipal); + Array.from(categoriasEncontradas) + .filter(Boolean) + .forEach(categoria => abasSet.add(categoria)); + + dadosCarteira.abas = Array.from(abasSet); + + const ativosParaPeso = [ + ...dadosCarteira.acoes, + ...dadosCarteira.fiis, + ...dadosCarteira.rendaFixa, + ...dadosCarteira.exterior, + ...dadosCarteira.cripto, + ...dadosCarteira.outros + ]; + + const valorTotal = ativosParaPeso.reduce((total, item) => total + (item.valorAtual || 0), 0); + const quantidadeAtivos = ativosParaPeso.length; + + if (quantidadeAtivos === 0) { + console.error('⌠STEP 8: Nenhum ativo válido foi identificado na aba principal.'); + return NextResponse.json({ + error: 'Nenhum ativo válido encontrado na planilha.', + details: 'Verifique se as colunas possuem valores numéricos para quantidade e preço médio.' + }, { status: 400 }); + } + + const calcularPeso = (valor: number) => (valorTotal > 0 ? (valor / valorTotal) * 100 : 0); + + const aplicarPeso = (lista: any[]) => + lista.map(item => ({ + ...item, + pesoCarteira: calcularPeso(item.valorAtual || 0) + })); + + dadosCarteira.acoes = aplicarPeso(dadosCarteira.acoes); + dadosCarteira.fiis = aplicarPeso(dadosCarteira.fiis); + dadosCarteira.rendaFixa = aplicarPeso(dadosCarteira.rendaFixa); + dadosCarteira.exterior = aplicarPeso(dadosCarteira.exterior); + dadosCarteira.cripto = aplicarPeso(dadosCarteira.cripto); + dadosCarteira.outros = aplicarPeso(dadosCarteira.outros); + + console.log(`✅ STEP 8: Dados extraídos com sucesso!`); + console.log(`📊 Resumo: ${quantidadeAtivos} ativos, R$ ${valorTotal.toFixed(2)}`); + console.log(`📈 Ações: ${dadosCarteira.acoes.length}`); + console.log(`ðŸ˜ï¸ FIIs: ${dadosCarteira.fiis.length}`); + console.log(`💰 Renda Fixa: ${dadosCarteira.rendaFixa.length}`); + console.log(`🌎 Exterior: ${dadosCarteira.exterior.length}`); + console.log(`â‚¿ Cripto: ${dadosCarteira.cripto.length}`); + console.log(`📦 Outros: ${dadosCarteira.outros.length}`); + console.log(`🦠Reserva: ${dadosCarteira.reserva.length}`); + + const valorFinal = valorTotal; + const quantidadeFinal = quantidadeAtivos; + + // STEP 9: SALVAR NO BANCO - CORREÇÃO DEFINITIVA + console.log('ðŸ—„ï¸ STEP 9: Salvando no banco...'); + console.log('📠Dados que serão salvos:', { + userId: session.user.id, + nomeArquivo: file.name, + arquivoUrl: `uploads/${file.name}`, // VALOR CORRETO PARA arquivoUrl + valorTotal, + quantidadeAtivos, + status: 'PENDENTE' + }); + + try { + const novaCarteira = await prisma.carteiraAnalise.create({ + data: { + userId: session.user.id, + nomeArquivo: file.name, + arquivoUrl: `uploads/${file.name}`, + valorTotal: valorFinal, + quantidadeAtivos: quantidadeFinal, + status: 'PENDENTE', + dadosEstruturados: JSON.stringify(dadosCarteira) // Salvar dados extraídos + } + }); + + console.log('✅ STEP 9: Carteira salva com sucesso:', novaCarteira.id); + console.log('==================== DEBUG UPLOAD SUCCESS ===================='); + + return NextResponse.json({ + success: true, + message: 'Carteira enviada com sucesso!', + carteiraId: novaCarteira.id, + resumo: { + valorTotal: valorTotal, + quantidadeAtivos: quantidadeAtivos, + arquivoProcessado: true, + questionarioIncluido: !!questionario, + nomeArquivo: file.name + } + }); + + } catch (dbError) { + console.error('⌠STEP 9: ERRO FINAL no banco:', dbError); + console.error('⌠Mensagem completa:', dbError.message); + console.log('==================== DEBUG UPLOAD FAILED ===================='); + + return NextResponse.json({ + error: 'Erro ao salvar no banco de dados', + details: dbError.message, + step: 'database_save_final' + }, { status: 500 }); + } + + } catch (globalError) { + console.error('💥 ERRO GLOBAL:', globalError); + return NextResponse.json({ + error: 'Erro interno do servidor', + details: globalError.message + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/cf-renda-turbinada-turma-2/lessons/[lessonId]/materials/route.ts b/src/app/api/cf-renda-turbinada-turma-2/lessons/[lessonId]/materials/route.ts new file mode 100644 index 000000000..3d25ace1d --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-2/lessons/[lessonId]/materials/route.ts @@ -0,0 +1,84 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +import { serializeLessonMaterial } from '../../../utils'; + +export async function POST(request: NextRequest, { params }: { params: { lessonId: string } }) { + const lessonId = Number(params.lessonId); + + if (Number.isNaN(lessonId)) { + return NextResponse.json({ error: 'Lesson id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const lesson = await prisma.cfRendaTurbinadaTurma2Lesson.findUnique({ + where: { id: lessonId }, + include: { materials: true } + }); + + if (!lesson) { + return NextResponse.json({ error: 'Lesson not found' }, { status: 404 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, fileUrl } = body as { + title?: unknown; + fileUrl?: unknown; + }; + + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título é obrigatório' }, { status: 400 }); + } + + if (typeof fileUrl !== 'string' || !fileUrl.trim()) { + return NextResponse.json({ error: 'URL do arquivo é obrigatória' }, { status: 400 }); + } + + const trimmedTitle = title.trim(); + const trimmedFileUrl = fileUrl.trim(); + + const material = await prisma.cfRendaTurbinadaTurma2LessonMaterial.create({ + data: { + lessonId, + title: trimmedTitle, + fileUrl: trimmedFileUrl + } + }); + + return NextResponse.json({ material: serializeLessonMaterial(material) }, { status: 201 }); + } catch (error) { + console.error('Erro ao anexar material na aula do CF Renda Turbinada Turma 2:', error); + return NextResponse.json({ error: 'Erro ao anexar material' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-2/lessons/[lessonId]/route.ts b/src/app/api/cf-renda-turbinada-turma-2/lessons/[lessonId]/route.ts new file mode 100644 index 000000000..6726f3670 --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-2/lessons/[lessonId]/route.ts @@ -0,0 +1,211 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +import { serializeLesson } from '../../utils'; + +export async function PATCH(request: NextRequest, { params }: { params: { lessonId: string } }) { + const lessonId = Number(params.lessonId); + + if (Number.isNaN(lessonId)) { + return NextResponse.json({ error: 'Lesson id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const lesson = await prisma.cfRendaTurbinadaTurma2Lesson.findUnique({ + where: { id: lessonId }, + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + }); + + if (!lesson) { + return NextResponse.json({ error: 'Lesson not found' }, { status: 404 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, duration, description, videoUrl, content, sortOrder } = body as { + title?: unknown; + duration?: unknown; + description?: unknown; + videoUrl?: unknown; + content?: unknown; + sortOrder?: unknown; + }; + + const data: { + title?: string; + duration?: string; + description?: string | null; + videoUrl?: string | null; + content?: string | null; + sortOrder?: number | null; + } = {}; + + if (typeof title !== 'undefined') { + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título inválido' }, { status: 400 }); + } + + data.title = title.trim(); + } + + if (typeof duration !== 'undefined') { + if (typeof duration !== 'string' || !duration.trim()) { + return NextResponse.json({ error: 'Duração inválida' }, { status: 400 }); + } + + data.duration = duration.trim(); + } + + if (typeof description !== 'undefined') { + if (description !== null && typeof description !== 'string') { + return NextResponse.json({ error: 'Descrição inválida' }, { status: 400 }); + } + + if (typeof description === 'string') { + const trimmedDescription = description.trim(); + data.description = trimmedDescription.length > 0 ? trimmedDescription : null; + } else { + data.description = null; + } + } + + if (typeof videoUrl !== 'undefined') { + if (videoUrl !== null && typeof videoUrl !== 'string') { + return NextResponse.json({ error: 'URL do vídeo inválida' }, { status: 400 }); + } + + if (typeof videoUrl === 'string') { + const trimmedVideoUrl = videoUrl.trim(); + data.videoUrl = trimmedVideoUrl.length > 0 ? trimmedVideoUrl : null; + } else { + data.videoUrl = null; + } + } + + if (typeof content !== 'undefined') { + if (content !== null && typeof content !== 'string') { + return NextResponse.json({ error: 'Conteúdo inválido' }, { status: 400 }); + } + + if (typeof content === 'string') { + const trimmedContent = content.trim(); + data.content = trimmedContent.length > 0 ? trimmedContent : null; + } else { + data.content = null; + } + } + + if (typeof sortOrder !== 'undefined') { + if (sortOrder !== null && typeof sortOrder !== 'number') { + return NextResponse.json({ error: 'Posição inválida' }, { status: 400 }); + } + + data.sortOrder = sortOrder; + } + + if (Object.keys(data).length === 0) { + return NextResponse.json({ error: 'Nenhum campo para atualizar' }, { status: 400 }); + } + + const updated = await prisma.cfRendaTurbinadaTurma2Lesson.update({ + where: { id: lessonId }, + data, + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + }); + + return NextResponse.json({ lesson: serializeLesson(updated) }); + } catch (error) { + console.error('Erro ao atualizar aula do CF Renda Turbinada Turma 2:', error); + return NextResponse.json({ error: 'Erro ao atualizar aula' }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest, { params }: { params: { lessonId: string } }) { + const lessonId = Number(params.lessonId); + + if (Number.isNaN(lessonId)) { + return NextResponse.json({ error: 'Lesson id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const existing = await prisma.cfRendaTurbinadaTurma2Lesson.findUnique({ + where: { id: lessonId } + }); + + if (!existing) { + return NextResponse.json({ error: 'Lesson not found' }, { status: 404 }); + } + + await prisma.cfRendaTurbinadaTurma2Lesson.delete({ + where: { id: lessonId } + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao excluir aula do CF Renda Turbinada Turma 2:', error); + return NextResponse.json({ error: 'Erro ao excluir aula' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-2/lessons/reorder/route.ts b/src/app/api/cf-renda-turbinada-turma-2/lessons/reorder/route.ts new file mode 100644 index 000000000..8a9a6c300 --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-2/lessons/reorder/route.ts @@ -0,0 +1,89 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { moduleId, lessons } = body as { + moduleId?: unknown; + lessons?: unknown; + }; + + if (typeof moduleId !== 'string' || !moduleId.trim()) { + return NextResponse.json({ error: 'ID do módulo é obrigatório' }, { status: 400 }); + } + + if (!Array.isArray(lessons) || lessons.length === 0) { + return NextResponse.json({ error: 'Lista de aulas inválida' }, { status: 400 }); + } + + const validLessons = lessons.every( + lesson => lesson && typeof lesson === 'object' && typeof (lesson as { id?: unknown }).id === 'number' + ); + + if (!validLessons) { + return NextResponse.json({ error: 'Formato inválido para aulas' }, { status: 400 }); + } + + await prisma.$transaction( + lessons.map((lesson, index) => + prisma.cfRendaTurbinadaTurma2Lesson.update({ + where: { id: (lesson as { id: number }).id }, + data: { sortOrder: index + 1 } + }) + ) + ); + + const reordered = await prisma.cfRendaTurbinadaTurma2Lesson.findMany({ + where: { moduleId }, + orderBy: [ + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + }); + + return NextResponse.json({ lessons: reordered }); + } catch (error) { + console.error('Erro ao reordenar aulas do CF Renda Turbinada Turma 2:', error); + return NextResponse.json({ error: 'Erro ao reordenar aulas' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-2/live-events/[id]/route.ts b/src/app/api/cf-renda-turbinada-turma-2/live-events/[id]/route.ts new file mode 100644 index 000000000..3e9f8e142 --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-2/live-events/[id]/route.ts @@ -0,0 +1,220 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeLiveEvent } from '../../utils'; + +function parseDurationMinutes(value: unknown): number | null { + if (typeof value === 'number' && Number.isFinite(value)) { + const rounded = Math.round(value); + return rounded > 0 ? rounded : null; + } + + if (typeof value === 'string') { + const trimmed = value.trim(); + if (!trimmed) { + return null; + } + + const parsed = Number.parseInt(trimmed, 10); + if (!Number.isFinite(parsed) || Number.isNaN(parsed)) { + return null; + } + + return parsed > 0 ? parsed : null; + } + + return null; +} + +export async function PATCH( + request: NextRequest, + { params }: { params: { id: string } } +) { + const liveId = Number(params.id); + + if (!params.id || Number.isNaN(liveId)) { + return NextResponse.json({ error: 'Live id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const existingEvent = await prisma.cfRendaTurbinadaTurma2LiveEvent.findUnique({ + where: { id: liveId } + }); + + if (!existingEvent) { + return NextResponse.json({ error: 'Live event not found' }, { status: 404 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, description, eventDate, durationMinutes, meetingUrl, isCancelled } = body as { + title?: unknown; + description?: unknown; + eventDate?: unknown; + durationMinutes?: unknown; + meetingUrl?: unknown; + isCancelled?: unknown; + }; + + const data: { + title?: string; + description?: string | null; + eventDate?: Date; + durationMinutes?: number; + meetingUrl?: string | null; + isCancelled?: boolean; + } = {}; + + if (typeof title !== 'undefined') { + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título inválido' }, { status: 400 }); + } + + data.title = title.trim(); + } + + if (typeof description !== 'undefined') { + if (description !== null && typeof description !== 'string') { + return NextResponse.json({ error: 'Descrição inválida' }, { status: 400 }); + } + + if (typeof description === 'string') { + const trimmedDescription = description.trim(); + data.description = trimmedDescription.length > 0 ? trimmedDescription : null; + } else { + data.description = null; + } + } + + if (typeof eventDate !== 'undefined') { + if (typeof eventDate !== 'string' || !eventDate.trim()) { + return NextResponse.json({ error: 'Data e horário inválidos' }, { status: 400 }); + } + + const parsedDate = new Date(eventDate); + + if (Number.isNaN(parsedDate.getTime())) { + return NextResponse.json({ error: 'Data e horário inválidos' }, { status: 400 }); + } + + data.eventDate = parsedDate; + } + + if (typeof durationMinutes !== 'undefined') { + const parsedDuration = parseDurationMinutes(durationMinutes); + + if (parsedDuration === null) { + return NextResponse.json({ error: 'Duração inválida' }, { status: 400 }); + } + + data.durationMinutes = parsedDuration; + } + + if (typeof meetingUrl !== 'undefined') { + if (meetingUrl !== null && typeof meetingUrl !== 'string') { + return NextResponse.json({ error: 'Link de reunião inválido' }, { status: 400 }); + } + + if (typeof meetingUrl === 'string') { + const trimmedUrl = meetingUrl.trim(); + data.meetingUrl = trimmedUrl.length > 0 ? trimmedUrl : null; + } else { + data.meetingUrl = null; + } + } + + if (typeof isCancelled !== 'undefined') { + if (typeof isCancelled !== 'boolean') { + return NextResponse.json({ error: 'Status de cancelamento inválido' }, { status: 400 }); + } + + data.isCancelled = isCancelled; + } + + if (Object.keys(data).length === 0) { + return NextResponse.json({ error: 'Nenhum campo para atualizar' }, { status: 400 }); + } + + const updatedEvent = await prisma.cfRendaTurbinadaTurma2LiveEvent.update({ + where: { id: liveId }, + data + }); + + return NextResponse.json({ event: serializeLiveEvent(updatedEvent) }); + } catch (error) { + console.error('Erro ao atualizar live do CF Renda Turbinada Turma 2:', error); + return NextResponse.json({ error: 'Erro ao atualizar live' }, { status: 500 }); + } +} + +export async function DELETE( + _request: NextRequest, + { params }: { params: { id: string } } +) { + const liveId = Number(params.id); + + if (!params.id || Number.isNaN(liveId)) { + return NextResponse.json({ error: 'Live id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + await prisma.cfRendaTurbinadaTurma2LiveEvent.delete({ + where: { id: liveId } + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao excluir live do CF Renda Turbinada Turma 2:', error); + return NextResponse.json({ error: 'Erro ao excluir live' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-2/live-events/route.ts b/src/app/api/cf-renda-turbinada-turma-2/live-events/route.ts new file mode 100644 index 000000000..27d1c46af --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-2/live-events/route.ts @@ -0,0 +1,170 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeLiveEvent } from '../utils'; + +function parseDurationMinutes(value: unknown): number | null { + if (typeof value === 'number' && Number.isFinite(value)) { + const rounded = Math.round(value); + return rounded > 0 ? rounded : null; + } + + if (typeof value === 'string') { + const trimmed = value.trim(); + if (!trimmed) { + return null; + } + + const parsed = Number.parseInt(trimmed, 10); + if (!Number.isFinite(parsed) || Number.isNaN(parsed)) { + return null; + } + + return parsed > 0 ? parsed : null; + } + + return null; +} + +export async function GET() { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const events = await prisma.cfRendaTurbinadaTurma2LiveEvent.findMany({ + orderBy: [ + { eventDate: 'asc' }, + { createdAt: 'asc' } + ] + }); + + return NextResponse.json({ events: events.map(serializeLiveEvent) }); + } catch (error) { + console.error('Erro ao listar lives do CF Renda Turbinada Turma 2:', error); + return NextResponse.json({ error: 'Erro ao listar lives' }, { status: 500 }); + } +} + +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, description, eventDate, durationMinutes, meetingUrl } = body as { + title?: unknown; + description?: unknown; + eventDate?: unknown; + durationMinutes?: unknown; + meetingUrl?: unknown; + }; + + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título é obrigatório' }, { status: 400 }); + } + + if (typeof eventDate !== 'string' || !eventDate.trim()) { + return NextResponse.json({ error: 'Data e horário são obrigatórios' }, { status: 400 }); + } + + const parsedDate = new Date(eventDate); + + if (Number.isNaN(parsedDate.getTime())) { + return NextResponse.json({ error: 'Data e horário inválidos' }, { status: 400 }); + } + + let normalizedDuration = 60; + + if (typeof durationMinutes !== 'undefined') { + const parsedDuration = parseDurationMinutes(durationMinutes); + + if (parsedDuration === null) { + return NextResponse.json({ error: 'Duração inválida' }, { status: 400 }); + } + + normalizedDuration = parsedDuration; + } + + let normalizedDescription: string | null = null; + + if (typeof description !== 'undefined') { + if (description !== null && typeof description !== 'string') { + return NextResponse.json({ error: 'Descrição inválida' }, { status: 400 }); + } + + if (typeof description === 'string') { + const trimmedDescription = description.trim(); + normalizedDescription = trimmedDescription.length > 0 ? trimmedDescription : null; + } + } + + let normalizedMeetingUrl: string | null = null; + + if (typeof meetingUrl !== 'undefined') { + if (meetingUrl !== null && typeof meetingUrl !== 'string') { + return NextResponse.json({ error: 'Link de reunião inválido' }, { status: 400 }); + } + + if (typeof meetingUrl === 'string') { + const trimmedUrl = meetingUrl.trim(); + normalizedMeetingUrl = trimmedUrl.length > 0 ? trimmedUrl : null; + } + } + + const event = await prisma.cfRendaTurbinadaTurma2LiveEvent.create({ + data: { + title: title.trim(), + description: normalizedDescription, + eventDate: parsedDate, + durationMinutes: normalizedDuration, + meetingUrl: normalizedMeetingUrl + } + }); + + return NextResponse.json({ event: serializeLiveEvent(event) }, { status: 201 }); + } catch (error) { + console.error('Erro ao criar live do CF Renda Turbinada Turma 2:', error); + return NextResponse.json({ error: 'Erro ao criar live' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-2/mentorias/[mentoriaId]/materials/route.ts b/src/app/api/cf-renda-turbinada-turma-2/mentorias/[mentoriaId]/materials/route.ts new file mode 100644 index 000000000..9069d3ba1 --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-2/mentorias/[mentoriaId]/materials/route.ts @@ -0,0 +1,95 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeMentoriaMaterial } from '../../../utils'; +import { + DEFAULT_ALLOWED_MIME_TYPES, + MAX_FILE_SIZE_BYTES, + isFileLike, + processUploadedFile, + validateFile +} from '../../../upload-utils'; + +export async function POST(request: NextRequest, { params }: { params: { mentoriaId: string } }) { + const mentoriaId = Number(params.mentoriaId); + + if (!params.mentoriaId || Number.isNaN(mentoriaId)) { + return NextResponse.json({ error: 'Mentoria id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const mentoria = await prisma.cfRendaTurbinadaTurma2Mentoria.findUnique({ + where: { id: mentoriaId } + }); + + if (!mentoria) { + return NextResponse.json({ error: 'Mentoria not found' }, { status: 404 }); + } + + const formData = await request.formData(); + const file = formData.get('file'); + const title = formData.get('title'); + + if (!isFileLike(file)) { + return NextResponse.json({ error: 'Arquivo é obrigatório' }, { status: 400 }); + } + + const validationError = validateFile(file, { + allowedMimeTypes: DEFAULT_ALLOWED_MIME_TYPES, + maxFileSizeBytes: MAX_FILE_SIZE_BYTES + }); + + if (validationError) { + return NextResponse.json({ error: validationError }, { status: 400 }); + } + + const processedFile = await processUploadedFile(file, { + uploadFolder: 'cf-renda-turbinada-turma-2/mentorias-materials' + }); + + const material = await prisma.cfRendaTurbinadaTurma2MentoriaMaterial.create({ + data: { + mentoriaId, + title: + typeof title === 'string' && title.trim().length > 0 + ? title.trim() + : processedFile.fileName, + fileUrl: processedFile.fileUrl + } + }); + + return NextResponse.json({ material: serializeMentoriaMaterial(material) }, { status: 201 }); + } catch (error) { + console.error('Erro ao anexar material na mentoria gravada do CF Renda Turbinada Turma 2:', error); + + if (error instanceof Error) { + return NextResponse.json({ error: error.message }, { status: 400 }); + } + + return NextResponse.json({ error: 'Erro ao anexar material' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-2/mentorias/[mentoriaId]/route.ts b/src/app/api/cf-renda-turbinada-turma-2/mentorias/[mentoriaId]/route.ts new file mode 100644 index 000000000..669e38ab0 --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-2/mentorias/[mentoriaId]/route.ts @@ -0,0 +1,240 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeMentoria } from '../../utils'; + +function parseDuration(value: unknown): number | null { + if (typeof value === 'number' && Number.isFinite(value)) { + const rounded = Math.round(value); + return rounded > 0 ? rounded : null; + } + + if (typeof value === 'string') { + const trimmed = value.trim(); + + if (!trimmed) { + return null; + } + + const parsed = Number.parseInt(trimmed, 10); + + if (!Number.isFinite(parsed) || Number.isNaN(parsed)) { + return null; + } + + return parsed > 0 ? parsed : null; + } + + return null; +} + +export async function PATCH(request: NextRequest, { params }: { params: { mentoriaId: string } }) { + const mentoriaId = Number(params.mentoriaId); + + if (!params.mentoriaId || Number.isNaN(mentoriaId)) { + return NextResponse.json({ error: 'Mentoria id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, description, content, recordedAt, releaseDate, videoUrl, durationMinutes } = body as { + title?: unknown; + description?: unknown; + content?: unknown; + recordedAt?: unknown; + releaseDate?: unknown; + videoUrl?: unknown; + durationMinutes?: unknown; + }; + + const updateData: Record = {}; + + if (typeof title !== 'undefined') { + if (title !== null && typeof title !== 'string') { + return NextResponse.json({ error: 'Título inválido' }, { status: 400 }); + } + + if (typeof title === 'string') { + if (!title.trim()) { + return NextResponse.json({ error: 'Título é obrigatório' }, { status: 400 }); + } + + updateData.title = title.trim(); + } + } + + if (typeof recordedAt !== 'undefined') { + if (recordedAt !== null && typeof recordedAt !== 'string') { + return NextResponse.json({ error: 'Data da mentoria inválida' }, { status: 400 }); + } + + if (typeof recordedAt === 'string') { + if (!recordedAt.trim()) { + return NextResponse.json({ error: 'Data da mentoria é obrigatória' }, { status: 400 }); + } + + const parsedRecordedAt = new Date(recordedAt); + + if (Number.isNaN(parsedRecordedAt.getTime())) { + return NextResponse.json({ error: 'Data da mentoria inválida' }, { status: 400 }); + } + + updateData.recordedAt = parsedRecordedAt; + } + } + + if (typeof releaseDate !== 'undefined') { + if (releaseDate !== null && typeof releaseDate !== 'string') { + return NextResponse.json({ error: 'Data de liberação inválida' }, { status: 400 }); + } + + if (typeof releaseDate === 'string') { + if (!releaseDate.trim()) { + return NextResponse.json({ error: 'Data de liberação é obrigatória' }, { status: 400 }); + } + + const parsedReleaseDate = new Date(releaseDate); + + if (Number.isNaN(parsedReleaseDate.getTime())) { + return NextResponse.json({ error: 'Data de liberação inválida' }, { status: 400 }); + } + + updateData.releaseDate = parsedReleaseDate; + } + } + + if (typeof description !== 'undefined') { + if (description !== null && typeof description !== 'string') { + return NextResponse.json({ error: 'Descrição inválida' }, { status: 400 }); + } + + if (typeof description === 'string') { + const trimmed = description.trim(); + updateData.description = trimmed.length > 0 ? trimmed : null; + } else { + updateData.description = null; + } + } + + if (typeof content !== 'undefined') { + if (content !== null && typeof content !== 'string') { + return NextResponse.json({ error: 'Conteúdo inválido' }, { status: 400 }); + } + + if (typeof content === 'string') { + const trimmed = content.trim(); + updateData.content = trimmed.length > 0 ? trimmed : null; + } else { + updateData.content = null; + } + } + + if (typeof videoUrl !== 'undefined') { + if (videoUrl !== null && typeof videoUrl !== 'string') { + return NextResponse.json({ error: 'Link do vídeo inválido' }, { status: 400 }); + } + + if (typeof videoUrl === 'string') { + const trimmed = videoUrl.trim(); + updateData.videoUrl = trimmed.length > 0 ? trimmed : null; + } else { + updateData.videoUrl = null; + } + } + + if (typeof durationMinutes !== 'undefined') { + if (durationMinutes === null) { + updateData.durationMinutes = null; + } else { + const parsedDuration = parseDuration(durationMinutes); + + if (parsedDuration === null) { + return NextResponse.json({ error: 'Duração inválida' }, { status: 400 }); + } + + updateData.durationMinutes = parsedDuration; + } + } + + const mentoria = await prisma.cfRendaTurbinadaTurma2Mentoria.update({ + where: { id: mentoriaId }, + data: updateData, + include: { materials: true } + }); + + return NextResponse.json({ mentoria: serializeMentoria(mentoria) }); + } catch (error) { + console.error('Erro ao atualizar mentoria gravada do CF Renda Turbinada Turma 2:', error); + return NextResponse.json({ error: 'Erro ao atualizar mentoria' }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest, { params }: { params: { mentoriaId: string } }) { + const mentoriaId = Number(params.mentoriaId); + + if (!params.mentoriaId || Number.isNaN(mentoriaId)) { + return NextResponse.json({ error: 'Mentoria id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + await prisma.cfRendaTurbinadaTurma2Mentoria.delete({ + where: { id: mentoriaId } + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao excluir mentoria gravada do CF Renda Turbinada Turma 2:', error); + return NextResponse.json({ error: 'Erro ao excluir mentoria' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-2/mentorias/materials/[materialId]/route.ts b/src/app/api/cf-renda-turbinada-turma-2/mentorias/materials/[materialId]/route.ts new file mode 100644 index 000000000..261d342d0 --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-2/mentorias/materials/[materialId]/route.ts @@ -0,0 +1,65 @@ +import { unlink } from 'fs/promises'; +import path from 'path'; +import { NextRequest, NextResponse } from 'next/server'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +export async function DELETE(request: NextRequest, { params }: { params: { materialId: string } }) { + const materialId = Number(params.materialId); + + if (!params.materialId || Number.isNaN(materialId)) { + return NextResponse.json({ error: 'Material id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const material = await prisma.cfRendaTurbinadaTurma2MentoriaMaterial.findUnique({ + where: { id: materialId } + }); + + if (!material) { + return NextResponse.json({ error: 'Material not found' }, { status: 404 }); + } + + await prisma.cfRendaTurbinadaTurma2MentoriaMaterial.delete({ + where: { id: materialId } + }); + + if (material.fileUrl.startsWith('/uploads/')) { + const filePath = path.join(process.cwd(), 'public', material.fileUrl.replace(/^\//, '')); + + try { + await unlink(filePath); + } catch (error) { + console.warn('Não foi possível remover arquivo de material de mentoria gravada (Turma 2):', error); + } + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao remover material de mentoria gravada do CF Renda Turbinada Turma 2:', error); + return NextResponse.json({ error: 'Erro ao remover material' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-2/mentorias/reorder/route.ts b/src/app/api/cf-renda-turbinada-turma-2/mentorias/reorder/route.ts new file mode 100644 index 000000000..18e249cba --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-2/mentorias/reorder/route.ts @@ -0,0 +1,135 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeMentoria } from '../../utils'; + +function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); +} + +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { mentoriaId, targetPosition } = body as { mentoriaId?: unknown; targetPosition?: unknown }; + + const parsedMentoriaId = + typeof mentoriaId === 'number' + ? mentoriaId + : typeof mentoriaId === 'string' + ? Number.parseInt(mentoriaId, 10) + : Number.NaN; + + if (Number.isNaN(parsedMentoriaId) || parsedMentoriaId <= 0) { + return NextResponse.json({ error: 'ID da mentoria inválido' }, { status: 400 }); + } + + const parsedPosition = + typeof targetPosition === 'number' + ? targetPosition + : typeof targetPosition === 'string' + ? Number.parseInt(targetPosition, 10) + : Number.NaN; + + if (Number.isNaN(parsedPosition) || parsedPosition < 1) { + return NextResponse.json({ error: 'Posição de destino inválida' }, { status: 400 }); + } + + const mentorias = await prisma.cfRendaTurbinadaTurma2Mentoria.findMany({ + orderBy: [ + { sortOrder: 'asc' }, + { releaseDate: 'desc' }, + { recordedAt: 'desc' }, + { createdAt: 'desc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + }); + + if (mentorias.length === 0) { + return NextResponse.json({ error: 'Nenhuma mentoria encontrada' }, { status: 404 }); + } + + const orderedIds = mentorias.map(mentoria => mentoria.id); + + if (!orderedIds.includes(parsedMentoriaId)) { + return NextResponse.json({ error: 'Mentoria não encontrada' }, { status: 404 }); + } + + const otherIds = orderedIds.filter(id => id !== parsedMentoriaId); + const clampedIndex = clamp(parsedPosition - 1, 0, otherIds.length); + otherIds.splice(clampedIndex, 0, parsedMentoriaId); + + await prisma.$transaction(async tx => { + for (let index = 0; index < otherIds.length; index += 1) { + const id = otherIds[index]; + + await tx.cfRendaTurbinadaTurma2Mentoria.update({ + where: { id }, + data: { sortOrder: index + 1 } + }); + } + }); + + const updatedMentorias = await prisma.cfRendaTurbinadaTurma2Mentoria.findMany({ + orderBy: [ + { sortOrder: 'asc' }, + { releaseDate: 'desc' }, + { recordedAt: 'desc' }, + { createdAt: 'desc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + }); + + const serialized = updatedMentorias.map(mentoria => ({ ...serializeMentoria(mentoria), isReleased: true })); + + return NextResponse.json({ mentorias: serialized }); + } catch (error) { + console.error('Erro ao reordenar mentorias do CF Renda Turbinada Turma 2:', error); + return NextResponse.json({ error: 'Erro ao reordenar mentorias' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-2/mentorias/route.ts b/src/app/api/cf-renda-turbinada-turma-2/mentorias/route.ts new file mode 100644 index 000000000..bbc756955 --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-2/mentorias/route.ts @@ -0,0 +1,241 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeMentoria } from '../utils'; + +function parseDuration(value: unknown): number | null { + if (typeof value === 'number' && Number.isFinite(value)) { + const rounded = Math.round(value); + return rounded > 0 ? rounded : null; + } + + if (typeof value === 'string') { + const trimmed = value.trim(); + + if (!trimmed) { + return null; + } + + const parsed = Number.parseInt(trimmed, 10); + + if (!Number.isFinite(parsed) || Number.isNaN(parsed)) { + return null; + } + + return parsed > 0 ? parsed : null; + } + + return null; +} + +export async function GET() { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const isAdmin = user.plan === 'ADMIN'; + const now = new Date(); + + const mentorias = await prisma.cfRendaTurbinadaTurma2Mentoria.findMany({ + orderBy: [ + { sortOrder: 'asc' }, + { releaseDate: 'desc' }, + { recordedAt: 'desc' }, + { createdAt: 'desc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + }); + + const serializedMentorias = mentorias.map(mentoria => { + const base = serializeMentoria(mentoria); + const releaseTime = mentoria.releaseDate?.getTime(); + const isReleased = isAdmin || (typeof releaseTime === 'number' && releaseTime <= now.getTime()); + + if (isAdmin || isReleased) { + return { + ...base, + isReleased + }; + } + + return { + ...base, + isReleased, + content: undefined, + videoUrl: undefined, + materials: [] + }; + }); + + return NextResponse.json({ mentorias: serializedMentorias }); + } catch (error) { + console.error('Erro ao listar mentorias gravadas do CF Renda Turbinada Turma 2:', error); + return NextResponse.json({ error: 'Erro ao listar mentorias' }, { status: 500 }); + } +} + +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, description, content, recordedAt, releaseDate, videoUrl, durationMinutes } = body as { + title?: unknown; + description?: unknown; + content?: unknown; + recordedAt?: unknown; + releaseDate?: unknown; + videoUrl?: unknown; + durationMinutes?: unknown; + }; + + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título é obrigatório' }, { status: 400 }); + } + + if (typeof recordedAt !== 'string' || !recordedAt.trim()) { + return NextResponse.json({ error: 'Data da mentoria é obrigatória' }, { status: 400 }); + } + + const parsedRecordedAt = new Date(recordedAt); + + if (Number.isNaN(parsedRecordedAt.getTime())) { + return NextResponse.json({ error: 'Data da mentoria inválida' }, { status: 400 }); + } + + if (typeof releaseDate !== 'string' || !releaseDate.trim()) { + return NextResponse.json({ error: 'Data de liberação é obrigatória' }, { status: 400 }); + } + + const parsedReleaseDate = new Date(releaseDate); + + if (Number.isNaN(parsedReleaseDate.getTime())) { + return NextResponse.json({ error: 'Data de liberação inválida' }, { status: 400 }); + } + + let normalizedDescription: string | null = null; + + if (typeof description !== 'undefined') { + if (description !== null && typeof description !== 'string') { + return NextResponse.json({ error: 'Descrição inválida' }, { status: 400 }); + } + + if (typeof description === 'string') { + const trimmedDescription = description.trim(); + normalizedDescription = trimmedDescription.length > 0 ? trimmedDescription : null; + } + } + + let normalizedContent: string | null = null; + + if (typeof content !== 'undefined') { + if (content !== null && typeof content !== 'string') { + return NextResponse.json({ error: 'Conteúdo inválido' }, { status: 400 }); + } + + if (typeof content === 'string') { + const trimmedContent = content.trim(); + normalizedContent = trimmedContent.length > 0 ? trimmedContent : null; + } + } + + let normalizedVideoUrl: string | null = null; + + if (typeof videoUrl !== 'undefined') { + if (videoUrl !== null && typeof videoUrl !== 'string') { + return NextResponse.json({ error: 'Link do vídeo inválido' }, { status: 400 }); + } + + if (typeof videoUrl === 'string') { + const trimmedUrl = videoUrl.trim(); + normalizedVideoUrl = trimmedUrl.length > 0 ? trimmedUrl : null; + } + } + + let normalizedDuration: number | null = null; + + if (typeof durationMinutes !== 'undefined') { + const parsedDuration = parseDuration(durationMinutes); + + if (parsedDuration === null) { + return NextResponse.json({ error: 'Duração inválida' }, { status: 400 }); + } + + normalizedDuration = parsedDuration; + } + + const sortOrderAggregate = await prisma.cfRendaTurbinadaTurma2Mentoria.aggregate({ + _max: { sortOrder: true } + }); + + const nextSortOrder = (sortOrderAggregate._max.sortOrder ?? 0) + 1; + + const mentoria = await prisma.cfRendaTurbinadaTurma2Mentoria.create({ + data: { + title: title.trim(), + description: normalizedDescription, + content: normalizedContent, + videoUrl: normalizedVideoUrl, + recordedAt: parsedRecordedAt, + releaseDate: parsedReleaseDate, + durationMinutes: normalizedDuration ?? undefined, + sortOrder: nextSortOrder + }, + include: { materials: true } + }); + + return NextResponse.json({ mentoria: serializeMentoria(mentoria) }, { status: 201 }); + } catch (error) { + console.error('Erro ao criar mentoria gravada do CF Renda Turbinada Turma 2:', error); + return NextResponse.json({ error: 'Erro ao criar mentoria' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-2/modules/[id]/lessons/route.ts b/src/app/api/cf-renda-turbinada-turma-2/modules/[id]/lessons/route.ts new file mode 100644 index 000000000..101035259 --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-2/modules/[id]/lessons/route.ts @@ -0,0 +1,108 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +import { serializeLesson } from '../../../utils'; + +export async function POST(request: NextRequest, { params }: { params: { id: string } }) { + const { id: moduleId } = params; + + if (!moduleId || typeof moduleId !== 'string') { + return NextResponse.json({ error: 'Module id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const module = await prisma.cfRendaTurbinadaTurma2Module.findUnique({ + where: { id: moduleId } + }); + + if (!module) { + return NextResponse.json({ error: 'Module not found' }, { status: 404 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, duration, description, videoUrl, content } = body as { + title?: unknown; + duration?: unknown; + description?: unknown; + videoUrl?: unknown; + content?: unknown; + }; + + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título é obrigatório' }, { status: 400 }); + } + + if (typeof duration !== 'string' || !duration.trim()) { + return NextResponse.json({ error: 'Duração é obrigatória' }, { status: 400 }); + } + + const trimmedTitle = title.trim(); + const trimmedDuration = duration.trim(); + const trimmedDescription = typeof description === 'string' ? description.trim() : undefined; + const trimmedVideoUrl = typeof videoUrl === 'string' ? videoUrl.trim() : undefined; + const trimmedContent = typeof content === 'string' ? content.trim() : undefined; + + const aggregate = await prisma.cfRendaTurbinadaTurma2Lesson.aggregate({ + where: { moduleId }, + _max: { sortOrder: true } + }); + + const nextSortOrder = (aggregate._max.sortOrder ?? 0) + 1; + + const lesson = await prisma.cfRendaTurbinadaTurma2Lesson.create({ + data: { + moduleId, + title: trimmedTitle, + duration: trimmedDuration, + description: trimmedDescription && trimmedDescription.length > 0 ? trimmedDescription : null, + videoUrl: trimmedVideoUrl && trimmedVideoUrl.length > 0 ? trimmedVideoUrl : null, + content: trimmedContent && trimmedContent.length > 0 ? trimmedContent : null, + sortOrder: nextSortOrder + }, + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + }); + + return NextResponse.json({ lesson: serializeLesson(lesson) }, { status: 201 }); + } catch (error) { + console.error('Erro ao adicionar aula ao módulo do CF Renda Turbinada Turma 2:', error); + return NextResponse.json({ error: 'Erro ao adicionar aula ao módulo' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-2/modules/[id]/route.ts b/src/app/api/cf-renda-turbinada-turma-2/modules/[id]/route.ts new file mode 100644 index 000000000..0163c8cda --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-2/modules/[id]/route.ts @@ -0,0 +1,203 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +import { serializeModule } from '../../utils'; + +export async function PATCH(request: NextRequest, { params }: { params: { id: string } }) { + const { id } = params; + + if (!id || typeof id !== 'string') { + return NextResponse.json({ error: 'Module id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const module = await prisma.cfRendaTurbinadaTurma2Module.findUnique({ + where: { id }, + include: { + lessons: { + orderBy: [ + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + } + } + }); + + if (!module) { + return NextResponse.json({ error: 'Module not found' }, { status: 404 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, highlight, releaseDate, isHidden } = body as { + title?: unknown; + highlight?: unknown; + releaseDate?: unknown; + isHidden?: unknown; + }; + + const data: { + title?: string; + highlight?: string | null; + releaseDate?: Date; + isHidden?: boolean; + } = {}; + + if (typeof title !== 'undefined') { + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título inválido' }, { status: 400 }); + } + + data.title = title.trim(); + } + + if (typeof highlight !== 'undefined') { + if (highlight !== null && typeof highlight !== 'string') { + return NextResponse.json({ error: 'Destaque inválido' }, { status: 400 }); + } + + if (typeof highlight === 'string') { + const trimmedHighlight = highlight.trim(); + data.highlight = trimmedHighlight.length > 0 ? trimmedHighlight : null; + } else { + data.highlight = null; + } + } + + if (typeof releaseDate !== 'undefined') { + if (typeof releaseDate !== 'string' || !releaseDate.trim()) { + return NextResponse.json({ error: 'Data de liberação inválida' }, { status: 400 }); + } + + const parsedReleaseDate = new Date(releaseDate); + + if (Number.isNaN(parsedReleaseDate.getTime())) { + return NextResponse.json({ error: 'Data de liberação inválida' }, { status: 400 }); + } + + data.releaseDate = parsedReleaseDate; + } + + if (typeof isHidden !== 'undefined') { + if (typeof isHidden !== 'boolean') { + return NextResponse.json({ error: 'Valor inválido para visibilidade' }, { status: 400 }); + } + + data.isHidden = isHidden; + } + + if (Object.keys(data).length === 0) { + return NextResponse.json({ error: 'Nenhum campo para atualizar' }, { status: 400 }); + } + + const updated = await prisma.cfRendaTurbinadaTurma2Module.update({ + where: { id }, + data, + include: { + lessons: { + orderBy: [ + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + } + } + }); + + return NextResponse.json({ module: serializeModule(updated) }); + } catch (error) { + console.error('Erro ao atualizar módulo do CF Renda Turbinada Turma 2:', error); + return NextResponse.json({ error: 'Erro ao atualizar módulo' }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) { + const { id } = params; + + if (!id || typeof id !== 'string') { + return NextResponse.json({ error: 'Module id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const existingModule = await prisma.cfRendaTurbinadaTurma2Module.findUnique({ + where: { id } + }); + + if (!existingModule) { + return NextResponse.json({ error: 'Module not found' }, { status: 404 }); + } + + await prisma.cfRendaTurbinadaTurma2Module.delete({ + where: { id } + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao excluir módulo do CF Renda Turbinada Turma 2:', error); + return NextResponse.json({ error: 'Erro ao excluir módulo' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-2/modules/route.ts b/src/app/api/cf-renda-turbinada-turma-2/modules/route.ts new file mode 100644 index 000000000..dfb040d51 --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-2/modules/route.ts @@ -0,0 +1,189 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +import { serializeModule } from '../utils'; + +export async function GET(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const isAdmin = user.plan === 'ADMIN'; + const now = new Date(); + + const modules = await prisma.cfRendaTurbinadaTurma2Module.findMany({ + where: isAdmin + ? undefined + : { + isHidden: false + }, + orderBy: [ + { releaseDate: 'asc' }, + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ], + include: { + lessons: { + orderBy: [ + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + } + } + }); + + const serializedModules = modules.map(module => { + const baseModule = serializeModule(module); + const releaseTimestamp = module.releaseDate instanceof Date + ? module.releaseDate.getTime() + : Number.NaN; + const isReleased = isAdmin || (!Number.isNaN(releaseTimestamp) && releaseTimestamp <= now.getTime()); + + if (isReleased) { + return { + ...baseModule, + isReleased + }; + } + + const safeLessons = module.lessons.map(lesson => ({ + id: lesson.id, + title: lesson.title, + duration: lesson.duration, + description: lesson.description ?? undefined, + videoUrl: undefined, + content: undefined, + sortOrder: lesson.sortOrder ?? undefined, + materials: [] + })); + + return { + ...baseModule, + isReleased, + lessons: safeLessons + }; + }); + + return NextResponse.json({ modules: serializedModules }); + } catch (error) { + console.error('Erro ao listar módulos do CF Renda Turbinada Turma 2:', error); + return NextResponse.json({ error: 'Erro ao listar módulos' }, { status: 500 }); + } +} + +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const isAdmin = user.plan === 'ADMIN'; + + if (!isAdmin) { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, highlight, releaseDate } = body as { + title?: unknown; + highlight?: unknown; + releaseDate?: unknown; + }; + + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título é obrigatório' }, { status: 400 }); + } + + if (typeof releaseDate !== 'string' || !releaseDate.trim()) { + return NextResponse.json({ error: 'Data de liberação é obrigatória' }, { status: 400 }); + } + + const parsedReleaseDate = new Date(releaseDate); + + if (Number.isNaN(parsedReleaseDate.getTime())) { + return NextResponse.json({ error: 'Data de liberação inválida' }, { status: 400 }); + } + + const trimmedTitle = title.trim(); + const trimmedHighlight = typeof highlight === 'string' ? highlight.trim() : undefined; + + const sortOrderAggregate = await prisma.cfRendaTurbinadaTurma2Module.aggregate({ + _max: { sortOrder: true } + }); + + const nextSortOrder = (sortOrderAggregate._max.sortOrder ?? 0) + 1; + + const module = await prisma.cfRendaTurbinadaTurma2Module.create({ + data: { + title: trimmedTitle, + highlight: trimmedHighlight && trimmedHighlight.length > 0 ? trimmedHighlight : null, + releaseDate: parsedReleaseDate, + sortOrder: nextSortOrder + }, + include: { + lessons: { + orderBy: [ + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + } + } + }); + + return NextResponse.json({ module: serializeModule(module) }, { status: 201 }); + } catch (error) { + console.error('Erro ao criar módulo do CF Renda Turbinada Turma 2:', error); + return NextResponse.json({ error: 'Erro ao criar módulo' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-2/progress/[lessonId]/route.ts b/src/app/api/cf-renda-turbinada-turma-2/progress/[lessonId]/route.ts new file mode 100644 index 000000000..a16f1b793 --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-2/progress/[lessonId]/route.ts @@ -0,0 +1,87 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +export async function POST( + request: NextRequest, + { params }: { params: { lessonId: string } } +) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Não autorizado' }, { status: 401 }); + } + + const lessonId = Number.parseInt(params.lessonId, 10); + + if (Number.isNaN(lessonId)) { + return NextResponse.json({ error: 'ID da aula inválido' }, { status: 400 }); + } + + const lesson = await prisma.cfRendaTurbinadaTurma2Lesson.findUnique({ + where: { id: lessonId } + }); + + if (!lesson) { + return NextResponse.json({ error: 'Aula não encontrada' }, { status: 404 }); + } + + await prisma.lessonProgressTurma2.upsert({ + where: { + userId_lessonId: { + userId: session.user.id, + lessonId + } + }, + create: { + userId: session.user.id, + lessonId + }, + update: { + completedAt: new Date() + } + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao marcar aula como concluída (turma 2):', error); + return NextResponse.json({ error: 'Erro ao marcar aula como concluída' }, { status: 500 }); + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: { lessonId: string } } +) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Não autorizado' }, { status: 401 }); + } + + const lessonId = Number.parseInt(params.lessonId, 10); + + if (Number.isNaN(lessonId)) { + return NextResponse.json({ error: 'ID da aula inválido' }, { status: 400 }); + } + + await prisma.lessonProgressTurma2 + .delete({ + where: { + userId_lessonId: { + userId: session.user.id, + lessonId + } + } + }) + .catch(() => {}); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao desmarcar aula (turma 2):', error); + return NextResponse.json({ error: 'Erro ao desmarcar aula' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-2/progress/route.ts b/src/app/api/cf-renda-turbinada-turma-2/progress/route.ts new file mode 100644 index 000000000..6d3d9726a --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-2/progress/route.ts @@ -0,0 +1,31 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +export async function GET(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Não autorizado' }, { status: 401 }); + } + + const progress = await prisma.lessonProgressTurma2.findMany({ + where: { + userId: session.user.id + }, + select: { + lessonId: true, + completedAt: true + } + }); + + const completedLessonIds = progress.map(progressItem => progressItem.lessonId); + + return NextResponse.json({ completedLessons: completedLessonIds }); + } catch (error) { + console.error('Erro ao buscar progresso (turma 2):', error); + return NextResponse.json({ error: 'Erro ao buscar progresso' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-2/upload-utils.ts b/src/app/api/cf-renda-turbinada-turma-2/upload-utils.ts new file mode 100644 index 000000000..b77efb834 --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-2/upload-utils.ts @@ -0,0 +1 @@ +export * from '../cf-renda-turbinada/upload-utils'; diff --git a/src/app/api/cf-renda-turbinada-turma-2/utils.ts b/src/app/api/cf-renda-turbinada-turma-2/utils.ts new file mode 100644 index 000000000..0c9fa176a --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-2/utils.ts @@ -0,0 +1,94 @@ +import { + CfRendaTurbinadaTurma2Lesson, + CfRendaTurbinadaTurma2LessonMaterial, + CfRendaTurbinadaTurma2LiveEvent, + CfRendaTurbinadaTurma2Mentoria, + CfRendaTurbinadaTurma2MentoriaMaterial, + CfRendaTurbinadaTurma2Module +} from '@prisma/client'; + +type ModuleWithLessons = CfRendaTurbinadaTurma2Module & { + lessons: (CfRendaTurbinadaTurma2Lesson & { + materials: CfRendaTurbinadaTurma2LessonMaterial[]; + })[]; +}; + +type LessonWithMaterials = CfRendaTurbinadaTurma2Lesson & { + materials: CfRendaTurbinadaTurma2LessonMaterial[]; +}; + +type MentoriaWithMaterials = CfRendaTurbinadaTurma2Mentoria & { + materials: CfRendaTurbinadaTurma2MentoriaMaterial[]; +}; + +export function serializeLessonMaterial(material: CfRendaTurbinadaTurma2LessonMaterial) { + return { + id: material.id, + title: material.title, + fileUrl: material.fileUrl, + createdAt: material.createdAt.toISOString(), + updatedAt: material.updatedAt.toISOString() + }; +} + +export function serializeLesson(lesson: LessonWithMaterials) { + return { + id: lesson.id, + title: lesson.title, + duration: lesson.duration, + description: lesson.description ?? undefined, + videoUrl: lesson.videoUrl ?? undefined, + content: lesson.content ?? undefined, + sortOrder: lesson.sortOrder ?? undefined, + materials: lesson.materials.map(serializeLessonMaterial) + }; +} + +export function serializeModule(module: ModuleWithLessons) { + return { + id: module.id, + title: module.title, + highlight: module.highlight ?? '', + releaseDate: module.releaseDate.toISOString(), + isHidden: module.isHidden, + sortOrder: module.sortOrder ?? undefined, + lessons: module.lessons.map(serializeLesson) + }; +} + +export function serializeLiveEvent(event: CfRendaTurbinadaTurma2LiveEvent) { + return { + id: event.id, + title: event.title, + description: event.description ?? undefined, + eventDate: event.eventDate.toISOString(), + durationMinutes: event.durationMinutes, + meetingUrl: event.meetingUrl ?? undefined, + isCancelled: event.isCancelled + }; +} + +export function serializeMentoriaMaterial(material: CfRendaTurbinadaTurma2MentoriaMaterial) { + return { + id: material.id, + title: material.title, + fileUrl: material.fileUrl, + createdAt: material.createdAt.toISOString(), + updatedAt: material.updatedAt.toISOString() + }; +} + +export function serializeMentoria(mentoria: MentoriaWithMaterials) { + return { + id: mentoria.id, + title: mentoria.title, + description: mentoria.description ?? undefined, + content: mentoria.content ?? undefined, + videoUrl: mentoria.videoUrl ?? undefined, + recordedAt: mentoria.recordedAt.toISOString(), + releaseDate: mentoria.releaseDate?.toISOString(), + durationMinutes: mentoria.durationMinutes ?? undefined, + sortOrder: mentoria.sortOrder ?? undefined, + materials: mentoria.materials.map(serializeMentoriaMaterial) + }; +} diff --git a/src/app/api/cf-renda-turbinada-turma-3/lessons/[lessonId]/materials/route.ts b/src/app/api/cf-renda-turbinada-turma-3/lessons/[lessonId]/materials/route.ts new file mode 100644 index 000000000..0896c7998 --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-3/lessons/[lessonId]/materials/route.ts @@ -0,0 +1,84 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +import { serializeLessonMaterial } from '../../../utils'; + +export async function POST(request: NextRequest, { params }: { params: { lessonId: string } }) { + const lessonId = Number(params.lessonId); + + if (Number.isNaN(lessonId)) { + return NextResponse.json({ error: 'Lesson id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const lesson = await prisma.cfRendaTurbinadaTurma3Lesson.findUnique({ + where: { id: lessonId }, + include: { materials: true } + }); + + if (!lesson) { + return NextResponse.json({ error: 'Lesson not found' }, { status: 404 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, fileUrl } = body as { + title?: unknown; + fileUrl?: unknown; + }; + + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título é obrigatório' }, { status: 400 }); + } + + if (typeof fileUrl !== 'string' || !fileUrl.trim()) { + return NextResponse.json({ error: 'URL do arquivo é obrigatória' }, { status: 400 }); + } + + const trimmedTitle = title.trim(); + const trimmedFileUrl = fileUrl.trim(); + + const material = await prisma.cfRendaTurbinadaTurma3LessonMaterial.create({ + data: { + lessonId, + title: trimmedTitle, + fileUrl: trimmedFileUrl + } + }); + + return NextResponse.json({ material: serializeLessonMaterial(material) }, { status: 201 }); + } catch (error) { + console.error('Erro ao anexar material na aula do CF Renda Turbinada Turma 3:', error); + return NextResponse.json({ error: 'Erro ao anexar material' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-3/lessons/[lessonId]/route.ts b/src/app/api/cf-renda-turbinada-turma-3/lessons/[lessonId]/route.ts new file mode 100644 index 000000000..70315af7f --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-3/lessons/[lessonId]/route.ts @@ -0,0 +1,211 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +import { serializeLesson } from '../../utils'; + +export async function PATCH(request: NextRequest, { params }: { params: { lessonId: string } }) { + const lessonId = Number(params.lessonId); + + if (Number.isNaN(lessonId)) { + return NextResponse.json({ error: 'Lesson id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const lesson = await prisma.cfRendaTurbinadaTurma3Lesson.findUnique({ + where: { id: lessonId }, + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + }); + + if (!lesson) { + return NextResponse.json({ error: 'Lesson not found' }, { status: 404 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, duration, description, videoUrl, content, sortOrder } = body as { + title?: unknown; + duration?: unknown; + description?: unknown; + videoUrl?: unknown; + content?: unknown; + sortOrder?: unknown; + }; + + const data: { + title?: string; + duration?: string; + description?: string | null; + videoUrl?: string | null; + content?: string | null; + sortOrder?: number | null; + } = {}; + + if (typeof title !== 'undefined') { + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título inválido' }, { status: 400 }); + } + + data.title = title.trim(); + } + + if (typeof duration !== 'undefined') { + if (typeof duration !== 'string' || !duration.trim()) { + return NextResponse.json({ error: 'Duração inválida' }, { status: 400 }); + } + + data.duration = duration.trim(); + } + + if (typeof description !== 'undefined') { + if (description !== null && typeof description !== 'string') { + return NextResponse.json({ error: 'Descrição inválida' }, { status: 400 }); + } + + if (typeof description === 'string') { + const trimmedDescription = description.trim(); + data.description = trimmedDescription.length > 0 ? trimmedDescription : null; + } else { + data.description = null; + } + } + + if (typeof videoUrl !== 'undefined') { + if (videoUrl !== null && typeof videoUrl !== 'string') { + return NextResponse.json({ error: 'URL do vídeo inválida' }, { status: 400 }); + } + + if (typeof videoUrl === 'string') { + const trimmedVideoUrl = videoUrl.trim(); + data.videoUrl = trimmedVideoUrl.length > 0 ? trimmedVideoUrl : null; + } else { + data.videoUrl = null; + } + } + + if (typeof content !== 'undefined') { + if (content !== null && typeof content !== 'string') { + return NextResponse.json({ error: 'Conteúdo inválido' }, { status: 400 }); + } + + if (typeof content === 'string') { + const trimmedContent = content.trim(); + data.content = trimmedContent.length > 0 ? trimmedContent : null; + } else { + data.content = null; + } + } + + if (typeof sortOrder !== 'undefined') { + if (sortOrder !== null && typeof sortOrder !== 'number') { + return NextResponse.json({ error: 'Posição inválida' }, { status: 400 }); + } + + data.sortOrder = sortOrder; + } + + if (Object.keys(data).length === 0) { + return NextResponse.json({ error: 'Nenhum campo para atualizar' }, { status: 400 }); + } + + const updated = await prisma.cfRendaTurbinadaTurma3Lesson.update({ + where: { id: lessonId }, + data, + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + }); + + return NextResponse.json({ lesson: serializeLesson(updated) }); + } catch (error) { + console.error('Erro ao atualizar aula do CF Renda Turbinada Turma 3:', error); + return NextResponse.json({ error: 'Erro ao atualizar aula' }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest, { params }: { params: { lessonId: string } }) { + const lessonId = Number(params.lessonId); + + if (Number.isNaN(lessonId)) { + return NextResponse.json({ error: 'Lesson id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const existing = await prisma.cfRendaTurbinadaTurma3Lesson.findUnique({ + where: { id: lessonId } + }); + + if (!existing) { + return NextResponse.json({ error: 'Lesson not found' }, { status: 404 }); + } + + await prisma.cfRendaTurbinadaTurma3Lesson.delete({ + where: { id: lessonId } + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao excluir aula do CF Renda Turbinada Turma 3:', error); + return NextResponse.json({ error: 'Erro ao excluir aula' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-3/lessons/reorder/route.ts b/src/app/api/cf-renda-turbinada-turma-3/lessons/reorder/route.ts new file mode 100644 index 000000000..930c01347 --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-3/lessons/reorder/route.ts @@ -0,0 +1,89 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { moduleId, lessons } = body as { + moduleId?: unknown; + lessons?: unknown; + }; + + if (typeof moduleId !== 'string' || !moduleId.trim()) { + return NextResponse.json({ error: 'ID do módulo é obrigatório' }, { status: 400 }); + } + + if (!Array.isArray(lessons) || lessons.length === 0) { + return NextResponse.json({ error: 'Lista de aulas inválida' }, { status: 400 }); + } + + const validLessons = lessons.every( + lesson => lesson && typeof lesson === 'object' && typeof (lesson as { id?: unknown }).id === 'number' + ); + + if (!validLessons) { + return NextResponse.json({ error: 'Formato inválido para aulas' }, { status: 400 }); + } + + await prisma.$transaction( + lessons.map((lesson, index) => + prisma.cfRendaTurbinadaTurma3Lesson.update({ + where: { id: (lesson as { id: number }).id }, + data: { sortOrder: index + 1 } + }) + ) + ); + + const reordered = await prisma.cfRendaTurbinadaTurma3Lesson.findMany({ + where: { moduleId }, + orderBy: [ + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + }); + + return NextResponse.json({ lessons: reordered }); + } catch (error) { + console.error('Erro ao reordenar aulas do CF Renda Turbinada Turma 3:', error); + return NextResponse.json({ error: 'Erro ao reordenar aulas' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-3/live-events/[id]/route.ts b/src/app/api/cf-renda-turbinada-turma-3/live-events/[id]/route.ts new file mode 100644 index 000000000..71ed55bd8 --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-3/live-events/[id]/route.ts @@ -0,0 +1,220 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeLiveEvent } from '../../utils'; + +function parseDurationMinutes(value: unknown): number | null { + if (typeof value === 'number' && Number.isFinite(value)) { + const rounded = Math.round(value); + return rounded > 0 ? rounded : null; + } + + if (typeof value === 'string') { + const trimmed = value.trim(); + if (!trimmed) { + return null; + } + + const parsed = Number.parseInt(trimmed, 10); + if (!Number.isFinite(parsed) || Number.isNaN(parsed)) { + return null; + } + + return parsed > 0 ? parsed : null; + } + + return null; +} + +export async function PATCH( + request: NextRequest, + { params }: { params: { id: string } } +) { + const liveId = Number(params.id); + + if (!params.id || Number.isNaN(liveId)) { + return NextResponse.json({ error: 'Live id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const existingEvent = await prisma.cfRendaTurbinadaTurma3LiveEvent.findUnique({ + where: { id: liveId } + }); + + if (!existingEvent) { + return NextResponse.json({ error: 'Live event not found' }, { status: 404 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, description, eventDate, durationMinutes, meetingUrl, isCancelled } = body as { + title?: unknown; + description?: unknown; + eventDate?: unknown; + durationMinutes?: unknown; + meetingUrl?: unknown; + isCancelled?: unknown; + }; + + const data: { + title?: string; + description?: string | null; + eventDate?: Date; + durationMinutes?: number; + meetingUrl?: string | null; + isCancelled?: boolean; + } = {}; + + if (typeof title !== 'undefined') { + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título inválido' }, { status: 400 }); + } + + data.title = title.trim(); + } + + if (typeof description !== 'undefined') { + if (description !== null && typeof description !== 'string') { + return NextResponse.json({ error: 'Descrição inválida' }, { status: 400 }); + } + + if (typeof description === 'string') { + const trimmedDescription = description.trim(); + data.description = trimmedDescription.length > 0 ? trimmedDescription : null; + } else { + data.description = null; + } + } + + if (typeof eventDate !== 'undefined') { + if (typeof eventDate !== 'string' || !eventDate.trim()) { + return NextResponse.json({ error: 'Data e horário inválidos' }, { status: 400 }); + } + + const parsedDate = new Date(eventDate); + + if (Number.isNaN(parsedDate.getTime())) { + return NextResponse.json({ error: 'Data e horário inválidos' }, { status: 400 }); + } + + data.eventDate = parsedDate; + } + + if (typeof durationMinutes !== 'undefined') { + const parsedDuration = parseDurationMinutes(durationMinutes); + + if (parsedDuration === null) { + return NextResponse.json({ error: 'Duração inválida' }, { status: 400 }); + } + + data.durationMinutes = parsedDuration; + } + + if (typeof meetingUrl !== 'undefined') { + if (meetingUrl !== null && typeof meetingUrl !== 'string') { + return NextResponse.json({ error: 'Link de reunião inválido' }, { status: 400 }); + } + + if (typeof meetingUrl === 'string') { + const trimmedUrl = meetingUrl.trim(); + data.meetingUrl = trimmedUrl.length > 0 ? trimmedUrl : null; + } else { + data.meetingUrl = null; + } + } + + if (typeof isCancelled !== 'undefined') { + if (typeof isCancelled !== 'boolean') { + return NextResponse.json({ error: 'Status de cancelamento inválido' }, { status: 400 }); + } + + data.isCancelled = isCancelled; + } + + if (Object.keys(data).length === 0) { + return NextResponse.json({ error: 'Nenhum campo para atualizar' }, { status: 400 }); + } + + const updatedEvent = await prisma.cfRendaTurbinadaTurma3LiveEvent.update({ + where: { id: liveId }, + data + }); + + return NextResponse.json({ event: serializeLiveEvent(updatedEvent) }); + } catch (error) { + console.error('Erro ao atualizar live do CF Renda Turbinada Turma 3:', error); + return NextResponse.json({ error: 'Erro ao atualizar live' }, { status: 500 }); + } +} + +export async function DELETE( + _request: NextRequest, + { params }: { params: { id: string } } +) { + const liveId = Number(params.id); + + if (!params.id || Number.isNaN(liveId)) { + return NextResponse.json({ error: 'Live id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + await prisma.cfRendaTurbinadaTurma3LiveEvent.delete({ + where: { id: liveId } + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao excluir live do CF Renda Turbinada Turma 3:', error); + return NextResponse.json({ error: 'Erro ao excluir live' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-3/live-events/route.ts b/src/app/api/cf-renda-turbinada-turma-3/live-events/route.ts new file mode 100644 index 000000000..14a7ac3ec --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-3/live-events/route.ts @@ -0,0 +1,170 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeLiveEvent } from '../utils'; + +function parseDurationMinutes(value: unknown): number | null { + if (typeof value === 'number' && Number.isFinite(value)) { + const rounded = Math.round(value); + return rounded > 0 ? rounded : null; + } + + if (typeof value === 'string') { + const trimmed = value.trim(); + if (!trimmed) { + return null; + } + + const parsed = Number.parseInt(trimmed, 10); + if (!Number.isFinite(parsed) || Number.isNaN(parsed)) { + return null; + } + + return parsed > 0 ? parsed : null; + } + + return null; +} + +export async function GET() { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const events = await prisma.cfRendaTurbinadaTurma3LiveEvent.findMany({ + orderBy: [ + { eventDate: 'asc' }, + { createdAt: 'asc' } + ] + }); + + return NextResponse.json({ events: events.map(serializeLiveEvent) }); + } catch (error) { + console.error('Erro ao listar lives do CF Renda Turbinada Turma 3:', error); + return NextResponse.json({ error: 'Erro ao listar lives' }, { status: 500 }); + } +} + +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, description, eventDate, durationMinutes, meetingUrl } = body as { + title?: unknown; + description?: unknown; + eventDate?: unknown; + durationMinutes?: unknown; + meetingUrl?: unknown; + }; + + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título é obrigatório' }, { status: 400 }); + } + + if (typeof eventDate !== 'string' || !eventDate.trim()) { + return NextResponse.json({ error: 'Data e horário são obrigatórios' }, { status: 400 }); + } + + const parsedDate = new Date(eventDate); + + if (Number.isNaN(parsedDate.getTime())) { + return NextResponse.json({ error: 'Data e horário inválidos' }, { status: 400 }); + } + + let normalizedDuration = 60; + + if (typeof durationMinutes !== 'undefined') { + const parsedDuration = parseDurationMinutes(durationMinutes); + + if (parsedDuration === null) { + return NextResponse.json({ error: 'Duração inválida' }, { status: 400 }); + } + + normalizedDuration = parsedDuration; + } + + let normalizedDescription: string | null = null; + + if (typeof description !== 'undefined') { + if (description !== null && typeof description !== 'string') { + return NextResponse.json({ error: 'Descrição inválida' }, { status: 400 }); + } + + if (typeof description === 'string') { + const trimmedDescription = description.trim(); + normalizedDescription = trimmedDescription.length > 0 ? trimmedDescription : null; + } + } + + let normalizedMeetingUrl: string | null = null; + + if (typeof meetingUrl !== 'undefined') { + if (meetingUrl !== null && typeof meetingUrl !== 'string') { + return NextResponse.json({ error: 'Link de reunião inválido' }, { status: 400 }); + } + + if (typeof meetingUrl === 'string') { + const trimmedUrl = meetingUrl.trim(); + normalizedMeetingUrl = trimmedUrl.length > 0 ? trimmedUrl : null; + } + } + + const event = await prisma.cfRendaTurbinadaTurma3LiveEvent.create({ + data: { + title: title.trim(), + description: normalizedDescription, + eventDate: parsedDate, + durationMinutes: normalizedDuration, + meetingUrl: normalizedMeetingUrl + } + }); + + return NextResponse.json({ event: serializeLiveEvent(event) }, { status: 201 }); + } catch (error) { + console.error('Erro ao criar live do CF Renda Turbinada Turma 3:', error); + return NextResponse.json({ error: 'Erro ao criar live' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-3/mentorias/[mentoriaId]/materials/route.ts b/src/app/api/cf-renda-turbinada-turma-3/mentorias/[mentoriaId]/materials/route.ts new file mode 100644 index 000000000..3e1b73701 --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-3/mentorias/[mentoriaId]/materials/route.ts @@ -0,0 +1,95 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeMentoriaMaterial } from '../../../utils'; +import { + DEFAULT_ALLOWED_MIME_TYPES, + MAX_FILE_SIZE_BYTES, + isFileLike, + processUploadedFile, + validateFile +} from '../../../upload-utils'; + +export async function POST(request: NextRequest, { params }: { params: { mentoriaId: string } }) { + const mentoriaId = Number(params.mentoriaId); + + if (!params.mentoriaId || Number.isNaN(mentoriaId)) { + return NextResponse.json({ error: 'Mentoria id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const mentoria = await prisma.cfRendaTurbinadaTurma3Mentoria.findUnique({ + where: { id: mentoriaId } + }); + + if (!mentoria) { + return NextResponse.json({ error: 'Mentoria not found' }, { status: 404 }); + } + + const formData = await request.formData(); + const file = formData.get('file'); + const title = formData.get('title'); + + if (!isFileLike(file)) { + return NextResponse.json({ error: 'Arquivo é obrigatório' }, { status: 400 }); + } + + const validationError = validateFile(file, { + allowedMimeTypes: DEFAULT_ALLOWED_MIME_TYPES, + maxFileSizeBytes: MAX_FILE_SIZE_BYTES + }); + + if (validationError) { + return NextResponse.json({ error: validationError }, { status: 400 }); + } + + const processedFile = await processUploadedFile(file, { + uploadFolder: 'cf-renda-turbinada-turma-3/mentorias-materials' + }); + + const material = await prisma.cfRendaTurbinadaTurma3MentoriaMaterial.create({ + data: { + mentoriaId, + title: + typeof title === 'string' && title.trim().length > 0 + ? title.trim() + : processedFile.fileName, + fileUrl: processedFile.fileUrl + } + }); + + return NextResponse.json({ material: serializeMentoriaMaterial(material) }, { status: 201 }); + } catch (error) { + console.error('Erro ao anexar material na mentoria gravada do CF Renda Turbinada Turma 3:', error); + + if (error instanceof Error) { + return NextResponse.json({ error: error.message }, { status: 400 }); + } + + return NextResponse.json({ error: 'Erro ao anexar material' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-3/mentorias/[mentoriaId]/route.ts b/src/app/api/cf-renda-turbinada-turma-3/mentorias/[mentoriaId]/route.ts new file mode 100644 index 000000000..ffd394cfe --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-3/mentorias/[mentoriaId]/route.ts @@ -0,0 +1,240 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeMentoria } from '../../utils'; + +function parseDuration(value: unknown): number | null { + if (typeof value === 'number' && Number.isFinite(value)) { + const rounded = Math.round(value); + return rounded > 0 ? rounded : null; + } + + if (typeof value === 'string') { + const trimmed = value.trim(); + + if (!trimmed) { + return null; + } + + const parsed = Number.parseInt(trimmed, 10); + + if (!Number.isFinite(parsed) || Number.isNaN(parsed)) { + return null; + } + + return parsed > 0 ? parsed : null; + } + + return null; +} + +export async function PATCH(request: NextRequest, { params }: { params: { mentoriaId: string } }) { + const mentoriaId = Number(params.mentoriaId); + + if (!params.mentoriaId || Number.isNaN(mentoriaId)) { + return NextResponse.json({ error: 'Mentoria id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, description, content, recordedAt, releaseDate, videoUrl, durationMinutes } = body as { + title?: unknown; + description?: unknown; + content?: unknown; + recordedAt?: unknown; + releaseDate?: unknown; + videoUrl?: unknown; + durationMinutes?: unknown; + }; + + const updateData: Record = {}; + + if (typeof title !== 'undefined') { + if (title !== null && typeof title !== 'string') { + return NextResponse.json({ error: 'Título inválido' }, { status: 400 }); + } + + if (typeof title === 'string') { + if (!title.trim()) { + return NextResponse.json({ error: 'Título é obrigatório' }, { status: 400 }); + } + + updateData.title = title.trim(); + } + } + + if (typeof recordedAt !== 'undefined') { + if (recordedAt !== null && typeof recordedAt !== 'string') { + return NextResponse.json({ error: 'Data da mentoria inválida' }, { status: 400 }); + } + + if (typeof recordedAt === 'string') { + if (!recordedAt.trim()) { + return NextResponse.json({ error: 'Data da mentoria é obrigatória' }, { status: 400 }); + } + + const parsedRecordedAt = new Date(recordedAt); + + if (Number.isNaN(parsedRecordedAt.getTime())) { + return NextResponse.json({ error: 'Data da mentoria inválida' }, { status: 400 }); + } + + updateData.recordedAt = parsedRecordedAt; + } + } + + if (typeof releaseDate !== 'undefined') { + if (releaseDate !== null && typeof releaseDate !== 'string') { + return NextResponse.json({ error: 'Data de liberação inválida' }, { status: 400 }); + } + + if (typeof releaseDate === 'string') { + if (!releaseDate.trim()) { + return NextResponse.json({ error: 'Data de liberação é obrigatória' }, { status: 400 }); + } + + const parsedReleaseDate = new Date(releaseDate); + + if (Number.isNaN(parsedReleaseDate.getTime())) { + return NextResponse.json({ error: 'Data de liberação inválida' }, { status: 400 }); + } + + updateData.releaseDate = parsedReleaseDate; + } + } + + if (typeof description !== 'undefined') { + if (description !== null && typeof description !== 'string') { + return NextResponse.json({ error: 'Descrição inválida' }, { status: 400 }); + } + + if (typeof description === 'string') { + const trimmed = description.trim(); + updateData.description = trimmed.length > 0 ? trimmed : null; + } else { + updateData.description = null; + } + } + + if (typeof content !== 'undefined') { + if (content !== null && typeof content !== 'string') { + return NextResponse.json({ error: 'Conteúdo inválido' }, { status: 400 }); + } + + if (typeof content === 'string') { + const trimmed = content.trim(); + updateData.content = trimmed.length > 0 ? trimmed : null; + } else { + updateData.content = null; + } + } + + if (typeof videoUrl !== 'undefined') { + if (videoUrl !== null && typeof videoUrl !== 'string') { + return NextResponse.json({ error: 'Link do vídeo inválido' }, { status: 400 }); + } + + if (typeof videoUrl === 'string') { + const trimmed = videoUrl.trim(); + updateData.videoUrl = trimmed.length > 0 ? trimmed : null; + } else { + updateData.videoUrl = null; + } + } + + if (typeof durationMinutes !== 'undefined') { + if (durationMinutes === null) { + updateData.durationMinutes = null; + } else { + const parsedDuration = parseDuration(durationMinutes); + + if (parsedDuration === null) { + return NextResponse.json({ error: 'Duração inválida' }, { status: 400 }); + } + + updateData.durationMinutes = parsedDuration; + } + } + + const mentoria = await prisma.cfRendaTurbinadaTurma3Mentoria.update({ + where: { id: mentoriaId }, + data: updateData, + include: { materials: true } + }); + + return NextResponse.json({ mentoria: serializeMentoria(mentoria) }); + } catch (error) { + console.error('Erro ao atualizar mentoria gravada do CF Renda Turbinada Turma 3:', error); + return NextResponse.json({ error: 'Erro ao atualizar mentoria' }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest, { params }: { params: { mentoriaId: string } }) { + const mentoriaId = Number(params.mentoriaId); + + if (!params.mentoriaId || Number.isNaN(mentoriaId)) { + return NextResponse.json({ error: 'Mentoria id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + await prisma.cfRendaTurbinadaTurma3Mentoria.delete({ + where: { id: mentoriaId } + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao excluir mentoria gravada do CF Renda Turbinada Turma 3:', error); + return NextResponse.json({ error: 'Erro ao excluir mentoria' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-3/mentorias/materials/[materialId]/route.ts b/src/app/api/cf-renda-turbinada-turma-3/mentorias/materials/[materialId]/route.ts new file mode 100644 index 000000000..0f553bccf --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-3/mentorias/materials/[materialId]/route.ts @@ -0,0 +1,65 @@ +import { unlink } from 'fs/promises'; +import path from 'path'; +import { NextRequest, NextResponse } from 'next/server'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +export async function DELETE(request: NextRequest, { params }: { params: { materialId: string } }) { + const materialId = Number(params.materialId); + + if (!params.materialId || Number.isNaN(materialId)) { + return NextResponse.json({ error: 'Material id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const material = await prisma.cfRendaTurbinadaTurma3MentoriaMaterial.findUnique({ + where: { id: materialId } + }); + + if (!material) { + return NextResponse.json({ error: 'Material not found' }, { status: 404 }); + } + + await prisma.cfRendaTurbinadaTurma3MentoriaMaterial.delete({ + where: { id: materialId } + }); + + if (material.fileUrl.startsWith('/uploads/')) { + const filePath = path.join(process.cwd(), 'public', material.fileUrl.replace(/^\//, '')); + + try { + await unlink(filePath); + } catch (error) { + console.warn('Não foi possível remover arquivo de material de mentoria gravada (Turma 3):', error); + } + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao remover material de mentoria gravada do CF Renda Turbinada Turma 3:', error); + return NextResponse.json({ error: 'Erro ao remover material' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-3/mentorias/reorder/route.ts b/src/app/api/cf-renda-turbinada-turma-3/mentorias/reorder/route.ts new file mode 100644 index 000000000..01c97c776 --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-3/mentorias/reorder/route.ts @@ -0,0 +1,135 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeMentoria } from '../../utils'; + +function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); +} + +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { mentoriaId, targetPosition } = body as { mentoriaId?: unknown; targetPosition?: unknown }; + + const parsedMentoriaId = + typeof mentoriaId === 'number' + ? mentoriaId + : typeof mentoriaId === 'string' + ? Number.parseInt(mentoriaId, 10) + : Number.NaN; + + if (Number.isNaN(parsedMentoriaId) || parsedMentoriaId <= 0) { + return NextResponse.json({ error: 'ID da mentoria inválido' }, { status: 400 }); + } + + const parsedPosition = + typeof targetPosition === 'number' + ? targetPosition + : typeof targetPosition === 'string' + ? Number.parseInt(targetPosition, 10) + : Number.NaN; + + if (Number.isNaN(parsedPosition) || parsedPosition < 1) { + return NextResponse.json({ error: 'Posição de destino inválida' }, { status: 400 }); + } + + const mentorias = await prisma.cfRendaTurbinadaTurma3Mentoria.findMany({ + orderBy: [ + { sortOrder: 'asc' }, + { releaseDate: 'desc' }, + { recordedAt: 'desc' }, + { createdAt: 'desc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + }); + + if (mentorias.length === 0) { + return NextResponse.json({ error: 'Nenhuma mentoria encontrada' }, { status: 404 }); + } + + const orderedIds = mentorias.map(mentoria => mentoria.id); + + if (!orderedIds.includes(parsedMentoriaId)) { + return NextResponse.json({ error: 'Mentoria não encontrada' }, { status: 404 }); + } + + const otherIds = orderedIds.filter(id => id !== parsedMentoriaId); + const clampedIndex = clamp(parsedPosition - 1, 0, otherIds.length); + otherIds.splice(clampedIndex, 0, parsedMentoriaId); + + await prisma.$transaction(async tx => { + for (let index = 0; index < otherIds.length; index += 1) { + const id = otherIds[index]; + + await tx.cfRendaTurbinadaTurma3Mentoria.update({ + where: { id }, + data: { sortOrder: index + 1 } + }); + } + }); + + const updatedMentorias = await prisma.cfRendaTurbinadaTurma3Mentoria.findMany({ + orderBy: [ + { sortOrder: 'asc' }, + { releaseDate: 'desc' }, + { recordedAt: 'desc' }, + { createdAt: 'desc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + }); + + const serialized = updatedMentorias.map(mentoria => ({ ...serializeMentoria(mentoria), isReleased: true })); + + return NextResponse.json({ mentorias: serialized }); + } catch (error) { + console.error('Erro ao reordenar mentorias do CF Renda Turbinada Turma 3:', error); + return NextResponse.json({ error: 'Erro ao reordenar mentorias' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-3/mentorias/route.ts b/src/app/api/cf-renda-turbinada-turma-3/mentorias/route.ts new file mode 100644 index 000000000..15761ac30 --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-3/mentorias/route.ts @@ -0,0 +1,241 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeMentoria } from '../utils'; + +function parseDuration(value: unknown): number | null { + if (typeof value === 'number' && Number.isFinite(value)) { + const rounded = Math.round(value); + return rounded > 0 ? rounded : null; + } + + if (typeof value === 'string') { + const trimmed = value.trim(); + + if (!trimmed) { + return null; + } + + const parsed = Number.parseInt(trimmed, 10); + + if (!Number.isFinite(parsed) || Number.isNaN(parsed)) { + return null; + } + + return parsed > 0 ? parsed : null; + } + + return null; +} + +export async function GET() { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const isAdmin = user.plan === 'ADMIN'; + const now = new Date(); + + const mentorias = await prisma.cfRendaTurbinadaTurma3Mentoria.findMany({ + orderBy: [ + { sortOrder: 'asc' }, + { releaseDate: 'desc' }, + { recordedAt: 'desc' }, + { createdAt: 'desc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + }); + + const serializedMentorias = mentorias.map(mentoria => { + const base = serializeMentoria(mentoria); + const releaseTime = mentoria.releaseDate?.getTime(); + const isReleased = isAdmin || (typeof releaseTime === 'number' && releaseTime <= now.getTime()); + + if (isAdmin || isReleased) { + return { + ...base, + isReleased + }; + } + + return { + ...base, + isReleased, + content: undefined, + videoUrl: undefined, + materials: [] + }; + }); + + return NextResponse.json({ mentorias: serializedMentorias }); + } catch (error) { + console.error('Erro ao listar mentorias gravadas do CF Renda Turbinada Turma 3:', error); + return NextResponse.json({ error: 'Erro ao listar mentorias' }, { status: 500 }); + } +} + +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, description, content, recordedAt, releaseDate, videoUrl, durationMinutes } = body as { + title?: unknown; + description?: unknown; + content?: unknown; + recordedAt?: unknown; + releaseDate?: unknown; + videoUrl?: unknown; + durationMinutes?: unknown; + }; + + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título é obrigatório' }, { status: 400 }); + } + + if (typeof recordedAt !== 'string' || !recordedAt.trim()) { + return NextResponse.json({ error: 'Data da mentoria é obrigatória' }, { status: 400 }); + } + + const parsedRecordedAt = new Date(recordedAt); + + if (Number.isNaN(parsedRecordedAt.getTime())) { + return NextResponse.json({ error: 'Data da mentoria inválida' }, { status: 400 }); + } + + if (typeof releaseDate !== 'string' || !releaseDate.trim()) { + return NextResponse.json({ error: 'Data de liberação é obrigatória' }, { status: 400 }); + } + + const parsedReleaseDate = new Date(releaseDate); + + if (Number.isNaN(parsedReleaseDate.getTime())) { + return NextResponse.json({ error: 'Data de liberação inválida' }, { status: 400 }); + } + + let normalizedDescription: string | null = null; + + if (typeof description !== 'undefined') { + if (description !== null && typeof description !== 'string') { + return NextResponse.json({ error: 'Descrição inválida' }, { status: 400 }); + } + + if (typeof description === 'string') { + const trimmedDescription = description.trim(); + normalizedDescription = trimmedDescription.length > 0 ? trimmedDescription : null; + } + } + + let normalizedContent: string | null = null; + + if (typeof content !== 'undefined') { + if (content !== null && typeof content !== 'string') { + return NextResponse.json({ error: 'Conteúdo inválido' }, { status: 400 }); + } + + if (typeof content === 'string') { + const trimmedContent = content.trim(); + normalizedContent = trimmedContent.length > 0 ? trimmedContent : null; + } + } + + let normalizedVideoUrl: string | null = null; + + if (typeof videoUrl !== 'undefined') { + if (videoUrl !== null && typeof videoUrl !== 'string') { + return NextResponse.json({ error: 'Link do vídeo inválido' }, { status: 400 }); + } + + if (typeof videoUrl === 'string') { + const trimmedUrl = videoUrl.trim(); + normalizedVideoUrl = trimmedUrl.length > 0 ? trimmedUrl : null; + } + } + + let normalizedDuration: number | null = null; + + if (typeof durationMinutes !== 'undefined') { + const parsedDuration = parseDuration(durationMinutes); + + if (parsedDuration === null) { + return NextResponse.json({ error: 'Duração inválida' }, { status: 400 }); + } + + normalizedDuration = parsedDuration; + } + + const sortOrderAggregate = await prisma.cfRendaTurbinadaTurma3Mentoria.aggregate({ + _max: { sortOrder: true } + }); + + const nextSortOrder = (sortOrderAggregate._max.sortOrder ?? 0) + 1; + + const mentoria = await prisma.cfRendaTurbinadaTurma3Mentoria.create({ + data: { + title: title.trim(), + description: normalizedDescription, + content: normalizedContent, + videoUrl: normalizedVideoUrl, + recordedAt: parsedRecordedAt, + releaseDate: parsedReleaseDate, + durationMinutes: normalizedDuration ?? undefined, + sortOrder: nextSortOrder + }, + include: { materials: true } + }); + + return NextResponse.json({ mentoria: serializeMentoria(mentoria) }, { status: 201 }); + } catch (error) { + console.error('Erro ao criar mentoria gravada do CF Renda Turbinada Turma 3:', error); + return NextResponse.json({ error: 'Erro ao criar mentoria' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-3/modules/[id]/lessons/route.ts b/src/app/api/cf-renda-turbinada-turma-3/modules/[id]/lessons/route.ts new file mode 100644 index 000000000..1cb1c2983 --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-3/modules/[id]/lessons/route.ts @@ -0,0 +1,108 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +import { serializeLesson } from '../../../utils'; + +export async function POST(request: NextRequest, { params }: { params: { id: string } }) { + const { id: moduleId } = params; + + if (!moduleId || typeof moduleId !== 'string') { + return NextResponse.json({ error: 'Module id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const module = await prisma.cfRendaTurbinadaTurma3Module.findUnique({ + where: { id: moduleId } + }); + + if (!module) { + return NextResponse.json({ error: 'Module not found' }, { status: 404 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, duration, description, videoUrl, content } = body as { + title?: unknown; + duration?: unknown; + description?: unknown; + videoUrl?: unknown; + content?: unknown; + }; + + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título é obrigatório' }, { status: 400 }); + } + + if (typeof duration !== 'string' || !duration.trim()) { + return NextResponse.json({ error: 'Duração é obrigatória' }, { status: 400 }); + } + + const trimmedTitle = title.trim(); + const trimmedDuration = duration.trim(); + const trimmedDescription = typeof description === 'string' ? description.trim() : undefined; + const trimmedVideoUrl = typeof videoUrl === 'string' ? videoUrl.trim() : undefined; + const trimmedContent = typeof content === 'string' ? content.trim() : undefined; + + const aggregate = await prisma.cfRendaTurbinadaTurma3Lesson.aggregate({ + where: { moduleId }, + _max: { sortOrder: true } + }); + + const nextSortOrder = (aggregate._max.sortOrder ?? 0) + 1; + + const lesson = await prisma.cfRendaTurbinadaTurma3Lesson.create({ + data: { + moduleId, + title: trimmedTitle, + duration: trimmedDuration, + description: trimmedDescription && trimmedDescription.length > 0 ? trimmedDescription : null, + videoUrl: trimmedVideoUrl && trimmedVideoUrl.length > 0 ? trimmedVideoUrl : null, + content: trimmedContent && trimmedContent.length > 0 ? trimmedContent : null, + sortOrder: nextSortOrder + }, + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + }); + + return NextResponse.json({ lesson: serializeLesson(lesson) }, { status: 201 }); + } catch (error) { + console.error('Erro ao adicionar aula ao módulo do CF Renda Turbinada Turma 3:', error); + return NextResponse.json({ error: 'Erro ao adicionar aula ao módulo' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-3/modules/[id]/route.ts b/src/app/api/cf-renda-turbinada-turma-3/modules/[id]/route.ts new file mode 100644 index 000000000..bf0115bf7 --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-3/modules/[id]/route.ts @@ -0,0 +1,203 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +import { serializeModule } from '../../utils'; + +export async function PATCH(request: NextRequest, { params }: { params: { id: string } }) { + const { id } = params; + + if (!id || typeof id !== 'string') { + return NextResponse.json({ error: 'Module id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const module = await prisma.cfRendaTurbinadaTurma3Module.findUnique({ + where: { id }, + include: { + lessons: { + orderBy: [ + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + } + } + }); + + if (!module) { + return NextResponse.json({ error: 'Module not found' }, { status: 404 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, highlight, releaseDate, isHidden } = body as { + title?: unknown; + highlight?: unknown; + releaseDate?: unknown; + isHidden?: unknown; + }; + + const data: { + title?: string; + highlight?: string | null; + releaseDate?: Date; + isHidden?: boolean; + } = {}; + + if (typeof title !== 'undefined') { + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título inválido' }, { status: 400 }); + } + + data.title = title.trim(); + } + + if (typeof highlight !== 'undefined') { + if (highlight !== null && typeof highlight !== 'string') { + return NextResponse.json({ error: 'Destaque inválido' }, { status: 400 }); + } + + if (typeof highlight === 'string') { + const trimmedHighlight = highlight.trim(); + data.highlight = trimmedHighlight.length > 0 ? trimmedHighlight : null; + } else { + data.highlight = null; + } + } + + if (typeof releaseDate !== 'undefined') { + if (typeof releaseDate !== 'string' || !releaseDate.trim()) { + return NextResponse.json({ error: 'Data de liberação inválida' }, { status: 400 }); + } + + const parsedReleaseDate = new Date(releaseDate); + + if (Number.isNaN(parsedReleaseDate.getTime())) { + return NextResponse.json({ error: 'Data de liberação inválida' }, { status: 400 }); + } + + data.releaseDate = parsedReleaseDate; + } + + if (typeof isHidden !== 'undefined') { + if (typeof isHidden !== 'boolean') { + return NextResponse.json({ error: 'Valor inválido para visibilidade' }, { status: 400 }); + } + + data.isHidden = isHidden; + } + + if (Object.keys(data).length === 0) { + return NextResponse.json({ error: 'Nenhum campo para atualizar' }, { status: 400 }); + } + + const updated = await prisma.cfRendaTurbinadaTurma3Module.update({ + where: { id }, + data, + include: { + lessons: { + orderBy: [ + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + } + } + }); + + return NextResponse.json({ module: serializeModule(updated) }); + } catch (error) { + console.error('Erro ao atualizar módulo do CF Renda Turbinada Turma 3:', error); + return NextResponse.json({ error: 'Erro ao atualizar módulo' }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) { + const { id } = params; + + if (!id || typeof id !== 'string') { + return NextResponse.json({ error: 'Module id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const existingModule = await prisma.cfRendaTurbinadaTurma3Module.findUnique({ + where: { id } + }); + + if (!existingModule) { + return NextResponse.json({ error: 'Module not found' }, { status: 404 }); + } + + await prisma.cfRendaTurbinadaTurma3Module.delete({ + where: { id } + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao excluir módulo do CF Renda Turbinada Turma 3:', error); + return NextResponse.json({ error: 'Erro ao excluir módulo' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-3/modules/route.ts b/src/app/api/cf-renda-turbinada-turma-3/modules/route.ts new file mode 100644 index 000000000..aab63e60b --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-3/modules/route.ts @@ -0,0 +1,189 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +import { serializeModule } from '../utils'; + +export async function GET(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const isAdmin = user.plan === 'ADMIN'; + const now = new Date(); + + const modules = await prisma.cfRendaTurbinadaTurma3Module.findMany({ + where: isAdmin + ? undefined + : { + isHidden: false + }, + orderBy: [ + { releaseDate: 'asc' }, + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ], + include: { + lessons: { + orderBy: [ + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + } + } + }); + + const serializedModules = modules.map(module => { + const baseModule = serializeModule(module); + const releaseTimestamp = module.releaseDate instanceof Date + ? module.releaseDate.getTime() + : Number.NaN; + const isReleased = isAdmin || (!Number.isNaN(releaseTimestamp) && releaseTimestamp <= now.getTime()); + + if (isReleased) { + return { + ...baseModule, + isReleased + }; + } + + const safeLessons = module.lessons.map(lesson => ({ + id: lesson.id, + title: lesson.title, + duration: lesson.duration, + description: lesson.description ?? undefined, + videoUrl: undefined, + content: undefined, + sortOrder: lesson.sortOrder ?? undefined, + materials: [] + })); + + return { + ...baseModule, + isReleased, + lessons: safeLessons + }; + }); + + return NextResponse.json({ modules: serializedModules }); + } catch (error) { + console.error('Erro ao listar módulos do CF Renda Turbinada Turma 3:', error); + return NextResponse.json({ error: 'Erro ao listar módulos' }, { status: 500 }); + } +} + +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const isAdmin = user.plan === 'ADMIN'; + + if (!isAdmin) { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, highlight, releaseDate } = body as { + title?: unknown; + highlight?: unknown; + releaseDate?: unknown; + }; + + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título é obrigatório' }, { status: 400 }); + } + + if (typeof releaseDate !== 'string' || !releaseDate.trim()) { + return NextResponse.json({ error: 'Data de liberação é obrigatória' }, { status: 400 }); + } + + const parsedReleaseDate = new Date(releaseDate); + + if (Number.isNaN(parsedReleaseDate.getTime())) { + return NextResponse.json({ error: 'Data de liberação inválida' }, { status: 400 }); + } + + const trimmedTitle = title.trim(); + const trimmedHighlight = typeof highlight === 'string' ? highlight.trim() : undefined; + + const sortOrderAggregate = await prisma.cfRendaTurbinadaTurma3Module.aggregate({ + _max: { sortOrder: true } + }); + + const nextSortOrder = (sortOrderAggregate._max.sortOrder ?? 0) + 1; + + const module = await prisma.cfRendaTurbinadaTurma3Module.create({ + data: { + title: trimmedTitle, + highlight: trimmedHighlight && trimmedHighlight.length > 0 ? trimmedHighlight : null, + releaseDate: parsedReleaseDate, + sortOrder: nextSortOrder + }, + include: { + lessons: { + orderBy: [ + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + } + } + }); + + return NextResponse.json({ module: serializeModule(module) }, { status: 201 }); + } catch (error) { + console.error('Erro ao criar módulo do CF Renda Turbinada Turma 3:', error); + return NextResponse.json({ error: 'Erro ao criar módulo' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-3/progress/[lessonId]/route.ts b/src/app/api/cf-renda-turbinada-turma-3/progress/[lessonId]/route.ts new file mode 100644 index 000000000..63b058ac7 --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-3/progress/[lessonId]/route.ts @@ -0,0 +1,87 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +export async function POST( + request: NextRequest, + { params }: { params: { lessonId: string } } +) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Não autorizado' }, { status: 401 }); + } + + const lessonId = Number.parseInt(params.lessonId, 10); + + if (Number.isNaN(lessonId)) { + return NextResponse.json({ error: 'ID da aula inválido' }, { status: 400 }); + } + + const lesson = await prisma.cfRendaTurbinadaTurma3Lesson.findUnique({ + where: { id: lessonId } + }); + + if (!lesson) { + return NextResponse.json({ error: 'Aula não encontrada' }, { status: 404 }); + } + + await prisma.lessonProgressTurma3.upsert({ + where: { + userId_lessonId: { + userId: session.user.id, + lessonId + } + }, + create: { + userId: session.user.id, + lessonId + }, + update: { + completedAt: new Date() + } + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao marcar aula como concluída (turma 3):', error); + return NextResponse.json({ error: 'Erro ao marcar aula como concluída' }, { status: 500 }); + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: { lessonId: string } } +) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Não autorizado' }, { status: 401 }); + } + + const lessonId = Number.parseInt(params.lessonId, 10); + + if (Number.isNaN(lessonId)) { + return NextResponse.json({ error: 'ID da aula inválido' }, { status: 400 }); + } + + await prisma.lessonProgressTurma3 + .delete({ + where: { + userId_lessonId: { + userId: session.user.id, + lessonId + } + } + }) + .catch(() => {}); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao desmarcar aula (turma 3):', error); + return NextResponse.json({ error: 'Erro ao desmarcar aula' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-3/progress/route.ts b/src/app/api/cf-renda-turbinada-turma-3/progress/route.ts new file mode 100644 index 000000000..f2b482410 --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-3/progress/route.ts @@ -0,0 +1,31 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +export async function GET(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Não autorizado' }, { status: 401 }); + } + + const progress = await prisma.lessonProgressTurma3.findMany({ + where: { + userId: session.user.id + }, + select: { + lessonId: true, + completedAt: true + } + }); + + const completedLessonIds = progress.map(progressItem => progressItem.lessonId); + + return NextResponse.json({ completedLessons: completedLessonIds }); + } catch (error) { + console.error('Erro ao buscar progresso (turma 3):', error); + return NextResponse.json({ error: 'Erro ao buscar progresso' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada-turma-3/upload-utils.ts b/src/app/api/cf-renda-turbinada-turma-3/upload-utils.ts new file mode 100644 index 000000000..b77efb834 --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-3/upload-utils.ts @@ -0,0 +1 @@ +export * from '../cf-renda-turbinada/upload-utils'; diff --git a/src/app/api/cf-renda-turbinada-turma-3/utils.ts b/src/app/api/cf-renda-turbinada-turma-3/utils.ts new file mode 100644 index 000000000..3def5f8c1 --- /dev/null +++ b/src/app/api/cf-renda-turbinada-turma-3/utils.ts @@ -0,0 +1,94 @@ +import { + CfRendaTurbinadaTurma3Lesson, + CfRendaTurbinadaTurma3LessonMaterial, + CfRendaTurbinadaTurma3LiveEvent, + CfRendaTurbinadaTurma3Mentoria, + CfRendaTurbinadaTurma3MentoriaMaterial, + CfRendaTurbinadaTurma3Module +} from '@prisma/client'; + +type ModuleWithLessons = CfRendaTurbinadaTurma3Module & { + lessons: (CfRendaTurbinadaTurma3Lesson & { + materials: CfRendaTurbinadaTurma3LessonMaterial[]; + })[]; +}; + +type LessonWithMaterials = CfRendaTurbinadaTurma3Lesson & { + materials: CfRendaTurbinadaTurma3LessonMaterial[]; +}; + +type MentoriaWithMaterials = CfRendaTurbinadaTurma3Mentoria & { + materials: CfRendaTurbinadaTurma3MentoriaMaterial[]; +}; + +export function serializeLessonMaterial(material: CfRendaTurbinadaTurma3LessonMaterial) { + return { + id: material.id, + title: material.title, + fileUrl: material.fileUrl, + createdAt: material.createdAt.toISOString(), + updatedAt: material.updatedAt.toISOString() + }; +} + +export function serializeLesson(lesson: LessonWithMaterials) { + return { + id: lesson.id, + title: lesson.title, + duration: lesson.duration, + description: lesson.description ?? undefined, + videoUrl: lesson.videoUrl ?? undefined, + content: lesson.content ?? undefined, + sortOrder: lesson.sortOrder ?? undefined, + materials: lesson.materials.map(serializeLessonMaterial) + }; +} + +export function serializeModule(module: ModuleWithLessons) { + return { + id: module.id, + title: module.title, + highlight: module.highlight ?? '', + releaseDate: module.releaseDate.toISOString(), + isHidden: module.isHidden, + sortOrder: module.sortOrder ?? undefined, + lessons: module.lessons.map(serializeLesson) + }; +} + +export function serializeLiveEvent(event: CfRendaTurbinadaTurma3LiveEvent) { + return { + id: event.id, + title: event.title, + description: event.description ?? undefined, + eventDate: event.eventDate.toISOString(), + durationMinutes: event.durationMinutes, + meetingUrl: event.meetingUrl ?? undefined, + isCancelled: event.isCancelled + }; +} + +export function serializeMentoriaMaterial(material: CfRendaTurbinadaTurma3MentoriaMaterial) { + return { + id: material.id, + title: material.title, + fileUrl: material.fileUrl, + createdAt: material.createdAt.toISOString(), + updatedAt: material.updatedAt.toISOString() + }; +} + +export function serializeMentoria(mentoria: MentoriaWithMaterials) { + return { + id: mentoria.id, + title: mentoria.title, + description: mentoria.description ?? undefined, + content: mentoria.content ?? undefined, + videoUrl: mentoria.videoUrl ?? undefined, + recordedAt: mentoria.recordedAt.toISOString(), + releaseDate: mentoria.releaseDate?.toISOString(), + durationMinutes: mentoria.durationMinutes ?? undefined, + sortOrder: mentoria.sortOrder ?? undefined, + materials: mentoria.materials.map(serializeMentoriaMaterial) + }; +} diff --git a/src/app/api/cf-renda-turbinada/carteira/route.ts b/src/app/api/cf-renda-turbinada/carteira/route.ts new file mode 100644 index 000000000..7d0f29045 --- /dev/null +++ b/src/app/api/cf-renda-turbinada/carteira/route.ts @@ -0,0 +1,329 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +export const maxDuration = 5; + +type CarteiraPayload = { + id?: unknown; + ordem?: unknown; + ativoBase?: unknown; + codigoOpcao?: unknown; + strike?: unknown; + premio?: unknown; + premioCustom?: unknown; + rendaPercentual?: unknown; + garantia?: unknown; + precoReferencia?: unknown; + vencimento?: unknown; + tipoOperacao?: unknown; + status?: unknown; + observacao?: unknown; +}; + +const DEFAULT_OPERATIONS = { + puts: [ + { + id: 'put-1', + ordem: 1, + ativoBase: 'BBSE3', + codigoOpcao: 'BBSEX352', + strike: 33.26, + premio: null, + premioCustom: 'R$ 0,44 e R$ 0,47', + rendaPercentual: 1.31, + garantia: 3326.0, + precoReferencia: 33.26, + vencimento: '19/12/2025', + tipoOperacao: 'PUT', + status: 'Aberta' + }, + { + id: 'put-2', + ordem: 2, + ativoBase: 'B3SA3', + codigoOpcao: 'B3SAX132', + strike: 13.02, + premio: 0.28, + rendaPercentual: 2.08, + garantia: 1302.0, + precoReferencia: 13.02, + vencimento: '19/12/2025', + tipoOperacao: 'PUT', + status: 'Fechada' + }, + { + id: 'put-3', + ordem: 3, + ativoBase: 'PETR4', + codigoOpcao: 'PETRX326', + strike: 32.0, + premio: null, + premioCustom: 'R$ 0,62 e R$ 0,65', + rendaPercentual: 1.95, + garantia: 3200.0, + precoReferencia: 32.0, + vencimento: '19/12/2025', + tipoOperacao: 'PUT', + status: 'Aberta' + }, + { + id: 'put-4', + ordem: 4, + ativoBase: 'BBAS3', + codigoOpcao: 'BBASX228', + strike: 22.44, + premio: null, + premioCustom: 'R$ 0,59 e R$ 0,63', + rendaPercentual: 2.69, + garantia: 2244.0, + precoReferencia: 22.44, + vencimento: '19/12/2025', + tipoOperacao: 'PUT', + status: 'Aberta' + } + ], + calls: [ + { + id: 'call-1', + ordem: 1, + ativoBase: 'VALE3', + codigoOpcao: 'VALEL70', + strike: 66.29, + premio: 1.52, + rendaPercentual: 2.33, + garantia: null, + precoReferencia: 66.29, + vencimento: '19/12/2025', + tipoOperacao: 'CALL', + status: 'Encerrada' + }, + { + id: 'call-2', + ordem: 2, + ativoBase: 'PETR4', + codigoOpcao: 'PETRL41', + strike: 34.25, + premio: 0.7, + rendaPercentual: 2.1, + garantia: null, + precoReferencia: 34.25, + vencimento: '19/12/2025', + tipoOperacao: 'CALL', + status: 'Encerrada' + }, + { + id: 'call-3', + ordem: 3, + ativoBase: 'SANB11', + codigoOpcao: 'SANBL345', + strike: 34.1, + premio: 0.66, + rendaPercentual: 2.0, + garantia: null, + precoReferencia: 34.1, + vencimento: '19/12/2025', + tipoOperacao: 'CALL', + status: 'Encerrada' + }, + { + id: 'call-4', + ordem: 4, + ativoBase: 'VALE3', + codigoOpcao: 'VALEL69', + strike: 65.79, + premio: null, + premioCustom: 'R$ 1,35 a R$ 1,38', + rendaPercentual: 2.08, + garantia: null, + precoReferencia: 65.79, + vencimento: '19/12/2025', + tipoOperacao: 'CALL', + status: 'Aberta' + }, + { + id: 'call-5', + ordem: 5, + ativoBase: 'B3SA3', + codigoOpcao: 'B3SAL149', + strike: 14.77, + premio: null, + premioCustom: 'R$ 0,31 a R$ 0,34', + rendaPercentual: 2.26, + garantia: null, + precoReferencia: 14.77, + vencimento: '19/12/2025', + tipoOperacao: 'CALL', + status: 'Aberta' + } + ] +}; + +const serializeOperation = (operation: CarteiraPayload) => { + const { id, ordem, ativoBase, codigoOpcao, vencimento, tipoOperacao, status } = operation; + + if ( + typeof id !== 'string' || + typeof ordem !== 'number' || + typeof ativoBase !== 'string' || + typeof codigoOpcao !== 'string' || + typeof vencimento !== 'string' || + (tipoOperacao !== 'PUT' && tipoOperacao !== 'CALL') || + (status !== 'Aberta' && status !== 'Fechada' && status !== 'Encerrada') + ) { + return null; + } + + const parseNullableNumber = (value: unknown) => { + if (typeof value === 'number' && !Number.isNaN(value)) return value; + if (value === null || value === undefined) return null; + return null; + }; + + return { + id, + ordem, + ativoBase, + codigoOpcao, + vencimento, + tipoOperacao, + status, + strike: parseNullableNumber(operation.strike), + premio: parseNullableNumber(operation.premio), + premioCustom: typeof operation.premioCustom === 'string' ? operation.premioCustom : null, + rendaPercentual: parseNullableNumber(operation.rendaPercentual), + garantia: parseNullableNumber(operation.garantia), + precoReferencia: parseNullableNumber(operation.precoReferencia), + observacao: typeof operation.observacao === 'string' ? operation.observacao : null + }; +}; + +async function getUserPlan(userId: string) { + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { plan: true } + }); + + return user?.plan; +} + +export async function GET() { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + let operations = await prisma.cfRendaTurbinadaCarteiraOperacao.findMany({ + orderBy: [ + { tipoOperacao: 'asc' }, + { ordem: 'asc' }, + { createdAt: 'asc' } + ] + }); + + if (operations.length === 0) { + await prisma.cfRendaTurbinadaCarteiraOperacao.createMany({ + data: [...DEFAULT_OPERATIONS.puts, ...DEFAULT_OPERATIONS.calls].map(op => ({ + ...op + })) + }); + + operations = await prisma.cfRendaTurbinadaCarteiraOperacao.findMany({ + orderBy: [ + { tipoOperacao: 'asc' }, + { ordem: 'asc' }, + { createdAt: 'asc' } + ] + }); + } + + const puts = operations.filter(op => op.tipoOperacao === 'PUT'); + const calls = operations.filter(op => op.tipoOperacao === 'CALL'); + + return NextResponse.json({ puts, calls }); + } catch (error) { + console.error('Erro ao listar operações da carteira recomendada:', error); + return NextResponse.json({ error: 'Erro ao carregar operações' }, { status: 500 }); + } +} + +export async function PUT(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const plan = await getUserPlan(session.user.id); + + if (plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { puts, calls } = body as { puts?: CarteiraPayload[]; calls?: CarteiraPayload[] }; + + if (!Array.isArray(puts) || !Array.isArray(calls)) { + return NextResponse.json({ error: 'Listas de operações são obrigatórias' }, { status: 400 }); + } + + const parsedOperations = [...puts, ...calls] + .map(serializeOperation) + .filter(Boolean) as ReturnType[]; + + if (parsedOperations.length !== puts.length + calls.length) { + return NextResponse.json({ error: 'Dados de operação inválidos' }, { status: 400 }); + } + + const idsToKeep = parsedOperations.map(op => op.id); + + await prisma.$transaction(async tx => { + await tx.cfRendaTurbinadaCarteiraOperacao.deleteMany({ + where: { id: { notIn: idsToKeep } } + }); + + for (const operation of parsedOperations) { + await tx.cfRendaTurbinadaCarteiraOperacao.upsert({ + where: { id: operation.id }, + update: { + ...operation + }, + create: { + ...operation + } + }); + } + }); + + const updated = await prisma.cfRendaTurbinadaCarteiraOperacao.findMany({ + orderBy: [ + { tipoOperacao: 'asc' }, + { ordem: 'asc' }, + { createdAt: 'asc' } + ] + }); + + return NextResponse.json({ + puts: updated.filter(op => op.tipoOperacao === 'PUT'), + calls: updated.filter(op => op.tipoOperacao === 'CALL') + }); + } catch (error) { + console.error('Erro ao salvar operações da carteira recomendada:', error); + return NextResponse.json({ error: 'Erro ao salvar operações' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada/lessons/[lessonId]/materials/route.ts b/src/app/api/cf-renda-turbinada/lessons/[lessonId]/materials/route.ts new file mode 100644 index 000000000..c060e9227 --- /dev/null +++ b/src/app/api/cf-renda-turbinada/lessons/[lessonId]/materials/route.ts @@ -0,0 +1,95 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeLessonMaterial } from '../../../utils'; +import { + DEFAULT_ALLOWED_MIME_TYPES, + MAX_FILE_SIZE_BYTES, + isFileLike, + processUploadedFile, + validateFile +} from '../../../upload-utils'; + +export async function POST(request: NextRequest, { params }: { params: { lessonId: string } }) { + const lessonId = Number(params.lessonId); + + if (!params.lessonId || Number.isNaN(lessonId)) { + return NextResponse.json({ error: 'Lesson id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const lesson = await prisma.cfRendaTurbinadaLesson.findUnique({ + where: { id: lessonId } + }); + + if (!lesson) { + return NextResponse.json({ error: 'Lesson not found' }, { status: 404 }); + } + + const formData = await request.formData(); + const file = formData.get('file'); + const title = formData.get('title'); + + if (!isFileLike(file)) { + return NextResponse.json({ error: 'Arquivo é obrigatório' }, { status: 400 }); + } + + const validationError = validateFile(file, { + allowedMimeTypes: DEFAULT_ALLOWED_MIME_TYPES, + maxFileSizeBytes: MAX_FILE_SIZE_BYTES + }); + + if (validationError) { + return NextResponse.json({ error: validationError }, { status: 400 }); + } + + const processedFile = await processUploadedFile(file, { + uploadFolder: 'cf-renda-turbinada/lessons-materials' + }); + + const material = await prisma.cfRendaTurbinadaLessonMaterial.create({ + data: { + lessonId, + title: typeof title === 'string' && title.trim().length > 0 + ? title.trim() + : processedFile.fileName, + fileUrl: processedFile.fileUrl + } + }); + + return NextResponse.json({ material: serializeLessonMaterial(material) }, { status: 201 }); + + } catch (error) { + console.error('Erro ao anexar material no CF Renda Turbinada:', error); + + // ✅ RETORNAR MENSAGEM DE ERRO CLARA + if (error instanceof Error) { + return NextResponse.json({ error: error.message }, { status: 400 }); + } + + return NextResponse.json({ error: 'Erro ao anexar material' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada/lessons/[lessonId]/route.ts b/src/app/api/cf-renda-turbinada/lessons/[lessonId]/route.ts new file mode 100644 index 000000000..cb2c5d557 --- /dev/null +++ b/src/app/api/cf-renda-turbinada/lessons/[lessonId]/route.ts @@ -0,0 +1,193 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +import { serializeLesson } from '../../utils'; + +export async function PATCH(request: NextRequest, { params }: { params: { lessonId: string } }) { + const lessonId = Number(params.lessonId); + + if (!params.lessonId || Number.isNaN(lessonId)) { + return NextResponse.json({ error: 'Lesson id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const lesson = await prisma.cfRendaTurbinadaLesson.findUnique({ + where: { id: lessonId } + }); + + if (!lesson) { + return NextResponse.json({ error: 'Lesson not found' }, { status: 404 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, duration, description, videoUrl, content } = body as { + title?: unknown; + duration?: unknown; + description?: unknown; + videoUrl?: unknown; + content?: unknown; + }; + + const data: { + title?: string; + duration?: string; + description?: string | null; + videoUrl?: string | null; + content?: string | null; + } = {}; + + if (typeof title !== 'undefined') { + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título inválido' }, { status: 400 }); + } + + data.title = title.trim(); + } + + if (typeof duration !== 'undefined') { + if (typeof duration !== 'string' || !duration.trim()) { + return NextResponse.json({ error: 'Duração inválida' }, { status: 400 }); + } + + data.duration = duration.trim(); + } + + if (typeof description !== 'undefined') { + if (description !== null && typeof description !== 'string') { + return NextResponse.json({ error: 'Descrição inválida' }, { status: 400 }); + } + + if (typeof description === 'string') { + const trimmedDescription = description.trim(); + data.description = trimmedDescription.length > 0 ? trimmedDescription : null; + } else { + data.description = null; + } + } + + if (typeof videoUrl !== 'undefined') { + if (videoUrl !== null && typeof videoUrl !== 'string') { + return NextResponse.json({ error: 'Link de vídeo inválido' }, { status: 400 }); + } + + if (typeof videoUrl === 'string') { + const trimmedVideoUrl = videoUrl.trim(); + data.videoUrl = trimmedVideoUrl.length > 0 ? trimmedVideoUrl : null; + } else { + data.videoUrl = null; + } + } + + if (typeof content !== 'undefined') { + if (content !== null && typeof content !== 'string') { + return NextResponse.json({ error: 'Conteúdo inválido' }, { status: 400 }); + } + + if (typeof content === 'string') { + const trimmedContent = content.trim(); + data.content = trimmedContent.length > 0 ? trimmedContent : null; + } else { + data.content = null; + } + } + + if (Object.keys(data).length === 0) { + return NextResponse.json({ error: 'Nenhum campo para atualizar' }, { status: 400 }); + } + + const updated = await prisma.cfRendaTurbinadaLesson.update({ + where: { id: lessonId }, + data, + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + }); + + return NextResponse.json({ lesson: serializeLesson(updated) }); + } catch (error) { + console.error('Erro ao atualizar aula do CF Renda Turbinada:', error); + return NextResponse.json({ error: 'Erro ao atualizar aula' }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest, { params }: { params: { lessonId: string } }) { + const lessonId = Number(params.lessonId); + + if (!params.lessonId || Number.isNaN(lessonId)) { + return NextResponse.json({ error: 'Lesson id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const lesson = await prisma.cfRendaTurbinadaLesson.findUnique({ + where: { id: lessonId } + }); + + if (!lesson) { + return NextResponse.json({ error: 'Lesson not found' }, { status: 404 }); + } + + await prisma.cfRendaTurbinadaLesson.delete({ + where: { id: lessonId } + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao excluir aula do CF Renda Turbinada:', error); + return NextResponse.json({ error: 'Erro ao excluir aula' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada/lessons/reorder/route.ts b/src/app/api/cf-renda-turbinada/lessons/reorder/route.ts new file mode 100644 index 000000000..4986b3dc8 --- /dev/null +++ b/src/app/api/cf-renda-turbinada/lessons/reorder/route.ts @@ -0,0 +1,196 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +import { serializeModule } from '../../utils'; + +function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); +} + +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { lessonId, targetModuleId, targetPosition } = body as { + lessonId?: unknown; + targetModuleId?: unknown; + targetPosition?: unknown; + }; + + const parsedLessonId = + typeof lessonId === 'number' + ? lessonId + : typeof lessonId === 'string' + ? Number.parseInt(lessonId, 10) + : Number.NaN; + + if (Number.isNaN(parsedLessonId) || parsedLessonId <= 0) { + return NextResponse.json({ error: 'ID da aula inválido' }, { status: 400 }); + } + + const parsedTargetModuleId = typeof targetModuleId === 'string' ? targetModuleId.trim() : ''; + + if (!parsedTargetModuleId) { + return NextResponse.json({ error: 'Módulo de destino é obrigatório' }, { status: 400 }); + } + + const parsedTargetPosition = + typeof targetPosition === 'number' + ? targetPosition + : typeof targetPosition === 'string' + ? Number.parseInt(targetPosition, 10) + : Number.NaN; + + if (Number.isNaN(parsedTargetPosition) || parsedTargetPosition < 1) { + return NextResponse.json({ error: 'Posição de destino inválida' }, { status: 400 }); + } + + const lesson = await prisma.cfRendaTurbinadaLesson.findUnique({ + where: { id: parsedLessonId } + }); + + if (!lesson) { + return NextResponse.json({ error: 'Aula não encontrada' }, { status: 404 }); + } + + const targetModule = await prisma.cfRendaTurbinadaModule.findUnique({ + where: { id: parsedTargetModuleId }, + include: { lessons: true } + }); + + if (!targetModule) { + return NextResponse.json({ error: 'Módulo de destino não encontrado' }, { status: 404 }); + } + + const sourceModuleId = lesson.moduleId; + const isSameModule = sourceModuleId === parsedTargetModuleId; + + await prisma.$transaction(async tx => { + if (isSameModule) { + const lessonsInModule = await tx.cfRendaTurbinadaLesson.findMany({ + where: { moduleId: sourceModuleId }, + orderBy: [ + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ] + }); + + const otherLessons = lessonsInModule.filter(currentLesson => currentLesson.id !== parsedLessonId); + const clampedIndex = clamp(parsedTargetPosition - 1, 0, otherLessons.length); + const orderedIds = otherLessons.map(currentLesson => currentLesson.id); + orderedIds.splice(clampedIndex, 0, parsedLessonId); + + for (let index = 0; index < orderedIds.length; index += 1) { + const id = orderedIds[index]; + await tx.cfRendaTurbinadaLesson.update({ + where: { id }, + data: { sortOrder: index + 1 } + }); + } + + return; + } + + const sourceLessons = await tx.cfRendaTurbinadaLesson.findMany({ + where: { moduleId: sourceModuleId }, + orderBy: [ + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ] + }); + + const remainingIds = sourceLessons + .filter(currentLesson => currentLesson.id !== parsedLessonId) + .map(currentLesson => currentLesson.id); + + for (let index = 0; index < remainingIds.length; index += 1) { + const id = remainingIds[index]; + await tx.cfRendaTurbinadaLesson.update({ + where: { id }, + data: { sortOrder: index + 1 } + }); + } + + const targetLessons = await tx.cfRendaTurbinadaLesson.findMany({ + where: { moduleId: parsedTargetModuleId }, + orderBy: [ + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ] + }); + + const targetIds = targetLessons.map(currentLesson => currentLesson.id); + const clampedIndex = clamp(parsedTargetPosition - 1, 0, targetIds.length); + targetIds.splice(clampedIndex, 0, parsedLessonId); + + for (let index = 0; index < targetIds.length; index += 1) { + const id = targetIds[index]; + await tx.cfRendaTurbinadaLesson.update({ + where: { id }, + data: { + sortOrder: index + 1, + ...(id === parsedLessonId ? { moduleId: parsedTargetModuleId } : {}) + } + }); + } + }); + + const moduleIdsToFetch = isSameModule ? [sourceModuleId] : [sourceModuleId, parsedTargetModuleId]; + + const updatedModules = await prisma.cfRendaTurbinadaModule.findMany({ + where: { id: { in: moduleIdsToFetch } }, + include: { + lessons: { + orderBy: [ + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + } + } + }); + + return NextResponse.json({ modules: updatedModules.map(serializeModule) }); + } catch (error) { + console.error('Erro ao mover aula do CF Renda Turbinada:', error); + return NextResponse.json({ error: 'Erro ao mover aula' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada/live-events/[id]/route.ts b/src/app/api/cf-renda-turbinada/live-events/[id]/route.ts new file mode 100644 index 000000000..14856a0ba --- /dev/null +++ b/src/app/api/cf-renda-turbinada/live-events/[id]/route.ts @@ -0,0 +1,228 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeLiveEvent } from '../../utils'; + +function parseDurationMinutes(value: unknown): number | null { + if (typeof value === 'number' && Number.isFinite(value)) { + const rounded = Math.round(value); + return rounded > 0 ? rounded : null; + } + + if (typeof value === 'string') { + const trimmed = value.trim(); + if (!trimmed) { + return null; + } + + const parsed = Number.parseInt(trimmed, 10); + if (!Number.isFinite(parsed) || Number.isNaN(parsed)) { + return null; + } + + return parsed > 0 ? parsed : null; + } + + return null; +} + +export async function PATCH( + request: NextRequest, + { params }: { params: { id: string } } +) { + const liveId = Number(params.id); + + if (!params.id || Number.isNaN(liveId)) { + return NextResponse.json({ error: 'Live id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const existingEvent = await prisma.cfRendaTurbinadaLiveEvent.findUnique({ + where: { id: liveId } + }); + + if (!existingEvent) { + return NextResponse.json({ error: 'Live event not found' }, { status: 404 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, description, eventDate, durationMinutes, meetingUrl, isCancelled } = body as { + title?: unknown; + description?: unknown; + eventDate?: unknown; + durationMinutes?: unknown; + meetingUrl?: unknown; + isCancelled?: unknown; + }; + + const data: { + title?: string; + description?: string | null; + eventDate?: Date; + durationMinutes?: number; + meetingUrl?: string | null; + isCancelled?: boolean; + } = {}; + + if (typeof title !== 'undefined') { + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título inválido' }, { status: 400 }); + } + + data.title = title.trim(); + } + + if (typeof description !== 'undefined') { + if (description !== null && typeof description !== 'string') { + return NextResponse.json({ error: 'Descrição inválida' }, { status: 400 }); + } + + if (typeof description === 'string') { + const trimmedDescription = description.trim(); + data.description = trimmedDescription.length > 0 ? trimmedDescription : null; + } else { + data.description = null; + } + } + + if (typeof eventDate !== 'undefined') { + if (typeof eventDate !== 'string' || !eventDate.trim()) { + return NextResponse.json({ error: 'Data e horário inválidos' }, { status: 400 }); + } + + const parsedDate = new Date(eventDate); + + if (Number.isNaN(parsedDate.getTime())) { + return NextResponse.json({ error: 'Data e horário inválidos' }, { status: 400 }); + } + + data.eventDate = parsedDate; + } + + if (typeof durationMinutes !== 'undefined') { + const parsedDuration = parseDurationMinutes(durationMinutes); + + if (parsedDuration === null) { + return NextResponse.json({ error: 'Duração inválida' }, { status: 400 }); + } + + data.durationMinutes = parsedDuration; + } + + if (typeof meetingUrl !== 'undefined') { + if (meetingUrl !== null && typeof meetingUrl !== 'string') { + return NextResponse.json({ error: 'Link de reunião inválido' }, { status: 400 }); + } + + if (typeof meetingUrl === 'string') { + const trimmedUrl = meetingUrl.trim(); + data.meetingUrl = trimmedUrl.length > 0 ? trimmedUrl : null; + } else { + data.meetingUrl = null; + } + } + + if (typeof isCancelled !== 'undefined') { + if (typeof isCancelled !== 'boolean') { + return NextResponse.json({ error: 'Status de cancelamento inválido' }, { status: 400 }); + } + + data.isCancelled = isCancelled; + } + + if (Object.keys(data).length === 0) { + return NextResponse.json({ error: 'Nenhum campo para atualizar' }, { status: 400 }); + } + + const updatedEvent = await prisma.cfRendaTurbinadaLiveEvent.update({ + where: { id: liveId }, + data + }); + + return NextResponse.json({ event: serializeLiveEvent(updatedEvent) }); + } catch (error) { + console.error('Erro ao atualizar live do CF Renda Turbinada:', error); + return NextResponse.json({ error: 'Erro ao atualizar live' }, { status: 500 }); + } +} + +export async function DELETE( + _request: NextRequest, + { params }: { params: { id: string } } +) { + const liveId = Number(params.id); + + if (!params.id || Number.isNaN(liveId)) { + return NextResponse.json({ error: 'Live id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const existingEvent = await prisma.cfRendaTurbinadaLiveEvent.findUnique({ + where: { id: liveId } + }); + + if (!existingEvent) { + return NextResponse.json({ error: 'Live event not found' }, { status: 404 }); + } + + await prisma.cfRendaTurbinadaLiveEvent.delete({ + where: { id: liveId } + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao excluir live do CF Renda Turbinada:', error); + return NextResponse.json({ error: 'Erro ao excluir live' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada/live-events/route.ts b/src/app/api/cf-renda-turbinada/live-events/route.ts new file mode 100644 index 000000000..73011f9fd --- /dev/null +++ b/src/app/api/cf-renda-turbinada/live-events/route.ts @@ -0,0 +1,170 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeLiveEvent } from '../utils'; + +function parseDurationMinutes(value: unknown): number | null { + if (typeof value === 'number' && Number.isFinite(value)) { + const rounded = Math.round(value); + return rounded > 0 ? rounded : null; + } + + if (typeof value === 'string') { + const trimmed = value.trim(); + if (!trimmed) { + return null; + } + + const parsed = Number.parseInt(trimmed, 10); + if (!Number.isFinite(parsed) || Number.isNaN(parsed)) { + return null; + } + + return parsed > 0 ? parsed : null; + } + + return null; +} + +export async function GET() { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const events = await prisma.cfRendaTurbinadaLiveEvent.findMany({ + orderBy: [ + { eventDate: 'asc' }, + { createdAt: 'asc' } + ] + }); + + return NextResponse.json({ events: events.map(serializeLiveEvent) }); + } catch (error) { + console.error('Erro ao listar lives do CF Renda Turbinada:', error); + return NextResponse.json({ error: 'Erro ao listar lives' }, { status: 500 }); + } +} + +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, description, eventDate, durationMinutes, meetingUrl } = body as { + title?: unknown; + description?: unknown; + eventDate?: unknown; + durationMinutes?: unknown; + meetingUrl?: unknown; + }; + + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título é obrigatório' }, { status: 400 }); + } + + if (typeof eventDate !== 'string' || !eventDate.trim()) { + return NextResponse.json({ error: 'Data e horário são obrigatórios' }, { status: 400 }); + } + + const parsedDate = new Date(eventDate); + + if (Number.isNaN(parsedDate.getTime())) { + return NextResponse.json({ error: 'Data e horário inválidos' }, { status: 400 }); + } + + let normalizedDuration = 60; + + if (typeof durationMinutes !== 'undefined') { + const parsedDuration = parseDurationMinutes(durationMinutes); + + if (parsedDuration === null) { + return NextResponse.json({ error: 'Duração inválida' }, { status: 400 }); + } + + normalizedDuration = parsedDuration; + } + + let normalizedDescription: string | null = null; + + if (typeof description !== 'undefined') { + if (description !== null && typeof description !== 'string') { + return NextResponse.json({ error: 'Descrição inválida' }, { status: 400 }); + } + + if (typeof description === 'string') { + const trimmedDescription = description.trim(); + normalizedDescription = trimmedDescription.length > 0 ? trimmedDescription : null; + } + } + + let normalizedMeetingUrl: string | null = null; + + if (typeof meetingUrl !== 'undefined') { + if (meetingUrl !== null && typeof meetingUrl !== 'string') { + return NextResponse.json({ error: 'Link de reunião inválido' }, { status: 400 }); + } + + if (typeof meetingUrl === 'string') { + const trimmedUrl = meetingUrl.trim(); + normalizedMeetingUrl = trimmedUrl.length > 0 ? trimmedUrl : null; + } + } + + const event = await prisma.cfRendaTurbinadaLiveEvent.create({ + data: { + title: title.trim(), + description: normalizedDescription, + eventDate: parsedDate, + durationMinutes: normalizedDuration, + meetingUrl: normalizedMeetingUrl + } + }); + + return NextResponse.json({ event: serializeLiveEvent(event) }, { status: 201 }); + } catch (error) { + console.error('Erro ao criar live do CF Renda Turbinada:', error); + return NextResponse.json({ error: 'Erro ao criar live' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada/materials/[materialId]/route.ts b/src/app/api/cf-renda-turbinada/materials/[materialId]/route.ts new file mode 100644 index 000000000..706306545 --- /dev/null +++ b/src/app/api/cf-renda-turbinada/materials/[materialId]/route.ts @@ -0,0 +1,65 @@ +import { unlink } from 'fs/promises'; +import path from 'path'; +import { NextRequest, NextResponse } from 'next/server'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +export async function DELETE(request: NextRequest, { params }: { params: { materialId: string } }) { + const materialId = Number(params.materialId); + + if (!params.materialId || Number.isNaN(materialId)) { + return NextResponse.json({ error: 'Material id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const material = await prisma.cfRendaTurbinadaLessonMaterial.findUnique({ + where: { id: materialId } + }); + + if (!material) { + return NextResponse.json({ error: 'Material not found' }, { status: 404 }); + } + + await prisma.cfRendaTurbinadaLessonMaterial.delete({ + where: { id: materialId } + }); + + if (material.fileUrl.startsWith('/uploads/')) { + const filePath = path.join(process.cwd(), 'public', material.fileUrl.replace(/^\//, '')); + + try { + await unlink(filePath); + } catch (error) { + console.warn('Não foi possível remover arquivo de material do CF Renda Turbinada:', error); + } + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao remover material do CF Renda Turbinada:', error); + return NextResponse.json({ error: 'Erro ao remover material' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada/mentorias/[mentoriaId]/materials/route.ts b/src/app/api/cf-renda-turbinada/mentorias/[mentoriaId]/materials/route.ts new file mode 100644 index 000000000..5b6178841 --- /dev/null +++ b/src/app/api/cf-renda-turbinada/mentorias/[mentoriaId]/materials/route.ts @@ -0,0 +1,95 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeMentoriaMaterial } from '../../../utils'; +import { + DEFAULT_ALLOWED_MIME_TYPES, + MAX_FILE_SIZE_BYTES, + isFileLike, + processUploadedFile, + validateFile +} from '../../../upload-utils'; + +export async function POST(request: NextRequest, { params }: { params: { mentoriaId: string } }) { + const mentoriaId = Number(params.mentoriaId); + + if (!params.mentoriaId || Number.isNaN(mentoriaId)) { + return NextResponse.json({ error: 'Mentoria id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const mentoria = await prisma.cfRendaTurbinadaMentoria.findUnique({ + where: { id: mentoriaId } + }); + + if (!mentoria) { + return NextResponse.json({ error: 'Mentoria not found' }, { status: 404 }); + } + + const formData = await request.formData(); + const file = formData.get('file'); + const title = formData.get('title'); + + if (!isFileLike(file)) { + return NextResponse.json({ error: 'Arquivo é obrigatório' }, { status: 400 }); + } + + const validationError = validateFile(file, { + allowedMimeTypes: DEFAULT_ALLOWED_MIME_TYPES, + maxFileSizeBytes: MAX_FILE_SIZE_BYTES + }); + + if (validationError) { + return NextResponse.json({ error: validationError }, { status: 400 }); + } + + const processedFile = await processUploadedFile(file, { + uploadFolder: 'cf-renda-turbinada/mentorias-materials' + }); + + const material = await prisma.cfRendaTurbinadaMentoriaMaterial.create({ + data: { + mentoriaId, + title: + typeof title === 'string' && title.trim().length > 0 + ? title.trim() + : processedFile.fileName, + fileUrl: processedFile.fileUrl + } + }); + + return NextResponse.json({ material: serializeMentoriaMaterial(material) }, { status: 201 }); + } catch (error) { + console.error('Erro ao anexar material na mentoria gravada do CF Renda Turbinada:', error); + + if (error instanceof Error) { + return NextResponse.json({ error: error.message }, { status: 400 }); + } + + return NextResponse.json({ error: 'Erro ao anexar material' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada/mentorias/[mentoriaId]/route.ts b/src/app/api/cf-renda-turbinada/mentorias/[mentoriaId]/route.ts new file mode 100644 index 000000000..c7dece613 --- /dev/null +++ b/src/app/api/cf-renda-turbinada/mentorias/[mentoriaId]/route.ts @@ -0,0 +1,240 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeMentoria } from '../../utils'; + +function parseDuration(value: unknown): number | null { + if (typeof value === 'number' && Number.isFinite(value)) { + const rounded = Math.round(value); + return rounded > 0 ? rounded : null; + } + + if (typeof value === 'string') { + const trimmed = value.trim(); + + if (!trimmed) { + return null; + } + + const parsed = Number.parseInt(trimmed, 10); + + if (!Number.isFinite(parsed) || Number.isNaN(parsed)) { + return null; + } + + return parsed > 0 ? parsed : null; + } + + return null; +} + +export async function PATCH(request: NextRequest, { params }: { params: { mentoriaId: string } }) { + const mentoriaId = Number(params.mentoriaId); + + if (!params.mentoriaId || Number.isNaN(mentoriaId)) { + return NextResponse.json({ error: 'Mentoria id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, description, content, recordedAt, releaseDate, videoUrl, durationMinutes } = body as { + title?: unknown; + description?: unknown; + content?: unknown; + recordedAt?: unknown; + releaseDate?: unknown; + videoUrl?: unknown; + durationMinutes?: unknown; + }; + + const updateData: Record = {}; + + if (typeof title !== 'undefined') { + if (title !== null && typeof title !== 'string') { + return NextResponse.json({ error: 'Título inválido' }, { status: 400 }); + } + + if (typeof title === 'string') { + if (!title.trim()) { + return NextResponse.json({ error: 'Título é obrigatório' }, { status: 400 }); + } + + updateData.title = title.trim(); + } + } + + if (typeof recordedAt !== 'undefined') { + if (recordedAt !== null && typeof recordedAt !== 'string') { + return NextResponse.json({ error: 'Data da mentoria inválida' }, { status: 400 }); + } + + if (typeof recordedAt === 'string') { + if (!recordedAt.trim()) { + return NextResponse.json({ error: 'Data da mentoria é obrigatória' }, { status: 400 }); + } + + const parsedRecordedAt = new Date(recordedAt); + + if (Number.isNaN(parsedRecordedAt.getTime())) { + return NextResponse.json({ error: 'Data da mentoria inválida' }, { status: 400 }); + } + + updateData.recordedAt = parsedRecordedAt; + } + } + + if (typeof releaseDate !== 'undefined') { + if (releaseDate !== null && typeof releaseDate !== 'string') { + return NextResponse.json({ error: 'Data de liberação inválida' }, { status: 400 }); + } + + if (typeof releaseDate === 'string') { + if (!releaseDate.trim()) { + return NextResponse.json({ error: 'Data de liberação é obrigatória' }, { status: 400 }); + } + + const parsedReleaseDate = new Date(releaseDate); + + if (Number.isNaN(parsedReleaseDate.getTime())) { + return NextResponse.json({ error: 'Data de liberação inválida' }, { status: 400 }); + } + + updateData.releaseDate = parsedReleaseDate; + } + } + + if (typeof description !== 'undefined') { + if (description !== null && typeof description !== 'string') { + return NextResponse.json({ error: 'Descrição inválida' }, { status: 400 }); + } + + if (typeof description === 'string') { + const trimmed = description.trim(); + updateData.description = trimmed.length > 0 ? trimmed : null; + } else { + updateData.description = null; + } + } + + if (typeof content !== 'undefined') { + if (content !== null && typeof content !== 'string') { + return NextResponse.json({ error: 'Conteúdo inválido' }, { status: 400 }); + } + + if (typeof content === 'string') { + const trimmed = content.trim(); + updateData.content = trimmed.length > 0 ? trimmed : null; + } else { + updateData.content = null; + } + } + + if (typeof videoUrl !== 'undefined') { + if (videoUrl !== null && typeof videoUrl !== 'string') { + return NextResponse.json({ error: 'Link do vídeo inválido' }, { status: 400 }); + } + + if (typeof videoUrl === 'string') { + const trimmed = videoUrl.trim(); + updateData.videoUrl = trimmed.length > 0 ? trimmed : null; + } else { + updateData.videoUrl = null; + } + } + + if (typeof durationMinutes !== 'undefined') { + if (durationMinutes === null) { + updateData.durationMinutes = null; + } else { + const parsedDuration = parseDuration(durationMinutes); + + if (parsedDuration === null) { + return NextResponse.json({ error: 'Duração inválida' }, { status: 400 }); + } + + updateData.durationMinutes = parsedDuration; + } + } + + const mentoria = await prisma.cfRendaTurbinadaMentoria.update({ + where: { id: mentoriaId }, + data: updateData, + include: { materials: true } + }); + + return NextResponse.json({ mentoria: serializeMentoria(mentoria) }); + } catch (error) { + console.error('Erro ao atualizar mentoria gravada do CF Renda Turbinada:', error); + return NextResponse.json({ error: 'Erro ao atualizar mentoria' }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest, { params }: { params: { mentoriaId: string } }) { + const mentoriaId = Number(params.mentoriaId); + + if (!params.mentoriaId || Number.isNaN(mentoriaId)) { + return NextResponse.json({ error: 'Mentoria id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + await prisma.cfRendaTurbinadaMentoria.delete({ + where: { id: mentoriaId } + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao excluir mentoria gravada do CF Renda Turbinada:', error); + return NextResponse.json({ error: 'Erro ao excluir mentoria' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada/mentorias/materials/[materialId]/route.ts b/src/app/api/cf-renda-turbinada/mentorias/materials/[materialId]/route.ts new file mode 100644 index 000000000..2d44721b5 --- /dev/null +++ b/src/app/api/cf-renda-turbinada/mentorias/materials/[materialId]/route.ts @@ -0,0 +1,65 @@ +import { unlink } from 'fs/promises'; +import path from 'path'; +import { NextRequest, NextResponse } from 'next/server'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +export async function DELETE(request: NextRequest, { params }: { params: { materialId: string } }) { + const materialId = Number(params.materialId); + + if (!params.materialId || Number.isNaN(materialId)) { + return NextResponse.json({ error: 'Material id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const material = await prisma.cfRendaTurbinadaMentoriaMaterial.findUnique({ + where: { id: materialId } + }); + + if (!material) { + return NextResponse.json({ error: 'Material not found' }, { status: 404 }); + } + + await prisma.cfRendaTurbinadaMentoriaMaterial.delete({ + where: { id: materialId } + }); + + if (material.fileUrl.startsWith('/uploads/')) { + const filePath = path.join(process.cwd(), 'public', material.fileUrl.replace(/^\//, '')); + + try { + await unlink(filePath); + } catch (error) { + console.warn('Não foi possível remover arquivo de material de mentoria gravada:', error); + } + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao remover material de mentoria gravada do CF Renda Turbinada:', error); + return NextResponse.json({ error: 'Erro ao remover material' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada/mentorias/reorder/route.ts b/src/app/api/cf-renda-turbinada/mentorias/reorder/route.ts new file mode 100644 index 000000000..dc04934af --- /dev/null +++ b/src/app/api/cf-renda-turbinada/mentorias/reorder/route.ts @@ -0,0 +1,135 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeMentoria } from '../../utils'; + +function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); +} + +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { mentoriaId, targetPosition } = body as { mentoriaId?: unknown; targetPosition?: unknown }; + + const parsedMentoriaId = + typeof mentoriaId === 'number' + ? mentoriaId + : typeof mentoriaId === 'string' + ? Number.parseInt(mentoriaId, 10) + : Number.NaN; + + if (Number.isNaN(parsedMentoriaId) || parsedMentoriaId <= 0) { + return NextResponse.json({ error: 'ID da mentoria inválido' }, { status: 400 }); + } + + const parsedPosition = + typeof targetPosition === 'number' + ? targetPosition + : typeof targetPosition === 'string' + ? Number.parseInt(targetPosition, 10) + : Number.NaN; + + if (Number.isNaN(parsedPosition) || parsedPosition < 1) { + return NextResponse.json({ error: 'Posição de destino inválida' }, { status: 400 }); + } + + const mentorias = await prisma.cfRendaTurbinadaMentoria.findMany({ + orderBy: [ + { sortOrder: 'asc' }, + { releaseDate: 'desc' }, + { recordedAt: 'desc' }, + { createdAt: 'desc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + }); + + if (mentorias.length === 0) { + return NextResponse.json({ error: 'Nenhuma mentoria encontrada' }, { status: 404 }); + } + + const orderedIds = mentorias.map(mentoria => mentoria.id); + + if (!orderedIds.includes(parsedMentoriaId)) { + return NextResponse.json({ error: 'Mentoria não encontrada' }, { status: 404 }); + } + + const otherIds = orderedIds.filter(id => id !== parsedMentoriaId); + const clampedIndex = clamp(parsedPosition - 1, 0, otherIds.length); + otherIds.splice(clampedIndex, 0, parsedMentoriaId); + + await prisma.$transaction(async tx => { + for (let index = 0; index < otherIds.length; index += 1) { + const id = otherIds[index]; + + await tx.cfRendaTurbinadaMentoria.update({ + where: { id }, + data: { sortOrder: index + 1 } + }); + } + }); + + const updatedMentorias = await prisma.cfRendaTurbinadaMentoria.findMany({ + orderBy: [ + { sortOrder: 'asc' }, + { releaseDate: 'desc' }, + { recordedAt: 'desc' }, + { createdAt: 'desc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + }); + + const serialized = updatedMentorias.map(mentoria => ({ ...serializeMentoria(mentoria), isReleased: true })); + + return NextResponse.json({ mentorias: serialized }); + } catch (error) { + console.error('Erro ao reordenar mentorias do CF Renda Turbinada:', error); + return NextResponse.json({ error: 'Erro ao reordenar mentorias' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada/mentorias/route.ts b/src/app/api/cf-renda-turbinada/mentorias/route.ts new file mode 100644 index 000000000..c3c8e0c80 --- /dev/null +++ b/src/app/api/cf-renda-turbinada/mentorias/route.ts @@ -0,0 +1,241 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +import { serializeMentoria } from '../utils'; + +function parseDuration(value: unknown): number | null { + if (typeof value === 'number' && Number.isFinite(value)) { + const rounded = Math.round(value); + return rounded > 0 ? rounded : null; + } + + if (typeof value === 'string') { + const trimmed = value.trim(); + + if (!trimmed) { + return null; + } + + const parsed = Number.parseInt(trimmed, 10); + + if (!Number.isFinite(parsed) || Number.isNaN(parsed)) { + return null; + } + + return parsed > 0 ? parsed : null; + } + + return null; +} + +export async function GET() { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const isAdmin = user.plan === 'ADMIN'; + const now = new Date(); + + const mentorias = await prisma.cfRendaTurbinadaMentoria.findMany({ + orderBy: [ + { sortOrder: 'asc' }, + { releaseDate: 'desc' }, + { recordedAt: 'desc' }, + { createdAt: 'desc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + }); + + const serializedMentorias = mentorias.map(mentoria => { + const base = serializeMentoria(mentoria); + const releaseTime = mentoria.releaseDate?.getTime(); + const isReleased = isAdmin || (typeof releaseTime === 'number' && releaseTime <= now.getTime()); + + if (isAdmin || isReleased) { + return { + ...base, + isReleased + }; + } + + return { + ...base, + isReleased, + content: undefined, + videoUrl: undefined, + materials: [] + }; + }); + + return NextResponse.json({ mentorias: serializedMentorias }); + } catch (error) { + console.error('Erro ao listar mentorias gravadas do CF Renda Turbinada:', error); + return NextResponse.json({ error: 'Erro ao listar mentorias' }, { status: 500 }); + } +} + +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, description, content, recordedAt, releaseDate, videoUrl, durationMinutes } = body as { + title?: unknown; + description?: unknown; + content?: unknown; + recordedAt?: unknown; + releaseDate?: unknown; + videoUrl?: unknown; + durationMinutes?: unknown; + }; + + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título é obrigatório' }, { status: 400 }); + } + + if (typeof recordedAt !== 'string' || !recordedAt.trim()) { + return NextResponse.json({ error: 'Data da mentoria é obrigatória' }, { status: 400 }); + } + + const parsedRecordedAt = new Date(recordedAt); + + if (Number.isNaN(parsedRecordedAt.getTime())) { + return NextResponse.json({ error: 'Data da mentoria inválida' }, { status: 400 }); + } + + if (typeof releaseDate !== 'string' || !releaseDate.trim()) { + return NextResponse.json({ error: 'Data de liberação é obrigatória' }, { status: 400 }); + } + + const parsedReleaseDate = new Date(releaseDate); + + if (Number.isNaN(parsedReleaseDate.getTime())) { + return NextResponse.json({ error: 'Data de liberação inválida' }, { status: 400 }); + } + + let normalizedDescription: string | null = null; + + if (typeof description !== 'undefined') { + if (description !== null && typeof description !== 'string') { + return NextResponse.json({ error: 'Descrição inválida' }, { status: 400 }); + } + + if (typeof description === 'string') { + const trimmedDescription = description.trim(); + normalizedDescription = trimmedDescription.length > 0 ? trimmedDescription : null; + } + } + + let normalizedContent: string | null = null; + + if (typeof content !== 'undefined') { + if (content !== null && typeof content !== 'string') { + return NextResponse.json({ error: 'Conteúdo inválido' }, { status: 400 }); + } + + if (typeof content === 'string') { + const trimmedContent = content.trim(); + normalizedContent = trimmedContent.length > 0 ? trimmedContent : null; + } + } + + let normalizedVideoUrl: string | null = null; + + if (typeof videoUrl !== 'undefined') { + if (videoUrl !== null && typeof videoUrl !== 'string') { + return NextResponse.json({ error: 'Link do vídeo inválido' }, { status: 400 }); + } + + if (typeof videoUrl === 'string') { + const trimmedUrl = videoUrl.trim(); + normalizedVideoUrl = trimmedUrl.length > 0 ? trimmedUrl : null; + } + } + + let normalizedDuration: number | null = null; + + if (typeof durationMinutes !== 'undefined') { + const parsedDuration = parseDuration(durationMinutes); + + if (parsedDuration === null) { + return NextResponse.json({ error: 'Duração inválida' }, { status: 400 }); + } + + normalizedDuration = parsedDuration; + } + + const sortOrderAggregate = await prisma.cfRendaTurbinadaMentoria.aggregate({ + _max: { sortOrder: true } + }); + + const nextSortOrder = (sortOrderAggregate._max.sortOrder ?? 0) + 1; + + const mentoria = await prisma.cfRendaTurbinadaMentoria.create({ + data: { + title: title.trim(), + description: normalizedDescription, + content: normalizedContent, + videoUrl: normalizedVideoUrl, + recordedAt: parsedRecordedAt, + releaseDate: parsedReleaseDate, + durationMinutes: normalizedDuration ?? undefined, + sortOrder: nextSortOrder + }, + include: { materials: true } + }); + + return NextResponse.json({ mentoria: serializeMentoria(mentoria) }, { status: 201 }); + } catch (error) { + console.error('Erro ao criar mentoria gravada do CF Renda Turbinada:', error); + return NextResponse.json({ error: 'Erro ao criar mentoria' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada/modules/[id]/lessons/route.ts b/src/app/api/cf-renda-turbinada/modules/[id]/lessons/route.ts new file mode 100644 index 000000000..de4084c71 --- /dev/null +++ b/src/app/api/cf-renda-turbinada/modules/[id]/lessons/route.ts @@ -0,0 +1,108 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +import { serializeLesson } from '../../../utils'; + +export async function POST(request: NextRequest, { params }: { params: { id: string } }) { + const { id: moduleId } = params; + + if (!moduleId || typeof moduleId !== 'string') { + return NextResponse.json({ error: 'Module id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const module = await prisma.cfRendaTurbinadaModule.findUnique({ + where: { id: moduleId } + }); + + if (!module) { + return NextResponse.json({ error: 'Module not found' }, { status: 404 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, duration, description, videoUrl, content } = body as { + title?: unknown; + duration?: unknown; + description?: unknown; + videoUrl?: unknown; + content?: unknown; + }; + + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título é obrigatório' }, { status: 400 }); + } + + if (typeof duration !== 'string' || !duration.trim()) { + return NextResponse.json({ error: 'Duração é obrigatória' }, { status: 400 }); + } + + const trimmedTitle = title.trim(); + const trimmedDuration = duration.trim(); + const trimmedDescription = typeof description === 'string' ? description.trim() : undefined; + const trimmedVideoUrl = typeof videoUrl === 'string' ? videoUrl.trim() : undefined; + const trimmedContent = typeof content === 'string' ? content.trim() : undefined; + + const aggregate = await prisma.cfRendaTurbinadaLesson.aggregate({ + where: { moduleId }, + _max: { sortOrder: true } + }); + + const nextSortOrder = (aggregate._max.sortOrder ?? 0) + 1; + + const lesson = await prisma.cfRendaTurbinadaLesson.create({ + data: { + moduleId, + title: trimmedTitle, + duration: trimmedDuration, + description: trimmedDescription && trimmedDescription.length > 0 ? trimmedDescription : null, + videoUrl: trimmedVideoUrl && trimmedVideoUrl.length > 0 ? trimmedVideoUrl : null, + content: trimmedContent && trimmedContent.length > 0 ? trimmedContent : null, + sortOrder: nextSortOrder + }, + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + }); + + return NextResponse.json({ lesson: serializeLesson(lesson) }, { status: 201 }); + } catch (error) { + console.error('Erro ao criar aula do CF Renda Turbinada:', error); + return NextResponse.json({ error: 'Erro ao criar aula' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada/modules/[id]/route.ts b/src/app/api/cf-renda-turbinada/modules/[id]/route.ts new file mode 100644 index 000000000..d2de9b39f --- /dev/null +++ b/src/app/api/cf-renda-turbinada/modules/[id]/route.ts @@ -0,0 +1,203 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +import { serializeModule } from '../../utils'; + +export async function PATCH(request: NextRequest, { params }: { params: { id: string } }) { + const { id } = params; + + if (!id || typeof id !== 'string') { + return NextResponse.json({ error: 'Module id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const module = await prisma.cfRendaTurbinadaModule.findUnique({ + where: { id }, + include: { + lessons: { + orderBy: [ + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + } + } + }); + + if (!module) { + return NextResponse.json({ error: 'Module not found' }, { status: 404 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, highlight, releaseDate, isHidden } = body as { + title?: unknown; + highlight?: unknown; + releaseDate?: unknown; + isHidden?: unknown; + }; + + const data: { + title?: string; + highlight?: string | null; + releaseDate?: Date; + isHidden?: boolean; + } = {}; + + if (typeof title !== 'undefined') { + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título inválido' }, { status: 400 }); + } + + data.title = title.trim(); + } + + if (typeof highlight !== 'undefined') { + if (highlight !== null && typeof highlight !== 'string') { + return NextResponse.json({ error: 'Destaque inválido' }, { status: 400 }); + } + + if (typeof highlight === 'string') { + const trimmedHighlight = highlight.trim(); + data.highlight = trimmedHighlight.length > 0 ? trimmedHighlight : null; + } else { + data.highlight = null; + } + } + + if (typeof releaseDate !== 'undefined') { + if (typeof releaseDate !== 'string' || !releaseDate.trim()) { + return NextResponse.json({ error: 'Data de liberação inválida' }, { status: 400 }); + } + + const parsedReleaseDate = new Date(releaseDate); + + if (Number.isNaN(parsedReleaseDate.getTime())) { + return NextResponse.json({ error: 'Data de liberação inválida' }, { status: 400 }); + } + + data.releaseDate = parsedReleaseDate; + } + + if (typeof isHidden !== 'undefined') { + if (typeof isHidden !== 'boolean') { + return NextResponse.json({ error: 'Valor inválido para visibilidade' }, { status: 400 }); + } + + data.isHidden = isHidden; + } + + if (Object.keys(data).length === 0) { + return NextResponse.json({ error: 'Nenhum campo para atualizar' }, { status: 400 }); + } + + const updated = await prisma.cfRendaTurbinadaModule.update({ + where: { id }, + data, + include: { + lessons: { + orderBy: [ + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + } + } + }); + + return NextResponse.json({ module: serializeModule(updated) }); + } catch (error) { + console.error('Erro ao atualizar módulo do CF Renda Turbinada:', error); + return NextResponse.json({ error: 'Erro ao atualizar módulo' }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) { + const { id } = params; + + if (!id || typeof id !== 'string') { + return NextResponse.json({ error: 'Module id is required' }, { status: 400 }); + } + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const existingModule = await prisma.cfRendaTurbinadaModule.findUnique({ + where: { id } + }); + + if (!existingModule) { + return NextResponse.json({ error: 'Module not found' }, { status: 404 }); + } + + await prisma.cfRendaTurbinadaModule.delete({ + where: { id } + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao excluir módulo do CF Renda Turbinada:', error); + return NextResponse.json({ error: 'Erro ao excluir módulo' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada/modules/route.ts b/src/app/api/cf-renda-turbinada/modules/route.ts new file mode 100644 index 000000000..9ad662041 --- /dev/null +++ b/src/app/api/cf-renda-turbinada/modules/route.ts @@ -0,0 +1,189 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +import { serializeModule } from '../utils'; + +export async function GET(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const isAdmin = user.plan === 'ADMIN'; + const now = new Date(); + + const modules = await prisma.cfRendaTurbinadaModule.findMany({ + where: isAdmin + ? undefined + : { + isHidden: false + }, + orderBy: [ + { releaseDate: 'asc' }, + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ], + include: { + lessons: { + orderBy: [ + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + } + } + }); + + const serializedModules = modules.map(module => { + const baseModule = serializeModule(module); + const releaseTimestamp = module.releaseDate instanceof Date + ? module.releaseDate.getTime() + : Number.NaN; + const isReleased = isAdmin || (!Number.isNaN(releaseTimestamp) && releaseTimestamp <= now.getTime()); + + if (isReleased) { + return { + ...baseModule, + isReleased + }; + } + + const safeLessons = module.lessons.map(lesson => ({ + id: lesson.id, + title: lesson.title, + duration: lesson.duration, + description: lesson.description ?? undefined, + videoUrl: undefined, + content: undefined, + sortOrder: lesson.sortOrder ?? undefined, + materials: [] + })); + + return { + ...baseModule, + isReleased, + lessons: safeLessons + }; + }); + + return NextResponse.json({ modules: serializedModules }); + } catch (error) { + console.error('Erro ao listar módulos do CF Renda Turbinada:', error); + return NextResponse.json({ error: 'Erro ao listar módulos' }, { status: 500 }); + } +} + +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { plan: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const isAdmin = user.plan === 'ADMIN'; + + if (!isAdmin) { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch (parseError) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body || typeof body !== 'object') { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { title, highlight, releaseDate } = body as { + title?: unknown; + highlight?: unknown; + releaseDate?: unknown; + }; + + if (typeof title !== 'string' || !title.trim()) { + return NextResponse.json({ error: 'Título é obrigatório' }, { status: 400 }); + } + + if (typeof releaseDate !== 'string' || !releaseDate.trim()) { + return NextResponse.json({ error: 'Data de liberação é obrigatória' }, { status: 400 }); + } + + const parsedReleaseDate = new Date(releaseDate); + + if (Number.isNaN(parsedReleaseDate.getTime())) { + return NextResponse.json({ error: 'Data de liberação inválida' }, { status: 400 }); + } + + const trimmedTitle = title.trim(); + const trimmedHighlight = typeof highlight === 'string' ? highlight.trim() : undefined; + + const sortOrderAggregate = await prisma.cfRendaTurbinadaModule.aggregate({ + _max: { sortOrder: true } + }); + + const nextSortOrder = (sortOrderAggregate._max.sortOrder ?? 0) + 1; + + const module = await prisma.cfRendaTurbinadaModule.create({ + data: { + title: trimmedTitle, + highlight: trimmedHighlight && trimmedHighlight.length > 0 ? trimmedHighlight : null, + releaseDate: parsedReleaseDate, + sortOrder: nextSortOrder + }, + include: { + lessons: { + orderBy: [ + { sortOrder: 'asc' }, + { createdAt: 'asc' } + ], + include: { + materials: { + orderBy: [ + { createdAt: 'asc' }, + { id: 'asc' } + ] + } + } + } + } + }); + + return NextResponse.json({ module: serializeModule(module) }, { status: 201 }); + } catch (error) { + console.error('Erro ao criar módulo do CF Renda Turbinada:', error); + return NextResponse.json({ error: 'Erro ao criar módulo' }, { status: 500 }); + } +} diff --git a/src/app/api/cf-renda-turbinada/progress/[lessonId]/route.ts b/src/app/api/cf-renda-turbinada/progress/[lessonId]/route.ts new file mode 100644 index 000000000..f986ffc45 --- /dev/null +++ b/src/app/api/cf-renda-turbinada/progress/[lessonId]/route.ts @@ -0,0 +1,108 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +export async function POST( + request: NextRequest, + { params }: { params: { lessonId: string } } +) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json( + { error: 'Não autorizado' }, + { status: 401 } + ); + } + + const lessonId = Number.parseInt(params.lessonId, 10); + + if (Number.isNaN(lessonId)) { + return NextResponse.json( + { error: 'ID da aula inválido' }, + { status: 400 } + ); + } + + const lesson = await prisma.cfRendaTurbinadaLesson.findUnique({ + where: { id: lessonId } + }); + + if (!lesson) { + return NextResponse.json( + { error: 'Aula não encontrada' }, + { status: 404 } + ); + } + + await prisma.lessonProgress.upsert({ + where: { + userId_lessonId: { + userId: session.user.id, + lessonId + } + }, + create: { + userId: session.user.id, + lessonId + }, + update: { + completedAt: new Date() + } + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao marcar aula como concluída:', error); + return NextResponse.json( + { error: 'Erro ao marcar aula como concluída' }, + { status: 500 } + ); + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: { lessonId: string } } +) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json( + { error: 'Não autorizado' }, + { status: 401 } + ); + } + + const lessonId = Number.parseInt(params.lessonId, 10); + + if (Number.isNaN(lessonId)) { + return NextResponse.json( + { error: 'ID da aula inválido' }, + { status: 400 } + ); + } + + await prisma.lessonProgress + .delete({ + where: { + userId_lessonId: { + userId: session.user.id, + lessonId + } + } + }) + .catch(() => {}); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erro ao desmarcar aula:', error); + return NextResponse.json( + { error: 'Erro ao desmarcar aula' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/cf-renda-turbinada/progress/route.ts b/src/app/api/cf-renda-turbinada/progress/route.ts new file mode 100644 index 000000000..4bbd342ac --- /dev/null +++ b/src/app/api/cf-renda-turbinada/progress/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +export async function GET(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json( + { error: 'Não autorizado' }, + { status: 401 } + ); + } + + const progress = await prisma.lessonProgress.findMany({ + where: { + userId: session.user.id + }, + select: { + lessonId: true, + completedAt: true + } + }); + + const completedLessonIds = progress.map(progressItem => progressItem.lessonId); + + return NextResponse.json({ + completedLessons: completedLessonIds + }); + } catch (error) { + console.error('Erro ao buscar progresso:', error); + return NextResponse.json( + { error: 'Erro ao buscar progresso' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/cf-renda-turbinada/upload-utils.ts b/src/app/api/cf-renda-turbinada/upload-utils.ts new file mode 100644 index 000000000..567e76400 --- /dev/null +++ b/src/app/api/cf-renda-turbinada/upload-utils.ts @@ -0,0 +1,151 @@ +import { mkdir, writeFile } from 'fs/promises'; +import path from 'path'; +import { randomUUID } from 'crypto'; + +export type FileLike = { + arrayBuffer: () => Promise; + size: number; + type?: string; + name?: string; +}; + +export type ProcessedFile = { + fileUrl: string; + fileName: string; + storage: 'base64' | 'filesystem'; +}; + +export const DEFAULT_ALLOWED_MIME_TYPES = new Set([ + 'application/pdf', + 'application/vnd.ms-powerpoint', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'image/jpeg', + 'image/png', + 'image/webp', + 'image/gif' +]); + +export const MAX_FILE_SIZE_BYTES = 25 * 1024 * 1024; // 25MB +const DEFAULT_BASE64_LIMIT_BYTES = 5 * 1024 * 1024; // 5MB + +const MIME_EXTENSION_MAP: Record = { + 'application/pdf': '.pdf', + 'application/vnd.ms-powerpoint': '.ppt', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx', + 'image/jpeg': '.jpg', + 'image/png': '.png', + 'image/webp': '.webp', + 'image/gif': '.gif' +}; + +export function isFileLike(value: unknown): value is FileLike { + return ( + typeof value === 'object' && + value !== null && + typeof (value as FileLike).arrayBuffer === 'function' && + typeof (value as FileLike).size === 'number' + ); +} + +function sanitizeDisplayName(originalName: unknown, fallback: string): string { + if (typeof originalName !== 'string') { + return fallback; + } + + const trimmed = originalName.trim(); + + if (!trimmed) { + return fallback; + } + + // Remove characters that could break file systems or URLs while keeping a readable name + const cleaned = trimmed.replace(/[^\p{L}\p{N}\s._-]/gu, '').replace(/\s+/g, ' ').trim(); + + return cleaned.length > 0 ? cleaned : fallback; +} + +function resolveExtension(file: FileLike, displayName: string): string { + const fromName = path.extname(displayName); + + if (fromName) { + return fromName; + } + + if (file.type && MIME_EXTENSION_MAP[file.type]) { + return MIME_EXTENSION_MAP[file.type]; + } + + return ''; +} + +export async function processUploadedFile( + file: FileLike, + { + uploadFolder, + base64LimitBytes = DEFAULT_BASE64_LIMIT_BYTES + }: { + uploadFolder: string; + base64LimitBytes?: number; + } +): Promise { + const displayName = sanitizeDisplayName(file.name, 'material'); + + const bytes = await file.arrayBuffer(); + const buffer = Buffer.from(bytes); + + if (file.size <= base64LimitBytes) { + const mimeType = file.type || 'application/octet-stream'; + const base64 = buffer.toString('base64'); + const fileUrl = `data:${mimeType};base64,${base64}`; + + return { + fileUrl, + fileName: displayName, + storage: 'base64' + }; + } + + const uploadsRoot = path.join(process.cwd(), 'public', 'uploads'); + const uploadDir = path.join(uploadsRoot, uploadFolder); + + await mkdir(uploadDir, { recursive: true }); + + const extension = resolveExtension(file, displayName); + const generatedName = `${randomUUID()}${extension}`; + const filePath = path.join(uploadDir, generatedName); + + await writeFile(filePath, buffer); + + const relativeUrl = path.posix.join('/uploads', uploadFolder.replace(/\\/g, '/'), generatedName); + + return { + fileUrl: relativeUrl, + fileName: displayName, + storage: 'filesystem' + }; +} + +export function validateFile( + file: FileLike, + { + allowedMimeTypes = DEFAULT_ALLOWED_MIME_TYPES, + maxFileSizeBytes = MAX_FILE_SIZE_BYTES + }: { + allowedMimeTypes?: Set; + maxFileSizeBytes?: number; + } +): string | null { + if (file.size <= 0) { + return 'Arquivo vazio'; + } + + if (file.size > maxFileSizeBytes) { + return `Arquivo maior que ${(maxFileSizeBytes / 1024 / 1024).toFixed(0)}MB`; + } + + if (file.type && !allowedMimeTypes.has(file.type)) { + return 'Formato de arquivo não suportado'; + } + + return null; +} diff --git a/src/app/api/cf-renda-turbinada/utils.ts b/src/app/api/cf-renda-turbinada/utils.ts new file mode 100644 index 000000000..8e93074b5 --- /dev/null +++ b/src/app/api/cf-renda-turbinada/utils.ts @@ -0,0 +1,92 @@ +import { + CfRendaTurbinadaLesson, + CfRendaTurbinadaLessonMaterial, + CfRendaTurbinadaLiveEvent, + CfRendaTurbinadaMentoria, + CfRendaTurbinadaMentoriaMaterial, + CfRendaTurbinadaModule +} from '@prisma/client'; + +type ModuleWithLessons = CfRendaTurbinadaModule & { + lessons: (CfRendaTurbinadaLesson & { materials: CfRendaTurbinadaLessonMaterial[] })[]; +}; + +type LessonWithMaterials = CfRendaTurbinadaLesson & { + materials: CfRendaTurbinadaLessonMaterial[]; +}; + +type MentoriaWithMaterials = CfRendaTurbinadaMentoria & { + materials: CfRendaTurbinadaMentoriaMaterial[]; +}; + +export function serializeLessonMaterial(material: CfRendaTurbinadaLessonMaterial) { + return { + id: material.id, + title: material.title, + fileUrl: material.fileUrl, + createdAt: material.createdAt.toISOString(), + updatedAt: material.updatedAt.toISOString() + }; +} + +export function serializeLesson(lesson: LessonWithMaterials) { + return { + id: lesson.id, + title: lesson.title, + duration: lesson.duration, + description: lesson.description ?? undefined, + videoUrl: lesson.videoUrl ?? undefined, + content: lesson.content ?? undefined, + sortOrder: lesson.sortOrder ?? undefined, + materials: lesson.materials.map(serializeLessonMaterial) + }; +} + +export function serializeModule(module: ModuleWithLessons) { + return { + id: module.id, + title: module.title, + highlight: module.highlight ?? '', + releaseDate: module.releaseDate.toISOString(), + isHidden: module.isHidden, + sortOrder: module.sortOrder ?? undefined, + lessons: module.lessons.map(serializeLesson) + }; +} + +export function serializeLiveEvent(event: CfRendaTurbinadaLiveEvent) { + return { + id: event.id, + title: event.title, + description: event.description ?? undefined, + eventDate: event.eventDate.toISOString(), + durationMinutes: event.durationMinutes, + meetingUrl: event.meetingUrl ?? undefined, + isCancelled: event.isCancelled + }; +} + +export function serializeMentoriaMaterial(material: CfRendaTurbinadaMentoriaMaterial) { + return { + id: material.id, + title: material.title, + fileUrl: material.fileUrl, + createdAt: material.createdAt.toISOString(), + updatedAt: material.updatedAt.toISOString() + }; +} + +export function serializeMentoria(mentoria: MentoriaWithMaterials) { + return { + id: mentoria.id, + title: mentoria.title, + description: mentoria.description ?? undefined, + content: mentoria.content ?? undefined, + videoUrl: mentoria.videoUrl ?? undefined, + recordedAt: mentoria.recordedAt.toISOString(), + releaseDate: mentoria.releaseDate?.toISOString(), + durationMinutes: mentoria.durationMinutes ?? undefined, + sortOrder: mentoria.sortOrder ?? undefined, + materials: mentoria.materials.map(serializeMentoriaMaterial) + }; +} diff --git a/src/app/api/check-admin/route.ts b/src/app/api/check-admin/route.ts new file mode 100644 index 000000000..9fe1a5e51 --- /dev/null +++ b/src/app/api/check-admin/route.ts @@ -0,0 +1,26 @@ +import { NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +export async function GET() { + try { + const users = await prisma.user.findMany({ + select: { + id: true, + email: true, + firstName: true, + lastName: true, + plan: true + } + }); + + return NextResponse.json({ + success: true, + users, + jwtConfigured: !!process.env.JWT_SECRET + }); + } catch (error) { + return NextResponse.json({ + error: error.message + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/cron/update-ativos/route.ts b/src/app/api/cron/update-ativos/route.ts new file mode 100644 index 000000000..fa5b93bb4 --- /dev/null +++ b/src/app/api/cron/update-ativos/route.ts @@ -0,0 +1,39 @@ +// app/api/cron/update-ativos/route.ts +export const dynamic = 'force-dynamic' + +export async function GET(request: Request) { // <-- Adicione request como parâmetro + // Verificar auth do cron + if (process.env.NODE_ENV === 'production') { + const authHeader = request.headers.get('authorization'); + if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + } + + const ativosPopulares = [ + 'PETR4', 'VALE3', 'ITUB4', 'BBDC4', 'BBAS3', + 'AAPL', 'NVDA', 'GOOGL', 'META', 'AMZN' + ]; + + let sucessos = 0; + let erros = 0; + + for (const ticker of ativosPopulares) { + try { + const dados = await fetchDadosConsolidados(ticker); + await CacheService.set(CacheService.getCacheKey(ticker), dados, 600); + sucessos++; + } catch (error) { + console.error(`Erro ao atualizar ${ticker}:`, error); + erros++; + } + } + + return Response.json({ + success: true, + processados: ativosPopulares.length, + sucessos, + erros, + timestamp: new Date().toISOString() + }); +} \ No newline at end of file diff --git a/src/app/api/dados/route.ts b/src/app/api/dados/route.ts new file mode 100644 index 000000000..d307b8ab8 --- /dev/null +++ b/src/app/api/dados/route.ts @@ -0,0 +1,32 @@ +export async function GET() { + const agora = new Date(); + const hora = agora.getHours(); + const minuto = agora.getMinutes(); + + // Simulação realista baseada no horário + const isHorarioComercial = hora >= 9 && hora <= 18; + const valorBase = 3440; + const variacao = (Math.random() - 0.5) * (isHorarioComercial ? 3 : 1); + const novoValor = valorBase * (1 + variacao / 100); + + const dadosIfix = { + valor: novoValor, + valorFormatado: novoValor.toLocaleString('pt-BR', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }), + variacao: valorBase * (variacao / 100), + variacaoPercent: variacao, + trend: variacao >= 0 ? 'up' : 'down', + timestamp: agora.toISOString(), + fonte: 'SIMULAÇÃO_VERCEL', + nota: `Atualizado ${hora.toString().padStart(2, '0')}:${minuto.toString().padStart(2, '0')}`, + symbol: 'IFIX' + }; + + return Response.json({ + ifix: dadosIfix, + success: true, + timestamp: agora.toISOString() + }); +} diff --git a/src/app/api/dividend-yield/batch/route.ts b/src/app/api/dividend-yield/batch/route.ts new file mode 100644 index 000000000..622c5a906 --- /dev/null +++ b/src/app/api/dividend-yield/batch/route.ts @@ -0,0 +1,159 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { calcularDyAPartirDoResultado, formatarDy } from '@/utils/dividendYield'; +import { getMarketSnapshot, type CombinedQuote, type MarketSnapshot } from '@/server/market-data'; + +export const dynamic = 'force-dynamic'; +export const revalidate = 0; +export const runtime = 'nodejs'; + +type DividendYieldResult = { + dy12Meses: number; + dyFormatado: string; + fonte: string; + symbol?: string; + requested: string; +}; + +function normalizeTickerKey(symbol?: string): string { + if (!symbol) return ''; + const upper = symbol.toUpperCase(); + if (upper.startsWith('^')) { + return upper; + } + const suffixes = ['.SA', '.BS', '.US', '.F']; + for (const suffix of suffixes) { + if (upper.endsWith(suffix)) { + return upper.slice(0, -suffix.length); + } + } + return upper; +} + +function resolveQuote(snapshot: MarketSnapshot, ticker: string): CombinedQuote | undefined { + const normalized = normalizeTickerKey(ticker); + const candidates = new Set([ + normalized, + ticker.toUpperCase(), + `${normalized}.SA` + ]); + + for (const key of candidates) { + const quote = snapshot.quotes[key]; + if (quote) { + return quote; + } + } + + return undefined; +} + +function buildDividendYieldResult(ticker: string, quote: CombinedQuote | undefined): DividendYieldResult { + const normalized = normalizeTickerKey(ticker); + + if (quote?.brapi) { + const { valor, origem } = calcularDyAPartirDoResultado(quote.brapi, normalized); + const dy12Meses = valor && valor > 0 ? valor : 0; + + return { + dy12Meses, + dyFormatado: dy12Meses > 0 ? formatarDy(dy12Meses) : '0,00%', + fonte: `market_fetcher | ${origem}`, + symbol: typeof quote.brapi.symbol === 'string' ? String(quote.brapi.symbol).toUpperCase() : normalized, + requested: normalized + }; + } + + return { + dy12Meses: 0, + dyFormatado: '0,00%', + fonte: 'market_fetcher | sem_dados', + symbol: normalized, + requested: normalized + }; +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const tickers = Array.isArray(body?.tickers) ? body.tickers : []; + + if (tickers.length === 0) { + return NextResponse.json( + { success: false, error: 'Lista de tickers vazia' }, + { status: 400 } + ); + } + + const sanitizedTickers = tickers + .map((ticker: string) => String(ticker || '').trim().toUpperCase()) + .filter((ticker) => ticker); + + const uniqueTickers = Array.from(new Set(sanitizedTickers)); + + const snapshot = await getMarketSnapshot(uniqueTickers, { includeDividends: true }); + + const results: Record = {}; + + uniqueTickers.forEach((ticker) => { + const normalized = normalizeTickerKey(ticker); + const quote = resolveQuote(snapshot, ticker); + results[normalized] = buildDividendYieldResult(ticker, quote); + }); + + return NextResponse.json({ + success: true, + timestamp: new Date().toISOString(), + results, + metadata: { + totalRequested: uniqueTickers.length, + cache: { + fromCache: snapshot.fromCache, + cachedAt: snapshot.cachedAt, + cacheTtlMs: snapshot.cacheTtlMs, + expiresAt: snapshot.cachedAt + snapshot.cacheTtlMs + }, + upstream: snapshot.metadata + } + }); + } catch (error) { + console.error('⌠Erro na API de DY batch:', error); + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Erro interno', + results: {} + }, + { status: 500 } + ); + } +} + +export async function GET() { + return NextResponse.json({ + success: true, + message: 'Utilize POST com { "tickers": ["ITUB4", "VALE3"] } para calcular DY em lote.', + example: { + method: 'POST', + body: { tickers: ['ITUB4', 'VALE3'] }, + }, + timestamp: new Date().toISOString(), + }, { + status: 200, + headers: { + 'Cache-Control': 'no-store, max-age=0', + 'CDN-Cache-Control': 'no-store', + 'Vercel-CDN-Cache-Control': 'no-store', + }, + }); +} + +export async function OPTIONS() { + return new NextResponse(null, { + status: 204, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + }, + }); +} diff --git a/src/app/api/faq/create/route.ts b/src/app/api/faq/create/route.ts new file mode 100644 index 000000000..c9cd34ffb --- /dev/null +++ b/src/app/api/faq/create/route.ts @@ -0,0 +1,147 @@ +// src/app/api/faq/create/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +export async function POST(request: NextRequest) { + try { + console.log('🚀 [FAQ-CREATE] Iniciando criação de FAQ...'); + + // USAR O MESMO SISTEMA QUE AS OUTRAS APIS FUNCIONAIS + // Verificar autenticação da mesma forma que /api/auth/me + + let user; + + // Opção 1: Verificar se existe um middleware de autenticação global + // Opção 2: Usar cookies/session como as outras APIs + + // Primeiro, vamos tentar extrair usuário do request da mesma forma que outras APIs + try { + // Verificar se existe user no request (middleware) + // @ts-ignore - Verificar se o middleware já setou o user + user = (request as any).user; + + if (!user) { + // Se não tem user no request, verificar cookies/session + const cookies = request.cookies; + console.log('🪠[DEBUG] Cookies disponíveis:', Array.from(cookies.getAll().map(c => c.name))); + + // Verificar se existe um cookie de sessão ou auth + const sessionCookie = cookies.get('session') || + cookies.get('auth') || + cookies.get('token') || + cookies.get('next-auth.session-token') || + cookies.get('__Secure-next-auth.session-token'); + + if (sessionCookie) { + console.log('🪠[AUTH] Cookie de sessão encontrado:', sessionCookie.name); + // Aqui você precisa verificar como decodificar/validar o cookie + // Isso depende de como o sistema de auth atual funciona + } + + // TEMPORÃRIO: Buscar usuário por email hardcoded para teste + // REMOVER ISSO DEPOIS DE DESCOBRIR A AUTENTICAÇÃO CORRETA + const testUser = await prisma.user.findFirst({ + where: { + plan: 'ADMIN', + status: 'ACTIVE' + } + }); + + if (testUser) { + console.log('âš ï¸ [TEMP] Usando usuário admin para teste:', testUser.email); + user = testUser; + } + } + + } catch (error) { + console.log('⌠[AUTH] Erro na verificação de autenticação:', error.message); + } + + if (!user) { + console.log('⌠[AUTH] Usuário não encontrado - cookies/session inválidos'); + return NextResponse.json({ + error: 'Authentication required', + debug: 'No valid session found' + }, { status: 401 }); + } + + if (user.plan !== 'ADMIN' && user.plan !== 'ANALISTA') { + console.log('⌠[AUTH] Acesso negado - Plano do usuário:', user.plan); + return NextResponse.json({ + error: 'Admin access required', + debug: `User plan: ${user.plan}, required: ADMIN` + }, { status: 403 }); + } + + console.log('✅ [AUTH] Usuário autenticado:', user.email, '- Plano:', user.plan); + + const body = await request.json(); + const { title, content, category, faqOrder = 0 } = body; + + // Validação + if (!title || !content || !category) { + return NextResponse.json({ + error: 'Title, content and category are required' + }, { status: 400 }); + } + + // Criar pergunta virtual + const question = await prisma.question.create({ + data: { + title: title, + content: `FAQ criada diretamente: ${title}`, + category: category, + status: 'RESPONDIDA', + userId: user.id, + readByAdmin: true + } + }); + + // Criar resposta como FAQ + const answer = await prisma.answer.create({ + data: { + content: content, + questionId: question.id, + adminId: user.id, + isOfficial: true, + readByUser: true, + isFaq: true, + faqTitle: title, + faqOrder: faqOrder + }, + include: { + question: { + select: { + id: true, + title: true, + content: true, + category: true + } + }, + admin: { + select: { + id: true, + firstName: true, + lastName: true + } + } + } + }); + + console.log('✅ [SUCCESS] FAQ criada com sucesso:', answer.id); + + return NextResponse.json({ + faq: answer, + message: 'FAQ criada com sucesso' + }, { status: 201 }); + + } catch (error) { + console.error('💥 [ERROR] Erro ao criar FAQ:', error); + return NextResponse.json({ + error: 'Internal server error', + details: process.env.NODE_ENV === 'development' ? error.message : 'Server error' + }, { status: 500 }); + } finally { + await prisma.$disconnect(); + } +} diff --git a/src/app/api/faq/route.test.js b/src/app/api/faq/route.test.js new file mode 100644 index 000000000..f40a99aa4 --- /dev/null +++ b/src/app/api/faq/route.test.js @@ -0,0 +1,59 @@ +const fs = require('fs'); +const path = require('path'); +const ts = require('typescript'); +const { NextRequest } = require('next/server'); + +const mockFaqs = [ + { + id: 1, + content: 'Example content', + question: { category: 'GENERAL' }, + admin: { firstName: 'John', lastName: 'Doe' }, + faqOrder: 0 + } +]; + +jest.mock('@prisma/client', () => { + return { + PrismaClient: jest.fn().mockImplementation(() => ({ + answer: { + findMany: jest.fn().mockResolvedValue(mockFaqs) + }, + $disconnect: jest.fn().mockResolvedValue(undefined) + })) + }; +}); + +function loadRoute() { + const tsFile = fs.readFileSync(path.join(__dirname, 'route.ts'), 'utf8'); + const transpiled = ts.transpileModule(tsFile, { compilerOptions: { module: ts.ModuleKind.CommonJS } }); + const module = { exports: {} }; + const fn = new Function('require', 'module', 'exports', transpiled.outputText); + fn(require, module, module.exports); + return module.exports; +} + +const { GET } = loadRoute(); + +describe('GET /api/faq', () => { + it('returns FAQs without credentials', async () => { + const req = new NextRequest('http://localhost/api/faq'); + const res = await GET(req); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.faqs).toEqual(mockFaqs); + }); + + it('returns FAQs with credentials', async () => { + const req = new NextRequest('http://localhost/api/faq', { + headers: { + 'X-User-Email': 'user@example.com', + Authorization: 'Bearer token' + } + }); + const res = await GET(req); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.faqs).toEqual(mockFaqs); + }); +}); diff --git a/src/app/api/faq/route.ts b/src/app/api/faq/route.ts new file mode 100644 index 000000000..246d85e73 --- /dev/null +++ b/src/app/api/faq/route.ts @@ -0,0 +1,53 @@ +// src/app/api/faq/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +export async function GET(request: NextRequest) { + try { + // Public endpoint: no authentication headers are required + const { searchParams } = new URL(request.url); + const page = Math.max(1, Number(searchParams.get('page') ?? 1)); + const rawLimit = Number(searchParams.get('limit') ?? 20); + const limit = Math.min(50, Math.max(10, Number.isFinite(rawLimit) ? rawLimit : 20)); + + const faqs = await prisma.answer.findMany({ + where: { isFaq: true }, + select: { + id: true, + content: true, + faqTitle: true, + faqOrder: true, + createdAt: true, + admin: { select: { firstName: true, lastName: true } }, + question: { + select: { + id: true, + title: true, + category: true + } + } + }, + orderBy: [{ faqOrder: 'asc' }, { createdAt: 'desc' }], + skip: (page - 1) * limit, + take: limit + }); + const total = await prisma.answer.count({ where: { isFaq: true } }); + return NextResponse.json( + { + faqs, + pagination: { page, limit, total, totalPages: Math.ceil(total / limit) }, + }, + { + headers: { + 'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=120', + }, + } + ); + + } catch (error) { + console.error('Error fetching FAQs:', error); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } finally { + await prisma.$disconnect(); + } +} diff --git a/src/app/api/fiis/portfolio/route.ts b/src/app/api/fiis/portfolio/route.ts new file mode 100644 index 000000000..81b133363 --- /dev/null +++ b/src/app/api/fiis/portfolio/route.ts @@ -0,0 +1,166 @@ +import { NextResponse } from 'next/server'; +import { type UserFiis, type ViesOverride } from '@prisma/client'; +import { prisma } from '@/lib/prisma'; +import { DEFAULT_FIIS_PORTFOLIO } from '@/data/carteiras-default'; +import { auth } from '@/lib/auth'; +import { CacheService } from '@/lib/cache-service'; + +export const dynamic = 'force-dynamic'; +export const runtime = 'nodejs'; + +interface FiiPortfolioAsset { + id: string; + ticker: string; + setor: string; + dataEntrada: string; + precoEntrada: number; + precoTeto: number | null; + posicaoEncerrada: boolean; + dataSaida: string | null; + precoSaida: number | null; + viesOverride: ViesOverride | 'AUTO'; + ordem: number; +} + +const FALLBACK_ASSETS: FiiPortfolioAsset[] = DEFAULT_FIIS_PORTFOLIO.map((fii, index) => ({ + id: fii.id ?? `fallback-${index}`, + ticker: fii.ticker, + setor: fii.setor, + dataEntrada: fii.dataEntrada, + precoEntrada: Number(fii.precoEntrada ?? 0), + precoTeto: typeof fii.precoTeto === 'number' ? Number(fii.precoTeto) : null, + posicaoEncerrada: false, + dataSaida: null, + precoSaida: null, + viesOverride: 'AUTO', + ordem: index + 1, +})); + +function mapDatabaseAssets(records: UserFiis[]): FiiPortfolioAsset[] { + return records.map((record, index) => ({ + id: record.id, + ticker: record.ticker, + setor: record.setor, + dataEntrada: record.dataEntrada, + precoEntrada: Number(record.precoEntrada ?? 0), + precoTeto: typeof record.precoTeto === 'number' ? Number(record.precoTeto) : null, + posicaoEncerrada: Boolean(record.posicaoEncerrada), + dataSaida: record.dataSaida ?? null, + precoSaida: typeof record.precoSaida === 'number' ? Number(record.precoSaida) : null, + viesOverride: record.viesOverride ?? 'AUTO', + ordem: index + 1, + })); +} + +function calculateMetrics(ativos: FiiPortfolioAsset[]) { + const totalAtivos = ativos.length; + const ativosAtivos = ativos.filter(ativo => !ativo.posicaoEncerrada).length; + const ativosEncerrados = totalAtivos - ativosAtivos; + + const totalInvestido = ativos.reduce((sum, ativo) => sum + (ativo.precoEntrada || 0), 0); + const precoEntradaMedio = totalAtivos > 0 ? totalInvestido / totalAtivos : 0; + + const ativosComTeto = ativos.filter(ativo => typeof ativo.precoTeto === 'number' && ativo.precoTeto !== null); + const precoTetoMedio = ativosComTeto.length > 0 + ? ativosComTeto.reduce((sum, ativo) => sum + (ativo.precoTeto || 0), 0) / ativosComTeto.length + : 0; + + const setores = ativos.reduce>((acc, ativo) => { + const chave = ativo.setor || 'Indefinido'; + acc[chave] = (acc[chave] || 0) + 1; + return acc; + }, {}); + + const distribuicaoSetorial = Object.entries(setores) + .map(([setor, quantidade]) => ({ + setor, + quantidade, + percentual: totalAtivos > 0 ? Number(((quantidade / totalAtivos) * 100).toFixed(2)) : 0, + })) + .sort((a, b) => b.quantidade - a.quantidade); + + return { + totais: { + totalAtivos, + ativosAtivos, + ativosEncerrados, + totalInvestido: Number(totalInvestido.toFixed(2)), + precoEntradaMedio: Number(precoEntradaMedio.toFixed(2)), + precoTetoMedio: Number(precoTetoMedio.toFixed(2)), + }, + distribuicaoSetorial, + destaqueSetorial: distribuicaoSetorial[0] ?? null, + }; +} + +export async function GET() { + const session = await auth(); + const userId = session?.user?.id ?? 'anonymous'; + + const cacheKey = CacheService.getCacheKey('fii-portfolio', userId); + + const { data, metadata } = await CacheService.withCache( + cacheKey, + async () => { + let assets: FiiPortfolioAsset[] | null = null; + let source: 'database' | 'static-fallback' = 'database'; + let ultimaAtualizacao: string | null = null; + let fetchError: string | null = null; + + try { + const databaseAssets = await prisma.userFiis.findMany({ + orderBy: { editadoEm: 'asc' }, + }); + + if (databaseAssets.length > 0) { + assets = mapDatabaseAssets(databaseAssets); + const mostRecentUpdate = databaseAssets.reduce((acc, current) => { + const updatedAt = current.updatedAt instanceof Date + ? current.updatedAt.getTime() + : new Date(current.updatedAt as unknown as string).getTime(); + return Math.max(acc, updatedAt || 0); + }, 0); + ultimaAtualizacao = mostRecentUpdate ? new Date(mostRecentUpdate).toISOString() : null; + } + } catch (error) { + fetchError = error instanceof Error ? error.message : 'Erro desconhecido ao consultar FIIs'; + console.error('⌠Erro ao buscar FIIs no banco de dados:', error); + } + + if (!assets || assets.length === 0) { + assets = FALLBACK_ASSETS; + source = 'static-fallback'; + } + + const metrics = calculateMetrics(assets); + + return { + success: true, + source, + generatedAt: new Date().toISOString(), + portfolio: { + ...metrics, + totalSetores: metrics.distribuicaoSetorial.length, + ultimaAtualizacao, + }, + assets, + metadata: { + fallbackUtilizado: source !== 'database', + fetchError, + }, + }; + }, + { + ttlSeconds: 300, + tags: ['fii-portfolio', userId], + } + ); + + return NextResponse.json(data, { + status: 200, + headers: { + 'Cache-Control': 'public, max-age=300', + 'X-Cache-Hit': String(metadata.cached), + }, + }); +} diff --git a/src/app/api/financial/fii-market-data/route.ts b/src/app/api/financial/fii-market-data/route.ts new file mode 100644 index 000000000..bac994bbc --- /dev/null +++ b/src/app/api/financial/fii-market-data/route.ts @@ -0,0 +1,258 @@ +// app/api/financial/fii-market-data/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { buildCacheHeaders } from '@/server/cache-headers'; +import { CacheService } from '@/lib/cache-service'; + +export const maxDuration = 5; + +// 🔥 TIPOS PARA DADOS DE MERCADO DOS FIIs +interface FIIMarketData { + ifix: { + value: string; + trend: 'up' | 'down'; + diff: number; + }; + carteiraHoje: { + value: string; + trend: 'up' | 'down'; + diff: number; + }; + dividendYield: { + value: string; + trend: 'up' | 'down'; + diff: number; + }; + carteiraPeriodo: { + value: string; + trend: 'up' | 'down'; + diff: number; + }; +} + +const CACHE_HEADERS = buildCacheHeaders({ ttl: 300, swr: 1200 }); +const CACHE_DURATION = 300000; +let cachedData: { data: FIIMarketData; timestamp: number } | null = null; + +// 🔑 TOKEN BRAPI VALIDADO +const BRAPI_TOKEN = 'jJrMYVy9MATGEicx3GxBp8'; + +// 📊 FUNÇÃO PARA BUSCAR IFIX EM TEMPO REAL +async function fetchIFIXRealTime(): Promise<{ value: string; trend: 'up' | 'down'; diff: number }> { + try { + console.log('🔠Buscando IFIX via BRAPI...'); + + const response = await fetch(`https://brapi.dev/api/quote/IFIX?token=${BRAPI_TOKEN}`, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'User-Agent': 'FII-Market-Data-API' + } + }); + + if (response.ok) { + const data = await response.json(); + + if (data.results && data.results.length > 0) { + const ifixData = data.results[0]; + + return { + value: Math.round(ifixData.regularMarketPrice).toLocaleString('pt-BR'), + trend: (ifixData.regularMarketChangePercent || 0) >= 0 ? 'up' : 'down', + diff: ifixData.regularMarketChangePercent || 0 + }; + } + } + + throw new Error('Falha ao obter dados do IFIX'); + } catch (error) { + console.error('⌠Erro ao buscar IFIX:', error); + + // 🔄 FALLBACK + return { + value: "3.200", + trend: "up", + diff: 0.25 + }; + } +} + +// 📈 FUNÇÃO PARA SIMULAR DADOS DE CARTEIRA (pode ser substituída por dados reais) +function generatePortfolioData(): { + carteiraHoje: { value: string; trend: 'up' | 'down'; diff: number }; + dividendYield: { value: string; trend: 'up' | 'down'; diff: number }; + carteiraPeriodo: { value: string; trend: 'up' | 'down'; diff: number }; +} { + // 🎯 EM PRODUÇÃO, AQUI VIRIAM OS CÃLCULOS REAIS DA CARTEIRA + // baseados nos FIIs do usuário e suas cotações atuais + + const now = new Date(); + const hour = now.getHours(); + + // 📊 Variação baseada no horário para simular movimento do mercado + const baseVariation = Math.sin((hour * Math.PI) / 12) * 2; + + const carteiraHoje = { + value: "12.4%", + trend: baseVariation >= 0 ? "up" as const : "down" as const, + diff: 12.4 + baseVariation + }; + + const dividendYield = { + value: "11.8%", + trend: "up" as const, + diff: 11.8 + }; + + const carteiraPeriodo = { + value: "15.2%", + trend: "up" as const, + diff: 15.2 + (baseVariation * 0.5) + }; + + return { + carteiraHoje, + dividendYield, + carteiraPeriodo + }; +} + +// 🚀 FUNÇÃO PRINCIPAL PARA BUSCAR TODOS OS DADOS +async function fetchFIIMarketData(): Promise { + try { + console.log('🔄 Iniciando busca de dados de mercado FIIs...'); + + // 📊 BUSCAR DADOS EM PARALELO + const [ifixData, portfolioData] = await Promise.all([ + fetchIFIXRealTime(), + Promise.resolve(generatePortfolioData()) + ]); + + const marketData: FIIMarketData = { + ifix: ifixData, + ...portfolioData + }; + + console.log('✅ Dados de mercado FIIs coletados:', marketData); + return marketData; + + } catch (error) { + console.error('⌠Erro ao buscar dados de mercado FIIs:', error); + + // 🔄 FALLBACK COMPLETO + return { + ifix: { value: "3.200", trend: "up", diff: 0.25 }, + carteiraHoje: { value: "12.4%", trend: "up", diff: 12.4 }, + dividendYield: { value: "11.8%", trend: "up", diff: 11.8 }, + carteiraPeriodo: { value: "15.2%", trend: "up", diff: 15.2 } + }; + } +} + +// 🌠HANDLER DA API +export async function GET(request: NextRequest) { + try { + console.log('🌠API FII Market Data chamada'); + const cacheKey = CacheService.getCacheKey('market', 'fii', 'all'); + + const { data, metadata } = await CacheService.withCache( + cacheKey, + async () => { + const marketData = await fetchFIIMarketData(); + const now = Date.now(); + + return { + success: true, + marketData, + timestamp: new Date(now).toISOString(), + source: 'fresh', + nextUpdate: new Date(now + 300000).toISOString() + }; + }, + { + ttlSeconds: 300, + tags: ['market', 'fii'], + } + ); + + cachedData = { + data: data.marketData, + timestamp: Date.now(), + }; + + return NextResponse.json( + metadata.cached + ? { ...data, source: 'cache' } + : data, + { headers: CACHE_HEADERS } + ); + + } catch (error) { + console.error('⌠Erro na API FII Market Data:', error); + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Erro interno do servidor', + timestamp: new Date().toISOString(), + marketData: { + ifix: { value: "3.200", trend: "up", diff: 0.25 }, + carteiraHoje: { value: "12.4%", trend: "up", diff: 12.4 }, + dividendYield: { value: "11.8%", trend: "up", diff: 11.8 }, + carteiraPeriodo: { value: "15.2%", trend: "up", diff: 15.2 } + } + }, { status: 500 }); + } +} + +// 🔄 MÉTODO POST PARA FORÇAR REFRESH DO CACHE +export async function POST(request: NextRequest) { + try { + console.log('🔄 Forçando refresh do cache FII Market Data...'); + + // ðŸ—‘ï¸ LIMPAR CACHE + cachedData = null; + + // 📊 BUSCAR DADOS FRESCOS + const marketData = await fetchFIIMarketData(); + + // 💾 ATUALIZAR CACHE + cachedData = { + data: marketData, + timestamp: Date.now() + }; + + return NextResponse.json({ + success: true, + message: 'Cache refreshed successfully', + marketData, + timestamp: new Date().toISOString(), + source: 'force_refresh' + }); + + } catch (error) { + console.error('⌠Erro ao forçar refresh:', error); + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Erro ao refresh', + timestamp: new Date().toISOString() + }, { status: 500 }); + } +} + +// 📊 MÉTODO PARA OBTER STATUS DO CACHE +export async function HEAD(request: NextRequest) { + const headers = new Headers(); + + if (cachedData) { + headers.set('X-Cache-Status', 'HIT'); + headers.set('X-Cache-Age', Math.floor((Date.now() - cachedData.timestamp) / 1000).toString()); + headers.set('X-Cache-Expires', Math.floor((CACHE_DURATION - (Date.now() - cachedData.timestamp)) / 1000).toString()); + } else { + headers.set('X-Cache-Status', 'MISS'); + } + + headers.set('X-API-Version', '1.0'); + headers.set('X-Data-Source', 'BRAPI + Custom'); + + return new NextResponse(null, { status: 200, headers }); +} diff --git a/src/app/api/financial/international-data/route.ts b/src/app/api/financial/international-data/route.ts new file mode 100644 index 000000000..3c1a32979 --- /dev/null +++ b/src/app/api/financial/international-data/route.ts @@ -0,0 +1,332 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { CacheService } from '@/lib/cache-service'; +import { buildCacheHeaders } from '@/server/cache-headers'; + +// ðŸ›¡ï¸ CONFIGURAÇÃO PARA VERCEL - EVITA ERRO DE STATIC GENERATION +export const dynamic = 'force-dynamic'; +export const revalidate = 0; +export const fetchCache = 'force-no-store'; +export const runtime = 'nodejs'; +export const maxDuration = 5; + +// 🔑 SUA CHAVE DA API BRAPI +const BRAPI_TOKEN = 'jJrMYVy9MATGEicx3GxBp8'; +const isDev = process.env.NODE_ENV !== 'production'; +const CACHE_HEADERS = buildCacheHeaders({ ttl: 300, swr: 600 }); + +// 🔧 FUNÇÕES AUXILIARES +function getTrendDirection(change: number): 'up' | 'down' { + return change >= 0 ? 'up' : 'down'; +} + +function formatValue(value: number): string { + return new Intl.NumberFormat('en-US', { + minimumFractionDigits: 0, + maximumFractionDigits: 0 + }).format(value); +} + +function formatPercent(value: number): string { + const formatted = Math.abs(value).toFixed(2); + return value >= 0 ? `+${formatted}%` : `-${formatted}%`; +} + +// 🌎 FUNÇÃO PARA BUSCAR DADOS REAIS DA BRAPI +async function fetchBrapiData() { + try { + if (isDev) { + console.log('🔄 Buscando dados da Brapi...'); + } + + // Buscar índices americanos via Brapi + const symbols = [ + '^GSPC', // S&P 500 + '^IXIC', // NASDAQ + '^DJI', // Dow Jones + '^BVSP' // Ibovespa (bonus) + ].join(','); + + const response = await fetch( + `https://brapi.dev/api/quote/${symbols}?token=${BRAPI_TOKEN}&fundamental=false`, + { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'User-Agent': 'Mozilla/5.0 (compatible; InvestmentApp/1.0)' + }, + next: { revalidate: 300 } // Cache por 5 minutos + } + ); + + if (!response.ok) { + throw new Error(`Brapi HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + if (isDev) { + console.log('✅ Dados recebidos da Brapi:', data.results?.length, 'símbolos'); + } + + if (!data.results || data.results.length === 0) { + throw new Error('Nenhum dado retornado pela Brapi'); + } + + // Mapear dados dos índices + const indices: any = {}; + + data.results.forEach((stock: any) => { + const changePercent = stock.regularMarketChangePercent || 0; + const currentPrice = stock.regularMarketPrice || 0; + + if (isDev) { + console.log(`📊 ${stock.symbol}: ${currentPrice} (${changePercent.toFixed(2)}%)`); + } + + switch (stock.symbol) { + case '^GSPC': // S&P 500 + indices.sp500 = { + value: formatValue(currentPrice), + trend: getTrendDirection(changePercent), + diff: Number(changePercent.toFixed(2)), + price: currentPrice, + change: stock.regularMarketChange || 0 + }; + break; + + case '^IXIC': // NASDAQ + indices.nasdaq = { + value: formatValue(currentPrice), + trend: getTrendDirection(changePercent), + diff: Number(changePercent.toFixed(2)), + price: currentPrice, + change: stock.regularMarketChange || 0 + }; + break; + + case '^DJI': // Dow Jones + indices.dow = { + value: formatValue(currentPrice), + trend: getTrendDirection(changePercent), + diff: Number(changePercent.toFixed(2)), + price: currentPrice, + change: stock.regularMarketChange || 0 + }; + break; + + case '^BVSP': // Ibovespa + indices.ibovespa = { + value: formatValue(currentPrice), + trend: getTrendDirection(changePercent), + diff: Number(changePercent.toFixed(2)), + price: currentPrice, + change: stock.regularMarketChange || 0 + }; + break; + } + }); + + return indices; + + } catch (error) { + console.error('⌠Erro ao buscar dados da Brapi:', error); + return null; + } +} + +// 🔠FUNÇÃO AUXILIAR PARA VERIFICAR SE MERCADO ESTà ABERTO +function isMarketOpen(): boolean { + const now = new Date(); + const nyTime = new Date(now.toLocaleString("en-US", {timeZone: "America/New_York"})); + const day = nyTime.getDay(); // 0 = domingo, 6 = sábado + const hour = nyTime.getHours(); + const minute = nyTime.getMinutes(); + const totalMinutes = hour * 60 + minute; + + // Mercado aberto de segunda a sexta, 9:30 às 16:00 (hora de NY) + const isWeekday = day >= 1 && day <= 5; + const isMarketHours = totalMinutes >= (9 * 60 + 30) && totalMinutes <= (16 * 60); + + return isWeekday && isMarketHours; +} + +// 🚀 HANDLER PRINCIPAL DA API +export async function GET(request: NextRequest) { + try { + const cacheKey = 'market:international:all'; + + const data = await CacheService.withCache({ + key: cacheKey, + ttlSeconds: 300, + tags: ['market', 'international'], + fn: async () => { + const startTime = Date.now(); + + if (isDev) { + console.log('🌎 International Data API - Iniciando com Brapi...'); + } + + // Buscar dados reais da Brapi + const brapiData = await fetchBrapiData(); + + // Dados de fallback caso a Brapi falhe + const fallbackData = { + sp500: { value: "5.845", trend: "up" as const, diff: 25.13 }, + nasdaq: { value: "19.345", trend: "down" as const, diff: -1.00 }, + dow: { value: "42.156", trend: "up" as const, diff: 0.44 } + }; + + // Usar dados da Brapi se disponíveis, senão fallback + const indices = brapiData || fallbackData; + + // 📊 DADOS ESPECÃFICOS PARA EXTERIOR STOCKS (4 INDICADORES) + const internationalData = { + // Dados dos índices (vindos da Brapi ou fallback) + sp500: indices.sp500, + nasdaq: indices.nasdaq, + dow: indices.dow, + + // 🎯 DADOS ESPECÃFICOS PARA EXTERIOR STOCKS (4 CARDS) + carteira: { + value: "+62,66%", + trend: "up" as const, + diff: 62.66, + timestamp: new Date().toISOString() + }, + + sp500Periodo: { + value: "+36,93%", + trend: "up" as const, + diff: 36.93, + timestamp: new Date().toISOString() + }, + + sp500Hoje: { + value: indices.sp500 ? formatPercent(indices.sp500.diff) : "-0,67%", + trend: indices.sp500 ? indices.sp500.trend : "down" as const, + diff: indices.sp500 ? indices.sp500.diff : -0.67, + timestamp: new Date().toISOString() + }, + + nasdaqHoje: { + value: indices.nasdaq ? formatPercent(indices.nasdaq.diff) : "-1,00%", + trend: indices.nasdaq ? indices.nasdaq.trend : "down" as const, + diff: indices.nasdaq ? indices.nasdaq.diff : -1.00, + timestamp: new Date().toISOString() + }, + + // Dados extras se necessário + marketStatus: { + isOpen: isMarketOpen(), + timezone: 'America/New_York', + lastUpdate: new Date().toISOString(), + source: brapiData ? 'brapi' : 'fallback' + } + }; + + if (isDev) { + console.log('✅ Dados internacionais compilados via', brapiData ? 'Brapi' : 'fallback'); + } + + const processingTime = Date.now() - startTime; + + return { + success: true, + internationalData: internationalData, + metadata: { + timestamp: new Date().toISOString(), + processingTime, + source: brapiData ? 'brapi' : 'fallback', + lastUpdate: new Date().toLocaleString('pt-BR', { + timeZone: 'America/Sao_Paulo' + }), + version: '3.0-brapi-integration', + dataProvider: 'Brapi.dev' + } + }; + }, + }); + + return NextResponse.json(data, { + status: 200, + headers: { + ...CACHE_HEADERS, + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization' + } + }); + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Erro desconhecido'; + console.error('⌠International Data API Error:', errorMessage); + + // Retornar dados de fallback em caso de erro + const fallbackData = { + carteira: { value: "+62,66%", trend: "up" as const, diff: 62.66 }, + sp500Periodo: { value: "+36,93%", trend: "up" as const, diff: 36.93 }, + sp500Hoje: { value: "-0,67%", trend: "down" as const, diff: -0.67 }, + nasdaqHoje: { value: "-1,00%", trend: "down" as const, diff: -1.00 }, + }; + + return NextResponse.json({ + success: false, + internationalData: fallbackData, + error: errorMessage, + metadata: { + timestamp: new Date().toISOString(), + fallback: true, + error: errorMessage, + source: 'fallback' + } + }, { + status: 200, // Retornar 200 mesmo com erro para não quebrar o frontend + headers: { + 'Cache-Control': 'no-store', + 'Access-Control-Allow-Origin': '*' + } + }); + } +} + +// POST method para refresh de dados se necessário +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + if (isDev) { + console.log('🌎 POST International Data API - Force refresh solicitado'); + } + + // Forçar refresh dos dados + const brapiData = await fetchBrapiData(); + + return NextResponse.json({ + success: true, + message: 'Dados atualizados via Brapi', + timestamp: new Date().toISOString(), + source: brapiData ? 'brapi' : 'fallback' + }); + + } catch (error) { + console.error('⌠POST International Data API Error:', error); + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Erro interno do servidor', + timestamp: new Date().toISOString() + }, { + status: 500 + }); + } +} + +// OPTIONS method para CORS +export async function OPTIONS(request: NextRequest) { + return new NextResponse(null, { + status: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); +} diff --git a/src/app/api/financial/market-data/route.ts b/src/app/api/financial/market-data/route.ts new file mode 100644 index 000000000..c88553ff4 --- /dev/null +++ b/src/app/api/financial/market-data/route.ts @@ -0,0 +1,163 @@ +// src/app/api/financial/market-data/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { getMarketSnapshot, type CombinedQuote } from '@/server/market-data'; +import { buildCacheHeaders } from '@/server/cache-headers'; +import { logApiMetric } from '@/lib/metrics'; +import { CacheService } from '@/lib/cache-service'; + +export const dynamic = 'force-dynamic'; +export const revalidate = 300; +export const fetchCache = 'default-cache'; +export const runtime = 'nodejs'; +export const maxDuration = 5; + +const MARKET_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes +const RESPONSE_CACHE_HEADERS = buildCacheHeaders({ ttl: 300, swr: 600 }); + +const FALLBACK_MARKET_DATA = { + ibovespa: { value: '136.431', trend: 'down' as const, diff: -0.26 }, + indiceSmall: { value: '2.237,86', trend: 'up' as const, diff: 1.56 }, + carteiraHoje: { value: '88.7%', trend: 'up' as const, diff: 88.7 }, + dividendYield: { value: '7.4%', trend: 'up' as const, diff: 7.4 }, + ibovespaPeriodo: { value: '6.1%', trend: 'up' as const, diff: 6.1 }, + carteiraPeriodo: { value: '9.3%', trend: 'up' as const, diff: 9.3 } +}; + +type MarketMetric = { + value: string; + trend: 'up' | 'down'; + diff: number; +}; + +function formatMarketValue(value: number): string { + if (value >= 1000 && value < 10000) { + return new Intl.NumberFormat('pt-BR', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }).format(value); + } + + if (value >= 10000) { + return new Intl.NumberFormat('pt-BR', { + minimumFractionDigits: 0, + maximumFractionDigits: 0 + }).format(value); + } + + return value.toFixed(2); +} + +function buildMetric(quote: CombinedQuote | undefined, fallback: MarketMetric): MarketMetric { + if (!quote || quote.price == null) { + return fallback; + } + + const diff = typeof quote.changePercent === 'number' ? Number(quote.changePercent.toFixed(2)) : fallback.diff; + const trend = quote.trend ?? (diff >= 0 ? 'up' : 'down'); + + return { + value: formatMarketValue(quote.price), + trend, + diff + }; +} + +export async function GET(_request: NextRequest) { + const startedAt = Date.now(); + + try { + const { data, metadata } = await CacheService.withCache( + CacheService.getCacheKey('market', 'data', 'all'), + async () => { + const snapshot = await getMarketSnapshot( + [ + { symbol: 'IBOV', key: 'IBOV' }, + { symbol: 'SMLL', key: 'SMLL' } + ], + { cacheTtlMs: MARKET_CACHE_TTL_MS } + ); + + const ibovespaMetric = buildMetric(snapshot.quotes.IBOV, FALLBACK_MARKET_DATA.ibovespa); + const smllMetric = buildMetric(snapshot.quotes.SMLL, FALLBACK_MARKET_DATA.indiceSmall); + + const processingTime = Date.now() - startedAt; + + return { + body: { + marketData: { + ibovespa: ibovespaMetric, + indiceSmall: smllMetric, + carteiraHoje: FALLBACK_MARKET_DATA.carteiraHoje, + dividendYield: FALLBACK_MARKET_DATA.dividendYield, + ibovespaPeriodo: FALLBACK_MARKET_DATA.ibovespaPeriodo, + carteiraPeriodo: FALLBACK_MARKET_DATA.carteiraPeriodo + }, + metadata: { + timestamp: new Date().toISOString(), + processingTime, + cache: { + fromCache: snapshot.fromCache, + cachedAt: snapshot.cachedAt, + cacheTtlMs: snapshot.cacheTtlMs, + expiresAt: snapshot.cachedAt + snapshot.cacheTtlMs + }, + sources: { + ibovespa: snapshot.quotes.IBOV?.sources ?? {}, + indiceSmall: snapshot.quotes.SMLL?.sources ?? {} + }, + requested: snapshot.metadata.requested, + upstream: snapshot.metadata, + lastUpdate: new Date().toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' }), + version: '7.0-server-fetcher' + } + } + }; + }, + { + ttlSeconds: 300, + tags: ['market', 'market-data'], + } + ); + + const response = NextResponse.json(data.body, { + status: 200, + headers: { + ...RESPONSE_CACHE_HEADERS, + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type' + } + }); + + logApiMetric({ + route: '/api/financial/market-data', + cacheHit: metadata.cached, + cacheBackend: metadata.backend, + durationMs: Date.now() - startedAt, + status: 200, + }); + + return response; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Erro desconhecido'; + + return NextResponse.json( + { + error: `Falha ao buscar dados: ${errorMessage}`, + marketData: FALLBACK_MARKET_DATA, + metadata: { + timestamp: new Date().toISOString(), + fallback: true, + error: errorMessage + } + }, + { + status: 200, + headers: { + 'Cache-Control': 'no-store', + 'Access-Control-Allow-Origin': '*' + } + } + ); + } +} diff --git a/src/app/api/financial/quotes/route.ts b/src/app/api/financial/quotes/route.ts new file mode 100644 index 000000000..a62871749 --- /dev/null +++ b/src/app/api/financial/quotes/route.ts @@ -0,0 +1,143 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { BrapiService } from '@/lib/brapi-service'; +import { logApiMetric } from '@/lib/metrics'; + +// ðŸ›¡ï¸ CONFIGURAÇÃO PARA VERCEL - EVITA ERRO DE STATIC GENERATION +export const dynamic = 'force-dynamic'; +export const revalidate = 300; // ✅ REDUZIDO: 5 minutos para dados mais frescos +export const fetchCache = 'default-cache'; +export const runtime = 'nodejs'; +export const maxDuration = 5; + +// ✅ HEADERS OTIMIZADOS PARA DADOS FINANCEIROS +// - Sem stale-while-revalidate (evita servir dados vencidos) +// - must-revalidate força checagem após expiração +// - 300 segundos = equilíbrio entre frescor e performance +const CACHE_HEADERS = { + 'Cache-Control': 'public, s-maxage=300, must-revalidate', + 'CDN-Cache-Control': 'public, s-maxage=300, must-revalidate', + 'Vercel-CDN-Cache-Control': 'public, s-maxage=300, must-revalidate', +} as const; + +export async function GET(request: NextRequest) { + const startedAt = Date.now(); + + try { + // ✅ USAR request.nextUrl EM VEZ DE new URL(request.url) + const { searchParams } = request.nextUrl; + const symbols = searchParams.get('symbols'); + + if (!symbols) { + return NextResponse.json( + { error: 'Parâmetro symbols é obrigatório' }, + { status: 400 } + ); + } + + const symbolsArray = symbols.split(',').map(s => s.trim().toUpperCase()); + + const queryParams: Record = {}; + searchParams.forEach((value, key) => { + if (key === 'symbols') return; + if (value) { + queryParams[key] = value; + } + }); + + // ✅ TTL REDUZIDO: 300 segundos (alinhado com revalidate e cache headers) + const { data, metadata } = await BrapiService.fetchQuotes(symbolsArray, { + cacheTtlMs: 300 * 1000, // 5 minutos + queryParams, + }); + + logApiMetric({ + route: '/api/financial/quotes', + cacheHit: metadata.cached, + cacheBackend: metadata.backend, + durationMs: Date.now() - startedAt, + status: 200, + extra: { symbolsCount: symbolsArray.length }, + }); + + return NextResponse.json( + { + quotes: data, + results: data, + timestamp: new Date().toISOString(), + symbols: symbolsArray, + success: true + }, + { + status: 200, + headers: { + ...CACHE_HEADERS, + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type' + }, + } + ); + + } catch (error) { + console.error('⌠Quotes API Error:', error); + + const errorMessage = error instanceof Error ? error.message : 'Erro desconhecido'; + + return NextResponse.json( + { + error: 'Erro interno do servidor', + quotes: [], + timestamp: new Date().toISOString(), + success: false, + details: errorMessage + }, + { + status: 500, + headers: { + 'Cache-Control': 'no-store', + 'Access-Control-Allow-Origin': '*' + } + } + ); + } +} + +// POST method para atualizar quotes se necessário +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + console.log('📊 POST Quotes API - Dados recebidos:', body); + + // Implementar lógica de POST se necessário + // Por exemplo: cache refresh, webhook processing, etc. + + return NextResponse.json({ + success: true, + message: 'Quotes atualizadas com sucesso', + timestamp: new Date().toISOString() + }); + + } catch (error) { + console.error('⌠POST Quotes API Error:', error); + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Erro interno do servidor', + timestamp: new Date().toISOString() + }, { + status: 500 + }); + } +} + +// OPTIONS method para CORS +export async function OPTIONS(request: NextRequest) { + return new NextResponse(null, { + status: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); +} diff --git a/src/app/api/financial/route.ts b/src/app/api/financial/route.ts new file mode 100644 index 000000000..af8b52625 --- /dev/null +++ b/src/app/api/financial/route.ts @@ -0,0 +1,80 @@ +import { NextResponse } from 'next/server'; + +export const dynamic = 'force-dynamic'; +export const revalidate = 0; +export const runtime = 'nodejs'; + +type EndpointInfo = { + path: string; + description: string; + method?: 'GET' | 'POST' | 'OPTIONS'; +}; + +const ENDPOINTS: EndpointInfo[] = [ + { + path: '/api/financial/market-data', + description: 'Resumo de mercado com Ibovespa, Small Caps e métricas agregadas.', + method: 'GET', + }, + { + path: '/api/financial/fii-market-data', + description: 'Dados agregados dos FIIs incluindo IFIX e desempenho simulado da carteira.', + method: 'GET', + }, + { + path: '/api/financial/quotes', + description: 'Cotação em lote via parâmetro "symbols" com tickers separados por vírgula.', + method: 'GET', + }, + { + path: '/api/b3/market-data', + description: 'Alias compatível para os dados consolidados da B3 (Ibovespa e Small Caps).', + method: 'GET', + }, + { + path: '/api/b3/investments', + description: 'Alias compatível que descreve as carteiras simuladas utilizadas na área do assinante.', + method: 'GET', + }, + { + path: '/api/dividend-yield/batch', + description: 'Calcula DY em lote para múltiplos tickers (POST com { tickers: string[] }).', + method: 'POST', + }, +]; + +export async function GET() { + const timestamp = new Date().toISOString(); + const body = { + success: true, + message: 'API financeira ativa. Utilize os endpoints listados abaixo para obter dados detalhados.', + timestamp, + endpoints: ENDPOINTS, + rotas: { + lista: ENDPOINTS.map(endpoint => endpoint.path), + metodos: ENDPOINTS.reduce>((acc, endpoint) => { + acc[endpoint.path] = endpoint.method ?? 'GET'; + return acc; + }, {}), + mapa: ENDPOINTS.reduce>((acc, endpoint) => { + acc[endpoint.path] = endpoint.description; + return acc; + }, {}), + // Compatibilidade com clientes que verificam a existência de rotas. + exists: null as null, + existe: ENDPOINTS.reduce>((acc, endpoint) => { + acc[endpoint.path] = true; + return acc; + }, {}), + }, + }; + + return NextResponse.json(body, { + status: 200, + headers: { + 'Cache-Control': 'no-store, max-age=0', + 'CDN-Cache-Control': 'no-store', + 'Vercel-CDN-Cache-Control': 'no-store', + }, + }); +} diff --git a/src/app/api/finnhub-metrics/[ticker]/route.ts b/src/app/api/finnhub-metrics/[ticker]/route.ts new file mode 100644 index 000000000..aec57aa2c --- /dev/null +++ b/src/app/api/finnhub-metrics/[ticker]/route.ts @@ -0,0 +1,61 @@ +import { NextResponse } from 'next/server'; +import { CacheService } from '@/lib/redis'; + +const FINNHUB_API_KEY = 'd4b42s9r01qrv4askhmgd4b42s9r01qrv4askhn0'; +const CACHE_HEADERS = { + 'Cache-Control': 'public, s-maxage=3600, must-revalidate', + 'CDN-Cache-Control': 'public, s-maxage=3600, must-revalidate' +} as const; + +export async function GET( + request: Request, + { params }: { params: { ticker: string } } +) { + const ticker = params.ticker; + + try { + const cacheKey = CacheService.getCacheKey('finnhub', 'metrics', ticker); + const { data } = await CacheService.withCache( + cacheKey, + async () => { + const response = await fetch( + `https://finnhub.io/api/v1/stock/metric?symbol=${ticker}&metric=all&token=${FINNHUB_API_KEY}`, + { + headers: { + Accept: 'application/json' + } + } + ); + + if (!response.ok) { + throw new Error(`Finnhub API error: ${response.status}`); + } + + const payload = await response.json(); + const metrics = payload.metric || {}; + + return { + ticker, + dividendYield: + metrics.currentDividendYieldTTM || + metrics.dividendYieldIndicatedAnnual || + 0, + dividendPerShare: + metrics.dividendPerShareTTM || metrics.dividendPerShareAnnual || 0, + dividendGrowthRate5Y: metrics.dividendGrowthRate5Y || 0, + payoutRatio: metrics.payoutRatioTTM || metrics.payoutRatioAnnual || 0, + rawMetrics: metrics + }; + }, + { ttlSeconds: 3600, tags: ['finnhub'] } + ); + + return NextResponse.json(data, { headers: CACHE_HEADERS }); + } catch (error) { + console.error(`Error fetching Finnhub data for ${ticker}:`, error); + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/generate-pdf/route.ts b/src/app/api/generate-pdf/route.ts new file mode 100644 index 000000000..91d3f2de5 --- /dev/null +++ b/src/app/api/generate-pdf/route.ts @@ -0,0 +1,85 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; +import puppeteer, { Browser } from 'puppeteer'; +import { auth } from '@/lib/auth'; +import { CacheService } from '@/lib/redis'; +import { enforceRateLimit } from '@/lib/rate-limit'; + +const s3 = new S3Client({ region: process.env.AWS_REGION }); + +const PDF_RATE_LIMIT = { + windowSeconds: 60, + maxRequests: 5, +}; + +let browserPromise: Promise | null = null; + +async function getBrowser() { + if (!browserPromise) { + browserPromise = puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] }); + } + return browserPromise; +} + +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const rateKey = CacheService.getCacheKey('pdf', session.user.id); + const rateResult = await enforceRateLimit( + rateKey, + PDF_RATE_LIMIT.maxRequests, + PDF_RATE_LIMIT.windowSeconds * 1000 + ); + + if (!rateResult.allowed) { + return NextResponse.json( + { error: 'Too many requests, try again later.' }, + { + status: 429, + headers: rateResult.retryAfter ? { 'Retry-After': `${rateResult.retryAfter}` } : undefined, + } + ); + } + + const { html, fileName } = await request.json(); + + if (!html) { + return NextResponse.json({ error: 'html is required' }, { status: 400 }); + } + + const browser = await getBrowser(); + const page = await browser.newPage(); + + const timeoutController = new AbortController(); + const timeout = setTimeout(() => timeoutController.abort(), 30_000); + + try { + await page.setContent(html, { waitUntil: 'networkidle0', signal: timeoutController.signal }); + const pdfBuffer = await page.pdf({ format: 'A4' }); + + const key = fileName || `${Date.now()}.pdf`; + + await s3.send( + new PutObjectCommand({ + Bucket: process.env.PDF_BUCKET, + Key: key, + Body: pdfBuffer, + ContentType: 'application/pdf', + }) + ); + + return NextResponse.json({ key }, { status: 200 }); + } finally { + clearTimeout(timeout); + await page.close(); + } + } catch (error) { + console.error('Error generating PDF:', error); + return NextResponse.json({ error: 'Failed to generate PDF' }, { status: 500 }); + } +} diff --git a/src/app/api/hotmart/webhook/route.ts b/src/app/api/hotmart/webhook/route.ts new file mode 100644 index 000000000..d0afd5312 --- /dev/null +++ b/src/app/api/hotmart/webhook/route.ts @@ -0,0 +1,341 @@ +export const dynamic = 'force-dynamic'; + +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { isLifetimePlan } from '@/lib/lifetime-plans'; +import { getHotmartBuyerPhone } from '@/lib/phone'; +import { hashPassword } from '@/lib/auth/password'; + +// Função para gerar senha segura +function generateSecurePassword(): string { + const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789'; + const symbols = '!@#$%&*'; + let password = ''; + + for (let i = 0; i < 6; i++) { + password += chars.charAt(Math.floor(Math.random() * chars.length)); + } + for (let i = 0; i < 2; i++) { + password += symbols.charAt(Math.floor(Math.random() * symbols.length)); + } + + return password.split('').sort(() => Math.random() - 0.5).join(''); +} + +// Calcular data de expiração +function calculateExpirationDate(plan: string): Date | null { + if (isLifetimePlan(plan)) { + console.log(`🎉 Plano ${plan}: acesso vitalício (sem data de expiração).`); + return null; + } + + const expirationMap: Record = { + 'VIP': 365, + 'LITE': 365, + 'LITE_V2': 365, + 'AMERICA': 365, + 'FIIS': 365, + 'RENDA_PASSIVA': 365 + }; + + const days = expirationMap[plan as keyof typeof expirationMap] ?? 365; + + const expirationDate = new Date(); + expirationDate.setDate(expirationDate.getDate() + days); + return expirationDate; +} + +export async function POST(request: NextRequest) { + try { + console.log('🔔 Webhook Hotmart recebido'); + + let webhookData; + try { + webhookData = await request.json(); + } catch (jsonError) { + console.error('⌠Erro ao parsear JSON:', jsonError); + return NextResponse.json( + { error: 'JSON inválido' }, + { status: 400 } + ); + } + + console.log('📦 Dados completos recebidos:', JSON.stringify(webhookData, null, 2)); + + // Extrair dados de forma flexível - tentar diferentes estruturas + let event, productData, buyerData, purchaseData; + + // Estrutura 1: webhook.event, webhook.data + if (webhookData.event && webhookData.data) { + event = webhookData.event; + productData = webhookData.data.product || webhookData.data; + buyerData = webhookData.data.buyer || webhookData.data; + purchaseData = webhookData.data.purchase || webhookData.data; + } + // Estrutura 2: dados diretos + else { + event = webhookData.event || 'PURCHASE_APPROVED'; + productData = webhookData.product || webhookData; + buyerData = webhookData.buyer || webhookData; + purchaseData = webhookData.purchase || webhookData; + } + + console.log('📋 Dados extraídos:', { event, productData, buyerData, purchaseData }); + + // Extrair informações essenciais com fallbacks + const productId = productData?.ucode || productData?.id || productData?.product_id || 'unknown'; + const productName = productData?.name || productData?.product_name || 'Produto Hotmart'; + + const buyerEmail = buyerData?.email || webhookData.email || null; + const buyerName = buyerData?.name || buyerData?.full_name || webhookData.name || 'Cliente Hotmart'; + const buyerPhone = getHotmartBuyerPhone(webhookData, buyerData); + + const transactionId = purchaseData?.transaction || purchaseData?.transaction_id || + webhookData.transaction || `TXN_${Date.now()}`; + const amount = purchaseData?.price?.value || purchaseData?.amount || + webhookData.price || 0; + + console.log('🔠Informações finais:', { + event, productId, productName, buyerEmail, buyerName, transactionId, amount + }); + + // Validação mínima + if (!buyerEmail || !buyerEmail.includes('@')) { + console.log('⌠Email inválido ou ausente:', buyerEmail); + return NextResponse.json({ + error: 'Email do comprador é obrigatório e deve ser válido', + received_email: buyerEmail + }, { status: 400 }); + } + + // Mapear produto para plano - ACEITA QUALQUER PRODUTO + const PRODUCT_PLAN_MAPPING: Record = { + 'fb056612-bcc6-4217-9e6d-2a5d1110ac2f': 'VIP', + 'vip-plan': 'VIP', + 'lite-plan': 'LITE', + 'lite-2-plan': 'LITE_V2', + 'renda-passiva': 'RENDA_PASSIVA', + 'fiis': 'FIIS', + 'america': 'AMERICA' + }; + + // SEMPRE usar VIP como padrão - aceita qualquer produto + const plan = PRODUCT_PLAN_MAPPING[productId] || 'VIP'; + console.log(`✅ Produto "${productId}" → Plano ${plan} ${PRODUCT_PLAN_MAPPING[productId] ? '(mapeado)' : '(padrão VIP)'}`); + + // Conectar ao banco + await prisma.$connect(); + + // Verificar se usuário já existe + const email = buyerEmail.toLowerCase().trim(); + let user = await prisma.user.findUnique({ + where: { email } + }); + + let isNewUser = false; + let tempPassword = ''; + + if (user) { + // ATUALIZAR usuário existente + console.log(`🔄 Atualizando usuário existente: ${email}`); + + const phonePayload = buyerPhone && !user.phone ? { phone: buyerPhone } : {}; + + user = await prisma.user.update({ + where: { email }, + data: { + plan: plan, + status: 'ACTIVE', + hotmartCustomerId: transactionId, + expirationDate: calculateExpirationDate(plan), + ...phonePayload, + // Manter senha existente se houver + ...(user.password ? {} : { + password: generateSecurePassword(), + passwordCreatedAt: new Date(), + mustChangePassword: true + }) + } + }); + + console.log(`✅ Usuário atualizado: ${email} → ${plan}`); + + } else { + // CRIAR novo usuário + isNewUser = true; + tempPassword = generateSecurePassword(); + const hashedPassword = await hashPassword(tempPassword); + + console.log(`âž• Criando novo usuário: ${email}`); + + const nameParts = buyerName.split(' '); + const firstName = nameParts[0] || 'Cliente'; + const lastName = nameParts.slice(1).join(' ') || 'Hotmart'; + + user = await prisma.user.create({ + data: { + email: email, + firstName: firstName, + lastName: lastName, + phone: buyerPhone ?? null, + plan: plan, + status: 'ACTIVE', + hotmartCustomerId: transactionId, + expirationDate: calculateExpirationDate(plan), + password: hashedPassword, + passwordCreatedAt: new Date(), + mustChangePassword: true, + customPermissions: '[]' + } + }); + + console.log(`✅ Novo usuário criado: ${email} → ${plan}`); + } + + // ✅ REGISTRAR COMPRA COM PROTEÇÃO CONTRA DUPLICATAS + try { + let purchaseHandled = false; + + // ✅ ESTRATÉGIA 1: Verificar por transactionId (duplicata exata) + if (transactionId) { + const existingPurchaseByTxn = await prisma.purchase.findFirst({ + where: { + hotmartTransactionId: transactionId, + }, + orderBy: { + createdAt: 'desc', + }, + }); + + if (existingPurchaseByTxn) { + // ✅ Mesma transação = atualizar + await prisma.purchase.update({ + where: { id: existingPurchaseByTxn.id }, + data: { + userId: user.id, + amount: amount || 0, + productName: productName, + status: 'COMPLETED', + }, + }); + + console.log(`â™»ï¸ Compra Hotmart atualizada (mesma transação): ${amount} - ${productName} (TXN: ${transactionId})`); + purchaseHandled = true; + } + } + + // ✅ ESTRATÉGIA 2: Verificar mesmo produto em período próximo (7 dias) + if (!purchaseHandled) { + const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); + + const recentSameProduct = await prisma.purchase.findFirst({ + where: { + userId: user.id, + productName: productName, + createdAt: { + gte: sevenDaysAgo // Últimos 7 dias + }, + status: 'COMPLETED' + }, + orderBy: { + createdAt: 'desc', + }, + }); + + if (recentSameProduct) { + // ✅ Mesmo produto em menos de 7 dias = atualizar (duplicata) + await prisma.purchase.update({ + where: { id: recentSameProduct.id }, + data: { + amount: amount || 0, + hotmartTransactionId: transactionId, + status: 'COMPLETED', + }, + }); + + console.log(`â™»ï¸ Compra Hotmart atualizada (mesmo produto em 7 dias): ${amount} - ${productName}`); + purchaseHandled = true; + } + } + + // ✅ CRIAR NOVA COMPRA se: + // - Não existe transação duplicada E + // - Não existe mesmo produto nos últimos 7 dias + // Isso permite: renovações anuais, produtos diferentes, primeira compra + if (!purchaseHandled) { + await prisma.purchase.create({ + data: { + userId: user.id, + amount: amount || 0, + productName: productName, + hotmartTransactionId: transactionId, + status: 'COMPLETED' + } + }); + console.log(`💰 Nova compra Hotmart registrada: ${amount} - ${productName} (TXN: ${transactionId})`); + } + } catch (purchaseError) { + console.error('âš ï¸ Erro ao registrar compra (não crítico):', purchaseError); + } + + await prisma.$disconnect(); + + const response = { + success: true, + message: 'Webhook processado com sucesso', + user: { + id: user.id, + email: user.email, + plan: user.plan, + status: user.status, + isNewUser: isNewUser, + tempPassword: isNewUser ? tempPassword : undefined + }, + transaction: { + id: transactionId, + amount: amount, + product: productName + }, + timestamp: new Date().toISOString() + }; + + console.log('🎉 Webhook Hotmart processado com sucesso:', response); + + return NextResponse.json(response); + + } catch (error: any) { + console.error('⌠Erro no webhook Hotmart:', error); + + try { + await prisma.$disconnect(); + } catch (disconnectError) { + console.error('⌠Erro ao desconectar:', disconnectError); + } + + return NextResponse.json( + { + error: 'Erro interno do servidor', + message: error.message, + timestamp: new Date().toISOString() + }, + { status: 500 } + ); + } +} + +export async function GET() { + return NextResponse.json({ + message: 'Webhook Hotmart ativo e funcionando', + timestamp: new Date().toISOString(), + status: 'ready', + endpoints: { + POST: 'Receber webhooks da Hotmart', + GET: 'Status da API' + }, + features: { + duplicateProtection: true, + passwordHashing: true, + lifetimePlans: true + } + }); +} diff --git a/src/app/api/ifix/route.ts b/src/app/api/ifix/route.ts new file mode 100644 index 000000000..d9fc05be4 --- /dev/null +++ b/src/app/api/ifix/route.ts @@ -0,0 +1,53 @@ +export async function GET() { + try { + const agora = new Date(); + const hora = agora.getHours(); + const minuto = agora.getMinutes(); + + // Simulação realista + const isHorarioComercial = hora >= 9 && hora <= 18; + const valorBase = 3440; + const variacao = (Math.random() - 0.5) * (isHorarioComercial ? 3 : 1); + const novoValor = valorBase * (1 + variacao / 100); + + const dadosIfix = { + valor: novoValor, + valorFormatado: novoValor.toLocaleString('pt-BR', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }), + variacao: valorBase * (variacao / 100), + variacaoPercent: variacao, + trend: variacao >= 0 ? 'up' : 'down', + timestamp: agora.toISOString(), + fonte: 'SIMULAÇÃO_VERCEL', + nota: `Atualizado ${hora.toString().padStart(2, '0')}:${minuto.toString().padStart(2, '0')}`, + symbol: 'IFIX' + }; + + return new Response(JSON.stringify({ + ifix: dadosIfix, + success: true, + timestamp: agora.toISOString() + }), { + status: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-cache' + } + }); + + } catch (error) { + return new Response(JSON.stringify({ + error: 'Erro interno', + message: error instanceof Error ? error.message : 'Erro desconhecido', + timestamp: new Date().toISOString() + }), { + status: 500, + headers: { + 'Content-Type': 'application/json' + } + }); + } +} diff --git a/src/app/api/instagram-cadastro/route.ts b/src/app/api/instagram-cadastro/route.ts new file mode 100644 index 000000000..7128661ab --- /dev/null +++ b/src/app/api/instagram-cadastro/route.ts @@ -0,0 +1,218 @@ +// src/app/api/instagram-cadastro/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; +import { CacheService } from '@/lib/redis'; + +const RATE_LIMIT_WINDOW_SECONDS = 60; +const RATE_LIMIT_MAX = 20; + +async function requireUser(req: NextRequest): Promise { + const authData = await auth(); + if (authData?.user?.id) { + return authData.user.id; + } + + const tokenHeader = req.headers.get('authorization'); + if (tokenHeader?.startsWith('Bearer ')) { + return null; + } + + return null; +} + +async function enforceRateLimit(userId: string) { + const key = CacheService.getCacheKey('instagram-cadastro', userId); + const current = (await CacheService.get(key)) ?? 0; + if (current >= RATE_LIMIT_MAX) { + return false; + } + await CacheService.set(key, current + 1, RATE_LIMIT_WINDOW_SECONDS); + return true; +} + +export async function POST(req: NextRequest) { + try { + console.log('📠POST - Cadastrando Instagram...'); + + const userId = await requireUser(req); + + if (!userId) { + return NextResponse.json({ error: 'Usuário não autenticado' }, { status: 401 }); + } + + const allowed = await enforceRateLimit(userId); + if (!allowed) { + return NextResponse.json({ error: 'Muitas requisições, tente novamente em instantes.' }, { status: 429 }); + } + const { instagram } = await req.json(); + + if (!instagram || instagram.trim().length < 3) { + return NextResponse.json( + { error: 'Instagram inválido' }, + { status: 400 } + ); + } + const cleanInstagram = instagram.replace('@', '').trim(); + + // Verificar se já existe cadastro + const existingCadastro = await prisma.instagramCadastro.findUnique({ + where: { userId } + }); + + // Criar ou atualizar o cadastro do Instagram + const instagramCadastro = await prisma.instagramCadastro.upsert({ + where: { userId }, + update: { + instagram: cleanInstagram, + previousInstagram: existingCadastro?.instagram || null, // Salvar o anterior + isUpdated: existingCadastro ? true : false, // Marcar como atualizado se já existia + updatedAt: new Date() + }, + create: { + userId, + instagram: cleanInstagram, + previousInstagram: null, // Primeiro cadastro não tem anterior + isUpdated: false // Primeiro cadastro não é atualização + } + }); + + console.log('✅ Instagram cadastrado:', cleanInstagram); + console.log('📊 Status:', existingCadastro ? 'ATUALIZADO' : 'NOVO'); + + return NextResponse.json({ + success: true, + message: 'Instagram cadastrado com sucesso', + data: { + instagram: instagramCadastro.instagram, + isUpdated: instagramCadastro.isUpdated, + previousInstagram: instagramCadastro.previousInstagram, + createdAt: instagramCadastro.createdAt + } + }); + } catch (error) { + console.error('💥 Erro ao cadastrar Instagram:', error); + return NextResponse.json( + { error: 'Erro interno do servidor' }, + { status: 500 } + ); + } +} + +export async function PUT(req: NextRequest) { + try { + console.log('🔄 PUT - Atualizando Instagram...'); + + const userId = await requireUser(req); + + if (!userId) { + console.log('⌠Usuário não autenticado'); + return NextResponse.json( + { error: 'Usuário não autenticado' }, + { status: 401 } + ); + } + + const { instagram } = await req.json(); + + if (!instagram || instagram.trim().length < 3) { + console.log('⌠Instagram inválido:', instagram); + return NextResponse.json( + { error: 'Instagram deve ter pelo menos 3 caracteres' }, + { status: 400 } + ); + } + + const cleanInstagram = instagram.replace('@', '').trim(); + + // Buscar o cadastro atual para salvar como anterior + const currentCadastro = await prisma.instagramCadastro.findUnique({ + where: { userId } + }); + + if (!currentCadastro) { + // Se não existe, criar um novo + const newCadastro = await prisma.instagramCadastro.create({ + data: { + userId, + instagram: cleanInstagram, + previousInstagram: null, + isUpdated: false + } + }); + + console.log('✅ Instagram criado (não existia):', cleanInstagram); + + return NextResponse.json({ + success: true, + message: 'Instagram cadastrado com sucesso', + data: { + instagram: newCadastro.instagram, + isUpdated: false, + previousInstagram: null, + updatedAt: newCadastro.updatedAt + } + }); + } + + // Atualizar o cadastro salvando o anterior + const instagramCadastro = await prisma.instagramCadastro.update({ + where: { userId }, + data: { + instagram: cleanInstagram, + previousInstagram: currentCadastro.instagram, // Salvar o atual como anterior + isUpdated: true, // Marcar como atualizado + updatedAt: new Date() + } + }); + + console.log('✅ Instagram atualizado:', cleanInstagram); + console.log('📠Instagram anterior:', currentCadastro.instagram); + + return NextResponse.json({ + success: true, + message: 'Instagram atualizado com sucesso', + data: { + instagram: instagramCadastro.instagram, + previousInstagram: instagramCadastro.previousInstagram, + isUpdated: instagramCadastro.isUpdated, + updatedAt: instagramCadastro.updatedAt + } + }); + + } catch (error) { + console.error('💥 Erro ao atualizar Instagram:', error); + return NextResponse.json( + { error: 'Erro interno do servidor' }, + { status: 500 } + ); + } +} + +export async function GET(req: NextRequest) { + try { + const userId = await requireUser(req); + + if (!userId) { + return NextResponse.json( + { error: 'Usuário não autenticado' }, + { status: 401 } + ); + } + + const instagramCadastro = await prisma.instagramCadastro.findUnique({ + where: { userId } + }); + + return NextResponse.json({ + success: true, + data: instagramCadastro + }); + } catch (error) { + console.error('Erro ao buscar Instagram:', error); + return NextResponse.json( + { error: 'Erro interno do servidor' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/limpar-cache-temp/route.ts b/src/app/api/limpar-cache-temp/route.ts new file mode 100644 index 000000000..91e9b8a53 --- /dev/null +++ b/src/app/api/limpar-cache-temp/route.ts @@ -0,0 +1,41 @@ +import { NextResponse } from 'next/server'; + +export const dynamic = 'force-dynamic'; + +export async function GET() { + try { + // Tentar importar CacheService + const { CacheService } = await import('@/lib/redis'); + + // Invalidar tags + const tags = [ + 'proventos:list', + 'proventos:stats', + 'central-proventos', + 'proventos', + ]; + + for (const tag of tags) { + await CacheService.invalidateTags([tag]); + } + + return NextResponse.json({ + success: true, + message: 'Cache Redis invalidado!', + tags, + timestamp: new Date().toISOString() + }); + + } catch (error) { + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined + }, { status: 500 }); + } +} + +// Também aceitar POST +export async function POST() { + return GET(); +} diff --git a/src/app/api/market/etf/holdings/route.ts b/src/app/api/market/etf/holdings/route.ts new file mode 100644 index 000000000..01254a40b --- /dev/null +++ b/src/app/api/market/etf/holdings/route.ts @@ -0,0 +1,191 @@ +import { NextResponse } from 'next/server'; +import { buildCacheHeaders } from '@/server/cache-headers'; + +type Holding = { + symbol: string; + name: string; + weight: number; + sector: string; +}; + +type SectorBreakdown = { + sector: string; + weight: number; +}; + +type ETFData = { + name: string; + description: string; + totalHoldings: number; + topHoldings: Holding[]; + sectorBreakdown: SectorBreakdown[]; +}; + +const YAHOO_ENDPOINT = 'https://query2.finance.yahoo.com/v10/finance/quoteSummary/'; +const YAHOO_MODULES = 'topHoldings%2CsummaryProfile%2CquoteType'; + +const ETF_CACHE_HEADERS = buildCacheHeaders({ ttl: 300, swr: 1800 }); + +async function fetchFromYahoo(ticker: string): Promise { + try { + const response = await fetch( + `${YAHOO_ENDPOINT}${ticker}?modules=${YAHOO_MODULES}`, + { + headers: { + 'User-Agent': 'Mozilla/5.0 (compatible; MembrosFatosDaBolsaBot/1.0)', + 'Accept': 'application/json' + }, + cache: 'force-cache', + next: { revalidate: 300 } + } + ); + + if (!response.ok) { + return null; + } + + const json = await response.json(); + const result = json?.quoteSummary?.result?.[0]; + + const holdings = result?.topHoldings?.holdings; + if (!Array.isArray(holdings) || holdings.length === 0) { + return null; + } + + const name: string = result?.quoteType?.longName || `${ticker} ETF`; + const description: string = + result?.summaryProfile?.longBusinessSummary || + result?.assetProfile?.longBusinessSummary || + ''; + + const processedHoldings: Holding[] = holdings.slice(0, 10).map((holding: any) => ({ + symbol: holding?.symbol ?? '', + name: holding?.holdingName ?? holding?.symbol ?? 'Unknown', + weight: typeof holding?.holdingPercent === 'number' + ? holding.holdingPercent * 100 + : Number(holding?.holdingPercent) * 100 || 0, + sector: holding?.sector || holding?.holdingCategory || 'Unknown' + })); + + return { + name, + description, + totalHoldings: holdings.length, + topHoldings: processedHoldings, + sectorBreakdown: [] + }; + } catch (error) { + console.error(`[ETF Holdings] Yahoo request failed for ${ticker}:`, error); + return null; + } +} + +async function fetchFromFMP(ticker: string): Promise { + const apiKey = + process.env.FMP_API_KEY || + process.env.FINANCIAL_MODELING_PREP_KEY || + process.env.NEXT_PUBLIC_FMP_API_KEY || + 'demo'; + + const url = `https://financialmodelingprep.com/api/v3/etf-holder/${ticker}?apikey=${apiKey}`; + + try { + const response = await fetch(url, { + cache: 'force-cache', + next: { revalidate: 300 } + }); + if (!response.ok) { + return null; + } + + const data = await response.json(); + if (!Array.isArray(data) || data.length === 0) { + return null; + } + + const topHoldings: Holding[] = data.slice(0, 10).map((holding: any) => ({ + symbol: holding?.asset ?? '', + name: holding?.assetName || holding?.asset || 'Unknown', + weight: Number(holding?.weightPercentage) || 0, + sector: holding?.sector || 'Unknown' + })); + + const sectorWeights: Record = {}; + data.forEach((holding: any) => { + const sector = holding?.sector || 'Unknown'; + const weight = Number(holding?.weightPercentage) || 0; + sectorWeights[sector] = (sectorWeights[sector] || 0) + weight; + }); + + const sectorBreakdown: SectorBreakdown[] = Object.entries(sectorWeights) + .map(([sector, weight]) => ({ sector, weight })) + .sort((a, b) => b.weight - a.weight); + + return { + name: `${ticker} ETF`, + description: `Exchange Traded Fund ${ticker}`, + totalHoldings: data.length, + topHoldings, + sectorBreakdown + }; + } catch (error) { + console.error(`[ETF Holdings] FMP request failed for ${ticker}:`, error); + return null; + } +} + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const ticker = searchParams.get('ticker'); + + if (!ticker) { + return NextResponse.json( + { + success: false, + message: 'Ticker é obrigatório.' + }, + { status: 400 } + ); + } + + const normalizedTicker = ticker.trim().toUpperCase(); + + try { + const yahooData = await fetchFromYahoo(normalizedTicker); + if (yahooData) { + return NextResponse.json({ + success: true, + source: 'yahoo', + data: yahooData + }, { headers: ETF_CACHE_HEADERS }); + } + + const fmpData = await fetchFromFMP(normalizedTicker); + if (fmpData) { + return NextResponse.json({ + success: true, + source: 'fmp', + data: fmpData + }, { headers: ETF_CACHE_HEADERS }); + } + + return NextResponse.json( + { + success: false, + source: null, + data: null, + message: `Holdings não encontrados para ${normalizedTicker}.` + }, + { status: 404, headers: ETF_CACHE_HEADERS } + ); + } catch (error) { + console.error(`[ETF Holdings] Unexpected error for ${normalizedTicker}:`, error); + return NextResponse.json( + { + success: false, + message: error instanceof Error ? error.message : 'Erro desconhecido ao buscar holdings.' + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/market/exchange-rate/route.ts b/src/app/api/market/exchange-rate/route.ts new file mode 100644 index 000000000..e1fda94bd --- /dev/null +++ b/src/app/api/market/exchange-rate/route.ts @@ -0,0 +1,48 @@ +import { NextResponse } from 'next/server'; +import { CacheService } from '@/lib/redis'; + +const CACHE_HEADERS = { + 'Cache-Control': 'public, s-maxage=3600, must-revalidate', + 'CDN-Cache-Control': 'public, s-maxage=3600, must-revalidate' +} as const; + +export async function GET() { + try { + const cacheKey = CacheService.getCacheKey('exchange-rate', 'usd-brl'); + const { data } = await CacheService.withCache( + cacheKey, + async () => { + const response = await fetch('https://api.exchangerate-api.com/v4/latest/USD', { + headers: { + Accept: 'application/json' + }, + cache: 'no-store' + }); + + if (!response.ok) { + throw new Error(`Exchange rate API error: ${response.status}`); + } + + const payload = await response.json(); + const usdToBrl = payload?.rates?.BRL; + + if (typeof usdToBrl !== 'number') { + throw new Error('Taxa BRL não encontrada'); + } + + return { + usdToBrl + }; + }, + { ttlSeconds: 3600, tags: ['exchange-rate'] } + ); + + return NextResponse.json(data, { headers: CACHE_HEADERS }); + } catch (error) { + console.error('Erro ao buscar USD/BRL:', error); + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Erro desconhecido' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/market/hgbrasil/route.ts b/src/app/api/market/hgbrasil/route.ts new file mode 100644 index 000000000..22023e6d8 --- /dev/null +++ b/src/app/api/market/hgbrasil/route.ts @@ -0,0 +1,109 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { CacheService } from '@/lib/redis'; +import { logApiMetric } from '@/lib/metrics'; + +export const dynamic = 'force-dynamic'; +export const revalidate = 600; +export const runtime = 'nodejs'; +export const maxDuration = 5; + +const CACHE_TTL_SECONDS = 600; + +const CACHE_HEADERS = { + 'Cache-Control': 'public, s-maxage=600, must-revalidate', + 'CDN-Cache-Control': 'public, s-maxage=600, must-revalidate', + 'Vercel-CDN-Cache-Control': 'public, s-maxage=600, must-revalidate', +} as const; + +export async function GET(request: NextRequest) { + const startedAt = Date.now(); + const { searchParams } = request.nextUrl; + const symbol = searchParams.get('symbol')?.trim().toUpperCase(); + + if (!symbol) { + return NextResponse.json({ error: 'Parâmetro symbol é obrigatório' }, { status: 400 }); + } + + const apiKey = + process.env.HGBRASIL_API_KEY?.trim() || + process.env.NEXT_PUBLIC_HGBRASIL_API_KEY?.trim() || + 'a666e15c'; + + const cacheKey = CacheService.getCacheKey('hgbrasil', 'stock_price', symbol); + + try { + const { data, metadata } = await CacheService.withCache( + cacheKey, + async () => { + const url = new URL('https://api.hgbrasil.com/finance/stock_price'); + url.searchParams.set('key', apiKey); + url.searchParams.set('symbol', symbol); + + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + Accept: 'application/json', + 'User-Agent': 'MembrosFatosBolsa/1.0', + }, + cache: 'no-store', + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`HG Brasil HTTP ${response.status}: ${errorText}`); + } + + const payload = await response.json(); + const result = payload?.results?.[symbol] ?? null; + + return { + result, + raw: payload, + }; + }, + { ttlSeconds: CACHE_TTL_SECONDS } + ); + + logApiMetric({ + route: '/api/market/hgbrasil', + cacheHit: metadata.cached, + cacheBackend: metadata.backend, + durationMs: Date.now() - startedAt, + status: 200, + extra: { symbol }, + }); + + return NextResponse.json( + { + success: true, + symbol, + result: data.result, + cached: metadata.cached, + backend: metadata.backend, + timestamp: new Date().toISOString(), + }, + { status: 200, headers: CACHE_HEADERS } + ); + } catch (error) { + console.error('⌠HG Brasil API Error:', error); + + logApiMetric({ + route: '/api/market/hgbrasil', + cacheHit: false, + cacheBackend: CacheService.getBackend(), + durationMs: Date.now() - startedAt, + status: 500, + extra: { symbol }, + }); + + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Erro desconhecido', + symbol, + timestamp: new Date().toISOString(), + }, + { status: 500, headers: { 'Cache-Control': 'no-store' } } + ); + } +} diff --git a/src/app/api/market/ifix/route.ts b/src/app/api/market/ifix/route.ts new file mode 100644 index 000000000..e34efda58 --- /dev/null +++ b/src/app/api/market/ifix/route.ts @@ -0,0 +1,61 @@ +import { buildCacheHeaders } from '@/server/cache-headers'; + +const IFIX_CACHE_HEADERS = buildCacheHeaders({ ttl: 90, swr: 600 }); + +export async function GET() { + try { + const agora = new Date(); + const hora = agora.getHours(); + const minuto = agora.getMinutes(); + + // Simulação realista + const isHorarioComercial = hora >= 9 && hora <= 18; + const valorBase = 3440; + const variacao = (Math.random() - 0.5) * (isHorarioComercial ? 3 : 1); + const novoValor = valorBase * (1 + variacao / 100); + + const dadosIfix = { + valor: novoValor, + valorFormatado: novoValor.toLocaleString('pt-BR', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }), + variacao: valorBase * (variacao / 100), + variacaoPercent: variacao, + trend: variacao >= 0 ? 'up' : 'down', + timestamp: agora.toISOString(), + fonte: 'SIMULAÇÃO_VERCEL', + nota: `Atualizado ${hora.toString().padStart(2, '0')}:${minuto.toString().padStart(2, '0')}`, + symbol: 'IFIX' + }; + + console.log('API IFIX chamada:', dadosIfix); + + return new Response(JSON.stringify({ + ifix: dadosIfix, + success: true, + timestamp: agora.toISOString(), + debug: 'API funcionando no Vercel' + }), { + status: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + ...IFIX_CACHE_HEADERS + } + }); + + } catch (error) { + console.error('Erro na API IFIX:', error); + return new Response(JSON.stringify({ + error: 'Erro interno', + message: error instanceof Error ? error.message : 'Erro desconhecido', + timestamp: new Date().toISOString() + }), { + status: 500, + headers: { + 'Content-Type': 'application/json' + } + }); + } +} diff --git a/src/app/api/market/yahoo/ifix/route.ts b/src/app/api/market/yahoo/ifix/route.ts new file mode 100644 index 000000000..888ffacc3 --- /dev/null +++ b/src/app/api/market/yahoo/ifix/route.ts @@ -0,0 +1,74 @@ +import { NextResponse } from 'next/server'; +import { CacheService } from '@/lib/redis'; +import { logApiMetric } from '@/lib/metrics'; + +export const dynamic = 'force-dynamic'; +export const revalidate = 300; +export const runtime = 'nodejs'; +export const maxDuration = 5; + +const CACHE_TTL_SECONDS = 300; + +const CACHE_HEADERS = { + 'Cache-Control': 'public, s-maxage=300, must-revalidate', + 'CDN-Cache-Control': 'public, s-maxage=300, must-revalidate', + 'Vercel-CDN-Cache-Control': 'public, s-maxage=300, must-revalidate', +} as const; + +export async function GET() { + const startedAt = Date.now(); + const cacheKey = CacheService.getCacheKey('yahoo', 'chart', 'IFIX.SA'); + + try { + const { data, metadata } = await CacheService.withCache( + cacheKey, + async () => { + const url = 'https://query1.finance.yahoo.com/v8/finance/chart/IFIX.SA'; + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + 'User-Agent': 'Mozilla/5.0 (compatible; MembrosFatosDaBolsaBot/1.0)', + }, + cache: 'no-store', + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Yahoo IFIX HTTP ${response.status}: ${errorText}`); + } + + return response.json(); + }, + { ttlSeconds: CACHE_TTL_SECONDS } + ); + + logApiMetric({ + route: '/api/market/yahoo/ifix', + cacheHit: metadata.cached, + cacheBackend: metadata.backend, + durationMs: Date.now() - startedAt, + status: 200, + }); + + return NextResponse.json(data, { status: 200, headers: CACHE_HEADERS }); + } catch (error) { + console.error('⌠Yahoo IFIX API Error:', error); + + logApiMetric({ + route: '/api/market/yahoo/ifix', + cacheHit: false, + cacheBackend: CacheService.getBackend(), + durationMs: Date.now() - startedAt, + status: 500, + }); + + return NextResponse.json( + { + error: error instanceof Error ? error.message : 'Erro desconhecido', + }, + { status: 500, headers: { 'Cache-Control': 'no-store' } } + ); + } +} diff --git a/src/app/api/meus-ativos/[carteira]/reorder/route.ts b/src/app/api/meus-ativos/[carteira]/reorder/route.ts new file mode 100644 index 000000000..c5a56197f --- /dev/null +++ b/src/app/api/meus-ativos/[carteira]/reorder/route.ts @@ -0,0 +1,128 @@ +export const dynamic = 'force-dynamic'; +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +const CARTEIRA_MODELS = { + microCaps: 'userMicroCaps', + smallCaps: 'userSmallCaps', + dividendos: 'userDividendos', + fiis: 'userFiis', + dividendosInternacional: 'userDividendosInternacional', + etfs: 'userEtfs', + projetoAmerica: 'userProjetoAmerica', + exteriorStocks: 'userExteriorStocks' +} as const; + +async function getAuthenticatedUser(_request: NextRequest) { + try { + console.log('🔠Iniciando autenticação via sessão...'); + const session = await auth(); + const user = session?.user; + + if (!user) { + console.log('⌠Usuário não autenticado'); + return null; + } + + console.log('✅ Usuário autenticado:', user.email); + return user; + } catch (error) { + console.error('⌠Erro ao autenticar usuário:', error); + return null; + } +} + +// POST - Reordenar ativos +export async function POST(request: NextRequest, { params }: { params: { carteira: string } }) { + try { + console.log('🔄 INICIO REORDENAÇÃO - Carteira:', params.carteira); + + const user = await getAuthenticatedUser(request); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { carteira } = params; + const { novosAtivos } = await request.json(); + + console.log('🔄 Nova ordem recebida:', novosAtivos.map((a: any) => a.ticker)); + + const modelName = CARTEIRA_MODELS[carteira as keyof typeof CARTEIRA_MODELS]; + if (!modelName) { + return NextResponse.json({ error: 'Carteira inválida' }, { status: 400 }); + } + + const model = (prisma as any)[modelName]; + + // 🔥 BUSCAR ATIVOS DO BANCO (FILTRADOS POR USUÃRIO QUANDO NECESSÃRIO) + console.log('🔠Buscando ativos do banco com escopo do usuário...'); + const isAdmin = user.plan === 'ADMIN' || user.email === 'admin@fatosdobolsa.com'; + + const ativosReais = await model.findMany({ + ...(isAdmin ? {} : { where: { userId: user.id } }), + select: { id: true, ticker: true } + }); + + // 🔥 MAPEAR TICKERS PARA IDs REAIS + const tickerParaId = new Map(); + ativosReais.forEach((ativo: any) => { + tickerParaId.set(ativo.ticker, ativo.id); + }); + + console.log('📊 Mapeamento ticker → ID:', Object.fromEntries(tickerParaId.entries())); + + // 🔥 ATUALIZAR EM SEQUÊNCIA PARA MANTER ORDEM + console.log('🔄 Atualizando ordem sequencial...'); + + const baseTime = new Date(); + const atualizacoes = []; + + for (let i = 0; i < novosAtivos.length; i++) { + const ativo = novosAtivos[i]; + const idReal = tickerParaId.get(ativo.ticker); + + if (idReal) { + // Criar timestamps sequenciais (com diferença de 1 segundo) + const timestampOrdem = new Date(baseTime.getTime() + (i * 1000)); + + console.log(`📠Posição ${i + 1}: ${ativo.ticker} (${idReal}) → ${timestampOrdem.toISOString()}`); + + atualizacoes.push( + model.update({ + where: { id: idReal }, + data: { + editadoEm: timestampOrdem + } + }) + ); + } else { + console.warn(`âš ï¸ Ativo ${ativo.ticker} não encontrado no banco`); + } + } + + // Executar todas as atualizações + await prisma.$transaction(atualizacoes); + + // Limpar cache após reordenação + const chaveCache = `dados_mestres_${carteira}`; + if (typeof cacheDadosMestres !== 'undefined') { + cacheDadosMestres.delete(chaveCache); + } + + console.log(`✅ Reordenação da carteira ${carteira} concluída com ${atualizacoes.length} ativos`); + + return NextResponse.json({ + success: true, + message: `${atualizacoes.length} ativos reordenados`, + novaOrdem: novosAtivos.map((a: any) => a.ticker) + }); + + } catch (error) { + console.error('⌠Erro na reordenação:', error); + return NextResponse.json({ + error: 'Erro ao reordenar', + details: (error as Error).message + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/meus-ativos/[carteira]/route.test.ts b/src/app/api/meus-ativos/[carteira]/route.test.ts new file mode 100644 index 000000000..ba9c224a5 --- /dev/null +++ b/src/app/api/meus-ativos/[carteira]/route.test.ts @@ -0,0 +1,67 @@ +import { NextRequest } from 'next/server'; +import { GET } from './route'; +import { auth } from '@/lib/auth'; + +jest.mock('@/lib/auth', () => ({ + auth: jest.fn() +})); + +function createMockCarteiraData() { + return [ + { + id: 1, + createdAt: new Date('2024-01-01T00:00:00Z'), + updatedAt: new Date('2024-01-02T00:00:00Z'), + editadoEm: new Date('2024-01-03T00:00:00Z'), + ticker: 'TEST3' + }, + { + id: 2, + createdAt: new Date('2024-02-01T00:00:00Z'), + updatedAt: new Date('2024-02-02T00:00:00Z'), + editadoEm: new Date('2024-02-03T00:00:00Z'), + ticker: 'TEST4' + } + ]; +} + +const mockCarteiraData = createMockCarteiraData(); + +jest.mock('@prisma/client', () => { + const data = createMockCarteiraData(); + const prismaMock = { + userMicroCaps: { findMany: jest.fn().mockResolvedValue(data) }, + userSmallCaps: { findMany: jest.fn().mockResolvedValue(data) }, + userDividendos: { findMany: jest.fn().mockResolvedValue(data) }, + userFiis: { findMany: jest.fn().mockResolvedValue(data) }, + userRendaTurbinada: { findMany: jest.fn().mockResolvedValue(data) }, + userDividendosInternacional: { findMany: jest.fn().mockResolvedValue(data) }, + userEtfs: { findMany: jest.fn().mockResolvedValue(data) }, + userProjetoAmerica: { findMany: jest.fn().mockResolvedValue(data) }, + userExteriorStocks: { findMany: jest.fn().mockResolvedValue(data) } + }; + + return { + PrismaClient: jest.fn(() => prismaMock) + }; +}); + +describe('GET /api/meus-ativos/[carteira]', () => { + it('retorna todas as posições disponíveis para planos autorizados sem truncamento', async () => { + (auth as jest.MockedFunction).mockResolvedValue({ + user: { + email: 'teste@fatosdabolsa.com', + plan: 'VIP' + } + }); + + const request = new NextRequest('http://localhost/api/meus-ativos/smallCaps'); + + const response = await GET(request, { params: { carteira: 'smallCaps' } }); + const json = await response.json(); + + expect(Array.isArray(json)).toBe(true); + expect(json).toHaveLength(mockCarteiraData.length); + expect(json[0].ticker).toBe('TEST3'); + }); +}); diff --git a/src/app/api/meus-ativos/[carteira]/route.ts b/src/app/api/meus-ativos/[carteira]/route.ts new file mode 100644 index 000000000..6e39db4d4 --- /dev/null +++ b/src/app/api/meus-ativos/[carteira]/route.ts @@ -0,0 +1,905 @@ +// ===== ARQUIVO: src/app/api/meus-ativos/[carteira]/route.ts ===== +// 🔒 VERSÃO FINAL INTEGRADA - AUDITORIA + CACHE + PERMISSÕES + +export const dynamic = 'force-dynamic'; +import { NextRequest, NextResponse } from 'next/server'; +import { ViesOverride } from '@prisma/client'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; +import { CacheService } from '@/lib/cache-service'; + +export const maxDuration = 5; + +const normalizarViesOverrideValor = (valor: unknown): ViesOverride => + valor === 'MANTER' ? 'MANTER' : 'AUTO'; + +// 🔠SISTEMA DE AUDITORIA E CACHE PARA 4 MIL USUÃRIOS +interface AuditLog { + userEmail: string; + userPlan: string; + carteira: string; + acessoPermitido: boolean; + totalItensRetornados: number; + timestamp: Date; + ip?: string; + userAgent?: string; +} + +// 🔠CACHE EM MEMÓRIA PARA PERFORMANCE (4 mil usuários) +const cachePermissoes = new Map(); +const cacheDadosMestres = new Map(); +const inflightDadosMestres = new Map>(); +const CACHE_TTL = 10 * 60 * 1000; // 10 minutos para reduzir chamadas repetidas + +// 📊 ESTATÃSTICAS EM TEMPO REAL +let estatisticasUso = { + totalRequests: 0, + requestsPorPlano: {} as Record, + requestsPorCarteira: {} as Record, + acessosNegados: 0, + ultimaLimpezaCache: Date.now() +}; + +// 🔠FUNÇÃO DE DEBUG +function debugLog(message: string, data?: any) { + console.log(`🔠[DEBUG] ${message}`, data || ''); +} + +// 🔒 FUNÇÃO DE AUDITORIA (CRÃTICA PARA SEGURANÇA) +function registrarAcessoAuditoria(auditData: AuditLog) { + // Log detalhado para auditoria + console.log(`🔠[AUDIT] ${auditData.userEmail} (${auditData.userPlan}) → ${auditData.carteira} | ${auditData.acessoPermitido ? '✅ PERMITIDO' : '⌠NEGADO'} | ${auditData.totalItensRetornados} itens`); + + // Incrementar estatísticas + estatisticasUso.totalRequests++; + estatisticasUso.requestsPorPlano[auditData.userPlan] = (estatisticasUso.requestsPorPlano[auditData.userPlan] || 0) + 1; + estatisticasUso.requestsPorCarteira[auditData.carteira] = (estatisticasUso.requestsPorCarteira[auditData.carteira] || 0) + 1; + + if (!auditData.acessoPermitido) { + estatisticasUso.acessosNegados++; + // 🚨 ALERTA DE SEGURANÇA + console.warn(`🚨 [SECURITY] ACESSO NEGADO: ${auditData.userEmail} tentou acessar ${auditData.carteira} sem permissão`); + } + + // A cada 100 requests, mostrar estatísticas + if (estatisticasUso.totalRequests % 100 === 0) { + console.log('📊 [STATS]', estatisticasUso); + } +} + +// ✅ FUNÇÃO DE AUTENTICAÇÃO UTILIZANDO COOKIES +async function getAuthenticatedUser(_request: NextRequest) { + try { + debugLog('🔠Iniciando autenticação via cookies...'); + + const session = await auth(); + const user = session?.user; + + if (!user) { + debugLog('⌠Usuário não autenticado'); + return null; + } + + debugLog('✅ Usuário autenticado:', user.email); + return user; + } catch (error) { + debugLog('⌠Erro na autenticação:', error); + return null; + } +} + +// 🔒 FUNÇÃO DE VERIFICAÇÃO DE PERMISSÕES (CRÃTICA!) +function verificarPermissaoCarteiraPorPlano(plano: string, carteira: string): boolean { + const PERMISSOES_POR_PLANO = { + 'VIP': ['smallCaps', 'microCaps', 'dividendos', 'fiis', 'dividendosInternacional', 'etfs', 'projetoAmerica', 'exteriorStocks'], + 'LITE': ['smallCaps', 'dividendos', 'fiis', 'dividendosInternacional', 'etfs', 'exteriorStocks'], + 'LITE_V2': ['smallCaps', 'dividendos', 'fiis'], // 🔒 RESTRITO + 'RENDA_PASSIVA': ['dividendos', 'fiis'], // 🔒 MUITO RESTRITO + 'FIIS': ['fiis'], // 🔒 SÓ FIIS + 'RENDA_TURBINADA': ['cfRendaTurbinada'], // 🔒 Plano focado nas recomendações exclusivas do CF Renda Turbinada + 'RENDA_TURBINADA_T2': ['cfRendaTurbinada'], // 🔒 Plano focado nas recomendações exclusivas do CF Renda Turbinada Turma 2 + 'RENDA_TURBINADA_T3': ['cfRendaTurbinada'], // 🔒 Plano focado nas recomendações exclusivas do CF Renda Turbinada Turma 3 + 'AMERICA': ['dividendosInternacional', 'etfs', 'projetoAmerica', 'exteriorStocks'], // 🔒 SÓ INTERNACIONAL + 'ADMIN': ['smallCaps', 'microCaps', 'dividendos', 'fiis', 'cfRendaTurbinada', 'dividendosInternacional', 'etfs', 'projetoAmerica', 'exteriorStocks'] // 🔓 TUDO + }; + + const carteirasPermitidas = PERMISSOES_POR_PLANO[plano] || []; + const temPermissao = carteirasPermitidas.includes(carteira); + + console.log(`🔠[SEGURANÇA] Plano ${plano} - Carteira ${carteira}: ${temPermissao ? '✅ PERMITIDO' : '⌠NEGADO'}`); + + return temPermissao; +} + +// 🔒 VERIFICAÇÃO DE PERMISSÃO COM CACHE +function verificarPermissaoComCache(plano: string, carteira: string): boolean { + const chaveCache = `perm_${plano}_${carteira}`; + + // Verificar cache de permissões + if (cachePermissoes.has(chaveCache)) { + return cachePermissoes.get(chaveCache)!; + } + + // Calcular permissão + const temPermissao = verificarPermissaoCarteiraPorPlano(plano, carteira); + + // Cachear resultado (permissões não mudam) + cachePermissoes.set(chaveCache, temPermissao); + + return temPermissao; +} + +// 📊 FUNÇÃO PARA BUSCAR DADOS MESTRES - CORRIGIDA PARA MOSTRAR TODOS OS DADOS +async function buscarDadosMestres(carteira: string): Promise { + console.log(`📊 Buscando dados mestres para ${carteira} - TODOS OS DADOS DO BANCO`); + + switch (carteira) { + case 'microCaps': + return await prisma.userMicroCaps.findMany({ + orderBy: { editadoEm: 'asc' } + }); + case 'smallCaps': + return await prisma.userSmallCaps.findMany({ + orderBy: { editadoEm: 'asc' } + }); + case 'dividendos': + return await prisma.userDividendos.findMany({ + orderBy: { editadoEm: 'asc' } + }); + case 'fiis': + return await prisma.userFiis.findMany({ + orderBy: { editadoEm: 'asc' } + }); + case 'cfRendaTurbinada': + return await prisma.userRendaTurbinada.findMany({ + orderBy: { editadoEm: 'asc' } + }); + case 'dividendosInternacional': + return await prisma.userDividendosInternacional.findMany({ + orderBy: { editadoEm: 'asc' } + }); + case 'etfs': + return await prisma.userEtfs.findMany({ + orderBy: { editadoEm: 'asc' } + }); + case 'projetoAmerica': + return await prisma.userProjetoAmerica.findMany({ + orderBy: { editadoEm: 'asc' } + }); + case 'exteriorStocks': + return await prisma.userExteriorStocks.findMany({ + orderBy: { editadoEm: 'asc' } + }); + default: + throw new Error('Carteira não implementada'); + } +} + +// âš¡ FUNÇÃO DE CACHE PARA PERFORMANCE +function obterDadosMestresComCache(carteira: string): Promise { + const chaveCache = `dados_mestres_${carteira}`; + const agora = Date.now(); + + // Verificar cache + const dadosCache = cacheDadosMestres.get(chaveCache); + if (dadosCache && (agora - dadosCache.timestamp) < CACHE_TTL) { + console.log(`âš¡ [CACHE] Hit para ${carteira} - dados em cache`); + return Promise.resolve(dadosCache.data); + } + + if (inflightDadosMestres.has(chaveCache)) { + console.log(`â³ [CACHE] Reutilizando promise em voo para ${carteira}`); + return inflightDadosMestres.get(chaveCache)!; + } + + // Cache miss - buscar dados + console.log(`🔄 [CACHE] Miss para ${carteira} - buscando dados frescos`); + + const promise = buscarDadosMestres(carteira) + .then(dados => { + // Atualizar cache + cacheDadosMestres.set(chaveCache, { + data: dados, + timestamp: agora + }); + + inflightDadosMestres.delete(chaveCache); + + // Limpeza periódica do cache + if (agora - estatisticasUso.ultimaLimpezaCache > CACHE_TTL * 2) { + limparCacheExpirado(); + estatisticasUso.ultimaLimpezaCache = agora; + } + + return dados; + }) + .catch(error => { + inflightDadosMestres.delete(chaveCache); + throw error; + }); + + inflightDadosMestres.set(chaveCache, promise); + + return promise; +} + +// 🧹 LIMPEZA DE CACHE +function limparCacheExpirado() { + const agora = Date.now(); + let removidos = 0; + + for (const [chave, valor] of cacheDadosMestres.entries()) { + if (agora - valor.timestamp > CACHE_TTL) { + cacheDadosMestres.delete(chave); + removidos++; + } + } + + console.log(`🧹 [CACHE] Limpeza: ${removidos} entradas removidas`); +} + +// 🔒 FILTROS RIGOROSOS POR PLANO +function aplicarFiltrosRigorososPorPlano( + dados: any[], + plano: string, + carteira: string, + userEmail: string +): any[] { + + console.log(`🔒 [FILTRO] Aplicando filtros para plano ${plano} na carteira ${carteira}`); + console.log(`📊 [FILTRO] Dados de entrada: ${dados.length} itens`); + + // 🔒 CONFIGURAÇÃO DE FILTROS POR PLANO + const CONFIGURACAO_PLANOS = { + 'VIP': { + filtros: {} // VIP vê tudo + }, + 'LITE': { + filtros: {} + }, + 'LITE_V2': { + filtros: {} + }, + 'RENDA_PASSIVA': { + filtros: {} + }, + 'FIIS': { + filtros: { + fiis: (item: any) => true // FIIS vê todos os FIIs + } + }, + 'RENDA_TURBINADA': { + filtros: {} // Conteúdo específico é entregue em páginas dedicadas + }, + 'RENDA_TURBINADA_T2': { + filtros: {} + }, + 'RENDA_TURBINADA_T3': { + filtros: {} + }, + 'AMERICA': { + filtros: {} // América vê tudo do internacional + }, + 'ADMIN': { + filtros: {} // Admin vê tudo + } + }; + + const config = CONFIGURACAO_PLANOS[plano]; + if (!config) { + console.log(`⌠[FILTRO] Plano ${plano} não reconhecido - retornando vazio`); + return []; + } + + let dadosFiltrados = [...dados]; + + // 1. Aplicar filtros de conteúdo se existirem + const filtroConteudo = config.filtros[carteira]; + if (filtroConteudo) { + dadosFiltrados = dadosFiltrados.filter(filtroConteudo); + console.log(`🔠[FILTRO] Após filtro de conteúdo: ${dadosFiltrados.length} itens`); + } + + console.log(`📊 [FILTRO] Resultado final sem limite aplicado: ${dadosFiltrados.length} itens`); + console.log(`👤 [FILTRO] Usuário ${userEmail} (${plano}) acessando ${carteira}`); + + return dadosFiltrados; +} + +// 📊 FUNÇÃO PARA NÃVEL DE ACESSO +function getAccessLevel(plano: string, carteira: string): string { + if (plano === 'ADMIN') return 'FULL'; + if (plano === 'VIP') return 'PREMIUM'; + if (['LITE', 'LITE_V2'].includes(plano)) return 'STANDARD'; + return 'LIMITED'; +} + +// 🔠GET - BUSCAR ATIVOS - VERSÃO FINAL INTEGRADA +export async function GET( + request: NextRequest, + { params }: { params: { carteira: string } } +) { + const inicioRequest = Date.now(); + let auditData: Partial = { + carteira: params.carteira, + timestamp: new Date() + }; + + try { + debugLog('📊 INICIO GET - Carteira:', params.carteira); + + // Extrair informações de auditoria + auditData.ip = request.ip || request.headers.get('x-forwarded-for') || 'unknown'; + auditData.userAgent = request.headers.get('user-agent') || 'unknown'; + + // 1. Autenticação + const user = await getAuthenticatedUser(request); + if (!user) { + auditData.acessoPermitido = false; + auditData.totalItensRetornados = 0; + auditData.userEmail = 'UNKNOWN'; + auditData.userPlan = 'UNKNOWN'; + registrarAcessoAuditoria(auditData as AuditLog); + return NextResponse.json({ error: 'Não autorizado' }, { status: 401 }); + } + + auditData.userEmail = user.email; + auditData.userPlan = user.plan; + + // 2. Validar carteira + const carteirasValidas = [ + 'microCaps', 'smallCaps', 'dividendos', 'fiis', + 'cfRendaTurbinada', + 'dividendosInternacional', 'etfs', 'projetoAmerica', 'exteriorStocks' + ]; + + if (!carteirasValidas.includes(params.carteira)) { + auditData.acessoPermitido = false; + auditData.totalItensRetornados = 0; + registrarAcessoAuditoria(auditData as AuditLog); + return NextResponse.json({ error: 'Carteira inválida' }, { status: 400 }); + } + + // 3. âš¡ VERIFICAR PERMISSÕES COM CACHE + const temPermissao = verificarPermissaoComCache(user.plan, params.carteira); + auditData.acessoPermitido = temPermissao; + + if (!temPermissao) { + debugLog(`🚫 Plano ${user.plan} NÃO tem acesso à carteira ${params.carteira}`); + auditData.totalItensRetornados = 0; + registrarAcessoAuditoria(auditData as AuditLog); + return NextResponse.json([]); // Retorna vazio para planos sem acesso + } + + debugLog(`✅ Plano ${user.plan} tem acesso à carteira ${params.carteira}`); + + const cacheKey = CacheService.getCacheKey('user-assets', user.id, params.carteira); + + const { data, metadata } = await CacheService.withCache( + cacheKey, + async () => { + // 4. âš¡ BUSCAR DADOS MESTRES COM CACHE + const dadosMestres = await obterDadosMestresComCache(params.carteira); + debugLog(`📊 Dados mestres encontrados: ${dadosMestres.length} itens`); + + // 5. 🔒 APLICAR FILTROS RIGOROSOS + const dadosFiltrados = aplicarFiltrosRigorososPorPlano( + dadosMestres, + user.plan, + params.carteira, + user.email + ); + + debugLog(`🔒 Após filtro do plano ${user.plan}: ${dadosFiltrados.length} itens`); + + // 6. Preparar resposta otimizada (sem requestTime dinâmico) + return dadosFiltrados.map(ativo => ({ + ...ativo, + createdAt: ativo.createdAt?.toISOString(), + updatedAt: ativo.updatedAt?.toISOString(), + editadoEm: ativo.editadoEm?.toISOString(), + // Metadados de performance e segurança + isFiltered: true, + userPlan: user.plan, + accessLevel: getAccessLevel(user.plan, params.carteira), + cached: true, + requestTime: 0 + })); + }, + { + ttlSeconds: 300, + tags: ['user-assets', user.id, params.carteira], + } + ); + + auditData.totalItensRetornados = data.length; + + const ativosSerializados = data.map(ativo => ({ + ...ativo, + requestTime: Date.now() - inicioRequest + })); + + // 7. Registrar auditoria de sucesso + registrarAcessoAuditoria(auditData as AuditLog); + + // 8. Headers de performance + const response = NextResponse.json(ativosSerializados); + response.headers.set('X-Cache-Status', metadata.cached ? 'HIT' : 'MISS'); + response.headers.set('X-Items-Count', data.length.toString()); + response.headers.set('X-Access-Level', getAccessLevel(user.plan, params.carteira)); + response.headers.set('X-Request-Time', (Date.now() - inicioRequest).toString()); + + return response; + + } catch (error) { + debugLog('⌠Erro GET:', error); + auditData.acessoPermitido = false; + auditData.totalItensRetornados = 0; + registrarAcessoAuditoria(auditData as AuditLog); + + console.error(`⌠[ERROR] ${auditData.userEmail} - ${params.carteira}:`, error); + return NextResponse.json({ error: 'Erro interno' }, { status: 500 }); + } +} + +// 🔠POST - CRIAR NOVO ATIVO +export async function POST( + request: NextRequest, + { params }: { params: { carteira: string } } +) { + const inicioRequest = Date.now(); + let auditData: Partial = { + carteira: params.carteira, + timestamp: new Date() + }; + + try { + debugLog('âž• INICIO POST - Carteira:', params.carteira); + + // Extrair informações de auditoria + auditData.ip = request.ip || request.headers.get('x-forwarded-for') || 'unknown'; + auditData.userAgent = request.headers.get('user-agent') || 'unknown'; + + // 1. Autenticação + const user = await getAuthenticatedUser(request); + if (!user) { + auditData.acessoPermitido = false; + auditData.totalItensRetornados = 0; + auditData.userEmail = 'UNKNOWN'; + auditData.userPlan = 'UNKNOWN'; + registrarAcessoAuditoria(auditData as AuditLog); + return NextResponse.json({ error: 'Não autorizado' }, { status: 401 }); + } + + auditData.userEmail = user.email; + auditData.userPlan = user.plan; + + // 2. Validar carteira + const carteirasValidas = [ + 'microCaps', 'smallCaps', 'dividendos', 'fiis', + 'cfRendaTurbinada', + 'dividendosInternacional', 'etfs', 'projetoAmerica', 'exteriorStocks' + ]; + + if (!carteirasValidas.includes(params.carteira)) { + auditData.acessoPermitido = false; + auditData.totalItensRetornados = 0; + registrarAcessoAuditoria(auditData as AuditLog); + return NextResponse.json({ error: 'Carteira inválida' }, { status: 400 }); + } + + // 3. âš¡ VERIFICAR PERMISSÕES COM CACHE + const temPermissao = verificarPermissaoComCache(user.plan, params.carteira); + auditData.acessoPermitido = temPermissao; + + if (!temPermissao) { + debugLog(`🚫 Plano ${user.plan} NÃO tem permissão para criar na carteira ${params.carteira}`); + auditData.totalItensRetornados = 0; + registrarAcessoAuditoria(auditData as AuditLog); + return NextResponse.json({ error: 'Sem permissão para esta carteira' }, { status: 403 }); + } + + debugLog(`✅ Plano ${user.plan} tem permissão para criar na carteira ${params.carteira}`); + + // 4. Parse do body + const body = await request.json(); + debugLog('âž• Body recebido:', body); + + // 5. Preparar dados para criação + const dadosCreate: any = { + userId: user.id, + ticker: body.ticker?.toUpperCase(), + setor: body.setor, + dataEntrada: body.dataEntrada, + precoEntrada: parseFloat(body.precoEntrada), + editadoEm: new Date(), + viesOverride: normalizarViesOverrideValor(body.viesOverride) + }; + + // Campos opcionais + if (body.precoTeto) dadosCreate.precoTeto = parseFloat(body.precoTeto); + if (body.precoTetoBDR) dadosCreate.precoTetoBDR = parseFloat(body.precoTetoBDR); + if (body.posicaoEncerrada !== undefined) dadosCreate.posicaoEncerrada = body.posicaoEncerrada; + if (body.dataSaida) dadosCreate.dataSaida = body.dataSaida; + if (body.precoSaida) dadosCreate.precoSaida = parseFloat(body.precoSaida); + if (body.motivoEncerramento) dadosCreate.motivoEncerramento = body.motivoEncerramento; + + debugLog('âž• Dados para criar:', dadosCreate); + + let ativoCriado; + + // 6. Criar ativo na carteira específica + switch (params.carteira) { + case 'smallCaps': + ativoCriado = await prisma.userSmallCaps.create({ + data: dadosCreate + }); + break; + case 'microCaps': + ativoCriado = await prisma.userMicroCaps.create({ + data: dadosCreate + }); + break; + case 'dividendos': + ativoCriado = await prisma.userDividendos.create({ + data: dadosCreate + }); + break; + case 'fiis': + ativoCriado = await prisma.userFiis.create({ + data: dadosCreate + }); + break; + case 'cfRendaTurbinada': + ativoCriado = await prisma.userRendaTurbinada.create({ + data: dadosCreate + }); + break; + case 'dividendosInternacional': + ativoCriado = await prisma.userDividendosInternacional.create({ + data: dadosCreate + }); + break; + case 'etfs': + ativoCriado = await prisma.userEtfs.create({ + data: dadosCreate + }); + break; + case 'projetoAmerica': + ativoCriado = await prisma.userProjetoAmerica.create({ + data: dadosCreate + }); + break; + case 'exteriorStocks': + ativoCriado = await prisma.userExteriorStocks.create({ + data: dadosCreate + }); + break; + default: + return NextResponse.json({ error: 'Carteira não implementada' }, { status: 400 }); + } + + debugLog('✅ Ativo criado:', ativoCriado.id); + console.log(`✅ Ativo ${dadosCreate.ticker} criado na carteira ${params.carteira} pelo usuário ${user.email} (${user.plan})`); + + // 7. Limpar cache após criação + const chaveCache = `dados_mestres_${params.carteira}`; + cacheDadosMestres.delete(chaveCache); + console.log(`🔄 Cache limpo para ${params.carteira} após criação`); + await CacheService.invalidateTags(['user-assets', user.id, params.carteira]); + + // 8. Registrar auditoria de sucesso + auditData.totalItensRetornados = 1; + registrarAcessoAuditoria(auditData as AuditLog); + + // 9. Preparar resposta + const ativoSerializado = { + ...ativoCriado, + createdAt: ativoCriado.createdAt?.toISOString(), + updatedAt: ativoCriado.updatedAt?.toISOString(), + editadoEm: ativoCriado.editadoEm?.toISOString(), + // Metadados + userPlan: user.plan, + accessLevel: getAccessLevel(user.plan, params.carteira), + requestTime: Date.now() - inicioRequest + }; + + // 10. Headers de resposta + const response = NextResponse.json(ativoSerializado); + response.headers.set('X-Operation', 'CREATE'); + response.headers.set('X-Access-Level', getAccessLevel(user.plan, params.carteira)); + response.headers.set('X-Request-Time', (Date.now() - inicioRequest).toString()); + + return response; + + } catch (error) { + debugLog('⌠Erro POST:', error); + auditData.acessoPermitido = false; + auditData.totalItensRetornados = 0; + registrarAcessoAuditoria(auditData as AuditLog); + + console.error(`⌠[ERROR] POST ${auditData.userEmail} - ${params.carteira}:`, error); + return NextResponse.json({ error: 'Erro interno' }, { status: 500 }); + } +} + +// 🔠PUT - EDITAR ATIVO (MANTENDO FUNCIONALIDADE DE REORDENAÇÃO) +export async function PUT(request: NextRequest, { params }: { params: { carteira: string } }) { + try { + console.log('âœï¸ INICIO PUT - Carteira:', params.carteira); + + // Autenticação + const user = await getAuthenticatedUser(request); + if (!user) { + return NextResponse.json({ error: 'Não autorizado' }, { status: 401 }); + } + + const { carteira } = params; + const body = await request.json(); + + // Verificar se é reordenação ou edição individual + if (body.novosAtivos && Array.isArray(body.novosAtivos)) { + // REORDENAÇÃO + console.log('🔄 REORDENAÇÃO detectada para:', carteira); + + const CARTEIRA_MODELS = { + microCaps: 'userMicroCaps', + smallCaps: 'userSmallCaps', + dividendos: 'userDividendos', + fiis: 'userFiis', + cfRendaTurbinada: 'userRendaTurbinada', + dividendosInternacional: 'userDividendosInternacional', + etfs: 'userEtfs', + projetoAmerica: 'userProjetoAmerica', + exteriorStocks: 'userExteriorStocks' + } as const; + + const modelName = CARTEIRA_MODELS[carteira as keyof typeof CARTEIRA_MODELS]; + if (!modelName) { + return NextResponse.json({ error: 'Carteira inválida' }, { status: 400 }); + } + + const model = (prisma as any)[modelName]; + + // Atualizar ordem de todos os ativos + const atualizacoes = body.novosAtivos.map(async (ativo: any, index: number) => { + return model.update({ + where: { id: ativo.id }, + data: { + ordem: index, + editadoEm: new Date() + } + }); + }); + + await Promise.all(atualizacoes); + + // Limpar cache após reordenação + const chaveCache = `dados_mestres_${carteira}`; + cacheDadosMestres.delete(chaveCache); + await CacheService.invalidateTags(['user-assets', user.id, carteira]); + + console.log(`✅ Reordenação concluída para ${carteira}`); + return NextResponse.json({ success: true, message: 'Reordenação realizada com sucesso' }); + + } else { + // EDIÇÃO INDIVIDUAL + const { id, ...dadosAtualizacao } = body; + + console.log('âœï¸ Editando ativo ID:', id); + + if (!id) { + return NextResponse.json({ error: 'ID é obrigatório' }, { status: 400 }); + } + + const CARTEIRA_MODELS = { + microCaps: 'userMicroCaps', + smallCaps: 'userSmallCaps', + dividendos: 'userDividendos', + fiis: 'userFiis', + cfRendaTurbinada: 'userRendaTurbinada', + dividendosInternacional: 'userDividendosInternacional', + etfs: 'userEtfs', + projetoAmerica: 'userProjetoAmerica', + exteriorStocks: 'userExteriorStocks' + } as const; + + const modelName = CARTEIRA_MODELS[carteira as keyof typeof CARTEIRA_MODELS]; + if (!modelName) { + return NextResponse.json({ error: 'Carteira inválida' }, { status: 400 }); + } + + const model = (prisma as any)[modelName]; + + // Preparar dados para atualização + const dadosUpdate: any = { + editadoEm: new Date() + }; + + // Campos obrigatórios + if (dadosAtualizacao.ticker) { + dadosUpdate.ticker = dadosAtualizacao.ticker.toUpperCase(); + } + if (dadosAtualizacao.setor) { + dadosUpdate.setor = dadosAtualizacao.setor; + } + if (dadosAtualizacao.dataEntrada) { + dadosUpdate.dataEntrada = dadosAtualizacao.dataEntrada; + } + if (dadosAtualizacao.precoEntrada) { + dadosUpdate.precoEntrada = parseFloat(dadosAtualizacao.precoEntrada); + } + + // Campos opcionais + if (dadosAtualizacao.precoTeto !== undefined) { + dadosUpdate.precoTeto = dadosAtualizacao.precoTeto ? parseFloat(dadosAtualizacao.precoTeto) : null; + } + if (dadosAtualizacao.precoTetoBDR !== undefined) { + dadosUpdate.precoTetoBDR = dadosAtualizacao.precoTetoBDR ? parseFloat(dadosAtualizacao.precoTetoBDR) : null; + } + if (dadosAtualizacao.posicaoEncerrada !== undefined) { + dadosUpdate.posicaoEncerrada = dadosAtualizacao.posicaoEncerrada; + } + if (dadosAtualizacao.dataSaida) { + dadosUpdate.dataSaida = dadosAtualizacao.dataSaida; + } + if (dadosAtualizacao.precoSaida !== undefined) { + dadosUpdate.precoSaida = dadosAtualizacao.precoSaida ? parseFloat(dadosAtualizacao.precoSaida) : null; + } + if (dadosAtualizacao.motivoEncerramento) { + dadosUpdate.motivoEncerramento = dadosAtualizacao.motivoEncerramento; + } + if (dadosAtualizacao.viesOverride !== undefined) { + dadosUpdate.viesOverride = normalizarViesOverrideValor(dadosAtualizacao.viesOverride); + } + + console.log('âœï¸ Dados para atualizar:', dadosUpdate); + + // Atualizar no banco (SEM filtro de userId para permitir edição de qualquer registro) + const ativoAtualizado = await model.update({ + where: { id: id }, + data: dadosUpdate + }); + + // Limpar cache após edição + const chaveCache = `dados_mestres_${carteira}`; + cacheDadosMestres.delete(chaveCache); + await CacheService.invalidateTags(['user-assets', user.id, carteira]); + + console.log('✅ Ativo atualizado com sucesso:', ativoAtualizado.id); + + return NextResponse.json({ + success: true, + ativo: ativoAtualizado, + message: `Ativo ${ativoAtualizado.ticker} atualizado com sucesso` + }); + } + + } catch (error) { + debugLog('⌠Erro PUT:', error); + auditData.acessoPermitido = false; + auditData.totalItensRetornados = 0; + registrarAcessoAuditoria(auditData as AuditLog); + + console.error(`⌠[ERROR] PUT ${auditData.userEmail} - ${params.carteira}:`, error); + return NextResponse.json({ + error: 'Erro interno', + details: (error as Error).message + }, { status: 500 }); + } +} + +// 🔠DELETE - REMOVER ATIVO +export async function DELETE(request: NextRequest, { params }: { params: { carteira: string } }) { + try { + console.log('ðŸ—‘ï¸ INICIO DELETE - Carteira:', params.carteira); + + // Autenticação + const user = await getAuthenticatedUser(request); + if (!user) { + return NextResponse.json({ error: 'Não autorizado' }, { status: 401 }); + } + + const { carteira } = params; + const body = await request.json(); + const { id } = body; + + console.log('ðŸ—‘ï¸ Removendo ativo ID:', id); + + if (!id) { + return NextResponse.json({ error: 'ID é obrigatório' }, { status: 400 }); + } + + const CARTEIRA_MODELS = { + microCaps: 'userMicroCaps', + smallCaps: 'userSmallCaps', + dividendos: 'userDividendos', + fiis: 'userFiis', + cfRendaTurbinada: 'userRendaTurbinada', + dividendosInternacional: 'userDividendosInternacional', + etfs: 'userEtfs', + projetoAmerica: 'userProjetoAmerica', + exteriorStocks: 'userExteriorStocks' + } as const; + + const modelName = CARTEIRA_MODELS[carteira as keyof typeof CARTEIRA_MODELS]; + if (!modelName) { + return NextResponse.json({ error: 'Carteira inválida' }, { status: 400 }); + } + + const model = (prisma as any)[modelName]; + + // Verificar se o ativo existe (SEM filtro de userId) + const ativoExistente = await model.findFirst({ + where: { id: id }, + select: { + id: true, + ticker: true, + setor: true + } + }); + + if (!ativoExistente) { + console.log('⌠Ativo não encontrado'); + return NextResponse.json({ + error: 'Ativo não encontrado' + }, { status: 404 }); + } + + console.log('✅ Ativo encontrado:', ativoExistente.ticker); + + // Remover do banco (SEM filtro de userId) + const ativoRemovido = await model.delete({ + where: { id: id } + }); + + // Limpar cache após remoção + const chaveCache = `dados_mestres_${carteira}`; + cacheDadosMestres.delete(chaveCache); + await CacheService.invalidateTags(['user-assets', user.id, carteira]); + + console.log('✅ Ativo removido com sucesso:', ativoRemovido.id); + + return NextResponse.json({ + success: true, + ativo: ativoRemovido, + message: `Ativo ${ativoRemovido.ticker} removido com sucesso` + }); + + } catch (error) { + console.error('⌠Erro na remoção:', error); + + if (error instanceof Error && error.message.includes('Record to delete does not exist')) { + return NextResponse.json({ + error: 'Ativo não encontrado para remoção' + }, { status: 404 }); + } + + return NextResponse.json({ + error: 'Erro ao remover ativo', + details: (error as Error).message + }, { status: 500 }); + } +} + +// 📊 ENDPOINT PARA ESTATÃSTICAS (ADMIN ONLY) +export async function OPTIONS(request: NextRequest) { + const user = await getAuthenticatedUser(request); + + if (user?.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Acesso negado' }, { status: 403 }); + } + + return NextResponse.json({ + estatisticas: estatisticasUso, + cache: { + permissoes: cachePermissoes.size, + dadosMestres: cacheDadosMestres.size, + ultimaLimpeza: new Date(estatisticasUso.ultimaLimpezaCache) + }, + performance: { + cacheHitRate: ((estatisticasUso.totalRequests - Object.values(estatisticasUso.requestsPorCarteira).length) / estatisticasUso.totalRequests * 100).toFixed(2) + '%', + acessosNegadosPercent: (estatisticasUso.acessosNegados / estatisticasUso.totalRequests * 100).toFixed(2) + '%' + } + }); +} diff --git a/src/app/api/notifications/[id]/route.ts b/src/app/api/notifications/[id]/route.ts new file mode 100644 index 000000000..cfb356d36 --- /dev/null +++ b/src/app/api/notifications/[id]/route.ts @@ -0,0 +1,178 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +import { auth } from '@/lib/auth'; +import { emitNotificationEvent } from '@/lib/notificationsEmitter'; +import { CacheService } from '@/lib/redis'; +import { logApiMetric } from '@/lib/metrics'; + +export async function GET(request: NextRequest, { params }: { params: { id: string } }) { + const startedAt = Date.now(); + + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const cacheKey = CacheService.getCacheKey('notification', params.id, session.user.id); + + const { data, metadata } = await CacheService.withCache( + cacheKey, + async () => { + const notification = await prisma.notification.findFirst({ + where: { + id: params.id, + userId: session.user.id, + }, + }); + + return notification; + }, + { ttlSeconds: 60, tags: [`notifications:${session.user.id}`] } + ); + + if (!data) { + return NextResponse.json({ error: 'Notification not found' }, { status: 404 }); + } + + logApiMetric({ + route: '/api/notifications/[id]', + cacheHit: metadata.cached, + cacheBackend: metadata.backend, + durationMs: Date.now() - startedAt, + status: 200, + }); + + return NextResponse.json(data); + } catch (error) { + console.error('Error fetching notification detail:', error); + return NextResponse.json({ + error: 'Internal server error', + details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined + }, { status: 500 }); + } +} + +// PUT - Marcar notificação como lida +export async function PUT(request: NextRequest, { params }: { params: { id: string } }) { + const startedAt = Date.now(); + + try { + const session = await auth(); + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + // ✅ Debounce com cache temporário de 5s + const dedupeKey = `mark-read:${session.user.id}:${params.id}`; + + const { data, metadata } = await CacheService.withCache( + dedupeKey, + async () => { + // Atualizar no banco + const result = await prisma.notification.updateMany({ + where: { + id: params.id, + userId: session.user.id + }, + data: { read: true } + }); + + if (result.count === 0) { + throw new Error('Notification not found'); + } + + // Invalidar cache de listagem + await CacheService.invalidateTags([`notifications:${session.user.id}`]); + + // Emitir evento SSE + emitNotificationEvent(session.user.id, { + type: 'refresh', + reason: 'updated' + }); + + return { success: true, message: 'Notification marked as read' }; + }, + { + ttlSeconds: 5, // ✅ Cache por 5 segundos (debounce) + tags: [] // Não precisa de tags aqui + } + ); + + if (!data) { + return NextResponse.json({ error: 'Notification not found' }, { status: 404 }); + } + + // ✅ CORRIGIDO: Remover campos extras que logApiMetric não suporta + logApiMetric({ + route: '/api/notifications/[id]', // PUT + cacheHit: metadata.cached, + cacheBackend: metadata.backend, + durationMs: Date.now() - startedAt, + status: 200, + }); + + // ✅ ADICIONAR: Log extra para debug (opcional) + if (metadata.cached) { + console.log(`[Debounce] PUT /api/notifications/${params.id} deduplicado`); + } + + return NextResponse.json(data); + + } catch (error) { + console.error('Error marking notification as read:', error); + + logApiMetric({ + route: '/api/notifications/[id]', // PUT + cacheHit: false, + cacheBackend: 'none', + durationMs: Date.now() - startedAt, + status: 500, + }); + + return NextResponse.json({ + error: 'Internal server error', + details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined + }, { status: 500 }); + } +} + +// DELETE - Remover notificação +export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const result = await prisma.notification.deleteMany({ + where: { + id: params.id, + userId: session.user.id + } + }); + + if (result.count === 0) { + return NextResponse.json({ error: 'Notification not found' }, { status: 404 }); + } + + emitNotificationEvent(session.user.id, { + type: 'refresh', + reason: 'deleted' + }); + + await CacheService.invalidateTags([`notifications:${session.user.id}`]); + + return NextResponse.json({ message: 'Notification deleted successfully' }); + } catch (error) { + console.error('Error deleting notification:', error); + return NextResponse.json({ + error: 'Internal server error', + details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined + }, { status: 500 }); + } +} + diff --git a/src/app/api/notifications/fluxo/route.ts b/src/app/api/notifications/fluxo/route.ts new file mode 100644 index 000000000..da58f2863 --- /dev/null +++ b/src/app/api/notifications/fluxo/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { createNotificationStreamResponse } from '@/lib/notificationStream'; +import { consumeRateLimit, getClientRateLimitKey } from '@/lib/rateLimiter'; + +export const dynamic = 'force-dynamic'; +export const fetchCache = 'force-no-store'; + +export async function GET(request: NextRequest) { + const { allowed, retryAfterMs } = consumeRateLimit({ + key: `notifications:stream:${getClientRateLimitKey(request)}`, + limit: 10, + windowMs: 10 * 60 * 1000 + }); + + if (!allowed) { + return NextResponse.json( + { error: 'Stream rate limit exceeded' }, + { status: 429, headers: { 'Retry-After': Math.ceil(retryAfterMs / 1000).toString() } } + ); + } + + return createNotificationStreamResponse(request, { + routeLabel: '/api/notifications/fluxo', + // Mantemos a conexão aberta por mais tempo para evitar reconexões + // frequentes que estavam gerando excesso de requisições e erros 429. + connectionTimeoutMs: 30000 + }); +} diff --git a/src/app/api/notifications/mark-all-read/route.ts b/src/app/api/notifications/mark-all-read/route.ts new file mode 100644 index 000000000..0393e45fc --- /dev/null +++ b/src/app/api/notifications/mark-all-read/route.ts @@ -0,0 +1,47 @@ +import { NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +import { auth } from '@/lib/auth'; +import { emitNotificationEvent } from '@/lib/notificationsEmitter'; + +// PUT - Marcar todas as notificações como lidas +export async function PUT() { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = session.user; + + const result = await prisma.notification.updateMany({ + where: { + userId: user.id, + read: false + }, + data: { + read: true + } + }); + + emitNotificationEvent(user.id, { + type: 'refresh', + reason: 'mark-all-read' + }); + + return NextResponse.json({ updatedCount: result.count }); + } catch (error) { + console.error('Error marking notifications as read:', error); + return NextResponse.json( + { + error: 'Internal server error', + details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined + }, + { status: 500 } + ); + } finally { + await prisma.$disconnect(); + } +} + diff --git a/src/app/api/notifications/route.ts b/src/app/api/notifications/route.ts new file mode 100644 index 000000000..e62153ea2 --- /dev/null +++ b/src/app/api/notifications/route.ts @@ -0,0 +1,277 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; +import { emitNotificationEvent } from '@/lib/notificationsEmitter'; +import { CacheService } from '@/lib/redis'; +import { logApiMetric } from '@/lib/metrics'; +import { consumeRateLimit, getClientRateLimitKey } from '@/lib/rateLimiter'; + +export const maxDuration = 5; + +// GET - Listar notificações do usuário +export async function GET(request: NextRequest) { + const startedAt = Date.now(); + + const { allowed, retryAfterMs } = consumeRateLimit({ + key: `notifications:list:${getClientRateLimitKey(request)}`, + limit: 20, + windowMs: 5 * 60 * 1000 + }); + + if (!allowed) { + return NextResponse.json( + { error: 'Too many notification fetches, try again later' }, + { status: 429, headers: { 'Retry-After': Math.ceil(retryAfterMs / 1000).toString() } } + ); + } + + try { + const { searchParams } = new URL(request.url); + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = session.user; + + const page = parseInt(searchParams.get('page') || '1'); + const limit = parseInt(searchParams.get('limit') || '20'); + const unreadOnly = searchParams.get('unreadOnly') === 'true'; + + const whereClause: any = { + userId: user.id + }; + + if (unreadOnly) { + whereClause.read = false; + } + + const cacheKey = CacheService.getCacheKey( + 'notifications', + user.id, + `page:${page}`, + `limit:${limit}`, + `unread:${unreadOnly}` + ); + + const { data, metadata } = await CacheService.withCache( + cacheKey, + async () => { + const [notifications, totalNotifications, unreadCount] = await Promise.all([ + prisma.notification.findMany({ + where: whereClause, + select: { + id: true, + type: true, + title: true, + message: true, + read: true, + createdAt: true, + actionUrl: true, // ✅ CORRIGIDO: era "link" + category: true, // ✅ ADICIONADO + metadata: true, // ✅ ADICIONADO + }, + orderBy: { + createdAt: 'desc' + }, + skip: (page - 1) * limit, + take: limit + }), + prisma.notification.count({ + where: whereClause + }), + prisma.notification.count({ + where: { + userId: user.id, + read: false + } + }) + ]); + + return { + notifications, + unreadCount, + pagination: { + page, + limit, + total: totalNotifications, + totalPages: Math.ceil(totalNotifications / limit) + } + }; + }, + { + ttlSeconds: 120, + tags: [`notifications:${user.id}`], + } + ); + + const response = NextResponse.json(data, { + headers: { + 'Cache-Control': 'private, max-age=0, must-revalidate', + } + }); + + logApiMetric({ + route: '/api/notifications', + cacheHit: metadata.cached, + cacheBackend: metadata.backend, + durationMs: Date.now() - startedAt, + status: 200, + extra: { unreadOnly, page, limit }, + }); + + return response; + + } catch (error) { + console.error('Error fetching notifications:', error); + return NextResponse.json({ + error: 'Internal server error', + details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined + }, { status: 500 }); + } +} + +// POST - Criar nova notificação (para admins) +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = session.user; + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Admin access required' }, { status: 403 }); + } + + const body = await request.json(); + const { + targetUserId, + targetPlan, + targetPlans, + title, + message, + type = 'info', + category = 'general', + actionUrl, + metadata = {} + } = body; + + if (!title || !message) { + return NextResponse.json({ + error: 'title and message are required' + }, { status: 400 }); + } + + const plansInput: string[] = []; + + if (Array.isArray(targetPlans)) { + plansInput.push( + ...targetPlans + .filter((plan: unknown): plan is string => typeof plan === 'string') + .map(plan => plan.trim()) + .filter(Boolean) + ); + } + + if (typeof targetPlan === 'string' && targetPlan.trim()) { + plansInput.push(targetPlan.trim()); + } + + const uniquePlans = Array.from(new Set(plansInput)); + + if (!targetUserId && uniquePlans.length === 0) { + return NextResponse.json({ + error: 'targetUserId or targetPlans is required' + }, { status: 400 }); + } + + const recipientIds = new Set(); + + if (targetUserId && typeof targetUserId === 'string') { + recipientIds.add(targetUserId); + } + + if (uniquePlans.length > 0) { + const usersFromPlans = await prisma.user.findMany({ + where: { + plan: { + in: uniquePlans + } + }, + select: { + id: true + } + }); + + usersFromPlans.forEach(userRecord => { + if (userRecord.id) { + recipientIds.add(userRecord.id); + } + }); + } + + if (recipientIds.size === 0) { + return NextResponse.json({ + error: 'No recipients found for the provided criteria' + }, { status: 404 }); + } + + const metadataPayload = + metadata && typeof metadata === 'object' && !Array.isArray(metadata) + ? metadata + : {}; + + const baseMetadata = { + ...metadataPayload, + ...(uniquePlans.length > 0 ? { targetPlans: uniquePlans } : {}), + broadcast: uniquePlans.length > 0, + createdBy: user.id, + createdByEmail: user.email + }; + + const sanitizedActionUrl = + typeof actionUrl === 'string' && actionUrl.trim() ? actionUrl.trim() : undefined; + + const notifications = await prisma.$transaction( + Array.from(recipientIds).map(recipientId => + prisma.notification.create({ + data: { + userId: recipientId, + title: title.trim(), + message: message.trim(), + type, + category, + actionUrl: sanitizedActionUrl, + metadata: baseMetadata + } + }) + ) + ); + + Array.from(recipientIds).forEach(recipientId => { + emitNotificationEvent(recipientId, { + type: 'refresh', + reason: 'created' + }); + }); + + await CacheService.invalidateTags(Array.from(recipientIds).map(id => `notifications:${id}`)); + + return NextResponse.json({ + notificationsCreated: notifications.length, + recipients: Array.from(recipientIds), + message: `Notification created for ${notifications.length} recipient(s)` + }, { status: 201 }); + + } catch (error) { + console.error('Error creating notification:', error); + return NextResponse.json({ + error: 'Internal server error', + details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined + }, { status: 500 }); + } +} diff --git a/src/app/api/notifications/stream/route.ts b/src/app/api/notifications/stream/route.ts new file mode 100644 index 000000000..4b7b1be6e --- /dev/null +++ b/src/app/api/notifications/stream/route.ts @@ -0,0 +1,9 @@ +// app/api/notifications/stream/route.ts +import { NextResponse } from 'next/server'; + +export async function GET() { + return NextResponse.redirect( + new URL('/api/notifications/fluxo', 'https://portal.fatosdabolsa.com.br'), + 308 + ); +} diff --git a/src/app/api/proventos/[ticker]/route.ts b/src/app/api/proventos/[ticker]/route.ts new file mode 100644 index 000000000..e07fbf643 --- /dev/null +++ b/src/app/api/proventos/[ticker]/route.ts @@ -0,0 +1,59 @@ +// 📠/src/app/api/proventos/[ticker]/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { CacheService } from '@/lib/redis'; +import { logApiMetric } from '@/lib/metrics'; + +export const maxDuration = 5; + +export async function GET( + request: NextRequest, + { params }: { params: { ticker: string } } +) { + const startedAt = Date.now(); + + try { + const { ticker } = params; + + if (!ticker) { + return NextResponse.json( + { error: 'Ticker inválido' }, + { status: 400 } + ); + } + + const cacheKey = CacheService.getCacheKey('proventos', ticker.toUpperCase()); + + const { data, metadata } = await CacheService.withCache( + cacheKey, + async () => + prisma.provento.findMany({ + where: { + ticker: ticker.toUpperCase() + }, + orderBy: { dataObj: 'desc' } + }), + { + ttlSeconds: 3600, + tags: ['proventos', ticker.toUpperCase()], + } + ); + + logApiMetric({ + route: '/api/proventos/[ticker]', + cacheHit: metadata.cached, + cacheBackend: metadata.backend, + durationMs: Date.now() - startedAt, + status: 200, + extra: { ticker: ticker.toUpperCase() }, + }); + + return NextResponse.json(data); + } catch (error) { + console.error(`Erro ao buscar proventos:`, error); + return NextResponse.json( + { error: 'Erro interno do servidor' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/proventos/estatisticas/route.ts b/src/app/api/proventos/estatisticas/route.ts new file mode 100644 index 000000000..670e22298 --- /dev/null +++ b/src/app/api/proventos/estatisticas/route.ts @@ -0,0 +1,81 @@ +// 📠/src/app/api/proventos/estatisticas/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +export async function GET() { + try { + console.log('📊 Buscando estatísticas de proventos...'); + + // 📊 BUSCAR ESTATÃSTICAS USANDO OS MODELOS CORRETOS + const [ + totalProventos, + empresasUnicas, + valorTotal, + ultimoUpload + ] = await Promise.all([ + // Total de proventos + prisma.provento.count(), + + // Empresas únicas (usando distinct corretamente) + prisma.provento.findMany({ + select: { ticker: true }, + distinct: ['ticker'] + }), + + // Valor total + prisma.provento.aggregate({ + _sum: { valor: true } + }), + + // Último upload (usando ProventoUpload com P maiúsculo) + prisma.proventoUpload.findFirst({ + orderBy: { createdAt: 'desc' }, + select: { createdAt: true } + }) + ]); + + const estatisticas = { + totalEmpresas: empresasUnicas.length, + totalProventos: totalProventos, + valorTotal: valorTotal._sum.valor || 0, + dataUltimoUpload: ultimoUpload?.createdAt?.toISOString() || '' + }; + + console.log('✅ Estatísticas calculadas:', estatisticas); + + return NextResponse.json(estatisticas); + + } catch (error) { + console.error('⌠Erro ao buscar estatísticas:', error); + + // Em caso de erro, retornar estatísticas básicas sem usar ProventoUpload + try { + const [totalProventos, empresasUnicas, valorTotal] = await Promise.all([ + prisma.provento.count(), + prisma.provento.findMany({ + select: { ticker: true }, + distinct: ['ticker'] + }), + prisma.provento.aggregate({ + _sum: { valor: true } + }) + ]); + + return NextResponse.json({ + totalEmpresas: empresasUnicas.length, + totalProventos: totalProventos, + valorTotal: valorTotal._sum.valor || 0, + dataUltimoUpload: '' + }); + } catch (fallbackError) { + console.error('⌠Erro no fallback:', fallbackError); + return NextResponse.json( + { + error: 'Erro interno do servidor', + details: error.message + }, + { status: 500 } + ); + } + } +} \ No newline at end of file diff --git a/src/app/api/proventos/route.ts b/src/app/api/proventos/route.ts new file mode 100644 index 000000000..8ddfab5e6 --- /dev/null +++ b/src/app/api/proventos/route.ts @@ -0,0 +1,516 @@ +// 📠/src/app/api/proventos/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { prisma } from '@/lib/prisma'; +import { CacheService } from '@/lib/redis'; +import { enforceRateLimit } from '@/lib/rate-limit'; + +type ProventoPayload = { + ticker: string; + valor: number; + tipo: string; + data: string; + dataObj: string; + dataCom?: string | null; + dataPagamento?: string | null; + dataFormatada: string; + valorFormatado: string; + dividendYield?: number | null; + fonte?: string | null; +}; + +const proventoSchema = z.object({ + ticker: z.string().min(1), + valor: z.number().finite(), + tipo: z.string().min(1), + data: z.string().min(1), + dataObj: z.string().min(1), + dataCom: z.string().nullable().optional(), + dataPagamento: z.string().nullable().optional(), + dataFormatada: z.string().min(1), + valorFormatado: z.string().min(1), + dividendYield: z.number().nullable().optional(), + fonte: z.string().nullable().optional(), +}); + +const proventosUploadSchema = z.object({ + proventos: z.array(proventoSchema).min(1), +}); + +type UploadJobState = { + id: string; + status: 'pending' | 'running' | 'completed' | 'failed'; + total: number; + inserted: number; + failed: number; + startedAt: number; + finishedAt?: number; + error?: string; +}; + +type StatsJobState = { + id: string; + status: 'pending' | 'running' | 'completed' | 'failed'; + totalTickers: number; + processed: number; + failed: number; + startedAt: number; + finishedAt?: number; + error?: string; +}; + +type DeleteJobState = { + id: string; + status: 'pending' | 'running' | 'completed' | 'failed'; + total: number; + deleted: number; + startedAt: number; + finishedAt?: number; + error?: string; +}; + +const PROVENTOS_CACHE_TAG = 'proventos:list'; +const PROVENTOS_STATS_CACHE_TAG = 'proventos:stats'; +const UPLOAD_JOB_TTL_SECONDS = 60 * 60; // 1 hour +const STATS_JOB_TTL_SECONDS = 60 * 60; // 1 hour +const DELETE_JOB_TTL_SECONDS = 60 * 60; // 1 hour + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const page = Math.max(parseInt(searchParams.get('page') || '1', 10), 1); + const limitParam = parseInt(searchParams.get('limit') || '', 10); + const pageSizeParam = parseInt(searchParams.get('pageSize') || '', 10); + const resolvedPageSize = Number.isFinite(limitParam) + ? limitParam + : Number.isFinite(pageSizeParam) + ? pageSizeParam + : 20; + const pageSize = Math.min(Math.max(resolvedPageSize, 1), 200); + const ticker = searchParams.get('ticker') || undefined; + const tipo = searchParams.get('tipo') || undefined; + const origem = searchParams.get('origem') || undefined; + const startDate = searchParams.get('startDate'); + const endDate = searchParams.get('endDate'); + const orderDirection = searchParams.get('order') === 'asc' ? 'asc' : 'desc'; + + const where = { + ...(ticker ? { ticker } : {}), + ...(tipo ? { tipo } : {}), + ...(origem ? { origem } : {}), + ...(startDate || endDate + ? { + dataObj: { + ...(startDate ? { gte: new Date(startDate) } : {}), + ...(endDate ? { lte: new Date(endDate) } : {}), + }, + } + : {}), + }; + + const cacheKey = CacheService.getCacheKey( + 'proventos', + 'list', + page, + pageSize, + ticker, + tipo, + startDate, + endDate, + orderDirection, + origem + ); + + const { data } = await CacheService.withCache( + cacheKey, + async () => { + const [items, total] = await Promise.all([ + prisma.provento.findMany({ + where, + orderBy: { dataObj: orderDirection }, + skip: (page - 1) * pageSize, + take: pageSize, + select: { + id: true, + ticker: true, + valor: true, + tipo: true, + data: true, + dataObj: true, + dataCom: true, + dataPagamento: true, + dataFormatada: true, + valorFormatado: true, + dividendYield: true, + fonte: true, + origem: true, + observacoes: true, + }, + }), + prisma.provento.count({ where }), + ]); + + return { + items, + total, + }; + }, + { + ttlSeconds: 120, + tags: [PROVENTOS_CACHE_TAG], + logLabel: 'proventos:list', + } + ); + + return NextResponse.json( + { + data: data.items, + pagination: { + page, + pageSize, + total: data.total, + totalPages: Math.ceil(data.total / pageSize), + }, + }, + { + headers: { + 'Cache-Control': 'public, s-maxage=120, stale-while-revalidate=60', + }, + } + ); + } catch (error) { + console.error('Erro ao buscar proventos:', error); + return NextResponse.json( + { error: 'Erro interno do servidor' }, + { status: 500 } + ); + } +} + +export async function POST(request: NextRequest) { + try { + const rateLimitKey = `proventos:upload:${request.ip ?? 'unknown'}`; + const rateLimit = await enforceRateLimit(rateLimitKey, 5, 60_000); + if (!rateLimit.allowed) { + return NextResponse.json( + { + error: 'Muitas requisições. Tente novamente em instantes.', + }, + { + status: 429, + headers: rateLimit.retryAfter + ? { 'Retry-After': rateLimit.retryAfter.toString() } + : undefined, + } + ); + } + + const body = await request.json(); + const parseResult = proventosUploadSchema.safeParse(body); + + if (!parseResult.success) { + return NextResponse.json( + { error: 'Payload inválido', issues: parseResult.error.issues }, + { status: 400 } + ); + } + + const { proventos } = parseResult.data; + + console.log(`🚀 Iniciando upload de ${proventos.length} proventos`); + + const uploadJobId = `proventos-upload-${Date.now()}`; + const uploadJob: UploadJobState = { + id: uploadJobId, + status: 'running', + total: proventos.length, + inserted: 0, + failed: 0, + startedAt: Date.now(), + }; + await CacheService.set(uploadJobId, uploadJob, UPLOAD_JOB_TTL_SECONDS); + + const BATCH_SIZE = 200; + const MAX_CONCURRENCY = 3; + const batches: ProventoPayload[][] = []; + for (let i = 0; i < proventos.length; i += BATCH_SIZE) { + batches.push(proventos.slice(i, i + BATCH_SIZE)); + } + + for (let i = 0; i < batches.length; i += MAX_CONCURRENCY) { + const batchSlice = batches.slice(i, i + MAX_CONCURRENCY); + const results = await Promise.allSettled( + batchSlice.map(async (batch) => { + const resultado = await prisma.provento.createMany({ + data: batch.map((p) => ({ + ticker: p.ticker, + valor: p.valor, + tipo: p.tipo, + data: p.data, + dataObj: new Date(p.dataObj), + dataCom: p.dataCom ?? null, + dataPagamento: p.dataPagamento ?? null, + dataFormatada: p.dataFormatada, + valorFormatado: p.valorFormatado, + dividendYield: p.dividendYield ?? null, + fonte: p.fonte ?? 'csv_upload', + })), + skipDuplicates: true, + }); + return { inserted: resultado.count, batchSize: batch.length }; + }) + ); + + results.forEach((result, idx) => { + const currentBatchSize = batchSlice[idx]?.length ?? 0; + + if (result.status === 'fulfilled') { + uploadJob.inserted += result.value.inserted; + uploadJob.failed += result.value.batchSize - result.value.inserted; + } else { + uploadJob.failed += currentBatchSize; + console.error('⌠Erro ao processar lote:', result.reason); + } + }); + + await CacheService.set(uploadJobId, uploadJob, UPLOAD_JOB_TTL_SECONDS); + } + + uploadJob.status = 'completed'; + uploadJob.finishedAt = Date.now(); + await CacheService.set(uploadJobId, uploadJob, UPLOAD_JOB_TTL_SECONDS); + + await CacheService.invalidateTags([PROVENTOS_CACHE_TAG, PROVENTOS_STATS_CACHE_TAG]); + + const statsJob = await startEstatisticasJob(); + + return NextResponse.json({ + message: 'Upload iniciado com sucesso', + totalInseridos: uploadJob.inserted, + totalErros: uploadJob.failed, + uploadJobId, + statsJobId: statsJob.id, + }); + } catch (error) { + console.error('Erro ao processar upload de proventos:', error); + return NextResponse.json( + { error: 'Erro interno do servidor' }, + { status: 500 } + ); + } +} + +export async function DELETE() { + try { + const totalAntes = await prisma.provento.count(); + + const resultado = await prisma.$transaction(async (tx) => { + const deleteResult = await tx.provento.deleteMany({}); + await tx.proventoEstatistica.deleteMany({}); + return deleteResult; + }); + + await CacheService.invalidateTags([PROVENTOS_CACHE_TAG, PROVENTOS_STATS_CACHE_TAG]); + + return NextResponse.json({ + message: 'Todos os proventos foram removidos com sucesso', + totalRemovido: resultado.count, + totalAntes, + }); + } catch (error) { + console.error('Erro ao deletar proventos:', error); + return NextResponse.json( + { error: 'Erro interno do servidor' }, + { status: 500 } + ); + } +} + +async function mergeJobState( + jobId: string, + ttlSeconds: number, + partial: Partial | ((state: T) => Partial) +) { + const existing = await CacheService.get(jobId); + if (!existing) return; + + const next = { + ...existing, + ...(typeof partial === 'function' ? partial(existing) : partial), + } as T; + + await CacheService.set(jobId, next, ttlSeconds); +} + +async function startEstatisticasJob(): Promise { + const jobId = `proventos-stats-${Date.now()}`; + const jobState: StatsJobState = { + id: jobId, + status: 'pending', + totalTickers: 0, + processed: 0, + failed: 0, + startedAt: Date.now(), + }; + + await CacheService.set(jobId, jobState, STATS_JOB_TTL_SECONDS); + + setImmediate(() => { + rebuildEstatisticas(jobId).catch((error) => { + console.error('⌠Erro ao processar estatísticas:', error); + }); + }); + + return jobState; +} + +async function rebuildEstatisticas(jobId: string) { + try { + await mergeJobState(jobId, STATS_JOB_TTL_SECONDS, { status: 'running' }); + + const tickers = await prisma.provento.findMany({ + select: { ticker: true }, + distinct: ['ticker'], + }); + + await mergeJobState(jobId, STATS_JOB_TTL_SECONDS, { totalTickers: tickers.length }); + + const TICKER_BATCH_SIZE = 10; + for (let i = 0; i < tickers.length; i += TICKER_BATCH_SIZE) { + const tickerBatch = tickers.slice(i, i + TICKER_BATCH_SIZE); + + const results = await Promise.allSettled( + tickerBatch.map(async ({ ticker }) => { + const stats = await prisma.provento.aggregate({ + where: { ticker }, + _count: { id: true }, + _sum: { valor: true }, + _avg: { valor: true }, + _min: { dataObj: true }, + _max: { dataObj: true }, + }); + + await prisma.proventoEstatistica.upsert({ + where: { ticker }, + create: { + ticker, + totalProventos: stats._count.id, + valorTotal: stats._sum.valor || 0, + valorMedio: stats._avg.valor || 0, + primeiroProvento: stats._min.dataObj, + ultimoProvento: stats._max.dataObj, + }, + update: { + totalProventos: stats._count.id, + valorTotal: stats._sum.valor || 0, + valorMedio: stats._avg.valor || 0, + primeiroProvento: stats._min.dataObj, + ultimoProvento: stats._max.dataObj, + ultimaAtualizacao: new Date(), + }, + }); + }) + ); + + const processedSuccess = results.filter((result) => result.status === 'fulfilled').length; + const processedFailed = results.length - processedSuccess; + + await mergeJobState(jobId, STATS_JOB_TTL_SECONDS, (state) => ({ + processed: state.processed + processedSuccess, + failed: state.failed + processedFailed, + })); + } + + await CacheService.invalidateTags([PROVENTOS_STATS_CACHE_TAG, PROVENTOS_CACHE_TAG]); + + await mergeJobState(jobId, STATS_JOB_TTL_SECONDS, { + status: 'completed', + finishedAt: Date.now(), + }); + } catch (error) { + await mergeJobState(jobId, STATS_JOB_TTL_SECONDS, { + status: 'failed', + error: (error as Error).message, + finishedAt: Date.now(), + }); + throw error; + } +} + +async function startDeleteJob(): Promise { + const jobId = `proventos-delete-${Date.now()}`; + const jobState: DeleteJobState = { + id: jobId, + status: 'pending', + total: 0, + deleted: 0, + startedAt: Date.now(), + }; + + await CacheService.set(jobId, jobState, DELETE_JOB_TTL_SECONDS); + + setImmediate(() => { + processDeleteJob(jobId).catch((error) => { + console.error('⌠Erro no job de limpeza:', error); + }); + }); + + return jobState; +} + +async function processDeleteJob(jobId: string) { + try { + await mergeJobState(jobId, DELETE_JOB_TTL_SECONDS, { status: 'running' }); + + const BATCH_SIZE = 1000; + let totalRemovidos = 0; + + while (true) { + const idsBatch = await prisma.provento.findMany({ + select: { id: true }, + orderBy: { id: 'asc' }, + take: BATCH_SIZE, + }); + + if (idsBatch.length === 0) { + break; + } + + const ids = idsBatch.map((item) => item.id); + const resultado = await prisma.provento.deleteMany({ + where: { + id: { + in: ids, + }, + }, + }); + + totalRemovidos += resultado.count; + + await mergeJobState(jobId, DELETE_JOB_TTL_SECONDS, { + total: totalRemovidos, + deleted: totalRemovidos, + }); + + if (idsBatch.length < BATCH_SIZE) { + break; + } + } + + await prisma.proventoEstatistica.deleteMany({}); + + await CacheService.invalidateTags([PROVENTOS_CACHE_TAG, PROVENTOS_STATS_CACHE_TAG]); + + await mergeJobState(jobId, DELETE_JOB_TTL_SECONDS, { + status: 'completed', + finishedAt: Date.now(), + }); + } catch (error) { + await mergeJobState(jobId, DELETE_JOB_TTL_SECONDS, { + status: 'failed', + error: (error as Error).message, + finishedAt: Date.now(), + }); + throw error; + } +} diff --git a/src/app/api/proventos/sync/[syncId]/route.ts b/src/app/api/proventos/sync/[syncId]/route.ts new file mode 100644 index 000000000..cf26e811a --- /dev/null +++ b/src/app/api/proventos/sync/[syncId]/route.ts @@ -0,0 +1,154 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; +import { CacheService } from '@/lib/redis'; + +const PROGRESS_KEY_PREFIX = 'sync:progress:'; + +export async function GET( + _request: NextRequest, + { params }: { params: { syncId: string } } +) { + try { + const session = await auth(); + + if (!session) { + return NextResponse.json({ error: 'Não autenticado' }, { status: 401 }); + } + + if (session.user.plan !== 'ADMIN' && session.user.role !== 'ADMIN') { + return NextResponse.json({ error: 'Acesso negado' }, { status: 403 }); + } + + const syncId = Number(params.syncId); + + if (!Number.isInteger(syncId)) { + return NextResponse.json({ error: 'ID de sincronização inválido' }, { status: 400 }); + } + + const progressCacheKey = `${PROGRESS_KEY_PREFIX}${syncId}`; + const progressCache = await CacheService.get<{ + syncId: number; + ticker: string; + progresso: number; + total: number; + status: string; + totalNovos?: number; + totalAtualizados?: number; + } | string>(progressCacheKey); + + if (progressCache) { + try { + const progress = + typeof progressCache === 'string' ? JSON.parse(progressCache) : progressCache; + + const porcentagem = progress.total > 0 + ? Math.round((progress.progresso / progress.total) * 100) + : 0; + + return NextResponse.json({ + id: syncId, + status: 'EM_PROGRESSO', + progresso: progress.progresso, + total: progress.total, + porcentagem, + tickerAtual: progress.ticker, + totalNovos: progress.totalNovos || 0, + totalAtualizados: progress.totalAtualizados || 0, + message: `Processando ${progress.progresso} de ${progress.total} tickers...`, + }); + } catch (parseError) { + console.error('Erro ao parsear cache de progresso:', parseError); + } + } + + const sync = await prisma.proventoSincronizacao.findUnique({ + where: { id: syncId }, + }); + + if (!sync) { + return NextResponse.json({ error: 'Sincronização não encontrada' }, { status: 404 }); + } + + const duracaoMs = sync.dataFim && sync.dataInicio + ? sync.dataFim.getTime() - sync.dataInicio.getTime() + : null; + const duracaoSegundos = duracaoMs ? Math.round(duracaoMs / 1000) : null; + + const porcentagem = sync.status === 'CONCLUIDO' + ? 100 + : sync.status === 'ERRO' + ? sync.totalProcessados + sync.totalErros > 0 + ? Math.round((sync.totalProcessados / (sync.totalProcessados + sync.totalErros)) * 100) + : 0 + : 0; + + return NextResponse.json({ + id: sync.id, + ticker: sync.ticker || 'Todos', + status: sync.status, + porcentagem, + dataInicio: sync.dataInicio, + dataFim: sync.dataFim, + duracao: duracaoSegundos ? `${duracaoSegundos}s` : null, + metricas: { + totalProcessados: sync.totalProcessados, + totalNovos: sync.totalNovos, + totalAtualizados: sync.totalAtualizados, + totalErros: sync.totalErros, + }, + erros: sync.erros, + message: + sync.status === 'CONCLUIDO' + ? `Sincronização concluída: ${sync.totalNovos} novos, ${sync.totalAtualizados} atualizados` + : sync.status === 'ERRO' + ? 'Sincronização finalizada com erros' + : 'Sincronização em andamento', + }); + } catch (error: any) { + console.error('⌠Erro ao buscar status de sincronização:', error); + + return NextResponse.json( + { + error: 'Erro ao buscar status', + message: error instanceof Error ? error.message : 'Erro desconhecido', + }, + { status: 500 } + ); + } +} + +export async function DELETE( + _request: NextRequest, + { params }: { params: { syncId: string } } +) { + try { + const session = await auth(); + + if (!session || (session.user.plan !== 'ADMIN' && session.user.role !== 'ADMIN')) { + return NextResponse.json({ error: 'Não autorizado' }, { status: 403 }); + } + + const syncId = Number(params.syncId); + + if (!Number.isInteger(syncId)) { + return NextResponse.json({ error: 'ID inválido' }, { status: 400 }); + } + + await CacheService.delete(`${PROGRESS_KEY_PREFIX}${syncId}`); + + await prisma.proventoSincronizacao.update({ + where: { id: syncId }, + data: { + status: 'ERRO', + dataFim: new Date(), + erros: { cancelado: true, message: 'Cancelado pelo usuário' }, + }, + }); + + return NextResponse.json({ message: 'Sincronização cancelada', syncId }); + } catch (error) { + console.error('Erro ao cancelar sincronização:', error); + return NextResponse.json({ error: 'Erro ao cancelar sincronização' }, { status: 500 }); + } +} diff --git a/src/app/api/proventos/sync/historico/route.ts b/src/app/api/proventos/sync/historico/route.ts new file mode 100644 index 000000000..34fec1994 --- /dev/null +++ b/src/app/api/proventos/sync/historico/route.ts @@ -0,0 +1,126 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +const MAX_LIMIT = 100; + +export async function GET(request: NextRequest) { + try { + const session = await auth(); + + if (!session) { + return NextResponse.json({ error: 'Não autenticado' }, { status: 401 }); + } + + if (session.user.plan !== 'ADMIN' && session.user.role !== 'ADMIN') { + return NextResponse.json({ error: 'Acesso negado' }, { status: 403 }); + } + + const { searchParams } = new URL(request.url); + const limitParam = parseInt(searchParams.get('limit') ?? '20', 10); + const offsetParam = parseInt(searchParams.get('offset') ?? '0', 10); + const statusParam = searchParams.get('status'); + + const limit = Math.min(Math.max(limitParam, 1), MAX_LIMIT); + const offset = Math.max(offsetParam, 0); + + const where: { status?: string } = {}; + + if (statusParam && ['EM_PROGRESSO', 'CONCLUIDO', 'ERRO'].includes(statusParam)) { + where.status = statusParam; + } + + const [historico, total] = await Promise.all([ + prisma.proventoSincronizacao.findMany({ + where, + orderBy: { createdAt: 'desc' }, + take: limit, + skip: offset, + select: { + id: true, + ticker: true, + status: true, + dataInicio: true, + dataFim: true, + totalProcessados: true, + totalNovos: true, + totalAtualizados: true, + totalErros: true, + erros: true, + createdAt: true, + }, + }), + prisma.proventoSincronizacao.count({ where }), + ]); + + const historicoEnriquecido = historico.map((sync) => { + const duracaoMs = sync.dataFim && sync.dataInicio ? sync.dataFim.getTime() - sync.dataInicio.getTime() : null; + const duracaoSegundos = duracaoMs ? Math.round(duracaoMs / 1000) : null; + + const total = sync.totalProcessados + sync.totalErros; + const taxaSucesso = total > 0 ? Math.round((sync.totalProcessados / total) * 100) : 0; + + return { + id: sync.id, + ticker: sync.ticker || 'Todos', + status: sync.status, + dataInicio: sync.dataInicio, + dataFim: sync.dataFim, + duracao: duracaoSegundos ? `${duracaoSegundos}s` : null, + metricas: { + totalProcessados: sync.totalProcessados, + totalNovos: sync.totalNovos, + totalAtualizados: sync.totalAtualizados, + totalErros: sync.totalErros, + taxaSucesso: `${taxaSucesso}%`, + }, + temErros: sync.totalErros > 0, + erros: sync.totalErros > 0 ? sync.erros : null, + }; + }); + + const estatisticas = await prisma.proventoSincronizacao.aggregate({ + _count: { id: true }, + _sum: { + totalNovos: true, + totalAtualizados: true, + totalErros: true, + }, + }); + + const ultimaSincronizacao = historico[0] || null; + + return NextResponse.json({ + historico: historicoEnriquecido, + paginacao: { + total, + limit, + offset, + temMais: offset + limit < total, + }, + estatisticas: { + totalSincronizacoes: estatisticas._count.id, + totalProventosNovos: estatisticas._sum.totalNovos || 0, + totalProventosAtualizados: estatisticas._sum.totalAtualizados || 0, + totalErros: estatisticas._sum.totalErros || 0, + }, + ultimaSincronizacao: ultimaSincronizacao + ? { + id: ultimaSincronizacao.id, + dataInicio: ultimaSincronizacao.dataInicio, + status: ultimaSincronizacao.status, + } + : null, + }); + } catch (error) { + console.error('⌠Erro ao buscar histórico:', error); + + return NextResponse.json( + { + error: 'Erro ao buscar histórico', + message: error instanceof Error ? error.message : 'Erro desconhecido', + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/proventos/sync/route.ts b/src/app/api/proventos/sync/route.ts new file mode 100644 index 000000000..dbde47a02 --- /dev/null +++ b/src/app/api/proventos/sync/route.ts @@ -0,0 +1,122 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { auth } from '@/lib/auth'; +import { CacheService } from '@/lib/redis'; +import { syncPayloadSchema } from '@/lib/validations/proventos-sync'; +import { sincronizarProventos } from '@/workers/proventos-sync-worker'; + +const RATE_LIMIT_WINDOW_SECONDS = 300; // 5 minutes + +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session) { + return NextResponse.json({ error: 'Não autenticado' }, { status: 401 }); + } + + if (session.user.plan !== 'ADMIN') { + return NextResponse.json( + { error: 'Acesso negado. Apenas administradores podem sincronizar.' }, + { status: 403 } + ); + } + + const body = await request.json(); + const config = syncPayloadSchema.parse(body); + + const syncConfig = { + tickers: config.tickers, + dataInicio: config.dataInicio ? new Date(config.dataInicio) : undefined, + dataFim: config.dataFim ? new Date(config.dataFim) : undefined, + forcarAtualizacao: config.forcarAtualizacao ?? false, + }; + + const ip = + request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || + request.headers.get('x-real-ip') || + 'unknown'; + + const rateLimitKey = CacheService.getCacheKey('ratelimit', 'sync', ip); + const existingSync = await CacheService.get(rateLimitKey); + + if (existingSync) { + return NextResponse.json( + { + error: 'Aguarde 5 minutos antes de iniciar nova sincronização', + retryAfter: RATE_LIMIT_WINDOW_SECONDS, + }, + { status: 429 } + ); + } + + await CacheService.set(rateLimitKey, '1', RATE_LIMIT_WINDOW_SECONDS); + + console.log(`🔄 Sincronização iniciada por: ${session.user.email || session.user.name}`); + console.log('📋 Config:', syncConfig); + + sincronizarProventos(syncConfig) + .then((resultado) => { + console.log('✅ Sincronização concluída:', resultado); + }) + .catch((error) => { + console.error('⌠Erro na sincronização:', error); + }); + + return NextResponse.json( + { + message: 'Sincronização iniciada com sucesso', + status: 'EM_PROGRESSO', + config: { + tickers: syncConfig.tickers || 'Todos', + dataInicio: syncConfig.dataInicio, + dataFim: syncConfig.dataFim, + forcarAtualizacao: syncConfig.forcarAtualizacao, + }, + }, + { status: 202 } + ); + } catch (error) { + console.error('⌠Erro ao processar requisição de sincronização:', error); + + if (error instanceof z.ZodError) { + return NextResponse.json( + { + error: 'Dados inválidos', + details: error.errors, + }, + { status: 400 } + ); + } + + return NextResponse.json( + { + error: 'Erro ao iniciar sincronização', + message: error instanceof Error ? error.message : 'Erro desconhecido', + }, + { status: 500 } + ); + } +} + +export async function GET(request: NextRequest) { + try { + const session = await auth(); + + if (!session || (session.user.plan !== 'ADMIN' && session.user.role !== 'ADMIN')) { + return NextResponse.json({ error: 'Não autorizado' }, { status: 403 }); + } + + return NextResponse.json({ + service: 'Sincronização de Proventos BRAPI', + status: 'online', + endpoints: { + sync: 'POST /api/proventos/sync', + status: 'GET /api/proventos/sync/[syncId]', + historico: 'GET /api/proventos/sync/historico', + }, + }); + } catch (error) { + return NextResponse.json({ error: 'Erro ao verificar status' }, { status: 500 }); + } +} diff --git a/src/app/api/proventos/upload/route.ts b/src/app/api/proventos/upload/route.ts new file mode 100644 index 000000000..bee52c646 --- /dev/null +++ b/src/app/api/proventos/upload/route.ts @@ -0,0 +1,136 @@ +// 📠/src/app/api/proventos/upload/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { CacheService } from '@/lib/redis'; + +const PROVENTOS_CACHE_TAG = 'proventos:list'; +const PROVENTOS_STATS_CACHE_TAG = 'proventos:stats'; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { + nomeArquivo, + tamanhoArquivo, + totalLinhas, + linhasProcessadas, + linhasComErro, + proventos + } = body; + + // 📊 REGISTRAR LOG DO UPLOAD + const upload = await prisma.proventoUpload.create({ + data: { + nomeArquivo, + tamanhoArquivo, + totalLinhas, + linhasProcessadas, + linhasComErro, + iniciadoEm: new Date(), + // userId: req.user?.id, // Se tiver autenticação + } + }); + + // 📤 PROCESSAR PROVENTOS EM LOTES + let totalInseridos = 0; + let totalAtualizados = 0; + let totalSobrescritosBRAPI = 0; + + for (const provento of proventos) { + try { + const compositeKey = { + ticker: provento.ticker, + dataPagamento: provento.dataPagamento ?? null, + tipo: provento.tipo, + }; + + const existente = await prisma.provento.findUnique({ + where: { unique_provento_ticker_dataPagamento_tipo: compositeKey }, + }); + + await prisma.provento.upsert({ + where: { unique_provento_ticker_dataPagamento_tipo: compositeKey }, + update: { + valor: provento.valor, + dataPagamento: provento.dataPagamento ?? null, + observacoes: provento.descricao ?? provento.observacoes, + origem: 'CSV', + dataUltimaAtualizacao: new Date(), + data: provento.data, + dataObj: new Date(provento.dataObj), + dataFormatada: provento.dataFormatada, + valorFormatado: provento.valorFormatado, + dividendYield: provento.dividendYield, + fonte: 'csv_upload', + }, + create: { + ticker: provento.ticker, + valor: provento.valor, + tipo: provento.tipo, + data: provento.data, + dataObj: new Date(provento.dataObj), + dataCom: provento.dataCom ?? null, + dataPagamento: provento.dataPagamento ?? null, + dataFormatada: provento.dataFormatada, + valorFormatado: provento.valorFormatado, + dividendYield: provento.dividendYield, + fonte: 'csv_upload', + observacoes: provento.descricao ?? provento.observacoes, + origem: 'CSV', + dataUltimaAtualizacao: new Date(), + }, + }); + + if (existente) { + totalAtualizados++; + + if (existente.origem === 'BRAPI') { + totalSobrescritosBRAPI++; + console.log( + `âš ï¸ CSV sobrescreveu provento BRAPI: ${provento.ticker} - ${provento.dataPagamento} - ${provento.tipo}` + ); + } + } else { + totalInseridos++; + } + } catch (error) { + console.error('Erro ao processar provento:', error); + } + } + + const totalProcessados = proventos.length; + + // ✅ FINALIZAR LOG DO UPLOAD + await prisma.proventoUpload.update({ + where: { id: upload.id }, + data: { + finalizadoEm: new Date(), + proventosCriados: totalInseridos, + proventosAtualizados: totalAtualizados, + }, + }); + + await CacheService.invalidateTags([PROVENTOS_CACHE_TAG, PROVENTOS_STATS_CACHE_TAG]); + + console.log( + `Upload concluído: ${totalInseridos} inseridos, ${totalAtualizados} atualizados (sobrescritos BRAPI: ${totalSobrescritosBRAPI})` + ); + + return NextResponse.json({ + message: 'Upload concluído com sucesso', + estatisticas: { + totalProcessados, + totalInseridos, + totalAtualizados, + totalSobrescritosBRAPI, + }, + uploadId: upload.id, + }); + } catch (error) { + console.error('Erro no upload:', error); + return NextResponse.json( + { error: 'Erro interno do servidor' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/questions/[id]/answers/route.ts b/src/app/api/questions/[id]/answers/route.ts new file mode 100644 index 000000000..3b090f605 --- /dev/null +++ b/src/app/api/questions/[id]/answers/route.ts @@ -0,0 +1,265 @@ +// src/app/api/questions/[id]/answers/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { notifyQuestionAnswered } from '@/utils/notifications'; +import { auth } from '@/lib/auth'; + +// POST - Criar resposta (só admin) +export async function POST( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + // Verificar se é admin + if (user.plan !== 'ADMIN' && user.plan !== 'ANALISTA') { + return NextResponse.json({ error: 'Only admins can create answers' }, { status: 403 }); + } + + // Verificar se a pergunta existe + const question = await prisma.question.findUnique({ + where: { id: params.id }, + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + } + }); + + if (!question) { + return NextResponse.json({ error: 'Question not found' }, { status: 404 }); + } + + const body = await request.json(); + const { content, isOfficial = true } = body; + + // Validações + if (!content) { + return NextResponse.json({ + error: 'Content is required' + }, { status: 400 }); + } + + if (typeof content !== 'string') { + return NextResponse.json({ + error: 'Content must be a string' + }, { status: 400 }); + } + + if (content.trim().length === 0) { + return NextResponse.json({ + error: 'Content cannot be empty' + }, { status: 400 }); + } + + if (content.length > 5000) { + return NextResponse.json({ + error: 'Content must be less than 5000 characters' + }, { status: 400 }); + } + + // Criar resposta + const answer = await prisma.answer.create({ + data: { + content: content.trim(), + questionId: params.id, + adminId: user.id, + isOfficial: Boolean(isOfficial) + }, + include: { + admin: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + } + }); + + // Atualizar status da pergunta para RESPONDIDA + await prisma.question.update({ + where: { id: params.id }, + data: { + status: 'RESPONDIDA' + } + }); + + // Dispara notificação para o usuário (APÓS atualizar status) + try { + const adminName = `${user.firstName} ${user.lastName}`; + await notifyQuestionAnswered(params.id, adminName); + } catch (notificationError) { + console.error('Erro ao enviar notificação:', notificationError); + // Não falha a resposta se a notificação falhar + } + + return NextResponse.json({ + answer, + message: 'Answer created successfully' + }, { status: 201 }); + + } catch (error) { + console.error('Error creating answer:', error); + + // Tratamento específico de erros do Prisma + if (error && typeof error === 'object' && 'code' in error) { + const prismaError = error as any; + + if (prismaError.code === 'P2025') { + return NextResponse.json({ + error: 'Question not found' + }, { status: 404 }); + } + + if (prismaError.code === 'P2003') { + return NextResponse.json({ + error: 'Invalid question or admin reference' + }, { status: 400 }); + } + } + + return NextResponse.json({ + error: 'Internal server error', + details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined + }, { status: 500 }); + } +} + + +// GET - Listar respostas de uma pergunta +export async function GET( + _request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + // Verificar se a pergunta existe + const question = await prisma.question.findUnique({ + where: { id: params.id } + }); + + if (!question) { + return NextResponse.json({ error: 'Question not found' }, { status: 404 }); + } + + // Verificar permissão + const isAdmin = user.plan === 'ADMIN' || user.plan === 'ANALISTA'; + const isOwner = question.userId === user.id; + + if (!isAdmin && !isOwner) { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + // Buscar respostas + const answers = await prisma.answer.findMany({ + where: { questionId: params.id }, + include: { + admin: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + }, + comments: { + include: { + author: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + }, + orderBy: { + createdAt: 'asc' + } + } + }, + orderBy: { + createdAt: 'asc' + } + }); + + // Marcar como lidas pelo usuário se for o dono + if (isOwner && !isAdmin) { + const unreadAnswerIds = answers + .filter(answer => !answer.readByUser) + .map(answer => answer.id); + + if (unreadAnswerIds.length > 0) { + await prisma.answer.updateMany({ + where: { + id: { in: unreadAnswerIds } + }, + data: { + readByUser: true + } + }); + + // Atualizar o array local para refletir mudança + answers.forEach(answer => { + if (unreadAnswerIds.includes(answer.id)) { + answer.readByUser = true; + } + }); + } + } + + return NextResponse.json({ answers }); + + } catch (error) { + console.error('Error fetching answers:', error); + + // Tratamento específico de erros do Prisma + if (error && typeof error === 'object' && 'code' in error) { + const prismaError = error as any; + + if (prismaError.code === 'P2025') { + return NextResponse.json({ + error: 'Question not found' + }, { status: 404 }); + } + } + + return NextResponse.json({ + error: 'Internal server error', + details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined + }, { status: 500 }); + } +} diff --git a/src/app/api/questions/[id]/comments/[commentId]/route.ts b/src/app/api/questions/[id]/comments/[commentId]/route.ts new file mode 100644 index 000000000..57c723025 --- /dev/null +++ b/src/app/api/questions/[id]/comments/[commentId]/route.ts @@ -0,0 +1,43 @@ +// src/app/api/questions/[id]/comments/[commentId]/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +// DELETE - Remover comentário +export async function DELETE( + _request: NextRequest, + { params }: { params: { id: string; commentId: string } } +) { + try { + const session = await auth(); + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id } + }); + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const comment = await prisma.comment.findUnique({ + where: { id: params.commentId } + }); + if (!comment || comment.questionId !== params.id) { + return NextResponse.json({ error: 'Comment not found' }, { status: 404 }); + } + + const isAdmin = user.plan === 'ADMIN' || user.plan === 'ANALISTA'; + const isAuthor = comment.authorId === user.id; + if (!isAdmin && !isAuthor) { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + await prisma.comment.delete({ where: { id: params.commentId } }); + return NextResponse.json({ message: 'Comment deleted' }); + } catch (error) { + console.error('Error deleting comment:', error); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +} diff --git a/src/app/api/questions/[id]/comments/route.ts b/src/app/api/questions/[id]/comments/route.ts new file mode 100644 index 000000000..6250416ac --- /dev/null +++ b/src/app/api/questions/[id]/comments/route.ts @@ -0,0 +1,76 @@ +// src/app/api/questions/[id]/comments/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +// POST - Criar comentário na pergunta ou resposta +export async function POST( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const session = await auth(); + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id } + }); + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const question = await prisma.question.findUnique({ + where: { id: params.id } + }); + if (!question) { + return NextResponse.json({ error: 'Question not found' }, { status: 404 }); + } + + const isAdmin = user.plan === 'ADMIN' || user.plan === 'ANALISTA'; + if (!isAdmin) { + console.warn( + `Unauthorized comment creation attempt by user ${user.id} on question ${params.id}` + ); + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + const body = await request.json(); + const { content, answerId } = body; + + if (!content || typeof content !== 'string' || content.trim().length === 0) { + return NextResponse.json({ error: 'Content is required' }, { status: 400 }); + } + + if (content.length > 1000) { + return NextResponse.json({ error: 'Content must be less than 1000 characters' }, { status: 400 }); + } + + if (answerId) { + const answer = await prisma.answer.findUnique({ where: { id: answerId } }); + if (!answer || answer.questionId !== params.id) { + return NextResponse.json({ error: 'Invalid answer reference' }, { status: 400 }); + } + } + + const comment = await prisma.comment.create({ + data: { + content: content.trim(), + questionId: params.id, + answerId: answerId || undefined, + authorId: user.id + }, + include: { + author: { + select: { id: true, firstName: true, lastName: true, email: true } + } + } + }); + + return NextResponse.json({ comment }, { status: 201 }); + } catch (error) { + console.error('Error creating comment:', error); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +} diff --git a/src/app/api/questions/[id]/route.ts b/src/app/api/questions/[id]/route.ts new file mode 100644 index 000000000..c8bc16e0e --- /dev/null +++ b/src/app/api/questions/[id]/route.ts @@ -0,0 +1,305 @@ +// src/app/api/questions/[id]/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +// GET - Buscar pergunta específica +export async function GET( + _request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const question = await prisma.question.findUnique({ + where: { id: params.id }, + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true, + plan: true + } + }, + answers: { + include: { + admin: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + }, + comments: { + include: { + author: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + }, + orderBy: { + createdAt: 'asc' + } + } + }, + orderBy: { + createdAt: 'asc' + } + }, + comments: { + include: { + author: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + }, + orderBy: { + createdAt: 'asc' + } + } + } + }); + + if (!question) { + return NextResponse.json({ error: 'Question not found' }, { status: 404 }); + } + + // Verificar permissão: usuário só pode ver suas próprias perguntas ou ser admin + const isAdmin = user.plan === 'ADMIN' || user.plan === 'ANALISTA'; + const isOwner = question.userId === user.id; + + if (!isAdmin && !isOwner) { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + // Marcar respostas como lidas pelo usuário se for o dono + if (isOwner && !isAdmin) { + const unreadAnswerIds = question.answers + .filter(answer => !answer.readByUser) + .map(answer => answer.id); + + if (unreadAnswerIds.length > 0) { + await prisma.answer.updateMany({ + where: { + id: { in: unreadAnswerIds } + }, + data: { + readByUser: true + } + }); + } + } + + return NextResponse.json({ question, isAdmin, isOwner }); + + } catch (error) { + console.error('Error fetching question:', error); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +} + +// PUT - Atualizar pergunta (fechar, alterar status, etc.) +export async function PUT( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const question = await prisma.question.findUnique({ + where: { id: params.id } + }); + + if (!question) { + return NextResponse.json({ error: 'Question not found' }, { status: 404 }); + } + + const body = await request.json(); + const { status, priority } = body; + + const isAdmin = user.plan === 'ADMIN' || user.plan === 'ANALISTA'; + const isOwner = question.userId === user.id; + + // Verificar permissões para diferentes ações + if (status === 'FECHADA') { + // Admin pode fechar qualquer pergunta, usuário só pode fechar suas próprias + if (!isAdmin && !isOwner) { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + } else if (status && status !== 'FECHADA') { + // Só admin pode alterar status para outros valores + if (!isAdmin) { + return NextResponse.json({ error: 'Only admins can change question status' }, { status: 403 }); + } + } + + if (priority && !isAdmin) { + return NextResponse.json({ error: 'Only admins can change priority' }, { status: 403 }); + } + + // Preparar dados para atualização + const updateData: any = {}; + + if (status) { + updateData.status = status; + if (status === 'FECHADA') { + updateData.closedAt = new Date(); + updateData.closedBy = user.id; + } + } + + if (priority) { + updateData.priority = priority; + } + + const updatedQuestion = await prisma.question.update({ + where: { id: params.id }, + data: updateData, + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true, + plan: true + } + }, + answers: { + include: { + admin: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + }, + comments: { + include: { + author: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + }, + orderBy: { + createdAt: 'asc' + } + } + }, + orderBy: { + createdAt: 'asc' + } + }, + comments: { + include: { + author: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + }, + orderBy: { + createdAt: 'asc' + } + } + } + }); + + return NextResponse.json({ question: updatedQuestion }); + + } catch (error) { + console.error('Error updating question:', error); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +} + +// DELETE - Deletar pergunta (só admin ou dono) +export async function DELETE( + _request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const question = await prisma.question.findUnique({ + where: { id: params.id } + }); + + if (!question) { + return NextResponse.json({ error: 'Question not found' }, { status: 404 }); + } + + const isAdmin = user.plan === 'ADMIN' || user.plan === 'ANALISTA'; + const isOwner = question.userId === user.id; + + if (!isAdmin && !isOwner) { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }); + } + + // Deletar pergunta (cascata vai deletar as respostas) + await prisma.question.delete({ + where: { id: params.id } + }); + + return NextResponse.json({ message: 'Question deleted successfully' }); + + } catch (error) { + console.error('Error deleting question:', error); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/questions/route.test.js b/src/app/api/questions/route.test.js new file mode 100644 index 000000000..e3c844b6c --- /dev/null +++ b/src/app/api/questions/route.test.js @@ -0,0 +1,131 @@ +const fs = require('fs'); +const path = require('path'); +const vm = require('vm'); +const ts = require('typescript'); + +// Mocks +const authMock = { auth: jest.fn() }; + +const prismaClientMock = { + user: { findUnique: jest.fn() }, + question: { + findMany: jest.fn(), + count: jest.fn(), + updateMany: jest.fn(), + }, +}; + +function loadRoute() { + const filePath = path.resolve(__dirname, 'route.ts'); + const source = fs.readFileSync(filePath, 'utf8'); + const { outputText } = ts.transpileModule(source, { + compilerOptions: { module: ts.ModuleKind.CommonJS, esModuleInterop: true }, + }); + const wrapper = `(function (exports, require, module, __filename, __dirname) { ${outputText} \n})`; + const compiled = vm.runInThisContext(wrapper, { filename: filePath }); + const module = { exports: {} }; + function customRequire(p) { + if (p === '@/lib/auth') return authMock; + if (p === '@/lib/prisma') return { prisma: prismaClientMock }; + return require(p); + } + compiled(module.exports, customRequire, module, filePath, path.dirname(filePath)); + return module.exports; +} + +describe('questions route pagination', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('uses defaults when page and limit are missing', async () => { + const { GET } = loadRoute(); + + authMock.auth.mockResolvedValue({ user: { id: '1' } }); + prismaClientMock.user.findUnique.mockResolvedValue({ id: '1', plan: 'USER' }); + prismaClientMock.question.findMany.mockResolvedValue([]); + prismaClientMock.question.count.mockResolvedValue(0); + + const req = { url: 'http://localhost/api/questions' }; + const res = await GET(req); + expect(res.status).toBe(200); + expect(prismaClientMock.question.findMany).toHaveBeenCalledWith( + expect.objectContaining({ skip: 0, take: 10 }) + ); + const data = await res.json(); + expect(data.pagination.page).toBe(1); + expect(data.pagination.limit).toBe(10); + }); + + it('defaults when page or limit are malformed', async () => { + const { GET } = loadRoute(); + + authMock.auth.mockResolvedValue({ user: { id: '1' } }); + prismaClientMock.user.findUnique.mockResolvedValue({ id: '1', plan: 'USER' }); + prismaClientMock.question.findMany.mockResolvedValue([]); + prismaClientMock.question.count.mockResolvedValue(0); + + const req = { url: 'http://localhost/api/questions?page=foo&limit=-5' }; + const res = await GET(req); + expect(res.status).toBe(200); + expect(prismaClientMock.question.findMany).toHaveBeenCalledWith( + expect.objectContaining({ skip: 0, take: 10 }) + ); + const data = await res.json(); + expect(data.pagination.page).toBe(1); + expect(data.pagination.limit).toBe(10); + }); +}); + +describe('questions route excludeVirtual filter', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('excludes questions with faq answers when excludeVirtual=true', async () => { + const { GET } = loadRoute(); + + authMock.auth.mockResolvedValue({ user: { id: '1' } }); + prismaClientMock.user.findUnique.mockResolvedValue({ id: '1', plan: 'USER' }); + prismaClientMock.question.findMany.mockResolvedValue([]); + prismaClientMock.question.count.mockResolvedValue(0); + + const req = { url: 'http://localhost/api/questions?excludeVirtual=true' }; + await GET(req); + + expect(prismaClientMock.question.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + where: expect.objectContaining({ + answers: { none: { isFaq: true } } + }) + }) + ); + }); +}); + +describe('questions route status filter', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('supports filtering by multiple statuses', async () => { + const { GET } = loadRoute(); + + authMock.auth.mockResolvedValue({ user: { id: '1' } }); + prismaClientMock.user.findUnique.mockResolvedValue({ id: '1', plan: 'ADMIN' }); + prismaClientMock.question.findMany.mockResolvedValue([]); + prismaClientMock.question.count.mockResolvedValue(0); + + const req = { url: 'http://localhost/api/questions?status=RESPONDIDA,FECHADA' }; + await GET(req); + + expect(prismaClientMock.question.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + where: expect.objectContaining({ + status: { in: ['RESPONDIDA', 'FECHADA'] }, + }), + }), + ); + }); +}); + diff --git a/src/app/api/questions/route.ts b/src/app/api/questions/route.ts new file mode 100644 index 000000000..6313b5da9 --- /dev/null +++ b/src/app/api/questions/route.ts @@ -0,0 +1,285 @@ +// src/app/api/questions/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; +import { Prisma } from '@prisma/client'; + +// GET - Listar perguntas +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + // Buscar usuário + const user = await prisma.user.findUnique({ + where: { id: session.user.id } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + // Verificar se é admin + const isAdmin = user.plan === 'ADMIN' || user.plan === 'ANALISTA'; + + // Filtros opcionais + const statusParam = searchParams.get('status'); + const excludeVirtual = searchParams.get('excludeVirtual') === 'true'; + + // Parse paginação com valores padrão seguros + const parsePositiveInt = (value: string | null, defaultValue: number) => { + const parsed = Number(value); + if (!value || Number.isNaN(parsed) || parsed < 1) { + return defaultValue; + } + return Math.floor(parsed); + }; + + const page = parsePositiveInt(searchParams.get('page'), 1); + const limit = parsePositiveInt(searchParams.get('limit'), 10); + + // Query base + let whereClause: any = {}; + + // Se não é admin, só mostra suas próprias perguntas + if (!isAdmin) { + whereClause.userId = user.id; + } + + // Filtros adicionais + if (statusParam) { + const statusFilters = statusParam + .split(',') + .map((value) => value.trim()) + .filter((value) => value.length > 0); + + if (statusFilters.length === 1) { + whereClause.status = statusFilters[0]; + } else if (statusFilters.length > 1) { + whereClause.status = { in: statusFilters }; + } + } + + + // Para admins, buscar perguntas específicas de outros usuários + const targetUserId = searchParams.get('userId'); + if (isAdmin && targetUserId) { + whereClause.userId = targetUserId; + } + + if (excludeVirtual) { + whereClause.answers = { none: { isFaq: true } }; + } + + const questions = await prisma.question.findMany({ + where: whereClause, + select: { + id: true, + title: true, + content: true, + status: true, + createdAt: true, + readByAdmin: true, + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true, + plan: true + } + }, + answers: { + select: { + id: true, + content: true, + createdAt: true, + admin: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + }, + comments: { + select: { + id: true, + content: true, + createdAt: true, + author: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + }, + orderBy: { + createdAt: 'asc' + } + } + }, + orderBy: { + createdAt: 'asc' + } + }, + comments: { + select: { + id: true, + content: true, + createdAt: true, + author: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + }, + orderBy: { + createdAt: 'asc' + } + } + }, + orderBy: { + createdAt: 'desc' + }, + skip: (page - 1) * limit, + take: limit + }); + + // Marcar como lidas pelo admin se necessário + if (isAdmin) { + const unreadQuestionIds = questions + .filter(q => !q.readByAdmin) + .map(q => q.id); + + if (unreadQuestionIds.length > 0) { + await prisma.question.updateMany({ + where: { + id: { in: unreadQuestionIds } + }, + data: { + readByAdmin: true + } + }); + } + } + + // Contar total para paginação + const total = await prisma.question.count({ + where: whereClause + }); + + return NextResponse.json({ + questions, + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit) + }, + isAdmin + }); + + } catch (error) { + console.error('GET /api/questions failed:', error); + + let errorMessage = 'Internal server error'; + + if (error instanceof Prisma.PrismaClientKnownRequestError) { + errorMessage = 'Database error'; + } + + if (process.env.NODE_ENV === 'development' && error instanceof Error) { + errorMessage += `: ${error.message}`; + } + + return NextResponse.json({ error: errorMessage }, { status: 500 }); + } +} + +// POST - Criar nova pergunta (só usuários) +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const body = await request.json(); + const { title, content, priority = 'NORMAL' } = body; + + // Validações + if (!title || !content) { + return NextResponse.json({ + error: 'Title and content are required' + }, { status: 400 }); + } + + if (title.length > 200) { + return NextResponse.json({ + error: 'Title must be less than 200 characters' + }, { status: 400 }); + } + + if (content.length > 5000) { + return NextResponse.json({ + error: 'Content must be less than 5000 characters' + }, { status: 400 }); + } + + const question = await prisma.question.create({ + data: { + title, + content, + priority, + userId: user.id + }, + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true, + plan: true + } + } + } + }); + + return NextResponse.json({ question }, { status: 201 }); + + } catch (error) { + console.error('POST /api/questions failed:', error); + + let errorMessage = 'Internal server error'; + + if (error instanceof Prisma.PrismaClientKnownRequestError) { + errorMessage = 'Database error'; + } + + if (process.env.NODE_ENV === 'development' && error instanceof Error) { + errorMessage += `: ${error.message}`; + } + + return NextResponse.json({ error: errorMessage }, { status: 500 }); + } +} diff --git a/src/app/api/questions/search/route.test.js b/src/app/api/questions/search/route.test.js new file mode 100644 index 000000000..94c301994 --- /dev/null +++ b/src/app/api/questions/search/route.test.js @@ -0,0 +1,59 @@ +const fs = require('fs'); +const path = require('path'); +const vm = require('vm'); +const ts = require('typescript'); + +// Mock auth +const authMock = { auth: jest.fn() }; + +// Mock Prisma client and expose real Prisma for sql tag +const actualPrisma = require('@prisma/client'); +const prismaClientMock = { + user: { findUnique: jest.fn() }, + $queryRaw: jest.fn(), +}; +const prismaModuleMock = { + PrismaClient: jest.fn(() => prismaClientMock), + Prisma: actualPrisma.Prisma, +}; + +function loadRoute() { + const filePath = path.resolve(__dirname, 'route.ts'); + const source = fs.readFileSync(filePath, 'utf8'); + const { outputText } = ts.transpileModule(source, { + compilerOptions: { module: ts.ModuleKind.CommonJS, esModuleInterop: true } + }); + const wrapper = `(function (exports, require, module, __filename, __dirname) { ${outputText} \n})`; + const compiled = vm.runInThisContext(wrapper, { filename: filePath }); + const module = { exports: {} }; + function customRequire(p) { + if (p === '@/lib/auth') return authMock; + if (p === '@prisma/client') return prismaModuleMock; + if (p === '@/lib/prisma') return { prisma: prismaClientMock }; + return require(p); + } + compiled(module.exports, customRequire, module, filePath, path.dirname(filePath)); + return module.exports; +} + +describe('search route', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('parameterizes category filter', async () => { + const { GET } = loadRoute(); + authMock.auth.mockResolvedValue({ user: { id: '1' } }); + prismaClientMock.user.findUnique.mockResolvedValue({ id: '1', plan: 'ADMIN' }); + // question results and totalQuestions + prismaClientMock.$queryRaw.mockResolvedValueOnce([]); + prismaClientMock.$queryRaw.mockResolvedValueOnce([{ count: 0 }]); + + const req = { url: 'http://localhost/api/questions/search?q=test&category=STOCK' }; + await GET(req); + + const sqlObj = prismaClientMock.$queryRaw.mock.calls[0][0]; + expect(sqlObj.values).toContain('STOCK'); + expect(sqlObj.sql).not.toMatch(/STOCK/); + }); +}); diff --git a/src/app/api/questions/search/route.ts b/src/app/api/questions/search/route.ts new file mode 100644 index 000000000..8c9e57946 --- /dev/null +++ b/src/app/api/questions/search/route.ts @@ -0,0 +1,199 @@ +// src/app/api/questions/search/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { auth } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; +import { Prisma } from '@prisma/client'; + +// GET - Busca inteligente em perguntas e respostas +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const query = searchParams.get('q'); + const category = searchParams.get('category'); + const includeAnswers = searchParams.get('includeAnswers') === 'true'; + const includeFaq = searchParams.get('includeFaq') === 'true'; + const page = parseInt(searchParams.get('page') || '1'); + const limit = parseInt(searchParams.get('limit') || '10'); + + if (!query || query.trim().length < 2) { + return NextResponse.json({ + error: 'Query must be at least 2 characters' + }, { status: 400 }); + } + + const isAdmin = user.plan === 'ADMIN' || user.plan === 'ANALISTA'; + + + const whereCategory = category && category !== 'ALL' + ? Prisma.sql`AND q.category = ${category}` + : Prisma.empty; + + const whereUser = !isAdmin + ? Prisma.sql`AND q."userId" = ${user.id}` + : Prisma.empty; + + // Buscar em perguntas + const questionQuery = Prisma.sql` + SELECT + q.id, + q.title, + q.content, + q.category, + q.status, + q."createdAt", + q."userId", + u."firstName", + u."lastName", + u.email, + u.plan, + ts_rank(to_tsvector('portuguese', q.title || ' ' || q.content), plainto_tsquery('portuguese', ${query})) as rank, + 'question' as type + FROM questions q + JOIN "User" u ON q."userId" = u.id + WHERE to_tsvector('portuguese', q.title || ' ' || q.content) @@ plainto_tsquery('portuguese', ${query}) + ${whereUser} + ${whereCategory} + ORDER BY rank DESC + LIMIT ${limit} + OFFSET ${(page - 1) * limit} + `; + const questionResults = await prisma.$queryRaw(questionQuery) as any[]; + + let answerResults: any[] = []; + + // Buscar em respostas se includeAnswers for true + if (includeAnswers) { + const whereFaq = includeFaq ? Prisma.sql`AND a."isFaq" = true` : Prisma.empty; + + const answerQuery = Prisma.sql` + SELECT + a.id, + a.content, + a."createdAt", + a."isFaq", + a."faqTitle", + q.id as "questionId", + q.title as "questionTitle", + q.category, + q."userId" as "questionUserId", + admin."firstName" as "adminFirstName", + admin."lastName" as "adminLastName", + ts_rank(to_tsvector('portuguese', a.content), plainto_tsquery('portuguese', ${query})) as rank, + 'answer' as type + FROM answers a + JOIN questions q ON a."questionId" = q.id + JOIN "User" admin ON a."adminId" = admin.id + WHERE to_tsvector('portuguese', a.content) @@ plainto_tsquery('portuguese', ${query}) + ${whereUser} + ${whereCategory} + ${whereFaq} + ORDER BY rank DESC + LIMIT ${limit} + OFFSET ${(page - 1) * limit} + `; + answerResults = await prisma.$queryRaw(answerQuery) as any[]; + } + + // Buscar FAQ específicas se includeFaq for true + let faqResults: any[] = []; + if (includeFaq) { + const faqQuery = Prisma.sql` + SELECT + a.id, + a.content, + a."faqTitle", + a."faqOrder", + a."createdAt", + q.id as "questionId", + q.title as "questionTitle", + q.category, + admin."firstName" as "adminFirstName", + admin."lastName" as "adminLastName", + ts_rank(to_tsvector('portuguese', COALESCE(a."faqTitle", '') || ' ' || a.content), plainto_tsquery('portuguese', ${query})) as rank, + 'faq' as type + FROM answers a + JOIN questions q ON a."questionId" = q.id + JOIN "User" admin ON a."adminId" = admin.id + WHERE a."isFaq" = true + AND to_tsvector('portuguese', COALESCE(a."faqTitle", '') || ' ' || a.content) @@ plainto_tsquery('portuguese', ${query}) + ${whereCategory} + ORDER BY rank DESC, a."faqOrder" ASC + LIMIT ${limit} + OFFSET ${(page - 1) * limit} + `; + faqResults = await prisma.$queryRaw(faqQuery) as any[]; + } + + // Combinar resultados e ordenar por relevância + const allResults = [ + ...questionResults.map(r => ({ ...r, type: 'question' })), + ...answerResults.map(r => ({ ...r, type: 'answer' })), + ...faqResults.map(r => ({ ...r, type: 'faq' })) + ].sort((a, b) => parseFloat(b.rank) - parseFloat(a.rank)); + + // Contar totais para paginação + const totalQuestionsQuery = Prisma.sql` + SELECT COUNT(*) as count + FROM questions q + WHERE to_tsvector('portuguese', q.title || ' ' || q.content) @@ plainto_tsquery('portuguese', ${query}) + ${whereUser} + ${whereCategory} + `; + const totalQuestions = await prisma.$queryRaw(totalQuestionsQuery) as any[]; + + const totalAnswers = includeAnswers ? await prisma.$queryRaw(Prisma.sql` + SELECT COUNT(*) as count + FROM answers a + JOIN questions q ON a."questionId" = q.id + WHERE to_tsvector('portuguese', a.content) @@ plainto_tsquery('portuguese', ${query}) + ${whereUser} + ${whereCategory} + `) as any[] : [{ count: 0 }]; + + const totalFaqs = includeFaq ? await prisma.$queryRaw(Prisma.sql` + SELECT COUNT(*) as count + FROM answers a + JOIN questions q ON a."questionId" = q.id + WHERE a."isFaq" = true + AND to_tsvector('portuguese', COALESCE(a."faqTitle", '') || ' ' || a.content) @@ plainto_tsquery('portuguese', ${query}) + ${whereCategory} + `) as any[] : [{ count: 0 }]; + + const total = Number(totalQuestions[0].count) + Number(totalAnswers[0].count) + Number(totalFaqs[0].count); + + return NextResponse.json({ + results: allResults, + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit) + }, + stats: { + totalQuestions: Number(totalQuestions[0].count), + totalAnswers: Number(totalAnswers[0].count), + totalFaqs: Number(totalFaqs[0].count) + }, + query, + isAdmin + }); + + } catch (error) { + console.error('Error searching questions:', error); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/questions/stats/route.ts b/src/app/api/questions/stats/route.ts new file mode 100644 index 000000000..770b63ab0 --- /dev/null +++ b/src/app/api/questions/stats/route.ts @@ -0,0 +1,208 @@ +// src/app/api/questions/stats/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +// GET - Estatísticas para dashboard admin +export async function GET(_request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id } + }); + + if (!user || (user.plan !== 'ADMIN' && user.plan !== 'ANALISTA')) { + return NextResponse.json({ error: 'Admin access required' }, { status: 403 }); + } + + // Estatísticas gerais + const totalQuestions = await prisma.question.count(); + const totalAnswers = await prisma.answer.count(); + + // Por status + const questionsByStatus = await prisma.question.groupBy({ + by: ['status'], + _count: { + status: true + } + }); + + // Por prioridade + const questionsByPriority = await prisma.question.groupBy({ + by: ['priority'], + _count: { + priority: true + } + }); + + // Perguntas não lidas por admin + const unreadQuestions = await prisma.question.count({ + where: { + readByAdmin: false + } + }); + + // Perguntas pendentes (não respondidas) + const pendingQuestions = await prisma.question.count({ + where: { + status: 'NOVA' + } + }); + + // Tempo médio de resposta (em horas) + const questionsWithAnswers = await prisma.question.findMany({ + where: { + answers: { + some: {} + } + }, + include: { + answers: { + orderBy: { + createdAt: 'asc' + }, + take: 1 + } + } + }); + + let averageResponseTime = 0; + if (questionsWithAnswers.length > 0) { + const responseTimes = questionsWithAnswers.map(q => { + const questionTime = new Date(q.createdAt).getTime(); + const firstAnswerTime = new Date(q.answers[0].createdAt).getTime(); + return (firstAnswerTime - questionTime) / (1000 * 60 * 60); // em horas + }); + + averageResponseTime = responseTimes.reduce((acc, time) => acc + time, 0) / responseTimes.length; + } + + // Últimas 30 dias - atividade + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + + const recentQuestions = await prisma.question.count({ + where: { + createdAt: { + gte: thirtyDaysAgo + } + } + }); + + const recentAnswers = await prisma.answer.count({ + where: { + createdAt: { + gte: thirtyDaysAgo + } + } + }); + + // Top 5 usuários com mais perguntas + const topUsers = await prisma.question.groupBy({ + by: ['userId'], + _count: { + userId: true + }, + orderBy: { + _count: { + userId: 'desc' + } + }, + take: 5 + }); + + // Buscar detalhes dos top users + const topUsersDetails = await Promise.all( + topUsers.map(async (userStat) => { + const userDetails = await prisma.user.findUnique({ + where: { id: userStat.userId }, + select: { + id: true, + firstName: true, + lastName: true, + email: true, + plan: true + } + }); + return { + user: userDetails, + questionCount: userStat._count.userId + }; + }) + ); + + // Perguntas por dia nos últimos 7 dias + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); + + // CORREÇÃO: usar "createdAt" não "created_at" + const dailyQuestions = await prisma.$queryRaw` + SELECT + DATE("createdAt") as date, + COUNT(*) as count + FROM questions + WHERE "createdAt" >= ${sevenDaysAgo} + GROUP BY DATE("createdAt") + ORDER BY date ASC + ` as Array<{ date: Date; count: bigint }>; + + // Formatar dados de resposta + const stats = { + overview: { + totalQuestions, + totalAnswers, + unreadQuestions, + pendingQuestions, + averageResponseTimeHours: Math.round(averageResponseTime * 100) / 100 + }, + breakdown: { + byStatus: questionsByStatus.map(item => ({ + status: item.status, + count: item._count.status + })), + byPriority: questionsByPriority.map(item => ({ + priority: item.priority, + count: item._count.priority + })) + }, + activity: { + last30Days: { + questions: recentQuestions, + answers: recentAnswers + }, + dailyQuestions: dailyQuestions.map(item => ({ + date: item.date, + count: Number(item.count) + })) + }, + topUsers: topUsersDetails + }; + + return NextResponse.json(stats, { + headers: { + 'Cache-Control': 'private, max-age=60, stale-while-revalidate=300' + } + }); + + } catch (error) { + console.error('Error fetching question stats:', error); + + // Log mais detalhado do erro + if (error instanceof Error) { + console.error('Error message:', error.message); + console.error('Error stack:', error.stack); + } + + return NextResponse.json({ + error: 'Internal server error', + details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined + }, { status: 500 }); + } finally { + await prisma.$disconnect(); + } +} diff --git a/src/app/api/relatorio-semanal/route.ts b/src/app/api/relatorio-semanal/route.ts new file mode 100644 index 000000000..24b5950a7 --- /dev/null +++ b/src/app/api/relatorio-semanal/route.ts @@ -0,0 +1,427 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { notifyNewReport } from '@/utils/notifications'; +import { auth } from '@/lib/auth'; +import { verifyToken } from '@/utils/auth'; + +export const dynamic = 'force-dynamic'; +export const runtime = 'nodejs'; + +// Centraliza a autenticação de administradores +async function authenticateAdmin(request: NextRequest) { + const session = await auth(); + + if (session?.user) { + if (session.user.plan !== 'ADMIN') { + return { user: null, status: 403 }; + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id } + }); + + return { user, status: user ? 200 : 401 }; + } + + const token = request.headers.get('Authorization')?.replace('Bearer ', ''); + + try { + const payload = verifyToken(token); + const user = await prisma.user.findUnique({ where: { email: payload.email } }); + + if (!user || user.plan !== 'ADMIN') { + return { user: null, status: 403 }; + } + + return { user, status: 200 }; + } catch { + return { user: null, status: 401 }; + } +} + +// Função para transformar dados do Prisma para o formato esperado pelo React +function transformPrismaData(relatorio: any) { + return { + ...relatorio, + // Converter campos Json para arrays + macro: Array.isArray(relatorio.macro) + ? relatorio.macro + : (typeof relatorio.macro === 'string' ? JSON.parse(relatorio.macro || '[]') : []), + + dividendos: Array.isArray(relatorio.dividendos) + ? relatorio.dividendos + : (typeof relatorio.dividendos === 'string' ? JSON.parse(relatorio.dividendos || '[]') : []), + + smallCaps: Array.isArray(relatorio.smallCaps) + ? relatorio.smallCaps + : (typeof relatorio.smallCaps === 'string' ? JSON.parse(relatorio.smallCaps || '[]') : []), + + microCaps: Array.isArray(relatorio.microCaps) + ? relatorio.microCaps + : (typeof relatorio.microCaps === 'string' ? JSON.parse(relatorio.microCaps || '[]') : []), + + exteriorStocks: Array.isArray(relatorio.exteriorStocks) + ? relatorio.exteriorStocks + : (typeof relatorio.exteriorStocks === 'string' ? JSON.parse(relatorio.exteriorStocks || '[]') : []), + + exteriorETFs: Array.isArray(relatorio.exteriorETFs) + ? relatorio.exteriorETFs + : (typeof relatorio.exteriorETFs === 'string' ? JSON.parse(relatorio.exteriorETFs || '[]') : []), + + exteriorDividendos: Array.isArray(relatorio.exteriorDividendos) + ? relatorio.exteriorDividendos + : (typeof relatorio.exteriorDividendos === 'string' ? JSON.parse(relatorio.exteriorDividendos || '[]') : []), + + exteriorProjetoAmerica: Array.isArray(relatorio.exteriorProjetoAmerica) + ? relatorio.exteriorProjetoAmerica + : (typeof relatorio.exteriorProjetoAmerica === 'string' ? JSON.parse(relatorio.exteriorProjetoAmerica || '[]') : []), + + // Campos legados para compatibilidade + proventos: Array.isArray(relatorio.proventos) + ? relatorio.proventos + : (typeof relatorio.proventos === 'string' ? JSON.parse(relatorio.proventos || '[]') : []), + + exterior: Array.isArray(relatorio.exterior) + ? relatorio.exterior + : (typeof relatorio.exterior === 'string' ? JSON.parse(relatorio.exterior || '[]') : []) + }; +} + +// GET - Buscar relatórios (admin) ou atual (público) +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const isAdmin = searchParams.get('admin') === 'true'; + + if (isAdmin) { + // ADMIN: Autenticação via cookie (auth) ou headers + const { user, status } = await authenticateAdmin(request); + + if (!user) { + const message = status === 401 ? 'Authentication required' : 'Admin access required'; + return NextResponse.json({ error: message }, { status }); + } + + // Buscar todos os relatórios para admin + const relatorios = await prisma.relatorioSemanal.findMany({ + orderBy: { createdAt: 'desc' } + }); + + // CORREÇÃO: Transformar dados para admin também + const relatoriosTransformados = relatorios.map(transformPrismaData); + + console.log(`Admin: encontrados ${relatoriosTransformados.length} relatórios`); + return NextResponse.json(relatoriosTransformados); + + } else { + const listFlag = searchParams.has('list'); + const reportId = searchParams.get('id'); + const reportDate = searchParams.get('date'); + const reportWeek = searchParams.get('semana'); + + if (listFlag) { + const relatoriosPublicados = await prisma.relatorioSemanal.findMany({ + where: { status: 'published' }, + orderBy: [ + { dataPublicacao: 'desc' }, + { createdAt: 'desc' } + ], + select: { + id: true, + dataPublicacao: true, + semana: true, + titulo: true + } + }); + + return NextResponse.json(relatoriosPublicados); + } + + let relatorio; + + if (reportId) { + relatorio = await prisma.relatorioSemanal.findFirst({ + where: { + id: reportId, + status: 'published' + } + }); + } else if (reportDate || reportWeek) { + const filters: any[] = []; + + if (reportDate) { + filters.push({ OR: [{ dataPublicacao: reportDate }, { date: reportDate }] }); + } + + if (reportWeek) { + filters.push({ OR: [{ semana: reportWeek }, { weekOf: reportWeek }] }); + } + + relatorio = await prisma.relatorioSemanal.findFirst({ + where: { + status: 'published', + ...(filters.length ? { AND: filters } : {}) + }, + orderBy: { createdAt: 'desc' } + }); + } + + if (!relatorio) { + // PÚBLICO: Apenas o mais recente publicado + relatorio = await prisma.relatorioSemanal.findFirst({ + where: { status: 'published' }, + orderBy: { createdAt: 'desc' } + }); + } + + if (relatorio) { + // Converter para formato compatível com visualização + const relatorioFormatted = { + ...transformPrismaData(relatorio), + // Campos para compatibilidade + date: relatorio.dataPublicacao || relatorio.date, + weekOf: relatorio.semana || relatorio.weekOf + }; + return NextResponse.json(relatorioFormatted); + } + + return NextResponse.json(null); + } + } catch (error) { + console.error('Erro GET relatório:', error); + return NextResponse.json({ + error: 'Erro ao buscar relatório', + details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined + }, { status: 500 }); + } finally { + await prisma.$disconnect(); + } +} + +// POST - Criar/Atualizar relatório (com autenticação) +export async function POST(request: NextRequest) { + try { + // Verificar autenticação via cookie ou headers + const { user, status } = await authenticateAdmin(request); + + if (!user) { + const message = status === 401 ? 'Authentication required' : 'Admin access required'; + return NextResponse.json({ error: message }, { status }); + } + + const data = await request.json(); + console.log('Dados recebidos:', { + semana: data.semana, + titulo: data.titulo, + id: data.id, + status: data.status + }); + + // Validações básicas + if (!data.titulo || typeof data.titulo !== 'string') { + return NextResponse.json({ + error: 'Título é obrigatório e deve ser uma string' + }, { status: 400 }); + } + + if (!data.semana || typeof data.semana !== 'string') { + return NextResponse.json({ + error: 'Semana é obrigatória e deve ser uma string' + }, { status: 400 }); + } + + // Preparar dados compatíveis + const relatorioData = { + // Campos do admin + semana: data.semana.trim(), + dataPublicacao: data.dataPublicacao || new Date().toISOString().split('T')[0], + autor: data.autor?.trim() || `${user.firstName} ${user.lastName}`, + titulo: data.titulo.trim(), + + // Campos legados para compatibilidade + date: data.dataPublicacao || data.date || new Date().toISOString().split('T')[0], + weekOf: data.semana?.trim() || data.weekOf || 'Nova semana', + + // Arrays de conteúdo + macro: Array.isArray(data.macro) ? data.macro : [], + dividendos: Array.isArray(data.dividendos) ? data.dividendos : [], + smallCaps: Array.isArray(data.smallCaps) ? data.smallCaps : [], + microCaps: Array.isArray(data.microCaps) ? data.microCaps : [], + exteriorStocks: Array.isArray(data.exteriorStocks) ? data.exteriorStocks : [], + exteriorETFs: Array.isArray(data.exteriorETFs) ? data.exteriorETFs : [], + exteriorDividendos: Array.isArray(data.exteriorDividendos) ? data.exteriorDividendos : [], + exteriorProjetoAmerica: Array.isArray(data.exteriorProjetoAmerica) ? data.exteriorProjetoAmerica : [], + + // Campos legados + proventos: Array.isArray(data.proventos) ? data.proventos : [], + exterior: Array.isArray(data.exterior) ? data.exterior : Array.isArray(data.exteriorStocks) ? data.exteriorStocks : [], + + status: data.status || 'draft', + authorId: user.id + }; + + let relatorio; + let isNewReport = false; + let wasPublished = false; + + if (data.id && data.id !== 'novo') { + // Tentar atualizar + try { + // Verificar status anterior + const existingReport = await prisma.relatorioSemanal.findUnique({ + where: { id: data.id }, + select: { status: true } + }); + + relatorio = await prisma.relatorioSemanal.update({ + where: { id: data.id }, + data: relatorioData + }); + + // Verificar se foi publicado agora (mudou de draft para published) + wasPublished = existingReport?.status !== 'published' && relatorio.status === 'published'; + + console.log('Relatório atualizado:', relatorio.id); + } catch (updateError) { + if ((updateError as any).code === 'P2025') { + // Não encontrou, criar novo + const { id, ...dataWithoutId } = relatorioData; + relatorio = await prisma.relatorioSemanal.create({ + data: dataWithoutId + }); + isNewReport = true; + wasPublished = relatorio.status === 'published'; + console.log('Relatório criado (não encontrou):', relatorio.id); + } else { + throw updateError; + } + } + } else { + // Criar novo + const { id, ...dataWithoutId } = relatorioData; + relatorio = await prisma.relatorioSemanal.create({ + data: dataWithoutId + }); + isNewReport = true; + wasPublished = relatorio.status === 'published'; + console.log('Relatório criado:', relatorio.id); + } + + // Disparar notificação se foi publicado (novo ou atualizado para published) + if (wasPublished) { + try { + const adminName = `${user.firstName} ${user.lastName}`; + await notifyNewReport( + relatorio, + adminName, + ['VIP', 'LITE', 'LITE_V2', 'RENDA_PASSIVA', 'FIIS', 'RENDA_TURBINADA', 'RENDA_TURBINADA_T2', 'RENDA_TURBINADA_T3', 'AMERICA'] + ); + console.log('Notificações enviadas para usuários sobre novo relatório'); + } catch (notificationError) { + console.error('Erro ao enviar notificações:', notificationError); + // Não falha a criação/atualização se a notificação falhar + } + } + + // CORREÇÃO: Transformar dados na resposta também + const relatorioTransformado = transformPrismaData(relatorio); + + return NextResponse.json({ + ...relatorioTransformado, + message: isNewReport ? 'Relatório criado com sucesso' : 'Relatório atualizado com sucesso', + notificationsSent: wasPublished + }); + + } catch (error) { + console.error('Erro POST relatório:', error); + + // Tratamento específico de erros do Prisma + if (error && typeof error === 'object' && 'code' in error) { + const prismaError = error as any; + + if (prismaError.code === 'P2002') { + return NextResponse.json({ + error: 'Já existe um relatório com estes dados únicos' + }, { status: 400 }); + } + + if (prismaError.code === 'P2003') { + return NextResponse.json({ + error: 'Referência de usuário inválida' + }, { status: 400 }); + } + } + + return NextResponse.json({ + error: 'Erro ao salvar relatório', + details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined + }, { status: 500 }); + } finally { + await prisma.$disconnect(); + } +} + +// DELETE - Deletar relatório +export async function DELETE(request: NextRequest) { + try { + // Verificar autenticação via cookie ou headers + const { user, status } = await authenticateAdmin(request); + + if (!user) { + const message = status === 401 ? 'Authentication required' : 'Admin access required'; + return NextResponse.json({ error: message }, { status }); + } + + const { searchParams } = new URL(request.url); + const id = searchParams.get('id'); + + if (!id) { + return NextResponse.json({ error: 'ID necessário' }, { status: 400 }); + } + + // Verificar se o relatório existe antes de deletar + const existingReport = await prisma.relatorioSemanal.findUnique({ + where: { id }, + select: { id: true, titulo: true, status: true } + }); + + if (!existingReport) { + return NextResponse.json({ error: 'Relatório não encontrado' }, { status: 404 }); + } + + // Verificar se pode deletar (por exemplo, não deletar se já publicado) + if (existingReport.status === 'published') { + return NextResponse.json({ + error: 'Não é possível deletar relatórios já publicados' + }, { status: 400 }); + } + + await prisma.relatorioSemanal.delete({ + where: { id } + }); + + console.log('Relatório deletado:', id); + return NextResponse.json({ + success: true, + message: 'Relatório deletado com sucesso' + }); + + } catch (error) { + console.error('Erro DELETE relatório:', error); + + if (error && typeof error === 'object' && 'code' in error) { + const prismaError = error as any; + + if (prismaError.code === 'P2025') { + return NextResponse.json({ error: 'Relatório não encontrado' }, { status: 404 }); + } + } + + return NextResponse.json({ + error: 'Erro ao deletar relatório', + details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined + }, { status: 500 }); + } finally { + await prisma.$disconnect(); + } +} diff --git a/src/app/api/relatorios/[id]/route.ts b/src/app/api/relatorios/[id]/route.ts new file mode 100644 index 000000000..5e40203bc --- /dev/null +++ b/src/app/api/relatorios/[id]/route.ts @@ -0,0 +1,260 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +// 📄 GET - Buscar relatório por ID +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const id = params.id; + + if (!id) { + return NextResponse.json( + { + success: false, + error: 'ID do relatório é obrigatório' + }, + { status: 400 } + ); + } + + const relatorio = await prisma.relatorio.findUnique({ + where: { id } + }); + + if (!relatorio) { + return NextResponse.json( + { + success: false, + error: 'Relatório não encontrado' + }, + { status: 404 } + ); + } + + // Converter para formato da interface + const relatorioFormatado = { + id: relatorio.id, + ticker: relatorio.ticker, + nome: relatorio.nome, + tipo: relatorio.tipo as 'trimestral' | 'anual' | 'apresentacao' | 'outros', + dataReferencia: relatorio.dataReferencia, + dataUpload: relatorio.dataUpload.toISOString(), + linkCanva: relatorio.linkCanva || undefined, + linkExterno: relatorio.linkExterno || undefined, + tipoVisualizacao: relatorio.tipoVisualizacao as 'iframe' | 'canva' | 'link' | 'pdf', + arquivoPdf: relatorio.arquivoPdf || undefined, + nomeArquivoPdf: relatorio.nomeArquivoPdf || undefined, + tamanhoArquivo: relatorio.tamanhoArquivo || undefined, + tipoPdf: (relatorio.tipoPdf as 'base64' | 'referencia') || undefined, + hashArquivo: relatorio.hashArquivo || undefined, + solicitarReupload: relatorio.solicitarReupload || undefined + }; + + return NextResponse.json({ + success: true, + relatorio: relatorioFormatado + }); + + } catch (error) { + console.error('Erro ao buscar relatório por ID:', error); + + return NextResponse.json( + { + success: false, + error: 'Erro interno do servidor ao buscar relatório', + details: error instanceof Error ? error.message : 'Erro desconhecido' + }, + { status: 500 } + ); + } +} + +// âœï¸ PUT - Atualizar relatório por ID +export async function PUT( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const id = params.id; + const dadosAtualizacao = await request.json(); + + if (!id) { + return NextResponse.json( + { + success: false, + error: 'ID do relatório é obrigatório' + }, + { status: 400 } + ); + } + + // Verificar se o relatório existe + const relatorioExistente = await prisma.relatorio.findUnique({ + where: { id } + }); + + if (!relatorioExistente) { + return NextResponse.json( + { + success: false, + error: 'Relatório não encontrado' + }, + { status: 404 } + ); + } + + // Atualizar relatório + const relatorioAtualizado = await prisma.relatorio.update({ + where: { id }, + data: { + ticker: dadosAtualizacao.ticker?.toUpperCase() || relatorioExistente.ticker, + nome: dadosAtualizacao.nome || relatorioExistente.nome, + tipo: dadosAtualizacao.tipo || relatorioExistente.tipo, + dataReferencia: dadosAtualizacao.dataReferencia !== undefined + ? dadosAtualizacao.dataReferencia + : relatorioExistente.dataReferencia, + linkCanva: dadosAtualizacao.linkCanva !== undefined + ? dadosAtualizacao.linkCanva + : relatorioExistente.linkCanva, + linkExterno: dadosAtualizacao.linkExterno !== undefined + ? dadosAtualizacao.linkExterno + : relatorioExistente.linkExterno, + tipoVisualizacao: dadosAtualizacao.tipoVisualizacao || relatorioExistente.tipoVisualizacao, + arquivoPdf: dadosAtualizacao.arquivoPdf !== undefined + ? dadosAtualizacao.arquivoPdf + : relatorioExistente.arquivoPdf, + nomeArquivoPdf: dadosAtualizacao.nomeArquivoPdf !== undefined + ? dadosAtualizacao.nomeArquivoPdf + : relatorioExistente.nomeArquivoPdf, + tamanhoArquivo: dadosAtualizacao.tamanhoArquivo !== undefined + ? dadosAtualizacao.tamanhoArquivo + : relatorioExistente.tamanhoArquivo, + tipoPdf: dadosAtualizacao.tipoPdf !== undefined + ? dadosAtualizacao.tipoPdf + : relatorioExistente.tipoPdf, + hashArquivo: dadosAtualizacao.hashArquivo !== undefined + ? dadosAtualizacao.hashArquivo + : relatorioExistente.hashArquivo, + solicitarReupload: dadosAtualizacao.solicitarReupload !== undefined + ? dadosAtualizacao.solicitarReupload + : relatorioExistente.solicitarReupload + } + }); + + console.log(`âœï¸ Relatório atualizado:`, { + id, + ticker: relatorioAtualizado.ticker, + nome: relatorioAtualizado.nome + }); + + // Converter para formato da interface + const relatorioFormatado = { + id: relatorioAtualizado.id, + ticker: relatorioAtualizado.ticker, + nome: relatorioAtualizado.nome, + tipo: relatorioAtualizado.tipo as 'trimestral' | 'anual' | 'apresentacao' | 'outros', + dataReferencia: relatorioAtualizado.dataReferencia, + dataUpload: relatorioAtualizado.dataUpload.toISOString(), + linkCanva: relatorioAtualizado.linkCanva || undefined, + linkExterno: relatorioAtualizado.linkExterno || undefined, + tipoVisualizacao: relatorioAtualizado.tipoVisualizacao as 'iframe' | 'canva' | 'link' | 'pdf', + arquivoPdf: relatorioAtualizado.arquivoPdf || undefined, + nomeArquivoPdf: relatorioAtualizado.nomeArquivoPdf || undefined, + tamanhoArquivo: relatorioAtualizado.tamanhoArquivo || undefined, + tipoPdf: (relatorioAtualizado.tipoPdf as 'base64' | 'referencia') || undefined, + hashArquivo: relatorioAtualizado.hashArquivo || undefined, + solicitarReupload: relatorioAtualizado.solicitarReupload || undefined + }; + + return NextResponse.json({ + success: true, + message: 'Relatório atualizado com sucesso', + relatorio: relatorioFormatado + }); + + } catch (error) { + console.error('Erro ao atualizar relatório:', error); + + return NextResponse.json( + { + success: false, + error: 'Erro interno do servidor ao atualizar relatório', + details: error instanceof Error ? error.message : 'Erro desconhecido' + }, + { status: 500 } + ); + } +} + +// ðŸ—‘ï¸ DELETE - Excluir relatório por ID +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const id = params.id; + + if (!id) { + return NextResponse.json( + { + success: false, + error: 'ID do relatório é obrigatório' + }, + { status: 400 } + ); + } + + // Buscar relatório antes da exclusão (para log) + const relatorioExistente = await prisma.relatorio.findUnique({ + where: { id }, + select: { + id: true, + ticker: true, + nome: true, + tipo: true + } + }); + + if (!relatorioExistente) { + return NextResponse.json( + { + success: false, + error: 'Relatório não encontrado' + }, + { status: 404 } + ); + } + + // Excluir relatório + await prisma.relatorio.delete({ + where: { id } + }); + + console.log(`ðŸ—‘ï¸ Relatório excluído:`, { + id, + ticker: relatorioExistente.ticker, + nome: relatorioExistente.nome, + tipo: relatorioExistente.tipo + }); + + return NextResponse.json({ + success: true, + message: 'Relatório excluído com sucesso', + relatorioExcluido: relatorioExistente + }); + + } catch (error) { + console.error('Erro ao excluir relatório:', error); + + return NextResponse.json( + { + success: false, + error: 'Erro interno do servidor ao excluir relatório', + details: error instanceof Error ? error.message : 'Erro desconhecido' + }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/relatorios/estatisticas/route.ts b/src/app/api/relatorios/estatisticas/route.ts new file mode 100644 index 000000000..06d452b33 --- /dev/null +++ b/src/app/api/relatorios/estatisticas/route.ts @@ -0,0 +1,238 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +// 🔠FUNÇÃO DE AUTENTICAÇÃO FLEXÃVEL +async function getAuthenticatedUser(request: NextRequest) { + try { + console.log('🔠[AUTH] Iniciando autenticação flexível...'); + + const userEmail = request.headers.get('x-user-email'); + console.log('🔠[AUTH] Email do header:', userEmail); + + if (!userEmail) { + console.log('âš ï¸ [AUTH] Email não fornecido - permitindo acesso público'); + return { isPublic: true, user: null }; + } + + // Buscar no banco de dados + console.log('🔠[AUTH] Buscando usuário no banco...'); + const user = await prisma.user.findUnique({ + where: { email: userEmail }, + select: { + id: true, + firstName: true, + lastName: true, + email: true, + plan: true, + status: true + } + }); + + if (!user) { + console.log('âš ï¸ [AUTH] Usuário não encontrado - permitindo acesso público'); + return { isPublic: true, user: null }; + } + + console.log('✅ [AUTH] Usuário encontrado:', user.email, 'Plano:', user.plan); + return { isPublic: false, user }; + } catch (error) { + console.log('âš ï¸ [AUTH] Erro na autenticação - fallback para público:', error); + return { isPublic: true, user: null }; + } +} + +// 👑 VERIFICAR PERMISSÕES FLEXÃVEIS +function getPermissions(user: any, isPublic: boolean) { + if (isPublic) { + // ACESSO PÚBLICO: ver todos os relatórios (sem restrição) + return { + canView: true, + viewAll: true, + accessType: 'public' + }; + } + + if (!user) { + return { + canView: true, + viewAll: true, + accessType: 'public' + }; + } + + // USUÃRIOS LOGADOS: permissões baseadas no plano + const PERMISSOES_RELATORIOS = { + 'ADMIN': { canView: true, viewAll: true, accessType: 'admin' }, + 'VIP': { canView: true, viewAll: true, accessType: 'vip' }, + 'LITE': { canView: true, viewAll: true, accessType: 'lite' }, // ✅ LITE pode ver todos + 'LITE_V2': { canView: true, viewAll: true, accessType: 'lite_v2' }, // ✅ LITE_V2 pode ver todos + 'RENDA_PASSIVA': { canView: true, viewAll: true, accessType: 'renda_passiva' }, + 'FIIS': { canView: true, viewAll: true, accessType: 'fiis' }, + 'RENDA_TURBINADA': { canView: true, viewAll: true, accessType: 'renda_turbinada' }, + 'RENDA_TURBINADA_T2': { canView: true, viewAll: true, accessType: 'renda_turbinada_t2' }, + 'RENDA_TURBINADA_T3': { canView: true, viewAll: true, accessType: 'renda_turbinada_t3' }, + 'AMERICA': { canView: true, viewAll: true, accessType: 'america' } + }; + + const permissao = PERMISSOES_RELATORIOS[user.plan] || { + canView: true, + viewAll: true, + accessType: 'default' + }; + + console.log(`👑 [PERM] Usuário ${user.email} (${user.plan}):`, permissao); + + return permissao; +} + +// 📊 GET - Obter estatísticas dos relatórios (ACESSO PÚBLICO) +export async function GET(request: NextRequest) { + try { + await prisma.$connect(); + } catch (error) { + console.error('⌠[DB] Erro ao conectar ao banco de dados:', error); + return NextResponse.json( + { + success: false, + error: 'Serviço temporariamente indisponível - falha na conexão com o banco de dados', + details: error instanceof Error ? error.message : 'Erro desconhecido' + }, + { status: 503 } + ); + } + + try { + console.log('📊 [STATS] INICIO - Obter estatísticas (acesso público permitido)'); + + // 🔠Autenticação flexível + const { isPublic, user } = await getAuthenticatedUser(request); + + // 👑 Verificar permissões (sempre permite acesso) + const { canView, viewAll, accessType } = getPermissions(user, isPublic); + + console.log(`🔓 [STATS] Acesso permitido - Tipo: ${accessType}`); + + // 📋 Buscar TODOS os relatórios (sem filtro de usuário) + // Para a página dos ativos, mostramos todos os relatórios disponíveis + console.log(`🌠[STATS] Buscando todos os relatórios disponíveis`); + + const relatorios = await prisma.relatorio.findMany({ + orderBy: [ + { ticker: 'asc' }, + { dataReferencia: 'desc' } + ], + include: { + reportFiles: { + orderBy: { createdAt: 'desc' }, + take: 1, + }, + }, + }); + + console.log('📋 [STATS] Relatórios encontrados:', relatorios.length); + + // Calcular estatísticas + const totalRelatorios = relatorios.length; + + // Contar tickers únicos + const tickersUnicos = new Set(relatorios.map(r => r.ticker)); + const totalTickers = tickersUnicos.size; + + // Contar relatórios com PDF + const relatoriosComPdf = relatorios.filter(r => + r.arquivoPdf || r.nomeArquivoPdf + ).length; + + // Calcular tamanho total em MB + const tamanhoTotalBytes = relatorios.reduce((sum, r) => + sum + (r.tamanhoArquivo || 0), 0 + ); + const tamanhoTotalMB = tamanhoTotalBytes / (1024 * 1024); + + // Data do último upload + const ultimoUpload = relatorios.length > 0 + ? Math.max(...relatorios.map(r => r.dataUpload ? r.dataUpload.getTime() : 0)) + : null; + + const dataUltimoUpload = ultimoUpload + ? new Date(ultimoUpload).toISOString() + : undefined; + + // Converter relatórios para o formato da interface + const relatoriosFormatados = relatorios.map(relatorio => ({ + id: relatorio.id, + ticker: relatorio.ticker, + nome: relatorio.nome, + tipo: relatorio.tipo as 'trimestral' | 'anual' | 'apresentacao' | 'outros', + dataReferencia: relatorio.dataReferencia, + dataUpload: relatorio.dataUpload ? relatorio.dataUpload.toISOString() : undefined, + linkCanva: relatorio.linkCanva || undefined, + linkExterno: relatorio.linkExterno || undefined, + tipoVisualizacao: relatorio.tipoVisualizacao as 'iframe' | 'canva' | 'link' | 'pdf', + arquivoPdf: relatorio.arquivoPdf || undefined, + nomeArquivoPdf: relatorio.nomeArquivoPdf || undefined, + tamanhoArquivo: relatorio.tamanhoArquivo || undefined, + tipoPdf: (relatorio.tipoPdf as 'base64' | 'referencia') || undefined, + hashArquivo: relatorio.hashArquivo || undefined, + solicitarReupload: relatorio.solicitarReupload || undefined, + status: relatorio.reportFiles[0]?.status || undefined, + pdfUrl: relatorio.reportFiles[0]?.pdfUrl || undefined, + })); + + // Estatísticas por ticker + const estatisticasPorTicker = Array.from(tickersUnicos).map(ticker => { + const relatoriosDoTicker = relatorios.filter(r => r.ticker === ticker); + return { + ticker, + total: relatoriosDoTicker.length, + comPdf: relatoriosDoTicker.filter(r => r.arquivoPdf || r.nomeArquivoPdf).length, + tamanhoMB: relatoriosDoTicker.reduce((sum, r) => + sum + (r.tamanhoArquivo || 0), 0 + ) / (1024 * 1024) + }; + }).sort((a, b) => b.total - a.total); + + console.log(`✅ [STATS] Retornando ${totalRelatorios} relatórios (acesso ${accessType})`); + + return NextResponse.json({ + success: true, + totalRelatorios, + totalTickers, + relatoriosComPdf, + tamanhoTotalMB: Number(tamanhoTotalMB.toFixed(2)), + dataUltimoUpload, + relatorios: relatoriosFormatados, + estatisticasPorTicker, + // 🔠Informações de contexto + context: { + accessType, + isPublic, + userEmail: user?.email || 'público', + userPlan: user?.plan || 'público', + environment: process.env.NODE_ENV + } + }); + + } catch (error) { + console.error('⌠[STATS] Erro ao calcular estatísticas dos relatórios:', error); + + return NextResponse.json( + { + success: false, + error: 'Erro interno do servidor ao calcular estatísticas', + details: error instanceof Error ? error.message : 'Erro desconhecido', + // Retornar dados vazios em caso de erro + totalRelatorios: 0, + totalTickers: 0, + relatoriosComPdf: 0, + tamanhoTotalMB: 0, + relatorios: [] + }, + { status: 500 } + ); + } finally { + await prisma.$disconnect().catch(disconnectError => + console.error('⌠[DB] Erro ao desconectar do banco de dados:', disconnectError) + ); + } +} diff --git a/src/app/api/relatorios/excluir-multiplos/route.ts b/src/app/api/relatorios/excluir-multiplos/route.ts new file mode 100644 index 000000000..e428d770e --- /dev/null +++ b/src/app/api/relatorios/excluir-multiplos/route.ts @@ -0,0 +1,92 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +// ðŸ—‘ï¸ DELETE - Excluir múltiplos relatórios por IDs +export async function DELETE(request: NextRequest) { + try { + const { ids } = await request.json(); + + // Validar entrada + if (!Array.isArray(ids) || ids.length === 0) { + return NextResponse.json( + { + success: false, + error: 'É necessário fornecer um array de IDs para exclusão' + }, + { status: 400 } + ); + } + + // Verificar se todos os IDs são strings válidas + const idsValidos = ids.filter(id => typeof id === 'string' && id.trim().length > 0); + + if (idsValidos.length === 0) { + return NextResponse.json( + { + success: false, + error: 'Nenhum ID válido fornecido' + }, + { status: 400 } + ); + } + + // Buscar relatórios existentes antes da exclusão (para log) + const relatoriosExistentes = await prisma.relatorio.findMany({ + where: { + id: { + in: idsValidos + } + }, + select: { + id: true, + ticker: true, + nome: true + } + }); + + if (relatoriosExistentes.length === 0) { + return NextResponse.json( + { + success: false, + error: 'Nenhum relatório encontrado com os IDs fornecidos' + }, + { status: 404 } + ); + } + + // Executar exclusão em lote + const resultado = await prisma.relatorio.deleteMany({ + where: { + id: { + in: idsValidos + } + } + }); + + console.log(`ðŸ—‘ï¸ Relatórios excluídos em lote:`, { + idsEnviados: idsValidos.length, + relatoriosEncontrados: relatoriosExistentes.length, + relatoriosExcluidos: resultado.count, + detalhes: relatoriosExistentes.map(r => `${r.ticker} - ${r.nome}`) + }); + + return NextResponse.json({ + success: true, + message: `${resultado.count} relatórios excluídos com sucesso`, + relatoriosExcluidos: resultado.count, + detalhes: relatoriosExistentes + }); + + } catch (error) { + console.error('Erro ao excluir múltiplos relatórios:', error); + + return NextResponse.json( + { + success: false, + error: 'Erro interno do servidor ao excluir relatórios', + details: error instanceof Error ? error.message : 'Erro desconhecido' + }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/relatorios/exportar/route.ts b/src/app/api/relatorios/exportar/route.ts new file mode 100644 index 000000000..611024701 --- /dev/null +++ b/src/app/api/relatorios/exportar/route.ts @@ -0,0 +1,105 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +// 📥 GET - Exportar todos os relatórios em formato JSON +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const rawLimit = Number.parseInt(searchParams.get('limit') || '1000', 10); + const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? rawLimit : 1000; + + // Buscar todos os relatórios + const relatorios = await prisma.relatorio.findMany({ + take: limit, + orderBy: [ + { createdAt: 'desc' } + ] + }); + const total = await prisma.relatorio.count(); + + if (relatorios.length === 0) { + return NextResponse.json( + { + success: false, + error: 'Nenhum relatório encontrado para exportar' + }, + { status: 404 } + ); + } + + // Converter para formato da interface (mantendo compatibilidade) + const relatoriosFormatados = relatorios.map(relatorio => ({ + id: relatorio.id, + ticker: relatorio.ticker, + nome: relatorio.nome, + tipo: relatorio.tipo, + dataReferencia: relatorio.dataReferencia, + dataUpload: relatorio.dataUpload.toISOString(), + linkCanva: relatorio.linkCanva || undefined, + linkExterno: relatorio.linkExterno || undefined, + tipoVisualizacao: relatorio.tipoVisualizacao, + arquivoPdf: relatorio.arquivoPdf || undefined, + nomeArquivoPdf: relatorio.nomeArquivoPdf || undefined, + tamanhoArquivo: relatorio.tamanhoArquivo || undefined, + tipoPdf: relatorio.tipoPdf || undefined, + hashArquivo: relatorio.hashArquivo || undefined, + solicitarReupload: relatorio.solicitarReupload || undefined + })); + + // Criar estrutura de backup com metadados + const backup = { + metadata: { + versao: '2.0', + dataExportacao: new Date().toISOString(), + totalRelatorios: total, + limit, + totalTickers: new Set(relatorios.map(r => r.ticker)).size, + tipoBackup: 'completo', + origem: 'API/Prisma', + compatibilidade: ['IndexedDB', 'localStorage', 'API'] + }, + relatorios: relatoriosFormatados, + estatisticas: { + porTicker: Array.from(new Set(relatorios.map(r => r.ticker))).map(ticker => ({ + ticker, + total: relatorios.filter(r => r.ticker === ticker).length + })), + porTipo: { + trimestral: relatorios.filter(r => r.tipo === 'trimestral').length, + anual: relatorios.filter(r => r.tipo === 'anual').length, + apresentacao: relatorios.filter(r => r.tipo === 'apresentacao').length, + outros: relatorios.filter(r => r.tipo === 'outros').length + } + } + }; + + console.log(`📥 Exportação de relatórios executada:`, { + totalRelatorios: relatorios.length, + timestamp: new Date().toISOString() + }); + + // Retornar como download de arquivo JSON + const jsonString = JSON.stringify(backup, null, 2); + + return new NextResponse(jsonString, { + status: 200, + headers: { + 'Content-Type': 'application/json', + 'Content-Disposition': `attachment; filename="relatorios_backup_${new Date().toISOString().split('T')[0]}.json"`, + 'Content-Length': jsonString.length.toString() + } + }); + + } catch (error) { + console.error('Erro ao exportar relatórios:', error); + + return NextResponse.json( + { + success: false, + error: 'Erro interno do servidor ao exportar relatórios', + details: error instanceof Error ? error.message : 'Erro desconhecido' + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/relatorios/importar/route.ts b/src/app/api/relatorios/importar/route.ts new file mode 100644 index 000000000..476f4ac9e --- /dev/null +++ b/src/app/api/relatorios/importar/route.ts @@ -0,0 +1,150 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +// 📤 POST - Importar relatórios de backup JSON +export async function POST(request: NextRequest) { + try { + const dadosImportacao = await request.json(); + + // Validar estrutura básica + if (!dadosImportacao) { + return NextResponse.json( + { + success: false, + error: 'Dados de importação não fornecidos' + }, + { status: 400 } + ); + } + + let relatoriosParaImportar: any[] = []; + + // Detectar formato dos dados (compatibilidade com diferentes versões) + if (dadosImportacao.relatorios && Array.isArray(dadosImportacao.relatorios)) { + // Formato novo (API/Prisma) + relatoriosParaImportar = dadosImportacao.relatorios; + } else if (Array.isArray(dadosImportacao)) { + // Formato de array direto + relatoriosParaImportar = dadosImportacao; + } else if (typeof dadosImportacao === 'object') { + // Formato antigo (localStorage agrupado por ticker) + relatoriosParaImportar = []; + Object.entries(dadosImportacao).forEach(([ticker, relatorios]: [string, any]) => { + if (Array.isArray(relatorios)) { + relatorios.forEach(relatorio => { + relatoriosParaImportar.push({ + ...relatorio, + ticker: relatorio.ticker || ticker + }); + }); + } + }); + } + + if (relatoriosParaImportar.length === 0) { + return NextResponse.json( + { + success: false, + error: 'Nenhum relatório válido encontrado nos dados de importação' + }, + { status: 400 } + ); + } + + // Limpar dados existentes (importação substitui tudo) + console.log('ðŸ—‘ï¸ Limpando relatórios existentes antes da importação...'); + await prisma.relatorio.deleteMany({}); + + // Preparar dados para inserção + const relatoriosValidados = relatoriosParaImportar + .filter(relatorio => { + // Validar campos obrigatórios + return relatorio.ticker && relatorio.nome && relatorio.tipo; + }) + .map(relatorio => ({ + id: relatorio.id || `${relatorio.ticker}_${Date.now()}_${Math.random()}`, + ticker: relatorio.ticker.toUpperCase(), + nome: relatorio.nome, + tipo: relatorio.tipo, + dataReferencia: relatorio.dataReferencia || '', + dataUpload: relatorio.dataUpload ? new Date(relatorio.dataUpload) : new Date(), + linkCanva: relatorio.linkCanva || null, + linkExterno: relatorio.linkExterno || null, + tipoVisualizacao: relatorio.tipoVisualizacao || 'iframe', + arquivoPdf: relatorio.arquivoPdf || null, + nomeArquivoPdf: relatorio.nomeArquivoPdf || null, + tamanhoArquivo: relatorio.tamanhoArquivo || null, + tipoPdf: relatorio.tipoPdf || null, + hashArquivo: relatorio.hashArquivo || null, + solicitarReupload: relatorio.solicitarReupload || false + })); + + if (relatoriosValidados.length === 0) { + return NextResponse.json( + { + success: false, + error: 'Nenhum relatório válido após validação' + }, + { status: 400 } + ); + } + + // Inserir em lotes para melhor performance + const LOTE_SIZE = 100; + let totalInseridos = 0; + + for (let i = 0; i < relatoriosValidados.length; i += LOTE_SIZE) { + const lote = relatoriosValidados.slice(i, i + LOTE_SIZE); + + try { + await prisma.relatorio.createMany({ + data: lote, + skipDuplicates: true // Pular duplicados se houver + }); + totalInseridos += lote.length; + } catch (loteError) { + console.warn(`Erro em lote ${i}-${i + lote.length}:`, loteError); + // Tentar inserir individualmente em caso de erro no lote + for (const relatorio of lote) { + try { + await prisma.relatorio.create({ data: relatorio }); + totalInseridos++; + } catch (individualError) { + console.warn(`Erro ao inserir relatório individual:`, relatorio.ticker, relatorio.nome); + } + } + } + } + + console.log(`📤 Importação de relatórios concluída:`, { + totalEnviados: relatoriosParaImportar.length, + totalValidados: relatoriosValidados.length, + totalInseridos, + timestamp: new Date().toISOString() + }); + + return NextResponse.json({ + success: true, + message: `Importação concluída com sucesso`, + resultados: { + totalEnviados: relatoriosParaImportar.length, + totalValidados: relatoriosValidados.length, + totalInseridos, + tickersImportados: new Set(relatoriosValidados.map(r => r.ticker)).size + }, + timestamp: new Date().toISOString() + }); + + } catch (error) { + console.error('Erro ao importar relatórios:', error); + + return NextResponse.json( + { + success: false, + error: 'Erro interno do servidor ao importar relatórios', + details: error instanceof Error ? error.message : 'Erro desconhecido' + }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/relatorios/limpar/route.ts b/src/app/api/relatorios/limpar/route.ts new file mode 100644 index 000000000..ad55095ba --- /dev/null +++ b/src/app/api/relatorios/limpar/route.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +// ðŸ—‘ï¸ DELETE - Limpar todos os relatórios do sistema +export async function DELETE(request: NextRequest) { + try { + // Obter contagem antes da exclusão + const contagemAntes = await prisma.relatorio.count(); + + if (contagemAntes === 0) { + return NextResponse.json({ + success: true, + message: 'Nenhum relatório para excluir', + relatoriosExcluidos: 0 + }); + } + + // Executar limpeza completa + const resultado = await prisma.relatorio.deleteMany({}); + + console.log(`ðŸ—‘ï¸ Limpeza completa de relatórios executada:`, { + relatoriosExcluidos: resultado.count, + timestamp: new Date().toISOString() + }); + + return NextResponse.json({ + success: true, + message: `Todos os ${resultado.count} relatórios foram removidos do sistema`, + relatoriosExcluidos: resultado.count, + timestamp: new Date().toISOString() + }); + + } catch (error) { + console.error('Erro ao limpar todos os relatórios:', error); + + return NextResponse.json( + { + success: false, + error: 'Erro interno do servidor ao limpar relatórios', + details: error instanceof Error ? error.message : 'Erro desconhecido' + }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/relatorios/lote/route.ts b/src/app/api/relatorios/lote/route.ts new file mode 100644 index 000000000..ced2f8f6f --- /dev/null +++ b/src/app/api/relatorios/lote/route.ts @@ -0,0 +1,435 @@ +import { NextRequest, NextResponse } from 'next/server'; +import prisma from '@/lib/prisma'; + +const TIPOS_VALIDOS = new Set(['trimestral', 'anual', 'apresentacao', 'outros']); +const TIPOS_VISUALIZACAO_VALIDOS = new Set(['iframe', 'canva', 'link', 'pdf']); + +type MetadataUpload = { + nomeArquivo?: string; + tamanhoArquivo?: number; + totalItens?: number; + itensProcessados?: number; +}; + +type ErroProcessamento = { + index: number; + ticker?: string; + nome?: string; + motivo: string; +}; + +type RelatorioLotePayload = { + relatorios?: unknown; + metadata?: unknown; +}; + +type RelatorioNormalizado = { + index: number; + ticker: string; + nome: string; + dataReferencia: string; + dados: { + ticker: string; + nome: string; + tipo: string; + dataReferencia: string; + dataUpload: Date; + linkCanva: string | null; + linkExterno: string | null; + tipoVisualizacao: string; + arquivoPdf: string | null; + nomeArquivoPdf: string | null; + tamanhoArquivo: number | null; + tipoPdf: string | null; + hashArquivo: string | null; + solicitarReupload: boolean; + }; +}; + +function normalizarTipo(valor: unknown): 'trimestral' | 'anual' | 'apresentacao' | 'outros' { + if (typeof valor === 'string') { + const normalizado = valor.toLowerCase(); + if (TIPOS_VALIDOS.has(normalizado)) { + return normalizado as 'trimestral' | 'anual' | 'apresentacao' | 'outros'; + } + } + return 'outros'; +} + +function normalizarTipoVisualizacao(valor: unknown): 'iframe' | 'canva' | 'link' | 'pdf' { + if (typeof valor === 'string') { + const normalizado = valor.toLowerCase(); + if (TIPOS_VISUALIZACAO_VALIDOS.has(normalizado)) { + return normalizado as 'iframe' | 'canva' | 'link' | 'pdf'; + } + } + return 'iframe'; +} + +function parseDataUpload(valor: unknown): Date { + if (typeof valor === 'string') { + const data = new Date(valor); + if (!Number.isNaN(data.getTime())) { + return data; + } + } else if (valor instanceof Date && !Number.isNaN(valor.getTime())) { + return valor; + } + return new Date(); +} + +function toOptionalString(valor: unknown): string | null { + if (typeof valor === 'string') { + const texto = valor.trim(); + return texto.length > 0 ? texto : null; + } + return null; +} + +function toOptionalNumber(valor: unknown): number | null { + if (typeof valor === 'number' && Number.isFinite(valor)) { + return valor; + } + if (typeof valor === 'string') { + const numero = Number(valor); + if (!Number.isNaN(numero)) { + return numero; + } + } + return null; +} + +function toBoolean(valor: unknown, padrao = false): boolean { + if (typeof valor === 'boolean') { + return valor; + } + if (typeof valor === 'string') { + const texto = valor.trim().toLowerCase(); + if (['true', '1', 'sim', 'yes'].includes(texto)) { + return true; + } + if (['false', '0', 'nao', 'não', 'no'].includes(texto)) { + return false; + } + } + return padrao; +} + +function normalizarMetadata(valor: unknown): MetadataUpload & { + recebidoEm: string; +} { + const metadata: MetadataUpload = {}; + + if (valor && typeof valor === 'object') { + const dados = valor as Record; + + if (typeof dados.nomeArquivo === 'string' && dados.nomeArquivo.trim()) { + metadata.nomeArquivo = dados.nomeArquivo.trim(); + } + + const tamanho = toOptionalNumber(dados.tamanhoArquivo); + if (typeof tamanho === 'number') { + metadata.tamanhoArquivo = tamanho; + } + + const totalItens = toOptionalNumber(dados.totalItens); + if (typeof totalItens === 'number') { + metadata.totalItens = totalItens; + } + + const itensProcessados = toOptionalNumber(dados.itensProcessados); + if (typeof itensProcessados === 'number') { + metadata.itensProcessados = itensProcessados; + } + } + + return { + ...metadata, + recebidoEm: new Date().toISOString(), + }; +} + +function normalizarRelatorios(relatorios: unknown): { + validos: RelatorioNormalizado[]; + erros: ErroProcessamento[]; +} { + const erros: ErroProcessamento[] = []; + const validos: RelatorioNormalizado[] = []; + const combinacoesProcessadas = new Set(); + + if (!Array.isArray(relatorios)) { + return { validos, erros: [{ index: -1, motivo: 'Formato inválido: relatorios deve ser um array' }] }; + } + + relatorios.forEach((item, index) => { + if (!item || typeof item !== 'object') { + erros.push({ index, motivo: 'Item inválido: esperado objeto' }); + return; + } + + const dados = item as Record; + const ticker = typeof dados.ticker === 'string' ? dados.ticker.trim().toUpperCase() : ''; + const nome = typeof dados.nome === 'string' ? dados.nome.trim() : ''; + + if (!ticker) { + erros.push({ index, motivo: 'Ticker não informado ou inválido' }); + return; + } + + if (!nome) { + erros.push({ index, ticker, motivo: 'Nome do relatório não informado' }); + return; + } + + const tipo = normalizarTipo(dados.tipo); + const tipoVisualizacao = normalizarTipoVisualizacao(dados.tipoVisualizacao); + const dataReferencia = typeof dados.dataReferencia === 'string' ? dados.dataReferencia.trim() : ''; + const dataUpload = parseDataUpload(dados.dataUpload); + + const combinacao = `${ticker}::${nome}::${dataReferencia}`; + if (combinacoesProcessadas.has(combinacao)) { + erros.push({ index, ticker, nome, motivo: 'Relatório duplicado no lote (mesmo ticker/nome/dataReferencia)' }); + return; + } + + combinacoesProcessadas.add(combinacao); + + validos.push({ + index, + ticker, + nome, + dataReferencia, + dados: { + ticker, + nome, + tipo, + dataReferencia, + dataUpload, + linkCanva: toOptionalString(dados.linkCanva), + linkExterno: toOptionalString(dados.linkExterno), + tipoVisualizacao, + arquivoPdf: toOptionalString(dados.arquivoPdf), + nomeArquivoPdf: toOptionalString(dados.nomeArquivoPdf), + tamanhoArquivo: toOptionalNumber(dados.tamanhoArquivo), + tipoPdf: toOptionalString(dados.tipoPdf), + hashArquivo: toOptionalString(dados.hashArquivo), + solicitarReupload: toBoolean(dados.solicitarReupload), + }, + }); + }); + + return { validos, erros }; +} + +export async function POST(request: NextRequest) { + const inicioProcessamento = new Date(); + + try { + const payload = (await request.json()) as RelatorioLotePayload; + const { relatorios, metadata } = payload; + + if (!relatorios || !Array.isArray(relatorios) || relatorios.length === 0) { + return NextResponse.json( + { + success: false, + error: 'É necessário enviar ao menos um relatório para processamento', + }, + { status: 400 }, + ); + } + + const metadataNormalizada = metadata ? normalizarMetadata(metadata) : undefined; + + console.log('📥 Recebendo lote de relatórios', { + totalRecebidos: relatorios.length, + metadata: metadataNormalizada, + }); + + const { validos, erros: errosValidacao } = normalizarRelatorios(relatorios); + + if (validos.length === 0) { + return NextResponse.json( + { + success: false, + error: 'Nenhum relatório válido para processamento', + erros: errosValidacao, + metadata: metadataNormalizada, + }, + { status: 400 }, + ); + } + + const gerarChaveCombinacao = (ticker: string, nome: string, dataReferencia: string) => + `${ticker.toUpperCase()}::${nome}::${dataReferencia}`; + + const errosProcessamento: ErroProcessamento[] = [...errosValidacao]; + let totalCriados = 0; + let totalDuplicados = 0; + + const condicoesBusca = validos.map((relatorio) => ({ + ticker: relatorio.dados.ticker, + nome: relatorio.dados.nome, + dataReferencia: relatorio.dados.dataReferencia, + })); + + let combinacoesExistentes = new Set(); + + if (condicoesBusca.length > 0) { + const relatoriosExistentes = await prisma.relatorio.findMany({ + where: { + OR: condicoesBusca, + }, + select: { + ticker: true, + nome: true, + dataReferencia: true, + }, + }); + + combinacoesExistentes = new Set( + relatoriosExistentes.map((item) => + gerarChaveCombinacao(item.ticker, item.nome, item.dataReferencia), + ), + ); + } + + const relatoriosParaCriar: RelatorioNormalizado[] = []; + + validos.forEach((relatorio) => { + const combinacao = gerarChaveCombinacao( + relatorio.dados.ticker, + relatorio.dados.nome, + relatorio.dados.dataReferencia, + ); + + if (combinacoesExistentes.has(combinacao)) { + totalDuplicados += 1; + errosProcessamento.push({ + index: relatorio.index, + ticker: relatorio.ticker, + nome: relatorio.nome, + motivo: 'Já existe relatório com mesmo ticker, nome e data de referência', + }); + return; + } + + relatoriosParaCriar.push(relatorio); + }); + + if (relatoriosParaCriar.length > 0) { + try { + const relatoriosCriados = await prisma.$transaction(async (tx) => { + await tx.relatorio.createMany({ + data: relatoriosParaCriar.map((relatorio) => relatorio.dados), + }); + + const relatoriosInseridos = await tx.relatorio.findMany({ + where: { + OR: relatoriosParaCriar.map((relatorio) => ({ + ticker: relatorio.dados.ticker, + nome: relatorio.dados.nome, + dataReferencia: relatorio.dados.dataReferencia, + })), + }, + select: { + id: true, + ticker: true, + nome: true, + dataReferencia: true, + arquivoPdf: true, + }, + }); + + for (const relatorio of relatoriosInseridos) { + await tx.reportFile.create({ + data: { + reportId: relatorio.id, + status: 'pendente', + pdfUrl: relatorio.arquivoPdf ?? null, + }, + }); + } + + return relatoriosInseridos; + }); + + totalCriados += relatoriosCriados.length; + } catch (erroTransacao) { + console.error('⌠Erro na transação de criação de relatórios em lote', { + error: erroTransacao, + }); + + relatoriosParaCriar.forEach((relatorio) => { + errosProcessamento.push({ + index: relatorio.index, + ticker: relatorio.ticker, + nome: relatorio.nome, + motivo: + erroTransacao instanceof Error + ? erroTransacao.message + : 'Erro ao salvar relatório em lote', + }); + }); + } + } + + const fimProcessamento = new Date(); + + const sucessoTotal = errosProcessamento.length === 0; + const houveSucessoParcial = !sucessoTotal && totalCriados > 0; + const nenhumRelatorioCriado = totalCriados === 0; + + let message: string; + if (sucessoTotal) { + message = `Processamento concluído com sucesso (${totalCriados}/${relatorios.length}).`; + } else if (nenhumRelatorioCriado) { + message = + totalDuplicados > 0 + ? 'Nenhum relatório criado: todos os itens já existiam.' + : 'Nenhum relatório criado devido a erros durante o processamento.'; + } else { + message = `Processamento concluído com avisos. Inseridos ${totalCriados} de ${relatorios.length} relatórios.`; + } + + const resposta = { + success: sucessoTotal, + partialSuccess: houveSucessoParcial, + message, + resultados: { + totalRecebidos: relatorios.length, + totalValidos: validos.length, + totalCriados, + totalDuplicados, + totalComErro: errosProcessamento.length, + duracaoMs: fimProcessamento.getTime() - inicioProcessamento.getTime(), + }, + metadata: metadataNormalizada, + erros: errosProcessamento, + }; + + console.log('📦 Resultado do processamento em lote', { + totalRecebidos: resposta.resultados.totalRecebidos, + totalValidos: resposta.resultados.totalValidos, + totalCriados, + totalDuplicados, + totalErros: errosProcessamento.length, + sucessoTotal, + houveSucessoParcial, + }); + + const statusCode = sucessoTotal ? 201 : totalDuplicados > 0 ? 409 : 400; + + return NextResponse.json(resposta, { status: statusCode }); + } catch (error) { + console.error('Erro ao processar lote de relatórios:', error); + + return NextResponse.json( + { + success: false, + error: 'Erro interno do servidor ao processar lote de relatórios', + details: error instanceof Error ? error.message : 'Erro desconhecido', + }, + { status: 500 }, + ); + } +} \ No newline at end of file diff --git a/src/app/api/relatorios/route.ts b/src/app/api/relatorios/route.ts new file mode 100644 index 000000000..3198bbd7a --- /dev/null +++ b/src/app/api/relatorios/route.ts @@ -0,0 +1,213 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { prisma } from '@/lib/prisma'; +import { enforceRateLimit } from '@/lib/rate-limit'; + +const relatorioSchema = z.object({ + ticker: z.string().min(1), + nome: z.string().min(1), + tipo: z.enum(['trimestral', 'anual', 'apresentacao', 'outros']), + dataReferencia: z.string().optional(), + linkCanva: z.string().url().optional().nullable(), + linkExterno: z.string().url().optional().nullable(), + tipoVisualizacao: z.enum(['iframe', 'canva', 'link', 'pdf']).optional(), + arquivoPdf: z.string().optional().nullable(), + nomeArquivoPdf: z.string().optional().nullable(), + tamanhoArquivo: z.number().optional().nullable(), + tipoPdf: z.enum(['base64', 'referencia']).optional().nullable(), + hashArquivo: z.string().optional().nullable(), + solicitarReupload: z.boolean().optional(), +}); + +// 📊 GET - Listar todos os relatórios +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const ticker = searchParams.get('ticker'); + + let relatorios; + + if (ticker) { + // Buscar relatórios de um ticker específico + relatorios = await prisma.relatorio.findMany({ + where: { + ticker: ticker.toUpperCase() + }, + orderBy: [ + { dataReferencia: 'desc' }, + { dataUpload: 'desc' } + ] + }); + } else { + // Buscar todos os relatórios + relatorios = await prisma.relatorio.findMany({ + orderBy: [ + { ticker: 'asc' }, + { dataReferencia: 'desc' } + ], + include: { + reportFiles: { + orderBy: { createdAt: 'desc' }, + take: 1, + }, + }, + }); + } + + // Converter dados do Prisma para formato da interface + const relatoriosFormatados = relatorios.map(relatorio => ({ + id: relatorio.id, + ticker: relatorio.ticker, + nome: relatorio.nome, + tipo: relatorio.tipo as 'trimestral' | 'anual' | 'apresentacao' | 'outros', + dataReferencia: relatorio.dataReferencia, + dataUpload: relatorio.dataUpload.toISOString(), + linkCanva: relatorio.linkCanva || undefined, + linkExterno: relatorio.linkExterno || undefined, + tipoVisualizacao: relatorio.tipoVisualizacao as 'iframe' | 'canva' | 'link' | 'pdf', + arquivoPdf: relatorio.arquivoPdf || undefined, + nomeArquivoPdf: relatorio.nomeArquivoPdf || undefined, + tamanhoArquivo: relatorio.tamanhoArquivo || undefined, + tipoPdf: (relatorio.tipoPdf as 'base64' | 'referencia') || undefined, + hashArquivo: relatorio.hashArquivo || undefined, + solicitarReupload: relatorio.solicitarReupload || undefined, + status: relatorio.reportFiles[0]?.status || undefined, + pdfUrl: relatorio.reportFiles[0]?.pdfUrl || undefined, + })); + + return NextResponse.json( + { + success: true, + relatorios: relatoriosFormatados, + total: relatoriosFormatados.length, + }, + { + headers: { + 'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=120', + }, + } + ); + + } catch (error) { + console.error('Erro ao buscar relatórios:', error); + return NextResponse.json( + { + success: false, + error: 'Erro interno do servidor ao buscar relatórios', + details: error instanceof Error ? error.message : 'Erro desconhecido' + }, + { status: 500 } + ); + } +} + +// âž• POST - Criar novo relatório +export async function POST(request: NextRequest) { + try { + const rateLimitKey = `relatorios:create:${request.ip ?? 'unknown'}`; + const rateLimit = await enforceRateLimit(rateLimitKey, 20, 60_000); + if (!rateLimit.allowed) { + return NextResponse.json( + { + success: false, + error: 'Muitas requisições. Tente novamente em instantes.', + }, + { + status: 429, + headers: rateLimit.retryAfter + ? { 'Retry-After': rateLimit.retryAfter.toString() } + : undefined, + } + ); + } + + const payload = await request.json(); + const parsed = relatorioSchema.safeParse(payload); + + if (!parsed.success) { + return NextResponse.json( + { success: false, error: 'Payload inválido', issues: parsed.error.issues }, + { status: 400 } + ); + } + + const dados = parsed.data; + + // Criar relatório no banco + const novoRelatorio = await prisma.relatorio.create({ + data: { + ticker: dados.ticker.toUpperCase(), + nome: dados.nome, + tipo: dados.tipo, + dataReferencia: dados.dataReferencia || '', + dataUpload: new Date(), + linkCanva: dados.linkCanva || null, + linkExterno: dados.linkExterno || null, + tipoVisualizacao: dados.tipoVisualizacao || 'iframe', + arquivoPdf: dados.arquivoPdf || null, + nomeArquivoPdf: dados.nomeArquivoPdf || null, + tamanhoArquivo: dados.tamanhoArquivo || null, + tipoPdf: dados.tipoPdf || null, + hashArquivo: dados.hashArquivo || null, + solicitarReupload: dados.solicitarReupload || false, + }, + include: { reportFiles: true }, + }); + + // Registrar status inicial como "pendente" + await prisma.reportFile.create({ + data: { + reportId: novoRelatorio.id, + status: 'pendente', + pdfUrl: novoRelatorio.arquivoPdf || null, + }, + }); + + return NextResponse.json({ + success: true, + message: 'Relatório criado com sucesso', + relatorio: { + id: novoRelatorio.id, + ticker: novoRelatorio.ticker, + nome: novoRelatorio.nome, + tipo: novoRelatorio.tipo, + dataReferencia: novoRelatorio.dataReferencia, + dataUpload: novoRelatorio.dataUpload.toISOString(), + linkCanva: novoRelatorio.linkCanva || undefined, + linkExterno: novoRelatorio.linkExterno || undefined, + tipoVisualizacao: novoRelatorio.tipoVisualizacao, + arquivoPdf: novoRelatorio.arquivoPdf || undefined, + nomeArquivoPdf: novoRelatorio.nomeArquivoPdf || undefined, + tamanhoArquivo: novoRelatorio.tamanhoArquivo || undefined, + tipoPdf: novoRelatorio.tipoPdf || undefined, + hashArquivo: novoRelatorio.hashArquivo || undefined, + solicitarReupload: novoRelatorio.solicitarReupload || undefined, + status: 'pendente', + pdfUrl: novoRelatorio.arquivoPdf || undefined, + } + }, { status: 201 }); + + } catch (error) { + console.error('Erro ao criar relatório:', error); + + // Tratar erro de duplicação se existir + if (error instanceof Error && error.message.includes('Unique constraint')) { + return NextResponse.json( + { + success: false, + error: 'Já existe um relatório com esses dados' + }, + { status: 409 } + ); + } + + return NextResponse.json( + { + success: false, + error: 'Erro interno do servidor ao criar relatório', + details: error instanceof Error ? error.message : 'Erro desconhecido' + }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/relatorios/ticker/[ticker]/route.ts b/src/app/api/relatorios/ticker/[ticker]/route.ts new file mode 100644 index 000000000..ac3b8887c --- /dev/null +++ b/src/app/api/relatorios/ticker/[ticker]/route.ts @@ -0,0 +1,147 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +// ðŸ—‘ï¸ DELETE - Excluir todos os relatórios de um ticker específico +export async function DELETE( + request: NextRequest, + { params }: { params: { ticker: string } } +) { + try { + const ticker = params.ticker?.toUpperCase(); + + // Validar ticker + if (!ticker || ticker.trim().length === 0) { + return NextResponse.json( + { + success: false, + error: 'Ticker é obrigatório' + }, + { status: 400 } + ); + } + + // Buscar relatórios do ticker antes da exclusão (para log e contagem) + const relatoriosDoTicker = await prisma.relatorio.findMany({ + where: { + ticker: ticker + }, + select: { + id: true, + nome: true, + tipo: true, + dataReferencia: true + } + }); + + if (relatoriosDoTicker.length === 0) { + return NextResponse.json( + { + success: false, + error: `Nenhum relatório encontrado para o ticker ${ticker}` + }, + { status: 404 } + ); + } + + // Executar exclusão + const resultado = await prisma.relatorio.deleteMany({ + where: { + ticker: ticker + } + }); + + console.log(`ðŸ—‘ï¸ Relatórios do ticker ${ticker} excluídos:`, { + ticker, + quantidadeExcluida: resultado.count, + relatorios: relatoriosDoTicker.map(r => `${r.nome} (${r.tipo}) - ${r.dataReferencia}`) + }); + + return NextResponse.json({ + success: true, + message: `Todos os ${resultado.count} relatórios do ticker ${ticker} foram excluídos`, + ticker, + relatoriosExcluidos: resultado.count, + detalhes: relatoriosDoTicker + }); + + } catch (error) { + console.error('Erro ao excluir relatórios por ticker:', error); + + return NextResponse.json( + { + success: false, + error: 'Erro interno do servidor ao excluir relatórios do ticker', + details: error instanceof Error ? error.message : 'Erro desconhecido' + }, + { status: 500 } + ); + } +} + +// 📊 GET - Buscar relatórios de um ticker específico (funcionalidade extra) +export async function GET( + request: NextRequest, + { params }: { params: { ticker: string } } +) { + try { + const ticker = params.ticker?.toUpperCase(); + + if (!ticker) { + return NextResponse.json( + { + success: false, + error: 'Ticker é obrigatório' + }, + { status: 400 } + ); + } + + const relatorios = await prisma.relatorio.findMany({ + where: { + ticker: ticker + }, + orderBy: [ + { dataReferencia: 'desc' }, + { dataUpload: 'desc' } + ] + }); + + // Converter para formato da interface + const relatoriosFormatados = relatorios.map(relatorio => ({ + id: relatorio.id, + ticker: relatorio.ticker, + nome: relatorio.nome, + tipo: relatorio.tipo as 'trimestral' | 'anual' | 'apresentacao' | 'outros', + dataReferencia: relatorio.dataReferencia, + dataUpload: relatorio.dataUpload.toISOString(), + linkCanva: relatorio.linkCanva || undefined, + linkExterno: relatorio.linkExterno || undefined, + tipoVisualizacao: relatorio.tipoVisualizacao as 'iframe' | 'canva' | 'link' | 'pdf', + arquivoPdf: relatorio.arquivoPdf || undefined, + nomeArquivoPdf: relatorio.nomeArquivoPdf || undefined, + tamanhoArquivo: relatorio.tamanhoArquivo || undefined, + tipoPdf: (relatorio.tipoPdf as 'base64' | 'referencia') || undefined, + hashArquivo: relatorio.hashArquivo || undefined, + solicitarReupload: relatorio.solicitarReupload || undefined + })); + + return NextResponse.json({ + success: true, + ticker, + relatorios: relatoriosFormatados, + total: relatoriosFormatados.length + }); + + } catch (error) { + console.error('Erro ao buscar relatórios por ticker:', error); + + return NextResponse.json( + { + success: false, + error: 'Erro interno do servidor ao buscar relatórios do ticker', + details: error instanceof Error ? error.message : 'Erro desconhecido' + }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/report-files/route.ts b/src/app/api/report-files/route.ts new file mode 100644 index 000000000..d30a5f2c2 --- /dev/null +++ b/src/app/api/report-files/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +// 📦 Registrar status de processamento dos relatórios +export async function POST(request: NextRequest) { + try { + const { reportId, pdfUrl, status } = await request.json(); + + if (!reportId || !status) { + return NextResponse.json( + { + success: false, + error: 'Campos obrigatórios: reportId e status', + }, + { status: 400 } + ); + } + + const registro = await prisma.reportFile.create({ + data: { + reportId, + pdfUrl: pdfUrl || null, + status, + }, + }); + + return NextResponse.json( + { success: true, reportFile: registro }, + { status: 201 } + ); + } catch (error) { + console.error('Erro ao registrar status do relatório:', error); + return NextResponse.json( + { success: false, error: 'Erro interno do servidor' }, + { status: 500 } + ); + } +} + diff --git a/src/app/api/reports/[id]/route.ts b/src/app/api/reports/[id]/route.ts new file mode 100644 index 000000000..65f160689 --- /dev/null +++ b/src/app/api/reports/[id]/route.ts @@ -0,0 +1,19 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' + +export async function GET( + _request: NextRequest, + { params }: { params: { id: string } } +) { + const report = await prisma.report.findUnique({ where: { id: params.id } }) + + if (!report) { + return NextResponse.json( + { success: false, error: 'Report not found' }, + { status: 404 } + ) + } + + return NextResponse.json({ success: true, report }) +} + diff --git a/src/app/api/reports/batch-upload/route.ts b/src/app/api/reports/batch-upload/route.ts new file mode 100644 index 000000000..2d8fd6abb --- /dev/null +++ b/src/app/api/reports/batch-upload/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { reportQueue } from '@/lib/reportQueue' + +function extractCanvaId(input: string): string { + const match = input.match(/design\/([A-Za-z0-9_-]+)/) + return match ? match[1] : input +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const links: string[] = body?.links + + if (!Array.isArray(links) || links.length === 0) { + return NextResponse.json( + { success: false, error: 'links must be a non-empty array' }, + { status: 400 } + ) + } + + const createdReports = [] + + for (const link of links) { + const canvaId = extractCanvaId(link) + const report = await prisma.report.create({ + data: { canvaId } + }) + + reportQueue.enqueue({ reportId: report.id, canvaId }) + + createdReports.push({ id: report.id, status: report.status }) + } + + return NextResponse.json({ success: true, reports: createdReports }) + } catch (error) { + console.error('Erro no batch upload:', error) + return NextResponse.json( + { success: false, error: 'Erro interno ao processar uploads' }, + { status: 500 } + ) + } +} + diff --git a/src/app/api/response-templates/[id]/route.ts b/src/app/api/response-templates/[id]/route.ts new file mode 100644 index 000000000..b6c0aef22 --- /dev/null +++ b/src/app/api/response-templates/[id]/route.ts @@ -0,0 +1,252 @@ +// src/app/api/response-templates/[id]/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { verifyToken } from '@/utils/auth'; + +// GET - Buscar template específico +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const token = request.headers.get('Authorization')?.replace('Bearer ', ''); + + let user; + try { + const payload = verifyToken(token); + user = await prisma.user.findUnique({ where: { email: payload.email } }); + } catch (error) { + return NextResponse.json({ error: (error as Error).message }, { status: 401 }); + } + + if (!user || (user.plan !== 'ADMIN' && user.plan !== 'ANALISTA')) { + return NextResponse.json({ error: 'Admin access required' }, { status: 403 }); + } + + const template = await prisma.responseTemplate.findUnique({ + where: { id: params.id }, + include: { + creator: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + } + }); + + if (!template) { + return NextResponse.json({ error: 'Template not found' }, { status: 404 }); + } + + return NextResponse.json({ template }); + + } catch (error) { + console.error('Error fetching template:', error); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } finally { + await prisma.$disconnect(); + } +} + +// PUT - Atualizar template +export async function PUT( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const token = request.headers.get('Authorization')?.replace('Bearer ', ''); + + let user; + try { + const payload = verifyToken(token); + user = await prisma.user.findUnique({ where: { email: payload.email } }); + } catch (error) { + return NextResponse.json({ error: (error as Error).message }, { status: 401 }); + } + + if (!user || (user.plan !== 'ADMIN' && user.plan !== 'ANALISTA')) { + return NextResponse.json({ error: 'Admin access required' }, { status: 403 }); + } + + const template = await prisma.responseTemplate.findUnique({ + where: { id: params.id } + }); + + if (!template) { + return NextResponse.json({ error: 'Template not found' }, { status: 404 }); + } + + const body = await request.json(); + const { title, content, category, isActive } = body; + + // Validações + if (title && typeof title !== 'string') { + return NextResponse.json({ + error: 'Title must be a string' + }, { status: 400 }); + } + + if (content && typeof content !== 'string') { + return NextResponse.json({ + error: 'Content must be a string' + }, { status: 400 }); + } + + if (title && title.trim().length === 0) { + return NextResponse.json({ + error: 'Title cannot be empty' + }, { status: 400 }); + } + + if (content && content.trim().length === 0) { + return NextResponse.json({ + error: 'Content cannot be empty' + }, { status: 400 }); + } + + if (title && title.length > 200) { + return NextResponse.json({ + error: 'Title must be less than 200 characters' + }, { status: 400 }); + } + + if (content && content.length > 5000) { + return NextResponse.json({ + error: 'Content must be less than 5000 characters' + }, { status: 400 }); + } + + // Verificar duplicata de título na mesma categoria (se título ou categoria mudaram) + if (title || category) { + const newTitle = title ? title.trim() : template.title; + const newCategory = category || template.category; + + const existingTemplate = await prisma.responseTemplate.findFirst({ + where: { + title: newTitle, + category: newCategory, + id: { not: params.id } // Excluir o template atual da busca + } + }); + + if (existingTemplate) { + return NextResponse.json({ + error: 'A template with this title already exists in this category' + }, { status: 400 }); + } + } + + // Preparar dados para atualização + const updateData: any = {}; + + if (title !== undefined) updateData.title = title.trim(); + if (content !== undefined) updateData.content = content.trim(); + if (category !== undefined) updateData.category = category; + if (isActive !== undefined) updateData.isActive = Boolean(isActive); + + const updatedTemplate = await prisma.responseTemplate.update({ + where: { id: params.id }, + data: updateData, + include: { + creator: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + } + }); + + return NextResponse.json({ template: updatedTemplate }); + + } catch (error) { + console.error('Error updating template:', error); + + // Tratamento específico de erros do Prisma + if (error && typeof error === 'object' && 'code' in error) { + const prismaError = error as any; + + if (prismaError.code === 'P2002') { + return NextResponse.json({ + error: 'A template with this title already exists in this category' + }, { status: 400 }); + } + } + + return NextResponse.json({ + error: 'Internal server error', + details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined + }, { status: 500 }); + } finally { + await prisma.$disconnect(); + } +} + +// DELETE - Deletar template +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const token = request.headers.get('Authorization')?.replace('Bearer ', ''); + + let user; + try { + const payload = verifyToken(token); + user = await prisma.user.findUnique({ where: { email: payload.email } }); + } catch (error) { + return NextResponse.json({ error: (error as Error).message }, { status: 401 }); + } + + if (!user || (user.plan !== 'ADMIN' && user.plan !== 'ANALISTA')) { + return NextResponse.json({ error: 'Admin access required' }, { status: 403 }); + } + + const template = await prisma.responseTemplate.findUnique({ + where: { id: params.id } + }); + + if (!template) { + return NextResponse.json({ error: 'Template not found' }, { status: 404 }); + } + + // Verificar se o template está sendo usado antes de deletar + if (template.usageCount > 0) { + return NextResponse.json({ + error: 'Cannot delete template that has been used. Consider deactivating it instead.' + }, { status: 400 }); + } + + await prisma.responseTemplate.delete({ + where: { id: params.id } + }); + + return NextResponse.json({ message: 'Template deleted successfully' }); + + } catch (error) { + console.error('Error deleting template:', error); + + // Tratamento específico de erros do Prisma + if (error && typeof error === 'object' && 'code' in error) { + const prismaError = error as any; + + if (prismaError.code === 'P2025') { + return NextResponse.json({ + error: 'Template not found' + }, { status: 404 }); + } + } + + return NextResponse.json({ + error: 'Internal server error', + details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined + }, { status: 500 }); + } finally { + await prisma.$disconnect(); + } +} \ No newline at end of file diff --git a/src/app/api/response-templates/[id]/use/route.ts b/src/app/api/response-templates/[id]/use/route.ts new file mode 100644 index 000000000..40ea61bde --- /dev/null +++ b/src/app/api/response-templates/[id]/use/route.ts @@ -0,0 +1,86 @@ +// src/app/api/response-templates/[id]/use/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { verifyToken } from '@/utils/auth'; + +// POST - Incrementar uso do template +export async function POST( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const token = request.headers.get('Authorization')?.replace('Bearer ', ''); + + let user; + try { + const payload = verifyToken(token); + user = await prisma.user.findUnique({ where: { email: payload.email } }); + } catch (error) { + return NextResponse.json({ error: (error as Error).message }, { status: 401 }); + } + + if (!user || (user.plan !== 'ADMIN' && user.plan !== 'ANALISTA')) { + return NextResponse.json({ error: 'Admin access required' }, { status: 403 }); + } + + // Verificar se template existe e está ativo + const template = await prisma.responseTemplate.findUnique({ + where: { id: params.id } + }); + + if (!template) { + return NextResponse.json({ error: 'Template not found' }, { status: 404 }); + } + + if (!template.isActive) { + return NextResponse.json({ + error: 'Cannot use inactive template' + }, { status: 400 }); + } + + const updatedTemplate = await prisma.responseTemplate.update({ + where: { id: params.id }, + data: { + usageCount: { + increment: 1 + } + }, + include: { + creator: { // ✅ CORRETO: usar 'creator' não 'createdBy' + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + } + }); + + return NextResponse.json({ + template: updatedTemplate, + message: 'Template usage recorded' + }); + + } catch (error) { + console.error('Error recording template usage:', error); + + // Tratamento específico de erros do Prisma + if (error && typeof error === 'object' && 'code' in error) { + const prismaError = error as any; + + if (prismaError.code === 'P2025') { + return NextResponse.json({ + error: 'Template not found' + }, { status: 404 }); + } + } + + return NextResponse.json({ + error: 'Internal server error', + details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined + }, { status: 500 }); + } finally { + await prisma.$disconnect(); + } +} \ No newline at end of file diff --git a/src/app/api/response-templates/route.ts b/src/app/api/response-templates/route.ts new file mode 100644 index 000000000..31d110904 --- /dev/null +++ b/src/app/api/response-templates/route.ts @@ -0,0 +1,198 @@ +// src/app/api/response-templates/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { auth } from '@/lib/auth'; + +// GET - Listar templates +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = session.user; + + if (user.plan !== 'ADMIN' && user.plan !== 'ANALISTA') { + return NextResponse.json({ error: 'Admin access required' }, { status: 403 }); + } + + const category = searchParams.get('category'); + const isActive = searchParams.get('isActive'); + const page = parseInt(searchParams.get('page') || '1'); + const limit = parseInt(searchParams.get('limit') || '10'); + + let whereClause: any = {}; + + if (category && category !== 'ALL') { + whereClause.category = category; + } + + if (isActive !== null && isActive !== undefined) { + whereClause.isActive = isActive === 'true'; + } + + const templates = await prisma.responseTemplate.findMany({ + where: whereClause, + include: { + creator: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + }, + orderBy: [ + { usageCount: 'desc' }, + { createdAt: 'desc' } + ], + skip: (page - 1) * limit, + take: limit + }); + + const total = await prisma.responseTemplate.count({ + where: whereClause + }); + + return NextResponse.json({ + templates, + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit) + } + }); + + } catch (error) { + console.error('Error fetching templates:', error); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } finally { + await prisma.$disconnect(); + } +} + +// POST - Criar novo template +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); + } + + const user = session.user; + + if (user.plan !== 'ADMIN' && user.plan !== 'ANALISTA') { + return NextResponse.json({ error: 'Admin access required' }, { status: 403 }); + } + + const body = await request.json(); + const { title, content, category = 'GERAL', isActive = true } = body; + + // Validações + if (!title || !content) { + return NextResponse.json({ + error: 'Title and content are required' + }, { status: 400 }); + } + + if (typeof title !== 'string' || typeof content !== 'string') { + return NextResponse.json({ + error: 'Title and content must be strings' + }, { status: 400 }); + } + + if (title.trim().length === 0 || content.trim().length === 0) { + return NextResponse.json({ + error: 'Title and content cannot be empty' + }, { status: 400 }); + } + + if (title.length > 200) { + return NextResponse.json({ + error: 'Title must be less than 200 characters' + }, { status: 400 }); + } + + if (content.length > 5000) { + return NextResponse.json({ + error: 'Content must be less than 5000 characters' + }, { status: 400 }); + } + + // Verificar se já existe um template com o mesmo título + const existingTemplate = await prisma.responseTemplate.findFirst({ + where: { + title: title.trim(), + category: category + } + }); + + if (existingTemplate) { + return NextResponse.json({ + error: 'A template with this title already exists in this category' + }, { status: 400 }); + } + + const template = await prisma.responseTemplate.create({ + data: { + title: title.trim(), + content: content.trim(), + category, + isActive: Boolean(isActive), + usageCount: 0, + createdBy: user.id // Campo correto conforme schema + }, + include: { + creator: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + } + }); + + return NextResponse.json({ template }, { status: 201 }); + + } catch (error) { + console.error('Error creating template:', error); + + // Log mais detalhado do erro + if (error instanceof Error) { + console.error('Error message:', error.message); + console.error('Error stack:', error.stack); + } + + // Verificar se é um erro de validação do Prisma + if (error && typeof error === 'object' && 'code' in error) { + const prismaError = error as any; + + if (prismaError.code === 'P2002') { + return NextResponse.json({ + error: 'A template with this title already exists' + }, { status: 400 }); + } + + if (prismaError.code === 'P2003') { + return NextResponse.json({ + error: 'Invalid user reference' + }, { status: 400 }); + } + } + + return NextResponse.json({ + error: 'Internal server error', + details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined + }, { status: 500 }); + } finally { + await prisma.$disconnect(); + } +} \ No newline at end of file diff --git a/src/app/api/response-templates/stats/route.ts b/src/app/api/response-templates/stats/route.ts new file mode 100644 index 000000000..c45eba2d0 --- /dev/null +++ b/src/app/api/response-templates/stats/route.ts @@ -0,0 +1,168 @@ +// src/app/api/response-templates/stats/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { verifyToken } from '@/utils/auth'; + +export async function GET(request: NextRequest) { + try { + const token = request.headers.get('Authorization')?.replace('Bearer ', ''); + + let user; + try { + const payload = verifyToken(token); + user = await prisma.user.findUnique({ where: { email: payload.email } }); + } catch (error) { + return NextResponse.json({ error: (error as Error).message }, { status: 401 }); + } + + if (!user || (user.plan !== 'ADMIN' && user.plan !== 'ANALISTA')) { + return NextResponse.json({ error: 'Admin access required' }, { status: 403 }); + } + + // Buscar todos os templates com título incluído + const totalTemplatesCount = await prisma.responseTemplate.count(); + const allTemplates = await prisma.responseTemplate.findMany({ + select: { + id: true, + title: true, + category: true, + isActive: true, + usageCount: true, + createdAt: true + }, + take: 100, + orderBy: { usageCount: 'desc' } + }); + + // Calcular estatísticas básicas + const totalTemplates = allTemplates.length; + const activeTemplates = allTemplates.filter(t => t.isActive).length; + const totalUsage = allTemplates.reduce((sum, t) => sum + t.usageCount, 0); + + // Estatísticas por categoria + const categoryStats = new Map(); + + allTemplates.forEach(template => { + const category = template.category; + if (!categoryStats.has(category)) { + categoryStats.set(category, { + category, + count: 0, + usage: 0, + activeCount: 0 + }); + } + + const stats = categoryStats.get(category); + stats.count += 1; + stats.usage += template.usageCount; + if (template.isActive) { + stats.activeCount += 1; + } + }); + + const byCategory = Array.from(categoryStats.values()).sort((a, b) => b.usage - a.usage); + + // Categoria mais usada + const mostUsedCategory = byCategory.length > 0 ? byCategory[0].category : null; + + // Calcular média de uso + const averageUsage = totalTemplates > 0 ? Math.round(totalUsage / totalTemplates * 10) / 10 : 0; + + // Templates criados nos últimos 30 dias + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + + const recentTemplates = allTemplates.filter(t => + new Date(t.createdAt) >= thirtyDaysAgo + ).length; + + // Estatísticas de uso (templates mais usados) + const topUsedTemplates = allTemplates + .sort((a, b) => b.usageCount - a.usageCount) + .slice(0, 5) + .map(t => ({ + id: t.id, + title: t.title, + usageCount: t.usageCount, + category: t.category + })); + + // Estatísticas mensais (últimos 6 meses) + const monthlyStats = []; + for (let i = 5; i >= 0; i--) { + const date = new Date(); + date.setMonth(date.getMonth() - i); + const startOfMonth = new Date(date.getFullYear(), date.getMonth(), 1); + const endOfMonth = new Date(date.getFullYear(), date.getMonth() + 1, 0, 23, 59, 59); + + const templatesCreated = allTemplates.filter(t => { + const createdDate = new Date(t.createdAt); + return createdDate >= startOfMonth && createdDate <= endOfMonth; + }).length; + + monthlyStats.push({ + month: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`, + templatesCreated, + monthName: date.toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' }) + }); + } + + // Estatísticas de eficiência (templates com mais uso relativo à idade) + const templatesWithEfficiency = allTemplates.map(template => { + const ageInDays = Math.max(1, Math.floor((Date.now() - new Date(template.createdAt).getTime()) / (1000 * 60 * 60 * 24))); + const efficiency = template.usageCount / ageInDays; + return { + id: template.id, + title: template.title, + usageCount: template.usageCount, + ageInDays, + efficiency: Math.round(efficiency * 100) / 100 + }; + }).sort((a, b) => b.efficiency - a.efficiency).slice(0, 5); + + // Templates inativos que nunca foram usados + const unusedInactiveTemplates = allTemplates.filter(t => + !t.isActive && t.usageCount === 0 + ).length; + + // Resposta final + const stats = { + overview: { + totalTemplates, + totalTemplatesAvailable: totalTemplatesCount, + limit: 100, + activeTemplates, + inactiveTemplates: totalTemplates - activeTemplates, + totalUsage, + averageUsage, + recentTemplates, + unusedInactiveTemplates + }, + breakdown: { + byCategory, + byUsage: topUsedTemplates, + monthlyCreation: monthlyStats, + efficiency: templatesWithEfficiency + }, + insights: { + mostUsedCategory: mostUsedCategory || 'N/A', + leastActiveCategory: byCategory.length > 0 ? + byCategory[byCategory.length - 1].category : 'N/A', + averageUsagePerTemplate: averageUsage, + totalActiveTemplates: activeTemplates, + inactiveTemplatesRatio: totalTemplates > 0 ? + Math.round(((totalTemplates - activeTemplates) / totalTemplates) * 100) : 0, + templatesNeedingAttention: unusedInactiveTemplates + } + }; + + return NextResponse.json(stats); + + } catch (error) { + console.error('Error fetching template stats:', error); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } finally { + await prisma.$disconnect(); + } +} diff --git a/src/app/api/user/me/route.ts b/src/app/api/user/me/route.ts new file mode 100644 index 000000000..61ffa6d07 --- /dev/null +++ b/src/app/api/user/me/route.ts @@ -0,0 +1,50 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { auth } from '@/auth'; +import { prisma } from '@/lib/prisma'; +import { CacheService } from '@/lib/cache-service'; + +export const maxDuration = 3; + +export async function GET(request: NextRequest) { + try { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const cacheKey = `user:me:${session.user.id}`; + + return await CacheService.withCache({ + key: cacheKey, + ttlSeconds: 60, + tags: ['auth', `user:${session.user.id}`], + fn: async () => { + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { + id: true, + firstName: true, + lastName: true, + email: true, + role: true, + avatar: true, + plan: true, + status: true, + expirationDate: true, + customPermissions: true, + origin: true, + }, + }); + + if (!user) { + throw new Error('User not found'); + } + + return NextResponse.json(user); + }, + }); + } catch (error) { + console.error('Erro em /api/user/me:', error); + return NextResponse.json({ error: 'Internal error' }, { status: 500 }); + } +} diff --git a/src/app/api/user/profile/route.ts b/src/app/api/user/profile/route.ts new file mode 100644 index 000000000..b45bcdd4e --- /dev/null +++ b/src/app/api/user/profile/route.ts @@ -0,0 +1,158 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { auth } from '@/auth'; +import { prisma } from '@/lib/prisma'; +import { CacheService } from '@/lib/cache-service'; + +const REQUEST_TIMEOUT_MS = 5000; + +function withTimeout(promise: Promise, timeoutMs: number = REQUEST_TIMEOUT_MS, message?: string) { + let timeoutId: NodeJS.Timeout | undefined; + + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => reject(new Error(message ?? `Operation timed out after ${timeoutMs}ms`)), timeoutMs); + }); + + return Promise.race([promise, timeoutPromise]).finally(() => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }); +} + +export const maxDuration = 5; + +export async function GET(request: NextRequest) { + const correlationId = crypto.randomUUID(); + let userId: string | undefined; + + try { + const session = await withTimeout(auth(), REQUEST_TIMEOUT_MS, 'Authentication timed out'); + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + userId = session.user.id; + + console.info('[GET /api/user/profile] Iniciando busca de perfil', { correlationId, userId }); + + const cacheKey = `user:profile:${session.user.id}`; + + return await withTimeout(CacheService.withCache({ + key: cacheKey, + ttlSeconds: 60, + tags: ['auth', `user:${session.user.id}`], + fn: async () => { + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { + id: true, + firstName: true, + lastName: true, + email: true, + // role: true, // ⌠REMOVIDO - campo não existe + avatar: true, + plan: true, + status: true, + expirationDate: true, + customPermissions: true, + origin: true, + }, + }); + + if (!user) { + throw new Error('User not found'); + } + + return NextResponse.json(user); + }, + }), REQUEST_TIMEOUT_MS, 'Profile lookup timed out'); + } catch (error) { + const isTimeout = error instanceof Error && error.message.toLowerCase().includes('timed out'); + const status = isTimeout ? 504 : error instanceof Error && error.message === 'User not found' ? 404 : 500; + + console.error('[GET /api/user/profile] Falha ao carregar perfil', { + correlationId, + userId, + status, + message: error instanceof Error ? error.message : 'Unknown error', + stack: error instanceof Error ? error.stack : undefined, + }); + + const errorMessage = + status === 404 + ? 'User not found' + : isTimeout + ? 'Request timed out' + : 'Internal error'; + + return NextResponse.json({ error: errorMessage }, { status }); + } +} + +export async function PUT(request: NextRequest) { + const correlationId = crypto.randomUUID(); + let userId: string | undefined; + + try { + const session = await withTimeout(auth(), REQUEST_TIMEOUT_MS, 'Authentication timed out'); + const { firstName, lastName, avatar } = await withTimeout(request.json(), REQUEST_TIMEOUT_MS, 'Body parsing timed out'); + + if (!session?.user?.id) { + return NextResponse.json({ + error: 'Usuário não autenticado' + }, { status: 401 }); + } + + userId = session.user.id; + + if ((firstName && typeof firstName !== 'string') || (lastName && typeof lastName !== 'string')) { + console.warn('[PUT /api/user/profile] Dados inválidos recebidos', { correlationId, userId }); + return NextResponse.json({ error: 'Dados inválidos' }, { status: 400 }); + } + + console.info('[PUT /api/user/profile] Atualizando perfil', { correlationId, userId }); + + // Atualizar dados do usuário (apenas nome e avatar) + const updatedUser = await withTimeout(prisma.user.update({ + where: { id: session.user.id }, + data: { + firstName, + lastName, + avatar, + updatedAt: new Date() + } + }), REQUEST_TIMEOUT_MS, 'Profile update timed out'); + + // ✅ Invalidar cache após atualização + await CacheService.invalidate(`user:profile:${session.user.id}`); + + return NextResponse.json({ + message: 'Perfil atualizado com sucesso', + user: { + id: updatedUser.id, + firstName: updatedUser.firstName, + lastName: updatedUser.lastName, + email: updatedUser.email, + avatar: updatedUser.avatar + } + }); + } catch (error) { + const isTimeout = error instanceof Error && error.message.toLowerCase().includes('timed out'); + + console.error('⌠Erro ao atualizar perfil:', { + correlationId, + userId, + message: error instanceof Error ? error.message : 'Unknown error', + stack: error instanceof Error ? error.stack : undefined, + }); + + if (isTimeout) { + return NextResponse.json({ error: 'Request timed out' }, { status: 504 }); + } + + return NextResponse.json({ + error: 'Erro interno do servidor' + }, { status: 500 }); + } + // ✅ REMOVIDO: prisma.$disconnect() - não usar com singleton +} diff --git a/src/app/api/vies/[ticker]/route.ts b/src/app/api/vies/[ticker]/route.ts new file mode 100644 index 000000000..ff4e7e9b3 --- /dev/null +++ b/src/app/api/vies/[ticker]/route.ts @@ -0,0 +1,106 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { ViesOverride } from '@prisma/client'; +import { prisma } from '@/lib/prisma'; + +import { auth } from '@/lib/auth'; + +const CARTEIRA_MODEL_MAP = { + microCaps: prisma.userMicroCaps, + smallCaps: prisma.userSmallCaps, + dividendos: prisma.userDividendos, + fiis: prisma.userFiis, + cfRendaTurbinada: prisma.userRendaTurbinada, + dividendosInternacional: prisma.userDividendosInternacional, + etfs: prisma.userEtfs, + projetoAmerica: prisma.userProjetoAmerica, + exteriorStocks: prisma.userExteriorStocks +} as const; + +type Carteira = keyof typeof CARTEIRA_MODEL_MAP; + +const VIES_OVERRIDE_VALUES: ViesOverride[] = ['AUTO', 'MANTER']; + +const normalizarTicker = (ticker: string | string[] | undefined) => + (Array.isArray(ticker) ? ticker[0] : ticker)?.toUpperCase().trim(); + +const obterCarteiraValida = (carteira: string | string[] | undefined): Carteira | null => { + const valor = Array.isArray(carteira) ? carteira[0] : carteira; + if (!valor) { + return null; + } + + return valor in CARTEIRA_MODEL_MAP ? (valor as Carteira) : null; +}; + +export async function PATCH( + request: NextRequest, + { params }: { params: { ticker?: string } } +) { + const session = await auth(); + const user = session?.user; + + if (!user) { + return NextResponse.json({ error: 'Não autenticado' }, { status: 401 }); + } + + if (user.plan !== 'ADMIN') { + return NextResponse.json({ error: 'Acesso restrito a administradores' }, { status: 403 }); + } + + const tickerNormalizado = normalizarTicker(params.ticker); + + if (!tickerNormalizado) { + return NextResponse.json({ error: 'Ticker obrigatório' }, { status: 400 }); + } + + let body: { carteira?: string; viesOverride?: ViesOverride }; + + try { + body = await request.json(); + } catch (error) { + return NextResponse.json({ error: 'JSON inválido' }, { status: 400 }); + } + + const carteiraValida = obterCarteiraValida(body.carteira); + + if (!carteiraValida) { + return NextResponse.json({ error: 'Carteira inválida' }, { status: 400 }); + } + + const overrideSolicitado = body.viesOverride as ViesOverride | undefined; + + if (!overrideSolicitado || !VIES_OVERRIDE_VALUES.includes(overrideSolicitado)) { + return NextResponse.json({ error: 'Valor de viés inválido' }, { status: 400 }); + } + + const delegate = CARTEIRA_MODEL_MAP[carteiraValida]; + + try { + const updateResult = await delegate.updateMany({ + where: { ticker: tickerNormalizado }, + data: { + viesOverride: overrideSolicitado, + editadoEm: new Date() + } + }); + + if (updateResult.count === 0) { + return NextResponse.json({ error: 'Ativo não encontrado' }, { status: 404 }); + } + + const ativosAtualizados = await delegate.findMany({ + where: { ticker: tickerNormalizado }, + orderBy: { editadoEm: 'desc' } + }); + + return NextResponse.json({ + ticker: tickerNormalizado, + carteira: carteiraValida, + viesOverride: overrideSolicitado, + ativosAtualizados + }); + } catch (error) { + console.error('Erro ao atualizar viés manual:', error); + return NextResponse.json({ error: 'Erro interno ao atualizar viés' }, { status: 500 }); + } +} diff --git a/src/app/api/webhooks/eduzz/[token]/route.ts b/src/app/api/webhooks/eduzz/[token]/route.ts new file mode 100644 index 000000000..f7310cbca --- /dev/null +++ b/src/app/api/webhooks/eduzz/[token]/route.ts @@ -0,0 +1,578 @@ +export const dynamic = 'force-dynamic'; + +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { getEduzzTokenMapping } from '@/server/integration-secrets'; +import { activateEntitlement, deactivateEntitlement, getActiveEntitlementCodes, isAddonPlan } from '@/lib/entitlements'; +import { isLifetimePlan } from '@/lib/lifetime-plans'; +import { hashPassword } from '@/lib/auth/password'; +import { normalizePaymentMethod, normalizeInstallments } from '@/lib/payment-normalization'; + +const EDUZZ_TOKEN_MAPPING = getEduzzTokenMapping(); + +// 🔠FUNÇÃO PARA DETECTAR PLANO AUTOMATICAMENTE PELOS SEUS PRODUTOS EDUZZ +function detectarPlanoEduzz(webhookData: any): { plan: string; productName: string } { + try { + console.log('🔠Detectando plano do produto Eduzz:', webhookData); + + // ✅ CORRETO - Eduzz envia product_name direto no root + const productName = webhookData.product_name || ''; + + console.log(`📋 Nome do produto Eduzz recebido: "${productName}"`); + + // 🔠DETECTAR POR NOME (seus produtos na Eduzz) + const produtoLower = productName.toLowerCase(); + + // 🌟 CLOSE FRIENDS LITE 2.0 (detectar PRIMEIRO - mais específico) + if (produtoLower.includes('close friends lite 2.0') || + produtoLower.includes('cf lite 2.0') || + produtoLower.includes('lite 2.0')) { + return { + plan: 'LITE_V2', + productName: `Close Friends LITE 2.0 - ${productName}` + }; + } + + // â­ CLOSE FRIENDS LITE ORIGINAL + if (produtoLower.includes('close friends lite') || + produtoLower.includes('cf lite') || + (produtoLower.includes('lite') && !produtoLower.includes('2.0'))) { + return { + plan: 'LITE', + productName: `Close Friends LITE - ${productName}` + }; + } + + // 👑 CLOSE FRIENDS VIP + if (produtoLower.includes('close friends vip') || + produtoLower.includes('cf vip') || + produtoLower.includes('vip') || + produtoLower.includes('turma')) { + return { + plan: 'VIP', + productName: `Close Friends VIP - ${productName}` + }; + } + + // 🢠PROJETO FIIs + if (produtoLower.includes('projeto fiis') || + produtoLower.includes('fiis') || + produtoLower.includes('fii')) { + return { + plan: 'FIIS', + productName: `Projeto FIIs - ${productName}` + }; + } + + // 🔢 FALLBACK POR VALOR (se não conseguir pelo nome) + const valor = webhookData.trans_value || 0; // ✅ Campo correto da Eduzz + console.log(`💰 Valor da compra Eduzz: R$ ${valor}`); + + if (valor > 0) { + // Ajustar valores conforme seus preços na Eduzz + if (valor >= 400) { + return { plan: 'VIP', productName: `Produto VIP - R$ ${valor}` }; + } else { + // Para valores menores, assumir LITE_V2 como padrão + return { plan: 'LITE_V2', productName: `Close Friends LITE 2.0 - R$ ${valor}` }; + } + } + + // 🎯 FALLBACK FINAL: LITE_V2 como padrão mais seguro + console.log(`âš ï¸ Não foi possível detectar o plano Eduzz específico, usando LITE_V2 como padrão`); + return { + plan: 'LITE_V2', + productName: `Produto Não Identificado - ${productName || 'Sem Nome'}` + }; + + } catch (error) { + console.error('⌠Erro ao detectar plano Eduzz:', error); + return { + plan: 'LITE_V2', + productName: 'Produto com Erro de Detecção' + }; + } +} + +// Função para gerar senha segura +function generateSecurePassword(): string { + const chars = 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789'; + const symbols = '!@#$%&*'; + let password = ''; + + for (let i = 0; i < 6; i++) { + password += chars.charAt(Math.floor(Math.random() * chars.length)); + } + for (let i = 0; i < 2; i++) { + password += symbols.charAt(Math.floor(Math.random() * symbols.length)); + } + + return password.split('').sort(() => Math.random() - 0.5).join(''); +} + +// Calcular data de expiração considerando planos vitalícios +function calculateExpirationDate(plan: string): Date | null { + if (isLifetimePlan(plan)) { + console.log(`🎉 Plano ${plan}: acesso vitalício (sem data de expiração).`); + return null; + } + + const expirationDate = new Date(); + expirationDate.setDate(expirationDate.getDate() + 365); + return expirationDate; +} + +// ✅ Normalizar telefone brasileiro +function normalizeBrazilPhone(phone: string | null | undefined): string | null { + if (!phone) return null; + + // Remove tudo exceto números + const cleaned = phone.replace(/\D/g, ''); + + // Valida se tem pelo menos 10 dígitos + if (cleaned.length < 10) return null; + + // Remove código do país se existir (55) + const withoutCountry = cleaned.startsWith('55') ? cleaned.slice(2) : cleaned; + + // Adiciona código do país de volta + return `55${withoutCountry}`; +} + +export async function POST( + request: NextRequest, + { params }: { params: { token: string } } +) { + try { + const token = params.token; + console.log(`📚 Webhook Eduzz recebido para token: ${token}`); + + // Verificar se o token existe + const integration = EDUZZ_TOKEN_MAPPING[token]; + if (!integration) { + console.log(`⌠Token Eduzz ${token} não encontrado`); + return NextResponse.json( + { error: `Token Eduzz ${token} não configurado` }, + { status: 404 } + ); + } + + console.log(`✅ Integração Eduzz encontrada: ${integration.name} → Plano ${integration.plan}`); + + let webhookData; + try { + webhookData = await request.json(); + } catch (jsonError) { + console.error('⌠Erro ao parsear JSON:', jsonError); + return NextResponse.json( + { error: 'JSON inválido' }, + { status: 400 } + ); + } + + console.log('📦 Dados do webhook Eduzz:', JSON.stringify(webhookData, null, 2)); + + // 🔠DETECTAR PLANO AUTOMATICAMENTE (DEPOIS DO PARSE) + const { plan: planoDetectado, productName: nomeDetectado } = detectarPlanoEduzz(webhookData); + + // ✅ SOBRESCREVER DADOS DA INTEGRAÇÃO COM DETECÇÃO AUTOMÃTICA + const integrationData = { + name: nomeDetectado, + plan: planoDetectado, + integrationId: integration.integrationId + }; + + console.log(`✅ Plano Eduzz detectado: ${integrationData.name} → ${integrationData.plan}`); + + const isAddonPurchase = isAddonPlan(integrationData.plan); + + // ✅ CORRETO - Extrair evento usando event_name (documentação oficial Eduzz) + const event = webhookData.event_name || ''; + console.log(`🎯 Evento Eduzz recebido: ${event}`); + + // ✅ CORRETO - Extrair informações DIRETAMENTE do webhookData (Eduzz não usa estrutura aninhada) + const buyerEmail = webhookData.cus_email || ''; + const buyerName = webhookData.cus_name || 'Cliente Eduzz'; + const buyerPhone = normalizeBrazilPhone(webhookData.cus_cel || webhookData.cus_tel); + const transactionId = webhookData.trans_cod || `ED_${Date.now()}`; + const amount = webhookData.trans_value || 0; + const productName = webhookData.product_name || ''; + + // 💳 Extrair dados de pagamento + const paymentData = webhookData.payment || {}; + const paymentMethod = paymentData?.method || null; + const installments = normalizeInstallments( + paymentData?.totalOfInstallments || 1 + ); + + console.log('🔠Dados extraídos do Eduzz:', { + event, + buyerEmail, + buyerName, + buyerPhone, + transactionId, + amount, + productName, + plan: integrationData.plan, + integrationName: integrationData.name, + token: token, + paymentMethod, + installments + }); + + // ✅ CORRETO - Processar eventos de REEMBOLSO/CANCELAMENTO (nomes oficiais Eduzz) + if (event === 'invoice_refunded' || event === 'invoice_canceled') { + console.log(`🚫 Evento de ${event} no Eduzz - revogando acesso`); + + if (!buyerEmail || !buyerEmail.includes('@')) { + console.log('⌠Email inválido para reembolso Eduzz:', buyerEmail); + return NextResponse.json({ + error: 'Email do comprador é obrigatório para processar reembolso', + received_email: buyerEmail + }, { status: 400 }); + } + + await prisma.$connect(); + + const email = buyerEmail.toLowerCase().trim(); + let user = await prisma.user.findUnique({ + where: { email } + }); + + if (user) { + const treatAsAddon = isAddonPurchase && user.plan !== integrationData.plan; + let updatedUser = user; + + console.log(`â™»ï¸ Revogando acesso do produto ${integrationData.plan} (Eduzz)`); + await deactivateEntitlement(prisma, { + userId: user.id, + code: integrationData.plan, + origin: 'EDUZZ', + }); + + const otherActiveEntitlements = await prisma.userEntitlement.findMany({ + where: { + userId: user.id, + status: 'ACTIVE', + NOT: { code: integrationData.plan }, + }, + select: { code: true, status: true, expiresAt: true }, + }); + + const hasAnotherActivePlan = getActiveEntitlementCodes(otherActiveEntitlements).length > 0; + const shouldDeactivatePrimaryPlan = !treatAsAddon && user.plan === integrationData.plan && !hasAnotherActivePlan; + + if (treatAsAddon) { + console.log(`✅ Complemento ${integrationData.plan} removido sem afetar outros produtos`); + } else if (shouldDeactivatePrimaryPlan) { + console.log('🚫 Revogando plano principal sem bloquear demais produtos do usuário'); + updatedUser = await prisma.user.update({ + where: { email }, + data: { + status: 'INACTIVE', + plan: user.plan === integrationData.plan ? 'CANCELLED' : user.plan, + expirationDate: new Date(Date.now() - 24 * 60 * 60 * 1000), // Ontem + origin: 'EDUZZ' + } + }); + } else { + console.log('â„¹ï¸ Outro plano ativo encontrado; mantendo usuário ativo.'); + } + + // Registrar reembolso + try { + await prisma.purchase.create({ + data: { + userId: user.id, + amount: -(amount || 0), + productName: `${integrationData.name} - REEMBOLSO`, + hotmartTransactionId: transactionId, + status: 'REFUNDED', + paymentMethod: normalizePaymentMethod(paymentMethod), + installments: installments, + platform: 'EDUZZ' + } + }); + console.log(`💸 Reembolso Eduzz registrado: -${amount} - ${integrationData.name}`); + } catch (purchaseError) { + console.error('âš ï¸ Erro ao registrar reembolso Eduzz:', purchaseError); + } + + await prisma.$disconnect(); + + return NextResponse.json({ + success: true, + message: treatAsAddon + ? `Reembolso Eduzz processado - complemento ${integrationData.plan} removido` + : `Reembolso Eduzz processado - produto principal revogado`, + platform: 'Eduzz', + event: event, + user: { + id: updatedUser.id, + email: updatedUser.email, + status: updatedUser.status, + blocked: false, + }, + entitlementRemoved: treatAsAddon, + }); + } else { + await prisma.$disconnect(); + + return NextResponse.json({ + success: true, + message: 'Usuário não encontrado - reembolso registrado', + platform: 'Eduzz', + email: email, + event: event, + }); + } + } + + // ✅ CORRETO - Processar eventos de COMPRA (nome oficial Eduzz) + if (event !== 'invoice_paid') { + console.log(`📠Evento Eduzz ${event} não processado pelo sistema`); + return NextResponse.json({ + success: true, + message: `Evento Eduzz ${event} recebido mas não processado`, + platform: 'Eduzz', + event: event + }); + } + + console.log(`✅ Processando evento de compra Eduzz: ${event}`); + + // Validação mínima + if (!buyerEmail || !buyerEmail.includes('@')) { + console.log('⌠Email inválido:', buyerEmail); + return NextResponse.json({ + error: 'Email do comprador é obrigatório e deve ser válido', + received_email: buyerEmail + }, { status: 400 }); + } + + // Conectar ao banco + await prisma.$connect(); + + // Verificar se usuário já existe + const email = buyerEmail.toLowerCase().trim(); + let user = await prisma.user.findUnique({ + where: { email } + }); + + let isNewUser = false; + let tempPassword = ''; + let treatedAsAddon = false; + let entitlementExpiration: Date | null = null; + + if (user) { + // ATUALIZAR usuário existente + console.log(`🔄 Atualizando usuário existente: ${email}`); + + treatedAsAddon = isAddonPurchase && user.plan !== integrationData.plan; + const phonePayload = buyerPhone && !user.phone ? { phone: buyerPhone } : {}; + + if (treatedAsAddon) { + console.log(`âž• Ativando complemento ${integrationData.plan} para ${email} via Eduzz`); + entitlementExpiration = calculateExpirationDate(integrationData.plan); + user = await prisma.user.update({ + where: { email }, + data: { + status: 'ACTIVE', + hotmartCustomerId: transactionId, + origin: 'EDUZZ', + ...phonePayload + } + }); + } else { + const nextExpiration = calculateExpirationDate(integrationData.plan); + entitlementExpiration = isAddonPurchase ? nextExpiration : entitlementExpiration; + + user = await prisma.user.update({ + where: { email }, + data: { + plan: integrationData.plan, + status: 'ACTIVE', + hotmartCustomerId: transactionId, + expirationDate: nextExpiration, + origin: 'EDUZZ', + ...phonePayload + } + }); + } + + console.log(`✅ Usuário atualizado via Eduzz: ${email} → ${integrationData.plan}`); + + } else { + // CRIAR novo usuário + isNewUser = true; + tempPassword = generateSecurePassword(); + const hashedPassword = await hashPassword(tempPassword); + + console.log(`âž• Criando novo usuário via Eduzz: ${email}`); + + const nameParts = buyerName.split(' '); + const firstName = nameParts[0] || 'Cliente'; + const lastName = nameParts.slice(1).join(' ') || 'Eduzz'; + + const nextExpiration = calculateExpirationDate(integrationData.plan); + entitlementExpiration = isAddonPurchase ? nextExpiration : entitlementExpiration; + + user = await prisma.user.create({ + data: { + email: email, + firstName: firstName, + lastName: lastName, + phone: buyerPhone ?? null, + plan: integrationData.plan, + status: 'ACTIVE', + hotmartCustomerId: transactionId, + expirationDate: nextExpiration, + origin: 'EDUZZ', + password: hashedPassword, + passwordCreatedAt: new Date(), + mustChangePassword: true, + customPermissions: '[]' + } + }); + + console.log(`✅ Novo usuário criado via Eduzz: ${email} → ${integrationData.plan}`); + } + + if (isAddonPurchase) { + const expiresAt = entitlementExpiration ?? calculateExpirationDate(integrationData.plan); + await activateEntitlement(prisma, { + userId: user.id, + code: integrationData.plan, + origin: 'EDUZZ', + expiresAt, + }); + if (expiresAt) { + console.log(`🎠Complemento ${integrationData.plan} ativo até ${expiresAt.toISOString()} via Eduzz`); + } else { + console.log(`🎠Complemento ${integrationData.plan} ativado com acesso vitalício via Eduzz.`); + } + } + + // Registrar compra + try { + await prisma.purchase.create({ + data: { + userId: user.id, + amount: amount || 0, + productName: integrationData.name, + hotmartTransactionId: transactionId, + status: 'COMPLETED', + paymentMethod: normalizePaymentMethod(paymentMethod), + installments: installments, + platform: 'EDUZZ' + } + }); + console.log(`💰 Compra Eduzz registrada: ${amount} - ${integrationData.name} (${normalizePaymentMethod(paymentMethod)}, ${installments}x)`); + } catch (purchaseError) { + console.error('âš ï¸ Erro ao registrar compra Eduzz:', purchaseError); + } + + await prisma.$disconnect(); + + const response = { + success: true, + message: `Webhook Eduzz processado com sucesso`, + platform: 'Eduzz', + integration: { + id: integrationData.integrationId, + name: integrationData.name, + plan: integrationData.plan, + token: token + }, + productDetection: { + detectedPlan: integrationData.plan, + detectedProductName: integrationData.name, + originalProductName: productName, + amount: amount + }, + user: { + id: user.id, + email: user.email, + plan: user.plan, + status: user.status, + isNewUser: isNewUser, + tempPassword: isNewUser ? tempPassword : undefined, + addonGranted: isAddonPurchase, + treatedAsAddon, + }, + transaction: { + id: transactionId, + amount: amount, + product: integrationData.name + }, + timestamp: new Date().toISOString() + }; + + console.log(`📚 Webhook Eduzz ${token} processado com sucesso:`, response); + + return NextResponse.json(response); + + } catch (error: any) { + console.error(`⌠Erro no webhook Eduzz ${params.token}:`, error); + + try { + await prisma.$disconnect(); + } catch (disconnectError) { + console.error('⌠Erro ao desconectar:', disconnectError); + } + + return NextResponse.json( + { + error: 'Erro interno do servidor', + platform: 'Eduzz', + token: params.token, + message: error.message, + timestamp: new Date().toISOString() + }, + { status: 500 } + ); + } +} + +// GET para status da integração Eduzz +export async function GET( + request: NextRequest, + { params }: { params: { token: string } } +) { + const token = params.token; + const integration = EDUZZ_TOKEN_MAPPING[token]; + + if (!integration) { + return NextResponse.json( + { error: `Token Eduzz ${token} não encontrado` }, + { status: 404 } + ); + } + + return NextResponse.json({ + success: true, + platform: 'Eduzz', + integration: { + id: integration.integrationId, + name: 'Detecção Automática de Produtos', + plan: 'DETECTADO_AUTOMATICAMENTE', + token: token, + status: 'ACTIVE', + webhookUrl: `${new URL(request.url).origin}/api/webhooks/eduzz/${token}`, + supportedEvents: [ + 'invoice_paid', // Fatura paga + 'invoice_refunded', // Fatura reembolsada + 'invoice_canceled' // Fatura cancelada + ], + supportedProducts: [ + 'Close Friends LITE 2.0', + 'Close Friends VIP', + 'Close Friends LITE', + 'Projeto FIIs' + ] + }, + message: `Integração Eduzz ativa com detecção automática de produtos`, + documentation: 'https://atendimento.eduzz.com/portal/pt/kb/articles/documenta%C3%A7%C3%A3o-de-eventos-de-altera%C3%A7%C3%A3o-no-status-da-fatura', + timestamp: new Date().toISOString() + }); +} diff --git a/src/app/api/webhooks/hotmart/[id]/route.ts b/src/app/api/webhooks/hotmart/[id]/route.ts new file mode 100644 index 000000000..7df37a2fb --- /dev/null +++ b/src/app/api/webhooks/hotmart/[id]/route.ts @@ -0,0 +1,792 @@ +// src/app/api/webhooks/hotmart/[id]/route.ts +export const dynamic = 'force-dynamic'; + +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { hashPassword } from '@/lib/auth/password'; +import { activateEntitlement, deactivateEntitlement, getActiveEntitlementCodes, isAddonPlan } from '@/lib/entitlements'; +import { enviarEmailCredenciais } from '@/lib/auth/email'; // ✅ ADICIONAR ENVIO DE EMAIL +import { isLifetimePlan } from '@/lib/lifetime-plans'; +import { getHotmartBuyerPhone } from '@/lib/phone'; +import { normalizePaymentMethod, normalizeInstallments } from '@/lib/payment-normalization'; + +// ✅ TOKEN ÚNICO DA HOTMART CONFIGURADO +const TOKEN_MAPPING: Record = { + 'TokendYNZMSBlDXyWPST3VZPSsaqe3JfKYJ': { name: 'Produto Fatos da Bolsa Hotmart', plan: 'VIP', integrationId: 'HM001' }, +}; + +// 🔠FUNÇÃO PARA DETECTAR PLANO AUTOMATICAMENTE PELOS SEUS PRODUTOS HOTMART +const HOTMART_PRODUCT_IDS = { + LITE: new Set(['3129181']), + LITE_V2: new Set(['5060609', '1874171', '3671256']), + VIP: new Set(['1128762', '1762716', '2163067', '5060349', '1650879', '3670772', '2947386']), + FIIS: new Set(['4218223']), + RENDA_PASSIVA: new Set(['3547657']), + AMERICA: new Set(['5325106']), + RENDA_TURBINADA: new Set(['6558190']) +}; + +const RENDA_TURBINADA_TURMA2_START = new Date('2025-11-28T00:00:00Z'); +const RENDA_TURBINADA_TURMA3_START = new Date('2026-01-20T00:00:00Z'); + +// ✅ IDs DOS PRODUTOS QUE DEVEM ENVIAR EMAIL AUTOMÃTICO +const PRODUTOS_COM_EMAIL_AUTOMATICO = new Set([ + '6558190', // CF Renda Turbinada + // Adicione outros IDs aqui se precisar no futuro: + // '4218223', // Projeto FIIs + // '5325106', // Projeto América +]); + +function parseHotmartDate(value: any): Date | null { + if (!value) { + return null; + } + + const parsed = new Date(value); + return Number.isNaN(parsed.getTime()) ? null : parsed; +} + +function getPurchaseDate(webhookData: any): Date | null { + const purchaseData = webhookData.data?.purchase || webhookData.purchase || webhookData; + const candidateDates = [ + purchaseData?.approved_date, + purchaseData?.approvedDate, + purchaseData?.purchase_date, + purchaseData?.paid_at, + purchaseData?.paidDate, + purchaseData?.created_at, + webhookData.data?.subscription?.approved_date, + webhookData.event_date, + webhookData.created_at, + webhookData.timestamp, + ]; + + for (const candidate of candidateDates) { + const parsed = parseHotmartDate(candidate); + if (parsed) { + return parsed; + } + } + + return null; +} + +function resolveRendaTurbinadaPlan(webhookData: any): { plan: 'RENDA_TURBINADA' | 'RENDA_TURBINADA_T2' | 'RENDA_TURBINADA_T3'; label: string; purchaseDate: Date | null } { + const purchaseDate = getPurchaseDate(webhookData); + const isTurma3 = !purchaseDate || purchaseDate >= RENDA_TURBINADA_TURMA3_START; + const isTurma2 = !isTurma3 && purchaseDate >= RENDA_TURBINADA_TURMA2_START; + + return { + plan: isTurma3 ? 'RENDA_TURBINADA_T3' : isTurma2 ? 'RENDA_TURBINADA_T2' : 'RENDA_TURBINADA', + label: isTurma3 ? 'CF Renda Turbinada - Turma 3' : isTurma2 ? 'CF Renda Turbinada - Turma 2' : 'CF Renda Turbinada', + purchaseDate, + }; +} + +function normalizarIdHotmart(valor: any): string | null { + if (!valor && valor !== 0) return null; + + if (typeof valor === 'number') { + return valor.toString(); + } + + if (typeof valor === 'string') { + const trimmed = valor.trim(); + return trimmed.length > 0 ? trimmed : null; + } + + return null; +} + +function detectarPlanoHotmart(webhookData: any): { plan: string; productName: string } { + try { + console.log('🔠Detectando plano do produto Hotmart:', webhookData); + + const productData = webhookData.data?.product || webhookData.product || {}; + const productName = productData?.name || + productData?.product_name || + productData?.title || + webhookData.product_name || + webhookData.name || + ''; + + const productId = normalizarIdHotmart( + productData?.id || + productData?.product_id || + productData?.prod || + webhookData.product_id || + webhookData.prod || + webhookData.id + ); + + console.log(`📋 Nome do produto Hotmart recebido: "${productName}"`); + console.log(`🆔 ID do produto Hotmart recebido: "${productId ?? 'desconhecido'}"`); + + if (productId) { + if (HOTMART_PRODUCT_IDS.LITE.has(productId)) { + return { + plan: 'LITE', + productName: `Close Friends LITE Hotmart - Produto ${productId}` + }; + } + + if (HOTMART_PRODUCT_IDS.LITE_V2.has(productId)) { + return { + plan: 'LITE_V2', + productName: `Close Friends LITE 2.0 Hotmart - Produto ${productId}` + }; + } + + if (HOTMART_PRODUCT_IDS.VIP.has(productId)) { + return { + plan: 'VIP', + productName: `Close Friends VIP Hotmart - Produto ${productId}` + }; + } + + if (HOTMART_PRODUCT_IDS.FIIS.has(productId)) { + return { + plan: 'FIIS', + productName: `Projeto FIIs Hotmart - Produto ${productId}` + }; + } + + if (HOTMART_PRODUCT_IDS.RENDA_PASSIVA.has(productId)) { + return { + plan: 'RENDA_PASSIVA', + productName: `Projeto Renda Passiva Hotmart - Produto ${productId}` + }; + } + + if (HOTMART_PRODUCT_IDS.AMERICA.has(productId)) { + return { + plan: 'AMERICA', + productName: `Projeto Trump (América) Hotmart - Produto ${productId}` + }; + } + + if (HOTMART_PRODUCT_IDS.RENDA_TURBINADA.has(productId)) { + const rendaTurbinada = resolveRendaTurbinadaPlan(webhookData); + return { + plan: rendaTurbinada.plan, + productName: `${rendaTurbinada.label} Hotmart - Produto ${productId}` + }; + } + } + + const produtoLower = productName.toLowerCase(); + + if (produtoLower.includes('close friends lite 2.0') || + produtoLower.includes('cf lite 2.0') || + produtoLower.includes('lite 2.0') || + produtoLower.includes('lite v2')) { + return { + plan: 'LITE_V2', + productName: `Close Friends LITE 2.0 Hotmart - ${productName}` + }; + } + + if (produtoLower.includes('close friends lite') || + produtoLower.includes('cf lite') || + (produtoLower.includes('lite') && !produtoLower.includes('2.0') && !produtoLower.includes('v2'))) { + return { + plan: 'LITE', + productName: `Close Friends LITE Hotmart - ${productName}` + }; + } + + if (produtoLower.includes('close friends vip') || + produtoLower.includes('cf vip') || + produtoLower.includes('vip')) { + return { + plan: 'VIP', + productName: `Close Friends VIP Hotmart - ${productName}` + }; + } + + if (produtoLower.includes('renda turbinada') || + produtoLower.includes('cf renda')) { + const rendaTurbinada = resolveRendaTurbinadaPlan(webhookData); + return { + plan: rendaTurbinada.plan, + productName: `${rendaTurbinada.label} Hotmart - ${productName}` + }; + } + + if (produtoLower.includes('projeto fiis') || + produtoLower.includes('fiis') || + produtoLower.includes('fii')) { + return { + plan: 'FIIS', + productName: `Projeto FIIs Hotmart - ${productName}` + }; + } + + if (produtoLower.includes('renda passiva') || + produtoLower.includes('dividendos')) { + return { + plan: 'RENDA_PASSIVA', + productName: `Projeto Renda Passiva Hotmart - ${productName}` + }; + } + + if (produtoLower.includes('projeto trump') || + produtoLower.includes('trump') || + produtoLower.includes('projeto américa') || + produtoLower.includes('america')) { + return { + plan: 'AMERICA', + productName: `Projeto Trump (América) Hotmart - ${productName}` + }; + } + + const purchaseData = webhookData.data?.purchase || webhookData.purchase || {}; + const priceData = purchaseData?.price || {}; + const valor = priceData?.value || purchaseData?.amount || webhookData.price || 0; + console.log(`💰 Valor da compra Hotmart: R$ ${valor}`); + + if (valor > 0) { + if (valor >= 200) { + return { plan: 'VIP', productName: `Produto VIP Hotmart - R$ ${valor}` }; + } else if (valor >= 150) { + return { plan: 'FIIS', productName: `Projeto FIIs Hotmart - R$ ${valor}` }; + } else if (valor >= 100) { + return { plan: 'AMERICA', productName: `Projeto América Hotmart - R$ ${valor}` }; + } else if (valor >= 50) { + return { plan: 'LITE_V2', productName: `Close Friends LITE 2.0 Hotmart - R$ ${valor}` }; + } else { + return { plan: 'LITE', productName: `Close Friends LITE Hotmart - R$ ${valor}` }; + } + } + + const createdAt = webhookData.data?.created_at || webhookData.created_at; + if (createdAt) { + const productDate = new Date(createdAt); + const cutoffDate = new Date('2023-01-01'); + + if (productDate < cutoffDate) { + console.log(`📅 Produto criado em ${productDate.toISOString().split('T')[0]} (antes de 2023) → LITE original`); + return { + plan: 'LITE', + productName: `Close Friends LITE Original Hotmart - ${productName || 'Produto 2022 ou anterior'}` + }; + } else { + console.log(`📅 Produto criado em ${productDate.toISOString().split('T')[0]} (2023 ou depois) → LITE 2.0`); + return { + plan: 'LITE_V2', + productName: `Close Friends LITE 2.0 Hotmart - ${productName || 'Produto Recente'}` + }; + } + } + + console.log(`âš ï¸ Não foi possível detectar o plano Hotmart específico, usando VIP como padrão`); + return { + plan: 'VIP', + productName: `Produto Não Identificado Hotmart - ${productName || 'Sem Nome'}` + }; + + } catch (error) { + console.error('⌠Erro ao detectar plano Hotmart:', error); + return { + plan: 'VIP', + productName: 'Produto com Erro de Detecção Hotmart' + }; + } +} + +function generateSecurePassword(): string { + const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789'; + const symbols = '!@#$%&*'; + let password = ''; + + for (let i = 0; i < 6; i++) { + password += chars.charAt(Math.floor(Math.random() * chars.length)); + } + for (let i = 0; i < 2; i++) { + password += symbols.charAt(Math.floor(Math.random() * symbols.length)); + } + + return password.split('').sort(() => Math.random() - 0.5).join(''); +} + +function calculateExpirationDate(plan: string): Date | null { + if (isLifetimePlan(plan)) { + console.log(`🎉 Plano ${plan}: acesso vitalício (sem data de expiração).`); + return null; + } + + const expirationDate = new Date(); + expirationDate.setDate(expirationDate.getDate() + 365); + + console.log(`📅 Plano ${plan}: Expira em ${expirationDate.toISOString().split('T')[0]} (365 dias a partir de hoje)`); + + return expirationDate; +} + +export async function POST( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const token = params.id; + console.log(`🔔 Webhook Hotmart recebido para token: ${token}`); + + const integration = TOKEN_MAPPING[token]; + if (!integration) { + console.log(`⌠Token Hotmart ${token} não encontrado`); + return NextResponse.json( + { error: `Token Hotmart ${token} não configurado` }, + { status: 404 } + ); + } + + console.log(`✅ Integração Hotmart encontrada: ${integration.name} → Plano ${integration.plan}`); + + let webhookData; + try { + webhookData = await request.json(); + } catch (jsonError) { + console.error('⌠Erro ao parsear JSON:', jsonError); + return NextResponse.json( + { error: 'JSON inválido' }, + { status: 400 } + ); + } + + console.log('📦 Dados do webhook Hotmart:', JSON.stringify(webhookData, null, 2)); + + const { plan: planoDetectado, productName: nomeDetectado } = detectarPlanoHotmart(webhookData); + + const integrationData = { + name: nomeDetectado, + plan: planoDetectado, + integrationId: integration.integrationId + }; + + console.log(`✅ Plano Hotmart detectado: ${integrationData.name} → ${integrationData.plan}`); + + const isAddonPurchase = isAddonPlan(integrationData.plan); + + const event = webhookData.event || 'PURCHASE_APPROVED'; + console.log(`🎯 Evento Hotmart recebido: ${event}`); + + const buyerData = webhookData.data?.buyer || webhookData.buyer || webhookData; + const purchaseData = webhookData.data?.purchase || webhookData.purchase || webhookData; + const productData = webhookData.data?.product || webhookData.product || webhookData; + const buyerEmail = buyerData?.email || webhookData.email; + const buyerName = buyerData?.name || buyerData?.full_name || webhookData.name || 'Cliente Hotmart'; + const buyerPhone = getHotmartBuyerPhone(webhookData, buyerData); + const transactionId = purchaseData?.transaction || purchaseData?.transaction_id || + webhookData.transaction || `TXN_${integration.integrationId}_${Date.now()}`; + const amount = purchaseData?.price?.value || purchaseData?.amount || webhookData.price || 0; + + // 💳 Extrair dados de pagamento + const paymentData = purchaseData?.payment || webhookData.data?.payment || webhookData.payment || {}; + const paymentMethod = paymentData?.type || paymentData?.method || null; + const installments = normalizeInstallments( + paymentData?.installments_number || paymentData?.installments || 1 + ); + + console.log('🔠Dados extraídos da Hotmart:', { + event, buyerEmail, buyerName, transactionId, amount, + plan: integrationData.plan, + integrationName: integrationData.name, + token: token, + paymentMethod, + installments + }); + + if (event === 'PURCHASE_REFUNDED' || event === 'PURCHASE_CANCELLED' || event === 'PURCHASE_CHARGEBACK') { + console.log(`🚫 Evento de ${event} na Hotmart - avaliando revogação`); + + if (!buyerEmail || !buyerEmail.includes('@')) { + console.log('⌠Email inválido para reembolso Hotmart:', buyerEmail); + return NextResponse.json({ + error: 'Email do comprador é obrigatório para processar reembolso', + received_email: buyerEmail + }, { status: 400 }); + } + + await prisma.$connect(); + + const email = buyerEmail.toLowerCase().trim(); + let user = await prisma.user.findUnique({ + where: { email } + }); + + if (user) { + const treatAsAddon = isAddonPurchase && user.plan !== integrationData.plan; + let updatedUser = user; + + console.log(`â™»ï¸ Revogando acesso do produto ${integrationData.plan} (Hotmart)`); + await deactivateEntitlement(prisma, { + userId: user.id, + code: integrationData.plan, + origin: 'HOTMART', + }); + + const otherActiveEntitlements = await prisma.userEntitlement.findMany({ + where: { + userId: user.id, + status: 'ACTIVE', + NOT: { code: integrationData.plan }, + }, + select: { code: true, status: true, expiresAt: true }, + }); + + const hasAnotherActivePlan = getActiveEntitlementCodes(otherActiveEntitlements).length > 0; + const shouldDeactivatePrimaryPlan = !treatAsAddon && user.plan === integrationData.plan && !hasAnotherActivePlan; + + if (treatAsAddon) { + console.log(`✅ Complemento ${integrationData.plan} removido sem afetar outros produtos`); + } else if (shouldDeactivatePrimaryPlan) { + console.log('🚫 Revogando plano principal sem bloquear demais produtos do usuário'); + updatedUser = await prisma.user.update({ + where: { email }, + data: { + status: 'INACTIVE', + plan: user.plan === integrationData.plan ? 'CANCELLED' : user.plan, + expirationDate: new Date(Date.now() - 24 * 60 * 60 * 1000), + origin: 'HOTMART' + } + }); + } else { + console.log('â„¹ï¸ Outro plano ativo encontrado; mantendo usuário ativo.'); + } + + try { + await prisma.purchase.create({ + data: { + userId: user.id, + amount: -(amount || 0), + productName: `${integrationData.name} - REEMBOLSO`, + hotmartTransactionId: transactionId, + status: 'REFUNDED', + paymentMethod: normalizePaymentMethod(paymentMethod), + installments: installments, + platform: 'HOTMART' + } + }); + console.log(`💸 Reembolso Hotmart registrado: -${amount} - ${integrationData.name}`); + } catch (purchaseError) { + console.error('âš ï¸ Erro ao registrar reembolso Hotmart:', purchaseError); + } + + await prisma.$disconnect(); + + return NextResponse.json({ + success: true, + message: treatAsAddon + ? `Reembolso Hotmart processado - complemento ${integrationData.plan} removido` + : `Reembolso Hotmart processado - produto principal revogado`, + platform: 'Hotmart', + event: event, + integration: { + id: integrationData.integrationId, + name: integrationData.name, + plan: integrationData.plan, + token: token + }, + user: { + id: updatedUser.id, + email: updatedUser.email, + status: updatedUser.status, + blocked: false + }, + refund: { + id: transactionId, + amount: amount, + product: integrationData.name + }, + entitlementRemoved: treatAsAddon, + timestamp: new Date().toISOString() + }); + + } else { + await prisma.$disconnect(); + console.log(`âš ï¸ Usuário ${email} não encontrado para reembolso Hotmart`); + + return NextResponse.json({ + success: true, + message: 'Usuário não encontrado - reembolso registrado', + platform: 'Hotmart', + email: email, + event: event + }); + } + } + + if (!['PURCHASE_APPROVED', 'PURCHASE_COMPLETE', 'PURCHASE_PAID'].includes(event)) { + console.log(`📠Evento Hotmart ${event} não processado pelo sistema`); + return NextResponse.json({ + success: true, + message: `Evento Hotmart ${event} recebido mas não processado`, + platform: 'Hotmart', + event: event + }); + } + + console.log(`✅ Processando evento de compra Hotmart: ${event}`); + + if (!buyerEmail || !buyerEmail.includes('@')) { + console.log('⌠Email inválido:', buyerEmail); + return NextResponse.json({ + error: 'Email do comprador é obrigatório e deve ser válido', + received_email: buyerEmail + }, { status: 400 }); + } + + await prisma.$connect(); + + const email = buyerEmail.toLowerCase().trim(); + let user = await prisma.user.findUnique({ + where: { email } + }); + + let isNewUser = false; + let tempPassword = ''; + let treatedAsAddon = false; + let entitlementExpiration: Date | null = null; + + if (user) { + console.log(`🔄 Atualizando usuário existente: ${email}`); + + treatedAsAddon = isAddonPurchase && user.plan !== integrationData.plan; + const phonePayload = buyerPhone && !user.phone ? { phone: buyerPhone } : {}; + + if (treatedAsAddon) { + console.log(`âž• Ativando complemento ${integrationData.plan} para ${email}`); + entitlementExpiration = calculateExpirationDate(integrationData.plan); + user = await prisma.user.update({ + where: { email }, + data: { + status: 'ACTIVE', + hotmartCustomerId: transactionId, + origin: 'HOTMART', + ...phonePayload + } + }); + } else { + const nextExpiration = calculateExpirationDate(integrationData.plan); + entitlementExpiration = isAddonPurchase ? nextExpiration : entitlementExpiration; + + user = await prisma.user.update({ + where: { email }, + data: { + plan: integrationData.plan, + status: 'ACTIVE', + hotmartCustomerId: transactionId, + expirationDate: nextExpiration, + origin: 'HOTMART', + ...phonePayload + } + }); + } + + console.log(`✅ Usuário atualizado via Hotmart: ${email} → ${integrationData.plan} via token ${token}`); + + } else { + isNewUser = true; + tempPassword = generateSecurePassword(); + const hashedPassword = await hashPassword(tempPassword); + + console.log(`âž• Criando novo usuário via Hotmart: ${email}`); + + const nameParts = buyerName.split(' '); + const firstName = nameParts[0] || 'Cliente'; + const lastName = nameParts.slice(1).join(' ') || 'Hotmart'; + + const nextExpiration = calculateExpirationDate(integrationData.plan); + entitlementExpiration = isAddonPurchase ? nextExpiration : entitlementExpiration; + + user = await prisma.user.create({ + data: { + email: email, + firstName: firstName, + lastName: lastName, + phone: buyerPhone ?? null, + plan: integrationData.plan, + status: 'ACTIVE', + hotmartCustomerId: transactionId, + expirationDate: nextExpiration, + origin: 'HOTMART', + password: hashedPassword, + passwordCreatedAt: new Date(), + mustChangePassword: true, + customPermissions: '[]' + } + }); + + console.log(`✅ Novo usuário criado via Hotmart: ${email} → ${integrationData.plan} via token ${token}`); + + // ✅✅✅ ENVIAR EMAIL APENAS PARA PRODUTOS ESPECÃFICOS ✅✅✅ + if (tempPassword) { + // Extrair ID do produto + const productId = normalizarIdHotmart( + productData?.id || + productData?.product_id || + productData?.prod || + webhookData.product_id || + webhookData.prod || + webhookData.id + ); + + console.log(`🔠Produto ID: ${productId} | Verificando se deve enviar email...`); + + if (productId && PRODUTOS_COM_EMAIL_AUTOMATICO.has(productId)) { + try { + console.log(`📧 Produto ${productId} (${integrationData.plan}) está na lista de envio automático`); + console.log(`📧 Enviando email de boas-vindas para: ${email}`); + + await enviarEmailCredenciais( + email, + firstName, + tempPassword, + integrationData.plan + ); + + console.log(`✅ Email de boas-vindas enviado com sucesso para: ${email}`); + } catch (emailError: any) { + console.error(`⌠Erro ao enviar email para ${email}:`, emailError.message); + console.error(`Stack trace:`, emailError.stack); + // Não falhar o webhook por causa de erro no email + } + } else { + console.log(`â­ï¸ Produto ${productId} não está configurado para envio automático de email`); + console.log(`â„¹ï¸ Produtos com email automático: ${Array.from(PRODUTOS_COM_EMAIL_AUTOMATICO).join(', ')}`); + } + } + // ✅✅✅ FIM DO BLOCO DE ENVIO DE EMAIL ✅✅✅ + } + + if (isAddonPurchase) { + const expiresAt = entitlementExpiration ?? calculateExpirationDate(integrationData.plan); + await activateEntitlement(prisma, { + userId: user.id, + code: integrationData.plan, + origin: 'HOTMART', + expiresAt, + }); + if (expiresAt) { + console.log(`🎠Complemento ${integrationData.plan} ativo até ${expiresAt.toISOString()}`); + } else { + console.log(`🎠Complemento ${integrationData.plan} ativado com acesso vitalício.`); + } + } + + try { + await prisma.purchase.create({ + data: { + userId: user.id, + amount: amount || 0, + productName: integrationData.name, + hotmartTransactionId: transactionId, + status: 'COMPLETED', + paymentMethod: normalizePaymentMethod(paymentMethod), + installments: installments, + platform: 'HOTMART' + } + }); + console.log(`💰 Compra Hotmart registrada: ${amount} - ${integrationData.name} (${normalizePaymentMethod(paymentMethod)}, ${installments}x)`); + } catch (purchaseError) { + console.error('âš ï¸ Erro ao registrar compra Hotmart:', purchaseError); + } + + await prisma.$disconnect(); + + const response = { + success: true, + message: `Webhook Hotmart processado com sucesso via token ${token}`, + platform: 'Hotmart', + integration: { + id: integrationData.integrationId, + name: integrationData.name, + plan: integrationData.plan, + token: token + }, + productDetection: { + detectedPlan: integrationData.plan, + detectedProductName: integrationData.name, + originalProductName: webhookData.data?.product?.name || webhookData.product_name || 'N/A', + amount: amount + }, + user: { + id: user.id, + email: user.email, + plan: user.plan, + status: user.status, + isNewUser: isNewUser, + emailSent: isNewUser && tempPassword ? 'Verificar logs' : 'N/A', + addonGranted: isAddonPurchase, + treatedAsAddon, + }, + transaction: { + id: transactionId, + amount: amount, + product: integrationData.name + }, + timestamp: new Date().toISOString() + }; + + console.log(`🔥 Webhook Hotmart ${token} processado com sucesso:`, response); + + return NextResponse.json(response); + + } catch (error: any) { + console.error(`⌠Erro no webhook Hotmart ${params.id}:`, error); + + try { + await prisma.$disconnect(); + } catch (disconnectError) { + console.error('⌠Erro ao desconectar:', disconnectError); + } + + return NextResponse.json( + { + error: 'Erro interno do servidor', + platform: 'Hotmart', + token: params.id, + message: error.message, + timestamp: new Date().toISOString() + }, + { status: 500 } + ); + } +} + +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + const token = params.id; + const integration = TOKEN_MAPPING[token]; + + if (!integration) { + return NextResponse.json( + { error: `Token Hotmart ${token} não encontrado` }, + { status: 404 } + ); + } + + return NextResponse.json({ + success: true, + platform: 'Hotmart', + integration: { + id: integration.integrationId, + name: integration.name, + plan: integration.plan, + token: token, + status: 'ACTIVE', + webhookUrl: `${new URL(request.url).origin}/api/webhooks/hotmart/${token}` + }, + emailConfig: { + autoSendEnabled: true, + productsWithAutoEmail: Array.from(PRODUTOS_COM_EMAIL_AUTOMATICO), + productsInfo: { + '6558190': 'CF Renda Turbinada (EMAIL AUTOMÃTICO ATIVO)' + } + }, + message: `Integração Hotmart ${integration.name} ativa e funcionando`, + timestamp: new Date().toISOString() + }); +} diff --git a/src/app/api/webhooks/kiwify/[token]/route.ts b/src/app/api/webhooks/kiwify/[token]/route.ts new file mode 100644 index 000000000..27964c584 --- /dev/null +++ b/src/app/api/webhooks/kiwify/[token]/route.ts @@ -0,0 +1,835 @@ +export const dynamic = 'force-dynamic'; + +import { NextRequest, NextResponse } from 'next/server'; +import type { PrismaClient } from '@prisma/client'; +import { prisma } from '@/lib/prisma'; +import { enviarEmailCredenciais } from '@/lib/auth/email'; // ✅ ADICIONADO +import { hashPassword } from '@/lib/auth/password'; // ✅ ADICIONADO +import { getKiwifyTokenMapping } from '@/server/integration-secrets'; +import { activateEntitlement, deactivateEntitlement, getActiveEntitlementCodes, isAddonPlan } from '@/lib/entitlements'; +import { isLifetimePlan } from '@/lib/lifetime-plans'; +import { getKiwifyBuyerPhone } from '@/lib/phone'; +import { normalizePaymentMethod, normalizeInstallments } from '@/lib/payment-normalization'; + +const KIWIFY_TOKEN_MAPPING = getKiwifyTokenMapping(); + +function extractString(value: unknown): string { + if (typeof value === 'string') { + return value.trim(); + } + return ''; +} + +function formatProductLabel(prefix: string, displayName: string): string { + const normalizedDisplay = displayName.trim(); + if (!normalizedDisplay) { + return `${prefix} - Sem Nome`; + } + + if (normalizedDisplay.toLowerCase().includes(prefix.toLowerCase())) { + return normalizedDisplay; + } + + return `${prefix} - ${normalizedDisplay}`; +} + +function parseKiwifyAmount(value: unknown): number | undefined { + if (value === null || value === undefined) { + return undefined; + } + + let numValue: number; + + if (typeof value === 'number') { + numValue = value; + } else if (typeof value === 'string') { + // Remove TUDO exceto números + const cleaned = value.replace(/[^0-9]/g, ''); + if (!cleaned) return undefined; + numValue = Number(cleaned); + } else { + return undefined; + } + + if (!Number.isFinite(numValue) || numValue <= 0) { + return undefined; + } + + // ✅ KIWIFY SEMPRE ENVIA EM CENTAVOS - DIVIDIR POR 100 + // 119000 → 1190.00 + // 147688 → 1476.88 + const valorEmReais = numValue / 100; + + console.log(`💰 Kiwify: ${numValue} centavos → R$ ${valorEmReais.toFixed(2)}`); + + return Number(valorEmReais.toFixed(2)); +} + +function selectBestKiwifyAmount(values: unknown[]): number | undefined { + console.log('🔠Candidatos:', values.slice(0, 5)); + + for (const value of values) { + const parsed = parseKiwifyAmount(value); + if (parsed !== undefined && parsed > 0) { + console.log(`✅ Selecionado: R$ ${parsed.toFixed(2)}`); + return parsed; + } + } + + console.log('⌠Nenhum valor válido'); + return undefined; +} + +async function fixUnrealisticPurchaseAmounts( + prismaClient: PrismaClient, + userId: string +): Promise { + const purchasesToFix = await prismaClient.purchase.findMany({ + where: { + userId, + OR: [ + { amount: { gt: 10000 } }, + { amount: { lt: -10000 } }, + ], + }, + }); + + let updatedCount = 0; + + for (const purchase of purchasesToFix) { + const normalizedOptions = normalizeKiwifyAmounts(purchase.amount); + const correctedAmount = normalizedOptions.find( + (value) => Math.abs(value) > 0 && Math.abs(value) < 10000 + ); + + if (correctedAmount !== undefined && correctedAmount !== purchase.amount) { + await prismaClient.purchase.update({ + where: { id: purchase.id }, + data: { + amount: correctedAmount, + }, + }); + updatedCount += 1; + } + } + + return updatedCount; +} + +function extractProductNameFromKiwifyPayload(webhookData: any): { rawName: string; cleanedName: string } { + const orderData = webhookData?.order || webhookData?.data?.order || webhookData?.data || {}; + + const productCandidates: unknown[] = [ + webhookData?.product_name, + webhookData?.product?.name, + webhookData?.Product?.product_name, + webhookData?.Product?.name, + webhookData?.name, + webhookData?.title, + orderData?.product_name, + orderData?.product_title, + orderData?.product?.name, + orderData?.Product?.product_name, + orderData?.Product?.name, + orderData?.name, + orderData?.offer_name, + orderData?.Product?.title, + orderData?.plan?.name, + orderData?.Subscription?.plan?.name, + webhookData?.data?.product_name, + webhookData?.data?.product?.name, + webhookData?.payload?.product_name, + webhookData?.payload?.product?.name, + ]; + + if (Array.isArray(orderData?.products)) { + for (const product of orderData.products) { + productCandidates.push(product?.name, product?.product_name, product?.title); + } + } + + if (Array.isArray(webhookData?.products)) { + for (const product of webhookData.products) { + productCandidates.push(product?.name, product?.product_name, product?.title); + } + } + + const rawName = productCandidates + .map(extractString) + .find((candidate) => candidate.length > 0) || ''; + + return { rawName, cleanedName: rawName.toLowerCase() }; +} + +// 🔠FUNÇÃO PARA DETECTAR PLANO AUTOMATICAMENTE PELOS SEUS PRODUTOS +function detectarPlanoKiwify(webhookData: any): { plan: string; productName: string; rawProductName: string } { + try { + console.log('🔠Detectando plano do produto Kiwify:', webhookData); + + // Extrair nome do produto de diferentes campos possíveis + const { rawName: productName, cleanedName: produtoLower } = extractProductNameFromKiwifyPayload(webhookData); + const displayName = productName || 'Sem Nome'; + + const orderData = webhookData?.order || webhookData?.data?.order || webhookData?.data || {}; + + console.log(`📋 Nome do produto recebido: "${displayName}"`); + + // 🔠DETECTAR POR NOME (seus produtos reais) - ORDEM IMPORTA! + if (produtoLower.includes('close friends lite 2.0') || + produtoLower.includes('cf lite 2.0') || + produtoLower.includes('lite 2.0')) { + return { + plan: 'LITE_V2', + productName: formatProductLabel('Close Friends LITE 2.0', displayName), + rawProductName: productName + }; + } + + // â­ CLOSE FRIENDS LITE ORIGINAL (detectar depois - menos específico) + if (produtoLower.includes('close friends lite') || + produtoLower.includes('cf lite') || + (produtoLower.includes('lite') && !produtoLower.includes('2.0'))) { + return { + plan: 'LITE', + productName: formatProductLabel('Close Friends LITE', displayName), + rawProductName: productName + }; + } + + // 🔄 MIGRAÇÃO CF LITE (provavelmente é LITE original) + if (produtoLower.includes('migração') && produtoLower.includes('lite')) { + return { + plan: 'LITE', + productName: formatProductLabel('Migração CF LITE', displayName), + rawProductName: productName + }; + } + + // 👑 CLOSE FRIENDS VIP (todas as turmas) + if (produtoLower.includes('close friends vip') || + produtoLower.includes('cf vip') || + produtoLower.includes('vip') || + produtoLower.includes('turma')) { + return { + plan: 'VIP', + productName: formatProductLabel('Close Friends VIP', displayName), + rawProductName: productName + }; + } + + // 🢠PROJETO FIIs + if (produtoLower.includes('projeto fiis') || + produtoLower.includes('fiis') || + produtoLower.includes('fii')) { + return { + plan: 'FIIS', + productName: formatProductLabel('Projeto FIIs', displayName), + rawProductName: productName + }; + } + + // 📹 ANÃLISE EM VÃDEO (qual versão do LITE?) + if (produtoLower.includes('análise') && produtoLower.includes('vídeo')) { + // Por padrão, análise em vídeo = LITE original (você pode ajustar) + return { + plan: 'LITE', + productName: formatProductLabel('Análise em Vídeo', displayName), + rawProductName: productName + }; + } + + // 🔢 FALLBACK POR VALOR (se não conseguir pelo nome) + const valorCandidates = [ + webhookData?.amount, + webhookData?.total_value, + webhookData?.value, + orderData?.total_value, + orderData?.amount, + orderData?.value, + orderData?.price, + orderData?.payment_value, + orderData?.transaction_amount, + orderData?.gross_amount, + orderData?.charge_amount, + orderData?.Commissions?.product_base_price, + orderData?.Commissions?.charge_amount, + ]; + + const valor = selectBestKiwifyAmount(valorCandidates); + + console.log(`💰 Valor da compra: R$ ${valor ?? 0}`); + + if (valor && valor > 0) { + // Ajustar valores conforme seus preços reais + if (valor >= 400) { + return { plan: 'VIP', productName: `Produto VIP - R$ ${valor}`, rawProductName: productName }; + } + + // Valores abaixo de R$ 400 são tratados como ofertas LITE por padrão + return { + plan: 'LITE', + productName: `Close Friends LITE - R$ ${valor}`, + rawProductName: productName, + }; + } + + // 🎯 FALLBACK FINAL: LITE V2 como padrão mais seguro (produto mais recente) + console.log(`âš ï¸ Não foi possível detectar o plano específico, usando LITE V2 como padrão`); + return { + plan: 'LITE_V2', + productName: formatProductLabel('Produto Não Identificado', displayName), + rawProductName: productName + }; + + } catch (error) { + console.error('⌠Erro ao detectar plano:', error); + return { + plan: 'LITE_V2', + productName: 'Produto com Erro de Detecção', + rawProductName: '' + }; + } +} + +// Função para gerar senha segura +function generateSecurePassword(): string { + const chars = 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789'; + const symbols = '!@#$%&*'; + let password = ''; + + for (let i = 0; i < 6; i++) { + password += chars.charAt(Math.floor(Math.random() * chars.length)); + } + for (let i = 0; i < 2; i++) { + password += symbols.charAt(Math.floor(Math.random() * symbols.length)); + } + + return password.split('').sort(() => Math.random() - 0.5).join(''); +} + +// Calcular data de expiração considerando planos vitalícios +function calculateExpirationDate(plan: string): Date | null { + if (isLifetimePlan(plan)) { + console.log(`🎉 Plano ${plan}: acesso vitalício (sem data de expiração).`); + return null; + } + + const expirationDate = new Date(); + expirationDate.setDate(expirationDate.getDate() + 365); + return expirationDate; +} + +export async function POST( + request: NextRequest, + { params }: { params: { token: string } } +) { + try { + const token = params.token; + console.log(`🥠Webhook Kiwify recebido para token: ${token}`); + + // Verificar se o token existe + const integration = KIWIFY_TOKEN_MAPPING[token]; + if (!integration) { + console.log(`⌠Token Kiwify ${token} não encontrado`); + return NextResponse.json( + { error: `Token Kiwify ${token} não configurado` }, + { status: 404 } + ); + } + + let webhookData; + try { + webhookData = await request.json(); + } catch (jsonError) { + console.error('⌠Erro ao parsear JSON:', jsonError); + return NextResponse.json( + { error: 'JSON inválido' }, + { status: 400 } + ); + } + + console.log('📦 Dados do webhook Kiwify:', JSON.stringify(webhookData, null, 2)); + + // 🔠DETECTAR PLANO AUTOMATICAMENTE (AGORA DEPOIS DO PARSE) + const { plan: planoDetectado, productName: nomeDetectado, rawProductName } = detectarPlanoKiwify(webhookData); + + // ✅ SOBRESCREVER DADOS DA INTEGRAÇÃO COM DETECÇÃO AUTOMÃTICA + const integrationData = { + name: nomeDetectado, + plan: planoDetectado, + integrationId: integration.integrationId + }; + + console.log(`✅ Integração Kiwify encontrada: ${integrationData.name} → Plano ${integrationData.plan}`); + + const isAddonPurchase = isAddonPlan(integrationData.plan); + + // Extrair evento (Kiwify usa diferentes eventos) + const event = webhookData.event || webhookData.type || 'order.paid'; + console.log(`🎯 Evento Kiwify recebido: ${event}`); + + // Extrair informações do Kiwify (estrutura diferente do Hotmart) + const orderData = webhookData.order || webhookData.data || webhookData; + const customerData = orderData.Customer || orderData.customer || orderData; + + const buyerEmail = customerData?.email || orderData?.email || webhookData.email; + const buyerName = customerData?.name || customerData?.full_name || orderData?.customer_name || 'Cliente Kiwify'; + const buyerPhone = getKiwifyBuyerPhone(webhookData); + const transactionId = orderData?.order_id || orderData?.id || webhookData.order_id || `KW_${Date.now()}`; + const amountCandidates = [ + orderData?.total_value, + orderData?.amount, + orderData?.value, + orderData?.price, + orderData?.payment_value, + orderData?.transaction_amount, + orderData?.gross_amount, + orderData?.charge_amount, + orderData?.Commissions?.charge_amount, + orderData?.Commissions?.product_base_price, + webhookData?.amount, + webhookData?.total_value, + webhookData?.value, + ]; + + const amount = selectBestKiwifyAmount(amountCandidates); + const normalizedAmount = amount ?? 0; + + // 💳 Extrair dados de pagamento + const paymentMethod = orderData?.payment_method || webhookData.payment_method || null; + const installments = normalizeInstallments( + orderData?.installments || webhookData.installments || 1 + ); + + console.log('🔠Dados extraídos do Kiwify:', { + event, buyerEmail, buyerName, transactionId, amount: normalizedAmount, + plan: integrationData.plan, // ✅ CORRIGIDO + integrationName: integrationData.name, // ✅ CORRIGIDO + token: token, + paymentMethod, + installments + }); + + // Processar diferentes tipos de eventos Kiwify + if (event === 'order.refunded' || event === 'order.cancelled' || event === 'order.chargeback') { + // REEMBOLSO/CANCELAMENTO - BLOQUEAR USUÃRIO OU REVOGAR COMPLEMENTO + console.log(`🚫 Evento de ${event} no Kiwify - avaliando revogação`); + + if (!buyerEmail || !buyerEmail.includes('@')) { + console.log('⌠Email inválido para reembolso Kiwify:', buyerEmail); + return NextResponse.json({ + error: 'Email do comprador é obrigatório para processar reembolso', + received_email: buyerEmail + }, { status: 400 }); + } + + await prisma.$connect(); + + const email = buyerEmail.toLowerCase().trim(); + let user = await prisma.user.findUnique({ + where: { email } + }); + + if (user) { + const treatAsAddon = isAddonPurchase && user.plan !== integrationData.plan; + let updatedUser = user; + + console.log(`â™»ï¸ Revogando acesso do produto ${integrationData.plan} (Kiwify)`); + await deactivateEntitlement(prisma, { + userId: user.id, + code: integrationData.plan, + origin: 'KIWIFY', + }); + + const otherActiveEntitlements = await prisma.userEntitlement.findMany({ + where: { + userId: user.id, + status: 'ACTIVE', + NOT: { code: integrationData.plan }, + }, + select: { code: true, status: true, expiresAt: true }, + }); + + const hasAnotherActivePlan = getActiveEntitlementCodes(otherActiveEntitlements).length > 0; + const shouldDeactivatePrimaryPlan = !treatAsAddon && user.plan === integrationData.plan && !hasAnotherActivePlan; + + if (treatAsAddon) { + console.log(`✅ Complemento ${integrationData.plan} removido sem afetar outros produtos`); + } else if (shouldDeactivatePrimaryPlan) { + console.log('🚫 Revogando plano principal sem bloquear demais produtos do usuário'); + updatedUser = await prisma.user.update({ + where: { email }, + data: { + status: 'INACTIVE', + plan: user.plan === integrationData.plan ? 'CANCELLED' : user.plan, + expirationDate: new Date(Date.now() - 24 * 60 * 60 * 1000), // Ontem + origin: 'KIWIFY' + } + }); + } else { + console.log('â„¹ï¸ Outro plano ativo encontrado; mantendo usuário ativo.'); + } + + // Registrar reembolso + try { + await prisma.purchase.create({ + data: { + userId: user.id, + amount: -normalizedAmount, + productName: `${integrationData.name} - REEMBOLSO`, + hotmartTransactionId: transactionId, + status: 'REFUNDED', + paymentMethod: normalizePaymentMethod(paymentMethod), + installments: installments, + platform: 'KIWIFY' + } + }); + console.log(`💸 Reembolso Kiwify registrado: -${normalizedAmount} - ${integrationData.name}`); + + const correctedPurchases = await fixUnrealisticPurchaseAmounts(prisma, user.id); + if (correctedPurchases > 0) { + console.log( + `🧹 Corrigidos ${correctedPurchases} lançamentos antigos com valores inconsistentes para ${user.email}` + ); + } + } catch (purchaseError) { + console.error('âš ï¸ Erro ao registrar reembolso Kiwify:', purchaseError); + } + + await prisma.$disconnect(); + + return NextResponse.json({ + success: true, + message: treatAsAddon + ? `Reembolso Kiwify processado - complemento ${integrationData.plan} removido` + : `Reembolso Kiwify processado - produto principal revogado`, + platform: 'Kiwify', + event: event, + user: { + id: updatedUser.id, + email: updatedUser.email, + status: updatedUser.status, + blocked: false, + }, + entitlementRemoved: treatAsAddon, + }); + } else { + await prisma.$disconnect(); + + return NextResponse.json({ + success: true, + message: 'Usuário não encontrado - reembolso registrado', + platform: 'Kiwify', + email: email, + event: event, + }); + } + } + + // EVENTOS DE COMPRA (order.paid, order.approved, etc.) + if (!['order.paid', 'order.approved', 'order.completed'].includes(event)) { + console.log(`📠Evento Kiwify ${event} não processado pelo sistema`); + return NextResponse.json({ + success: true, + message: `Evento Kiwify ${event} recebido mas não processado`, + platform: 'Kiwify', + event: event + }); + } + + console.log(`✅ Processando evento de compra Kiwify: ${event}`); + + // Validação mínima + if (!buyerEmail || !buyerEmail.includes('@')) { + console.log('⌠Email inválido:', buyerEmail); + return NextResponse.json({ + error: 'Email do comprador é obrigatório e deve ser válido', + received_email: buyerEmail + }, { status: 400 }); + } + + // Conectar ao banco + await prisma.$connect(); + + // Verificar se usuário já existe + const email = buyerEmail.toLowerCase().trim(); + let user = await prisma.user.findUnique({ + where: { email } + }); + + let isNewUser = false; + let tempPassword = ''; + let treatedAsAddon = false; + let entitlementExpiration: Date | null = null; + + if (user) { + // ATUALIZAR usuário existente + console.log(`🔄 Atualizando usuário existente: ${email}`); + + treatedAsAddon = isAddonPurchase && user.plan !== integrationData.plan; + const phonePayload = buyerPhone && !user.phone ? { phone: buyerPhone } : {}; + + if (treatedAsAddon) { + console.log(`âž• Ativando complemento ${integrationData.plan} para ${email} via Kiwify`); + entitlementExpiration = calculateExpirationDate(integrationData.plan); + user = await prisma.user.update({ + where: { email }, + data: { + status: 'ACTIVE', + hotmartCustomerId: transactionId, + origin: 'KIWIFY', + ...phonePayload + } + }); + } else { + const nextExpiration = calculateExpirationDate(integrationData.plan); + entitlementExpiration = isAddonPurchase ? nextExpiration : entitlementExpiration; + + user = await prisma.user.update({ + where: { email }, + data: { + plan: integrationData.plan, // 🔥 PLANO DETECTADO AUTOMATICAMENTE + status: 'ACTIVE', + hotmartCustomerId: transactionId, + expirationDate: nextExpiration, + origin: 'KIWIFY', + ...phonePayload + } + }); + } + + console.log(`✅ Usuário atualizado via Kiwify: ${email} → ${integrationData.plan}`); + + } else { + // CRIAR novo usuário + isNewUser = true; + tempPassword = generateSecurePassword(); + const hashedPassword = await hashPassword(tempPassword); + + console.log(`âž• Criando novo usuário via Kiwify: ${email}`); + + const nameParts = buyerName.split(' '); + const firstName = nameParts[0] || 'Cliente'; + const lastName = nameParts.slice(1).join(' ') || 'Kiwify'; + + const nextExpiration = calculateExpirationDate(integrationData.plan); + entitlementExpiration = isAddonPurchase ? nextExpiration : entitlementExpiration; + + user = await prisma.user.create({ + data: { + email: email, + firstName: firstName, + lastName: lastName, + phone: buyerPhone ?? null, + plan: integrationData.plan, // 🔥 PLANO DETECTADO AUTOMATICAMENTE + status: 'ACTIVE', + hotmartCustomerId: transactionId, + expirationDate: nextExpiration, + origin: 'KIWIFY', + password: hashedPassword, // ✅ USANDO HASH SEGURO + passwordCreatedAt: new Date(), + mustChangePassword: true, + customPermissions: '[]' + } + }); + + console.log(`✅ Novo usuário criado via Kiwify: ${email} → ${integrationData.plan}`); + } + + if (isAddonPurchase) { + const expiresAt = entitlementExpiration ?? calculateExpirationDate(integrationData.plan); + await activateEntitlement(prisma, { + userId: user.id, + code: integrationData.plan, + origin: 'KIWIFY', + expiresAt, + }); + if (expiresAt) { + console.log(`🎠Complemento ${integrationData.plan} ativo até ${expiresAt.toISOString()} via Kiwify`); + } else { + console.log(`🎠Complemento ${integrationData.plan} ativado com acesso vitalício via Kiwify.`); + } + } + + // Registrar compra + try { + let purchaseHandled = false; + + if (transactionId) { + const existingPurchase = await prisma.purchase.findFirst({ + where: { + hotmartTransactionId: transactionId, + }, + orderBy: { + createdAt: 'desc', + }, + }); + + if (existingPurchase) { + await prisma.purchase.update({ + where: { id: existingPurchase.id }, + data: { + userId: user.id, + amount: normalizedAmount, + productName: integrationData.name, + status: 'COMPLETED', + paymentMethod: normalizePaymentMethod(paymentMethod), + installments: installments, + platform: 'KIWIFY' + }, + }); + + console.log( + `â™»ï¸ Compra Kiwify existente atualizada: ${normalizedAmount} - ${integrationData.name}` + ); + purchaseHandled = true; + } + } + + if (!purchaseHandled) { + await prisma.purchase.create({ + data: { + userId: user.id, + amount: normalizedAmount, + productName: integrationData.name, // 🔥 NOME DETECTADO + hotmartTransactionId: transactionId, + status: 'COMPLETED', + paymentMethod: normalizePaymentMethod(paymentMethod), + installments: installments, + platform: 'KIWIFY' + }, + }); + console.log(`💰 Compra Kiwify registrada: ${normalizedAmount} - ${integrationData.name} (${normalizePaymentMethod(paymentMethod)}, ${installments}x)`); + } + + const correctedPurchases = await fixUnrealisticPurchaseAmounts(prisma, user.id); + if (correctedPurchases > 0) { + console.log( + `🧹 Corrigidos ${correctedPurchases} lançamentos antigos com valores inconsistentes para ${user.email}` + ); + } + } catch (purchaseError) { + console.error('âš ï¸ Erro ao registrar compra Kiwify:', purchaseError); + } + + // ⌠REMOVIDO ENVIO DE EMAIL (TEMPORARIAMENTE) + // let emailSent = false; + // let emailError = null; + + // if (isNewUser && tempPassword) { + // try { + // console.log(`📧 Enviando email de boas-vindas para ${email}...`); + // await enviarEmailCredenciais(email, user.firstName || buyerName, tempPassword); + // emailSent = true; + // console.log('✅ Email enviado com sucesso via Kiwify!'); + // } catch (error: any) { + // emailError = error.message; + // console.error('⌠Erro ao enviar email via Kiwify:', error); + // } + // } + + await prisma.$disconnect(); + + const response = { + success: true, + message: `Webhook Kiwify processado com sucesso`, + platform: 'Kiwify', + integration: { + id: integrationData.integrationId, + name: integrationData.name, + plan: integrationData.plan, + token: token + }, + productDetection: { + detectedPlan: integrationData.plan, + detectedProductName: integrationData.name, + originalProductName: rawProductName || webhookData.product_name || webhookData.name || 'N/A', + amount: normalizedAmount + }, + user: { + id: user.id, + email: user.email, + plan: user.plan, + status: user.status, + isNewUser: isNewUser, + tempPassword: isNewUser ? tempPassword : undefined, // ✅ Mostra senha para debug + addonGranted: isAddonPurchase, + treatedAsAddon, + }, + transaction: { + id: transactionId, + amount: normalizedAmount, + product: integrationData.name + }, + timestamp: new Date().toISOString() + }; + + console.log(`🥠Webhook Kiwify ${token} processado com sucesso:`, response); + + return NextResponse.json(response); + + } catch (error: any) { + console.error(`⌠Erro no webhook Kiwify ${params.token}:`, error); + + try { + await prisma.$disconnect(); + } catch (disconnectError) { + console.error('⌠Erro ao desconectar:', disconnectError); + } + + return NextResponse.json( + { + error: 'Erro interno do servidor', + platform: 'Kiwify', + token: params.token, + message: error.message, + timestamp: new Date().toISOString() + }, + { status: 500 } + ); + } +} + +// GET para status da integração Kiwify +export async function GET( + request: NextRequest, + { params }: { params: { token: string } } +) { + const token = params.token; + const integration = KIWIFY_TOKEN_MAPPING[token]; + + if (!integration) { + return NextResponse.json( + { error: `Token Kiwify ${token} não encontrado` }, + { status: 404 } + ); + } + + return NextResponse.json({ + success: true, + platform: 'Kiwify', + integration: { + id: integration.integrationId, + name: 'Detecção Automática de Produtos', + plan: 'DETECTADO_AUTOMATICAMENTE', + token: token, + status: 'ACTIVE', + webhookUrl: `${new URL(request.url).origin}/api/webhooks/kiwify/${token}`, + supportedProducts: [ + 'Close Friends LITE 2.0', + 'Close Friends VIP - Turma 7', + 'Close Friends VIP - Turma 6', + 'Close Friends VIP - Turma 5', + 'Close Friends VIP - Turma 4', + 'Close Friends VIP - Turma 3', + 'Close Friends VIP - Turma 2', + 'Close Friends LITE', + 'Projeto FIIs - Assinatura anual', + 'Análise em vídeo - até 30 minutos', + 'Migração CF Lite' + ] + }, + message: `Integração Kiwify com detecção automática de ${11} produtos`, + timestamp: new Date().toISOString() + }); +} diff --git a/src/app/auth/change-password/page.tsx b/src/app/auth/change-password/page.tsx new file mode 100644 index 000000000..4a2293ac0 --- /dev/null +++ b/src/app/auth/change-password/page.tsx @@ -0,0 +1,102 @@ +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { Box, Typography, Alert } from '@mui/material'; +import { useUser } from '@/hooks/use-user'; +import { ChangePasswordForm } from '@/components/auth/change-password-form'; + +export default function ChangePasswordPage(): React.JSX.Element { + const router = useRouter(); + const { user, checkSession } = useUser(); + + React.useEffect(() => { + // Se não está logado, redirecionar para login + if (!user) { + router.push('/auth/sign-in'); + return; + } + + // Se não precisa mudar senha, redirecionar para dashboard + if (!user.mustChangePassword) { + router.push('/dashboard'); + return; + } + }, [user, router]); + + if (!user) { + return ( + + Redirecionando... + + ); + } + + return ( + + + + + + 🔠Alterar Senha + + + + Por segurança, você deve alterar sua senha antes de continuar. + + + + + âš ï¸ Primeiro acesso detectado
+ Por favor, defina uma nova senha para sua conta. +
+
+ + { + checkSession?.(); // Atualizar dados do usuário + router.push('/dashboard'); + }} + /> +
+
+ ); +} \ No newline at end of file diff --git a/src/app/auth/reset-password/[token]/page.tsx b/src/app/auth/reset-password/[token]/page.tsx new file mode 100644 index 000000000..9e0d42a06 --- /dev/null +++ b/src/app/auth/reset-password/[token]/page.tsx @@ -0,0 +1,155 @@ +'use client'; + +import * as React from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import { Box, Typography, Alert, CircularProgress } from '@mui/material'; +import { ResetPasswordWithTokenForm } from '@/components/auth/reset-password-token-form'; + +export default function ResetPasswordTokenPage(): React.JSX.Element { + const params = useParams(); + const router = useRouter(); + const token = params.token as string; + + const [loading, setLoading] = React.useState(true); + const [valid, setValid] = React.useState(false); + const [error, setError] = React.useState(null); + const [userEmail, setUserEmail] = React.useState(''); + + React.useEffect(() => { + const verifyToken = async () => { + try { + const response = await fetch(`/api/auth/reset-password/${token}`); + const data = await response.json(); + + if (data.valid) { + setValid(true); + setUserEmail(data.user.email); + } else { + setError(data.error || 'Token inválido'); + } + } catch (err) { + setError('Erro ao verificar token'); + } finally { + setLoading(false); + } + }; + + if (token) { + verifyToken(); + } + }, [token]); + + if (loading) { + return ( + + + + Verificando link... + + + ); + } + + if (!valid || error) { + return ( + + + + + + {error || 'Link inválido ou expirado'} + + + + Solicite um novo link de recuperação de senha. + + + router.push('/auth/reset-password')} + sx={{ + backgroundColor: '#00FF00', + color: '#000', + border: 'none', + borderRadius: '30px', + padding: '12px 24px', + fontWeight: 'bold', + cursor: 'pointer', + '&:hover': { + backgroundColor: '#00e600', + }, + }} + > + Solicitar novo link + + + + ); + } + + return ( + + + + + + Redefinir Senha + + + + Para: {userEmail} + + + + + + ); +} \ No newline at end of file diff --git a/src/app/auth/reset-password/page.tsx b/src/app/auth/reset-password/page.tsx new file mode 100644 index 000000000..e2eae7e1e --- /dev/null +++ b/src/app/auth/reset-password/page.tsx @@ -0,0 +1,39 @@ +'use client'; + +import * as React from 'react'; +import { Box } from '@mui/material'; +import { GuestGuard } from '@/components/auth/guest-guard'; +import { ResetPasswordForm } from '@/components/auth/reset-password-form'; + +export default function Page(): React.JSX.Element { + return ( + + + + + + + + + ); +} diff --git a/src/app/auth/sign-in/page.tsx b/src/app/auth/sign-in/page.tsx new file mode 100644 index 000000000..2962828a3 --- /dev/null +++ b/src/app/auth/sign-in/page.tsx @@ -0,0 +1,36 @@ +'use client'; + +import * as React from 'react'; +import { Box } from '@mui/material'; +import { SignInForm } from '@/components/auth/sign-in-form'; + +export default function Page(): React.JSX.Element { + return ( + + + + + + + ); +} diff --git a/src/app/auth/sign-up/page.tsx b/src/app/auth/sign-up/page.tsx new file mode 100644 index 000000000..41c34dd6a --- /dev/null +++ b/src/app/auth/sign-up/page.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import type { Metadata } from 'next'; + +import { config } from '@/config'; +import { GuestGuard } from '@/components/auth/guest-guard'; +import { Layout } from '@/components/auth/layout'; +import { SignUpForm } from '@/components/auth/sign-up-form'; + +export const metadata = { title: `Sign up | Auth | ${config.site.name}` } satisfies Metadata; + +export default function Page(): React.JSX.Element { + return ( + + + + + + ); +} diff --git a/src/app/dados-ifix/route.ts b/src/app/dados-ifix/route.ts new file mode 100644 index 000000000..2c6fce8d2 --- /dev/null +++ b/src/app/dados-ifix/route.ts @@ -0,0 +1,31 @@ +export async function GET() { + const agora = new Date(); + const hora = agora.getHours(); + const minuto = agora.getMinutes(); + + const isHorarioComercial = hora >= 9 && hora <= 18; + const valorBase = 3440; + const variacao = (Math.random() - 0.5) * (isHorarioComercial ? 3 : 1); + const novoValor = valorBase * (1 + variacao / 100); + + const dadosIfix = { + valor: novoValor, + valorFormatado: novoValor.toLocaleString('pt-BR', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }), + variacao: valorBase * (variacao / 100), + variacaoPercent: variacao, + trend: variacao >= 0 ? 'up' : 'down', + timestamp: agora.toISOString(), + fonte: 'SIMULAÇÃO_VERCEL', + nota: `Atualizado ${hora.toString().padStart(2, '0')}:${minuto.toString().padStart(2, '0')}`, + symbol: 'IFIX' + }; + + return Response.json({ + ifix: dadosIfix, + success: true, + timestamp: agora.toISOString() + }); +} diff --git a/src/app/dashboard/MicroCapsPageV2/page.tsx b/src/app/dashboard/MicroCapsPageV2/page.tsx new file mode 100644 index 000000000..3c847ea32 --- /dev/null +++ b/src/app/dashboard/MicroCapsPageV2/page.tsx @@ -0,0 +1,1104 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +'use client'; + +import * as React from 'react'; + +// 🚀 HOOKS CUSTOMIZADOS +import { useMicroCapsData } from '@/hooks/micro-caps/useMicroCapsData'; +import { useMarketData, useIbovespaPeriodo } from '@/hooks/micro-caps/useMarketData'; + +// 🧮 UTILITÃRIOS +import { calcularMetricasCarteira, formatCurrency, formatPercentage } from '@/utils/micro-caps/calculationUtils'; +import { getViesBadgePalette } from '@/utils/vies'; + +// 🎨 ESTILOS RESPONSIVOS +const styles = { + container: (isMobile: boolean) => ({ + minHeight: '100vh', + backgroundColor: '#f5f5f5', + padding: isMobile ? '16px' : '24px' + }), + + header: (isMobile: boolean) => ({ + marginBottom: isMobile ? '24px' : '32px' + }), + + title: (isMobile: boolean) => ({ + fontSize: isMobile ? '32px' : '48px', + fontWeight: '800', + color: '#1e293b', + margin: '0 0 8px 0', + lineHeight: 1.2 + }), + + metricsGrid: (isMobile: boolean) => ({ + display: 'grid', + gridTemplateColumns: isMobile ? 'repeat(2, 1fr)' : 'repeat(auto-fit, minmax(180px, 1fr))', + gap: isMobile ? '12px' : '16px', + marginBottom: isMobile ? '24px' : '32px' + }), + + card: (isMobile: boolean) => ({ + backgroundColor: '#ffffff', + borderRadius: '8px', + padding: isMobile ? '16px' : '20px', + border: '1px solid #e2e8f0', + boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)' + }), + + cardLabel: { + fontSize: '12px', + color: '#64748b', + fontWeight: '500', + marginBottom: '8px' + }, + + cardValue: (isMobile: boolean, color = '#1e293b') => ({ + fontSize: isMobile ? '20px' : '24px', + fontWeight: '700', + color, + lineHeight: '1' + }) +}; + +// 🔥 CSS ANIMATIONS +const cssAnimations = ` + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + + @keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } + } + + .fade-in { + animation: fadeIn 0.5s ease-out; + } + + .card-hover { + transition: all 0.2s ease; + } + + .card-hover:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); + } +`; + +// ðŸ·ï¸ COMPONENTE: Loading State +const LoadingState: React.FC<{ isMobile: boolean; screenWidth: number }> = ({ isMobile, screenWidth }) => ( +
+
+
+

+ Carregando Micro Caps V2 +

+

+ 📱 Dispositivo: {isMobile ? 'Mobile' : 'Desktop'} ({screenWidth}px) +

+
+
+); + +// ðŸ·ï¸ COMPONENTE: Metrics Card +const MetricCard: React.FC<{ + label: string; + value: string; + color?: string; + subValue?: string; + isMobile: boolean; +}> = ({ label, value, color = '#1e293b', subValue, isMobile }) => ( +
+
{label}
+
{value}
+ {subValue && ( +
+ {subValue} +
+ )} +
+); + +// ðŸ·ï¸ COMPONENTE: Asset Card (Mobile) +const AssetCard: React.FC<{ ativo: any; index: number }> = ({ ativo, index }) => { + const viesPalette = getViesBadgePalette(ativo.vies); + + return ( +
+ {/* Header do Card */} +
+
+ {`Logo { + // Fallback para ícone com iniciais se a imagem não carregar + const target = e.target as HTMLImageElement; + target.style.display = 'none'; + const parent = target.parentElement; + if (parent) { + parent.style.backgroundColor = ativo.performance >= 0 ? '#dcfce7' : '#fee2e2'; + parent.style.color = ativo.performance >= 0 ? '#065f46' : '#991b1b'; + parent.style.fontWeight = '700'; + parent.style.fontSize = '14px'; + parent.textContent = ativo.ticker.slice(0, 2); + } + }} + /> +
+
+
+ {ativo.ticker} +
+
+ {ativo.setor} +
+
+
= 0 ? '#10b981' : '#ef4444', + textAlign: 'right' + }}> + {formatPercentage(ativo.performance)} +
+
+ + {/* Dados do Card */} +
+
+
Entrada
+
{ativo.dataEntrada}
+
+
+
Preço Atual
+
{formatCurrency(ativo.precoAtual)}
+
+
+
DY 12M
+
{ativo.dy}
+
+
+
Viés
+ + {ativo.vies} + +
+
+
+ ); +}; + +// ðŸ·ï¸ COMPONENTE PRINCIPAL DA PÃGINA +export default function MicroCapsV2Page() { + // 🚀 HOOKS CUSTOMIZADOS + const { ativosAtualizados, loading, error, refetch, isMobile, screenWidth, stats } = useMicroCapsData(); + const { smllData, ibovespaData } = useMarketData(); + const { ibovespaPeriodo } = useIbovespaPeriodo(ativosAtualizados); + + // 🔠DEBUG: Verificar dados no mobile + React.useEffect(() => { + console.log('🔠DEBUG MOBILE:', { + isMobile, + screenWidth, + ativosCount: ativosAtualizados.length, + timestamp: new Date().toISOString() + }); + + // 🎯 NOVO: Listar todos os tickers para comparar + const tickersAtivos = ativosAtualizados.map(a => a.ticker).sort(); + console.log('📊 TICKERS CARREGADOS:', tickersAtivos); + console.log('📊 TOTAL DE TICKERS:', tickersAtivos.length); + + if (typeof window !== 'undefined') { + console.log('📦 localStorage keys:', Object.keys(localStorage)); + + // Verificar dados originais do store + const dadosStore = localStorage.getItem('dados-membros'); + if (dadosStore) { + try { + const parsed = JSON.parse(dadosStore); + const microCapsOriginal = parsed.microCaps || []; + const tickersOriginais = microCapsOriginal.map((a: any) => a.ticker).sort(); + + console.log('📋 TICKERS NO STORE ORIGINAL:', tickersOriginais); + console.log('📋 TOTAL NO STORE:', tickersOriginais.length); + + // 🎯 ENCONTRAR DIFERENÇAS + const tickersFaltando = tickersOriginais.filter((t: string) => !tickersAtivos.includes(t)); + const tickersExtras = tickersAtivos.filter(t => !tickersOriginais.includes(t)); + + if (tickersFaltando.length > 0) { + console.log('⌠TICKERS FALTANDO (no store mas não processados):', tickersFaltando); + } + if (tickersExtras.length > 0) { + console.log('âž• TICKERS EXTRAS (processados mas não no store):', tickersExtras); + } + if (tickersFaltando.length === 0 && tickersExtras.length === 0) { + console.log('✅ TODOS OS TICKERS SINCRONIZADOS'); + } + } catch (e) { + console.error('⌠Erro ao parsear dados do store:', e); + } + } + } + }, [ativosAtualizados, isMobile, screenWidth]); + + // 🚨 NOVO: Listener para mudanças no localStorage + React.useEffect(() => { + if (typeof window === 'undefined') return; + + const handleStorageChange = (e: StorageEvent) => { + if (e.key === 'dados-membros') { + console.log('🔄 STORAGE CHANGED DETECTED!'); + console.log('📦 Old Value length:', e.oldValue?.length || 0); + console.log('📦 New Value length:', e.newValue?.length || 0); + + // Forçar re-render em 1 segundo + setTimeout(() => { + console.log('🔄 Forçando refresh após storage change...'); + refetch(); + }, 1000); + } + }; + + // Listen for storage changes from other tabs/pages + window.addEventListener('storage', handleStorageChange); + + return () => { + window.removeEventListener('storage', handleStorageChange); + }; + }, [refetch]); + + // 🧮 CALCULAR MÉTRICAS + const metricas = React.useMemo(() => + calcularMetricasCarteira(ativosAtualizados, 1000), + [ativosAtualizados] + ); + + // 🔄 REFRESH MANUAL + const handleRefresh = React.useCallback(() => { + console.log('🔄 Refresh manual solicitado'); + refetch(); + }, [refetch]); + + // 🔠DEBUG DE PROVENTOS (TEMPORÃRIO) + const debugProventos = React.useCallback(() => { + if (typeof window === 'undefined') return; + + const debugInfo = []; + + const master = localStorage.getItem('proventos_central_master'); + debugInfo.push(`Master: ${master ? 'EXISTE' : 'NÃO EXISTE'}`); + + if (master) { + try { + const dados = JSON.parse(master); + debugInfo.push(`Total proventos no master: ${dados.length}`); + const tickers = new Set(dados.map((d: any) => d.ticker)); + debugInfo.push(`Tickers no master: ${Array.from(tickers).slice(0, 5).join(', ')}...`); + } catch (e: any) { + debugInfo.push(`Erro no master: ${e.message}`); + } + } + + const tickersAmostra = ativosAtualizados.slice(0, 3).map(a => a.ticker); + tickersAmostra.forEach(ticker => { + const individual = localStorage.getItem(`proventos_${ticker}`); + debugInfo.push(`${ticker} individual: ${individual ? 'EXISTE' : 'NÃO EXISTE'}`); + }); + + alert(debugInfo.join('\n')); + }, [ativosAtualizados]); + + // 🔄 FORÇA REFRESH TOTAL (MOBILE DEBUG) + const forceRefreshMobile = React.useCallback(() => { + console.log('🔄 FORÇA REFRESH MOBILE INICIADO'); + + // Limpar cache de hooks + if (typeof window !== 'undefined') { + console.log('📦 Dados antes:', localStorage.getItem('dados-membros')); + + // 🎯 FORÇAR RE-READ DO DATASTORE + window.dispatchEvent(new Event('storage')); + + // Tentar forçar refresh do hook + setTimeout(() => { + console.log('🔄 Forçando reload da página...'); + window.location.reload(); + }, 500); + } + + // Forçar re-fetch + refetch(); + }, [refetch]); + + // 🚨 NOVO: Debug específico do DataStore + const debugDataStore = React.useCallback(() => { + if (typeof window === 'undefined') return; + + const dadosAtual = localStorage.getItem('dados-membros'); + console.log('🔠DEBUG DATASTORE COMPLETO:'); + console.log('📦 Raw localStorage:', dadosAtual); + + if (dadosAtual) { + try { + const parsed = JSON.parse(dadosAtual); + console.log('📊 Dados parseados:', parsed); + console.log('📋 MicroCaps no store:', parsed.microCaps?.length || 0); + console.log('📋 Tickers no store:', parsed.microCaps?.map((a: any) => a.ticker) || []); + + // Verificar timestamp da última atualização + if (parsed.lastUpdated) { + console.log('ⰠÚltima atualização:', new Date(parsed.lastUpdated).toLocaleString()); + } + } catch (e) { + console.error('⌠Erro ao parsear:', e); + } + } + + alert(`DataStore Debug: + - Ativos na página: ${ativosAtualizados.length} + - Device: ${isMobile ? 'Mobile' : 'Desktop'} + - Veja console para detalhes`); + }, [ativosAtualizados, isMobile]); + + // 🔄 LOADING STATE + if (loading) { + return ( + <> + + + + ); + } + + return ( +
+ + + {/* 📱 Header Responsivo */} +
+

+ 🚀 Micro Caps V2 - Refatorado +

+
+

+ Hooks customizados • App Router • {metricas.quantidadeAtivos} ativos • 📱 {isMobile ? 'Mobile' : 'Desktop'} ({screenWidth}px) +

+ +
+ + + + + + + {isMobile && ( + + )} +
+
+ + {error && ( +
+ âš ï¸ {error} +
+ )} +
+ + {/* 📊 Cards de Métricas */} +
+ = 0 ? '#10b981' : '#ef4444'} + isMobile={isMobile} + /> + + + + + + + + = 0 ? '#10b981' : '#ef4444'} + subValue={`Desde ${ibovespaPeriodo?.dataInicial || 'jan/2020'}`} + isMobile={isMobile} + /> + + + + +
+ + {/* 📋 Tabela/Cards dos Ativos */} +
+ {/* Header */} +
+

+ Micro Caps • Performance Individual +

+

+ Hooks refatorados • {stats.ativosComCotacao}/{stats.totalAtivos} cotações • {stats.ativosComDY}/{stats.totalAtivos} DY +

+
+ + {/* Conteúdo */} + {isMobile ? ( +
+ {ativosAtualizados.map((ativo, index) => ( + + ))} +
+ ) : ( +
+ + + + + + + + + + + + + {ativosAtualizados.map((ativo, index) => { + const viesPalette = getViesBadgePalette(ativo.vies); + + return ( + { + e.currentTarget.style.backgroundColor = '#f8fafc'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.backgroundColor = 'transparent'; + }} + > + + + + + + + + ); + })} + +
+ ATIVO + + ENTRADA + + PREÇO ATUAL + + PERFORMANCE + + DY 12M + + VIÉS +
+
+
+ {`Logo { + // Fallback para ícone com iniciais se a imagem não carregar + const target = e.target as HTMLImageElement; + target.style.display = 'none'; + const parent = target.parentElement; + if (parent) { + parent.style.backgroundColor = ativo.performance >= 0 ? '#dcfce7' : '#fee2e2'; + parent.style.color = ativo.performance >= 0 ? '#065f46' : '#991b1b'; + parent.style.fontWeight = '700'; + parent.style.fontSize = '14px'; + parent.textContent = ativo.ticker.slice(0, 2); + } + }} + /> +
+
+
+ {ativo.ticker} +
+
+ {ativo.setor} +
+
+
+
+ {ativo.dataEntrada} + + {formatCurrency(ativo.precoAtual)} + = 0 ? '#10b981' : '#ef4444' + }}> + {formatPercentage(ativo.performance)} + + {ativo.dy} + + + {ativo.vies} + +
+
+ )} +
+ + {/* 📊 Gráfico de Composição por Ativos - RESPONSIVO */} +
+
+

+ 📊 Composição da Carteira +

+

+ Distribuição percentual • {ativosAtualizados.length} ativos • {isMobile ? 'Mobile' : 'Desktop'} +

+
+ +
+ {/* Gráfico SVG Responsivo */} +
+ {(() => { + const cores = [ + '#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', + '#06b6d4', '#84cc16', '#f97316', '#ec4899', '#6366f1', + '#14b8a6', '#eab308', '#dc2626', '#7c3aed', '#0891b2', + '#65a30d', '#ea580c', '#db2777', '#4f46e5', '#0d9488' + ]; + + // 📱 CONFIGURAÇÕES RESPONSIVAS + const radius = isMobile ? 100 : 150; + const innerRadius = isMobile ? 50 : 75; + const svgSize = isMobile ? 250 : 400; + const centerX = svgSize / 2; + const centerY = svgSize / 2; + const totalAtivos = ativosAtualizados.length; + + if (totalAtivos === 0) { + return ( +
+ Nenhum ativo para exibir +
+ ); + } + + const anglePerSlice = (2 * Math.PI) / totalAtivos; + + const createPath = (startAngle: number, endAngle: number) => { + const x1 = centerX + radius * Math.cos(startAngle); + const y1 = centerY + radius * Math.sin(startAngle); + const x2 = centerX + radius * Math.cos(endAngle); + const y2 = centerY + radius * Math.sin(endAngle); + + const x3 = centerX + innerRadius * Math.cos(endAngle); + const y3 = centerY + innerRadius * Math.sin(endAngle); + const x4 = centerX + innerRadius * Math.cos(startAngle); + const y4 = centerY + innerRadius * Math.sin(startAngle); + + const largeArcFlag = endAngle - startAngle <= Math.PI ? "0" : "1"; + + return `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2} L ${x3} ${y3} A ${innerRadius} ${innerRadius} 0 ${largeArcFlag} 0 ${x4} ${y4} Z`; + }; + + return ( + + + + + + {ativosAtualizados.map((ativo, index) => { + const startAngle = index * anglePerSlice - Math.PI / 2; + const endAngle = (index + 1) * anglePerSlice - Math.PI / 2; + const cor = cores[index % cores.length]; + const path = createPath(startAngle, endAngle); + + // Calcular posição do texto no meio da fatia + const middleAngle = (startAngle + endAngle) / 2; + const textRadius = (radius + innerRadius) / 2; + const textX = centerX + textRadius * Math.cos(middleAngle); + const textY = centerY + textRadius * Math.sin(middleAngle); + const porcentagem = (100 / totalAtivos).toFixed(1); + + return ( + + + {ativo.ticker}: {porcentagem}% + + + {/* Textos que aparecem no hover - Responsivos */} + + {/* Texto do ticker */} + + {ativo.ticker} + + + {/* Texto da porcentagem */} + + {porcentagem}% + + + + ); + })} + + {/* Círculo central */} + + + {/* Texto central - Responsivo */} + + {totalAtivos} + + + ATIVOS + + + ); + })()} +
+ + {/* Legenda Responsiva */} +
+ {ativosAtualizados.map((ativo, index) => { + const porcentagem = ativosAtualizados.length > 0 ? ((1 / ativosAtualizados.length) * 100).toFixed(1) : '0.0'; + const cores = [ + '#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', + '#06b6d4', '#84cc16', '#f97316', '#ec4899', '#6366f1', + '#14b8a6', '#eab308', '#dc2626', '#7c3aed', '#0891b2', + '#65a30d', '#ea580c', '#db2777', '#4f46e5', '#0d9488' + ]; + const cor = cores[index % cores.length]; + + return ( +
+
+
+
+ {ativo.ticker} +
+
+ {porcentagem}% +
+
+
+ ); + })} +
+
+
+ + {/* 📊 Debug Info */} +
+
✅ REFATORADO V2 • App Router • Next.js 13+ • Device: {isMobile ? 'Mobile' : 'Desktop'} • Screen: {screenWidth}px
+
🔄 Hooks: useMicroCapsData + useMarketData + useApiStrategy + useResponsive • Ativos: {stats.totalAtivos}
+
📈 API: {stats.ativosComCotacao} cotações + {stats.ativosComDY} DY • Layout: {isMobile ? 'Cards' : 'Table'} • Gráfico: {isMobile ? 'Mobile (250px)' : 'Desktop (400px)'}
+
📠Rota: /dashboard/MicroCapsPageV2 • Performance: Total Return (ação + proventos)
+ {isMobile && ( +
+ 📱 MOBILE SYNC ISSUE: Se alterações do gerenciamento não aparecem, use "📊 DataStore" e "📱 Force Sync" +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/src/app/dashboard/account/page.tsx b/src/app/dashboard/account/page.tsx new file mode 100644 index 000000000..07478c341 --- /dev/null +++ b/src/app/dashboard/account/page.tsx @@ -0,0 +1,53 @@ +'use client'; +import * as React from 'react'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +import { AccountDetailsForm } from '@/components/dashboard/account/account-details-form'; +import { AccountInfo } from '@/components/dashboard/account/account-info'; +import { AccountSecurity } from '@/components/dashboard/account/account-security'; +import { AccountSubscription } from '@/components/dashboard/account/account-subscription'; +import { useUser } from '@/hooks/use-user'; + +export default function Page(): React.JSX.Element { + const { user, isLoading } = useUser(); + + if (isLoading || !user) { // ADICIONE ESTA LINHA + return ( + + + Carregando... + + + ); + } + + return ( + +
+ + Minha Conta + + + Gerencie suas informações pessoais e configurações de segurança + +
+ + + + + + + + + + + + + + + + +
+ ); +} diff --git a/src/app/dashboard/admin/analise-tecnica/page.tsx b/src/app/dashboard/admin/analise-tecnica/page.tsx new file mode 100644 index 000000000..a70ad1f94 --- /dev/null +++ b/src/app/dashboard/admin/analise-tecnica/page.tsx @@ -0,0 +1,15 @@ +'use client'; + +import React, { Suspense } from 'react'; + +import AdminAnaliseTecnicaManager from '@/components/AdminAnaliseTecnicaManager'; + +export const dynamic = 'force-dynamic'; + +export default function AdminAnaliseTecnicaPage() { + return ( + + + + ); +} diff --git a/src/app/dashboard/admin/analise-tecnica/publicadas/page.tsx b/src/app/dashboard/admin/analise-tecnica/publicadas/page.tsx new file mode 100644 index 000000000..138c8638d --- /dev/null +++ b/src/app/dashboard/admin/analise-tecnica/publicadas/page.tsx @@ -0,0 +1,11 @@ +'use client'; + +import React from 'react'; + +import AdminAnaliseTecnicaList from '@/components/AdminAnaliseTecnicaList'; + +export const dynamic = 'force-dynamic'; + +export default function AdminAnaliseTecnicaPublicadasPage() { + return ; +} diff --git a/src/app/dashboard/admin/analises-trimesestrais/page.tsx b/src/app/dashboard/admin/analises-trimesestrais/page.tsx new file mode 100644 index 000000000..3fa082fb2 --- /dev/null +++ b/src/app/dashboard/admin/analises-trimesestrais/page.tsx @@ -0,0 +1,5 @@ +import AdminAnalisesTrimesestrais from '../../../../components/AdminAnalisesTrimesestrais'; + +export default function AnalisesTrimesestraisPage() { + return ; +} \ No newline at end of file diff --git a/src/app/dashboard/admin/carteiras/page.tsx b/src/app/dashboard/admin/carteiras/page.tsx new file mode 100644 index 000000000..5be7b5777 --- /dev/null +++ b/src/app/dashboard/admin/carteiras/page.tsx @@ -0,0 +1,2699 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import Link from 'next/link'; +import EnhancedAdminFeedback from '@/components/EnhancedAdminFeedback'; +import { API_CONFIG, buildBatchApiUrl } from '@/config/apiConfig'; + +export default function AdminCarteirasPage() { + const [currentTab, setCurrentTab] = useState(0); + const [carteiras, setCarteiras] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [editando, setEditando] = useState(null); + const [feedback, setFeedback] = useState(''); + const [pontuacao, setPontuacao] = useState(''); + const [recomendacoes, setRecomendacoes] = useState(''); + const [showAdvancedFeedback, setShowAdvancedFeedback] = useState(false); + const [expandedAnalysed, setExpandedAnalysed] = useState(new Set()); + const [expandedResumoAtivos, setExpandedResumoAtivos] = useState(new Set()); + const [showDetailedAnalysis, setShowDetailedAnalysis] = useState(null); + const [precosAtuais, setPrecosAtuais] = useState(new Map()); + const [statusCotacoes, setStatusCotacoes] = useState(new Map()); + + const [stats, setStats] = useState({ + total: 0, + pendente: 0, + em_analise: 0, + analisada: 0, + cancelada: 0 + }); + + const parseNumero = (valor) => { + if (valor === null || valor === undefined || valor === '') return null; + if (typeof valor === 'number') { + return Number.isFinite(valor) ? valor : null; + } + if (typeof valor === 'string') { + const normalizado = valor + .replace(/R\$/gi, '') + .replace(/\s+/g, '') + .replace(/\./g, '') + .replace(',', '.') + .replace(/[^0-9.-]/g, ''); + if (!normalizado) return null; + const convertido = parseFloat(normalizado); + return Number.isNaN(convertido) ? null : convertido; + } + return null; + }; + + const formatCurrency = (valor) => { + if (valor === null || valor === undefined || Number.isNaN(valor)) return '—'; + try { + return new Intl.NumberFormat('pt-BR', { + style: 'currency', + currency: 'BRL' + }).format(valor); + } catch (error) { + const numero = Number(valor); + return Number.isNaN(numero) ? '—' : `R$ ${numero.toFixed(2)}`; + } + }; + + const formatPercent = (valor) => { + if (valor === null || valor === undefined || Number.isNaN(valor)) return '—'; + const sinal = valor > 0 ? '+' : ''; + return `${sinal}${valor.toFixed(1)}%`; + }; + + const formatQuantidade = (valor) => { + if (valor === null || valor === undefined || Number.isNaN(valor)) return '—'; + return valor.toLocaleString('pt-BR', { + minimumFractionDigits: Number.isInteger(valor) ? 0 : 2, + maximumFractionDigits: 2 + }); + }; + + const coletarTickers = (dadosEstruturados) => { + if (!dadosEstruturados) return []; + + const camposCodigo = ['codigo', 'ticker', 'sigla', 'ativo', 'symbol', 'papel']; + const categoriasIgnoradas = new Set(['rendaFixa', 'reserva', 'cripto']); + const tickers = new Set(); + + Object.entries(dadosEstruturados).forEach(([categoria, itens]) => { + if (categoriasIgnoradas.has(categoria) || !Array.isArray(itens)) { + return; + } + + itens.forEach((item) => { + if (!item || typeof item !== 'object') return; + + for (const campo of camposCodigo) { + const valor = item[campo]; + if (typeof valor === 'string') { + const normalizado = valor.trim().toUpperCase(); + if (normalizado) { + tickers.add(normalizado); + break; + } + } + } + }); + }); + + return Array.from(tickers); + }; + + const consolidarAtivosCarteira = (dadosEstruturados, cotacoes) => { + if (!dadosEstruturados) return []; + + const ativosTemporarios = []; + + const calcularValor = (valorInformado, quantidade, preco) => { + if (valorInformado !== null && valorInformado !== undefined) { + return valorInformado; + } + if (quantidade !== null && quantidade !== undefined && preco !== null && preco !== undefined) { + return quantidade * preco; + } + return null; + }; + + const calcularRentabilidade = (rentabilidade, valorInvestido, valorAtual) => { + if (rentabilidade !== null && rentabilidade !== undefined) { + return rentabilidade; + } + if ( + valorInvestido !== null && + valorInvestido !== undefined && + valorAtual !== null && + valorAtual !== undefined && + valorInvestido !== 0 + ) { + return ((valorAtual - valorInvestido) / valorInvestido) * 100; + } + return null; + }; + + const adicionarAtivo = (ativo, incluirNoTotal = true) => { + ativosTemporarios.push({ + ...ativo, + incluirNoTotal + }); + }; + + const montarObservacoes = (...itens) => itens.filter(Boolean).join(' • '); + + const obterCotacao = (codigo) => { + if (!cotacoes || !codigo || typeof codigo !== 'string') return null; + const normalizado = codigo.trim().toUpperCase(); + if (!normalizado) return null; + + let valorCotacao = null; + + if (cotacoes instanceof Map) { + valorCotacao = cotacoes.get(normalizado); + } else if (typeof cotacoes === 'object') { + valorCotacao = cotacoes[normalizado]; + } + + return typeof valorCotacao === 'number' && !Number.isNaN(valorCotacao) ? valorCotacao : null; + }; + + // Ações + (dadosEstruturados.acoes || []).forEach((acao, index) => { + const quantidade = parseNumero(acao.quantidade); + const precoMedio = parseNumero(acao.precoMedio); + const precoInformado = parseNumero( + acao.cotacaoAtual ?? acao.cotacao ?? (acao.valorAtual && quantidade ? acao.valorAtual / quantidade : null) + ); + const codigoAtivo = typeof acao.codigo === 'string' ? acao.codigo : ''; + const precoCotacao = obterCotacao(codigoAtivo); + const precoAtual = precoCotacao ?? precoInformado; + const valorInvestido = calcularValor(parseNumero(acao.valorInvestido), quantidade, precoMedio); + let valorAtual = calcularValor( + precoCotacao !== null ? null : parseNumero(acao.valorAtual), + quantidade, + (precoAtual ?? precoMedio) + ); + + if (precoCotacao !== null && (valorAtual === null || valorAtual === undefined)) { + const valorInformado = parseNumero(acao.valorAtual); + if (valorInformado !== null && valorInformado !== undefined) { + valorAtual = valorInformado; + } else if (typeof quantidade === 'number' && Number.isFinite(quantidade)) { + valorAtual = quantidade * precoCotacao; + } + } + + if (valorAtual === null || valorAtual === undefined) { + const valorInformado = parseNumero(acao.valorAtual); + if (valorInformado !== null && valorInformado !== undefined) { + valorAtual = valorInformado; + } + } + + let rentabilidade = precoCotacao !== null + ? calcularRentabilidade(null, valorInvestido, valorAtual) + : calcularRentabilidade(parseNumero(acao.rentabilidade), valorInvestido, valorAtual); + + if (rentabilidade === null || Number.isNaN(rentabilidade)) { + rentabilidade = parseNumero(acao.rentabilidade); + } + const pesoInformado = parseNumero(acao.pesoCarteira); + + adicionarAtivo({ + id: `acao-${index}-${acao.codigo || 'nd'}`, + codigo: acao.codigo || '—', + nome: acao.nome || acao.codigo || 'Ação sem identificação', + categoria: 'Ações', + quantidade, + precoMedio, + precoAtual, + valorInvestido, + valorAtual, + rentabilidade, + pesoInformado, + segmento: acao.setor || acao.segmento || acao.subsetor || '', + observacoes: montarObservacoes(acao.observacoes) + }); + }); + + // FIIs + (dadosEstruturados.fiis || []).forEach((fii, index) => { + const quantidade = parseNumero(fii.quantidade); + const precoMedio = parseNumero(fii.precoMedio); + const precoInformado = parseNumero( + fii.cotacaoAtual ?? fii.cotacao ?? (fii.valorAtual && quantidade ? fii.valorAtual / quantidade : null) + ); + const codigoAtivo = typeof fii.codigo === 'string' ? fii.codigo : ''; + const precoCotacao = obterCotacao(codigoAtivo); + const precoAtual = precoCotacao ?? precoInformado; + const valorInvestido = calcularValor(parseNumero(fii.valorInvestido), quantidade, precoMedio); + let valorAtual = calcularValor( + precoCotacao !== null ? null : parseNumero(fii.valorAtual), + quantidade, + (precoAtual ?? precoMedio) + ); + + if (precoCotacao !== null && (valorAtual === null || valorAtual === undefined)) { + const valorInformado = parseNumero(fii.valorAtual); + if (valorInformado !== null && valorInformado !== undefined) { + valorAtual = valorInformado; + } else if (typeof quantidade === 'number' && Number.isFinite(quantidade)) { + valorAtual = quantidade * precoCotacao; + } + } + + if (valorAtual === null || valorAtual === undefined) { + const valorInformado = parseNumero(fii.valorAtual); + if (valorInformado !== null && valorInformado !== undefined) { + valorAtual = valorInformado; + } + } + + let rentabilidade = precoCotacao !== null + ? calcularRentabilidade(null, valorInvestido, valorAtual) + : calcularRentabilidade(parseNumero(fii.rentabilidade), valorInvestido, valorAtual); + + if (rentabilidade === null || Number.isNaN(rentabilidade)) { + rentabilidade = parseNumero(fii.rentabilidade); + } + const pesoInformado = parseNumero(fii.pesoCarteira); + + adicionarAtivo({ + id: `fii-${index}-${fii.codigo || 'nd'}`, + codigo: fii.codigo || '—', + nome: fii.nome || fii.codigo || 'FII sem identificação', + categoria: 'FIIs', + quantidade, + precoMedio, + precoAtual, + valorInvestido, + valorAtual, + rentabilidade, + pesoInformado, + segmento: fii.segmento || fii.setor || '', + observacoes: montarObservacoes(fii.observacoes) + }); + }); + + // Renda Fixa + (dadosEstruturados.rendaFixa || []).forEach((item, index) => { + const valorInvestido = parseNumero(item.valorInvestido); + const valorAtual = calcularValor(parseNumero(item.valorAtual), null, null) ?? valorInvestido; + const rentabilidade = calcularRentabilidade(parseNumero(item.rentabilidade), valorInvestido, valorAtual); + const pesoInformado = parseNumero(item.pesoCarteira); + + adicionarAtivo({ + id: `renda-fixa-${index}`, + codigo: item.produto || 'RENDA FIXA', + nome: item.banco ? `${item.banco}${item.produto ? ` • ${item.produto}` : ''}` : item.produto || 'Renda Fixa', + categoria: 'Renda Fixa', + quantidade: null, + precoMedio: null, + precoAtual: null, + valorInvestido, + valorAtual, + rentabilidade, + pesoInformado, + segmento: item.tipo || '', + observacoes: montarObservacoes(item.vencimento ? `Vencimento: ${item.vencimento}` : '', item.observacoes) + }); + }); + + // Exterior + (dadosEstruturados.exterior || []).forEach((ativo, index) => { + const quantidade = parseNumero(ativo.quantidade); + const taxaCambio = parseNumero(ativo.taxaCambio) ?? 5.5; + const precoMedioUSD = parseNumero(ativo.precoMedioUSD); + const precoAtualUSD = parseNumero(ativo.cotacaoUSD ?? ativo.cotacaoAtualUSD); + const precoMedio = precoMedioUSD !== null ? precoMedioUSD * taxaCambio : null; + const precoInformado = precoAtualUSD !== null ? precoAtualUSD * taxaCambio : null; + const codigoAtivo = typeof ativo.codigo === 'string' ? ativo.codigo : ''; + const precoCotacao = obterCotacao(codigoAtivo); + const precoAtual = precoCotacao ?? precoInformado; + const valorInvestido = calcularValor(parseNumero(ativo.valorInvestido), quantidade, precoMedio); + let valorAtual = calcularValor( + precoCotacao !== null ? null : parseNumero(ativo.valorAtual), + quantidade, + (precoAtual ?? precoMedio) + ); + + if (precoCotacao !== null && (valorAtual === null || valorAtual === undefined)) { + const valorInformado = parseNumero(ativo.valorAtual); + if (valorInformado !== null && valorInformado !== undefined) { + valorAtual = valorInformado; + } else if (typeof quantidade === 'number' && Number.isFinite(quantidade)) { + valorAtual = quantidade * precoCotacao; + } + } + + if (valorAtual === null || valorAtual === undefined) { + const valorInformado = parseNumero(ativo.valorAtual); + if (valorInformado !== null && valorInformado !== undefined) { + valorAtual = valorInformado; + } + } + + let rentabilidade = precoCotacao !== null + ? calcularRentabilidade(null, valorInvestido, valorAtual) + : calcularRentabilidade(parseNumero(ativo.rentabilidade), valorInvestido, valorAtual); + + if (rentabilidade === null || Number.isNaN(rentabilidade)) { + rentabilidade = parseNumero(ativo.rentabilidade); + } + const pesoInformado = parseNumero(ativo.pesoCarteira); + + adicionarAtivo({ + id: `exterior-${index}-${ativo.codigo || 'nd'}`, + codigo: ativo.codigo || '—', + nome: ativo.nome || ativo.codigo || 'Ativo Exterior', + categoria: 'Exterior', + quantidade, + precoMedio, + precoAtual, + valorInvestido, + valorAtual, + rentabilidade, + pesoInformado, + segmento: ativo.setor || ativo.segmento || '', + observacoes: montarObservacoes(ativo.pais, ativo.bolsa) + }); + }); + + // Cripto + (dadosEstruturados.cripto || []).forEach((cripto, index) => { + const quantidade = parseNumero(cripto.quantidade); + const precoMedio = parseNumero(cripto.precoMedio); + const valorInvestido = calcularValor(parseNumero(cripto.valorInvestido), quantidade, precoMedio); + const valorAtual = calcularValor(parseNumero(cripto.valorAtual), quantidade, parseNumero(cripto.cotacaoAtual) ?? precoMedio); + const rentabilidade = calcularRentabilidade(parseNumero(cripto.rentabilidade), valorInvestido, valorAtual); + const pesoInformado = parseNumero(cripto.pesoCarteira); + + adicionarAtivo({ + id: `cripto-${index}-${cripto.criptomoeda || 'nd'}`, + codigo: cripto.criptomoeda || 'CRIPTO', + nome: cripto.exchange ? `${cripto.exchange}${cripto.criptomoeda ? ` • ${cripto.criptomoeda}` : ''}` : cripto.criptomoeda || 'Criptoativo', + categoria: 'Cripto', + quantidade, + precoMedio, + precoAtual: null, + valorInvestido, + valorAtual, + rentabilidade, + pesoInformado, + segmento: cripto.tipo || '', + observacoes: montarObservacoes(cripto.exchange) + }); + }); + + // Reserva (não conta no peso geral) + (dadosEstruturados.reserva || []).forEach((item, index) => { + const valor = parseNumero(item.valor); + adicionarAtivo({ + id: `reserva-${index}-${item.onde || 'nd'}`, + codigo: item.onde || 'RESERVA', + nome: item.banco ? `${item.banco}${item.onde ? ` • ${item.onde}` : ''}` : item.onde || 'Reserva', + categoria: 'Reserva', + quantidade: null, + precoMedio: null, + precoAtual: null, + valorInvestido: valor, + valorAtual: valor, + rentabilidade: null, + pesoInformado: 0, + segmento: item.tipo || '', + observacoes: montarObservacoes(item.observacoes) + }, false); + }); + + const totalParaPeso = ativosTemporarios.reduce((acumulado, ativo) => { + if (!ativo.incluirNoTotal) return acumulado; + const baseValor = + typeof ativo.valorAtual === 'number' + ? ativo.valorAtual + : typeof ativo.valorInvestido === 'number' + ? ativo.valorInvestido + : 0; + return acumulado + (Number.isFinite(baseValor) ? baseValor : 0); + }, 0); + + return ativosTemporarios.map((ativo) => { + const baseValor = + typeof ativo.valorAtual === 'number' + ? ativo.valorAtual + : typeof ativo.valorInvestido === 'number' + ? ativo.valorInvestido + : 0; + + const pesoCalculado = + typeof ativo.pesoInformado === 'number' && !Number.isNaN(ativo.pesoInformado) + ? ativo.pesoInformado + : ativo.incluirNoTotal && totalParaPeso > 0 + ? (baseValor / totalParaPeso) * 100 + : null; + + const restante = { ...ativo }; + delete restante.incluirNoTotal; + delete restante.pesoInformado; + + return { + ...restante, + peso: pesoCalculado + }; + }); + }; + + const carregarPrecosBrapi = async (carteiraId, dadosEstruturados) => { + if (!dadosEstruturados) return; + + if (precosAtuais.has(carteiraId)) { + return; + } + + const statusAtual = statusCotacoes.get(carteiraId); + if (statusAtual?.loading) { + return; + } + + const tickers = coletarTickers(dadosEstruturados); + + if (!tickers.length) { + setPrecosAtuais((prev) => { + const atualizado = new Map(prev); + atualizado.set(carteiraId, {}); + return atualizado; + }); + setStatusCotacoes((prev) => { + const atualizado = new Map(prev); + atualizado.set(carteiraId, { loading: false, error: null }); + return atualizado; + }); + return; + } + + setStatusCotacoes((prev) => { + const atualizado = new Map(prev); + atualizado.set(carteiraId, { loading: true, error: null }); + return atualizado; + }); + + const url = buildBatchApiUrl(tickers); + const headers = { ...API_CONFIG.DEFAULT_HEADERS }; + const maxTentativas = Math.max(1, API_CONFIG.MAX_RETRIES || 1); + const atrasoTentativas = API_CONFIG.RETRY_DELAY || 0; + + let tentativa = 0; + let ultimoErro = null; + + while (tentativa < maxTentativas) { + try { + const response = await fetch(url, { + headers, + cache: 'no-store' + }); + + if (!response.ok) { + throw new Error(`Status ${response.status}`); + } + + const data = await response.json(); + const resultados = Array.isArray(data?.results) ? data.results : []; + const cotacoesRecebidas = resultados.reduce((acumulado, item) => { + if (!item || typeof item !== 'object') return acumulado; + + const simbolo = (item.symbol || item.ticker || item.stock || '').toString().trim().toUpperCase(); + const preco = item.regularMarketPrice; + + if (simbolo && typeof preco === 'number' && !Number.isNaN(preco)) { + acumulado[simbolo] = preco; + } + + return acumulado; + }, {}); + + setPrecosAtuais((prev) => { + const atualizado = new Map(prev); + atualizado.set(carteiraId, cotacoesRecebidas); + return atualizado; + }); + + setStatusCotacoes((prev) => { + const atualizado = new Map(prev); + atualizado.set(carteiraId, { loading: false, error: null }); + return atualizado; + }); + + return; + } catch (erro) { + ultimoErro = erro; + tentativa += 1; + + if (tentativa < maxTentativas && atrasoTentativas > 0) { + await new Promise((resolver) => setTimeout(resolver, atrasoTentativas)); + } + } + } + + setStatusCotacoes((prev) => { + const atualizado = new Map(prev); + atualizado.set(carteiraId, { + loading: false, + error: ultimoErro?.message || 'Erro ao carregar cotações' + }); + return atualizado; + }); + + console.error(`Erro ao carregar cotações da carteira ${carteiraId}:`, ultimoErro); + alert('Não foi possível atualizar as cotações agora. Exibindo os valores informados originalmente.'); + }; + + const toggleResumoAtivos = (carteiraId, dadosEstruturados) => { + const jaExpandido = expandedResumoAtivos.has(carteiraId); + + setExpandedResumoAtivos((prev) => { + const novo = new Set(prev); + if (novo.has(carteiraId)) { + novo.delete(carteiraId); + } else { + novo.add(carteiraId); + } + return novo; + }); + + if (!jaExpandido && !precosAtuais.has(carteiraId)) { + carregarPrecosBrapi(carteiraId, dadosEstruturados); + } + }; + + const renderResumoAtivosTabela = (carteiraId, dadosEstruturados) => { + const cotacoesCarteira = precosAtuais.get(carteiraId); + const statusCotacao = statusCotacoes.get(carteiraId) || { loading: false, error: null }; + const ativos = consolidarAtivosCarteira(dadosEstruturados, cotacoesCarteira); + + if (!ativos.length) { + return null; + } + + const totalValorInvestido = ativos.reduce((acumulado, ativo) => { + return acumulado + (typeof ativo.valorInvestido === 'number' && !Number.isNaN(ativo.valorInvestido) ? ativo.valorInvestido : 0); + }, 0); + + const totalValorAtual = ativos.reduce((acumulado, ativo) => { + const valor = typeof ativo.valorAtual === 'number' && !Number.isNaN(ativo.valorAtual) + ? ativo.valorAtual + : (typeof ativo.valorInvestido === 'number' && !Number.isNaN(ativo.valorInvestido) ? ativo.valorInvestido : 0); + return acumulado + valor; + }, 0); + + const totalPeso = ativos.reduce((acumulado, ativo) => { + return acumulado + (typeof ativo.peso === 'number' && !Number.isNaN(ativo.peso) ? ativo.peso : 0); + }, 0); + + const rentabilidadeTotal = totalValorInvestido > 0 + ? ((totalValorAtual - totalValorInvestido) / totalValorInvestido) * 100 + : null; + + const rentabilidadeTotalColor = + typeof rentabilidadeTotal === 'number' + ? rentabilidadeTotal > 0 + ? '#16a34a' + : rentabilidadeTotal < 0 + ? '#dc2626' + : '#64748b' + : '#64748b'; + + const totalPesoColor = + typeof totalPeso === 'number' && !Number.isNaN(totalPeso) + ? totalPeso > 0 + ? '#2563eb' + : '#64748b' + : '#64748b'; + + return ( +
+
+
+
Visão Geral dos Ativos
+
+ {ativos.length} {ativos.length === 1 ? 'ativo' : 'ativos'} consolidados • Patrimônio estimado: {formatCurrency(totalValorAtual)} +
+
+ +
+ + {statusCotacao.error && !statusCotacao.loading && ( +
+ Não foi possível atualizar todas as cotações automaticamente. Os valores originais foram mantidos. +
+ )} + + {expandedResumoAtivos.has(carteiraId) && ( +
+
+ + + + + + + + + + + + + + + + + {ativos.map((ativo, index) => { + const rentabilidadeColor = + typeof ativo.rentabilidade === 'number' + ? ativo.rentabilidade > 0 + ? '#16a34a' + : ativo.rentabilidade < 0 + ? '#dc2626' + : '#64748b' + : '#64748b'; + + const pesoColor = + typeof ativo.peso === 'number' + ? ativo.peso > 0 + ? '#2563eb' + : '#64748b' + : '#64748b'; + + return ( + + + + + + + + + + + + + ); + })} + + + + + + + + + + + + + + +
ATIVOCATEGORIAQTDEPREÇO MÉDIO + PREÇO ATUAL + {statusCotacao.loading && ( +
+ Atualizando... +
+ )} + {statusCotacao.error && !statusCotacao.loading && ( +
+ Falha ao atualizar +
+ )} +
VALOR INVESTIDOVALOR ATUALRENTAB.PESONOTAS
+
+ {ativo.codigo} + {ativo.nome && {ativo.nome}} + {ativo.segmento && {ativo.segmento}} +
+
{ativo.categoria}{formatQuantidade(ativo.quantidade)}{formatCurrency(ativo.precoMedio)} +
+ {formatCurrency(ativo.precoAtual)} + {statusCotacao.loading && index === 0 && ( + Atualizando... + )} + {statusCotacao.error && !statusCotacao.loading && index === 0 && ( + Usando dados originais + )} +
+
{formatCurrency(ativo.valorInvestido)}{formatCurrency(ativo.valorAtual)}{formatPercent(ativo.rentabilidade)}{formatPercent(ativo.peso)}{ativo.observacoes || '—'}
TOTAL CONSOLIDADO———{formatCurrency(totalValorInvestido)}{formatCurrency(totalValorAtual)}{formatPercent(rentabilidadeTotal)}{formatPercent(totalPeso)}—
+
+
+ )} +
+ ); + }; + + useEffect(() => { + carregarCarteiras(); + }, []); + + // Função para avaliar carteira automaticamente + const avaliarCarteira = (dadosEstruturados) => { + if (!dadosEstruturados) return null; + + let pontuacao = 0; + const checks = { + quantidadeAtivos: false, + concentracao: true, + reservaEmergencia: false, + temAcoes: false, + temFIIs: false, + exteriorDireto: false, + exteriorIndireto: false, + bonsFundamentos: true + }; + + let totalAtivos = 0; + let valorTotalCarteira = 0; + const alertas = []; + + // Contar ativos e calcular valor total + ['acoes', 'fiis', 'rendaFixa', 'exterior', 'cripto'].forEach(categoria => { + const itens = dadosEstruturados[categoria] || []; + totalAtivos += itens.length; + itens.forEach(item => { + valorTotalCarteira += item.valorAtual || item.valorInvestido || 0; + }); + }); + + // Check 1: Quantidade de ativos (15-30) - 2 pontos + if (totalAtivos >= 15 && totalAtivos <= 30) { + checks.quantidadeAtivos = true; + pontuacao += 2; + } else if (totalAtivos > 30) { + alertas.push(`Carteira muito pulverizada: ${totalAtivos} ativos (recomendado: 15-30)`); + } else { + alertas.push(`Poucos ativos na carteira: ${totalAtivos} (recomendado: 15-30)`); + } + + // Check 2: Concentração - verificar se algum ativo > 5% - 2 pontos + ['acoes', 'fiis', 'exterior'].forEach(categoria => { + const itens = dadosEstruturados[categoria] || []; + itens.forEach(item => { + const percentual = valorTotalCarteira > 0 ? (item.valorAtual || item.valorInvestido || 0) / valorTotalCarteira * 100 : 0; + if (percentual > 5) { + checks.concentracao = false; + alertas.push(`Concentração elevada em ${item.codigo || item.nome}: ${percentual.toFixed(1)}%`); + } + }); + }); + if (checks.concentracao) pontuacao += 2; + + // Check 3: Reserva de emergência - 1 ponto + if (dadosEstruturados.reserva && dadosEstruturados.reserva.length > 0) { + checks.reservaEmergencia = true; + pontuacao += 1; + } + + // Check 4: Investe em ações - 1 ponto + if (dadosEstruturados.acoes && dadosEstruturados.acoes.length > 0) { + checks.temAcoes = true; + pontuacao += 1; + } + + // Check 5: Investe em FIIs - 1 ponto + if (dadosEstruturados.fiis && dadosEstruturados.fiis.length > 0) { + checks.temFIIs = true; + pontuacao += 1; + } + + // Check 6: Investe diretamente no exterior - 2 pontos + if (dadosEstruturados.exterior && dadosEstruturados.exterior.length > 0) { + checks.exteriorDireto = true; + pontuacao += 2; + } else { + alertas.push('Sem exposição internacional direta - considere diversificar geograficamente'); + } + + // Determinar perfil baseado na pontuação + let perfil = 'CONSERVADOR'; + let avaliacaoGeral = 'REGULAR'; + + if (pontuacao >= 8) { + perfil = 'MODERADO'; + avaliacaoGeral = 'MUITO BOM'; + } else if (pontuacao >= 6) { + perfil = 'ARROJADO'; + avaliacaoGeral = 'BOM'; + } + + return { + pontuacao, + pontuacaoMaxima: 10, + checks, + perfil, + avaliacaoGeral, + alertas, + totalAtivos, + valorTotalCarteira, + metricas: { + qualidade: Math.min(100, (pontuacao / 10) * 100), + diversificacao: Math.min(100, (totalAtivos / 25) * 100), + adaptacao: Math.min(100, checks.concentracao ? 85 : 60) + } + }; + }; + + // Componente Gauge Chart + const GaugeChart = ({ label, value, color = '#10b981' }) => { + const circumference = 2 * Math.PI * 45; + const strokeDasharray = `${(value / 100) * circumference} ${circumference}`; + + return ( +
+
+ + + + +
+ {Math.round(value)}% +
+
+
+ {label} +
+
+ ); + }; + + // Modal de Análise Detalhada + const DetailedAnalysisModal = ({ carteira, analise, onClose }) => ( +
+
+ {/* Header do Modal */} +
+
+

+ Análise Detalhada da Carteira +

+

+ {carteira.nomeArquivo} • {carteira.cliente?.name || 'Cliente não informado'} +

+
+ +
+ +
+ {/* Avaliação Geral */} +
+
+ ANÃLISE DE CARTEIRA +
+
+ AVALIAÇÃO: {analise.avaliacaoGeral} +
+
+ Pontuação: {analise.pontuacao}/{analise.pontuacaoMaxima} pontos +
+
+ + {/* Métricas Visuais */} +
+ + + +
+ + {/* Checklist */} +
+

+ CHECK-LIST DA CARTEIRA: +

+ +
+ {[ + { key: 'quantidadeAtivos', label: 'POSSUI ENTRE 15-30 ATIVOS (PODE SER MAIS)', pontos: 2 }, + { key: 'concentracao', label: 'NENHUM ATIVO COM CONCENTRAÇÃO ELEVADA', pontos: 2 }, + { key: 'reservaEmergencia', label: 'POSSUI RESERVA DE EMERGÊNCIA/CAIXA/RENDA FIXA', pontos: 1 }, + { key: 'temAcoes', label: 'INVESTE EM AÇÕES', pontos: 1 }, + { key: 'temFIIs', label: 'INVESTE EM FUNDOS IMOBILIÃRIOS', pontos: 1 }, + { key: 'exteriorDireto', label: 'INVESTE DIRETAMENTE NO EXTERIOR', pontos: 2 }, + { key: 'exteriorIndireto', label: 'INVESTE INDIRETAMENTE NO EXTERIOR', pontos: 1 }, + { key: 'bonsFundamentos', label: 'MAIORIA DOS ATIVOS TEM BONS FUNDAMENTOS', pontos: 1 } + ].map(item => ( +
+ + {analise.checks[item.key] ? '✓' : '✗'} + + + ({item.pontos} PONTOS) + + + {item.label} + +
+ ))} +
+ +
+
+ SEU SOMATÓRIO FOI DE {analise.pontuacao} PONTOS. +
+
+ SEU PERFIL É: {analise.perfil} +
+
+
+ + {/* Alertas */} + {analise.alertas.length > 0 && ( +
+

+ Pontos de Atenção: +

+
    + {analise.alertas.map((alerta, index) => ( +
  • + {alerta} +
  • + ))} +
+
+ )} +
+
+
+ ); + + const carregarCarteiras = async () => { + try { + setLoading(true); + const response = await fetch('/api/admin/carteiras'); + const data = await response.json(); + + if (response.ok) { + setCarteiras(data.carteiras || []); + setStats(data.estatisticas || stats); + } else { + setError(data.error || 'Erro ao carregar carteiras'); + } + } catch (err) { + setError('Erro de conexão'); + console.error('Erro:', err); + } finally { + setLoading(false); + } + }; + + const matchesStatus = (status, statuses) => statuses.includes((status ?? '').toLowerCase()); + + const carteirasPendentes = carteiras.filter(c => + matchesStatus(c.status, ['pendente', 'nova', 'em_analise', 'processing', 'pending']) + ).sort((a, b) => { + const diasA = Math.floor((new Date() - new Date(a.dataEnvio)) / (1000 * 60 * 60 * 24)); + const diasB = Math.floor((new Date() - new Date(b.dataEnvio)) / (1000 * 60 * 60 * 24)); + return diasB - diasA; + }); + + const carteirasAnalisadas = carteiras.filter(c => + matchesStatus(c.status, ['analisada', 'fechada', 'completed']) + ).sort((a, b) => { + const dataA = a.dataAnalise || a.updatedAt || a.dataEnvio; + const dataB = b.dataAnalise || b.updatedAt || b.dataEnvio; + return new Date(dataB) - new Date(dataA); + }); + + const toggleExpandedAnalysed = (carteiraId) => { + const newExpanded = new Set(expandedAnalysed); + if (newExpanded.has(carteiraId)) { + newExpanded.delete(carteiraId); + } else { + newExpanded.add(carteiraId); + } + setExpandedAnalysed(newExpanded); + }; + + const handleDeleteCarteira = async (carteiraId, clienteNome) => { + const clienteEmail = carteiras.find(c => c.id === carteiraId)?.cliente?.email || + carteiras.find(c => c.id === carteiraId)?.user?.email || + 'Email não encontrado'; + + if (!confirm(`Tem certeza que deseja DELETAR a carteira de ${clienteNome}?\n\nEsta ação permitirá que o cliente envie uma nova carteira.`)) { + return; + } + + try { + const response = await fetch(`/api/carteiras/${carteiraId}`, { + method: 'DELETE' + }); + + if (response.ok) { + const result = await response.json(); + alert(`Sucesso: ${result.message}\n\nO cliente ${clienteEmail} agora pode enviar uma nova carteira.`); + carregarCarteiras(); + } else { + const error = await response.json(); + alert(`Erro: ${error.error}`); + } + } catch (error) { + alert('Erro ao remover carteira'); + console.error(error); + } + }; + + const salvarAnalise = async (dadosAnalise) => { + try { + const response = await fetch('/api/admin/carteiras', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: dadosAnalise.id, + feedback: dadosAnalise.feedback, + pontuacao: dadosAnalise.pontuacao, + recomendacoes: dadosAnalise.recomendacoes, + status: dadosAnalise.status, + avaliacaoQualidade: dadosAnalise.avaliacaoQualidade, + avaliacaoDiversificacao: dadosAnalise.avaliacaoDiversificacao, + avaliacaoAdaptacao: dadosAnalise.avaliacaoAdaptacao, + dadosEstruturados: dadosAnalise.dadosEstruturados, + dataAnalise: dadosAnalise.dataAnalise + }) + }); + + if (response.ok) { + alert('Análise salva com sucesso!'); + setEditando(null); + setShowAdvancedFeedback(false); + carregarCarteiras(); + } else { + const data = await response.json(); + alert('Erro: ' + data.error); + } + } catch (err) { + alert('Erro ao salvar análise'); + console.error(err); + } + }; + + const salvarAnaliseSimples = async (carteiraId) => { + try { + const response = await fetch('/api/admin/carteiras', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: carteiraId, + feedback, + pontuacao: parseFloat(pontuacao), + recomendacoes: recomendacoes.split('\n').filter(r => r.trim()), + status: 'ANALISADA' + }) + }); + + if (response.ok) { + alert('Análise salva com sucesso!'); + setEditando(null); + setFeedback(''); + setPontuacao(''); + setRecomendacoes(''); + carregarCarteiras(); + } else { + const data = await response.json(); + alert('Erro: ' + data.error); + } + } catch (err) { + alert('Erro ao salvar análise'); + console.error(err); + } + }; + + const iniciarEdicao = (carteira) => { + setEditando(carteira.id); + setFeedback(carteira.feedback || ''); + setPontuacao(carteira.pontuacao || ''); + setRecomendacoes(carteira.recomendacoes ? carteira.recomendacoes.join('\n') : ''); + }; + + const getStatusColor = (status) => { + switch (status?.toLowerCase()) { + case 'completed': + case 'analisada': + return '#10b981'; + case 'processing': + case 'em_analise': + case 'em análise': + return '#f59e0b'; + case 'pending': + case 'pendente': + case 'nova': + return '#64748b'; + case 'error': + case 'erro': + return '#ef4444'; + case 'cancelled': + case 'cancelada': + case 'fechada': + return '#6b7280'; + default: + return '#64748b'; + } + }; + + const getStatusText = (status) => { + switch (status?.toLowerCase()) { + case 'completed': + case 'analisada': + return 'Analisada'; + case 'processing': + case 'em_analise': + case 'em análise': + return 'Em Análise'; + case 'pending': + case 'pendente': + case 'nova': + return 'Pendente'; + case 'error': + case 'erro': + return 'Erro'; + case 'cancelled': + case 'cancelada': + case 'fechada': + return 'Fechada'; + default: + return 'Pendente'; + } + }; + + const getStatusIcon = (status) => { + switch (status?.toLowerCase()) { + case 'completed': + case 'analisada': + return '✅'; + case 'processing': + case 'em_analise': + case 'em análise': + return 'â³'; + case 'pending': + case 'pendente': + case 'nova': + return '📋'; + case 'error': + case 'erro': + return 'âŒ'; + case 'cancelled': + case 'cancelada': + case 'fechada': + return '🚫'; + default: + return '📋'; + } + }; + + const formatDate = (dateString) => { + return new Date(dateString).toLocaleDateString('pt-BR', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + }; + + const getPrazoInfo = (dataEnvio) => { + const diasDesdeEnvio = Math.floor((new Date() - new Date(dataEnvio)) / (1000 * 60 * 60 * 24)); + const diasRestantes = 30 - diasDesdeEnvio; + + let corPrazo = '#10b981'; + let iconePrazo = '✅'; + let textoPrazo = `${diasDesdeEnvio} dias atrás`; + + if (diasRestantes <= 0) { + corPrazo = '#dc2626'; + iconePrazo = '🚨'; + textoPrazo = `ATRASADO (${Math.abs(diasRestantes)} dias)`; + } else if (diasRestantes <= 5) { + corPrazo = '#ea580c'; + iconePrazo = 'âš ï¸'; + textoPrazo = `URGENTE (${diasRestantes} dias restantes)`; + } else if (diasRestantes <= 10) { + corPrazo = '#f59e0b'; + iconePrazo = 'â°'; + textoPrazo = `${diasRestantes} dias restantes`; + } + + return { corPrazo, iconePrazo, textoPrazo }; + }; + + if (loading) { + return ( +
+
+
Carregando painel administrativo...
+
+ ); + } + + if (error) { + return ( +
+
âŒ
+

Erro de Acesso

+

{error}

+ +
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+

+ Painel Administrativo +

+

+ Sistema inteligente de análise de carteiras com avaliação automática +

+
+ +
+ + Gerenciar Ativos + + + +
+
+
+ + {/* Stats Cards */} +
+
+
+ {stats.total} +
+
Total de Carteiras
+
+ +
+
+ {carteirasPendentes.length} +
+
Pendentes
+
+ +
+
+ {carteirasAnalisadas.length} +
+
Analisadas
+
+ +
+
+ {carteirasPendentes.filter(c => { + const diasDesdeEnvio = Math.floor((new Date() - new Date(c.dataEnvio)) / (1000 * 60 * 60 * 24)); + return diasDesdeEnvio > 30; + }).length} +
+
Atrasadas
+
+
+ + {/* Tabs */} +
+
+
+ + +
+
+ + {/* Conteúdo das Abas */} +
+ {/* ABA PENDENTES */} + {currentTab === 0 && ( + <> + {carteirasPendentes.length === 0 ? ( +
+
🎉
+

+ Todas as carteiras foram analisadas! +

+

+ Não há carteiras pendentes no momento +

+
+ ) : ( +
+ {carteirasPendentes.map((carteira) => { + // Processar dados estruturados + let dadosEstruturados = null; + let analise = null; + + try { + dadosEstruturados = carteira.dadosEstruturados && typeof carteira.dadosEstruturados === 'string' + ? JSON.parse(carteira.dadosEstruturados) + : carteira.dadosEstruturados; + + if (dadosEstruturados) { + analise = avaliarCarteira(dadosEstruturados); + } + } catch (error) { + console.error('Erro ao processar dados estruturados:', error); + } + + return ( +
+ {/* Header da Carteira Pendente */} +
+
+
+
+

+ {carteira.nomeArquivo} +

+ + {analise && ( + <> +
+ {analise.avaliacaoGeral} +
+ +
+ {analise.pontuacao}/{analise.pontuacaoMaxima} +
+ + + + )} +
+ +
+ + {carteira.cliente?.name || + (carteira.user ? `${carteira.user.firstName || ''} ${carteira.user.lastName || ''}`.trim() : '') || + 'Cliente não informado'} + + {(carteira.cliente?.email || carteira.user?.email) && ( + + ({carteira.cliente?.email || carteira.user?.email}) + + )} +
+ +
+ Enviado em: {formatDate(carteira.dataEnvio)} +
+ + {/* Indicador de prazo */} + {(() => { + const { corPrazo, iconePrazo, textoPrazo } = getPrazoInfo(carteira.dataEnvio); + return ( +
+ {iconePrazo} + {textoPrazo} + + (prazo: 30 dias) + +
+ ); + })()} +
+ +
+ {getStatusIcon(carteira.status)} + {getStatusText(carteira.status)} +
+
+ + {/* Mini Dashboard se tiver análise */} + {analise && ( +
+
+
+
+ {Math.round(analise.metricas.qualidade)}% +
+
Qualidade
+
+
+
+ {Math.round(analise.metricas.diversificacao)}% +
+
Diversificação
+
+
+
+ {Math.round(analise.metricas.adaptacao)}% +
+
Adaptação
+
+
+ + {/* Alertas principais */} + {analise.alertas.length > 0 && ( +
+
+ Principais Alertas: +
+
+ {analise.alertas.slice(0, 2).map((alerta, idx) => ( +
• {alerta}
+ ))} + {analise.alertas.length > 2 && ( +
• E mais {analise.alertas.length - 2} alertas...
+ )} +
+
+ )} +
+ )} + + {/* Dados da Carteira */} + {dadosEstruturados ? ( +
+ {(() => { + const categorias = [ + { key: 'acoes', nome: 'Ações', emoji: '📈', cor: '#10b981' }, + { key: 'fiis', nome: 'FIIs', emoji: 'ðŸ˜ï¸', cor: '#3b82f6' }, + { key: 'rendaFixa', nome: 'Renda Fixa', emoji: '💰', cor: '#8b5cf6' }, + { key: 'exterior', nome: 'Exterior', emoji: '🌎', cor: '#f59e0b' }, + { key: 'cripto', nome: 'Cripto', emoji: 'â‚¿', cor: '#ef4444' }, + { key: 'reserva', nome: 'Reserva', emoji: 'ðŸ¦', cor: '#6b7280' } + ]; + + const categoriasComDados = categorias.filter(cat => + dadosEstruturados[cat.key] && dadosEstruturados[cat.key].length > 0 + ); + + let valorTotalCalculado = 0; + categoriasComDados.forEach(cat => { + const itens = dadosEstruturados[cat.key] || []; + itens.forEach(item => { + if (cat.key === 'reserva') return; + valorTotalCalculado += item.valorAtual || item.valorInvestido || item.valor || 0; + }); + }); + + return ( +
+
+
+ VALOR TOTAL DA CARTEIRA +
+
+ R$ {valorTotalCalculado.toLocaleString('pt-BR', { minimumFractionDigits: 2 })} +
+
+ {analise?.totalAtivos || categoriasComDados.reduce((total, cat) => total + (dadosEstruturados[cat.key]?.length || 0), 0)} ativos em {categoriasComDados.length} categorias +
+
+ +
+ {categoriasComDados.map(categoria => { + const itens = dadosEstruturados[categoria.key] || []; + let valorCategoria = 0; + let quantidadeAtivos = itens.length; + + itens.forEach(item => { + if (categoria.key === 'reserva') { + valorCategoria += item.valor || 0; + } else { + valorCategoria += item.valorAtual || item.valorInvestido || item.valor || 0; + } + }); + + const percentual = categoria.key === 'reserva' + ? 0 + : valorTotalCalculado > 0 ? (valorCategoria / valorTotalCalculado) * 100 : 0; + + return ( +
+
+ {categoria.emoji} +
+ {categoria.nome} +
+
+ +
+ {categoria.key === 'exterior' ? + `US$ ${(valorCategoria / 5.5).toLocaleString('en-US', { minimumFractionDigits: 2 })}` : + `R$ ${valorCategoria.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}` + } +
+ +
+ {quantidadeAtivos} {quantidadeAtivos === 1 ? 'ativo' : 'ativos'} + {categoria.key !== 'reserva' && ( + + {percentual.toFixed(1)}% + + )} +
+
+ ); + })} +
+
+ ); + })()} + + {renderResumoAtivosTabela(carteira.id, dadosEstruturados)} +
+ ) : ( + // Fallback para carteiras antigas +
+
+
+ VALOR TOTAL +
+
+ {carteira.valorTotal ? + `R$ ${carteira.valorTotal.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}` + : 'N/A' + } +
+
+ +
+
+ ATIVOS +
+
+ {carteira.quantidadeAtivos || 'N/A'} +
+
+
+ )} + + {/* Questionário se existir */} + {carteira.questionario && ( +
+

+ Questionário do Cliente +

+ +
+ {(() => { + try { + const questionarioData = typeof carteira.questionario === 'string' + ? JSON.parse(carteira.questionario) + : carteira.questionario; + + return Object.entries(questionarioData).slice(0, 4).map(([key, value], index) => ( +
+
+ {key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}: +
+
+ {typeof value === 'string' && value.length > 80 ? + value.substring(0, 80) + '...' : + String(value) + } +
+
+ )); + } catch (error) { + return ( +
+ Erro ao carregar questionário +
+ ); + } + })()} +
+
+ )} + + {/* Botões de Análise */} + {editando === carteira.id && showAdvancedFeedback ? ( + { + setEditando(null); + setShowAdvancedFeedback(false); + }} + /> + ) : editando === carteira.id ? ( +
+

+ Análise Simples +

+ +
+ + setPontuacao(e.target.value)} + style={{ + width: '120px', + padding: '10px', + border: '1px solid #d1d5db', + borderRadius: '6px', + fontSize: '14px' + }} + /> +
+ +
+ +