Copyright, Financeiro, Lançamentos, aprimoramentos de ui
This commit is contained in:
22987
DBS/2026-03-19/schema.sql
Normal file
22987
DBS/2026-03-19/schema.sql
Normal file
File diff suppressed because it is too large
Load Diff
23516
DBS/2026-03-20/schema.sql
Normal file
23516
DBS/2026-03-20/schema.sql
Normal file
File diff suppressed because it is too large
Load Diff
41
public/README.md
Normal file
41
public/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# /public
|
||||
|
||||
Esta pasta é servida **estaticamente pelo Vite** na raiz do servidor (`/`).
|
||||
|
||||
Arquivos aqui **não passam pelo bundler** — são copiados diretamente para o build final e acessíveis via URL direta.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Não confundir com
|
||||
|
||||
| Pasta | O que é |
|
||||
|---|---|
|
||||
| `public/` (esta pasta) | Arquivos estáticos brutos. Acessíveis via `fetch('/arquivo.json')` ou `<img src="/imagem.png">` |
|
||||
| `src/views/pages/public/` | Componentes Vue de páginas públicas (sem autenticação). Fazem parte do bundle normalmente. |
|
||||
|
||||
---
|
||||
|
||||
## O que colocar aqui
|
||||
|
||||
- Arquivos JSON de configuração estática (ex: `loading-phrases.json`)
|
||||
- Favicons e ícones (`favicon.ico`, `apple-touch-icon.png`)
|
||||
- Imagens que não precisam ser processadas pelo Vite
|
||||
- Arquivos de verificação de domínio (Google, Meta, etc.)
|
||||
|
||||
## O que **não** colocar aqui
|
||||
|
||||
- Componentes `.vue`
|
||||
- Arquivos `.js` ou `.ts` que importam outros módulos
|
||||
- Qualquer coisa que precise de `@/` ou de `import`
|
||||
|
||||
---
|
||||
|
||||
## Exemplo de uso
|
||||
|
||||
```js
|
||||
// Acessando loading-phrases.json de qualquer componente
|
||||
const res = await fetch('/loading-phrases.json')
|
||||
const json = await res.json()
|
||||
```
|
||||
|
||||
O arquivo em disco é `public/loading-phrases.json`, mas o fetch usa `/loading-phrases.json` — sem o prefixo `public/`.
|
||||
54
public/loading-phrases.json
Normal file
54
public/loading-phrases.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"motivations": [
|
||||
"Você é luz na vida de alguém 💙",
|
||||
"Seu cuidado transforma histórias 🌱",
|
||||
"Presença é o maior presente 🤝",
|
||||
"Cada sessão é um passo de coragem 🌟",
|
||||
"Você faz diferença todos os dias ✨",
|
||||
"Cuidar de si é cuidar do outro 🌿",
|
||||
"Escutar é um ato de amor 🫶",
|
||||
"Sua dedicação não passa despercebida 🙏",
|
||||
"Cada palavra sua pode mudar uma vida 💬",
|
||||
"Você planta esperança todo dia 🌻",
|
||||
"A saúde mental começa com você 🧠",
|
||||
"Seu trabalho é semear cura 🕊️",
|
||||
"Obrigado por cuidar de quem precisa 💚",
|
||||
"Você é o porto seguro de alguém 🌊",
|
||||
"Sua empatia é um superpoder 🦋",
|
||||
"Cada pequeno avanço importa muito 📈",
|
||||
"Você constrói pontes onde havia muros 🌉",
|
||||
"Sua presença já é terapêutica ☀️",
|
||||
"O cuidado que você oferece volta para você 🔄",
|
||||
"Você ajuda pessoas a se encontrarem 🧭",
|
||||
"Cada consulta é uma semente plantada 🌾",
|
||||
"Sua escuta ativa salva vidas silenciosamente 🤫",
|
||||
"Você transforma dor em possibilidade 🦅",
|
||||
"Gratidão por existir nessa profissão 🌈",
|
||||
"Você é a diferença que alguém esperava 🎯",
|
||||
"Seu compromisso com o outro é admirável 🏅",
|
||||
"Cada história que você ouve é sagrada 📖",
|
||||
"Você cultiva coragem nos outros ⚡",
|
||||
"Pequenos gestos seus geram grandes mudanças 🪄",
|
||||
"Você é testemunha das maiores transformações 🦜",
|
||||
"Sua paciência é um presente raro 🎁",
|
||||
"O mundo é melhor com profissionais como você 🌍",
|
||||
"Você inspira mesmo quando não percebe 💫",
|
||||
"Cada paciente carrega um pedaço da sua dedicação 🧩",
|
||||
"Você acolhe o que o mundo muitas vezes rejeita 🤲",
|
||||
"Sua vocação é visível em cada detalhe 🔍",
|
||||
"Você cria espaços seguros onde havia medo 🏡",
|
||||
"Sua energia positiva é contagiante 🌞",
|
||||
"Você lembra às pessoas que elas não estão sozinhas 🫂",
|
||||
"Cada sessão bem conduzida é uma vitória 🏆",
|
||||
"Você acredita nas pessoas antes delas acreditarem em si 🌠",
|
||||
"Sua competência é tão importante quanto sua compaixão 💎",
|
||||
"Você guia sem tirar a autonomia de ninguém 🧗",
|
||||
"Sua história também inspira 📌",
|
||||
"Você faz o invisível se tornar visível 🔦",
|
||||
"Cuidar é um ato revolucionário 🔥",
|
||||
"Você nunca sabe o quanto uma sessão mudou tudo 🌪️",
|
||||
"Sua leveza ajuda o outro a respirar 💨",
|
||||
"Você escolheu a profissão mais humana do mundo ❤️",
|
||||
"Obrigado por não desistir de ninguém 🛡️"
|
||||
]
|
||||
}
|
||||
16
src/App.vue
16
src/App.vue
@@ -1,3 +1,19 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/App.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
16
src/app/bootstrapUserSettings.js
vendored
16
src/app/bootstrapUserSettings.js
vendored
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/app/bootstrapUserSettings.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { useLayout } from '@/layout/composables/layout'
|
||||
import { $t, updatePreset, updateSurfacePalette } from '@primeuix/themes'
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/app/session.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/assets/layout/_core.scss
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
font-size: 14px;
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/assets/layout/_footer.scss
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
.layout-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/assets/layout/_main.scss
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
.layout-main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/assets/layout/_menu.scss
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
@use 'mixins' as *;
|
||||
|
||||
/* ── Sidebar container ─────────────────────────────────────── */
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/assets/layout/_mixins.scss
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
@mixin focused() {
|
||||
outline-width: var(--focus-ring-width);
|
||||
outline-style: var(--focus-ring-style);
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/assets/layout/_preloading.scss
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
.preloader {
|
||||
position: fixed;
|
||||
z-index: 999999;
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/assets/layout/_responsive.scss
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
@media screen and (min-width: 1960px) {
|
||||
.layout-main,
|
||||
.landing-wrapper {
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/assets/layout/_topbar.scss
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
@use 'mixins' as *;
|
||||
|
||||
.layout-topbar {
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/assets/layout/_typography.scss
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/assets/layout/_utils.scss
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/* Utils */
|
||||
.clearfix:after {
|
||||
content: ' ';
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/assets/layout/layout.scss
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
@use './variables/_common';
|
||||
@use './variables/_light';
|
||||
@use './variables/_dark';
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/assets/layout/variables/_common.scss
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
:root {
|
||||
--primary-color: var(--p-primary-color);
|
||||
--primary-contrast-color: var(--p-primary-contrast-color);
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/assets/layout/variables/_dark.scss
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
:root[class*='app-dark'] {
|
||||
--surface-ground: var(--p-surface-950);
|
||||
--code-background: var(--p-surface-800);
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/assets/layout/variables/_light.scss
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
:root {
|
||||
--surface-ground: var(--p-surface-100);
|
||||
--code-background: var(--p-surface-900);
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/assets/styles.scss
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/* ── Imports ─────────────────────────── */
|
||||
@use 'primeicons/primeicons.css';
|
||||
@use '@/assets/layout/layout.scss';
|
||||
@@ -28,4 +45,191 @@
|
||||
.notif-card--highlight {
|
||||
animation: highlight-pulse 1s ease-out 3;
|
||||
border-color: rgba(99,102,241,0.6) !important;
|
||||
}
|
||||
.agenda-altura {
|
||||
height: calc(90vh - 150px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.fade-up-enter-active {
|
||||
transition: opacity 0.4s ease, transform 0.4s ease;
|
||||
}
|
||||
.fade-up-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px);
|
||||
}
|
||||
.fade-up-enter-active .anim-child {
|
||||
transition: opacity 0.45s ease, transform 0.45s ease;
|
||||
transition-delay: var(--delay, 0ms);
|
||||
}
|
||||
.fade-up-enter-from .anim-child {
|
||||
opacity: 0;
|
||||
transform: translateY(16px);
|
||||
}
|
||||
.menu-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(4px) scale(1.5);
|
||||
}
|
||||
|
||||
.menu-enter-active {
|
||||
transition:
|
||||
opacity 140ms ease,
|
||||
transform 260ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.menu-enter-to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
|
||||
/* ── LoadedPhraseBlock ───────────────── */
|
||||
.loaded-phrase-block {
|
||||
border: 2px dashed var(--surface-border);
|
||||
border-radius: 12px;
|
||||
padding: 0.875rem 1.125rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
background: color-mix(in srgb, var(--primary-color, #6366f1) 3%, var(--surface-card));
|
||||
}
|
||||
.loaded-phrase-block__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
.loaded-phrase-block__icon {
|
||||
font-size: 0.9rem;
|
||||
color: var(--green-500, #22c55e);
|
||||
}
|
||||
.loaded-phrase-block__title {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
.loaded-phrase-block__text {
|
||||
font-size: 0.88rem;
|
||||
color: var(--text-color);
|
||||
font-style: italic;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
.loaded-phrase-in-enter-active {
|
||||
transition: opacity 0.5s ease, transform 0.5s ease;
|
||||
}
|
||||
.loaded-phrase-in-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
/* ── Subheader de seção ──────────────────────────────── */
|
||||
.cfg-subheader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
padding: 0.875rem 1rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid color-mix(in srgb, var(--primary-color, #6366f1) 30%, transparent);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--primary-color, #6366f1) 12%, var(--surface-card)) 0%,
|
||||
color-mix(in srgb, var(--primary-color, #6366f1) 4%, var(--surface-card)) 60%,
|
||||
var(--surface-card) 100%
|
||||
);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
/* Brilho sutil no canto */
|
||||
.cfg-subheader::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -20px; right: -20px;
|
||||
width: 80px; height: 80px;
|
||||
border-radius: 50%;
|
||||
background: color-mix(in srgb, var(--primary-color, #6366f1) 15%, transparent);
|
||||
filter: blur(20px);
|
||||
pointer-events: none;
|
||||
}
|
||||
.cfg-subheader__icon {
|
||||
display: grid; place-items: center;
|
||||
flex-shrink: 0;
|
||||
background: color-mix(in srgb, var(--primary-color, #6366f1) 20%, transparent);
|
||||
color: var(--primary-color, #6366f1);
|
||||
}
|
||||
.cfg-subheader__title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color, #6366f1);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.cfg-subheader__sub {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-color-secondary);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.cfg-card__icon-wrap {
|
||||
border: 1px solid var(--surface-border);
|
||||
background: var(--surface-ground);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* ── Dash cards ───────────────────────────────────── */
|
||||
|
||||
.dash-card {
|
||||
display: flex; flex-direction: column;
|
||||
background: var(--surface-card);
|
||||
border: 1px solid var(--surface-border);
|
||||
overflow: hidden;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
.dash-card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.07); }
|
||||
.dash-card__head {
|
||||
display: flex; align-items: center;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
background: var(--surface-ground);
|
||||
}
|
||||
.dash-card__icon {
|
||||
width: 2.25rem; height: 2.25rem;
|
||||
border-radius: 8px;
|
||||
display: grid; place-items: center;
|
||||
flex-shrink: 0; font-size: 1rem;
|
||||
}
|
||||
.dash-card__sub { font-size: 0.875rem; color: var(--text-color-secondary); margin-top: 1px; }
|
||||
.dash-card__badge {
|
||||
margin-left: auto; flex-shrink: 0;
|
||||
padding: 2px 10px; border-radius: 999px;
|
||||
font-size: 0.875rem; font-weight: 700;
|
||||
}
|
||||
.dash-card__body {
|
||||
flex: 1; padding: 0.75rem 1rem;
|
||||
display: flex; flex-direction: column; gap: 0.625rem;
|
||||
min-height: 88px;
|
||||
}
|
||||
.dash-card__foot {
|
||||
padding: 0.625rem 1rem;
|
||||
border-top: 1px solid var(--surface-border);
|
||||
font-size: 0.875rem; font-weight: 700;
|
||||
color: var(--primary-color, #6366f1);
|
||||
cursor: pointer; transition: background 0.1s;
|
||||
}
|
||||
.dash-card__foot:hover { background: var(--surface-ground); }
|
||||
|
||||
/* ── Dash items (dentro dos cards) ───────────────── */
|
||||
.dash-item {
|
||||
display: flex; align-items: center;
|
||||
}
|
||||
.dash-item__avatar {
|
||||
width: 2rem; height: 2rem; border-radius: 50%;
|
||||
flex-shrink: 0; display: grid; place-items: center;
|
||||
font-size: 0.75rem; font-weight: 700; color: #fff;
|
||||
}
|
||||
.dash-item__name { font-size: 0.9375rem; font-weight: 600; color: var(--text-color); }
|
||||
.dash-item__sub { font-size: 0.8125rem; color: var(--text-color-secondary); margin-top: 1px; }
|
||||
.dash-item__actions { display: flex; gap: 0.375rem; margin-left: auto; flex-shrink: 0; }
|
||||
|
||||
/* ── Empty state dos cards ────────────────────────── */
|
||||
.dash-empty {
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
font-size: 0.875rem; color: var(--text-color-secondary);
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/assets/tailwind.css
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
@import "tailwindcss";
|
||||
@plugin "tailwindcss-primeui";
|
||||
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
<!-- src/components/AjudaDrawer.vue -->
|
||||
<!-- Painel de ajuda lateral — home com sessão/docs/faq + navegação interna + votação -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/AjudaDrawer.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
<!-- src/components/AppOfflineOverlay.vue -->
|
||||
<!-- Detecta offline via eventos nativos do browser + polling de fetch -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/AppOfflineOverlay.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/ComponentCadastroRapido.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<template>
|
||||
<Dialog
|
||||
v-model:visible="isOpen"
|
||||
@@ -15,7 +31,7 @@
|
||||
<div class="min-w-0">
|
||||
<div class="text-xl font-semibold">{{ title }}</div>
|
||||
<div class="text-sm text-surface-500">
|
||||
Crie um paciente rapidamente (nome, e-mail e telefone obrigatórios).
|
||||
Crie um paciente rapidamente.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +67,7 @@
|
||||
:disabled="saving"
|
||||
autocomplete="off"
|
||||
autofocus
|
||||
@keydown.enter.prevent="submit"
|
||||
@keydown.enter.prevent="submit('only')"
|
||||
/>
|
||||
</IconField>
|
||||
<label for="cr-nome">Nome completo *</label>
|
||||
@@ -71,7 +87,7 @@
|
||||
:disabled="saving"
|
||||
inputmode="email"
|
||||
autocomplete="off"
|
||||
@keydown.enter.prevent="submit"
|
||||
@keydown.enter.prevent="submit('only')"
|
||||
/>
|
||||
</IconField>
|
||||
<label for="cr-email">E-mail *</label>
|
||||
@@ -90,7 +106,7 @@
|
||||
class="w-full"
|
||||
variant="filled"
|
||||
:disabled="saving"
|
||||
@keydown.enter.prevent="submit"
|
||||
@keydown.enter.prevent="submit('only')"
|
||||
/>
|
||||
</IconField>
|
||||
<label for="cr-telefone">Telefone *</label>
|
||||
@@ -111,12 +127,31 @@
|
||||
:disabled="saving"
|
||||
@click="close"
|
||||
/>
|
||||
<!-- Na rota de pacientes: só "Salvar" -->
|
||||
<Button
|
||||
v-if="isOnPatientsPage"
|
||||
label="Salvar"
|
||||
:loading="saving"
|
||||
:disabled="saving"
|
||||
@click="submit"
|
||||
@click="submit('only')"
|
||||
/>
|
||||
<!-- Fora da rota de pacientes: "Salvar e fechar" + "Salvar e ver pacientes" -->
|
||||
<template v-else>
|
||||
<Button
|
||||
label="Salvar e fechar"
|
||||
severity="secondary"
|
||||
outlined
|
||||
:loading="saving"
|
||||
:disabled="saving"
|
||||
@click="submit('only')"
|
||||
/>
|
||||
<Button
|
||||
label="Salvar e ver pacientes"
|
||||
:loading="saving"
|
||||
:disabled="saving"
|
||||
@click="submit('view')"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
@@ -124,6 +159,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRoleGuard } from '@/composables/useRoleGuard'
|
||||
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
@@ -134,6 +170,13 @@ import Message from 'primevue/message'
|
||||
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
const { canSee } = useRoleGuard()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const isOnPatientsPage = computed(() => {
|
||||
const p = String(route.path || '')
|
||||
return p.includes('/patients') || p.includes('/pacientes')
|
||||
})
|
||||
|
||||
/**
|
||||
* Lista "curada" de pensadores influentes na psicanálise e seu entorno.
|
||||
@@ -325,7 +368,12 @@ function generateUser () {
|
||||
})
|
||||
}
|
||||
|
||||
async function submit () {
|
||||
function patientsListRoute () {
|
||||
const p = String(route.path || '')
|
||||
return p.startsWith('/therapist') ? '/therapist/patients' : '/admin/pacientes'
|
||||
}
|
||||
|
||||
async function submit (mode = 'only') {
|
||||
touched.value = true
|
||||
errorMsg.value = ''
|
||||
|
||||
@@ -378,6 +426,7 @@ async function submit () {
|
||||
|
||||
emit('created', data)
|
||||
if (props.closeOnCreated) close()
|
||||
if (mode === 'view') await router.push(patientsListRoute())
|
||||
} catch (err) {
|
||||
const msg = err?.message || err?.details || 'Não foi possível criar o paciente.'
|
||||
errorMsg.value = msg
|
||||
|
||||
416
src/components/agenda/AgendaEventoFinanceiroPanel.vue
Normal file
416
src/components/agenda/AgendaEventoFinanceiroPanel.vue
Normal file
@@ -0,0 +1,416 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/agenda/AgendaEventoFinanceiroPanel.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
|
||||
<!--
|
||||
AgendaEventoFinanceiroPanel
|
||||
───────────────────────────
|
||||
Painel compacto de status financeiro exibido dentro do modal de sessão.
|
||||
Mostra o financial_record vinculado ao evento e permite registrar pagamento
|
||||
ou gerar cobrança sem sair do contexto da agenda.
|
||||
|
||||
Props:
|
||||
evento — linha de agenda_eventos (deve ter: id, tipo, billed,
|
||||
billing_contract_id, price, patient_id, inicio_em)
|
||||
|
||||
Emits:
|
||||
cobranca-atualizada — após qualquer mutação, para o pai recarregar
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { useConfirm } from 'primevue/useconfirm'
|
||||
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { useAgendaFinanceiro } from '@/composables/useAgendaFinanceiro'
|
||||
|
||||
// ── props / emits ─────────────────────────────────────────────────────────────
|
||||
const props = defineProps({
|
||||
evento: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['cobranca-atualizada'])
|
||||
|
||||
// ── external ──────────────────────────────────────────────────────────────────
|
||||
const toast = useToast()
|
||||
const confirm = useConfirm()
|
||||
const { gerarCobrancaManual, loading: finLoading, error: finError } = useAgendaFinanceiro()
|
||||
|
||||
// ── estado local ──────────────────────────────────────────────────────────────
|
||||
const record = ref(null) // financial_record vinculado
|
||||
const fetching = ref(false)
|
||||
const generating = ref(false)
|
||||
|
||||
// ── opções de método de pagamento ─────────────────────────────────────────────
|
||||
const PAYMENT_METHODS = [
|
||||
{ label: 'Pix', value: 'pix' },
|
||||
{ label: 'Depósito', value: 'deposito' },
|
||||
{ label: 'Dinheiro', value: 'dinheiro' },
|
||||
{ label: 'Cartão', value: 'cartao' },
|
||||
{ label: 'Convênio', value: 'convenio' },
|
||||
]
|
||||
|
||||
function paymentLabel (method) {
|
||||
return PAYMENT_METHODS.find(o => o.value === method)?.label ?? method ?? '—'
|
||||
}
|
||||
|
||||
// ── formatação ─────────────────────────────────────────────────────────────────
|
||||
const _brl = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' })
|
||||
function fmtBRL (v) { return _brl.format(v ?? 0) }
|
||||
function fmtDate (iso) {
|
||||
if (!iso) return '—'
|
||||
const d = iso.includes('T') ? new Date(iso) : new Date(iso + 'T00:00:00')
|
||||
return new Intl.DateTimeFormat('pt-BR').format(d)
|
||||
}
|
||||
|
||||
// ── config visual de status ────────────────────────────────────────────────────
|
||||
const STATUS_CFG = {
|
||||
pending: { label: 'Pendente', severity: 'warn' },
|
||||
paid: { label: 'Pago', severity: 'success' },
|
||||
overdue: { label: 'Vencido', severity: 'danger' },
|
||||
cancelled: { label: 'Cancelado', severity: 'secondary' },
|
||||
}
|
||||
|
||||
// ── computed: cenário a renderizar ────────────────────────────────────────────
|
||||
const scenario = computed(() => {
|
||||
if (props.evento.tipo !== 'sessao') return 'noop' // bloqueio
|
||||
if (props.evento.billing_contract_id) return 'contrato' // pacote
|
||||
if (fetching.value) return 'carregando'
|
||||
if (record.value) return 'com-cobranca'
|
||||
return 'sem-cobranca'
|
||||
})
|
||||
|
||||
const canAct = computed(() =>
|
||||
record.value && (record.value.status === 'pending' || record.value.status === 'overdue')
|
||||
)
|
||||
|
||||
// ── buscar financial_record pelo evento ───────────────────────────────────────
|
||||
async function fetchRecord () {
|
||||
if (!props.evento.id) return
|
||||
|
||||
fetching.value = true
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('financial_records')
|
||||
.select('id, amount, discount_amount, final_amount, status, due_date, paid_at, payment_method')
|
||||
.eq('agenda_evento_id', props.evento.id)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle()
|
||||
|
||||
if (error) throw error
|
||||
record.value = data ?? null
|
||||
} catch (e) {
|
||||
console.warn('[AgendaEventoFinanceiroPanel] fetchRecord:', e?.message)
|
||||
record.value = null
|
||||
} finally {
|
||||
fetching.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.evento?.id, () => {
|
||||
record.value = null
|
||||
fetchRecord()
|
||||
}, { immediate: true })
|
||||
|
||||
// ── gerar cobrança ─────────────────────────────────────────────────────────────
|
||||
async function onGerarCobranca () {
|
||||
generating.value = true
|
||||
try {
|
||||
const result = await gerarCobrancaManual(props.evento)
|
||||
if (!result.ok) throw new Error(result.error)
|
||||
|
||||
await fetchRecord()
|
||||
emit('cobranca-atualizada')
|
||||
toast.add({ severity: 'success', summary: 'Cobrança gerada', detail: `${fmtBRL(props.evento.price ?? 0)} agendado para recebimento.`, life: 3000 })
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Não foi possível gerar a cobrança.', life: 4000 })
|
||||
} finally {
|
||||
generating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── dialog: registrar pagamento ───────────────────────────────────────────────
|
||||
const payDlgVisible = ref(false)
|
||||
const payDlgMethod = ref(null)
|
||||
const payDlgLoading = ref(false)
|
||||
|
||||
function openPayDialog () {
|
||||
payDlgMethod.value = null
|
||||
payDlgVisible.value = true
|
||||
}
|
||||
|
||||
async function confirmPayment () {
|
||||
if (!payDlgMethod.value || !record.value) return
|
||||
payDlgLoading.value = true
|
||||
try {
|
||||
const { data, error } = await supabase.rpc('mark_as_paid', {
|
||||
p_financial_record_id: record.value.id,
|
||||
p_payment_method: payDlgMethod.value,
|
||||
})
|
||||
if (error) throw error
|
||||
|
||||
payDlgVisible.value = false
|
||||
await fetchRecord()
|
||||
emit('cobranca-atualizada')
|
||||
toast.add({ severity: 'success', summary: 'Pago!', detail: `Recebimento via ${paymentLabel(payDlgMethod.value)} registrado.`, life: 3000 })
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Não foi possível registrar pagamento.', life: 4000 })
|
||||
} finally {
|
||||
payDlgLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── cancelar cobrança ─────────────────────────────────────────────────────────
|
||||
function requestCancel () {
|
||||
confirm.require({
|
||||
message: `Cancelar a cobrança de ${fmtBRL(record.value?.final_amount)} desta sessão?`,
|
||||
header: 'Cancelar cobrança',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
rejectLabel: 'Não',
|
||||
acceptLabel: 'Sim, cancelar',
|
||||
acceptSeverity: 'danger',
|
||||
accept: async () => {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('financial_records')
|
||||
.update({ status: 'cancelled', updated_at: new Date().toISOString() })
|
||||
.eq('id', record.value.id)
|
||||
|
||||
if (error) throw error
|
||||
|
||||
await fetchRecord()
|
||||
emit('cobranca-atualizada')
|
||||
toast.add({ severity: 'info', summary: 'Cancelado', detail: 'Cobrança cancelada.', life: 3000 })
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao cancelar.', life: 4000 })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Painel principal — noop (bloqueios) não renderiza nada -->
|
||||
<div v-if="scenario !== 'noop'" class="fin-panel">
|
||||
|
||||
<!-- Cabeçalho do painel -->
|
||||
<div class="fin-panel__header">
|
||||
<i class="pi pi-wallet" />
|
||||
<span>Cobrança</span>
|
||||
<Button
|
||||
v-if="props.evento.billed && !fetching"
|
||||
icon="pi pi-refresh"
|
||||
text
|
||||
size="small"
|
||||
severity="secondary"
|
||||
class="ml-auto h-6 w-6"
|
||||
v-tooltip.top="'Recarregar'"
|
||||
@click="fetchRecord"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ── Sessão de pacote / contrato ──────────────────────────────────── -->
|
||||
<div v-if="scenario === 'contrato'" class="fin-panel__body">
|
||||
<span class="fin-badge fin-badge--contract">
|
||||
<i class="pi pi-box text-xs" />
|
||||
Sessão de pacote
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- ── Sem cobrança gerada ──────────────────────────────────────────── -->
|
||||
<div v-else-if="scenario === 'sem-cobranca'" class="fin-panel__body fin-panel__body--empty">
|
||||
<div class="flex items-center gap-2 text-[var(--text-color-secondary)]">
|
||||
<i class="pi pi-minus-circle text-sm opacity-50" />
|
||||
<span class="text-sm">Sem cobrança gerada</span>
|
||||
</div>
|
||||
<Button
|
||||
label="Gerar cobrança"
|
||||
icon="pi pi-plus"
|
||||
size="small"
|
||||
class="rounded-full mt-2"
|
||||
:loading="generating || finLoading"
|
||||
@click="onGerarCobranca"
|
||||
/>
|
||||
<div v-if="props.evento.price" class="text-xs text-[var(--text-color-secondary)] mt-1">
|
||||
Valor da sessão: {{ fmtBRL(props.evento.price) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Carregando o financial_record ────────────────────────────────── -->
|
||||
<div v-else-if="scenario === 'carregando'" class="fin-panel__body">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<Skeleton height="1rem" class="w-24" />
|
||||
<Skeleton height="1.5rem" class="w-32" />
|
||||
<Skeleton height="1rem" class="w-20" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Com cobrança ─────────────────────────────────────────────────── -->
|
||||
<div v-else-if="scenario === 'com-cobranca'" class="fin-panel__body">
|
||||
|
||||
<!-- Linha de status + valor -->
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<Tag
|
||||
:value="STATUS_CFG[record.status]?.label ?? record.status"
|
||||
:severity="STATUS_CFG[record.status]?.severity"
|
||||
class="text-xs"
|
||||
/>
|
||||
<span class="font-bold text-sm text-[var(--text-color)]">{{ fmtBRL(record.final_amount) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Vencimento / data de pagamento -->
|
||||
<div class="flex items-center gap-1.5 text-xs text-[var(--text-color-secondary)] mt-1.5">
|
||||
<template v-if="record.status === 'paid'">
|
||||
<i class="pi pi-check-circle text-emerald-500" />
|
||||
<span class="text-emerald-600">{{ paymentLabel(record.payment_method) }} · {{ fmtDate(record.paid_at) }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<i class="pi pi-calendar" />
|
||||
<span :class="record.status === 'overdue' ? 'text-red-500 font-semibold' : ''">
|
||||
Vence {{ fmtDate(record.due_date) }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Ações: pendente / vencido -->
|
||||
<div v-if="canAct" class="flex gap-1.5 mt-3">
|
||||
<Button
|
||||
label="Receber"
|
||||
icon="pi pi-check"
|
||||
size="small"
|
||||
class="rounded-full flex-1"
|
||||
@click="openPayDialog"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-times"
|
||||
size="small"
|
||||
severity="danger"
|
||||
outlined
|
||||
class="rounded-full h-7 w-7"
|
||||
v-tooltip.top="'Cancelar cobrança'"
|
||||
@click="requestCancel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ── Dialog: Registrar Pagamento ──────────────────────────────────────── -->
|
||||
<Dialog
|
||||
v-model:visible="payDlgVisible"
|
||||
modal
|
||||
:draggable="false"
|
||||
pt:mask:class="backdrop-blur-xs"
|
||||
header="Registrar pagamento"
|
||||
class="w-[92vw] max-w-sm"
|
||||
>
|
||||
<div class="flex flex-col gap-4 pt-1">
|
||||
|
||||
<!-- Valor -->
|
||||
<div class="flex items-center justify-between px-4 py-2.5 rounded-md border border-[var(--surface-border)] bg-[var(--surface-ground)]">
|
||||
<span class="text-sm text-[var(--text-color-secondary)]">Valor a receber</span>
|
||||
<span class="font-bold text-[var(--text-color)]">{{ fmtBRL(record?.final_amount) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Método (grid de botões) -->
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-[var(--text-color)] mb-2">Método de pagamento</div>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<button
|
||||
v-for="opt in PAYMENT_METHODS"
|
||||
:key="opt.value"
|
||||
type="button"
|
||||
class="flex flex-col items-center gap-1 px-2 py-2 rounded-md border text-xs font-medium transition-all duration-150 cursor-pointer select-none"
|
||||
:class="payDlgMethod === opt.value
|
||||
? 'border-[var(--primary-color,#6366f1)] bg-[var(--primary-color,#6366f1)]/10 text-[var(--primary-color,#6366f1)]'
|
||||
: 'border-[var(--surface-border)] bg-[var(--surface-ground)] text-[var(--text-color-secondary)] hover:border-[var(--primary-color,#6366f1)]/40'"
|
||||
@click="payDlgMethod = opt.value"
|
||||
>
|
||||
<i
|
||||
class="text-base"
|
||||
:class="{
|
||||
'pi pi-bolt': opt.value === 'pix',
|
||||
'pi pi-building': opt.value === 'deposito',
|
||||
'pi pi-money-bill': opt.value === 'dinheiro',
|
||||
'pi pi-credit-card': opt.value === 'cartao',
|
||||
'pi pi-id-card': opt.value === 'convenio',
|
||||
}"
|
||||
/>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button label="Cancelar" severity="secondary" outlined class="rounded-full" :disabled="payDlgLoading" @click="payDlgVisible = false" />
|
||||
<Button label="Confirmar" icon="pi pi-check" class="rounded-full" :loading="payDlgLoading" :disabled="!payDlgMethod" @click="confirmPayment" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fin-panel {
|
||||
border: 1px solid var(--surface-border, #e2e8f0);
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
background: var(--surface-card, #fff);
|
||||
}
|
||||
|
||||
.fin-panel__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--surface-ground, #f8fafc);
|
||||
border-bottom: 1px solid var(--surface-border, #e2e8f0);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
.fin-panel__body {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.fin-panel__body--empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.fin-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
padding: 0.25rem 0.625rem;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.fin-badge--contract {
|
||||
background: color-mix(in srgb, var(--p-indigo-500, #6366f1) 10%, transparent);
|
||||
color: var(--p-indigo-600, #4f46e5);
|
||||
border: 1px solid color-mix(in srgb, var(--p-indigo-500, #6366f1) 20%, transparent);
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,19 @@
|
||||
<!-- src/components/agenda/AgendaOnlineGradeCard.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/agenda/AgendaOnlineGradeCard.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import TabView from 'primevue/tabview'
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
<!-- src/components/agenda/AgendaSlotsPorDiaCard.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/agenda/AgendaSlotsPorDiaCard.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { computed, ref, watch, onMounted } from 'vue'
|
||||
import TabView from 'primevue/tabview'
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
<!-- src/components/agenda/PausasChipsEditor.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/agenda/PausasChipsEditor.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import DatePicker from 'primevue/datepicker'
|
||||
|
||||
284
src/components/agendador/AgendadorPreview.vue
Normal file
284
src/components/agendador/AgendadorPreview.vue
Normal file
@@ -0,0 +1,284 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/agendador/AgendadorPreview.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
cfg: { type: Object, required: true }
|
||||
})
|
||||
|
||||
const cor = computed(() => props.cfg.cor_primaria || '#4b6bff')
|
||||
|
||||
const corMix = computed(() => `color-mix(in srgb, ${cor.value} 15%, transparent)`)
|
||||
|
||||
const tipos = [
|
||||
{ key: 'primeira', label: 'Primeira Entrevista', sub: 'Novo paciente', icon: 'pi-star', bg: '#0284c7', shadow: 'rgba(2,132,199,.25)' },
|
||||
{ key: 'retorno', label: 'Retorno', sub: 'Já sou paciente', icon: 'pi-refresh', bg: '#059669', shadow: 'rgba(5,150,105,.25)' },
|
||||
{ key: 'reagendar',label: 'Reagendar', sub: 'Mudar data ou horário', icon: 'pi-calendar-plus', bg: '#7c3aed', shadow: 'rgba(124,58,237,.25)' },
|
||||
]
|
||||
|
||||
const tiposAtivos = computed(() =>
|
||||
tipos.filter(t => props.cfg.tipos_habilitados?.includes(t.key))
|
||||
)
|
||||
|
||||
const modalidadeLabel = computed(() => ({
|
||||
presencial: 'Presencial',
|
||||
online: 'Online (vídeo)',
|
||||
ambos: 'Presencial · Online',
|
||||
}[props.cfg.modalidade] || 'Presencial'))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Frame de celular -->
|
||||
<div class="phone-frame">
|
||||
<div class="phone-notch" />
|
||||
|
||||
<!-- Root do agendador -->
|
||||
<div class="agdp-root">
|
||||
|
||||
<!-- Card principal -->
|
||||
<div class="agdp-card" :style="{ '--cp': cor }">
|
||||
|
||||
<!-- Hero -->
|
||||
<div class="agdp-hero">
|
||||
<!-- Blobs -->
|
||||
<div class="agdp-blobs" aria-hidden="true">
|
||||
<div class="agdp-blob agdp-blob--1" :style="{ background: `color-mix(in srgb, ${cor} 22%, transparent)` }" />
|
||||
<div class="agdp-blob agdp-blob--2" :style="{ background: `color-mix(in srgb, ${cor} 12%, transparent)` }" />
|
||||
</div>
|
||||
|
||||
<!-- Logo -->
|
||||
<div class="agdp-avatar">
|
||||
<img v-if="cfg.logomarca_url" :src="cfg.logomarca_url" alt="logo" class="w-full h-full object-cover" />
|
||||
<i v-else class="pi pi-heart text-xl" :style="{ color: cor }" />
|
||||
</div>
|
||||
|
||||
<!-- Nome -->
|
||||
<div class="agdp-name">
|
||||
{{ cfg.nome_exibicao || 'Seu nome aqui' }}
|
||||
</div>
|
||||
|
||||
<!-- Endereço -->
|
||||
<div v-if="cfg.botao_como_chegar_ativo && cfg.endereco" class="agdp-endereco">
|
||||
<i class="pi pi-map-marker text-[0.6rem]" />
|
||||
<span>{{ cfg.endereco }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Modalidade badge -->
|
||||
<div class="agdp-badge" :style="{ background: corMix, color: cor }">
|
||||
<i class="pi pi-video text-[0.55rem]" />
|
||||
{{ modalidadeLabel }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Home section -->
|
||||
<div class="agdp-section">
|
||||
|
||||
<!-- Mensagem de boas-vindas -->
|
||||
<p class="agdp-welcome">
|
||||
{{ cfg.mensagem_boas_vindas || 'Bem-vindo! Escolha o tipo de consulta para começar.' }}
|
||||
</p>
|
||||
|
||||
<!-- Como chegar -->
|
||||
<div v-if="cfg.botao_como_chegar_ativo && cfg.endereco" class="agdp-como-chegar">
|
||||
<i class="pi pi-directions text-[0.6rem]" />
|
||||
<span :style="{ color: cor }">Como chegar</span>
|
||||
</div>
|
||||
|
||||
<div class="agdp-divider" />
|
||||
|
||||
<!-- Botões de tipo -->
|
||||
<div class="agdp-tipos">
|
||||
<div
|
||||
v-for="t in tiposAtivos"
|
||||
:key="t.key"
|
||||
class="agdp-tipo-btn"
|
||||
:style="{ '--btn-bg': t.bg, '--btn-shadow': t.shadow }"
|
||||
>
|
||||
<div class="agdp-tipo-icon">
|
||||
<i :class="`pi ${t.icon} text-[0.65rem]`" />
|
||||
</div>
|
||||
<div class="agdp-tipo-label">
|
||||
<span class="agdp-tipo-main">{{ t.label }}</span>
|
||||
<span class="agdp-tipo-sub">{{ t.sub }}</span>
|
||||
</div>
|
||||
<i class="pi pi-arrow-right text-[0.55rem] opacity-40 ml-auto" />
|
||||
</div>
|
||||
|
||||
<!-- Placeholder se nenhum tipo ativo -->
|
||||
<div v-if="!tiposAtivos.length" class="agdp-no-tipos">
|
||||
Nenhum tipo de consulta habilitado
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Powered -->
|
||||
<p class="agdp-powered">Powered by <strong>Agência Psi</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ── Frame de celular ──────────────────── */
|
||||
.phone-frame {
|
||||
position: relative;
|
||||
width: 260px;
|
||||
min-height: 500px;
|
||||
margin: 0 auto;
|
||||
border-radius: 2.5rem;
|
||||
border: 8px solid #1e293b;
|
||||
background: #1e293b;
|
||||
box-shadow:
|
||||
0 0 0 2px #334155,
|
||||
0 32px 64px rgba(0,0,0,.35),
|
||||
0 8px 24px rgba(0,0,0,.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
.phone-notch {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 72px;
|
||||
height: 10px;
|
||||
background: #1e293b;
|
||||
border-radius: 0 0 10px 10px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* ── Root ──────────────────────────────── */
|
||||
.agdp-root {
|
||||
background: #f0f3fb;
|
||||
min-height: 100%;
|
||||
padding: 12px 10px 16px;
|
||||
overflow-y: auto;
|
||||
max-height: 560px;
|
||||
}
|
||||
|
||||
/* ── Card ──────────────────────────────── */
|
||||
.agdp-card {
|
||||
background: #fff;
|
||||
border-radius: 1.25rem;
|
||||
border: 1px solid rgba(0,0,0,.06);
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,.08), 0 2px 8px rgba(0,0,0,.04);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Hero ──────────────────────────────── */
|
||||
.agdp-hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 1.25rem 1rem 0.875rem;
|
||||
background: #f7f8fd;
|
||||
border-bottom: 1px solid rgba(0,0,0,.06);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.agdp-blobs { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
|
||||
.agdp-blob { position: absolute; border-radius: 50%; filter: blur(40px); }
|
||||
.agdp-blob--1 { width: 8rem; height: 8rem; top: -2rem; right: -2rem; }
|
||||
.agdp-blob--2 { width: 7rem; height: 7rem; bottom: -2rem; left: -2rem; }
|
||||
|
||||
.agdp-avatar {
|
||||
position: relative; z-index: 1;
|
||||
width: 52px; height: 52px;
|
||||
border-radius: 50%;
|
||||
border: 2.5px solid #fff;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,.12);
|
||||
background: #f1f5f9;
|
||||
overflow: hidden;
|
||||
display: grid; place-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.agdp-name {
|
||||
position: relative; z-index: 1;
|
||||
font-size: 0.82rem; font-weight: 800;
|
||||
letter-spacing: -0.02em;
|
||||
color: #111827;
|
||||
text-align: center;
|
||||
}
|
||||
.agdp-endereco {
|
||||
position: relative; z-index: 1;
|
||||
display: flex; align-items: center; gap: 3px;
|
||||
font-size: 0.62rem; color: #6b7280;
|
||||
text-align: center;
|
||||
max-width: 90%;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.agdp-badge {
|
||||
position: relative; z-index: 1;
|
||||
font-size: 0.58rem; font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
display: flex; align-items: center; gap: 3px;
|
||||
}
|
||||
|
||||
/* ── Section ───────────────────────────── */
|
||||
.agdp-section { padding: 0.875rem 0.875rem 0.75rem; }
|
||||
|
||||
.agdp-welcome {
|
||||
font-size: 0.68rem; color: #6b7280;
|
||||
text-align: center; line-height: 1.5;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.agdp-como-chegar {
|
||||
display: flex; align-items: center; justify-content: center; gap: 3px;
|
||||
font-size: 0.62rem; font-weight: 700;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.agdp-divider {
|
||||
height: 1px; background: #e5e7eb; margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
/* ── Tipos ─────────────────────────────── */
|
||||
.agdp-tipos { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.agdp-tipo-btn {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 0.75rem;
|
||||
border: 1.5px solid #e5e7eb;
|
||||
background: #fff;
|
||||
cursor: default;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
}
|
||||
.agdp-tipo-icon {
|
||||
width: 28px; height: 28px;
|
||||
border-radius: 0.5rem;
|
||||
display: grid; place-items: center;
|
||||
background: color-mix(in srgb, var(--btn-bg, #6366f1) 15%, transparent);
|
||||
color: var(--btn-bg, #6366f1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.agdp-tipo-label { display: flex; flex-direction: column; gap: 1px; flex: 1; min-width: 0; }
|
||||
.agdp-tipo-main { font-size: 0.72rem; font-weight: 700; color: #111827; }
|
||||
.agdp-tipo-sub { font-size: 0.58rem; color: #9ca3af; }
|
||||
|
||||
.agdp-no-tipos {
|
||||
font-size: 0.68rem; color: #9ca3af;
|
||||
text-align: center; padding: 1rem 0;
|
||||
}
|
||||
|
||||
.agdp-powered {
|
||||
text-align: center; font-size: 0.58rem; color: #9ca3af;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.agdp-powered strong { color: #6b7280; font-weight: 700; }
|
||||
</style>
|
||||
@@ -1,3 +1,19 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/dashboard/BestSellingWidget.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/dashboard/NotificationsWidget.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/dashboard/RecentSalesWidget.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ProductService } from '@/services/ProductService';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/dashboard/RevenueStreamWidget.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { useLayout } from '@/layout/composables/layout';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/dashboard/StatsWidget.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<template>
|
||||
<div class="col-span-12 lg:col-span-6 xl:col-span-3">
|
||||
<div class="card mb-0">
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/landing/FeaturesWidget.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<template>
|
||||
<div id="features" class="py-6 px-6 lg:px-20 mt-8 mx-0 lg:mx-20">
|
||||
<div class="grid grid-cols-12 gap-4 justify-center">
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/landing/FooterWidget.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<template>
|
||||
<div class="py-6 px-6 mx-0 mt-20 lg:mx-20">
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/landing/HeroWidget.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<template>
|
||||
<div
|
||||
id="hero"
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/landing/HighlightsWidget.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<template>
|
||||
<div id="highlights" class="py-6 px-6 lg:px-20 mx-0 my-12 lg:mx-20">
|
||||
<div class="text-center">
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/landing/PricingWidget.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<template>
|
||||
<div id="pricing" class="py-6 px-6 lg:px-20 my-2 md:my-6">
|
||||
<div class="text-center mb-6">
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/landing/TopbarWidget.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
function smoothScroll(id) {
|
||||
document.body.click();
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
<!-- src/components/notifications/NotificationDrawer.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/notifications/NotificationDrawer.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
<!-- src/components/notifications/NotificationItem.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/notifications/NotificationItem.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
<!-- src/components/patients/PatientActionMenu.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/patients/PatientActionMenu.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/security/FeatureGate.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useEntitlementsStore } from '@/stores/entitlementsStore'
|
||||
|
||||
87
src/components/ui/AppLoadingPhrases.vue
Normal file
87
src/components/ui/AppLoadingPhrases.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/ui/AppLoadingPhrases.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<template>
|
||||
<Transition name="fade-up" appear>
|
||||
<div class="flex flex-col items-center justify-center gap-6" :class="containerClass">
|
||||
|
||||
<!-- Motivação -->
|
||||
<div class="flex flex-col items-center gap-2 text-center px-6">
|
||||
<span class="text-base font-semibold text-[var(--text-color,#1e293b)] leading-snug max-w-[320px]">
|
||||
{{ motivation || '...' }}
|
||||
</span>
|
||||
<span class="text-sm text-[var(--text-color-secondary,#64748b)]">
|
||||
{{ action }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Progress bar -->
|
||||
<div class="w-[220px] h-[3px] rounded-full bg-[var(--surface-border,#e2e8f0)] overflow-hidden">
|
||||
<div class="progress-bar h-full rounded-full bg-[var(--primary-color,#6366f1)]" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
defineProps({
|
||||
action: { type: String, default: 'Carregando...' },
|
||||
containerClass: { type: String, default: 'py-24' },
|
||||
})
|
||||
|
||||
const motivation = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await fetch('/loading-phrases.json')
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
const json = await res.json()
|
||||
const list = json.motivations || []
|
||||
motivation.value = list.length
|
||||
? list[Math.floor(Math.random() * list.length)]
|
||||
: 'Carregando...'
|
||||
} catch (e) {
|
||||
console.warn('[AppLoadingPhrases] fetch falhou:', e)
|
||||
motivation.value = 'Carregando...'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Entrada do componente inteiro */
|
||||
.fade-up-enter-active {
|
||||
transition: opacity 0.45s ease, transform 0.45s ease;
|
||||
}
|
||||
.fade-up-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px);
|
||||
}
|
||||
|
||||
/* Progress bar — vai de 0% a 85% em ~2.5s, para não "completar" antes do loading acabar */
|
||||
.progress-bar {
|
||||
animation: progress-indeterminate 1.6s ease-in-out infinite;
|
||||
transform-origin: left;
|
||||
}
|
||||
@keyframes progress-indeterminate {
|
||||
0% { margin-left: 0%; width: 0%; }
|
||||
30% { margin-left: 0%; width: 60%; }
|
||||
70% { margin-left: 40%; width: 60%; }
|
||||
100% { margin-left: 100%; width: 0%; }
|
||||
}
|
||||
</style>
|
||||
44
src/components/ui/LoadedPhraseBlock.vue
Normal file
44
src/components/ui/LoadedPhraseBlock.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/ui/LoadedPhraseBlock.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<template>
|
||||
<Transition name="loaded-phrase-in" appear>
|
||||
<div v-if="phrase" class="loaded-phrase-block">
|
||||
<div class="loaded-phrase-block__header">
|
||||
<i class="pi pi-check-circle loaded-phrase-block__icon" />
|
||||
<span class="loaded-phrase-block__title">Ambiente carregado!</span>
|
||||
</div>
|
||||
<p class="loaded-phrase-block__text">{{ phrase }}</p>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const phrase = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await fetch('/loading-phrases.json')
|
||||
const json = await res.json()
|
||||
const list = json.motivations || []
|
||||
phrase.value = list.length ? list[Math.floor(Math.random() * list.length)] : null
|
||||
} catch {
|
||||
phrase.value = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
197
src/components/ui/PatientCadastroDialog.vue
Normal file
197
src/components/ui/PatientCadastroDialog.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/ui/PatientCadastroDialog.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Dialog de cadastro/edição de paciente.
|
||||
| Abre PatientsCadastroPage em modo dialog (sem navegação de rota).
|
||||
|
|
||||
| Props:
|
||||
| modelValue (Boolean) — controla visibilidade
|
||||
| patientId (String) — null = novo, id = edição
|
||||
|
|
||||
| Emits:
|
||||
| update:modelValue — fecha
|
||||
| created — paciente criado ou atualizado com sucesso
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<template>
|
||||
<Dialog
|
||||
v-model:visible="isOpen"
|
||||
modal
|
||||
:draggable="false"
|
||||
:closable="false"
|
||||
:dismissableMask="false"
|
||||
:maximizable="false"
|
||||
:style="{ width: '90vw', maxWidth: '1100px', height: maximized ? '100vh' : '90vh' }"
|
||||
:contentStyle="{ padding: 0, overflow: 'auto', height: '100%' }"
|
||||
pt:mask:class="backdrop-blur-xs"
|
||||
>
|
||||
<!-- ── Header ─────────────────────────────────────── -->
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between w-full gap-3">
|
||||
<!-- Título -->
|
||||
<span class="text-base font-semibold text-[var(--text-color)] leading-tight">
|
||||
{{ patientId ? 'Editar Paciente' : 'Cadastro de Paciente' }}
|
||||
</span>
|
||||
|
||||
<!-- Botões à direita -->
|
||||
<div class="flex items-center gap-1 ml-auto">
|
||||
|
||||
<!-- Preencher tudo (só testMODE) -->
|
||||
<Button
|
||||
v-if="pageRef?.canSee?.('testMODE')"
|
||||
label="Preencher tudo"
|
||||
icon="pi pi-bolt"
|
||||
severity="secondary"
|
||||
outlined
|
||||
size="small"
|
||||
class="rounded-full"
|
||||
:disabled="pageRef?.saving?.value || pageRef?.deleting?.value"
|
||||
@click="pageRef?.fillRandomPatient?.()"
|
||||
/>
|
||||
|
||||
<!-- Excluir (só em edição) -->
|
||||
<Button
|
||||
v-if="patientId"
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
outlined
|
||||
size="small"
|
||||
class="rounded-full"
|
||||
:loading="pageRef?.deleting?.value"
|
||||
:disabled="pageRef?.saving?.value || pageRef?.deleting?.value"
|
||||
title="Excluir paciente"
|
||||
@click="pageRef?.confirmDelete?.()"
|
||||
/>
|
||||
|
||||
<!-- Maximizar -->
|
||||
<button
|
||||
class="w-8 h-8 rounded-lg border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer grid place-items-center text-sm transition-colors hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)]"
|
||||
:title="maximized ? 'Restaurar' : 'Maximizar'"
|
||||
@click="maximized = !maximized"
|
||||
>
|
||||
<i :class="maximized ? 'pi pi-window-minimize' : 'pi pi-window-maximize'" />
|
||||
</button>
|
||||
|
||||
<!-- Fechar -->
|
||||
<button
|
||||
class="w-8 h-8 rounded-lg border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer grid place-items-center text-sm transition-colors hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)]"
|
||||
title="Fechar"
|
||||
@click="isOpen = false"
|
||||
>
|
||||
<i class="pi pi-times" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ── Conteúdo ────────────────────────────────────── -->
|
||||
<PatientsCadastroPage
|
||||
ref="pageRef"
|
||||
:dialog-mode="true"
|
||||
:patient-id="patientId"
|
||||
@cancel="isOpen = false"
|
||||
@created="onCreated"
|
||||
/>
|
||||
|
||||
<!-- ── Footer ─────────────────────────────────────── -->
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button
|
||||
label="Cancelar"
|
||||
severity="secondary"
|
||||
text
|
||||
:disabled="!!pageRef?.saving?.value || !!pageRef?.deleting?.value"
|
||||
@click="isOpen = false"
|
||||
/>
|
||||
<!-- Na rota de pacientes: só "Salvar" -->
|
||||
<Button
|
||||
v-if="isOnPatientsPage"
|
||||
label="Salvar"
|
||||
:loading="!!pageRef?.saving?.value"
|
||||
:disabled="!!pageRef?.saving?.value || !!pageRef?.deleting?.value"
|
||||
@click="submitWith('only')"
|
||||
/>
|
||||
<!-- Fora da rota de pacientes: "Salvar e fechar" + "Salvar e ver pacientes" -->
|
||||
<template v-else>
|
||||
<Button
|
||||
label="Salvar e fechar"
|
||||
severity="secondary"
|
||||
outlined
|
||||
:loading="pendingMode === 'only' && !!pageRef?.saving?.value"
|
||||
:disabled="!!pageRef?.saving?.value || !!pageRef?.deleting?.value"
|
||||
@click="submitWith('only')"
|
||||
/>
|
||||
<Button
|
||||
label="Salvar e ver pacientes"
|
||||
:loading="pendingMode === 'view' && !!pageRef?.saving?.value"
|
||||
:disabled="!!pageRef?.saving?.value || !!pageRef?.deleting?.value"
|
||||
@click="submitWith('view')"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import PatientsCadastroPage from '@/features/patients/cadastro/PatientsCadastroPage.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
patientId: { type: String, default: null }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'created'])
|
||||
|
||||
const isOpen = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emit('update:modelValue', v)
|
||||
})
|
||||
|
||||
// Reset maximized when dialog opens
|
||||
watch(() => props.modelValue, (v) => { if (!v) maximized.value = false })
|
||||
|
||||
const maximized = ref(false)
|
||||
const pageRef = ref(null)
|
||||
const pendingMode = ref('only')
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const isOnPatientsPage = computed(() => {
|
||||
const p = String(route.path || '')
|
||||
return p.includes('/patients') || p.includes('/pacientes')
|
||||
})
|
||||
|
||||
function patientsListRoute () {
|
||||
const p = String(route.path || '')
|
||||
return p.startsWith('/therapist') ? '/therapist/patients' : '/admin/pacientes'
|
||||
}
|
||||
|
||||
function submitWith (mode) {
|
||||
pendingMode.value = mode
|
||||
pageRef.value?.onSubmit()
|
||||
}
|
||||
|
||||
async function onCreated (data) {
|
||||
isOpen.value = false
|
||||
emit('created', data)
|
||||
if (pendingMode.value === 'view') {
|
||||
await router.push(patientsListRoute())
|
||||
}
|
||||
}
|
||||
</script>
|
||||
208
src/components/ui/PatientCreatePopover.vue
Normal file
208
src/components/ui/PatientCreatePopover.vue
Normal file
@@ -0,0 +1,208 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/ui/PatientCreatePopover.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Popover de cadastro de paciente — usado na página de pacientes e no
|
||||
| menu lateral. Encapsula as 3 ações: Cadastro Rápido, Cadastro Completo
|
||||
| e Link de Cadastro (com URL + copiar).
|
||||
|
|
||||
| Emits:
|
||||
| quick-create — usuário escolheu Cadastro Rápido
|
||||
| go-complete — usuário escolheu Cadastro Completo
|
||||
|
|
||||
| Expose:
|
||||
| toggle(event) — abre/fecha o Popover
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<template>
|
||||
<PatientCadastroDialog v-model="showCadastroDialog" />
|
||||
|
||||
<Popover ref="popRef">
|
||||
<div class="flex flex-col min-w-[230px]">
|
||||
|
||||
<!-- Cadastro rápido -->
|
||||
<button
|
||||
class="flex items-center gap-2.5 px-3 py-2.5 rounded-md cursor-pointer border-0 bg-transparent text-left w-full transition-colors duration-100 hover:bg-[var(--surface-ground,#f8fafc)]"
|
||||
@click="onQuickCreate"
|
||||
>
|
||||
<div class="w-8 h-8 rounded-md flex items-center justify-center flex-shrink-0 bg-indigo-500/10 text-indigo-500">
|
||||
<i class="pi pi-bolt text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-[var(--text-color)]">Cadastro Rápido</div>
|
||||
<div class="text-[0.7rem] text-[var(--text-color-secondary)]">Nome, e-mail e telefone</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Cadastro completo -->
|
||||
<button
|
||||
class="flex items-center gap-2.5 px-3 py-2.5 rounded-md cursor-pointer border-0 bg-transparent text-left w-full transition-colors duration-100 hover:bg-[var(--surface-ground,#f8fafc)]"
|
||||
@click="onGoComplete"
|
||||
>
|
||||
<div class="w-8 h-8 rounded-md flex items-center justify-center flex-shrink-0 bg-emerald-500/10 text-emerald-600">
|
||||
<i class="pi pi-user-plus text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-[var(--text-color)]">Cadastro Completo</div>
|
||||
<div class="text-[0.7rem] text-[var(--text-color-secondary)]">Formulário detalhado</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Divisor -->
|
||||
<div class="mx-3 my-1.5 border-t border-[var(--surface-border,#e2e8f0)]" />
|
||||
|
||||
<!-- Link de cadastro -->
|
||||
<div class="px-3 pb-3">
|
||||
<div class="flex items-center gap-1.5 text-[0.68rem] font-bold uppercase tracking-wider text-[var(--text-color-secondary)] opacity-60 mb-2">
|
||||
<i class="pi pi-link text-[0.6rem]" />
|
||||
Link de cadastro
|
||||
</div>
|
||||
|
||||
<!-- Carregando token -->
|
||||
<div v-if="loadingToken" class="flex items-center gap-1.5 text-xs text-[var(--text-color-secondary)] py-1">
|
||||
<i class="pi pi-spin pi-spinner text-[0.7rem]" /> Carregando link…
|
||||
</div>
|
||||
|
||||
<!-- Sem token ainda -->
|
||||
<div v-else-if="!inviteToken" class="text-[0.7rem] text-[var(--text-color-secondary)] opacity-60 py-1">
|
||||
Nenhum link ativo.
|
||||
<button class="underline cursor-pointer border-0 bg-transparent text-[var(--primary-color,#6366f1)]" @click="loadToken">Tentar novamente</button>
|
||||
</div>
|
||||
|
||||
<!-- URL + ações -->
|
||||
<template v-else>
|
||||
<InputGroup class="w-full">
|
||||
<InputText
|
||||
:value="publicUrl"
|
||||
readonly
|
||||
class="text-[0.68rem] font-mono"
|
||||
style="min-width: 0"
|
||||
/>
|
||||
<InputGroupAddon
|
||||
class="cursor-pointer hover:bg-[var(--surface-hover)] transition-colors"
|
||||
title="Copiar link"
|
||||
@click="copyLink"
|
||||
>
|
||||
<i class="pi pi-copy text-sm" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
|
||||
<div class="flex gap-1 mt-2">
|
||||
<Button
|
||||
label="Copiar mensagem"
|
||||
icon="pi pi-comment"
|
||||
text
|
||||
size="small"
|
||||
class="flex-1 text-xs rounded-full"
|
||||
@click="copyMessage"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-external-link"
|
||||
text
|
||||
size="small"
|
||||
class="rounded-full"
|
||||
v-tooltip.top="'Abrir no navegador'"
|
||||
@click="openLink"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import Popover from 'primevue/popover'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import PatientCadastroDialog from './PatientCadastroDialog.vue'
|
||||
|
||||
const emit = defineEmits(['quick-create'])
|
||||
const showCadastroDialog = ref(false)
|
||||
const toast = useToast()
|
||||
|
||||
const popRef = ref(null)
|
||||
const inviteToken = ref('')
|
||||
const loadingToken = ref(false)
|
||||
let tokenLoaded = false
|
||||
|
||||
const publicUrl = computed(() => {
|
||||
if (!inviteToken.value) return ''
|
||||
return `${window.location.origin}/cadastro/paciente?t=${encodeURIComponent(inviteToken.value)}`
|
||||
})
|
||||
|
||||
async function loadToken () {
|
||||
if (tokenLoaded || loadingToken.value) return
|
||||
loadingToken.value = true
|
||||
try {
|
||||
const { data: authData } = await supabase.auth.getUser()
|
||||
const uid = authData?.user?.id
|
||||
if (!uid) return
|
||||
const { data } = await supabase
|
||||
.from('patient_invites')
|
||||
.select('token')
|
||||
.eq('owner_id', uid)
|
||||
.eq('active', true)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1)
|
||||
if (data?.[0]?.token) {
|
||||
inviteToken.value = data[0].token
|
||||
tokenLoaded = true
|
||||
}
|
||||
} catch { /* silencioso */ } finally {
|
||||
loadingToken.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function toggle (event) {
|
||||
popRef.value?.toggle(event)
|
||||
loadToken()
|
||||
}
|
||||
|
||||
function close () {
|
||||
try { popRef.value?.hide() } catch {}
|
||||
}
|
||||
|
||||
function onQuickCreate () { close(); emit('quick-create') }
|
||||
function onGoComplete () { close(); showCadastroDialog.value = true }
|
||||
|
||||
async function copyLink () {
|
||||
if (!publicUrl.value) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(publicUrl.value)
|
||||
toast.add({ severity: 'success', summary: 'Copiado', detail: 'Link copiado para a área de transferência.', life: 1500 })
|
||||
} catch {
|
||||
window.prompt('Copie o link:', publicUrl.value)
|
||||
}
|
||||
}
|
||||
|
||||
async function copyMessage () {
|
||||
if (!publicUrl.value) return
|
||||
try {
|
||||
const msg = `Olá! Segue o link para seu pré-cadastro. Preencha com calma — campos opcionais podem ficar em branco:\n${publicUrl.value}`
|
||||
await navigator.clipboard.writeText(msg)
|
||||
toast.add({ severity: 'success', summary: 'Copiado', detail: 'Mensagem copiada para a área de transferência.', life: 1500 })
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function openLink () {
|
||||
if (!publicUrl.value) return
|
||||
window.open(publicUrl.value, '_blank', 'noopener')
|
||||
}
|
||||
|
||||
defineExpose({ toggle, close })
|
||||
</script>
|
||||
@@ -1,5 +1,19 @@
|
||||
// src/composables/useDocsHealth.js
|
||||
// Singleton que computa métricas de saúde dos documentos.
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/Usedocshealth.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
// Critério de "atenção": mais de 30% dos votos são negativos
|
||||
// (mínimo 3 votos totais para evitar falso-positivo com 1 voto negativo).
|
||||
//
|
||||
|
||||
271
src/composables/useAgendaFinanceiro.js
Normal file
271
src/composables/useAgendaFinanceiro.js
Normal file
@@ -0,0 +1,271 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/useAgendaFinanceiro.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* useAgendaFinanceiro
|
||||
*
|
||||
* Camada de orquestração entre agenda e financeiro.
|
||||
* Não modifica useAgendaEvents diretamente — recebe a função de update
|
||||
* como parâmetro para manter o desacoplamento.
|
||||
*
|
||||
* Uso:
|
||||
* const { handleStatusChange, gerarCobrancaManual } = useAgendaFinanceiro()
|
||||
*
|
||||
* // No handler de save do componente pai:
|
||||
* await handleStatusChange(eventoOriginal, novoStatus, agendaEvents.update)
|
||||
*/
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { useTenantStore } from '@/stores/tenantStore'
|
||||
|
||||
// ─── Cache de exceções financeiras (vive enquanto o módulo estiver carregado) ─
|
||||
// Chave: `${tenantId}:${exceptionType}` → FinancialException | null
|
||||
const _exceptionsCache = new Map()
|
||||
|
||||
// ─── helper ──────────────────────────────────────────────────────────────────
|
||||
async function getUid () {
|
||||
const { data, error } = await supabase.auth.getUser()
|
||||
if (error) throw error
|
||||
const uid = data?.user?.id
|
||||
if (!uid) throw new Error('Usuário não autenticado.')
|
||||
return uid
|
||||
}
|
||||
|
||||
// ─── mapeamento: status anterior → tipo de exceção a consultar ───────────────
|
||||
const STATUS_TO_EXCEPTION = {
|
||||
faltou: 'patient_no_show',
|
||||
cancelado: 'patient_cancellation',
|
||||
}
|
||||
|
||||
// ─── calcular valor cobrado por charge_mode ───────────────────────────────────
|
||||
function calcChargeAmount (originalAmount, rule) {
|
||||
if (!rule || rule.charge_mode === 'none') return 0
|
||||
if (rule.charge_mode === 'full') return originalAmount
|
||||
if (rule.charge_mode === 'fixed') return rule.charge_value ?? 0
|
||||
if (rule.charge_mode === 'percentage') {
|
||||
const pct = rule.charge_pct ?? 0
|
||||
return parseFloat(((originalAmount * pct) / 100).toFixed(2))
|
||||
}
|
||||
return originalAmount
|
||||
}
|
||||
|
||||
// ─── composable ──────────────────────────────────────────────────────────────
|
||||
export function useAgendaFinanceiro () {
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
// ── getFinancialExceptionRule ─────────────────────────────────────────────
|
||||
/**
|
||||
* Busca a regra de exceção financeira para um tipo, com cache em memória.
|
||||
* Prioridade: regra própria do owner > regra global do tenant (owner_id IS NULL)
|
||||
*
|
||||
* @param {string} tenantId
|
||||
* @param {'patient_no_show'|'patient_cancellation'|'professional_cancellation'} exceptionType
|
||||
* @returns {Promise<Object|null>}
|
||||
*/
|
||||
async function getFinancialExceptionRule (tenantId, exceptionType) {
|
||||
const cacheKey = `${tenantId}:${exceptionType}`
|
||||
if (_exceptionsCache.has(cacheKey)) return _exceptionsCache.get(cacheKey)
|
||||
|
||||
const uid = await getUid()
|
||||
|
||||
const { data, error: err } = await supabase
|
||||
.from('financial_exceptions')
|
||||
.select('*')
|
||||
.eq('tenant_id', tenantId)
|
||||
.eq('exception_type', exceptionType)
|
||||
.or(`owner_id.eq.${uid},owner_id.is.null`)
|
||||
.order('owner_id', { ascending: false, nullsLast: true }) // owner próprio tem prioridade
|
||||
.limit(1)
|
||||
.maybeSingle()
|
||||
|
||||
if (err) {
|
||||
console.warn('[useAgendaFinanceiro] getFinancialExceptionRule:', err.message)
|
||||
return null
|
||||
}
|
||||
|
||||
_exceptionsCache.set(cacheKey, data ?? null)
|
||||
return data ?? null
|
||||
}
|
||||
|
||||
// ── gerarCobrancaManual ───────────────────────────────────────────────────
|
||||
/**
|
||||
* Gera cobrança para uma sessão existente com `billed = false`.
|
||||
* Chama a RPC `create_financial_record_for_session`.
|
||||
*
|
||||
* @param {Object} evento - linha de agenda_eventos (com campo price)
|
||||
* @returns {Promise<{ok: boolean, data?: Object, error?: string}>}
|
||||
*/
|
||||
async function gerarCobrancaManual (evento) {
|
||||
if (evento.billing_contract_id) {
|
||||
// sessão de pacote — não gera cobrança individual
|
||||
return { ok: false, error: 'Sessão faz parte de um pacote. Cobrança gerenciada pelo contrato.' }
|
||||
}
|
||||
|
||||
const tenantStore = useTenantStore()
|
||||
const tenantId = tenantStore.activeTenantId
|
||||
if (!tenantId) return { ok: false, error: 'Tenant não identificado.' }
|
||||
|
||||
const ownerId = await getUid()
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const amount = evento.price ?? 0
|
||||
const dueDate = evento.inicio_em
|
||||
? new Date(evento.inicio_em).toISOString().slice(0, 10)
|
||||
: new Date().toISOString().slice(0, 10)
|
||||
|
||||
const { data, error: err } = await supabase.rpc('create_financial_record_for_session', {
|
||||
p_tenant_id: tenantId,
|
||||
p_owner_id: ownerId,
|
||||
p_patient_id: evento.patient_id ?? evento.paciente_id ?? null,
|
||||
p_agenda_evento_id: evento.id,
|
||||
p_amount: amount,
|
||||
p_due_date: dueDate,
|
||||
})
|
||||
|
||||
if (err) throw err
|
||||
|
||||
return { ok: true, data }
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Erro ao gerar cobrança.'
|
||||
return { ok: false, error: error.value }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── handleStatusChange ────────────────────────────────────────────────────
|
||||
/**
|
||||
* Orquestra a mudança de status de uma sessão + consequências financeiras.
|
||||
*
|
||||
* @param {Object} evento - linha atual de agenda_eventos (ANTES da mudança)
|
||||
* @param {string} novoStatus - novo status a aplicar
|
||||
* @param {Function} agendaUpdateFn - função que aplica o update na agenda (ex: agendaEvents.update)
|
||||
* signature: (id, patch) => Promise<void>
|
||||
* @returns {Promise<{ok: boolean, error?: string}>}
|
||||
*/
|
||||
async function handleStatusChange (evento, novoStatus, agendaUpdateFn) {
|
||||
// bloqueios e sessões de pacote não têm cobrança individual
|
||||
const ignorar = evento.tipo !== 'sessao' || !!evento.billing_contract_id
|
||||
const statusAnterior = evento.status
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
// 1. Aplica o update na agenda sempre (fonte da verdade é a agenda)
|
||||
await agendaUpdateFn(evento.id, { status: novoStatus })
|
||||
|
||||
if (ignorar) return { ok: true }
|
||||
if (statusAnterior === novoStatus) return { ok: true }
|
||||
|
||||
// 2. Lógica financeira por transição
|
||||
const tenantStore = useTenantStore()
|
||||
const tenantId = tenantStore.activeTenantId
|
||||
|
||||
// ── faltou / cancelado → consultar exceção financeira ──────────────
|
||||
const exceptionType = STATUS_TO_EXCEPTION[novoStatus]
|
||||
|
||||
if (exceptionType) {
|
||||
const rule = await getFinancialExceptionRule(tenantId, exceptionType)
|
||||
|
||||
if (!rule || rule.charge_mode === 'none') {
|
||||
// Cancelar cobrança existente, se houver
|
||||
if (evento.billed) {
|
||||
const { data: existingRec } = await supabase
|
||||
.from('financial_records')
|
||||
.select('id, status')
|
||||
.eq('agenda_evento_id', evento.id)
|
||||
.in('status', ['pending', 'overdue'])
|
||||
.maybeSingle()
|
||||
|
||||
if (existingRec) {
|
||||
await supabase
|
||||
.from('financial_records')
|
||||
.update({ status: 'cancelled', updated_at: new Date().toISOString() })
|
||||
.eq('id', existingRec.id)
|
||||
}
|
||||
}
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
// charge_mode != 'none' → ajustar valor da cobrança existente ou criar nova
|
||||
const chargeAmount = calcChargeAmount(evento.price ?? 0, rule)
|
||||
|
||||
if (evento.billed) {
|
||||
// Atualiza o valor da cobrança existente
|
||||
const { data: existingRec } = await supabase
|
||||
.from('financial_records')
|
||||
.select('id')
|
||||
.eq('agenda_evento_id', evento.id)
|
||||
.in('status', ['pending', 'overdue'])
|
||||
.maybeSingle()
|
||||
|
||||
if (existingRec) {
|
||||
await supabase
|
||||
.from('financial_records')
|
||||
.update({
|
||||
amount: chargeAmount,
|
||||
final_amount: chargeAmount,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq('id', existingRec.id)
|
||||
}
|
||||
} else if (chargeAmount > 0) {
|
||||
// Sessão sem cobrança: gera uma nova com o valor ajustado
|
||||
await gerarCobrancaManual({ ...evento, price: chargeAmount })
|
||||
}
|
||||
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
// ── remarcar → atualizar due_date da cobrança existente ────────────
|
||||
if (novoStatus === 'remarcar' && evento.billed) {
|
||||
// due_date mantém a data da sessão original por enquanto
|
||||
// (a nova data virá quando a sessão for reagendada)
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
// ── agendado → realizado: nenhuma ação financeira automática ────────
|
||||
return { ok: true }
|
||||
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Erro ao processar mudança de status.'
|
||||
return { ok: false, error: error.value }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── invalidar cache (use quando o usuário altera exceções financeiras) ───
|
||||
function invalidateExceptionsCache () {
|
||||
_exceptionsCache.clear()
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
handleStatusChange,
|
||||
gerarCobrancaManual,
|
||||
getFinancialExceptionRule,
|
||||
invalidateExceptionsCache,
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,19 @@
|
||||
// src/composables/useAjuda.js
|
||||
// Composable singleton para o drawer de ajuda.
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/useAjuda.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
// - Home: docs da sessão atual + outros docs paginados + FAQ
|
||||
// - Navegação interna com stack (voltar)
|
||||
// - Votação por usuário (útil / não útil)
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/useAuth.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------
|
||||
* useAuth()
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
// src/composables/useDocsAdmin.js
|
||||
// Estado compartilhado para abrir o dialog de edição de um doc
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/useDocsAdmin.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
// a partir de outra página (ex: SaasFaqPage → SaasDocsPage).
|
||||
|
||||
import { ref } from 'vue'
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
// src/composables/useFeriados.js
|
||||
// Fonte única de verdade para feriados: nacionais (algoritmo) + municipais (Supabase).
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/useFeriados.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import { ref, computed } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
328
src/composables/useFinancialRecords.js
Normal file
328
src/composables/useFinancialRecords.js
Normal file
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/useFinancialRecords.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import { ref, computed } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { useTenantStore } from '@/stores/tenantStore'
|
||||
|
||||
// ─── helpers internos ────────────────────────────────────────────────────────
|
||||
|
||||
function assertTenantId (tenantId) {
|
||||
if (!tenantId || tenantId === 'null' || tenantId === 'undefined') {
|
||||
throw new Error('Tenant ativo inválido. Selecione a clínica antes de operar no financeiro.')
|
||||
}
|
||||
}
|
||||
|
||||
async function getUid () {
|
||||
const { data, error } = await supabase.auth.getUser()
|
||||
if (error) throw error
|
||||
const uid = data?.user?.id
|
||||
if (!uid) throw new Error('Usuário não autenticado.')
|
||||
return uid
|
||||
}
|
||||
|
||||
// ─── select base com joins ───────────────────────────────────────────────────
|
||||
|
||||
const BASE_SELECT = `
|
||||
id, tenant_id, owner_id, patient_id, agenda_evento_id,
|
||||
amount, discount_amount, final_amount,
|
||||
status, due_date, paid_at, payment_method,
|
||||
notes, created_at, updated_at,
|
||||
patients!patient_id (
|
||||
id, nome_completo, identification_color
|
||||
),
|
||||
agenda_eventos!agenda_evento_id (
|
||||
id, inicio_em, status, tipo
|
||||
)
|
||||
`.trim()
|
||||
|
||||
// ─── composable ──────────────────────────────────────────────────────────────
|
||||
|
||||
export function useFinancialRecords () {
|
||||
const records = ref([])
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
// ── computed: resumo financeiro ─────────────────────────────────────────
|
||||
|
||||
const summary = computed(() => {
|
||||
const now = new Date()
|
||||
const thisYear = now.getFullYear()
|
||||
const thisMonth = now.getMonth() // 0-indexed
|
||||
|
||||
const countByStatus = { pending: 0, paid: 0, overdue: 0, cancelled: 0 }
|
||||
|
||||
let totalPending = 0
|
||||
let totalOverdue = 0
|
||||
let totalPaidThisMonth = 0
|
||||
|
||||
for (const r of records.value) {
|
||||
countByStatus[r.status] = (countByStatus[r.status] ?? 0) + 1
|
||||
|
||||
if (r.status === 'pending') {
|
||||
totalPending += r.final_amount ?? r.amount ?? 0
|
||||
}
|
||||
|
||||
if (r.status === 'overdue') {
|
||||
totalOverdue += r.final_amount ?? r.amount ?? 0
|
||||
}
|
||||
|
||||
if (r.status === 'paid' && r.paid_at) {
|
||||
const d = new Date(r.paid_at)
|
||||
if (d.getFullYear() === thisYear && d.getMonth() === thisMonth) {
|
||||
totalPaidThisMonth += r.final_amount ?? r.amount ?? 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { totalPending, totalOverdue, totalPaidThisMonth, countByStatus }
|
||||
})
|
||||
|
||||
// ── fetchRecords ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @param {Object} [filters]
|
||||
* @param {string} [filters.status] - 'pending'|'paid'|'overdue'|'partial'|'cancelled'|'refunded'
|
||||
* @param {string} [filters.type] - 'receita'|'despesa'
|
||||
* @param {string} [filters.patient_id] - UUID
|
||||
* @param {string} [filters.due_date_from] - ISO date string
|
||||
* @param {string} [filters.due_date_to] - ISO date string
|
||||
* @param {number} [filters.limit] - rows per page (default 50)
|
||||
* @param {number} [filters.offset] - row offset for pagination (default 0)
|
||||
* @returns {Promise<{total: number}>}
|
||||
*/
|
||||
async function fetchRecords (filters = {}) {
|
||||
const tenantStore = useTenantStore()
|
||||
const tenantId = tenantStore.activeTenantId
|
||||
assertTenantId(tenantId)
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
const limit = filters.limit ?? 50
|
||||
const offset = filters.offset ?? 0
|
||||
|
||||
try {
|
||||
let query = supabase
|
||||
.from('financial_records')
|
||||
.select(BASE_SELECT, { count: 'exact' })
|
||||
.eq('tenant_id', tenantId)
|
||||
.is('deleted_at', null)
|
||||
.order('due_date', { ascending: false })
|
||||
.range(offset, offset + limit - 1)
|
||||
|
||||
if (filters.status) query = query.eq('status', filters.status)
|
||||
if (filters.type) query = query.eq('type', filters.type)
|
||||
if (filters.patient_id) query = query.eq('patient_id', filters.patient_id)
|
||||
if (filters.due_date_from) query = query.gte('due_date', filters.due_date_from)
|
||||
if (filters.due_date_to) query = query.lte('due_date', filters.due_date_to)
|
||||
|
||||
const { data, error: err, count } = await query
|
||||
if (err) throw err
|
||||
|
||||
records.value = data ?? []
|
||||
return { total: count ?? 0 }
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Erro ao carregar registros financeiros.'
|
||||
records.value = []
|
||||
return { total: 0 }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── createRecord (via RPC — para sessões) ────────────────────────────────
|
||||
|
||||
/**
|
||||
* @param {Object} payload
|
||||
* @param {string} payload.patient_id
|
||||
* @param {string} payload.agenda_evento_id
|
||||
* @param {number} payload.amount
|
||||
* @param {string} payload.due_date - ISO date string
|
||||
*/
|
||||
async function createRecord (payload) {
|
||||
const tenantStore = useTenantStore()
|
||||
const tenantId = tenantStore.activeTenantId
|
||||
assertTenantId(tenantId)
|
||||
|
||||
const ownerId = await getUid()
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const { data, error: err } = await supabase.rpc('create_financial_record_for_session', {
|
||||
p_tenant_id: tenantId,
|
||||
p_owner_id: ownerId,
|
||||
p_patient_id: payload.patient_id,
|
||||
p_agenda_evento_id: payload.agenda_evento_id,
|
||||
p_amount: payload.amount,
|
||||
p_due_date: payload.due_date,
|
||||
})
|
||||
|
||||
if (err) throw err
|
||||
|
||||
// Re-fetch para garantir joins completos
|
||||
await fetchRecords()
|
||||
|
||||
return { ok: true, data }
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Erro ao criar cobrança.'
|
||||
return { ok: false, error: e?.message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── createManualRecord (INSERT direto — sem sessão) ──────────────────────
|
||||
|
||||
/**
|
||||
* @param {Object} payload
|
||||
* @param {string} payload.patient_id
|
||||
* @param {number} payload.amount
|
||||
* @param {number} [payload.discount_amount]
|
||||
* @param {string} payload.due_date
|
||||
* @param {string} [payload.status] - default 'pending'
|
||||
* @param {string} [payload.payment_method]
|
||||
* @param {string} [payload.notes]
|
||||
*/
|
||||
async function createManualRecord (payload) {
|
||||
const tenantStore = useTenantStore()
|
||||
const tenantId = tenantStore.activeTenantId
|
||||
assertTenantId(tenantId)
|
||||
|
||||
const ownerId = await getUid()
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const discount = payload.discount_amount ?? 0
|
||||
const amount = payload.amount ?? 0
|
||||
|
||||
const { data, error: err } = await supabase
|
||||
.from('financial_records')
|
||||
.insert([{
|
||||
tenant_id: tenantId,
|
||||
owner_id: ownerId,
|
||||
patient_id: payload.patient_id ?? null,
|
||||
agenda_evento_id: null,
|
||||
amount,
|
||||
discount_amount: discount,
|
||||
final_amount: amount - discount,
|
||||
status: payload.status ?? 'pending',
|
||||
due_date: payload.due_date,
|
||||
payment_method: payload.payment_method ?? null,
|
||||
notes: payload.notes ?? null,
|
||||
}])
|
||||
.select(BASE_SELECT)
|
||||
.single()
|
||||
|
||||
if (err) throw err
|
||||
|
||||
records.value = [data, ...records.value]
|
||||
|
||||
return { ok: true, data }
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Erro ao criar lançamento manual.'
|
||||
return { ok: false, error: e?.message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── markAsPaid ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @param {string} recordId
|
||||
* @param {string} paymentMethod - 'pix' | 'deposito' | 'dinheiro' | 'cartao' | 'convenio'
|
||||
*/
|
||||
async function markAsPaid (recordId, paymentMethod) {
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const { data, error: err } = await supabase.rpc('mark_as_paid', {
|
||||
p_financial_record_id: recordId,
|
||||
p_payment_method: paymentMethod,
|
||||
})
|
||||
|
||||
if (err) throw err
|
||||
|
||||
// RPC retorna SETOF (array) — patch local direto, sem depender do retorno
|
||||
const idx = records.value.findIndex(r => r.id === recordId)
|
||||
if (idx !== -1) {
|
||||
records.value[idx] = {
|
||||
...records.value[idx],
|
||||
status: 'paid',
|
||||
paid_at: new Date().toISOString(),
|
||||
payment_method: paymentMethod,
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true }
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Erro ao marcar como pago.'
|
||||
return { ok: false, error: e?.message }
|
||||
}
|
||||
}
|
||||
|
||||
// ── cancelRecord ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @param {string} recordId
|
||||
*/
|
||||
async function cancelRecord (recordId) {
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const { error: err } = await supabase
|
||||
.from('financial_records')
|
||||
.update({ status: 'cancelled', updated_at: new Date().toISOString() })
|
||||
.eq('id', recordId)
|
||||
|
||||
if (err) throw err
|
||||
|
||||
// Atualiza localmente sem re-fetch
|
||||
const idx = records.value.findIndex(r => r.id === recordId)
|
||||
if (idx !== -1) {
|
||||
records.value[idx] = { ...records.value[idx], status: 'cancelled' }
|
||||
}
|
||||
|
||||
return { ok: true }
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Erro ao cancelar registro.'
|
||||
return { ok: false, error: e?.message }
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
return {
|
||||
// estado
|
||||
records,
|
||||
loading,
|
||||
error,
|
||||
// computed
|
||||
summary,
|
||||
// ações
|
||||
fetchRecords,
|
||||
createRecord,
|
||||
createManualRecord,
|
||||
markAsPaid,
|
||||
cancelRecord,
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,19 @@
|
||||
// src/composables/useMenuBadges.js
|
||||
// Singleton — contadores para badges do menu (agenda hoje, cadastros e agendamentos recebidos)
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/useMenuBadges.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
// src/composables/useNotifications.js
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/useNotifications.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { useNotificationStore } from '@/stores/notificationStore'
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
// src/composables/usePatientLifecycle.js
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/usePatientLifecycle.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
export function usePatientLifecycle () {
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
// src/composables/usePlatformPermissions.js
|
||||
//
|
||||
// Permissões de PLATAFORMA (globais, não vinculadas a tenant).
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/usePlatformPermissions.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
// Distinto do RBAC de tenant (useRoleGuard).
|
||||
//
|
||||
// O campo `platform_roles text[]` na tabela `profiles` do Supabase
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/useRoleGuard.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { computed } from 'vue'
|
||||
import { useTenantStore } from '@/stores/tenantStore'
|
||||
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
// src/composables/useUserSettingsPersistence.js
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/useUserSettingsPersistence.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { useLayout } from '@/layout/composables/layout'
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
// src/constants/roles.js
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/constants/roles.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Roles canônicas do sistema (tenant-level)
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
<!-- src/features/agenda/components/AgendaCalendar.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/components/AgendaCalendar.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { computed, ref, watch, onMounted } from 'vue'
|
||||
|
||||
@@ -151,9 +166,12 @@ onMounted(() => {
|
||||
<template>
|
||||
<div class="agenda-calendar-wrap">
|
||||
<div v-if="loading" class="agenda-calendar-loading">
|
||||
<ProgressSpinner strokeWidth="3" />
|
||||
<div class="text-sm mt-2" style="color: var(--text-color-secondary);">
|
||||
Carregando agenda…
|
||||
<div class="flex flex-col gap-2 w-full px-2 py-2">
|
||||
<Skeleton height="2rem" class="mb-1" />
|
||||
<div class="grid grid-cols-7 gap-1">
|
||||
<Skeleton v-for="n in 7" :key="n" height="1.25rem" />
|
||||
</div>
|
||||
<Skeleton v-for="n in 8" :key="'row' + n" height="3rem" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
<!-- src/features/agenda/components/AgendaClinicCalendar.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/components/AgendaClinicCalendar.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
<!-- src/features/agenda/components/AgendaClinicMosaic.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/components/AgendaClinicMosaic.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { computed, ref, watch, nextTick } from 'vue'
|
||||
|
||||
import FullCalendar from '@fullcalendar/vue3'
|
||||
import timeGridPlugin from '@fullcalendar/timegrid'
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||
import listPlugin from '@fullcalendar/list'
|
||||
import interactionPlugin from '@fullcalendar/interaction'
|
||||
import ptBrLocale from '@fullcalendar/core/locales/pt-br'
|
||||
|
||||
@@ -111,7 +127,7 @@ function gotoDate (date) {
|
||||
}
|
||||
|
||||
function setView (v) {
|
||||
const target = v === 'week' ? 'timeGridWeek' : (v === 'month' ? 'dayGridMonth' : 'timeGridDay')
|
||||
const target = v === 'week' ? 'timeGridWeek' : v === 'month' ? 'dayGridMonth' : v === 'list' ? 'listWeek' : 'timeGridDay'
|
||||
forEachApi(api => api.changeView(target))
|
||||
}
|
||||
function setMode () {}
|
||||
@@ -241,7 +257,7 @@ function emitDebug (col) {
|
||||
|
||||
function buildFcOptions (ownerId) {
|
||||
const base = {
|
||||
plugins: [timeGridPlugin, dayGridPlugin, interactionPlugin],
|
||||
plugins: [timeGridPlugin, dayGridPlugin, listPlugin, interactionPlugin],
|
||||
locale: ptBrLocale,
|
||||
timeZone: props.timezone,
|
||||
|
||||
@@ -269,6 +285,12 @@ function buildFcOptions (ownerId) {
|
||||
|
||||
businessHours: props.businessHours,
|
||||
|
||||
views: {
|
||||
timeGridDay: {
|
||||
dayHeaderFormat: { day: 'numeric', month: 'long', year: 'numeric' },
|
||||
},
|
||||
},
|
||||
|
||||
height: 'auto',
|
||||
expandRows: true,
|
||||
allDaySlot: false,
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
<!-- src/features/agenda/components/AgendaEventDialog.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/components/AgendaEventDialog.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<template>
|
||||
<Dialog
|
||||
v-model:visible="visible"
|
||||
@@ -457,7 +472,7 @@
|
||||
:style="{ background: `#${selectedCommitment.bg_color}20`, color: `#${selectedCommitment.bg_color}`, borderColor: `#${selectedCommitment.bg_color}40` }"
|
||||
>{{ selectedCommitmentName }}</span>
|
||||
<Tag v-else :value="selectedCommitmentName" severity="info" />
|
||||
<Tag v-if="isSessionEvent" :value="labelStatusSessao(form.status)" :severity="statusSeverity(form.status)" />
|
||||
<Tag v-if="isSessionEvent" :value="labelStatusSessao(form.status)" :severity="statusSeverity(form.status)" :class="statusExtraClass(form.status)" />
|
||||
</div>
|
||||
|
||||
<div class="summary-row">
|
||||
@@ -713,6 +728,13 @@
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<!-- ── COBRANÇA DA SESSÃO ──────────────────────────── -->
|
||||
<AgendaEventoFinanceiroPanel
|
||||
v-if="isSessionEvent && isEdit && eventRow?.id"
|
||||
:evento="eventRow"
|
||||
class="mb-3"
|
||||
/>
|
||||
|
||||
<!-- Opção de recorrência para sessão SEM série (criação ou avulsa) -->
|
||||
<template v-if="!hasSerie">
|
||||
<div class="side-card__title mb-2">Frequência</div>
|
||||
@@ -1147,8 +1169,10 @@ import InputNumber from 'primevue/inputnumber'
|
||||
import RadioButton from 'primevue/radiobutton'
|
||||
import Message from 'primevue/message'
|
||||
import { useConfirm } from 'primevue/useconfirm'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import ComponentCadastroRapido from '@/components/ComponentCadastroRapido.vue'
|
||||
import AgendaEventoFinanceiroPanel from '@/components/agenda/AgendaEventoFinanceiroPanel.vue'
|
||||
import { useServices } from '@/features/agenda/composables/useServices'
|
||||
import { useCommitmentServices } from '@/features/agenda/composables/useCommitmentServices'
|
||||
import { usePatientDiscounts } from '@/features/agenda/composables/usePatientDiscounts'
|
||||
@@ -1193,8 +1217,9 @@ const props = defineProps({
|
||||
newPatientRoute: { type: String, default: '' },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'save', 'delete', 'updateSeriesEvent', 'editSeriesOccurrence'])
|
||||
const emit = defineEmits(['update:modelValue', 'save', 'delete', 'updateSeriesEvent', 'editSeriesOccurrence', 'updated'])
|
||||
const confirm = useConfirm()
|
||||
const toast = useToast()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
@@ -1205,6 +1230,7 @@ const visible = computed({
|
||||
|
||||
const step = ref(1)
|
||||
const isEdit = computed(() => !!props.eventRow?.id || !!props.eventRow?.is_occurrence)
|
||||
|
||||
const allowBack = computed(() => !props.lockCommitment && !props.presetCommitmentId)
|
||||
|
||||
// ── série ─────────────────────────────────────────────────
|
||||
@@ -1405,6 +1431,48 @@ function isNativeSession (c) {
|
||||
|
||||
const form = ref(resetForm())
|
||||
|
||||
// ── ConfirmDialog para status sensíveis (cancelado / remarcar) ────────────
|
||||
const _prevStatus = ref(null)
|
||||
const _skipStatusWatch = ref(false)
|
||||
watch(() => form.value?.status, async (newVal, oldVal) => {
|
||||
if (_skipStatusWatch.value) return
|
||||
if (!isEdit.value || !form.value?.id) return
|
||||
if (newVal !== 'cancelado' && newVal !== 'remarcar') return
|
||||
|
||||
_prevStatus.value = oldVal
|
||||
|
||||
const isCancelar = newVal === 'cancelado'
|
||||
confirm.require({
|
||||
header: isCancelar ? 'Cancelar sessão' : 'Remarcar sessão',
|
||||
message: isCancelar
|
||||
? 'Tem certeza que deseja cancelar esta sessão? O status será salvo imediatamente.'
|
||||
: 'Tem certeza que deseja marcar esta sessão para remarcação? O status será salvo imediatamente.',
|
||||
icon: isCancelar ? 'pi pi-times-circle' : 'pi pi-refresh',
|
||||
acceptLabel: 'Sim, confirmar',
|
||||
rejectLabel: 'Não',
|
||||
acceptSeverity: isCancelar ? 'danger' : 'warn',
|
||||
accept: async () => {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('agenda_eventos')
|
||||
.update({ status: newVal })
|
||||
.eq('id', form.value.id)
|
||||
.select()
|
||||
.single()
|
||||
if (error) throw error
|
||||
toast.add({ severity: 'success', summary: 'Status atualizado', detail: `Sessão marcada como ${labelStatusSessao(newVal)}.`, life: 3000 })
|
||||
emit('updated', data)
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Não foi possível atualizar o status.', life: 4000 })
|
||||
form.value.status = _prevStatus.value
|
||||
}
|
||||
},
|
||||
reject: () => {
|
||||
form.value.status = _prevStatus.value
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// ── Precificação / Serviços ─────────────────────────────────────────
|
||||
const { services, getDefaultPrice, load: loadServices } = useServices()
|
||||
const { loadItems: _csLoadItems, saveItems: saveCommitmentItems, loadItemsOrTemplate: _csLoadItemsOrTemplate } = useCommitmentServices()
|
||||
@@ -1818,7 +1886,10 @@ watch(
|
||||
console.log('[AgendaEventDialog] abriu — eventRow:', JSON.parse(JSON.stringify(props.eventRow || {})))
|
||||
console.log('[AgendaEventDialog] isEdit:', isEdit.value, 'hasSerie:', hasSerie.value)
|
||||
|
||||
_skipStatusWatch.value = true
|
||||
form.value = resetForm()
|
||||
await nextTick()
|
||||
_skipStatusWatch.value = false
|
||||
samePatientConflict.value = null
|
||||
recorrenciaType.value = 'avulsa'
|
||||
diasSelecionados.value = []
|
||||
@@ -2760,21 +2831,28 @@ const googleCalendarUrl = computed(() => {
|
||||
})
|
||||
|
||||
function labelStatusSessao (v) {
|
||||
const map = { agendado: 'Agendado', realizado: 'Realizado', faltou: 'Faltou', cancelado: 'Cancelado' }
|
||||
const map = { agendado: 'Agendado', realizado: 'Realizado', faltou: 'Faltou', cancelado: 'Cancelado', remarcar: 'Remarcar' }
|
||||
return map[v] || '—'
|
||||
}
|
||||
function statusSeverity (v) {
|
||||
if (v === 'agendado') return 'success'
|
||||
if (v === 'realizado') return 'secondary'
|
||||
if (v === 'faltou') return 'danger'
|
||||
if (v === 'agendado') return 'info'
|
||||
if (v === 'realizado') return 'success'
|
||||
if (v === 'faltou') return 'warn'
|
||||
if (v === 'cancelado') return 'danger'
|
||||
if (v === 'remarcar') return 'secondary' // cor real via classe CSS
|
||||
return 'secondary'
|
||||
}
|
||||
function statusExtraClass (v) {
|
||||
return v === 'remarcar' ? 'tag-remarcar' : ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.agenda-event-composer :deep(.p-dialog-content) { padding: .75rem; }
|
||||
|
||||
/* ── tag: remarcar (roxo — sem severity nativo no PrimeVue) ─ */
|
||||
:deep(.tag-remarcar) { background: #a855f7 !important; color: #fff !important; }
|
||||
|
||||
/* ── header dot ─────────────────────────────────── */
|
||||
.header-dot {
|
||||
width: 10px; height: 10px; border-radius: 50%;
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
<!-- src/features/agenda/components/AgendaRightPanel.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/components/AgendaRightPanel.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/components/AgendaToolbar.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
<!-- src/features/agenda/components/BloqueioDialog.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/components/BloqueioDialog.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/components/ConflictBanner.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/components/DeterminedCommitmentDialog.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<template>
|
||||
<Dialog
|
||||
v-model:visible="visible"
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/components/PreviewTimeline.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
<!-- src/features/agenda/components/ProximosFeriadosCard.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/components/ProximosFeriadosCard.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -223,8 +238,8 @@ function fmtDate (iso) {
|
||||
|
||||
<!-- Lista -->
|
||||
<div class="px-4 py-3">
|
||||
<div v-if="loading" class="flex justify-center py-3">
|
||||
<i class="pi pi-spinner pi-spin opacity-40" />
|
||||
<div v-if="loading" class="flex flex-col gap-2 py-1">
|
||||
<Skeleton v-for="n in 3" :key="n" height="2rem" class="rounded" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="!feriadosMes.length" class="text-sm text-[var(--text-color-secondary)] py-1">
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
<!-- src/features/agenda/components/AgendaToolbar.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/components/cards/AgendaNextSessionsCardList.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
<!-- src/features/agenda/components/cards/AgendaPulseCardGrid.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/components/cards/AgendaPulseCardGrid.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
<!-- src/features/agenda/components/dev/AgendaDevDocs.vue
|
||||
Documentação técnica da Agenda — exibida apenas em modo suporte/dev.
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/components/dev/AgendaDevDocs.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<!-- Documentação técnica da Agenda — exibida apenas em modo suporte/dev.
|
||||
Acessível via SupportDebugBanner → botão "Docs". -->
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
// src/features/agenda/composables/useAgendaClinicEvents.js
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/composables/useAgendaClinicEvents.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
import {
|
||||
listClinicEvents,
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
// src/features/agenda/composables/useAgendaClinicStaff.js
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/composables/useAgendaClinicStaff.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
import { listTenantStaff } from '../services/agendaRepository'
|
||||
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/composables/useAgendaEvents.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
/**
|
||||
* useAgendaEvents.js
|
||||
* src/features/agenda/composables/useAgendaEvents.js
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/composables/useAgendaLimits.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/composables/useAgendaPermissions.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/composables/useAgendaQuery.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
// src/features/agenda/composables/useAgendaSettings.js
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/composables/useAgendaSettings.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
import { getMyAgendaSettings, getMyWorkSchedule } from '../services/agendaRepository'
|
||||
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
// src/features/agenda/composables/useCommitmentServices.js
|
||||
//
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/composables/useCommitmentServices.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
// CRUD de commitment_services — itens de serviço vinculados a um evento.
|
||||
// CRUD de recurrence_rule_services — template de serviços de uma regra de recorrência.
|
||||
//
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/composables/useDeterminedCommitments.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { computed, ref } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
// src/features/agenda/composables/useFinancialExceptions.js
|
||||
//
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/composables/useFinancialExceptions.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
// CRUD sobre a tabela public.financial_exceptions.
|
||||
//
|
||||
// Interface pública:
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
// src/features/agenda/composables/useInsurancePlans.js
|
||||
//
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/composables/useInsurancePlans.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
// Interface pública:
|
||||
// plans – ref([]) todos os planos do owner (ativos e inativos)
|
||||
// loading – ref(false)
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
// src/features/agenda/composables/usePatientDiscounts.js
|
||||
//
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/composables/usePatientDiscounts.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
// CRUD completo sobre a tabela public.patient_discounts.
|
||||
//
|
||||
// Interface pública:
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
// src/features/agenda/composables/useProfessionalPricing.js
|
||||
//
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/composables/useProfessionalPricing.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
// Carrega a tabela professional_pricing do owner logado e expõe
|
||||
// getPriceFor(commitmentId) → number | null
|
||||
//
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/composables/useRecurrence.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
/**
|
||||
* useRecurrence.js
|
||||
* src/features/agenda/composables/useRecurrence.js
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
// src/features/agenda/composables/useServices.js
|
||||
//
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/composables/useServices.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
// Interface pública:
|
||||
// services – ref([]) todos os serviços do owner (ativos e inativos)
|
||||
// loading – ref(false)
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
<!-- src/views/pages/agenda/AgendaClinicaPage.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/pages/AgendaClinicaPage.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<template>
|
||||
<Toast />
|
||||
<ConfirmDialog />
|
||||
|
||||
<!-- Sentinel -->
|
||||
@@ -107,7 +121,7 @@
|
||||
:slotMinTime="slotMinTime"
|
||||
:slotMaxTime="slotMaxTime"
|
||||
:slotDuration="slotDuration"
|
||||
:slotMinHeight="14"
|
||||
|
||||
:expandRows="false"
|
||||
:businessHours="businessHours"
|
||||
:staff="staffCols"
|
||||
@@ -192,7 +206,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Calendar
|
||||
<DatePicker
|
||||
v-model="miniDate"
|
||||
inline
|
||||
class="ag-mini-cal"
|
||||
@@ -203,7 +217,7 @@
|
||||
<span class="mini-day-num">{{ date.day }}</span>
|
||||
<span v-if="hasMiniEvent(date)" class="mini-day-dot" />
|
||||
</template>
|
||||
</Calendar>
|
||||
</DatePicker>
|
||||
</div>
|
||||
|
||||
<ProximosFeriadosCard
|
||||
@@ -346,7 +360,7 @@
|
||||
<!-- Month Picker -->
|
||||
<Dialog v-model:visible="monthPickerVisible" modal header="Escolher mês" :style="{ width: '420px' }">
|
||||
<div class="p-2">
|
||||
<Calendar
|
||||
<DatePicker
|
||||
v-model="monthPickerDate"
|
||||
view="month"
|
||||
dateFormat="mm/yy"
|
||||
@@ -499,7 +513,7 @@ import { useRouter, useRoute } from 'vue-router'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { useConfirm } from 'primevue/useconfirm'
|
||||
|
||||
import Calendar from 'primevue/calendar'
|
||||
import DatePicker from 'primevue/datepicker'
|
||||
|
||||
import AgendaClinicMosaic from '@/features/agenda/components/AgendaClinicMosaic.vue'
|
||||
import AgendaEventDialog from '@/features/agenda/components/AgendaEventDialog.vue'
|
||||
@@ -621,9 +635,10 @@ const onlySessionsOptions = [
|
||||
{ label: 'Tudo', value: false }
|
||||
]
|
||||
const viewOptions = [
|
||||
{ label: 'Dia', value: 'day' },
|
||||
{ label: 'Semana', value: 'week' },
|
||||
{ label: 'Mês', value: 'month' }
|
||||
{ label: 'Dia', value: 'day' },
|
||||
{ label: 'Semana', value: 'week' },
|
||||
{ label: 'Mês', value: 'month' },
|
||||
{ label: 'Lista', value: 'list' }
|
||||
]
|
||||
const timeModeOptions = [
|
||||
{ label: '24h', value: '24' },
|
||||
@@ -2172,12 +2187,41 @@ const workDowSet = computed(() =>
|
||||
new Set(workRules.value.filter(r => r.ativo).map(r => Number(r.dia_semana)))
|
||||
)
|
||||
|
||||
// ── Mini calendário: set de dias da semana atual ─────────────
|
||||
const currentWeekIsoSet = computed(() => {
|
||||
const now = new Date()
|
||||
const monday = new Date(now)
|
||||
monday.setDate(now.getDate() - ((now.getDay() + 6) % 7))
|
||||
monday.setHours(0, 0, 0, 0)
|
||||
const set = new Set()
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const d = new Date(monday)
|
||||
d.setDate(monday.getDate() + i)
|
||||
set.add(`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`)
|
||||
}
|
||||
return set
|
||||
})
|
||||
|
||||
const todayISO = computed(() => {
|
||||
const n = new Date()
|
||||
return `${n.getFullYear()}-${String(n.getMonth()+1).padStart(2,'0')}-${String(n.getDate()).padStart(2,'0')}`
|
||||
})
|
||||
|
||||
// ── Mini calendário: classes por dia ──────────────────────────
|
||||
function miniDayClass (date) {
|
||||
const iso = `${date.year}-${String(date.month + 1).padStart(2,'0')}-${String(date.day).padStart(2,'0')}`
|
||||
if (miniBlockedDaySet.value.has(iso)) return 'mini-day-blocked'
|
||||
const dow = new Date(date.year, date.month, date.day).getDay()
|
||||
return workDowSet.value.has(dow) ? 'mini-day-work' : 'mini-day-off'
|
||||
const classes = []
|
||||
if (currentWeekIsoSet.value.has(iso)) {
|
||||
classes.push('mini-week-hl')
|
||||
if (dow === 1) classes.push('mini-week-hl--start')
|
||||
else if (dow === 0) classes.push('mini-week-hl--end')
|
||||
else classes.push('mini-week-hl--mid')
|
||||
}
|
||||
if (iso === todayISO.value) classes.push('mini-day-today')
|
||||
if (miniBlockedDaySet.value.has(iso)) classes.push('mini-day-blocked')
|
||||
else classes.push(workDowSet.value.has(dow) ? 'mini-day-work' : 'mini-day-off')
|
||||
return classes
|
||||
}
|
||||
|
||||
// ── Mini calendário: bolinhas + bloqueios de dia inteiro ──────
|
||||
@@ -2519,12 +2563,28 @@ function goRecorrencias () { router.push({ name: 'admin-agenda-recorrencias' })
|
||||
width: 100%; min-width: unset; border-radius: 6px;
|
||||
position: relative; display: flex; align-items: center; justify-content: center; aspect-ratio: 1;
|
||||
}
|
||||
:deep(.p-disabled.mini-day-work) { background: color-mix(in srgb, #9ca3af 18%, transparent) !important; opacity: 0.6; }
|
||||
.mini-day-num { display: block; text-align: center; line-height: 1; }
|
||||
.mini-day-dot {
|
||||
position: absolute; bottom: 2px; right: 2px;
|
||||
width: 4px; height: 4px; border-radius: 50%;
|
||||
background: var(--primary-color, #6366f1);
|
||||
}
|
||||
|
||||
/* Semana atual — faixa de fundo contínua seg→dom */
|
||||
:deep(.mini-week-hl) { background: color-mix(in srgb, var(--primary-color, #6366f1) 12%, transparent) !important; border-radius: 0 !important; }
|
||||
:deep(.mini-week-hl--start) { border-radius: 6px 0 0 6px !important; }
|
||||
:deep(.mini-week-hl--end) { border-radius: 0 6px 6px 0 !important; }
|
||||
|
||||
/* Hoje — cartão com borda + sombra */
|
||||
:deep(.mini-day-today) {
|
||||
background: color-mix(in srgb, var(--primary-color, #6366f1) 80%, #00000000) !important;
|
||||
border: 1px solid var(--surface-border) !important;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.06) !important;
|
||||
border-radius: 6px !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
<!-- src/features/agenda/pages/AgendaRecorrenciasPage.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/pages/AgendaRecorrenciasPage.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
@@ -303,7 +318,6 @@ onMounted(init)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Toast />
|
||||
|
||||
<!-- ─── Header ─────────────────────────────────────────────────── -->
|
||||
<div class="rr-page mx-3 md:mx-5">
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
<!-- src/views/pages/agenda/AgendaTerapeutaPage.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/pages/AgendaTerapeutaPage.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<template>
|
||||
<Toast />
|
||||
<ConfirmDialog />
|
||||
|
||||
<!-- ════ AGENDA TERAPEUTA — Layout 3 colunas ════ -->
|
||||
@@ -117,7 +131,7 @@
|
||||
<Button icon="pi pi-chevron-right" severity="secondary" text class="h-7 w-7 rounded-full" @click="miniNextMonth" />
|
||||
</div>
|
||||
</div>
|
||||
<Calendar
|
||||
<DatePicker
|
||||
v-model="miniDate"
|
||||
inline
|
||||
class="w-full"
|
||||
@@ -128,7 +142,7 @@
|
||||
<span class="mini-day-num">{{ date.day }}</span>
|
||||
<span v-if="hasMiniEvent(date)" class="mini-day-dot" />
|
||||
</template>
|
||||
</Calendar>
|
||||
</DatePicker>
|
||||
</div>
|
||||
|
||||
<div v-if="jornadaHoje" class="border border-[var(--surface-border)] rounded-md bg-[var(--surface-card)] p-3">
|
||||
@@ -140,6 +154,8 @@
|
||||
|
||||
<ProximosFeriadosCard :ownerId="ownerId" :tenantId="clinicTenantId" :workRules="workRules" @bloqueado="refetch" />
|
||||
|
||||
<LoadedPhraseBlock v-if="eventsHasLoaded" />
|
||||
|
||||
<!-- Divisor -->
|
||||
<div class="border-t border-[var(--surface-border)] my-1" />
|
||||
|
||||
@@ -149,10 +165,15 @@
|
||||
<span class="flex items-center gap-1.5 text-[1rem] font-bold uppercase tracking-[0.06em] text-[var(--text-color-secondary)] opacity-65"><i class="pi pi-chart-bar" />Hoje</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<div class="flex flex-col gap-0.5 p-2 rounded-md bg-[var(--surface-ground)] border border-[var(--surface-border)] text-center" v-for="s in todayStats" :key="s.label">
|
||||
<div class="text-[1.25rem] font-bold leading-none text-[var(--text-color)]" :class="{ 'text-green-500': s.cls === 'ag-stat--ok', 'text-red-500': s.cls === 'ag-stat--warn' }">{{ s.value }}</div>
|
||||
<div class="text-[0.65rem] font-semibold uppercase tracking-[0.04em] text-[var(--text-color-secondary)] opacity-70">{{ s.label }}</div>
|
||||
</div>
|
||||
<template v-if="eventsLoading">
|
||||
<Skeleton v-for="n in 4" :key="n" height="3rem" class="rounded-md" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex flex-col gap-0.5 p-2 rounded-md bg-[var(--surface-ground)] border border-[var(--surface-border)] text-center" v-for="s in todayStats" :key="s.label">
|
||||
<div class="text-[1.25rem] font-bold leading-none text-[var(--text-color)]" :class="{ 'text-green-500': s.cls === 'ag-stat--ok', 'text-red-500': s.cls === 'ag-stat--warn' }">{{ s.value }}</div>
|
||||
<div class="text-[0.65rem] font-semibold uppercase tracking-[0.04em] text-[var(--text-color-secondary)] opacity-70">{{ s.label }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -162,7 +183,12 @@
|
||||
<span class="flex items-center gap-1.5 text-[1rem] font-bold uppercase tracking-[0.06em] text-[var(--text-color-secondary)] opacity-65"><i class="pi pi-list" />Sessões hoje</span>
|
||||
<span class="inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 rounded-full bg-[var(--primary-color,#6366f1)] text-white text-[0.65rem] font-bold">{{ todayEvents.length }}</span>
|
||||
</div>
|
||||
<div v-if="!todayEvents.length" class="flex flex-col items-center justify-center gap-2 py-6 text-[var(--text-color-secondary)] text-sm text-center">
|
||||
<template v-if="eventsLoading">
|
||||
<div class="flex flex-col gap-1.5 mt-1">
|
||||
<Skeleton v-for="n in 3" :key="n" height="3.5rem" class="rounded-md" />
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="!todayEvents.length" class="flex flex-col items-center justify-center gap-2 py-6 text-[var(--text-color-secondary)] text-sm text-center">
|
||||
<i class="pi pi-sun text-2xl opacity-20" />
|
||||
<span>Nenhuma sessão hoje</span>
|
||||
</div>
|
||||
@@ -318,7 +344,7 @@
|
||||
<Button icon="pi pi-chevron-right" severity="secondary" outlined class="h-8 w-8 rounded-full" @click="goNext" />
|
||||
</div>
|
||||
</div>
|
||||
<Calendar
|
||||
<DatePicker
|
||||
v-model="miniDate"
|
||||
inline
|
||||
class="w-full"
|
||||
@@ -329,7 +355,7 @@
|
||||
<span class="mini-day-num">{{ date.day }}</span>
|
||||
<span v-if="hasMiniEvent(date)" class="mini-day-dot" />
|
||||
</template>
|
||||
</Calendar>
|
||||
</DatePicker>
|
||||
</div>
|
||||
|
||||
<div v-if="jornadaHoje" class="border border-[var(--surface-border)] rounded-md bg-[var(--surface-card)] p-3">
|
||||
@@ -341,6 +367,8 @@
|
||||
|
||||
<ProximosFeriadosCard :ownerId="ownerId" :tenantId="clinicTenantId" :workRules="workRules" @bloqueado="refetch" />
|
||||
|
||||
<LoadedPhraseBlock v-if="eventsHasLoaded" />
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Botão toggle painel (só mobile <xl) -->
|
||||
@@ -377,7 +405,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border border-[var(--surface-border)] rounded-md bg-[var(--surface-card)] overflow-hidden">
|
||||
<div class="border border-[var(--surface-border)] rounded-md bg-[var(--surface-card)] overflow-hidden shadow-sm agenda-altura">
|
||||
<div v-if="calendarView === 'day' && miniBlockedDaySet.has(currentDateISO)" class="flex items-center gap-2 px-4 py-2 text-sm font-semibold text-red-700 bg-red-400/10 border-b border-red-400/25">
|
||||
<i class="pi pi-lock text-xs" /> Dia bloqueado — sessões não permitidas
|
||||
</div>
|
||||
@@ -396,10 +424,15 @@
|
||||
<span class="flex items-center gap-1.5 text-[1rem] font-bold uppercase tracking-[0.06em] text-[var(--text-color-secondary)] opacity-65"><i class="pi pi-chart-bar" />Hoje</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<div class="flex flex-col gap-0.5 p-2 rounded-md bg-[var(--surface-ground)] border border-[var(--surface-border)] text-center" v-for="s in todayStats" :key="s.label">
|
||||
<div class="text-[1.25rem] font-bold leading-none text-[var(--text-color)]" :class="{ 'text-green-500': s.cls === 'ag-stat--ok', 'text-red-500': s.cls === 'ag-stat--warn' }">{{ s.value }}</div>
|
||||
<div class="text-[0.65rem] font-semibold uppercase tracking-[0.04em] text-[var(--text-color-secondary)] opacity-70">{{ s.label }}</div>
|
||||
</div>
|
||||
<template v-if="eventsLoading">
|
||||
<Skeleton v-for="n in 4" :key="n" height="3rem" class="rounded-md" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex flex-col gap-0.5 p-2 rounded-md bg-[var(--surface-ground)] border border-[var(--surface-border)] text-center" v-for="s in todayStats" :key="s.label">
|
||||
<div class="text-[1.25rem] font-bold leading-none text-[var(--text-color)]" :class="{ 'text-green-500': s.cls === 'ag-stat--ok', 'text-red-500': s.cls === 'ag-stat--warn' }">{{ s.value }}</div>
|
||||
<div class="text-[0.65rem] font-semibold uppercase tracking-[0.04em] text-[var(--text-color-secondary)] opacity-70">{{ s.label }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -410,7 +443,12 @@
|
||||
<span class="inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 rounded-full bg-[var(--primary-color,#6366f1)] text-white text-[0.65rem] font-bold">{{ todayEvents.length }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!todayEvents.length" class="flex flex-col items-center justify-center gap-2 py-6 text-[var(--text-color-secondary)] text-sm text-center">
|
||||
<template v-if="eventsLoading">
|
||||
<div class="flex flex-col gap-1.5 mt-1">
|
||||
<Skeleton v-for="n in 3" :key="n" height="3.5rem" class="rounded-md" />
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="!todayEvents.length" class="flex flex-col items-center justify-center gap-2 py-6 text-[var(--text-color-secondary)] text-sm text-center">
|
||||
<i class="pi pi-sun text-2xl opacity-20" />
|
||||
<span>Nenhuma sessão hoje</span>
|
||||
</div>
|
||||
@@ -670,7 +708,7 @@
|
||||
<!-- Month Picker -->
|
||||
<Dialog v-model:visible="monthPickerVisible" modal header="Escolher mês" :style="{ width: '420px' }">
|
||||
<div class="p-2">
|
||||
<Calendar v-model="monthPickerDate" view="month" dateFormat="mm/yy" class="w-full" />
|
||||
<DatePicker v-model="monthPickerDate" view="month" dateFormat="mm/yy" class="w-full" />
|
||||
<div class="mt-3 flex justify-end gap-2">
|
||||
<Button label="Cancelar" severity="secondary" outlined class="rounded-full" @click="monthPickerVisible = false" />
|
||||
<Button label="Ir" class="rounded-full" @click="applyMonthPick" />
|
||||
@@ -939,7 +977,7 @@ import { supabase } from '@/lib/supabase/client'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { useConfirm } from 'primevue/useconfirm'
|
||||
|
||||
import Calendar from 'primevue/calendar'
|
||||
import DatePicker from 'primevue/datepicker'
|
||||
|
||||
import FullCalendar from '@fullcalendar/vue3'
|
||||
import timeGridPlugin from '@fullcalendar/timegrid'
|
||||
@@ -1043,7 +1081,9 @@ const commitmentOptionsNormalized = computed(() => {
|
||||
// settings + events
|
||||
// -----------------------------
|
||||
const { error: settingsError, settings, workRules, load: loadSettings } = useAgendaSettings()
|
||||
const { error: eventsError, rows, loadMyRange, create, update, remove } = useAgendaEvents()
|
||||
const { error: eventsError, rows, loading: eventsLoading, loadMyRange, create, update, remove } = useAgendaEvents()
|
||||
const eventsHasLoaded = ref(false)
|
||||
watch(eventsLoading, (val) => { if (!val) eventsHasLoaded.value = true })
|
||||
|
||||
const {
|
||||
loadAndExpand,
|
||||
@@ -1096,9 +1136,10 @@ const onlySessionsOptions = [
|
||||
{ label: 'Tudo', value: false }
|
||||
]
|
||||
const viewOptions = [
|
||||
{ label: 'Dia', value: 'day' },
|
||||
{ label: 'Dia', value: 'day' },
|
||||
{ label: 'Semana', value: 'week' },
|
||||
{ label: 'Mês', value: 'month' }
|
||||
{ label: 'Mês', value: 'month' },
|
||||
{ label: 'Lista', value: 'list' }
|
||||
]
|
||||
const timeModeOptions = [
|
||||
{ label: '24h', value: '24' },
|
||||
@@ -1327,8 +1368,9 @@ const slotMaxTime = computed(() => {
|
||||
})
|
||||
|
||||
const fcViewName = computed(() => {
|
||||
if (calendarView.value === 'day') return 'timeGridDay'
|
||||
if (calendarView.value === 'week') return 'timeGridWeek'
|
||||
if (calendarView.value === 'day') return 'timeGridDay'
|
||||
if (calendarView.value === 'week') return 'timeGridWeek'
|
||||
if (calendarView.value === 'list') return 'listWeek'
|
||||
return 'dayGridMonth'
|
||||
})
|
||||
|
||||
@@ -1581,7 +1623,7 @@ const _initSlotMax = slotMaxTime.value
|
||||
// NÃO incluímos 'events' no fcOptions — evita que o Vue FC adapter gerencie
|
||||
// a fonte e conflite com o watch que usa getEventSources + addEventSource.
|
||||
const fcOptions = computed(() => ({
|
||||
plugins: [timeGridPlugin, dayGridPlugin, interactionPlugin],
|
||||
plugins: [timeGridPlugin, dayGridPlugin, listPlugin, interactionPlugin],
|
||||
locale: ptBrLocale,
|
||||
timeZone: timezone.value,
|
||||
|
||||
@@ -1602,12 +1644,16 @@ const fcOptions = computed(() => ({
|
||||
slotLabelContent,
|
||||
expandRows: false,
|
||||
height: 'auto',
|
||||
slotMinHeight: 14,
|
||||
|
||||
dayMaxEvents: true,
|
||||
weekends: true,
|
||||
eventMinHeight: 14,
|
||||
|
||||
views: {
|
||||
timeGridDay: {
|
||||
dayHeaderFormat: { day: 'numeric', month: 'long', year: 'numeric' },
|
||||
},
|
||||
},
|
||||
|
||||
businessHours: businessHours.value,
|
||||
|
||||
datesSet: async (arg) => {
|
||||
@@ -2052,13 +2098,42 @@ const currentDateISO = computed(() => {
|
||||
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`
|
||||
})
|
||||
|
||||
// ── Mini calendário: set de dias da semana atual ─────────────
|
||||
const currentWeekIsoSet = computed(() => {
|
||||
const now = new Date()
|
||||
const monday = new Date(now)
|
||||
monday.setDate(now.getDate() - ((now.getDay() + 6) % 7))
|
||||
monday.setHours(0, 0, 0, 0)
|
||||
const set = new Set()
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const d = new Date(monday)
|
||||
d.setDate(monday.getDate() + i)
|
||||
set.add(`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`)
|
||||
}
|
||||
return set
|
||||
})
|
||||
|
||||
const todayISO = computed(() => {
|
||||
const n = new Date()
|
||||
return `${n.getFullYear()}-${String(n.getMonth()+1).padStart(2,'0')}-${String(n.getDate()).padStart(2,'0')}`
|
||||
})
|
||||
|
||||
// ── Mini calendário: classes por dia ──────────────────────────
|
||||
// Prioridade: bloqueado total > dia de trabalho > folga
|
||||
function miniDayClass (date) {
|
||||
const iso = `${date.year}-${String(date.month + 1).padStart(2,'0')}-${String(date.day).padStart(2,'0')}`
|
||||
if (miniBlockedDaySet.value.has(iso)) return 'mini-day-blocked'
|
||||
const dow = new Date(date.year, date.month, date.day).getDay()
|
||||
return workDowSet.value.has(dow) ? 'mini-day-work' : 'mini-day-off'
|
||||
const classes = []
|
||||
if (currentWeekIsoSet.value.has(iso)) {
|
||||
classes.push('mini-week-hl')
|
||||
if (dow === 1) classes.push('mini-week-hl--start')
|
||||
else if (dow === 0) classes.push('mini-week-hl--end')
|
||||
else classes.push('mini-week-hl--mid')
|
||||
}
|
||||
if (iso === todayISO.value) classes.push('mini-day-today')
|
||||
if (miniBlockedDaySet.value.has(iso)) classes.push('mini-day-blocked')
|
||||
else classes.push(workDowSet.value.has(dow) ? 'mini-day-work' : 'mini-day-off')
|
||||
return classes
|
||||
}
|
||||
|
||||
// ── Mini calendário: bolinhas de compromissos + set de dias bloqueados ──
|
||||
@@ -3232,6 +3307,22 @@ onMounted(async () => {
|
||||
:deep(.mini-day-blocked) { background: color-mix(in srgb,#ef4444 20%,transparent) !important; border-radius: 4px; }
|
||||
:deep(.mini-day-work) { }
|
||||
:deep(.mini-day-off) { opacity: 0.45; }
|
||||
:deep(.p-disabled.mini-day-work) { background: color-mix(in srgb, #9ca3af 18%, transparent) !important; opacity: 0.6; }
|
||||
|
||||
/* Semana atual — faixa de fundo contínua seg→dom */
|
||||
:deep(.mini-week-hl) { background: color-mix(in srgb, var(--primary-color, #6366f1) 12%, transparent) !important; border-radius: 0 !important; }
|
||||
:deep(.mini-week-hl--start) { border-radius: 6px 0 0 6px !important; }
|
||||
:deep(.mini-week-hl--end) { border-radius: 0 6px 6px 0 !important; }
|
||||
|
||||
/* Hoje — cartão com borda + sombra */
|
||||
:deep(.mini-day-today) {
|
||||
background: color-mix(in srgb, var(--primary-color, #6366f1) 80%, #00000000) !important;
|
||||
border: 1px solid var(--surface-border) !important;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.06) !important;
|
||||
border-radius: 6px !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
<!-- src/features/agenda/pages/AgendamentosRecebidosPage.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/pages/AgendamentosRecebidosPage.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -40,6 +55,7 @@ const statusOpts = [
|
||||
// ── Lista ─────────────────────────────────────────────────
|
||||
const solicitacoes = ref([])
|
||||
const loading = ref(false)
|
||||
const hasLoaded = ref(false)
|
||||
const totalPendentes = ref(0)
|
||||
const totalAutorizados = ref(0)
|
||||
|
||||
@@ -87,6 +103,7 @@ async function load () {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: 'Não foi possível carregar as solicitações.', life: 4000 })
|
||||
} finally {
|
||||
loading.value = false
|
||||
hasLoaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,7 +285,6 @@ const emptySub = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Toast />
|
||||
|
||||
<!-- Sentinel -->
|
||||
<div class="h-px" />
|
||||
@@ -563,6 +579,8 @@ const emptySub = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LoadedPhraseBlock v-if="hasLoaded && !loading" class="mx-3 md:mx-4 mt-3 mb-2" />
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════
|
||||
Dialog: Recusar
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
<!-- src/features/agenda/pages/CompromissosDeterminados.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/pages/CompromissosDeterminados.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<template>
|
||||
<Toast />
|
||||
|
||||
<!-- Sentinel -->
|
||||
<div ref="headerSentinelRef" class="h-px" />
|
||||
@@ -101,29 +115,37 @@
|
||||
<!-- ══════════════════════════════════════
|
||||
Conteúdo principal
|
||||
═══════════════════════════════════════ -->
|
||||
<div class="px-3 md:px-4 pb-5 flex flex-col gap-3">
|
||||
<div class="px-3 md:px-4 pb-5 flex gap-4 items-start">
|
||||
|
||||
<!-- ── Coluna principal ── -->
|
||||
<div class="flex flex-col gap-3 flex-1 min-w-0">
|
||||
|
||||
<!-- Stats row -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div
|
||||
v-for="s in stats"
|
||||
:key="s.label"
|
||||
class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] min-w-[80px] flex-1"
|
||||
>
|
||||
<template v-if="loading">
|
||||
<Skeleton v-for="n in 4" :key="n" height="3.5rem" class="flex-1 min-w-[80px] rounded-md" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
class="text-[1.35rem] font-bold leading-none"
|
||||
:class="{
|
||||
'text-green-500': s.cls === 'stat-ok',
|
||||
'text-red-500': s.cls === 'stat-warn',
|
||||
'text-[var(--text-color)]': !s.cls,
|
||||
}"
|
||||
>{{ s.value }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] opacity-75 whitespace-nowrap">{{ s.label }}</div>
|
||||
</div>
|
||||
v-for="s in stats"
|
||||
:key="s.label"
|
||||
class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] min-w-[80px] flex-1"
|
||||
>
|
||||
<div
|
||||
class="text-[1.35rem] font-bold leading-none"
|
||||
:class="{
|
||||
'text-green-500': s.cls === 'stat-ok',
|
||||
'text-red-500': s.cls === 'stat-warn',
|
||||
'text-[var(--text-color)]': !s.cls,
|
||||
}"
|
||||
>{{ s.value }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] opacity-75 whitespace-nowrap">{{ s.label }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Cards grid -->
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-5 2xl:grid-cols-6">
|
||||
<!-- Cards grid (coluna direita — visível só até xl) -->
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3 xl:hidden">
|
||||
<div
|
||||
v-for="c in cardsCommitments"
|
||||
:key="c.id"
|
||||
@@ -241,7 +263,8 @@
|
||||
:loading="loading"
|
||||
:paginator="visibleCommitments.length > 10"
|
||||
:rows="10"
|
||||
responsiveLayout="scroll"
|
||||
scrollable
|
||||
scrollHeight="400px"
|
||||
class="p-datatable-sm cmpr-datatable"
|
||||
:rowClass="(r) => isRecent(r) ? 'row-new-highlight' : ''"
|
||||
:filters="filters"
|
||||
@@ -324,6 +347,62 @@
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<LoadedPhraseBlock v-if="hasLoaded" />
|
||||
|
||||
</div><!-- fim coluna principal -->
|
||||
|
||||
<!-- ── Coluna direita: cards de tipos (só xl+) ── -->
|
||||
<div class="hidden xl:flex flex-col gap-2 w-64 flex-shrink-0">
|
||||
<div class="text-xs font-bold uppercase tracking-widest text-[var(--text-color-secondary)] opacity-60 px-1 mb-1">
|
||||
Tipos de compromisso
|
||||
</div>
|
||||
|
||||
<template v-if="loading">
|
||||
<Skeleton v-for="n in 5" :key="n" height="4.5rem" class="rounded-md" />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="c in cardsCommitments"
|
||||
:key="c.id"
|
||||
class="flex flex-col gap-1.5 px-3 py-2.5 rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)]"
|
||||
>
|
||||
<!-- Nome com dot de cor -->
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<div
|
||||
class="w-2 h-2 rounded-full flex-shrink-0 bg-[var(--surface-border)]"
|
||||
:style="c.bg_color ? { background: `#${c.bg_color}` } : {}"
|
||||
/>
|
||||
<span
|
||||
class="text-[0.85rem] font-semibold truncate"
|
||||
:style="c.bg_color ? { color: `#${c.bg_color}` } : {}"
|
||||
>{{ c.name }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Descrição -->
|
||||
<div class="text-[0.72rem] text-[var(--text-color-secondary)] line-clamp-1 pl-4">{{ c.description || '—' }}</div>
|
||||
|
||||
<!-- Tempo total -->
|
||||
<div
|
||||
class="flex items-center gap-1 pl-4 text-[0.72rem] text-[var(--text-color-secondary)]"
|
||||
v-tooltip.bottom="getTotalMinutes(c.id) === 0 ? 'Você ainda não tem nenhum evento desse tipo agendado e concluído para contabilizar tempo/relatórios' : null"
|
||||
>
|
||||
<i class="pi pi-clock text-[0.65rem]" />
|
||||
<span>Tempo total: {{ formatMinutes(getTotalMinutes(c.id)) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state coluna direita -->
|
||||
<div
|
||||
v-if="!cardsCommitments.length"
|
||||
class="flex flex-col items-center gap-2 p-4 rounded-md border border-dashed border-[var(--surface-border)] text-center text-[var(--text-color-secondary)]"
|
||||
>
|
||||
<i class="pi pi-list text-2xl opacity-20" />
|
||||
<div class="text-xs">Nenhum compromisso</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Dialog -->
|
||||
@@ -381,8 +460,9 @@ onMounted(async () => {
|
||||
|
||||
onBeforeUnmount(() => { _observer?.disconnect() })
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const loading = ref(false)
|
||||
const hasLoaded = ref(false)
|
||||
const saving = ref(false)
|
||||
|
||||
const filters = reactive({
|
||||
global: { value: null, matchMode: 'contains' },
|
||||
@@ -503,6 +583,7 @@ async function fetchAll () {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao carregar.', life: 4500 })
|
||||
} finally {
|
||||
loading.value = false
|
||||
hasLoaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/services/agenda.service.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
// src/features/agenda/services/agendaClinicRepository.js
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/services/agendaClinicRepository.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
function assertValidTenantId (tenantId) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user