Adicionada compressão Brotli/Gzip, auto-import de Vue e PrimeVue, e análise visual do bundle para otimização de produção e Remove AppLayout duplicado de cada área (therapist, admin, configuracoes, account, supervisor, billing, features) e consolida sob um único pai no router/index.js. Adiciona RouterPassthrough para grupos de rota sem layout intermediário. Remove debug ativo (console.trace em router.push e queries Supabase em todo watch de rota) que degradava performance para todos os usuários.
This commit is contained in:
194
docs/architecture/router-shared-applayout.md
Normal file
194
docs/architecture/router-shared-applayout.md
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
# Router — AppLayout Compartilhado
|
||||||
|
|
||||||
|
**Data:** 2026-03-25
|
||||||
|
**Arquivo:** `src/router/index.js` e arquivos de rota relacionados
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problema
|
||||||
|
|
||||||
|
A aplicação apresentava comportamento de "reload" ao navegar entre seções distintas (ex: de `/configuracoes/agenda` para `/therapist/agenda/recorrencias`). O efeito visual era a sidebar e a topbar piscando e sendo completamente reconstruídas — mesmo sendo o mesmo layout.
|
||||||
|
|
||||||
|
Além disso, ao clicar rapidamente para trocar de página, o Vue recarregava o componente de layout inteiro, causando lentidão perceptível.
|
||||||
|
|
||||||
|
### Causa raiz
|
||||||
|
|
||||||
|
Cada área da aplicação tinha seu **próprio registro de rota com `component: AppLayout`**:
|
||||||
|
|
||||||
|
```
|
||||||
|
/therapist → { component: AppLayout } ← instância A
|
||||||
|
/admin → { component: AppLayout } ← instância B
|
||||||
|
/configuracoes→ { component: AppLayout } ← instância C
|
||||||
|
/account → { component: AppLayout } ← instância D
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
O Vue Router trata registros de rota distintos como componentes distintos. Ao navegar de `/configuracoes` para `/therapist`, o Vue **desmontava** a instância A e **montava** a instância B do zero — sidebar, topbar, watchers, stores, tudo reiniciava. Isso é o comportamento correto do Vue Router, mas estava sendo usado de forma incorreta para uma aplicação que tem um layout único.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solução
|
||||||
|
|
||||||
|
Consolidar todas as áreas autenticadas sob **um único registro de rota pai** com `component: AppLayout`. O Vue Router reutiliza a instância do componente pai enquanto navega entre os filhos.
|
||||||
|
|
||||||
|
### Arquitetura após a mudança
|
||||||
|
|
||||||
|
```
|
||||||
|
/ → AppLayout (instância única — nunca desmontada)
|
||||||
|
├── therapist/ → RouterPassthrough → página
|
||||||
|
├── admin/ → RouterPassthrough → página
|
||||||
|
├── configuracoes/ → ConfiguracoesPage → sub-página
|
||||||
|
├── account/ → RouterPassthrough → página
|
||||||
|
├── upgrade/ → RouterPassthrough → página
|
||||||
|
├── supervisor/ → RouterPassthrough → página
|
||||||
|
└── features/ → RouterPassthrough → página
|
||||||
|
```
|
||||||
|
|
||||||
|
**Antes:** AppLayout era desmontado/remontado ao trocar de seção.
|
||||||
|
**Depois:** Apenas o conteúdo (área central) é trocado. AppLayout nunca desmonta.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Componente RouterPassthrough
|
||||||
|
|
||||||
|
Criado em `src/layout/RouterPassthrough.vue`:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template><router-view /></template>
|
||||||
|
```
|
||||||
|
|
||||||
|
Serve como "rota de grupo com componente" para as áreas que não têm layout intermediário próprio (therapist, admin, account etc.). Ele é necessário porque o Vue Router precisa de um componente em cada nível da hierarquia para renderizar os filhos via `<router-view>`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Arquivos alterados
|
||||||
|
|
||||||
|
### `src/router/index.js`
|
||||||
|
|
||||||
|
Antes: importava cada rota e espalhava todas no nível raiz do array de rotas.
|
||||||
|
Depois: cria um único pai `{ path: '/', component: AppLayout }` e coloca todas as rotas de app como filhas.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const routes = [
|
||||||
|
// Rotas sem layout
|
||||||
|
...publicRoutes,
|
||||||
|
...authRoutes,
|
||||||
|
|
||||||
|
// Setup wizards (fullscreen, fora do AppLayout)
|
||||||
|
...therapistStandalone,
|
||||||
|
...clinicStandalone,
|
||||||
|
|
||||||
|
// ─── AppLayout compartilhado ───────────────────────────
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: AppLayout,
|
||||||
|
children: [
|
||||||
|
therapistRoutes, // path: 'therapist'
|
||||||
|
adminRoutes, // path: 'admin'
|
||||||
|
accountRoutes, // path: 'account'
|
||||||
|
billingRoutes, // path: 'upgrade'
|
||||||
|
supervisorRoutes, // path: 'supervisor'
|
||||||
|
featuresRoutes, // path: 'features'
|
||||||
|
configuracoesRoutes, // path: 'configuracoes'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// AppLayout próprio (usuários completamente distintos)
|
||||||
|
saasRoutes,
|
||||||
|
editorRoutes,
|
||||||
|
portalRoutes,
|
||||||
|
|
||||||
|
// Catch-all (sempre por último)
|
||||||
|
...miscRoutes,
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### `src/router/routes.therapist.js` e `routes.clinic.js`
|
||||||
|
|
||||||
|
Setup wizards (tela cheia, sem layout) foram separados como `export const therapistStandalone` / `clinicStandalone` e continuam com paths absolutos (`/therapist/setup`, `/admin/setup`).
|
||||||
|
|
||||||
|
A rota principal passou de:
|
||||||
|
```js
|
||||||
|
{ path: '/therapist', component: AppLayout, children: [...] }
|
||||||
|
```
|
||||||
|
Para:
|
||||||
|
```js
|
||||||
|
{ path: 'therapist', component: RouterPassthrough, meta: {...}, children: [...] }
|
||||||
|
```
|
||||||
|
|
||||||
|
### `src/router/routes.configs.js`
|
||||||
|
|
||||||
|
Removido o double-nesting desnecessário. Antes havia:
|
||||||
|
```js
|
||||||
|
{ path: '/configuracoes', component: AppLayout,
|
||||||
|
children: [
|
||||||
|
{ path: '', component: ConfiguracoesPage, children: [...] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Depois, `ConfiguracoesPage` (que já tem `<router-view>` próprio) assume diretamente:
|
||||||
|
```js
|
||||||
|
{ path: 'configuracoes', component: ConfiguracoesPage,
|
||||||
|
redirect: { name: 'ConfiguracoesAgenda' },
|
||||||
|
meta: { requiresAuth: true, roles: [...] },
|
||||||
|
children: [...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `src/router/guards.js`
|
||||||
|
|
||||||
|
`/configuracoes` adicionado ao `isTenantArea` para garantir que o tenant e entitlements são carregados corretamente ao acessar a URL diretamente:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const isTenantArea =
|
||||||
|
to.path.startsWith('/admin') ||
|
||||||
|
to.path.startsWith('/therapist') ||
|
||||||
|
to.path.startsWith('/supervisor') ||
|
||||||
|
to.path.startsWith('/configuracoes');
|
||||||
|
```
|
||||||
|
|
||||||
|
### `src/App.vue`
|
||||||
|
|
||||||
|
Mesma correção no `isTenantArea` local, que controla o `checkSetupWizard`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## O que foi mantido separado (e por quê)
|
||||||
|
|
||||||
|
| Área | Motivo para manter AppLayout próprio |
|
||||||
|
|---|---|
|
||||||
|
| `/saas` | Usado exclusivamente por `saas_admin`. Nunca há navegação cruzada com áreas de tenant. |
|
||||||
|
| `/editor` | Role de plataforma distinto. Área separada. |
|
||||||
|
| `/portal` | Sessão de paciente. Usuário completamente diferente. |
|
||||||
|
|
||||||
|
Mesclar essas áreas no AppLayout compartilhado não traria ganho algum e criaria acoplamento desnecessário entre contextos de usuário distintos.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debug removido na mesma sessão
|
||||||
|
|
||||||
|
Foram removidos resquícios de código de debug que estavam ativos em produção e impactavam **todos** os usuários:
|
||||||
|
|
||||||
|
- **`router/index.js`**: `console.trace()` injetado em `router.push` e `router.replace` a cada clique de navegação (gerava stack trace completo).
|
||||||
|
- **`App.vue`**: `debugSnapshot()` disparava 3 queries Supabase (`auth.getUser`, `profiles`, `rpc.my_tenants`) a **cada troca de rota**, causando o principal sintoma de lentidão.
|
||||||
|
|
||||||
|
O sistema de suporte técnico (`supportGuard` + `supportDebugStore`) **não foi afetado** — é uma feature controlada que só ativa via `?support=TOKEN` validado no banco.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Meta inheritance
|
||||||
|
|
||||||
|
O Vue Router 4 mescla automaticamente o `meta` de todos os registros de rota correspondidos (`to.matched`). Isso significa que os filhos do `RouterPassthrough` herdam o `meta` do pai sem nenhuma configuração adicional.
|
||||||
|
|
||||||
|
Exemplo: ao navegar para `/therapist/agenda`, `to.meta` conterá:
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
area: 'therapist', // herdado do pai 'therapist'
|
||||||
|
requiresAuth: true, // herdado do pai 'therapist'
|
||||||
|
roles: ['therapist'], // herdado do pai 'therapist'
|
||||||
|
feature: 'agenda.view' // da própria rota 'agenda'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Os guards continuam funcionando sem nenhuma mudança de lógica.
|
||||||
375
package-lock.json
generated
375
package-lock.json
generated
@@ -45,9 +45,10 @@
|
|||||||
"rollup-plugin-visualizer": "^7.0.1",
|
"rollup-plugin-visualizer": "^7.0.1",
|
||||||
"sass": "^1.55.0",
|
"sass": "^1.55.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"unplugin-auto-import": "^21.0.0",
|
"unplugin-auto-import": "^0.18.6",
|
||||||
"unplugin-vue-components": "^0.27.3",
|
"unplugin-vue-components": "^0.27.3",
|
||||||
"vite": "^5.3.1",
|
"vite": "^5.3.1",
|
||||||
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vitest": "^4.0.18"
|
"vitest": "^4.0.18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3189,6 +3190,20 @@
|
|||||||
"url": "https://github.com/sponsors/rawify"
|
"url": "https://github.com/sponsors/rawify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fs-extra": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.0",
|
||||||
|
"jsonfile": "^6.0.1",
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fs.realpath": {
|
"node_modules/fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
@@ -3547,6 +3562,18 @@
|
|||||||
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/jsonfile": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"graceful-fs": "^4.1.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
@@ -4764,9 +4791,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/strip-literal": {
|
"node_modules/strip-literal": {
|
||||||
"version": "3.1.0",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz",
|
||||||
"integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
|
"integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"js-tokens": "^9.0.1"
|
"js-tokens": "^9.0.1"
|
||||||
@@ -4947,28 +4974,25 @@
|
|||||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="
|
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="
|
||||||
},
|
},
|
||||||
"node_modules/unimport": {
|
"node_modules/unimport": {
|
||||||
"version": "5.7.0",
|
"version": "3.14.6",
|
||||||
"resolved": "https://registry.npmjs.org/unimport/-/unimport-5.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/unimport/-/unimport-3.14.6.tgz",
|
||||||
"integrity": "sha512-njnL6sp8lEA8QQbZrt+52p/g4X0rw3bnGGmUcJnt1jeG8+iiqO779aGz0PirCtydAIVcuTBRlJ52F0u46z309Q==",
|
"integrity": "sha512-CYvbDaTT04Rh8bmD8jz3WPmHYZRG/NnvYVzwD6V1YAlvvKROlAeNDUBhkBGzNav2RKaeuXvlWYaa1V4Lfi/O0g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"acorn": "^8.16.0",
|
"@rollup/pluginutils": "^5.1.4",
|
||||||
|
"acorn": "^8.14.0",
|
||||||
"escape-string-regexp": "^5.0.0",
|
"escape-string-regexp": "^5.0.0",
|
||||||
"estree-walker": "^3.0.3",
|
"estree-walker": "^3.0.3",
|
||||||
"local-pkg": "^1.1.2",
|
"fast-glob": "^3.3.3",
|
||||||
"magic-string": "^0.30.21",
|
"local-pkg": "^1.0.0",
|
||||||
"mlly": "^1.8.0",
|
"magic-string": "^0.30.17",
|
||||||
"pathe": "^2.0.3",
|
"mlly": "^1.7.4",
|
||||||
"picomatch": "^4.0.3",
|
"pathe": "^2.0.1",
|
||||||
"pkg-types": "^2.3.0",
|
"picomatch": "^4.0.2",
|
||||||
|
"pkg-types": "^1.3.0",
|
||||||
"scule": "^1.3.0",
|
"scule": "^1.3.0",
|
||||||
"strip-literal": "^3.1.0",
|
"strip-literal": "^2.1.1",
|
||||||
"tinyglobby": "^0.2.15",
|
"unplugin": "^1.16.1"
|
||||||
"unplugin": "^2.3.11",
|
|
||||||
"unplugin-utils": "^0.3.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.12.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/unimport/node_modules/confbox": {
|
"node_modules/unimport/node_modules/confbox": {
|
||||||
@@ -5015,7 +5039,7 @@
|
|||||||
"url": "https://github.com/sponsors/antfu"
|
"url": "https://github.com/sponsors/antfu"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/unimport/node_modules/pkg-types": {
|
"node_modules/unimport/node_modules/local-pkg/node_modules/pkg-types": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
||||||
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
|
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
|
||||||
@@ -5026,19 +5050,13 @@
|
|||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/unimport/node_modules/unplugin": {
|
"node_modules/universalify": {
|
||||||
"version": "2.3.11",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
"integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==",
|
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/remapping": "^2.3.5",
|
|
||||||
"acorn": "^8.15.0",
|
|
||||||
"picomatch": "^4.0.3",
|
|
||||||
"webpack-virtual-modules": "^0.6.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.12.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/unplugin": {
|
"node_modules/unplugin": {
|
||||||
@@ -5055,26 +5073,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/unplugin-auto-import": {
|
"node_modules/unplugin-auto-import": {
|
||||||
"version": "21.0.0",
|
"version": "0.18.6",
|
||||||
"resolved": "https://registry.npmjs.org/unplugin-auto-import/-/unplugin-auto-import-21.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unplugin-auto-import/-/unplugin-auto-import-0.18.6.tgz",
|
||||||
"integrity": "sha512-vWuC8SwqJmxZFYwPojhOhOXDb5xFhNNcEVb9K/RFkyk/3VnfaOjzitWN7v+8DEKpMjSsY2AEGXNgt6I0yQrhRQ==",
|
"integrity": "sha512-LMFzX5DtkTj/3wZuyG5bgKBoJ7WSgzqSGJ8ppDRdlvPh45mx6t6w3OcbExQi53n3xF5MYkNGPNR/HYOL95KL2A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"local-pkg": "^1.1.2",
|
"@antfu/utils": "^0.7.10",
|
||||||
"magic-string": "^0.30.21",
|
"@rollup/pluginutils": "^5.1.3",
|
||||||
"picomatch": "^4.0.3",
|
"fast-glob": "^3.3.2",
|
||||||
"unimport": "^5.6.0",
|
"local-pkg": "^0.5.1",
|
||||||
"unplugin": "^2.3.11",
|
"magic-string": "^0.30.14",
|
||||||
"unplugin-utils": "^0.3.1"
|
"minimatch": "^9.0.5",
|
||||||
|
"unimport": "^3.13.4",
|
||||||
|
"unplugin": "^1.16.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.19.0"
|
"node": ">=14"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/antfu"
|
"url": "https://github.com/sponsors/antfu"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@nuxt/kit": "^4.0.0",
|
"@nuxt/kit": "^3.2.2",
|
||||||
"@vueuse/core": "*"
|
"@vueuse/core": "*"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
@@ -5086,69 +5106,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/unplugin-auto-import/node_modules/confbox": {
|
"node_modules/unplugin-auto-import/node_modules/brace-expansion": {
|
||||||
"version": "0.2.4",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/unplugin-auto-import/node_modules/local-pkg": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==",
|
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mlly": "^1.7.4",
|
"balanced-match": "^1.0.0"
|
||||||
"pkg-types": "^2.3.0",
|
}
|
||||||
"quansync": "^0.2.11"
|
},
|
||||||
|
"node_modules/unplugin-auto-import/node_modules/minimatch": {
|
||||||
|
"version": "9.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||||
|
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^2.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=16 || 14 >=14.17"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/antfu"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/unplugin-auto-import/node_modules/pkg-types": {
|
|
||||||
"version": "2.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
|
||||||
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"confbox": "^0.2.2",
|
|
||||||
"exsolve": "^1.0.7",
|
|
||||||
"pathe": "^2.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/unplugin-auto-import/node_modules/unplugin": {
|
|
||||||
"version": "2.3.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
|
|
||||||
"integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/remapping": "^2.3.5",
|
|
||||||
"acorn": "^8.15.0",
|
|
||||||
"picomatch": "^4.0.3",
|
|
||||||
"webpack-virtual-modules": "^0.6.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/unplugin-utils": {
|
|
||||||
"version": "0.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz",
|
|
||||||
"integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"pathe": "^2.0.3",
|
|
||||||
"picomatch": "^4.0.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.19.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sxzz"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/unplugin-vue-components": {
|
"node_modules/unplugin-vue-components": {
|
||||||
@@ -5388,6 +5367,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vite-plugin-compression": {
|
||||||
|
"version": "0.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite-plugin-compression/-/vite-plugin-compression-0.5.1.tgz",
|
||||||
|
"integrity": "sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^4.1.2",
|
||||||
|
"debug": "^4.3.3",
|
||||||
|
"fs-extra": "^10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vite": ">=2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vitest": {
|
"node_modules/vitest": {
|
||||||
"version": "4.0.18",
|
"version": "4.0.18",
|
||||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
|
||||||
@@ -8189,6 +8182,17 @@
|
|||||||
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
|
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"fs-extra": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"graceful-fs": "^4.2.0",
|
||||||
|
"jsonfile": "^6.0.1",
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"fs.realpath": {
|
"fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
@@ -8439,6 +8443,16 @@
|
|||||||
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"jsonfile": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"graceful-fs": "^4.1.6",
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"keyv": {
|
"keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
@@ -9195,9 +9209,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"strip-literal": {
|
"strip-literal": {
|
||||||
"version": "3.1.0",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz",
|
||||||
"integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
|
"integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"js-tokens": "^9.0.1"
|
"js-tokens": "^9.0.1"
|
||||||
@@ -9327,25 +9341,25 @@
|
|||||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="
|
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="
|
||||||
},
|
},
|
||||||
"unimport": {
|
"unimport": {
|
||||||
"version": "5.7.0",
|
"version": "3.14.6",
|
||||||
"resolved": "https://registry.npmjs.org/unimport/-/unimport-5.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/unimport/-/unimport-3.14.6.tgz",
|
||||||
"integrity": "sha512-njnL6sp8lEA8QQbZrt+52p/g4X0rw3bnGGmUcJnt1jeG8+iiqO779aGz0PirCtydAIVcuTBRlJ52F0u46z309Q==",
|
"integrity": "sha512-CYvbDaTT04Rh8bmD8jz3WPmHYZRG/NnvYVzwD6V1YAlvvKROlAeNDUBhkBGzNav2RKaeuXvlWYaa1V4Lfi/O0g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"acorn": "^8.16.0",
|
"@rollup/pluginutils": "^5.1.4",
|
||||||
|
"acorn": "^8.14.0",
|
||||||
"escape-string-regexp": "^5.0.0",
|
"escape-string-regexp": "^5.0.0",
|
||||||
"estree-walker": "^3.0.3",
|
"estree-walker": "^3.0.3",
|
||||||
"local-pkg": "^1.1.2",
|
"fast-glob": "^3.3.3",
|
||||||
"magic-string": "^0.30.21",
|
"local-pkg": "^1.0.0",
|
||||||
"mlly": "^1.8.0",
|
"magic-string": "^0.30.17",
|
||||||
"pathe": "^2.0.3",
|
"mlly": "^1.7.4",
|
||||||
"picomatch": "^4.0.3",
|
"pathe": "^2.0.1",
|
||||||
"pkg-types": "^2.3.0",
|
"picomatch": "^4.0.2",
|
||||||
|
"pkg-types": "^1.3.0",
|
||||||
"scule": "^1.3.0",
|
"scule": "^1.3.0",
|
||||||
"strip-literal": "^3.1.0",
|
"strip-literal": "^2.1.1",
|
||||||
"tinyglobby": "^0.2.15",
|
"unplugin": "^1.16.1"
|
||||||
"unplugin": "^2.3.11",
|
|
||||||
"unplugin-utils": "^0.3.1"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"confbox": {
|
"confbox": {
|
||||||
@@ -9378,33 +9392,29 @@
|
|||||||
"mlly": "^1.7.4",
|
"mlly": "^1.7.4",
|
||||||
"pkg-types": "^2.3.0",
|
"pkg-types": "^2.3.0",
|
||||||
"quansync": "^0.2.11"
|
"quansync": "^0.2.11"
|
||||||
}
|
},
|
||||||
},
|
"dependencies": {
|
||||||
"pkg-types": {
|
"pkg-types": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
||||||
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
|
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"confbox": "^0.2.2",
|
"confbox": "^0.2.2",
|
||||||
"exsolve": "^1.0.7",
|
"exsolve": "^1.0.7",
|
||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"unplugin": {
|
|
||||||
"version": "2.3.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
|
|
||||||
"integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"@jridgewell/remapping": "^2.3.5",
|
|
||||||
"acorn": "^8.15.0",
|
|
||||||
"picomatch": "^4.0.3",
|
|
||||||
"webpack-virtual-modules": "^0.6.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"universalify": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"unplugin": {
|
"unplugin": {
|
||||||
"version": "1.16.1",
|
"version": "1.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz",
|
||||||
@@ -9416,71 +9426,41 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"unplugin-auto-import": {
|
"unplugin-auto-import": {
|
||||||
"version": "21.0.0",
|
"version": "0.18.6",
|
||||||
"resolved": "https://registry.npmjs.org/unplugin-auto-import/-/unplugin-auto-import-21.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unplugin-auto-import/-/unplugin-auto-import-0.18.6.tgz",
|
||||||
"integrity": "sha512-vWuC8SwqJmxZFYwPojhOhOXDb5xFhNNcEVb9K/RFkyk/3VnfaOjzitWN7v+8DEKpMjSsY2AEGXNgt6I0yQrhRQ==",
|
"integrity": "sha512-LMFzX5DtkTj/3wZuyG5bgKBoJ7WSgzqSGJ8ppDRdlvPh45mx6t6w3OcbExQi53n3xF5MYkNGPNR/HYOL95KL2A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"local-pkg": "^1.1.2",
|
"@antfu/utils": "^0.7.10",
|
||||||
"magic-string": "^0.30.21",
|
"@rollup/pluginutils": "^5.1.3",
|
||||||
"picomatch": "^4.0.3",
|
"fast-glob": "^3.3.2",
|
||||||
"unimport": "^5.6.0",
|
"local-pkg": "^0.5.1",
|
||||||
"unplugin": "^2.3.11",
|
"magic-string": "^0.30.14",
|
||||||
"unplugin-utils": "^0.3.1"
|
"minimatch": "^9.0.5",
|
||||||
|
"unimport": "^3.13.4",
|
||||||
|
"unplugin": "^1.16.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"confbox": {
|
"brace-expansion": {
|
||||||
"version": "0.2.4",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"local-pkg": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==",
|
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"mlly": "^1.7.4",
|
"balanced-match": "^1.0.0"
|
||||||
"pkg-types": "^2.3.0",
|
|
||||||
"quansync": "^0.2.11"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pkg-types": {
|
"minimatch": {
|
||||||
"version": "2.3.0",
|
"version": "9.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||||
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
|
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"confbox": "^0.2.2",
|
"brace-expansion": "^2.0.2"
|
||||||
"exsolve": "^1.0.7",
|
|
||||||
"pathe": "^2.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"unplugin": {
|
|
||||||
"version": "2.3.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
|
|
||||||
"integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"@jridgewell/remapping": "^2.3.5",
|
|
||||||
"acorn": "^8.15.0",
|
|
||||||
"picomatch": "^4.0.3",
|
|
||||||
"webpack-virtual-modules": "^0.6.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"unplugin-utils": {
|
|
||||||
"version": "0.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz",
|
|
||||||
"integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"pathe": "^2.0.3",
|
|
||||||
"picomatch": "^4.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"unplugin-vue-components": {
|
"unplugin-vue-components": {
|
||||||
"version": "0.27.5",
|
"version": "0.27.5",
|
||||||
"resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.27.5.tgz",
|
"resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.27.5.tgz",
|
||||||
@@ -9604,6 +9584,17 @@
|
|||||||
"rollup": "^4.20.0"
|
"rollup": "^4.20.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"vite-plugin-compression": {
|
||||||
|
"version": "0.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite-plugin-compression/-/vite-plugin-compression-0.5.1.tgz",
|
||||||
|
"integrity": "sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"chalk": "^4.1.2",
|
||||||
|
"debug": "^4.3.3",
|
||||||
|
"fs-extra": "^10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"vitest": {
|
"vitest": {
|
||||||
"version": "4.0.18",
|
"version": "4.0.18",
|
||||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
|
||||||
|
|||||||
@@ -50,9 +50,10 @@
|
|||||||
"rollup-plugin-visualizer": "^7.0.1",
|
"rollup-plugin-visualizer": "^7.0.1",
|
||||||
"sass": "^1.55.0",
|
"sass": "^1.55.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"unplugin-auto-import": "^21.0.0",
|
"unplugin-auto-import": "^0.18.6",
|
||||||
"unplugin-vue-components": "^0.27.3",
|
"unplugin-vue-components": "^0.27.3",
|
||||||
"vite": "^5.3.1",
|
"vite": "^5.3.1",
|
||||||
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vitest": "^4.0.18"
|
"vitest": "^4.0.18"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
118
src/App.vue
118
src/App.vue
@@ -19,7 +19,6 @@ import { onMounted, watch } from 'vue';
|
|||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { supabase } from '@/lib/supabase/client';
|
import { supabase } from '@/lib/supabase/client';
|
||||||
import { useTenantStore } from '@/stores/tenantStore';
|
import { useTenantStore } from '@/stores/tenantStore';
|
||||||
import { useEntitlementsStore } from '@/stores/entitlementsStore';
|
|
||||||
import { fetchDocsForPath } from '@/composables/useAjuda';
|
import { fetchDocsForPath } from '@/composables/useAjuda';
|
||||||
|
|
||||||
import AjudaDrawer from '@/components/AjudaDrawer.vue';
|
import AjudaDrawer from '@/components/AjudaDrawer.vue';
|
||||||
@@ -28,25 +27,14 @@ import AppOfflineOverlay from '@/components/AppOfflineOverlay.vue';
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const tenantStore = useTenantStore();
|
const tenantStore = useTenantStore();
|
||||||
const entStore = useEntitlementsStore();
|
|
||||||
|
|
||||||
function isTenantArea(path = '') {
|
function isTenantArea(path = '') {
|
||||||
return path.startsWith('/admin') || path.startsWith('/therapist');
|
return path.startsWith('/admin') || path.startsWith('/therapist') || path.startsWith('/configuracoes');
|
||||||
}
|
|
||||||
|
|
||||||
function isPortalArea(path = '') {
|
|
||||||
return path.startsWith('/portal');
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSaasArea(path = '') {
|
|
||||||
return path.startsWith('/saas');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Setup Wizard redirect ────────────────────────────────────────
|
// ── Setup Wizard redirect ────────────────────────────────────────
|
||||||
async function checkSetupWizard() {
|
async function checkSetupWizard() {
|
||||||
// Só verifica em área de tenant
|
|
||||||
if (!isTenantArea(route.path)) return;
|
if (!isTenantArea(route.path)) return;
|
||||||
// Não redireciona se já está no setup
|
|
||||||
if (route.path.includes('/setup')) return;
|
if (route.path.includes('/setup')) return;
|
||||||
|
|
||||||
const uid = tenantStore.user?.id;
|
const uid = tenantStore.user?.id;
|
||||||
@@ -56,7 +44,6 @@ async function checkSetupWizard() {
|
|||||||
|
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
// Determina o kind do tenant ativo para saber qual flag checar
|
|
||||||
const activeMembership = tenantStore.memberships?.find((m) => m.id === tenantStore.activeTenantId);
|
const activeMembership = tenantStore.memberships?.find((m) => m.id === tenantStore.activeTenantId);
|
||||||
const kind = activeMembership?.kind ?? tenantStore.activeRole ?? '';
|
const kind = activeMembership?.kind ?? tenantStore.activeRole ?? '';
|
||||||
const isClinic = kind.startsWith('clinic');
|
const isClinic = kind.startsWith('clinic');
|
||||||
@@ -68,113 +55,16 @@ async function checkSetupWizard() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function debugSnapshot(label = 'snapshot') {
|
onMounted(() => {
|
||||||
console.group(`🧭 [APP DEBUG] ${label}`);
|
|
||||||
try {
|
|
||||||
// 0) rota + meta
|
|
||||||
console.log('route.fullPath:', route.fullPath);
|
|
||||||
console.log('route.path:', route.path);
|
|
||||||
console.log('route.name:', route.name);
|
|
||||||
console.log('route.meta:', route.meta);
|
|
||||||
|
|
||||||
// 1) storage
|
|
||||||
console.groupCollapsed('📦 Storage');
|
|
||||||
console.log('localStorage.tenant_id:', localStorage.getItem('tenant_id'));
|
|
||||||
console.log('localStorage.currentTenantId:', localStorage.getItem('currentTenantId'));
|
|
||||||
console.log('localStorage.tenant:', localStorage.getItem('tenant'));
|
|
||||||
console.log('sessionStorage.redirect_after_login:', sessionStorage.getItem('redirect_after_login'));
|
|
||||||
console.log('sessionStorage.intended_area:', sessionStorage.getItem('intended_area'));
|
|
||||||
console.groupEnd();
|
|
||||||
|
|
||||||
// 2) sessão auth (fonte real)
|
|
||||||
const { data: authData, error: authErr } = await supabase.auth.getUser();
|
|
||||||
if (authErr) console.warn('[auth.getUser] error:', authErr);
|
|
||||||
const user = authData?.user || null;
|
|
||||||
console.log('auth.user:', user ? { id: user.id, email: user.email } : null);
|
|
||||||
|
|
||||||
// 3) profiles.role (identidade global)
|
|
||||||
let profileRole = null;
|
|
||||||
if (user?.id) {
|
|
||||||
const { data: profile, error: pErr } = await supabase.from('profiles').select('role').eq('id', user.id).single();
|
|
||||||
|
|
||||||
if (pErr) console.warn('[profiles] error:', pErr);
|
|
||||||
profileRole = profile?.role || null;
|
|
||||||
}
|
|
||||||
console.log('profiles.role (global):', profileRole);
|
|
||||||
|
|
||||||
// 4) memberships via RPC (fonte de verdade do tenantStore)
|
|
||||||
let rpcTenants = null;
|
|
||||||
if (user?.id) {
|
|
||||||
const { data: rpcData, error: rpcErr } = await supabase.rpc('my_tenants');
|
|
||||||
if (rpcErr) console.warn('[rpc my_tenants] error:', rpcErr);
|
|
||||||
rpcTenants = rpcData ?? null;
|
|
||||||
}
|
|
||||||
console.log('rpc.my_tenants():', rpcTenants);
|
|
||||||
|
|
||||||
// 5) stores (sempre logar)
|
|
||||||
console.groupCollapsed('🏪 Stores (before optional loads)');
|
|
||||||
console.log('tenantStore.activeTenantId:', tenantStore.activeTenantId);
|
|
||||||
console.log('tenantStore.activeRole:', tenantStore.activeRole);
|
|
||||||
console.log('tenantStore.memberships:', tenantStore.memberships);
|
|
||||||
console.log('entStore.loaded:', entStore.loaded);
|
|
||||||
console.log('entStore.tenantId:', entStore.activeTenantId || entStore.tenantId);
|
|
||||||
console.groupEnd();
|
|
||||||
|
|
||||||
// 6) IMPORTANTÍSSIMO: não carregar tenant fora da área tenant
|
|
||||||
const path = route.path || '';
|
|
||||||
|
|
||||||
if (isTenantArea(path)) {
|
|
||||||
console.log('✅ Tenant area detected → will loadSessionAndTenant + entitlements');
|
|
||||||
await tenantStore.loadSessionAndTenant();
|
|
||||||
|
|
||||||
if (tenantStore.activeTenantId) {
|
|
||||||
await entStore.loadForTenant(tenantStore.activeTenantId);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.groupCollapsed('🏪 Stores (after tenant loads)');
|
|
||||||
console.log('tenantStore.activeTenantId:', tenantStore.activeTenantId);
|
|
||||||
console.log('tenantStore.activeRole:', tenantStore.activeRole);
|
|
||||||
console.log('tenantStore.memberships:', tenantStore.memberships);
|
|
||||||
console.log("entStore.can('online_scheduling.manage'):", entStore.can?.('online_scheduling.manage'));
|
|
||||||
console.groupEnd();
|
|
||||||
|
|
||||||
// Redireciona para o wizard se setup não foi concluído
|
|
||||||
await checkSetupWizard();
|
|
||||||
} else if (isPortalArea(path)) {
|
|
||||||
console.log('🟣 Portal area detected → SKIP tenantStore.loadSessionAndTenant()');
|
|
||||||
} else if (isSaasArea(path)) {
|
|
||||||
console.log('🟠 SaaS area detected → SKIP tenantStore.loadSessionAndTenant()');
|
|
||||||
} else {
|
|
||||||
console.log('⚪ Other/public area detected → SKIP tenantStore.loadSessionAndTenant()');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[APP DEBUG] snapshot error:', e);
|
|
||||||
} finally {
|
|
||||||
console.groupEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
// 🔥 PRIMEIRO LOG — TENANT ID BRUTO (mantive sua ideia)
|
|
||||||
console.log('[SEU_TENANT_ID]', localStorage.getItem('tenant_id'));
|
|
||||||
|
|
||||||
// snapshot inicial
|
|
||||||
await debugSnapshot('mounted');
|
|
||||||
|
|
||||||
// Carrega docs de ajuda para a rota inicial
|
|
||||||
fetchDocsForPath(route.path);
|
fetchDocsForPath(route.path);
|
||||||
});
|
});
|
||||||
|
|
||||||
// snapshot a cada navegação (isso é o que vai te salvar)
|
|
||||||
watch(
|
watch(
|
||||||
() => route.fullPath,
|
() => route.fullPath,
|
||||||
async (to, from) => {
|
() => {
|
||||||
await debugSnapshot(`route change: ${from} -> ${to}`);
|
|
||||||
// Atualiza docs de ajuda ao navegar
|
|
||||||
fetchDocsForPath(route.path);
|
fetchDocsForPath(route.path);
|
||||||
// Verifica setup sempre que entrar em área tenant
|
|
||||||
if (isTenantArea(route.path) && tenantStore.loaded) {
|
if (isTenantArea(route.path) && tenantStore.loaded) {
|
||||||
await checkSetupWizard();
|
checkSetupWizard();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
114
src/auto-imports.d.ts
vendored
114
src/auto-imports.d.ts
vendored
@@ -6,68 +6,68 @@
|
|||||||
// biome-ignore lint: disable
|
// biome-ignore lint: disable
|
||||||
export {}
|
export {}
|
||||||
declare global {
|
declare global {
|
||||||
const EffectScope: typeof import('vue').EffectScope
|
const EffectScope: typeof import('vue')['EffectScope']
|
||||||
const computed: typeof import('vue').computed
|
const computed: typeof import('vue')['computed']
|
||||||
const createApp: typeof import('vue').createApp
|
const createApp: typeof import('vue')['createApp']
|
||||||
const customRef: typeof import('vue').customRef
|
const customRef: typeof import('vue')['customRef']
|
||||||
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
|
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||||
const defineComponent: typeof import('vue').defineComponent
|
const defineComponent: typeof import('vue')['defineComponent']
|
||||||
const effectScope: typeof import('vue').effectScope
|
const effectScope: typeof import('vue')['effectScope']
|
||||||
const getCurrentInstance: typeof import('vue').getCurrentInstance
|
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||||
const getCurrentScope: typeof import('vue').getCurrentScope
|
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||||
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
|
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
|
||||||
const h: typeof import('vue').h
|
const h: typeof import('vue')['h']
|
||||||
const inject: typeof import('vue').inject
|
const inject: typeof import('vue')['inject']
|
||||||
const isProxy: typeof import('vue').isProxy
|
const isProxy: typeof import('vue')['isProxy']
|
||||||
const isReactive: typeof import('vue').isReactive
|
const isReactive: typeof import('vue')['isReactive']
|
||||||
const isReadonly: typeof import('vue').isReadonly
|
const isReadonly: typeof import('vue')['isReadonly']
|
||||||
const isRef: typeof import('vue').isRef
|
const isRef: typeof import('vue')['isRef']
|
||||||
const isShallow: typeof import('vue').isShallow
|
const isShallow: typeof import('vue').isShallow
|
||||||
const markRaw: typeof import('vue').markRaw
|
const markRaw: typeof import('vue')['markRaw']
|
||||||
const nextTick: typeof import('vue').nextTick
|
const nextTick: typeof import('vue')['nextTick']
|
||||||
const onActivated: typeof import('vue').onActivated
|
const onActivated: typeof import('vue')['onActivated']
|
||||||
const onBeforeMount: typeof import('vue').onBeforeMount
|
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||||
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
|
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||||
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
|
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||||
const onDeactivated: typeof import('vue').onDeactivated
|
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||||
const onErrorCaptured: typeof import('vue').onErrorCaptured
|
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||||
const onMounted: typeof import('vue').onMounted
|
const onMounted: typeof import('vue')['onMounted']
|
||||||
const onRenderTracked: typeof import('vue').onRenderTracked
|
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||||
const onRenderTriggered: typeof import('vue').onRenderTriggered
|
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||||
const onScopeDispose: typeof import('vue').onScopeDispose
|
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||||
const onServerPrefetch: typeof import('vue').onServerPrefetch
|
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||||
const onUnmounted: typeof import('vue').onUnmounted
|
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||||
const onUpdated: typeof import('vue').onUpdated
|
const onUpdated: typeof import('vue')['onUpdated']
|
||||||
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
|
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||||
const provide: typeof import('vue').provide
|
const provide: typeof import('vue')['provide']
|
||||||
const reactive: typeof import('vue').reactive
|
const reactive: typeof import('vue')['reactive']
|
||||||
const readonly: typeof import('vue').readonly
|
const readonly: typeof import('vue')['readonly']
|
||||||
const ref: typeof import('vue').ref
|
const ref: typeof import('vue')['ref']
|
||||||
const resolveComponent: typeof import('vue').resolveComponent
|
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||||
const shallowReactive: typeof import('vue').shallowReactive
|
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||||
const shallowReadonly: typeof import('vue').shallowReadonly
|
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||||
const shallowRef: typeof import('vue').shallowRef
|
const shallowRef: typeof import('vue')['shallowRef']
|
||||||
const toRaw: typeof import('vue').toRaw
|
const toRaw: typeof import('vue')['toRaw']
|
||||||
const toRef: typeof import('vue').toRef
|
const toRef: typeof import('vue')['toRef']
|
||||||
const toRefs: typeof import('vue').toRefs
|
const toRefs: typeof import('vue')['toRefs']
|
||||||
const toValue: typeof import('vue').toValue
|
const toValue: typeof import('vue')['toValue']
|
||||||
const triggerRef: typeof import('vue').triggerRef
|
const triggerRef: typeof import('vue')['triggerRef']
|
||||||
const unref: typeof import('vue').unref
|
const unref: typeof import('vue')['unref']
|
||||||
const useAttrs: typeof import('vue').useAttrs
|
const useAttrs: typeof import('vue')['useAttrs']
|
||||||
const useCssModule: typeof import('vue').useCssModule
|
const useCssModule: typeof import('vue')['useCssModule']
|
||||||
const useCssVars: typeof import('vue').useCssVars
|
const useCssVars: typeof import('vue')['useCssVars']
|
||||||
const useId: typeof import('vue').useId
|
const useId: typeof import('vue')['useId']
|
||||||
const useModel: typeof import('vue').useModel
|
const useModel: typeof import('vue')['useModel']
|
||||||
const useSlots: typeof import('vue').useSlots
|
const useSlots: typeof import('vue')['useSlots']
|
||||||
const useTemplateRef: typeof import('vue').useTemplateRef
|
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||||
const watch: typeof import('vue').watch
|
const watch: typeof import('vue')['watch']
|
||||||
const watchEffect: typeof import('vue').watchEffect
|
const watchEffect: typeof import('vue')['watchEffect']
|
||||||
const watchPostEffect: typeof import('vue').watchPostEffect
|
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||||
const watchSyncEffect: typeof import('vue').watchSyncEffect
|
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||||
}
|
}
|
||||||
// for type re-export
|
// for type re-export
|
||||||
declare global {
|
declare global {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||||
import('vue')
|
import('vue')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,21 +15,21 @@
|
|||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
-->
|
-->
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, watch, onMounted, onBeforeUnmount } from 'vue';
|
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
|
||||||
import { useToast } from 'primevue/usetoast';
|
|
||||||
import { useConfirm } from 'primevue/useconfirm';
|
|
||||||
import { supabase } from '@/lib/supabase/client';
|
import { supabase } from '@/lib/supabase/client';
|
||||||
|
import Menu from 'primevue/menu';
|
||||||
import MultiSelect from 'primevue/multiselect';
|
import MultiSelect from 'primevue/multiselect';
|
||||||
import Popover from 'primevue/popover';
|
import Popover from 'primevue/popover';
|
||||||
import Menu from 'primevue/menu';
|
|
||||||
import ProgressSpinner from 'primevue/progressspinner';
|
import ProgressSpinner from 'primevue/progressspinner';
|
||||||
|
import { useConfirm } from 'primevue/useconfirm';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
import PatientProntuario from '@/features/patients/prontuario/PatientProntuario.vue';
|
|
||||||
import ComponentCadastroRapido from '@/components/ComponentCadastroRapido.vue';
|
import ComponentCadastroRapido from '@/components/ComponentCadastroRapido.vue';
|
||||||
import PatientActionMenu from '@/components/patients/PatientActionMenu.vue';
|
import PatientActionMenu from '@/components/patients/PatientActionMenu.vue';
|
||||||
import PatientCreatePopover from '@/components/ui/PatientCreatePopover.vue';
|
|
||||||
import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue';
|
import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue';
|
||||||
|
import PatientCreatePopover from '@/components/ui/PatientCreatePopover.vue';
|
||||||
|
import PatientProntuario from '@/features/patients/prontuario/PatientProntuario.vue';
|
||||||
import { getSysGroupColor, getSystemGroupDefaultColor } from '@/utils/systemGroupColors.js';
|
import { getSysGroupColor, getSystemGroupDefaultColor } from '@/utils/systemGroupColors.js';
|
||||||
|
|
||||||
// ── Descontos por paciente ────────────────────────────────────────
|
// ── Descontos por paciente ────────────────────────────────────────
|
||||||
|
|||||||
1
src/layout/RouterPassthrough.vue
Normal file
1
src/layout/RouterPassthrough.vue
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<template><router-view /></template>
|
||||||
102
src/main.js
102
src/main.js
@@ -1,56 +1,42 @@
|
|||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Agência PSI
|
| Agência PSI (OTIMIZADO)
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Criado e desenvolvido por Leonardo Nohama
|
|
||||||
|
|
|
||||||
| Tecnologia aplicada à escuta.
|
|
||||||
| Estrutura para o cuidado.
|
|
||||||
|
|
|
||||||
| Arquivo: src/main.js
|
|
||||||
| Data: 2026
|
|
||||||
| Local: São Carlos/SP — Brasil
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| © 2026 — Todos os direitos reservados
|
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { pinia } from '@/plugins/pinia';
|
||||||
|
import router from '@/router';
|
||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import router from '@/router';
|
|
||||||
import { pinia } from '@/plugins/pinia'; // ← singleton criado antes do router
|
|
||||||
import { setOnSignedOut, initSession, listenAuthChanges, refreshSession } from '@/app/session';
|
|
||||||
|
|
||||||
|
import { initSession, listenAuthChanges, refreshSession, setOnSignedOut } from '@/app/session';
|
||||||
|
|
||||||
|
// PrimeVue core
|
||||||
import Aura from '@primeuix/themes/aura';
|
import Aura from '@primeuix/themes/aura';
|
||||||
import PrimeVue from 'primevue/config';
|
import PrimeVue from 'primevue/config';
|
||||||
|
|
||||||
|
// serviços (ok global)
|
||||||
import ConfirmationService from 'primevue/confirmationservice';
|
import ConfirmationService from 'primevue/confirmationservice';
|
||||||
import ToastService from 'primevue/toastservice';
|
import ToastService from 'primevue/toastservice';
|
||||||
|
|
||||||
|
// ✅ SOMENTE COMPONENTES LEVES GLOBAIS
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
import Divider from 'primevue/divider';
|
||||||
|
import InputText from 'primevue/inputtext';
|
||||||
|
import Tag from 'primevue/tag';
|
||||||
|
import Toast from 'primevue/toast';
|
||||||
|
|
||||||
|
// seus componentes leves
|
||||||
import AppLoadingPhrases from '@/components/ui/AppLoadingPhrases.vue';
|
import AppLoadingPhrases from '@/components/ui/AppLoadingPhrases.vue';
|
||||||
import LoadedPhraseBlock from '@/components/ui/LoadedPhraseBlock.vue';
|
import LoadedPhraseBlock from '@/components/ui/LoadedPhraseBlock.vue';
|
||||||
|
|
||||||
// ── Componentes PrimeVue globais (≥ 10 usos no projeto) ──────────────────────
|
// estilos
|
||||||
import Button from 'primevue/button';
|
|
||||||
import InputText from 'primevue/inputtext';
|
|
||||||
import Tag from 'primevue/tag';
|
|
||||||
import FloatLabel from 'primevue/floatlabel';
|
|
||||||
import Toast from 'primevue/toast';
|
|
||||||
import IconField from 'primevue/iconfield';
|
|
||||||
import InputIcon from 'primevue/inputicon';
|
|
||||||
import Divider from 'primevue/divider';
|
|
||||||
import Card from 'primevue/card';
|
|
||||||
import SelectButton from 'primevue/selectbutton';
|
|
||||||
import Dialog from 'primevue/dialog';
|
|
||||||
import DataTable from 'primevue/datatable';
|
|
||||||
import Column from 'primevue/column';
|
|
||||||
import ConfirmDialog from 'primevue/confirmdialog';
|
|
||||||
import Menu from 'primevue/menu';
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
import '@/assets/tailwind.css';
|
|
||||||
import '@/assets/styles.scss';
|
import '@/assets/styles.scss';
|
||||||
|
import '@/assets/tailwind.css';
|
||||||
|
|
||||||
import { supabase } from '@/lib/supabase/client';
|
import { supabase } from '@/lib/supabase/client';
|
||||||
|
|
||||||
// ✅ pt-BR (PrimeVue locale global)
|
// locale
|
||||||
const ptBR = {
|
const ptBR = {
|
||||||
firstDayOfWeek: 1,
|
firstDayOfWeek: 1,
|
||||||
dayNames: ['domingo', 'segunda-feira', 'terça-feira', 'quarta-feira', 'quinta-feira', 'sexta-feira', 'sábado'],
|
dayNames: ['domingo', 'segunda-feira', 'terça-feira', 'quarta-feira', 'quinta-feira', 'sexta-feira', 'sábado'],
|
||||||
@@ -64,15 +50,16 @@ const ptBR = {
|
|||||||
dateFormat: 'dd/mm/yy'
|
dateFormat: 'dd/mm/yy'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// theme antecipado
|
||||||
async function applyUserThemeEarly() {
|
async function applyUserThemeEarly() {
|
||||||
try {
|
try {
|
||||||
const { data } = await supabase.auth.getUser();
|
const { data } = await supabase.auth.getUser();
|
||||||
const user = data?.user;
|
const user = data?.user;
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
const { data: settings, error } = await supabase.from('user_settings').select('theme_mode').eq('user_id', user.id).maybeSingle();
|
const { data: settings } = await supabase.from('user_settings').select('theme_mode').eq('user_id', user.id).maybeSingle();
|
||||||
|
|
||||||
if (error || !settings?.theme_mode) return;
|
if (!settings?.theme_mode) return;
|
||||||
|
|
||||||
const isDark = settings.theme_mode === 'dark';
|
const isDark = settings.theme_mode === 'dark';
|
||||||
document.documentElement.classList.toggle('app-dark', isDark);
|
document.documentElement.classList.toggle('app-dark', isDark);
|
||||||
@@ -80,15 +67,15 @@ async function applyUserThemeEarly() {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// logout
|
||||||
setOnSignedOut(() => {
|
setOnSignedOut(() => {
|
||||||
router.replace('/auth/login');
|
router.replace('/auth/login');
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== flags globais (debug/controle) =====
|
// flags
|
||||||
window.__sessionRefreshing = false;
|
window.__sessionRefreshing = false;
|
||||||
window.__fromVisibilityRefresh = false;
|
window.__fromVisibilityRefresh = false;
|
||||||
window.__appBootstrapped = false;
|
window.__appBootstrapped = false;
|
||||||
// ========================================
|
|
||||||
|
|
||||||
let lastVisibilityRefreshAt = 0;
|
let lastVisibilityRefreshAt = 0;
|
||||||
|
|
||||||
@@ -97,7 +84,7 @@ document.addEventListener('visibilitychange', async () => {
|
|||||||
if (!window.__appBootstrapped) return;
|
if (!window.__appBootstrapped) return;
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastVisibilityRefreshAt < 10_000) return;
|
if (now - lastVisibilityRefreshAt < 10000) return;
|
||||||
if (window.__sessionRefreshing) return;
|
if (window.__sessionRefreshing) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -106,7 +93,6 @@ document.addEventListener('visibilitychange', async () => {
|
|||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
lastVisibilityRefreshAt = now;
|
lastVisibilityRefreshAt = now;
|
||||||
console.log('[VISIBILITY] Aba voltou -> refreshSession()');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
window.__sessionRefreshing = true;
|
window.__sessionRefreshing = true;
|
||||||
@@ -114,16 +100,16 @@ document.addEventListener('visibilitychange', async () => {
|
|||||||
|
|
||||||
await refreshSession();
|
await refreshSession();
|
||||||
|
|
||||||
try {
|
const path = router.currentRoute.value?.path || '';
|
||||||
const path = router.currentRoute.value?.path || '';
|
const isTenantArea = path.startsWith('/admin') || path.startsWith('/therapist') || path.startsWith('/saas');
|
||||||
const isTenantArea = path.startsWith('/admin') || path.startsWith('/therapist') || path.startsWith('/saas');
|
|
||||||
|
|
||||||
if (isTenantArea) {
|
if (isTenantArea) {
|
||||||
window.dispatchEvent(new CustomEvent('app:session-refreshed', { detail: { source: 'visibility' } }));
|
window.dispatchEvent(
|
||||||
} else {
|
new CustomEvent('app:session-refreshed', {
|
||||||
console.log('[VISIBILITY] refresh ok (skip event) - area não-tenant:', path);
|
detail: { source: 'visibility' }
|
||||||
}
|
})
|
||||||
} catch {}
|
);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
window.__fromVisibilityRefresh = false;
|
window.__fromVisibilityRefresh = false;
|
||||||
window.__sessionRefreshing = false;
|
window.__sessionRefreshing = false;
|
||||||
@@ -133,12 +119,10 @@ document.addEventListener('visibilitychange', async () => {
|
|||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
await initSession({ initial: true });
|
await initSession({ initial: true });
|
||||||
listenAuthChanges();
|
listenAuthChanges();
|
||||||
|
|
||||||
await applyUserThemeEarly();
|
await applyUserThemeEarly();
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
// ✅ usa o pinia singleton — o mesmo que o router/guards já conhecem
|
|
||||||
app.use(pinia);
|
app.use(pinia);
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
@@ -155,21 +139,13 @@ async function bootstrap() {
|
|||||||
app.use(ToastService);
|
app.use(ToastService);
|
||||||
app.use(ConfirmationService);
|
app.use(ConfirmationService);
|
||||||
|
|
||||||
|
// ✅ globais leves
|
||||||
app.component('Button', Button);
|
app.component('Button', Button);
|
||||||
app.component('InputText', InputText);
|
app.component('InputText', InputText);
|
||||||
app.component('Tag', Tag);
|
app.component('Tag', Tag);
|
||||||
app.component('FloatLabel', FloatLabel);
|
|
||||||
app.component('Toast', Toast);
|
|
||||||
app.component('IconField', IconField);
|
|
||||||
app.component('InputIcon', InputIcon);
|
|
||||||
app.component('Divider', Divider);
|
app.component('Divider', Divider);
|
||||||
app.component('Card', Card);
|
app.component('Toast', Toast);
|
||||||
app.component('SelectButton', SelectButton);
|
|
||||||
app.component('Dialog', Dialog);
|
|
||||||
app.component('DataTable', DataTable);
|
|
||||||
app.component('Column', Column);
|
|
||||||
app.component('ConfirmDialog', ConfirmDialog);
|
|
||||||
app.component('Menu', Menu);
|
|
||||||
app.component('AppLoadingPhrases', AppLoadingPhrases);
|
app.component('AppLoadingPhrases', AppLoadingPhrases);
|
||||||
app.component('LoadedPhraseBlock', LoadedPhraseBlock);
|
app.component('LoadedPhraseBlock', LoadedPhraseBlock);
|
||||||
|
|
||||||
|
|||||||
@@ -341,7 +341,11 @@ export function applyGuards(router) {
|
|||||||
return { path: '/auth/login' };
|
return { path: '/auth/login' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTenantArea = to.path.startsWith('/admin') || to.path.startsWith('/therapist') || to.path.startsWith('/supervisor');
|
const isTenantArea =
|
||||||
|
to.path.startsWith('/admin') ||
|
||||||
|
to.path.startsWith('/therapist') ||
|
||||||
|
to.path.startsWith('/supervisor') ||
|
||||||
|
to.path.startsWith('/configuracoes');
|
||||||
|
|
||||||
// ======================================
|
// ======================================
|
||||||
// ✅ IDENTIDADE GLOBAL (cached por uid — sem query a cada navegação)
|
// ✅ IDENTIDADE GLOBAL (cached por uid — sem query a cada navegação)
|
||||||
|
|||||||
@@ -14,48 +14,77 @@
|
|||||||
| © 2026 — Todos os direitos reservados
|
| © 2026 — Todos os direitos reservados
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
import { createRouter, createWebHistory, isNavigationFailure, NavigationFailureType } from 'vue-router';
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
|
import AppLayout from '@/layout/AppLayout.vue';
|
||||||
|
|
||||||
import configuracoesRoutes from './routes.configs';
|
// ── Rotas de app (filhas do AppLayout compartilhado) ──────────────────────
|
||||||
import meRoutes from './routes.account';
|
import therapistRoutes, { therapistStandalone } from './routes.therapist';
|
||||||
import adminRoutes from './routes.clinic';
|
import adminRoutes, { clinicStandalone } from './routes.clinic';
|
||||||
import authRoutes from './routes.auth';
|
import accountRoutes from './routes.account';
|
||||||
import billingRoutes from './routes.billing';
|
import billingRoutes from './routes.billing';
|
||||||
import miscRoutes from './routes.misc';
|
|
||||||
import portalRoutes from './routes.portal';
|
|
||||||
import publicRoutes from './routes.public';
|
|
||||||
import saasRoutes from './routes.saas';
|
|
||||||
import therapistRoutes from './routes.therapist';
|
|
||||||
import supervisorRoutes from './routes.supervisor';
|
import supervisorRoutes from './routes.supervisor';
|
||||||
import editorRoutes from './routes.editor';
|
|
||||||
import featuresRoutes from './routes.features';
|
import featuresRoutes from './routes.features';
|
||||||
|
import configuracoesRoutes from './routes.configs';
|
||||||
|
|
||||||
import { pinia } from '@/plugins/pinia'; // ← singleton compartilhado
|
// ── Rotas com AppLayout próprio (usuários distintos, sem navegação cruzada) ──
|
||||||
|
import saasRoutes from './routes.saas';
|
||||||
|
import editorRoutes from './routes.editor';
|
||||||
|
import portalRoutes from './routes.portal';
|
||||||
|
|
||||||
|
// ── Rotas sem AppLayout ───────────────────────────────────────────────────
|
||||||
|
import authRoutes from './routes.auth';
|
||||||
|
import publicRoutes from './routes.public';
|
||||||
|
import miscRoutes from './routes.misc';
|
||||||
|
|
||||||
|
import { pinia } from '@/plugins/pinia';
|
||||||
import { supportGuard } from '@/support/supportGuard';
|
import { supportGuard } from '@/support/supportGuard';
|
||||||
import { applyGuards } from './guards';
|
import { applyGuards } from './guards';
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
|
// ── Sem layout (public + auth) ─────────────────────────────────────────
|
||||||
...(Array.isArray(publicRoutes) ? publicRoutes : [publicRoutes]),
|
...(Array.isArray(publicRoutes) ? publicRoutes : [publicRoutes]),
|
||||||
...(Array.isArray(authRoutes) ? authRoutes : [authRoutes]),
|
...(Array.isArray(authRoutes) ? authRoutes : [authRoutes]),
|
||||||
...(Array.isArray(miscRoutes) ? miscRoutes : [miscRoutes]),
|
|
||||||
...(Array.isArray(billingRoutes) ? billingRoutes : [billingRoutes]),
|
// ── Fullscreen — fora do AppLayout (setup wizards etc.) ────────────────
|
||||||
|
...therapistStandalone,
|
||||||
|
...clinicStandalone,
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
// AppLayout ÚNICO compartilhado por todas as áreas autenticadas.
|
||||||
|
//
|
||||||
|
// Benefício: sidebar, topbar e estado do layout NUNCA são desmontados
|
||||||
|
// ao navegar entre /therapist, /admin, /configuracoes, /account etc.
|
||||||
|
// Cada área usa um RouterPassthrough (path relativo sem componente visual)
|
||||||
|
// que mantém o AppLayout intacto enquanto só o conteúdo é trocado.
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: AppLayout,
|
||||||
|
children: [
|
||||||
|
therapistRoutes, // path: 'therapist'
|
||||||
|
adminRoutes, // path: 'admin'
|
||||||
|
accountRoutes, // path: 'account'
|
||||||
|
billingRoutes, // path: 'upgrade'
|
||||||
|
supervisorRoutes, // path: 'supervisor'
|
||||||
|
featuresRoutes, // path: 'features'
|
||||||
|
configuracoesRoutes // path: 'configuracoes'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── AppLayout próprio: áreas de usuários distintos ────────────────────
|
||||||
|
// Saas, editor e portal são sessões de usuário completamente diferentes.
|
||||||
|
// Nunca há navegação cruzada entre essas áreas e as áreas de app acima.
|
||||||
...(Array.isArray(saasRoutes) ? saasRoutes : [saasRoutes]),
|
...(Array.isArray(saasRoutes) ? saasRoutes : [saasRoutes]),
|
||||||
...(Array.isArray(meRoutes) ? meRoutes : [meRoutes]),
|
|
||||||
...(Array.isArray(adminRoutes) ? adminRoutes : [adminRoutes]),
|
|
||||||
...(Array.isArray(therapistRoutes) ? therapistRoutes : [therapistRoutes]),
|
|
||||||
...(Array.isArray(supervisorRoutes) ? supervisorRoutes : [supervisorRoutes]),
|
|
||||||
...(Array.isArray(editorRoutes) ? editorRoutes : [editorRoutes]),
|
...(Array.isArray(editorRoutes) ? editorRoutes : [editorRoutes]),
|
||||||
...(Array.isArray(portalRoutes) ? portalRoutes : [portalRoutes]),
|
...(Array.isArray(portalRoutes) ? portalRoutes : [portalRoutes]),
|
||||||
...(Array.isArray(configuracoesRoutes) ? configuracoesRoutes : [configuracoesRoutes]),
|
|
||||||
...(Array.isArray(featuresRoutes) ? featuresRoutes : [featuresRoutes]),
|
|
||||||
|
|
||||||
// ✅ compat: rota antiga /login → /auth/login
|
// ── Misc (catch-all SEMPRE por último) ────────────────────────────────
|
||||||
|
...(Array.isArray(miscRoutes) ? miscRoutes : [miscRoutes]),
|
||||||
|
|
||||||
|
// ── Compat: rota legada /login → /auth/login ──────────────────────────
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
redirect: (to) => ({
|
redirect: (to) => ({ path: '/auth/login', query: to.query || {} })
|
||||||
path: '/auth/login',
|
|
||||||
query: to.query || {}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -68,39 +97,6 @@ const router = createRouter({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* 🔎 DEBUG: listar todas as rotas registradas */
|
|
||||||
console.log(
|
|
||||||
'[ROUTES]',
|
|
||||||
router
|
|
||||||
.getRoutes()
|
|
||||||
.map((r) => r.path)
|
|
||||||
.sort()
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== DEBUG NAV + TRACE (remover depois) =====
|
|
||||||
const _push = router.push.bind(router);
|
|
||||||
router.push = async (loc) => {
|
|
||||||
console.log('[router.push]', loc);
|
|
||||||
console.trace('[push caller]');
|
|
||||||
const res = await _push(loc);
|
|
||||||
if (isNavigationFailure(res, NavigationFailureType.duplicated)) console.warn('[NAV FAIL] duplicated', res);
|
|
||||||
else if (isNavigationFailure(res, NavigationFailureType.cancelled)) console.warn('[NAV FAIL] cancelled', res);
|
|
||||||
else if (isNavigationFailure(res, NavigationFailureType.aborted)) console.warn('[NAV FAIL] aborted', res);
|
|
||||||
else if (isNavigationFailure(res, NavigationFailureType.redirected)) console.warn('[NAV FAIL] redirected', res);
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
|
|
||||||
const _replace = router.replace.bind(router);
|
|
||||||
router.replace = async (loc) => {
|
|
||||||
console.log('[router.replace]', loc);
|
|
||||||
console.trace('[replace caller]');
|
|
||||||
const res = await _replace(loc);
|
|
||||||
if (isNavigationFailure(res, NavigationFailureType.cancelled)) console.warn('[NAV FAIL replace] cancelled', res);
|
|
||||||
else if (isNavigationFailure(res, NavigationFailureType.aborted)) console.warn('[NAV FAIL replace] aborted', res);
|
|
||||||
else if (isNavigationFailure(res, NavigationFailureType.redirected)) console.warn('[NAV FAIL replace] redirected', res);
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
|
|
||||||
router.onError((e) => console.error('[router.onError]', e));
|
router.onError((e) => console.error('[router.onError]', e));
|
||||||
|
|
||||||
// ✅ support guard — passa pinia para garantir acesso ao store antes do app.use(pinia)
|
// ✅ support guard — passa pinia para garantir acesso ao store antes do app.use(pinia)
|
||||||
@@ -108,17 +104,6 @@ router.beforeEach(async (to) => {
|
|||||||
await supportGuard(to, pinia);
|
await supportGuard(to, pinia);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.beforeEach((to, from) => {
|
|
||||||
console.log('[beforeEach]', from.fullPath, '->', to.fullPath);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
router.afterEach((to, from, failure) => {
|
|
||||||
if (failure) console.warn('[afterEach failure]', failure);
|
|
||||||
else console.log('[afterEach ok]', from.fullPath, '->', to.fullPath);
|
|
||||||
});
|
|
||||||
// ===== /DEBUG NAV + TRACE =====
|
|
||||||
|
|
||||||
applyGuards(router);
|
applyGuards(router);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -14,11 +14,11 @@
|
|||||||
| © 2026 — Todos os direitos reservados
|
| © 2026 — Todos os direitos reservados
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
import AppLayout from '@/layout/AppLayout.vue';
|
import RouterPassthrough from '@/layout/RouterPassthrough.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
path: '/account',
|
path: 'account',
|
||||||
component: AppLayout,
|
component: RouterPassthrough,
|
||||||
meta: { requiresAuth: true, area: 'account' },
|
meta: { requiresAuth: true, area: 'account' },
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,11 +14,11 @@
|
|||||||
| © 2026 — Todos os direitos reservados
|
| © 2026 — Todos os direitos reservados
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
import AppLayout from '@/layout/AppLayout.vue';
|
import RouterPassthrough from '@/layout/RouterPassthrough.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
path: '/upgrade',
|
path: 'upgrade',
|
||||||
component: AppLayout,
|
component: RouterPassthrough,
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true },
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,225 +14,172 @@
|
|||||||
| © 2026 — Todos os direitos reservados
|
| © 2026 — Todos os direitos reservados
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
import AppLayout from '@/layout/AppLayout.vue';
|
import RouterPassthrough from '@/layout/RouterPassthrough.vue';
|
||||||
|
|
||||||
export default [
|
// ── Rotas fullscreen — ficam fora do AppLayout compartilhado ──────────────
|
||||||
// ======================================================
|
export const clinicStandalone = [
|
||||||
// 🚀 SETUP WIZARD — fora do AppLayout (fullscreen)
|
|
||||||
// ======================================================
|
|
||||||
{
|
{
|
||||||
path: '/admin/setup',
|
path: '/admin/setup',
|
||||||
name: 'admin.setup',
|
name: 'admin.setup',
|
||||||
component: () => import('@/features/setup/SetupWizardPage.vue'),
|
component: () => import('@/features/setup/SetupWizardPage.vue'),
|
||||||
meta: { area: 'admin', requiresAuth: true, roles: ['clinic_admin'], fullscreen: true }
|
meta: { area: 'admin', requiresAuth: true, roles: ['clinic_admin'], fullscreen: true }
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
path: '/admin',
|
|
||||||
component: AppLayout,
|
|
||||||
|
|
||||||
meta: {
|
|
||||||
// 🔐 Tudo aqui dentro exige login
|
|
||||||
area: 'admin',
|
|
||||||
requiresAuth: true,
|
|
||||||
|
|
||||||
// 👤 Perfil de acesso (tenant-level)
|
|
||||||
// tenantStore normaliza tenant_admin -> clinic_admin, mas mantemos compatibilidade
|
|
||||||
roles: ['clinic_admin']
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
// ======================================================
|
|
||||||
// 📊 DASHBOARD
|
|
||||||
// ======================================================
|
|
||||||
{ path: '', name: 'admin.dashboard', component: () => import('@/views/pages/clinic/ClinicDashboard.vue') },
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// 🧩 CLÍNICA — MÓDULOS (tenant_features)
|
|
||||||
// ======================================================
|
|
||||||
{
|
|
||||||
path: 'clinic/features',
|
|
||||||
name: 'admin-clinic-features',
|
|
||||||
component: () => import('@/views/pages/clinic/clinic/ClinicFeaturesPage.vue'),
|
|
||||||
meta: {
|
|
||||||
// opcional: restringir apenas para admin canônico
|
|
||||||
roles: ['clinic_admin', 'tenant_admin']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'clinic/professionals',
|
|
||||||
name: 'admin-clinic-professionals',
|
|
||||||
component: () => import('@/views/pages/clinic/clinic/ClinicProfessionalsPage.vue'),
|
|
||||||
meta: {
|
|
||||||
requiresAuth: true,
|
|
||||||
roles: ['clinic_admin', 'tenant_admin']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// 💳 MEU PLANO
|
|
||||||
// ======================================================
|
|
||||||
{
|
|
||||||
path: 'meu-plano',
|
|
||||||
name: 'admin-meu-plano',
|
|
||||||
component: () => import('@/views/pages/billing/ClinicMeuPlanoPage.vue')
|
|
||||||
},
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// 📅 AGENDA DA CLÍNICA
|
|
||||||
// ======================================================
|
|
||||||
|
|
||||||
{
|
|
||||||
path: 'agenda/clinica',
|
|
||||||
name: 'admin-agenda-clinica',
|
|
||||||
component: () => import('@/features/agenda/pages/AgendaClinicaPage.vue'),
|
|
||||||
meta: {
|
|
||||||
feature: 'agenda.view',
|
|
||||||
roles: ['clinic_admin', 'tenant_admin']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Recorrências
|
|
||||||
{
|
|
||||||
path: 'agenda/recorrencias',
|
|
||||||
name: 'admin-agenda-recorrencias',
|
|
||||||
component: () => import('@/features/agenda/pages/AgendaRecorrenciasPage.vue'),
|
|
||||||
meta: { feature: 'agenda.view', roles: ['clinic_admin', 'tenant_admin'], mode: 'clinic' }
|
|
||||||
},
|
|
||||||
|
|
||||||
// ✅ NOVO: Compromissos determinísticos (tipos)
|
|
||||||
{
|
|
||||||
path: 'agenda/compromissos',
|
|
||||||
name: 'admin-agenda-compromissos',
|
|
||||||
component: () => import('@/features/agenda/pages/CompromissosDeterminados.vue'),
|
|
||||||
meta: {
|
|
||||||
feature: 'agenda.view',
|
|
||||||
roles: ['clinic_admin', 'tenant_admin']
|
|
||||||
// ✅ sem tenantScope: a área /admin já está no tenant da clínica pelo fluxo normal
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// 👥 PACIENTES (módulo ativável por clínica)
|
|
||||||
// ======================================================
|
|
||||||
|
|
||||||
// 📋 Lista de pacientes
|
|
||||||
{
|
|
||||||
path: 'pacientes',
|
|
||||||
name: 'admin-pacientes',
|
|
||||||
component: () => import('@/features/patients/PatientsListPage.vue'),
|
|
||||||
meta: {
|
|
||||||
// ✅ depende do tenant_features.patients
|
|
||||||
tenantFeature: 'patients'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// ➕ Cadastro de paciente (novo)
|
|
||||||
{
|
|
||||||
path: 'pacientes/cadastro',
|
|
||||||
name: 'admin-pacientes-cadastro',
|
|
||||||
component: () => import('@/features/patients/cadastro/PatientsCadastroPage.vue'),
|
|
||||||
meta: {
|
|
||||||
tenantFeature: 'patients'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// ✏️ Editar paciente
|
|
||||||
{
|
|
||||||
path: 'pacientes/cadastro/:id',
|
|
||||||
name: 'admin-pacientes-cadastro-edit',
|
|
||||||
component: () => import('@/features/patients/cadastro/PatientsCadastroPage.vue'),
|
|
||||||
props: true,
|
|
||||||
meta: {
|
|
||||||
tenantFeature: 'patients'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 👥 Grupos de pacientes
|
|
||||||
{
|
|
||||||
path: 'pacientes/grupos',
|
|
||||||
name: 'admin-pacientes-grupos',
|
|
||||||
component: () => import('@/features/patients/grupos/GruposPacientesPage.vue'),
|
|
||||||
meta: {
|
|
||||||
tenantFeature: 'patients'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 🏷️ Tags de pacientes
|
|
||||||
{
|
|
||||||
path: 'pacientes/tags',
|
|
||||||
name: 'admin-pacientes-tags',
|
|
||||||
component: () => import('@/features/patients/tags/TagsPage.vue'),
|
|
||||||
meta: {
|
|
||||||
tenantFeature: 'patients'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 🔗 Link externo para cadastro
|
|
||||||
{
|
|
||||||
path: 'pacientes/link-externo',
|
|
||||||
name: 'admin-pacientes-link-externo',
|
|
||||||
component: () => import('@/features/patients/cadastro/PatientsExternalLinkPage.vue'),
|
|
||||||
meta: {
|
|
||||||
tenantFeature: 'patients'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 📥 Cadastros recebidos via link externo
|
|
||||||
{
|
|
||||||
path: 'pacientes/cadastro/recebidos',
|
|
||||||
name: 'admin-pacientes-recebidos',
|
|
||||||
component: () => import('@/features/patients/cadastro/recebidos/CadastrosRecebidosPage.vue'),
|
|
||||||
meta: {
|
|
||||||
tenantFeature: 'patients'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// 🔐 SEGURANÇA
|
|
||||||
// ======================================================
|
|
||||||
{
|
|
||||||
path: 'settings/security',
|
|
||||||
name: 'admin-settings-security',
|
|
||||||
component: () => import('@/views/pages/auth/SecurityPage.vue')
|
|
||||||
},
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// 🔒 MÓDULO PRO — Online Scheduling
|
|
||||||
// ======================================================
|
|
||||||
{
|
|
||||||
path: 'online-scheduling',
|
|
||||||
name: 'admin-online-scheduling',
|
|
||||||
component: () => import('@/views/pages/clinic/OnlineSchedulingAdminPage.vue'),
|
|
||||||
meta: {
|
|
||||||
feature: 'online_scheduling.manage'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// 🔒 PRO — Agendamentos Recebidos
|
|
||||||
// ======================================================
|
|
||||||
{
|
|
||||||
path: 'agendamentos-recebidos',
|
|
||||||
name: 'admin-agendamentos-recebidos',
|
|
||||||
component: () => import('@/features/agenda/pages/AgendamentosRecebidosPage.vue'),
|
|
||||||
meta: {
|
|
||||||
feature: 'online_scheduling.manage'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// 💰 FINANCEIRO
|
|
||||||
// ======================================================
|
|
||||||
{
|
|
||||||
path: 'financeiro',
|
|
||||||
name: 'admin-financeiro',
|
|
||||||
component: () => import('@/features/financeiro/pages/FinanceiroDashboardPage.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'financeiro/lancamentos',
|
|
||||||
name: 'admin-financeiro-lancamentos',
|
|
||||||
component: () => import('@/features/financeiro/pages/FinanceiroPage.vue')
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ── Rotas de app — serão filhas do AppLayout compartilhado no index.js ────
|
||||||
|
export default {
|
||||||
|
path: 'admin',
|
||||||
|
component: RouterPassthrough,
|
||||||
|
meta: {
|
||||||
|
area: 'admin',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['clinic_admin']
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
// ======================================================
|
||||||
|
// 📊 DASHBOARD
|
||||||
|
// ======================================================
|
||||||
|
{ path: '', name: 'admin.dashboard', component: () => import('@/views/pages/clinic/ClinicDashboard.vue') },
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// 🧩 CLÍNICA — MÓDULOS (tenant_features)
|
||||||
|
// ======================================================
|
||||||
|
{
|
||||||
|
path: 'clinic/features',
|
||||||
|
name: 'admin-clinic-features',
|
||||||
|
component: () => import('@/views/pages/clinic/clinic/ClinicFeaturesPage.vue'),
|
||||||
|
meta: { roles: ['clinic_admin', 'tenant_admin'] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'clinic/professionals',
|
||||||
|
name: 'admin-clinic-professionals',
|
||||||
|
component: () => import('@/views/pages/clinic/clinic/ClinicProfessionalsPage.vue'),
|
||||||
|
meta: { requiresAuth: true, roles: ['clinic_admin', 'tenant_admin'] }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// 💳 MEU PLANO
|
||||||
|
// ======================================================
|
||||||
|
{
|
||||||
|
path: 'meu-plano',
|
||||||
|
name: 'admin-meu-plano',
|
||||||
|
component: () => import('@/views/pages/billing/ClinicMeuPlanoPage.vue')
|
||||||
|
},
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// 📅 AGENDA DA CLÍNICA
|
||||||
|
// ======================================================
|
||||||
|
{
|
||||||
|
path: 'agenda/clinica',
|
||||||
|
name: 'admin-agenda-clinica',
|
||||||
|
component: () => import('@/features/agenda/pages/AgendaClinicaPage.vue'),
|
||||||
|
meta: { feature: 'agenda.view', roles: ['clinic_admin', 'tenant_admin'] }
|
||||||
|
},
|
||||||
|
|
||||||
|
// Recorrências
|
||||||
|
{
|
||||||
|
path: 'agenda/recorrencias',
|
||||||
|
name: 'admin-agenda-recorrencias',
|
||||||
|
component: () => import('@/features/agenda/pages/AgendaRecorrenciasPage.vue'),
|
||||||
|
meta: { feature: 'agenda.view', roles: ['clinic_admin', 'tenant_admin'], mode: 'clinic' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ✅ Compromissos determinísticos
|
||||||
|
{
|
||||||
|
path: 'agenda/compromissos',
|
||||||
|
name: 'admin-agenda-compromissos',
|
||||||
|
component: () => import('@/features/agenda/pages/CompromissosDeterminados.vue'),
|
||||||
|
meta: { feature: 'agenda.view', roles: ['clinic_admin', 'tenant_admin'] }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// 👥 PACIENTES
|
||||||
|
// ======================================================
|
||||||
|
{
|
||||||
|
path: 'pacientes',
|
||||||
|
name: 'admin-pacientes',
|
||||||
|
component: () => import('@/features/patients/PatientsListPage.vue'),
|
||||||
|
meta: { tenantFeature: 'patients' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'pacientes/cadastro',
|
||||||
|
name: 'admin-pacientes-cadastro',
|
||||||
|
component: () => import('@/features/patients/cadastro/PatientsCadastroPage.vue'),
|
||||||
|
meta: { tenantFeature: 'patients' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'pacientes/cadastro/:id',
|
||||||
|
name: 'admin-pacientes-cadastro-edit',
|
||||||
|
component: () => import('@/features/patients/cadastro/PatientsCadastroPage.vue'),
|
||||||
|
props: true,
|
||||||
|
meta: { tenantFeature: 'patients' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'pacientes/grupos',
|
||||||
|
name: 'admin-pacientes-grupos',
|
||||||
|
component: () => import('@/features/patients/grupos/GruposPacientesPage.vue'),
|
||||||
|
meta: { tenantFeature: 'patients' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'pacientes/tags',
|
||||||
|
name: 'admin-pacientes-tags',
|
||||||
|
component: () => import('@/features/patients/tags/TagsPage.vue'),
|
||||||
|
meta: { tenantFeature: 'patients' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'pacientes/link-externo',
|
||||||
|
name: 'admin-pacientes-link-externo',
|
||||||
|
component: () => import('@/features/patients/cadastro/PatientsExternalLinkPage.vue'),
|
||||||
|
meta: { tenantFeature: 'patients' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'pacientes/cadastro/recebidos',
|
||||||
|
name: 'admin-pacientes-recebidos',
|
||||||
|
component: () => import('@/features/patients/cadastro/recebidos/CadastrosRecebidosPage.vue'),
|
||||||
|
meta: { tenantFeature: 'patients' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// 🔐 SEGURANÇA
|
||||||
|
// ======================================================
|
||||||
|
{
|
||||||
|
path: 'settings/security',
|
||||||
|
name: 'admin-settings-security',
|
||||||
|
component: () => import('@/views/pages/auth/SecurityPage.vue')
|
||||||
|
},
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// 🔒 MÓDULO PRO — Online Scheduling
|
||||||
|
// ======================================================
|
||||||
|
{
|
||||||
|
path: 'online-scheduling',
|
||||||
|
name: 'admin-online-scheduling',
|
||||||
|
component: () => import('@/views/pages/clinic/OnlineSchedulingAdminPage.vue'),
|
||||||
|
meta: { feature: 'online_scheduling.manage' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// 🔒 PRO — Agendamentos Recebidos
|
||||||
|
// ======================================================
|
||||||
|
{
|
||||||
|
path: 'agendamentos-recebidos',
|
||||||
|
name: 'admin-agendamentos-recebidos',
|
||||||
|
component: () => import('@/features/agenda/pages/AgendamentosRecebidosPage.vue'),
|
||||||
|
meta: { feature: 'online_scheduling.manage' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// 💰 FINANCEIRO
|
||||||
|
// ======================================================
|
||||||
|
{
|
||||||
|
path: 'financeiro',
|
||||||
|
name: 'admin-financeiro',
|
||||||
|
component: () => import('@/features/financeiro/pages/FinanceiroDashboardPage.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'financeiro/lancamentos',
|
||||||
|
name: 'admin-financeiro-lancamentos',
|
||||||
|
component: () => import('@/features/financeiro/pages/FinanceiroPage.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|||||||
@@ -14,11 +14,13 @@
|
|||||||
| © 2026 — Todos os direitos reservados
|
| © 2026 — Todos os direitos reservados
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
import AppLayout from '@/layout/AppLayout.vue';
|
|
||||||
|
|
||||||
const configuracoesRoutes = {
|
// ConfiguracoesPage já tem <router-view> próprio — serve de layout intermediário.
|
||||||
path: '/configuracoes',
|
// Não precisa de RouterPassthrough.
|
||||||
component: AppLayout,
|
export default {
|
||||||
|
path: 'configuracoes',
|
||||||
|
component: () => import('@/layout/ConfiguracoesPage.vue'),
|
||||||
|
redirect: { name: 'ConfiguracoesAgenda' },
|
||||||
|
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
@@ -27,94 +29,84 @@ const configuracoesRoutes = {
|
|||||||
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: 'agenda',
|
||||||
component: () => import('@/layout/ConfiguracoesPage.vue'),
|
name: 'ConfiguracoesAgenda',
|
||||||
redirect: { name: 'ConfiguracoesAgenda' },
|
component: () => import('@/layout/configuracoes/ConfiguracoesAgendaPage.vue')
|
||||||
|
},
|
||||||
children: [
|
{
|
||||||
{
|
path: 'bloqueios',
|
||||||
path: 'agenda',
|
name: 'ConfiguracoesBloqueios',
|
||||||
name: 'ConfiguracoesAgenda',
|
component: () => import('@/layout/configuracoes/BloqueiosPage.vue')
|
||||||
component: () => import('@/layout/configuracoes/ConfiguracoesAgendaPage.vue')
|
},
|
||||||
},
|
{
|
||||||
{
|
path: 'agendador',
|
||||||
path: 'bloqueios',
|
name: 'ConfiguracoesAgendador',
|
||||||
name: 'ConfiguracoesBloqueios',
|
component: () => import('@/layout/configuracoes/ConfiguracoesAgendadorPage.vue')
|
||||||
component: () => import('@/layout/configuracoes/BloqueiosPage.vue')
|
},
|
||||||
},
|
{
|
||||||
{
|
path: 'pagamento',
|
||||||
path: 'agendador',
|
name: 'ConfiguracoesPagamento',
|
||||||
name: 'ConfiguracoesAgendador',
|
component: () => import('@/layout/configuracoes/ConfiguracoesPagamentoPage.vue')
|
||||||
component: () => import('@/layout/configuracoes/ConfiguracoesAgendadorPage.vue')
|
},
|
||||||
},
|
{
|
||||||
{
|
path: 'precificacao',
|
||||||
path: 'pagamento',
|
name: 'ConfiguracoesPrecificacao',
|
||||||
name: 'ConfiguracoesPagamento',
|
component: () => import('@/layout/configuracoes/ConfiguracoesPrecificacaoPage.vue')
|
||||||
component: () => import('@/layout/configuracoes/ConfiguracoesPagamentoPage.vue')
|
},
|
||||||
},
|
{
|
||||||
{
|
path: 'descontos',
|
||||||
path: 'precificacao',
|
name: 'ConfiguracoesDescontos',
|
||||||
name: 'ConfiguracoesPrecificacao',
|
component: () => import('@/layout/configuracoes/ConfiguracoesDescontosPage.vue')
|
||||||
component: () => import('@/layout/configuracoes/ConfiguracoesPrecificacaoPage.vue')
|
},
|
||||||
},
|
{
|
||||||
{
|
path: 'excecoes-financeiras',
|
||||||
path: 'descontos',
|
name: 'ConfiguracoesExcecoesFinanceiras',
|
||||||
name: 'ConfiguracoesDescontos',
|
component: () => import('@/layout/configuracoes/ConfiguracoesExcecoesFinanceirasPage.vue')
|
||||||
component: () => import('@/layout/configuracoes/ConfiguracoesDescontosPage.vue')
|
},
|
||||||
},
|
{
|
||||||
{
|
path: 'convenios',
|
||||||
path: 'excecoes-financeiras',
|
name: 'ConfiguracoesConvenios',
|
||||||
name: 'ConfiguracoesExcecoesFinanceiras',
|
component: () => import('@/layout/configuracoes/ConfiguracoesConveniosPage.vue')
|
||||||
component: () => import('@/layout/configuracoes/ConfiguracoesExcecoesFinanceirasPage.vue')
|
},
|
||||||
},
|
{
|
||||||
{
|
path: 'email-templates',
|
||||||
path: 'convenios',
|
name: 'ConfiguracoesEmailTemplates',
|
||||||
name: 'ConfiguracoesConvenios',
|
component: () => import('@/layout/configuracoes/ConfiguracoesEmailTemplatesPage.vue')
|
||||||
component: () => import('@/layout/configuracoes/ConfiguracoesConveniosPage.vue')
|
},
|
||||||
},
|
{
|
||||||
{
|
path: 'empresa',
|
||||||
path: 'email-templates',
|
name: 'ConfiguracoesMinhaEmpresa',
|
||||||
name: 'ConfiguracoesEmailTemplates',
|
component: () => import('@/layout/configuracoes/ConfiguracoesMinhaEmpresaPage.vue')
|
||||||
component: () => import('@/layout/configuracoes/ConfiguracoesEmailTemplatesPage.vue')
|
},
|
||||||
},
|
{
|
||||||
{
|
path: 'canais',
|
||||||
path: 'empresa',
|
name: 'ConfiguracoesCanais',
|
||||||
name: 'ConfiguracoesMinhaEmpresa',
|
component: () => import('@/layout/configuracoes/ConfiguracoesCanaisPage.vue')
|
||||||
component: () => import('@/layout/configuracoes/ConfiguracoesMinhaEmpresaPage.vue')
|
},
|
||||||
},
|
{
|
||||||
{
|
path: 'whatsapp',
|
||||||
path: 'canais',
|
name: 'ConfiguracoesWhatsapp',
|
||||||
name: 'ConfiguracoesCanais',
|
component: () => import('@/layout/configuracoes/ConfiguracoesWhatsappPage.vue')
|
||||||
component: () => import('@/layout/configuracoes/ConfiguracoesCanaisPage.vue')
|
},
|
||||||
},
|
{
|
||||||
{
|
path: 'whatsapp-twilio',
|
||||||
path: 'whatsapp',
|
name: 'ConfiguracoesWhatsappTwilio',
|
||||||
name: 'ConfiguracoesWhatsapp',
|
component: () => import('@/layout/configuracoes/ConfiguracoesTwilioWhatsappPage.vue')
|
||||||
component: () => import('@/layout/configuracoes/ConfiguracoesWhatsappPage.vue')
|
},
|
||||||
},
|
{
|
||||||
{
|
path: 'sms',
|
||||||
path: 'whatsapp-twilio',
|
name: 'ConfiguracoesSms',
|
||||||
name: 'ConfiguracoesWhatsappTwilio',
|
component: () => import('@/layout/configuracoes/ConfiguracoesSmsPage.vue')
|
||||||
component: () => import('@/layout/configuracoes/ConfiguracoesTwilioWhatsappPage.vue')
|
},
|
||||||
},
|
{
|
||||||
{
|
path: 'sms-canal',
|
||||||
path: 'sms',
|
name: 'ConfiguracoesSmsCanal',
|
||||||
name: 'ConfiguracoesSms',
|
component: () => import('@/views/pages/notifications/SmsChannelSetupPage.vue')
|
||||||
component: () => import('@/layout/configuracoes/ConfiguracoesSmsPage.vue')
|
},
|
||||||
},
|
{
|
||||||
{
|
path: 'recursos-extras',
|
||||||
path: 'sms-canal',
|
name: 'ConfiguracoesRecursosExtras',
|
||||||
name: 'ConfiguracoesSmsCanal',
|
component: () => import('@/layout/configuracoes/ConfiguracoesRecursosExtrasPage.vue')
|
||||||
component: () => import('@/views/pages/notifications/SmsChannelSetupPage.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'recursos-extras',
|
|
||||||
name: 'ConfiguracoesRecursosExtras',
|
|
||||||
component: () => import('@/layout/configuracoes/ConfiguracoesRecursosExtrasPage.vue')
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
export default configuracoesRoutes;
|
|
||||||
|
|||||||
@@ -14,18 +14,17 @@
|
|||||||
| © 2026 — Todos os direitos reservados
|
| © 2026 — Todos os direitos reservados
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
import AppLayout from '@/layout/AppLayout.vue';
|
import RouterPassthrough from '@/layout/RouterPassthrough.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
path: '/features',
|
path: 'features',
|
||||||
component: AppLayout,
|
component: RouterPassthrough,
|
||||||
meta: { requiresAuth: true }, // roles: se você quiser travar aqui também
|
meta: { requiresAuth: true },
|
||||||
children: [
|
children: [
|
||||||
// Patients
|
|
||||||
{
|
{
|
||||||
path: 'patients',
|
path: 'patients',
|
||||||
name: 'features.patients.list',
|
name: 'features.patients.list',
|
||||||
component: () => import('@/features/patients/PatientsListPage.vue') // ajuste se seu arquivo tiver outro nome
|
component: () => import('@/features/patients/PatientsListPage.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'patients/cadastro',
|
path: 'patients/cadastro',
|
||||||
|
|||||||
@@ -14,14 +14,12 @@
|
|||||||
| © 2026 — Todos os direitos reservados
|
| © 2026 — Todos os direitos reservados
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
import AppLayout from '@/layout/AppLayout.vue';
|
import RouterPassthrough from '@/layout/RouterPassthrough.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
path: '/supervisor',
|
path: 'supervisor',
|
||||||
component: AppLayout,
|
component: RouterPassthrough,
|
||||||
|
|
||||||
// tenantScope: 'supervisor' → o guard troca automaticamente para o tenant
|
|
||||||
// com kind='supervisor' quando o usuário navega para esta área.
|
|
||||||
meta: {
|
meta: {
|
||||||
area: 'supervisor',
|
area: 'supervisor',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
|
|||||||
@@ -14,173 +14,164 @@
|
|||||||
| © 2026 — Todos os direitos reservados
|
| © 2026 — Todos os direitos reservados
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
import AppLayout from '@/layout/AppLayout.vue';
|
import RouterPassthrough from '@/layout/RouterPassthrough.vue';
|
||||||
|
|
||||||
export default [
|
// ── Rotas fullscreen — ficam fora do AppLayout compartilhado ──────────────
|
||||||
// ======================================================
|
export const therapistStandalone = [
|
||||||
// 🚀 SETUP WIZARD — fora do AppLayout (fullscreen)
|
|
||||||
// ======================================================
|
|
||||||
{
|
{
|
||||||
path: '/therapist/setup',
|
path: '/therapist/setup',
|
||||||
name: 'therapist.setup',
|
name: 'therapist.setup',
|
||||||
component: () => import('@/features/setup/SetupWizardPage.vue'),
|
component: () => import('@/features/setup/SetupWizardPage.vue'),
|
||||||
meta: { area: 'therapist', requiresAuth: true, roles: ['therapist'], fullscreen: true }
|
meta: { area: 'therapist', requiresAuth: true, roles: ['therapist'], fullscreen: true }
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
path: '/therapist',
|
|
||||||
component: AppLayout,
|
|
||||||
|
|
||||||
meta: { area: 'therapist', requiresAuth: true, roles: ['therapist'] },
|
|
||||||
|
|
||||||
children: [
|
|
||||||
// ======================================================
|
|
||||||
// 📊 DASHBOARD
|
|
||||||
// ======================================================
|
|
||||||
{ path: '', name: 'therapist.dashboard', component: () => import('@/views/pages/therapist/TherapistDashboard.vue') },
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// 📅 AGENDA
|
|
||||||
// ======================================================
|
|
||||||
{
|
|
||||||
path: 'agenda',
|
|
||||||
name: 'therapist-agenda',
|
|
||||||
component: () => import('@/features/agenda/pages/AgendaTerapeutaPage.vue'),
|
|
||||||
meta: {
|
|
||||||
feature: 'agenda.view'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Recorrências
|
|
||||||
{
|
|
||||||
path: 'agenda/recorrencias',
|
|
||||||
name: 'therapist-agenda-recorrencias',
|
|
||||||
component: () => import('@/features/agenda/pages/AgendaRecorrenciasPage.vue'),
|
|
||||||
meta: { feature: 'agenda.view', mode: 'therapist' }
|
|
||||||
},
|
|
||||||
|
|
||||||
// ✅ Compromissos determinísticos
|
|
||||||
{
|
|
||||||
path: 'agenda/compromissos',
|
|
||||||
name: 'therapist-agenda-compromissos',
|
|
||||||
component: () => import('@/features/agenda/pages/CompromissosDeterminados.vue'),
|
|
||||||
meta: {
|
|
||||||
feature: 'agenda.view',
|
|
||||||
roles: ['therapist']
|
|
||||||
// ✅ sem tenantScope
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// 💳 MEU PLANO
|
|
||||||
// ======================================================
|
|
||||||
{
|
|
||||||
path: 'meu-plano',
|
|
||||||
name: 'therapist-meu-plano',
|
|
||||||
component: () => import('@/views/pages/billing/TherapistMeuPlanoPage.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'upgrade',
|
|
||||||
name: 'therapist-upgrade',
|
|
||||||
component: () => import('@/views/pages/billing/TherapistUpgradePage.vue')
|
|
||||||
},
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// 👥 PATIENTS
|
|
||||||
// ======================================================
|
|
||||||
{
|
|
||||||
path: 'patients',
|
|
||||||
name: 'therapist-patients',
|
|
||||||
component: () => import('@/features/patients/PatientsListPage.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'patients/cadastro',
|
|
||||||
name: 'therapist-patients-create',
|
|
||||||
component: () => import('@/features/patients/cadastro/PatientsCadastroPage.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'patients/cadastro/:id',
|
|
||||||
name: 'therapist-patients-edit',
|
|
||||||
component: () => import('@/features/patients/cadastro/PatientsCadastroPage.vue'),
|
|
||||||
props: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'patients/grupos',
|
|
||||||
name: 'therapist-patients-groups',
|
|
||||||
component: () => import('@/features/patients/grupos/GruposPacientesPage.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'patients/tags',
|
|
||||||
name: 'therapist-patients-tags',
|
|
||||||
component: () => import('@/features/patients/tags/TagsPage.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'patients/link-externo',
|
|
||||||
name: 'therapist-patients-link-externo',
|
|
||||||
component: () => import('@/features/patients/cadastro/PatientsExternalLinkPage.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'patients/cadastro/recebidos',
|
|
||||||
name: 'therapist-patients-recebidos',
|
|
||||||
component: () => import('@/features/patients/cadastro/recebidos/CadastrosRecebidosPage.vue')
|
|
||||||
},
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// 🔒 PRO — Online Scheduling
|
|
||||||
// ======================================================
|
|
||||||
{
|
|
||||||
path: 'online-scheduling',
|
|
||||||
name: 'therapist-online-scheduling',
|
|
||||||
component: () => import('@/views/pages/therapist/OnlineSchedulingPage.vue'),
|
|
||||||
meta: {
|
|
||||||
feature: 'online_scheduling.manage'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// 🔒 PRO — Agendamentos Recebidos
|
|
||||||
// ======================================================
|
|
||||||
{
|
|
||||||
path: 'agendamentos-recebidos',
|
|
||||||
name: 'therapist-agendamentos-recebidos',
|
|
||||||
component: () => import('@/features/agenda/pages/AgendamentosRecebidosPage.vue'),
|
|
||||||
meta: {
|
|
||||||
feature: 'online_scheduling.manage'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// 💰 FINANCEIRO
|
|
||||||
// ======================================================
|
|
||||||
{
|
|
||||||
path: 'financeiro',
|
|
||||||
name: 'therapist-financeiro',
|
|
||||||
component: () => import('@/features/financeiro/pages/FinanceiroDashboardPage.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'financeiro/lancamentos',
|
|
||||||
name: 'therapist-financeiro-lancamentos',
|
|
||||||
component: () => import('@/features/financeiro/pages/FinanceiroPage.vue')
|
|
||||||
},
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// 📈 RELATÓRIOS
|
|
||||||
// ======================================================
|
|
||||||
{
|
|
||||||
path: 'relatorios',
|
|
||||||
name: 'therapist-relatorios',
|
|
||||||
component: () => import('@/views/pages/therapist/RelatoriosPage.vue'),
|
|
||||||
meta: { feature: 'agenda.view' }
|
|
||||||
},
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// 🔐 SECURITY
|
|
||||||
// ======================================================
|
|
||||||
{
|
|
||||||
path: 'settings/security',
|
|
||||||
name: 'therapist-settings-security',
|
|
||||||
component: () => import('@/views/pages/auth/SecurityPage.vue')
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ── Rotas de app — serão filhas do AppLayout compartilhado no index.js ────
|
||||||
|
export default {
|
||||||
|
path: 'therapist',
|
||||||
|
component: RouterPassthrough,
|
||||||
|
meta: { area: 'therapist', requiresAuth: true, roles: ['therapist'] },
|
||||||
|
|
||||||
|
children: [
|
||||||
|
// ======================================================
|
||||||
|
// 📊 DASHBOARD
|
||||||
|
// ======================================================
|
||||||
|
{ path: '', name: 'therapist.dashboard', component: () => import('@/views/pages/therapist/TherapistDashboard.vue') },
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// 📅 AGENDA
|
||||||
|
// ======================================================
|
||||||
|
{
|
||||||
|
path: 'agenda',
|
||||||
|
name: 'therapist-agenda',
|
||||||
|
component: () => import('@/features/agenda/pages/AgendaTerapeutaPage.vue'),
|
||||||
|
meta: { feature: 'agenda.view' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// Recorrências
|
||||||
|
{
|
||||||
|
path: 'agenda/recorrencias',
|
||||||
|
name: 'therapist-agenda-recorrencias',
|
||||||
|
component: () => import('@/features/agenda/pages/AgendaRecorrenciasPage.vue'),
|
||||||
|
meta: { feature: 'agenda.view', mode: 'therapist' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ✅ Compromissos determinísticos
|
||||||
|
{
|
||||||
|
path: 'agenda/compromissos',
|
||||||
|
name: 'therapist-agenda-compromissos',
|
||||||
|
component: () => import('@/features/agenda/pages/CompromissosDeterminados.vue'),
|
||||||
|
meta: {
|
||||||
|
feature: 'agenda.view',
|
||||||
|
roles: ['therapist']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// 💳 MEU PLANO
|
||||||
|
// ======================================================
|
||||||
|
{
|
||||||
|
path: 'meu-plano',
|
||||||
|
name: 'therapist-meu-plano',
|
||||||
|
component: () => import('@/views/pages/billing/TherapistMeuPlanoPage.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'upgrade',
|
||||||
|
name: 'therapist-upgrade',
|
||||||
|
component: () => import('@/views/pages/billing/TherapistUpgradePage.vue')
|
||||||
|
},
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// 👥 PATIENTS
|
||||||
|
// ======================================================
|
||||||
|
{
|
||||||
|
path: 'patients',
|
||||||
|
name: 'therapist-patients',
|
||||||
|
component: () => import('@/features/patients/PatientsListPage.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'patients/cadastro',
|
||||||
|
name: 'therapist-patients-create',
|
||||||
|
component: () => import('@/features/patients/cadastro/PatientsCadastroPage.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'patients/cadastro/:id',
|
||||||
|
name: 'therapist-patients-edit',
|
||||||
|
component: () => import('@/features/patients/cadastro/PatientsCadastroPage.vue'),
|
||||||
|
props: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'patients/grupos',
|
||||||
|
name: 'therapist-patients-groups',
|
||||||
|
component: () => import('@/features/patients/grupos/GruposPacientesPage.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'patients/tags',
|
||||||
|
name: 'therapist-patients-tags',
|
||||||
|
component: () => import('@/features/patients/tags/TagsPage.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'patients/link-externo',
|
||||||
|
name: 'therapist-patients-link-externo',
|
||||||
|
component: () => import('@/features/patients/cadastro/PatientsExternalLinkPage.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'patients/cadastro/recebidos',
|
||||||
|
name: 'therapist-patients-recebidos',
|
||||||
|
component: () => import('@/features/patients/cadastro/recebidos/CadastrosRecebidosPage.vue')
|
||||||
|
},
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// 🔒 PRO — Online Scheduling
|
||||||
|
// ======================================================
|
||||||
|
{
|
||||||
|
path: 'online-scheduling',
|
||||||
|
name: 'therapist-online-scheduling',
|
||||||
|
component: () => import('@/views/pages/therapist/OnlineSchedulingPage.vue'),
|
||||||
|
meta: { feature: 'online_scheduling.manage' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// 🔒 PRO — Agendamentos Recebidos
|
||||||
|
// ======================================================
|
||||||
|
{
|
||||||
|
path: 'agendamentos-recebidos',
|
||||||
|
name: 'therapist-agendamentos-recebidos',
|
||||||
|
component: () => import('@/features/agenda/pages/AgendamentosRecebidosPage.vue'),
|
||||||
|
meta: { feature: 'online_scheduling.manage' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// 💰 FINANCEIRO
|
||||||
|
// ======================================================
|
||||||
|
{
|
||||||
|
path: 'financeiro',
|
||||||
|
name: 'therapist-financeiro',
|
||||||
|
component: () => import('@/features/financeiro/pages/FinanceiroDashboardPage.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'financeiro/lancamentos',
|
||||||
|
name: 'therapist-financeiro-lancamentos',
|
||||||
|
component: () => import('@/features/financeiro/pages/FinanceiroPage.vue')
|
||||||
|
},
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// 📈 RELATÓRIOS
|
||||||
|
// ======================================================
|
||||||
|
{
|
||||||
|
path: 'relatorios',
|
||||||
|
name: 'therapist-relatorios',
|
||||||
|
component: () => import('@/views/pages/therapist/RelatoriosPage.vue'),
|
||||||
|
meta: { feature: 'agenda.view' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// 🔐 SECURITY
|
||||||
|
// ======================================================
|
||||||
|
{
|
||||||
|
path: 'settings/security',
|
||||||
|
name: 'therapist-settings-security',
|
||||||
|
component: () => import('@/views/pages/auth/SecurityPage.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import vue from '@vitejs/plugin-vue';
|
|||||||
import { visualizer } from 'rollup-plugin-visualizer'; // bundle analyzer
|
import { visualizer } from 'rollup-plugin-visualizer'; // bundle analyzer
|
||||||
import AutoImport from 'unplugin-auto-import/vite'; // auto import Vue composables
|
import AutoImport from 'unplugin-auto-import/vite'; // auto import Vue composables
|
||||||
import Components from 'unplugin-vue-components/vite';
|
import Components from 'unplugin-vue-components/vite';
|
||||||
|
import viteCompression from 'vite-plugin-compression'; // 🔥 compressão gzip/brotli
|
||||||
import { defineConfig } from 'vitest/config';
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
@@ -36,11 +37,15 @@ export default defineConfig({
|
|||||||
|
|
||||||
// Visualizador de bundle (gera stats.html no build)
|
// Visualizador de bundle (gera stats.html no build)
|
||||||
visualizer({
|
visualizer({
|
||||||
filename: 'dist/stats-before.html', // depois você muda pra stats-after.html
|
filename: 'dist/stats-before.html',
|
||||||
open: true,
|
open: true,
|
||||||
gzipSize: true,
|
gzipSize: true,
|
||||||
brotliSize: true
|
brotliSize: true
|
||||||
})
|
}),
|
||||||
|
|
||||||
|
// 🔥 Gzip e Brotli
|
||||||
|
viteCompression({ algorithm: 'gzip', ext: '.gz', threshold: 1024 }),
|
||||||
|
viteCompression({ algorithm: 'brotliCompress', ext: '.br', threshold: 1024 })
|
||||||
],
|
],
|
||||||
|
|
||||||
resolve: {
|
resolve: {
|
||||||
@@ -55,5 +60,19 @@ export default defineConfig({
|
|||||||
api: 'modern-compiler'
|
api: 'modern-compiler'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
vue: ['vue'],
|
||||||
|
primevue: ['primevue'],
|
||||||
|
chart: ['chart.js'],
|
||||||
|
supabase: ['@supabase/supabase-js'],
|
||||||
|
editor: ['quill']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user