MelissaPaciente: dialog inline nova sessao + createSession mutation

Espelha o padrao do "Lancamento" mas pra agenda — botao "Agendar" agora
navega pra aba Agenda e abre dialog de nova sessao.

NOVO em src/features/patients/composables/usePatientSessions.js
- createSession(patientId, payload) — INSERT agenda_eventos com
  status='agendado', resolve owner_id (auth.getUser) e tenant_id (lazy
  import tenantStore). Auto-reload via _lastPatientId.
  Validacao: inicio_em + fim_em obrigatorios.
  Retorna {ok, data?, error?}.

NOVO em MelissaPaciente.vue
- Refs novaSessaoOpen + novaSessaoForm (tipo/data/hora/duracao_min/
  modalidade/titulo_custom/observacoes)
- 3 catalogos:
  - SESSAO_TIPOS: Sessao/Primeira/Retorno/Avaliacao/Devolutiva
  - SESSAO_DURACOES: 30/40/45/50/55/60/90/120 min
  - SESSAO_MODALIDADES: Presencial/Online
- goAgendar() agora alem de navegar pra aba Agenda, tambem inicializa
  o form (default amanha 09:00, sessao 50min presencial) e abre o dialog.
- salvarSessao() handler com validacao (data + hora) e construcao de
  inicio_em/fim_em a partir de data + hora + duracao_min. Local time
  -> ISO via Date constructor.
- <Dialog> 460px com form: Tipo + grid 2-col (data + hora) + grid 2-col
  (duracao + modalidade) + titulo opcional + observacoes Textarea.
- CSS .mpa-novo-lanc__opt pra "(opcional)" em cinza.

Validacoes:
- Data e hora obrigatorios (warn toast)
- Date constructor invalido -> warn toast

Pra criar sessoes mais complexas (recorrencia, multi-paciente, conflitos
de agenda), o user vai pra MelissaAgenda direto que tem o
AgendaEventDialog completo. Aqui no prontuario eh o caminho rapido.

ESLint: 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-08 11:20:00 -03:00
parent 21c71f75d6
commit a8ab13b201
2 changed files with 250 additions and 3 deletions
@@ -84,6 +84,64 @@ export function usePatientSessions() {
.slice(0, 6)
);
/**
* Cria uma nova sessao na agenda do paciente.
*
* payload: {
* inicio_em: ISO timestamp,
* fim_em: ISO timestamp,
* tipo: 'sessao' | 'primeira' | 'retorno' | etc,
* modalidade: 'presencial' | 'online',
* titulo?: string,
* titulo_custom?: string,
* observacoes?: string
* }
* Retorna {ok, data?, error?}.
*/
async function createSession(patientId, payload = {}) {
if (!patientId || busy.value) return { ok: false, error: 'busy' };
if (!payload?.inicio_em || !payload?.fim_em) {
return { ok: false, error: 'Inicio/fim obrigatorios' };
}
busy.value = true;
try {
const { data: userData } = await supabase.auth.getUser();
const ownerId = userData?.user?.id;
let tenantId = null;
try {
const { useTenantStore } = await import('@/stores/tenantStore');
tenantId = useTenantStore().activeTenantId || null;
} catch { /* sem tenant store — segue */ }
const row = {
patient_id: patientId,
owner_id: ownerId,
tenant_id: tenantId,
inicio_em: payload.inicio_em,
fim_em: payload.fim_em,
status: 'agendado',
modalidade: payload.modalidade || 'presencial',
tipo: payload.tipo || 'sessao',
titulo: String(payload.titulo || '').trim() || null,
titulo_custom: String(payload.titulo_custom || '').trim() || null,
observacoes: String(payload.observacoes || '').trim() || null
};
const { data, error: err } = await supabase
.from('agenda_eventos')
.insert([row])
.select()
.single();
if (err) throw err;
if (_lastPatientId) await load(_lastPatientId);
return { ok: true, data };
} catch (e) {
return { ok: false, error: e?.message || 'Erro ao agendar sessao' };
} finally {
busy.value = false;
}
}
/**
* Atualiza o status de uma sessao (mutation). Recarrega a lista do paciente
* ao final pra refletir o novo estado nos computeds derivados.
@@ -114,6 +172,7 @@ export function usePatientSessions() {
busy,
load,
updateStatus,
createSession,
proximaSessao,
ultimaSessao,
totalSessoes,