# 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/<modulo>/composables/
├── use<Entity>.js              # CRUD básico (thin wrapper)
├── use<Entity>Clinic.js        # variant clinic-scoped (se aplicável)
├── use<Entity>Settings.js      # config/preferences (com cache opt-in)
├── use<Entity>Lifecycle.js     # orquestrador de estados (se domain complexo)
└── <entity>Helpers.js          # funções puras auxiliares (não-composable)
```

**Convenção de nome:** sempre `use<Entity>...`. 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/<modulo>/services/<feature>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/<feature>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.<feature>` (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<X>` | `load<Scope><Range>` |
| Criar | `create` | `create<Scope>` (se houver ambiguidade) |
| Atualizar | `update` | `update<Scope>` |
| Remover | `remove` | `remove<Scope>` (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.<feature>` constante (TTL em ms)
- `.get(key, scope, ttl)` retorna valor ou null
- `.set(key, value, scope)` salva com timestamp
- Invalidação manual: `.invalidate('<feature>')`

**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 <nome>`, 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<Scope>`, `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:<modulo>`
- `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:<modulo>`
- Estratégia: `development/02-auditoria/PADRONIZACAO.md`
