Compare commits
40 Commits
4.0.0
...
676042268b
| Author | SHA1 | Date | |
|---|---|---|---|
| 676042268b | |||
| ec6b6ef53a | |||
| 76a3b60333 | |||
| 410c08d693 | |||
| a4b2c96b0d | |||
| a47200fdf7 | |||
| 7c32ae1f6f | |||
| db99863fac | |||
| c2ef85fcab | |||
| deea8861f8 | |||
| 319f976d2b | |||
| 13a50a3af3 | |||
| 23bcf922ab | |||
| e1ecd23050 | |||
| 2f5b71a3eb | |||
| 22ba8601f3 | |||
| 4a8745b497 | |||
| d5ec7dba67 | |||
| 7c54176132 | |||
| 03ef1236f0 | |||
| 7ac2ba9013 | |||
| 5f951584c7 | |||
| 817ffa0d62 | |||
| 9585f62298 | |||
| aad48fca63 | |||
| 59f3ebffe7 | |||
| 6fd2e4d96e | |||
| 1c65a74541 | |||
| a5aafc1d34 | |||
| 0f42b3760d | |||
| c4dec65f2a | |||
| 6f85c751de | |||
| 411fecb517 | |||
| 4c7b0c0f5d | |||
| 3ba6d75db2 | |||
| b00460a670 | |||
| 51c93b25d0 | |||
| c4c2d47d54 | |||
| cb0f98582a | |||
| fa5009486a |
@@ -0,0 +1,2 @@
|
|||||||
|
VITE_SUPABASE_URL=http://127.0.0.1:54321
|
||||||
|
VITE_SUPABASE_ANON_KEY=sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
VITE_SUPABASE_URL=http://127.0.0.1:54321
|
||||||
|
VITE_SUPABASE_ANON_KEY=sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH
|
||||||
@@ -5,10 +5,12 @@ coverage
|
|||||||
.nitro
|
.nitro
|
||||||
.cache
|
.cache
|
||||||
.output
|
.output
|
||||||
.env
|
# .env
|
||||||
dist
|
dist
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea
|
.idea
|
||||||
.eslintcache
|
.eslintcache
|
||||||
api-generator/typedoc.json
|
api-generator/typedoc.json
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
|
Dev-documentacao/
|
||||||
|
supabase/
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "src/assets"]
|
||||||
|
path = src/assets
|
||||||
|
url = https://github.com/primefaces/sakai-assets.git
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
### Observação sobre `tenant_admin` com UUID coincidente
|
||||||
|
|
||||||
|
Foi identificado que o registro de `tenant_members` possui:
|
||||||
|
|
||||||
|
- `tenant_id = 816b24fe-a0c3-4409-b79b-c6c0a6935d03`
|
||||||
|
- `user_id = 816b24fe-a0c3-4409-b79b-c6c0a6935d03`
|
||||||
|
- `role = tenant_admin`
|
||||||
|
|
||||||
|
À primeira vista pode parecer inconsistência, mas não é.
|
||||||
|
|
||||||
|
Verificação realizada:
|
||||||
|
O UUID `816b24fe-a0c3-4409-b79b-c6c0a6935d03` existe em `auth.users`
|
||||||
|
(email: admin@agenciapsi.com.br).
|
||||||
|
|
||||||
|
Portanto:
|
||||||
|
|
||||||
|
- `tenant_members.user_id` referencia corretamente `auth.users.id`
|
||||||
|
- Não há violação de integridade referencial
|
||||||
|
- O registro é válido
|
||||||
|
|
||||||
|
Trata-se de um caso em que:
|
||||||
|
|
||||||
|
- O usuário administrador principal possui um UUID específico
|
||||||
|
- O tenant foi criado com o mesmo UUID
|
||||||
|
- O administrador é `tenant_admin` desse próprio tenant
|
||||||
|
|
||||||
|
Esse padrão não quebra a arquitetura multi-tenant e é funcionalmente válido.
|
||||||
|
A coincidência entre `tenant_id` e `user_id` é apenas estrutural, não conceitual.
|
||||||
|
|
||||||
|
Conclusão:
|
||||||
|
Nenhuma correção estrutural é necessária.
|
||||||
@@ -1,5 +1,29 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 4.3.0 (2025-02-26)
|
||||||
|
|
||||||
|
**Implemented New Features and Enhancements**
|
||||||
|
|
||||||
|
- Update PrimeVue version
|
||||||
|
|
||||||
|
## 4.2.0 (2024-12-09)
|
||||||
|
|
||||||
|
**Implemented New Features and Enhancements**
|
||||||
|
|
||||||
|
- Refactored dashboard sections to components
|
||||||
|
- Migrate sass from @import to @use
|
||||||
|
|
||||||
|
## 4.1.0 (2024-07-29)
|
||||||
|
|
||||||
|
- Changed menu button location at topbar
|
||||||
|
- Add border to overlay menu
|
||||||
|
- Animation for mobile mask
|
||||||
|
- Fixed chart colors
|
||||||
|
|
||||||
|
## 4.0.0 (2024-07-29)
|
||||||
|
|
||||||
|
- Updated to PrimeVue v4
|
||||||
|
|
||||||
## 3.10.0 (2024-03-11)
|
## 3.10.0 (2024-03-11)
|
||||||
|
|
||||||
**Migration Guide**
|
**Migration Guide**
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
O que foi feito (até agora)
|
||||||
|
Usuários de teste criados
|
||||||
|
|
||||||
|
admin@agenciapsi.com.br
|
||||||
|
— senha: 123Mudar@
|
||||||
|
|
||||||
|
patient@agenciapsi.com.br
|
||||||
|
— senha: 123Mudar@
|
||||||
|
|
||||||
|
therapist@agenciapsi.com.br
|
||||||
|
— senha: 123Mudar@
|
||||||
|
|
||||||
|
Base funcionando
|
||||||
|
|
||||||
|
✅ Auth (Supabase) está funcionando
|
||||||
|
✅ Tabela profiles criada e ok
|
||||||
|
✅ Trigger automático cria profile após signup
|
||||||
|
✅ Campo role definido (admin | therapist | patient)
|
||||||
|
✅ RLS básico ativo
|
||||||
|
✅ Login funcionando
|
||||||
|
✅ Logout funcionando
|
||||||
|
✅ Guard de rota implementado e ativo
|
||||||
|
✅ RBAC básico operando via meta.role + redirect para painel correto
|
||||||
|
✅ Home pública / com 3 cards (Admin | Therapist | Patient) levando ao login
|
||||||
|
✅ Pós-login: busca profiles.role e redireciona para:
|
||||||
|
|
||||||
|
/admin
|
||||||
|
|
||||||
|
/therapist
|
||||||
|
|
||||||
|
/patient
|
||||||
|
|
||||||
|
Estrutura implementada agora (menus e sessão para o Sakai)
|
||||||
|
Sessão central (evita menu errado e if(role) espalhado)
|
||||||
|
|
||||||
|
✅ Criado src/app/session.js com:
|
||||||
|
|
||||||
|
sessionUser, sessionRole, sessionReady (refs globais)
|
||||||
|
|
||||||
|
initSession() (carrega user + role antes de renderizar o layout)
|
||||||
|
|
||||||
|
listenAuthChanges() (atualiza sessão ao logar/deslogar)
|
||||||
|
|
||||||
|
✅ Ajustado src/main.js para usar bootstrap async:
|
||||||
|
|
||||||
|
chama await initSession() antes de app.mount()
|
||||||
|
|
||||||
|
liga listenAuthChanges()
|
||||||
|
|
||||||
|
mantém PrimeVue, tema Aura, ToastService e ConfirmationService
|
||||||
|
|
||||||
|
mantém imports de CSS existentes
|
||||||
|
|
||||||
|
Menu dinâmico por role no Sakai
|
||||||
|
|
||||||
|
✅ Menus foram estruturados no formato do Sakai (sections com label + items) e separados por role:
|
||||||
|
|
||||||
|
src/navigation/menus/admin.menu.js
|
||||||
|
|
||||||
|
src/navigation/menus/therapist.menu.js
|
||||||
|
|
||||||
|
src/navigation/menus/patient.menu.js
|
||||||
|
|
||||||
|
✅ Criado src/navigation/index.js com getMenuByRole(role) para centralizar a escolha do menu (sem if(role) em componentes).
|
||||||
|
|
||||||
|
✅ Ajustado o AppMenu.vue (menu do Sakai) para:
|
||||||
|
|
||||||
|
usar computed() com sessionRole/sessionReady
|
||||||
|
|
||||||
|
carregar dinamicamente getMenuByRole(sessionRole.value)
|
||||||
|
|
||||||
|
evitar “piscar” menu errado antes de carregar (sessionReady)
|
||||||
|
|
||||||
|
Menu demo do Sakai mantido sem quebrar o produto
|
||||||
|
|
||||||
|
✅ Mantivemos o menu demo (UIKit/Blocks/Start etc.) em arquivo separado para não perder as páginas do template:
|
||||||
|
|
||||||
|
src/navigation/menus/sakai.demo.menu.js (conteúdo original do template)
|
||||||
|
|
||||||
|
✅ Estratégia adotada:
|
||||||
|
|
||||||
|
Admin pode ver o menu demo (idealmente só em DEV)
|
||||||
|
|
||||||
|
Therapist/Patient ficam com menu limpo (clínico)
|
||||||
|
|
||||||
|
Rotas demo do Sakai corrigidas (arquivos com sufixo Doc)
|
||||||
|
|
||||||
|
✅ Problema resolvido: itens do menu demo davam 404 porque as rotas/imports não existiam com os nomes esperados (Input.vue etc.).
|
||||||
|
✅ Ajuste aplicado: rotas demo apontam para arquivos *Doc.vue (ex.: ButtonDoc.vue, InputDoc.vue).
|
||||||
|
|
||||||
|
📌 Criado/ajustado src/router/routes.demo.js para mapear:
|
||||||
|
|
||||||
|
/uikit/* → @/views/uikit/*Doc.vue
|
||||||
|
|
||||||
|
e demais demos conforme existirem
|
||||||
|
|
||||||
|
✅ Incluído demoRoutes no router principal para o menu demo funcionar.
|
||||||
|
|
||||||
|
Testes
|
||||||
|
|
||||||
|
✅ Confirmado que localStorage.clear() limpa sessão para testar outros usuários/roles rapidamente.
|
||||||
@@ -1,29 +1,3 @@
|
|||||||
This template should help get you started developing with Vue 3 in Vite.
|
Sakai is an application template for Vue based on the [create-vue](https://github.com/vuejs/create-vue), the recommended way to start a Vite-powered Vue projects.
|
||||||
|
|
||||||
## Customize configuration
|
Visit the [documentation](https://sakai.primevue.org/documentation) to get started.
|
||||||
|
|
||||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
|
||||||
|
|
||||||
## Project Setup
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compile and Hot-Reload for Development
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compile and Minify for Production
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Lint with [ESLint](https://eslint.org/)
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run lint
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
🔁 CONTEXTO DO PROJETO (SaaS multi-tenant)
|
||||||
|
|
||||||
|
Stack:
|
||||||
|
- Supabase
|
||||||
|
- Multi-tenant por clinic/tenant
|
||||||
|
- Assinaturas por tenant (subscriptions.tenant_id)
|
||||||
|
- Controle de features: features, plan_features, subscription_intents, entitlementsStore, view v_tenant_entitlements
|
||||||
|
- Ativação manual: activate_subscription_from_intent()
|
||||||
|
- Merge concluído: agenda_online → online_scheduling.manage
|
||||||
|
- Entitlements e bloqueio PRO no menu funcionando
|
||||||
|
- Signup + intent funcionando; ativação cria subscription ativa; view retorna feature correta
|
||||||
|
|
||||||
|
Modelo de “Contas” decidido:
|
||||||
|
- Auth user (login) ≠ Clínica (tenant)
|
||||||
|
- Clínica = tenant; Usuário pode ser dono/admin de clínica e também profissional
|
||||||
|
- Clínica convida usuários (tenant_members). Usuário pode aceitar/recusar.
|
||||||
|
- Profissional pode trabalhar anos e depois sair: clínica mantém registros; profissional mantém histórico (audit trail), sem acesso após saída.
|
||||||
|
|
||||||
|
Regras de offboarding:
|
||||||
|
- Profissional só pode sair se NÃO houver agenda futura atribuída a ele.
|
||||||
|
- Se houver, cria “pedido de saída” e admin precisa realocar/cancelar; depois finaliza saída.
|
||||||
|
|
||||||
|
Tabelas existentes:
|
||||||
|
- tenant_members: (id uuid pk, tenant_id uuid, user_id uuid, role text, status text, created_at timestamptz)
|
||||||
|
- UNIQUE (tenant_id, user_id) atualmente
|
||||||
|
- Agenda: agenda_eventos, agenda_excecoes, agenda_configuracoes, agenda_regras_semanais
|
||||||
|
- Outros: subscriptions, subscription_intents, plan_features, features, subscription_events
|
||||||
|
|
||||||
|
O que estamos fazendo agora:
|
||||||
|
- Ajustar modelo de membership lifecycle e offboarding (exit_requests)
|
||||||
|
- Garantir integridade: histórico de vínculos + auditoria + bloqueio de saída com agenda futura
|
||||||
|
- Implementar SQL + RPC + RLS + UI (passo a passo)
|
||||||
|
|
||||||
|
✔ subscriptions
|
||||||
|
Representa o plano da clínica (tenant)
|
||||||
|
✔ tenant_members
|
||||||
|
Define quais usuários pertencem à clínica
|
||||||
|
✔ entitlements
|
||||||
|
|
||||||
|
Define o que aquela clínica pode usar
|
||||||
|
|
||||||
|
Dados que faltam confirmar:
|
||||||
|
1) Estrutura de agenda_eventos (colunas e como relaciona com profissional)
|
||||||
|
2) Valores usados em tenant_members.status (active/invited/etc)
|
||||||
|
3) Estratégia de reentrada: remover UNIQUE (tenant_id,user_id) e usar unique parcial por status ativo/convite
|
||||||
|
4) Se existe tabela public.users como espelho do auth.users
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sakai-vue",
|
"name": "sakai-vue",
|
||||||
"version": "4.0.0",
|
"version": "5.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
@@ -8,26 +8,29 @@
|
|||||||
"lint": "eslint --fix . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
|
"lint": "eslint --fix . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@primevue/themes": "^4.0.0",
|
"@primeuix/themes": "^2.0.0",
|
||||||
|
"@supabase/supabase-js": "^2.95.3",
|
||||||
"chart.js": "3.3.2",
|
"chart.js": "3.3.2",
|
||||||
"primeicons": "^6.0.1",
|
"pinia": "^3.0.4",
|
||||||
"primevue": "^4.0.0",
|
"primeicons": "^7.0.0",
|
||||||
|
"primevue": "^4.5.4",
|
||||||
|
"tailwindcss-primeui": "^0.6.0",
|
||||||
"vue": "^3.4.34",
|
"vue": "^3.4.34",
|
||||||
"vue-router": "^4.4.0"
|
"vue-router": "^4.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@primevue/auto-import-resolver": "^4.0.1",
|
"@primevue/auto-import-resolver": "^4.3.1",
|
||||||
"@rushstack/eslint-patch": "^1.8.0",
|
"@rushstack/eslint-patch": "^1.8.0",
|
||||||
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"@vitejs/plugin-vue": "^5.0.5",
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
"@vue/eslint-config-prettier": "^9.0.0",
|
"@vue/eslint-config-prettier": "^9.0.0",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.24",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-vue": "^9.23.0",
|
"eslint-plugin-vue": "^9.23.0",
|
||||||
"postcss": "^8.4.40",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"sass": "^1.55.0",
|
"sass": "^1.55.0",
|
||||||
"tailwindcss": "^3.4.6",
|
"tailwindcss": "^4.1.18",
|
||||||
"tailwindcss-primeui": "^0.3.2",
|
|
||||||
"unplugin-vue-components": "^0.27.3",
|
"unplugin-vue-components": "^0.27.3",
|
||||||
"vite": "^5.3.1"
|
"vite": "^5.3.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 1009 B |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="300px" height="200px" viewBox="0 0 300 200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>Artboard</title>
|
|
||||||
<g id="Artboard" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<rect id="Rectangle" fill="#F8F9FA" x="0" y="0" width="300" height="200"></rect>
|
|
||||||
<g id="image" transform="translate(110.000000, 70.000000)" fill="#BABABC" fill-rule="nonzero">
|
|
||||||
<path d="M75,0 L5,0 C2.23857625,0 0,2.23857625 0,5 L0,55 C0,57.7614237 2.23857625,60 5,60 L75,60 C77.7614237,60 80,57.7614237 80,55 L80,5 C80,2.23857625 77.7614237,0 75,0 Z M20,10 C25.5228475,10 30,14.4771525 30,20 C30,25.5228475 25.5228475,30 20,30 C14.4771525,30 10,25.5228475 10,20 C10,14.4771525 14.4771525,10 20,10 Z M70,40 L70,50 L10,50 L10,40 L18.55,35.7 C19.4648753,35.2524957 20.5351247,35.2524957 21.45,35.7 L30,40 L53.65,21.1 C54.4866298,20.4991452 55.6133702,20.4991452 56.45,21.1 L70,30 L70,40 Z" id="Shape"></path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
@@ -1,7 +1,25 @@
|
|||||||
<script setup></script>
|
<script setup>
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useTenantStore } from '@/stores/tenantStore'
|
||||||
|
import { useEntitlementsStore } from '@/stores/entitlementsStore'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const tenant = useTenantStore()
|
||||||
|
const ent = useEntitlementsStore()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await tenant.loadSessionAndTenant()
|
||||||
|
await ent.loadForTenant(tenant.activeTenantId)
|
||||||
|
|
||||||
|
// pode remover esses logs depois
|
||||||
|
console.log('tenant.activeTenantId', tenant.activeTenantId)
|
||||||
|
console.log('role', tenant.activeRole)
|
||||||
|
console.log('can online_scheduling.manage?', ent.can('online_scheduling.manage'))
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<router-view />
|
<router-view />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { supabase } from '@/lib/supabase/client'
|
||||||
|
import { useLayout } from '@/layout/composables/layout'
|
||||||
|
import { $t, updatePreset, updateSurfacePalette } from '@primeuix/themes'
|
||||||
|
import Aura from '@primeuix/themes/aura'
|
||||||
|
import Lara from '@primeuix/themes/lara'
|
||||||
|
import Nora from '@primeuix/themes/nora'
|
||||||
|
|
||||||
|
const presets = { Aura, Lara, Nora }
|
||||||
|
|
||||||
|
function safeEq (a, b) {
|
||||||
|
return String(a || '').trim() === String(b || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
// copia do seu getPresetExt (ou exporta ele do Perfil pra reutilizar)
|
||||||
|
function getPresetExt(primaryColors, layoutConfig) {
|
||||||
|
const color = primaryColors.find((c) => c.name === layoutConfig.primary) || { name: 'noir', palette: {} }
|
||||||
|
|
||||||
|
if (color.name === 'noir') {
|
||||||
|
return {
|
||||||
|
semantic: {
|
||||||
|
primary: {
|
||||||
|
50: '{surface.50}', 100: '{surface.100}', 200: '{surface.200}', 300: '{surface.300}',
|
||||||
|
400: '{surface.400}', 500: '{surface.500}', 600: '{surface.600}', 700: '{surface.700}',
|
||||||
|
800: '{surface.800}', 900: '{surface.900}', 950: '{surface.950}'
|
||||||
|
},
|
||||||
|
colorScheme: {
|
||||||
|
light: {
|
||||||
|
primary: { color: '{primary.950}', contrastColor: '#ffffff', hoverColor: '{primary.800}', activeColor: '{primary.700}' },
|
||||||
|
highlight: { background: '{primary.950}', focusBackground: '{primary.700}', color: '#ffffff', focusColor: '#ffffff' }
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
primary: { color: '{primary.50}', contrastColor: '{primary.950}', hoverColor: '{primary.200}', activeColor: '{primary.300}' },
|
||||||
|
highlight: { background: '{primary.50}', focusBackground: '{primary.300}', color: '{primary.950}', focusColor: '{primary.950}' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
semantic: {
|
||||||
|
primary: color.palette,
|
||||||
|
colorScheme: {
|
||||||
|
light: {
|
||||||
|
primary: { color: '{primary.500}', contrastColor: '#ffffff', hoverColor: '{primary.600}', activeColor: '{primary.700}' },
|
||||||
|
highlight: { background: '{primary.50}', focusBackground: '{primary.100}', color: '{primary.700}', focusColor: '{primary.800}' }
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
primary: { color: '{primary.400}', contrastColor: '{surface.900}', hoverColor: '{primary.300}', activeColor: '{primary.200}' },
|
||||||
|
highlight: {
|
||||||
|
background: 'color-mix(in srgb, {primary.400}, transparent 84%)',
|
||||||
|
focusBackground: 'color-mix(in srgb, {primary.400}, transparent 76%)',
|
||||||
|
color: 'rgba(255,255,255,.87)',
|
||||||
|
focusColor: 'rgba(255,255,255,.87)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function bootstrapUserSettings({
|
||||||
|
primaryColors = [], // passe a lista do seu Perfil (ou uma versão reduzida)
|
||||||
|
surfaces = [] // idem
|
||||||
|
} = {}) {
|
||||||
|
const { layoutConfig, isDarkTheme, toggleDarkMode, changeMenuMode } = useLayout()
|
||||||
|
|
||||||
|
const { data: uRes, error: uErr } = await supabase.auth.getUser()
|
||||||
|
if (uErr) return
|
||||||
|
const user = uRes?.user
|
||||||
|
if (!user) return
|
||||||
|
|
||||||
|
const { data: settings, error } = await supabase
|
||||||
|
.from('user_settings')
|
||||||
|
.select('theme_mode, preset, primary_color, surface_color, menu_mode')
|
||||||
|
.eq('user_id', user.id)
|
||||||
|
.maybeSingle()
|
||||||
|
|
||||||
|
if (error || !settings) return
|
||||||
|
|
||||||
|
// menu mode
|
||||||
|
if (settings.menu_mode && settings.menu_mode !== layoutConfig.menuMode) {
|
||||||
|
layoutConfig.menuMode = settings.menu_mode
|
||||||
|
changeMenuMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// preset
|
||||||
|
if (settings.preset && settings.preset !== layoutConfig.preset) {
|
||||||
|
layoutConfig.preset = settings.preset
|
||||||
|
const presetValue = presets[settings.preset] || presets.Aura
|
||||||
|
const surfacePalette = surfaces.find(s => s.name === layoutConfig.surface)?.palette
|
||||||
|
$t().preset(presetValue).preset(getPresetExt(primaryColors, layoutConfig)).surfacePalette(surfacePalette).use({ useDefaultOptions: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// colors
|
||||||
|
if (settings.primary_color && !safeEq(settings.primary_color, layoutConfig.primary)) {
|
||||||
|
layoutConfig.primary = settings.primary_color
|
||||||
|
updatePreset(getPresetExt(primaryColors, layoutConfig))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.surface_color && !safeEq(settings.surface_color, layoutConfig.surface)) {
|
||||||
|
layoutConfig.surface = settings.surface_color
|
||||||
|
const surface = surfaces.find(s => s.name === settings.surface_color)
|
||||||
|
if (surface) updateSurfacePalette(surface.palette)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dark/light
|
||||||
|
if (settings.theme_mode) {
|
||||||
|
const shouldBeDark = settings.theme_mode === 'dark'
|
||||||
|
if (shouldBeDark !== isDarkTheme) toggleDarkMode()
|
||||||
|
}
|
||||||
|
}
|
||||||