From f94a4ae97f21fb5bdbe98c4253ccdb3886dd35ed Mon Sep 17 00:00:00 2001 From: Leonardo Date: Thu, 21 May 2026 04:19:45 -0300 Subject: [PATCH] =?UTF-8?q?padronizacao:=20foundation=20Fase=200+0.5=20?= =?UTF-8?q?=E2=80=94=20blueprints=20+=20auditoria=20+=20clinical=5Fnotes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-MVP: 3 blueprints canonicos (repository, composable, quick-create overlay), AUDIT_BASELINE com 51 divergencias em 6 modulos, estrategia PADRONIZACAO de 4 fases, DESIGN_BILLING_ORCHESTRATOR. Schema clinical notes pronto pra Fase B (4 migrations + seed templates). AgendaEvent Dialog.vue.bak deletado (lixo de refator anterior). Co-Authored-By: Claude Opus 4.7 (1M context) --- blueprints/composable-blueprint.md | 514 +++ blueprints/quick-create-overlay-blueprint.md | 431 ++ blueprints/repository-blueprint.md | 379 ++ .../20260520000001_clinical_notes_tables.sql | 165 + .../20260520000002_clinical_notes_rls.sql | 111 + ...260520000003_clinical_notes_versioning.sql | 117 + ...520000004_documents_clinical_note_link.sql | 46 + .../seed_040_clinical_note_templates.sql | 151 + development/02-auditoria/AUDIT_BASELINE.md | 302 ++ .../DESIGN_BILLING_ORCHESTRATOR.md | 480 +++ development/02-auditoria/PADRONIZACAO.md | 161 + .../components/AgendaEventDialog.vue.bak | 3522 ----------------- 12 files changed, 2857 insertions(+), 3522 deletions(-) create mode 100644 blueprints/composable-blueprint.md create mode 100644 blueprints/quick-create-overlay-blueprint.md create mode 100644 blueprints/repository-blueprint.md create mode 100644 database-novo/migrations/20260520000001_clinical_notes_tables.sql create mode 100644 database-novo/migrations/20260520000002_clinical_notes_rls.sql create mode 100644 database-novo/migrations/20260520000003_clinical_notes_versioning.sql create mode 100644 database-novo/migrations/20260520000004_documents_clinical_note_link.sql create mode 100644 database-novo/seeds/seed_040_clinical_note_templates.sql create mode 100644 development/02-auditoria/AUDIT_BASELINE.md create mode 100644 development/02-auditoria/DESIGN_BILLING_ORCHESTRATOR.md create mode 100644 development/02-auditoria/PADRONIZACAO.md delete mode 100644 src/features/agenda/components/AgendaEventDialog.vue.bak diff --git a/blueprints/composable-blueprint.md b/blueprints/composable-blueprint.md new file mode 100644 index 0000000..6270ca7 --- /dev/null +++ b/blueprints/composable-blueprint.md @@ -0,0 +1,514 @@ +# Composable Blueprint + +> **Stack:** Vue 3 Composition API + Pinia (para state global) + Supabase via repository +> **Canônicos:** `src/features/agenda/composables/useAgendaEvents.js`, `useAgendaClinicEvents.js`, `useAgendaSettings.js` +> **Aplicável:** todo composable que orquestra estado reativo sobre uma repository + +--- + +## 1. Princípio + +Composable é **wrapper fino** sobre a repository. Responsabilidade: +- Manter **estado reativo** (data + loading + error) +- Chamar a repository (delegação 1:1) +- (Opcional) Cachear com stale-while-revalidate +- (Opcional) Compor outros composables + +**Não faz:** +- Lógica de banco direta (vai no repository) +- Lógica de UI (vai no componente) +- Manipulação de DOM +- I/O direto fora do repository + +> Regra de ouro: **se o composable tem `from('...')` do Supabase, ele virou repository disfarçado — refatorar.** + +--- + +## 2. Estrutura de arquivos + +``` +src/features//composables/ +├── use.js # CRUD básico (thin wrapper) +├── useClinic.js # variant clinic-scoped (se aplicável) +├── useSettings.js # config/preferences (com cache opt-in) +├── useLifecycle.js # orquestrador de estados (se domain complexo) +└── Helpers.js # funções puras auxiliares (não-composable) +``` + +**Convenção de nome:** sempre `use...`. Funções helpers de domínio NÃO usam prefixo `use` — não são composables. + +--- + +## 3. State shape canônico + +Todo composable expõe **no mínimo** este shape: + +```js +const rows = ref([]); // ou single ref dependendo do domínio +const loading = ref(false); // boolean +const error = ref(''); // string vazia, não null — facilita v-if +``` + +**Decisões importantes:** + +| Refs | Tipo | Inicial | Por quê | +|---|---|---|---| +| `loading` | `boolean` | `false` | Padrão V3 — UI binda `:disabled="loading"` direto | +| `error` | `string` | `''` (vazio) | `v-if="error"` é falsy-friendly; sem null check | +| `rows`/data | `Array` ou objeto | `[]` ou `null` | Reset pra `[]` em erro de load — UI fica previsível | + +**Anti-pattern:** misturar `error = ref(null)` num composable e `error = ref('')` em outro. Canonize `''` no projeto inteiro. + +--- + +## 4. Tipos de composable (3 patterns) + +### Tipo A — Thin wrapper (default) · referência: `useAgendaClinicEvents.js` + +CRUD direto, sem cache, com loading/error em TODA operação: + +```js +import { ref } from 'vue'; +import { listX, createX, updateX, deleteX } from '@/features//services/Repository'; + +export function useX() { + const rows = ref([]); + const loading = ref(false); + const error = ref(''); + + async function loadRange({ startISO, endISO, ...scope } = {}) { + loading.value = true; + error.value = ''; + try { + rows.value = await listX({ startISO, endISO, ...scope }); + } catch (e) { + error.value = e?.message || 'Falha ao carregar.'; + rows.value = []; + } finally { + loading.value = false; + } + } + + async function create(payload, opts = {}) { + loading.value = true; + error.value = ''; + try { + return await createX(payload, opts); + } catch (e) { + error.value = e?.message || 'Falha ao criar.'; + throw e; // ← re-throw: composable repassa o erro pro componente decidir + } finally { + loading.value = false; + } + } + + async function update(id, patch, opts = {}) { /* idem */ } + async function remove(id, opts = {}) { /* idem */ } + + return { rows, loading, error, loadRange, create, update, remove }; +} +``` + +**Por que re-throw nas mutações?** Componente precisa saber se o `await` falhou pra: +- Mostrar toast +- Não fechar modal +- Não navegar +- Manter form com dados + +`error.value` é só pra estado reativo persistente. Mutação síncrona precisa de throw também. + +### Tipo B — Thin wrapper "extra-leve" · referência: `useAgendaEvents.js` + +Variant aceitável quando mutações **não precisam de loading**: + +```js +async function create(payload) { + return createX(payload); // ← repassa erro nativamente; componente try/catch +} +``` + +**Quando usar:** UIs onde criar/editar tem feedback próprio (skeleton no item criado, optimistic UI, etc.). Default é o Tipo A. + +### Tipo C — Cache com stale-while-revalidate · referência: `useAgendaSettings.js` + +Para dados raros/pesados (settings, preferences, listas estáveis): + +```js +import { ref } from 'vue'; +import { getX } from '../services/Repository'; +import { useMelissaCacheStore, MELISSA_CACHE_TTL } from '@/stores/melissaCacheStore'; + +export function useX(opts = {}) { + const useCache = !!opts.cache; + const cache = useCache ? useMelissaCacheStore() : null; + + const data = ref(null); + const loading = ref(false); + const error = ref(''); + + async function _doFetch() { + const result = await getX(); + data.value = result; + if (cache) { + const key = result?.owner_id || 'anon'; + cache.set('xKey', result, key); + } + return result; + } + + async function load() { + if (cache) { + const cached = cache.get('xKey', undefined, MELISSA_CACHE_TTL.xKey); + if (cached) { + data.value = cached; + _doFetch().catch((e) => console.warn('[useX] revalidate', e)); + return; + } + } + + loading.value = true; + error.value = ''; + try { + await _doFetch(); + } catch (e) { + error.value = e?.message || 'Falha ao carregar.'; + data.value = null; + } finally { + loading.value = false; + } + } + + return { data, loading, error, load }; +} +``` + +**Decisões do Tipo C:** + +- **`opts.cache` default `false`** — páginas de configuração que editam settings esperam mudança imediata após salvar, então cache opt-in. +- **Cache key inclui scope** (`owner_id`/`tenant_id`) — invalida automaticamente em troca de usuário/tenant. +- **TTL constants no store** — `MELISSA_CACHE_TTL.` (não hardcoded no composable). +- **Stale-while-revalidate:** retorna cached SE existe + dispara fetch em background (sem await). +- **Revalidate fail é warn**, não error — UI já tem dados válidos do cache. + +--- + +## 5. Convenções de nomenclatura + +### Funções + +| Operação | Nome canônico | Variantes aceitas | +|---|---|---| +| Listar com filtro | `loadRange` / `loadMy` | `load` | +| Criar | `create` | `create` (se houver ambiguidade) | +| Atualizar | `update` | `update` | +| Remover | `remove` | `remove` (nunca `delete` — palavra reservada) | +| Recarregar | `refresh` | `reload` | +| Limpar estado | `reset` / `clear` | — | + +**Scope sufixo** quando o composable serve múltiplos contextos: `loadMyRange` (terapeuta) vs `loadClinicRange` (admin). + +### State refs + +- `rows` — coleção principal (array) +- `record` — single (quando faz sentido) +- `data` — genérico (settings, config) +- `loading` — boolean único; se há múltiplos `loading` (load vs save), nomear: `loadingList`, `saving` +- `error` — string única; mesmo princípio: `loadError`, `saveError` se precisar + +--- + +## 6. Anatomia padrão de uma operação `load*` + +```js +async function loadXxx(args) { + // 1. Validação leve (early return, não throw) + if (!args?.required) return; + + // 2. State flag + loading.value = true; + error.value = ''; + + try { + // 3. Delegate pra repository (UMA chamada — se múltiplas, Promise.all) + const result = await listX(args); + + // 4. Mutate state + rows.value = result; + } catch (e) { + // 5. Erro humano + reset de data (UI fica previsível) + error.value = e?.message || 'Mensagem PT-BR genérica.'; + rows.value = []; + } finally { + // 6. Sempre limpar loading + loading.value = false; + } +} +``` + +**Por que early-return em vez de throw na validação?** Composable é wrapper — chamadas inválidas (ex: `ownerId` ainda não chegou no mount) não devem quebrar UI. Throw fica pra repository. + +--- + +## 7. Múltiplos fetches paralelos + +Quando uma operação precisa de N queries: + +```js +async function _doFetch() { + const [cfg, rules, profile] = await Promise.all([ + getMyAgendaSettings(), + getMyWorkSchedule(), + getMyProfile() + ]); + settings.value = cfg; + workRules.value = rules; + profile.value = profile; +} +``` + +**Regras:** +- `Promise.all` (não `Promise.allSettled`) — falha de qualquer query falha a operação inteira +- Exception: quando uma query é opcional/best-effort → `Promise.allSettled` + processa por result +- **Nunca** sequenciar fetches independentes (await + await + await) + +--- + +## 8. Composição de composables + +Composable pode usar outros composables, mas: + +```js +// ✅ certo — composição estrutural +export function useAgendaEventLifecycle() { + const events = useAgendaEvents(); + const billing = useAgendaFinanceiro(); + const settings = useAgendaSettings({ cache: true }); + + async function realizar(eventId) { + // orquestra os 3 + } + + return { ...events, realizar, ... }; +} + +// ❌ errado — não compor pra economizar 1 linha +export function useOnlyToWrapList() { + const { rows, loadMyRange } = useAgendaEvents(); + return { rows, loadMyRange }; // ← isso é um re-export inútil +} +``` + +**Regra:** compõe quando há **orquestração**. Se é só forward, importa direto. + +--- + +## 9. Anti-patterns (NÃO fazer) + +### ❌ Composable que tem `supabase.from('...')` direto + +```js +// ❌ — violação de camadas +export function useFoo() { + async function load() { + const { data } = await supabase.from('foo').select('*'); + } +} +``` + +✅ Move pra repository, composable só delega. + +### ❌ `error` ora `null`, ora `''`, ora `Error` + +Canonize `string` (default `''`). Errors do JS dão `e?.message || 'fallback PT-BR'`. + +### ❌ Não resetar `rows` em erro de load + +```js +// ❌ +async function loadRange() { + try { rows.value = await listX(); } catch (e) { error.value = e.message; } + // rows.value mantém dados antigos = UI mostra coisa stale + alerta de erro +} +``` + +✅ Reset `rows.value = []` no catch — UI fica determinística. + +### ❌ Não re-throw mutações + +```js +// ❌ +async function create(payload) { + try { return await createX(payload); } + catch (e) { error.value = e.message; } + // componente faz `await create()` e nunca sabe que falhou +} +``` + +✅ Re-throw após setar `error.value`. + +### ❌ `Promise.all` quando uma falha é aceitável + +Quando uma das queries pode falhar sem invalidar as outras, usar `Promise.allSettled`. Comum em listings que enriquece com lookups opcionais. + +### ❌ State global em variável módulo + +```js +// ❌ — vaza entre componentes que compartilham o composable +const rows = ref([]); +export function useFoo() { + return { rows }; +} +``` + +✅ State sempre DENTRO da `function useFoo()`. Se precisar global, use Pinia store. + +### ❌ Composable que faz `watch` no próprio state pra "side effect" + +```js +// ❌ +const rows = ref([]); +watch(rows, () => { /* save algo */ }); +``` + +✅ Mover `watch` pro componente — composable não decide quando salvar. + +**Exceção:** watch pra sincronizar com prop externa do composable (`watchEffect(() => loadRange(props.range))`) é OK. + +### ❌ Composable retornando objeto enorme + +Se o `return` tem 20+ chaves, o composable está fazendo coisa demais. Quebrar em N composables menores ou extrair Pinia store. + +--- + +## 10. Cache store (Tipo C complementar) + +Quando criar um composable Tipo C, garantir que existe entry em: + +- `src/stores/melissaCacheStore.js` — `MELISSA_CACHE_TTL.` constante (TTL em ms) +- `.get(key, scope, ttl)` retorna valor ou null +- `.set(key, value, scope)` salva com timestamp +- Invalidação manual: `.invalidate('')` + +**TTL guidelines:** + +| Tipo de dado | TTL sugerido | +|---|---| +| Settings/preferences | 5 min | +| Listas estáveis (specialties, plans) | 30 min | +| Catálogo (services, pricing) | 10 min | +| Multi-tenant lookups | 5 min | +| Anything user-edited | NÃO cachear (Tipo A) | + +--- + +## 11. Checklist de auditoria por módulo + +Quando rodar `/audit-module `, validar cada composable: + +- [ ] Não tem `supabase.from(...)` direto — só importa da repository +- [ ] State shape: `rows`/`data`, `loading: boolean`, `error: string` +- [ ] `error` é string, default `''` +- [ ] Reset de data em erro de load (`rows.value = []`) +- [ ] Mutações re-throw após setar error.value +- [ ] Nomenclatura: `loadRange`/`load`, `create`, `update`, `remove` +- [ ] `remove` não `delete` (palavra reservada) +- [ ] Validação leve usa early-return (não throw) +- [ ] Múltiplos fetches em `Promise.all` (não sequencial) +- [ ] State DENTRO da `function use*()` (não em variável de módulo) +- [ ] Sem `watch` em própria state pra side effect (mover pro componente) +- [ ] Helpers de domínio em arquivo separado sem prefixo `use` +- [ ] Se cacheia (Tipo C): `opts.cache` opt-in, default `false`; TTL em `MELISSA_CACHE_TTL`; cache key inclui scope +- [ ] Return statement com chaves explícitas (não `return { ...state, ...actions }` opaco) +- [ ] Return ≤ 15 chaves (>15 = composable fazendo coisa demais) + +Divergências viram items em `dev_auditoria_items` com: +- `categoria`: `padronizacao` +- `tag`: `padronizacao:` +- `severidade`: alta se camada quebrada (composable com `from()`); média se viola convenção (error null vs ''); baixa se cosmético (nome de função) + +--- + +## 12. Exemplo completo (template) + +```js +/* +| Arquivo: src/features/patients/composables/usePatients.js +*/ +import { ref } from 'vue'; +import { + listPatients, + createPatient, + updatePatient, + deletePatient +} from '@/features/patients/services/patientsRepository'; + +export function usePatients() { + const rows = ref([]); + const loading = ref(false); + const error = ref(''); + + async function loadRange({ search, status, tenantId } = {}) { + loading.value = true; + error.value = ''; + try { + rows.value = await listPatients({ search, status, tenantId }); + } catch (e) { + error.value = e?.message || 'Falha ao carregar pacientes.'; + rows.value = []; + } finally { + loading.value = false; + } + } + + async function create(payload) { + loading.value = true; + error.value = ''; + try { + return await createPatient(payload); + } catch (e) { + error.value = e?.message || 'Falha ao criar paciente.'; + throw e; + } finally { + loading.value = false; + } + } + + async function update(id, patch) { + loading.value = true; + error.value = ''; + try { + return await updatePatient(id, patch); + } catch (e) { + error.value = e?.message || 'Falha ao atualizar paciente.'; + throw e; + } finally { + loading.value = false; + } + } + + async function remove(id) { + loading.value = true; + error.value = ''; + try { + await deletePatient(id); + } catch (e) { + error.value = e?.message || 'Falha ao remover paciente.'; + throw e; + } finally { + loading.value = false; + } + } + + return { rows, loading, error, loadRange, create, update, remove }; +} +``` + +--- + +## 13. Referências + +- Canônicos: `src/features/agenda/composables/useAgendaEvents.js`, `useAgendaClinicEvents.js`, `useAgendaSettings.js` +- Repository pareado: `blueprints/repository-blueprint.md` +- Cache store: `src/stores/melissaCacheStore.js` +- Tracker: `dev_auditoria_items` com tag `padronizacao:` +- Estratégia: `development/02-auditoria/PADRONIZACAO.md` diff --git a/blueprints/quick-create-overlay-blueprint.md b/blueprints/quick-create-overlay-blueprint.md new file mode 100644 index 0000000..61691c2 --- /dev/null +++ b/blueprints/quick-create-overlay-blueprint.md @@ -0,0 +1,431 @@ +# Quick-Create Overlay Blueprint + +> **Status:** Pattern **universal**. Promovido de agenda-only em 2026-05-20 após audit baseline (`development/02-auditoria/AUDIT_BASELINE.md`) identificar 3 candidates já em produção fora da agenda. +> **Stack:** Vue 3 + PrimeVue Dialog +> **Canônicos:** +> - `src/features/agenda/components/ServiceQuickCreateDialog.vue` (referência completa) +> - `src/features/agenda/components/InsurancePlanQuickCreateDialog.vue` +> - `src/features/agenda/components/InsurancePlanServiceQuickCreateDialog.vue` +> **Legacy a refatorar (supabase direto, sem repository):** +> - `src/components/CadastroRapidoMedico.vue` → migrar pra `features/medicos/components/` (módulo 1 da Fase 1) +> - `src/components/CadastroRapidoConvenio.vue` → migrar pra `features/insurance/components/` +> - `src/components/ComponentCadastroRapido.vue` → migrar pra path apropriado conforme dono da entidade + +--- + +## 1. Princípio + +**Problema:** usuário está num fluxo (ex: agendar uma sessão) e precisa de uma entidade dependente que ainda não existe (serviço, convênio, plano). Navegar pra outra página significa **perder o contexto** do form em progresso. + +**Solução:** mini-dialog **por cima** do dialog/fluxo atual, com **campos mínimos** pra criar a entidade, e ao salvar **pré-seleciona** ela no select que disparou o quick-create. + +**Regra absoluta:** criar dependência faltante em **qualquer fluxo** deve **abrir overlay POR CIMA, nunca navegar pra fora**. Aplicável em todo o sistema desde a promoção do blueprint (2026-05-20). Origem do pattern: agenda (memória `feedback_agenda_inline_quick_create`, agora generalizada). + +--- + +## 2. Quando aplicar (vs alternativas) + +| Situação | Solução | +|---|---| +| Fluxo crítico travado por dependência faltante (form em progresso) | **Quick-create overlay** ✅ | +| Cadastro completo, com todos os campos | Página dedicada `/entity/new` ou Dialog full | +| Apenas selecionar item existente | Select com busca; sem botão "+" | +| Onboarding ou setup wizard | Não — fluxo é a página inteira, não um overlay | + +**Anti-uso:** quick-create NÃO é "shortcut pra criar do menu lateral". É **fallback contextual** quando o form atual depende de algo que falta. O parent **precisa estar pronto pra receber o evento `created`** e usar o ID. + +--- + +## 3. Estrutura do componente `QuickCreateDialog.vue` + +```vue + + + + + +``` + +--- + +## 4. Contrato canônico de props/emits + +### Props (sempre) + +| Prop | Tipo | Default | Função | +|---|---|---|---| +| `modelValue` | `Boolean` | `false` | Visibilidade do dialog. Two-way via `v-model`. | +| `ownerId` | `String` | `''` | Owner_id (terapeuta). Default: usuário logado. | +| `initialName` | `String` | `''` | Pré-preenche o campo nome com o search atual do select (UX win). | + +### Props (opcionais por entidade) + +- `parentId` (`String`) — quando a entidade tem hierarquia (ex: `plan_id` em `plan_service`) +- `defaultDurationMin` (`Number`) — quando faz sentido herdar valor do contexto +- Outras herdadas do contexto, **nunca** mais que 3 props extras (senão vira form pesado, não quick-create) + +### Emits + +| Evento | Payload | Quando | +|---|---|---| +| `update:modelValue` | `Boolean` | `v-model` two-way | +| `created` | `Object` (row inserida completa) | Após insert bem-sucedido | + +**Nunca emitir** `cancelled`, `closed`, `error` — parent não precisa saber dessas distinções; `update:modelValue=false` cobre. + +--- + +## 5. Integração no parent + +### Slot do botão `+` ao lado do select + +```vue +
+