MelissaPaciente: bloco "Recorrencias do paciente" na Tab Agenda

User aprovou a ideia. Adiciona contexto "este paciente tem sessao toda
segunda 14h" direto no prontuario, evitando duplicacao de regras e
deixando claro o estado da serie.

NOVO src/features/patients/composables/usePatientRecurrences.js (~110L)
- load(patientId): SELECT recurrence_rules WHERE patient_id (DESC start_date)
- cancel(ruleId) / reactivate(ruleId): UPDATE status + auto-reload
- Computeds derivados: ativas, canceladas, totalAtivas, totalCanceladas
- busy flag pra disable de buttons

EXTENSAO src/features/patients/utils/patientFormatters.js
- WEEKDAY_LABEL + WEEKDAY_LABEL_SHORT (arrays 0=Domingo..6=Sabado)
- fmtRecurrenceLabel(rule): "Toda segunda às 14:00", "Quinzenal · Terça
  às 09:00", "Qua, Sex às 16:00" (custom_weekdays), "Mensal às 14:00",
  "Anual" — cobre todos os types do useRecurrence.
- fmtRecurrenceFim(rule): "Sem data de fim" / "Até DD/MM/YYYY" /
  "N sessões no total"

MELISSAPACIENTE.VUE
- Composable + handlers (onCancelRecurrence, onReactivateRecurrence) com
  toast feedback.
- recorrenciasShowCanc ref + recorrenciasVisiveis computed (toggle "ver
  canceladas").
- loadAll inclui recorrenciasHook.load.
- salvarSessao no caminho recorrente recarrega sessions+recorrencias em
  Promise.all (regra recem-criada aparece na lista imediatamente).
- 5o KPI na Tab Agenda: "Recorrencias" com count ativas + cap dinamica
  (cor #a855f7 quando > 0, cinza quando 0).
- Bloco <section class="mpa-panel"> entre KPIs e filter chips listando
  rules ativas (default) ou todas (toggle "Ver canceladas" no header,
  so aparece quando ha canceladas):
  - Icon roxo .mpa-recur-item__icon
  - Top: label + Tag status (verde Ativa / amarelo Cancelada)
  - Meta: duracao + modalidade + fim + "desde DATE"
  - Obs (quando preenchido): block textual
  - Actions: pi-ban (cancelar) ou pi-undo (reativar) com tooltip
- border-left adaptativa (#a855f7 ativo / cinza cancelado) + opacity 0.7
  pros cancelados.
- Mobile: stack icon+main em 2-col 2-row; actions full-width abaixo.

CSS: ~120L novos. Padrao Melissa: status pills, icon roxo distintivo
(diferente das sessoes que usam cinza), border-left por status.

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:37:45 -03:00
parent f1d6fbad73
commit 9e76e4e6ea
3 changed files with 426 additions and 3 deletions
@@ -0,0 +1,103 @@
/*
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Arquivo: src/features/patients/composables/usePatientRecurrences.js
|
| Carrega regras de recorrencia (recurrence_rules) filtradas por paciente.
| Usado pela Tab Agenda do MelissaPaciente pra mostrar "este paciente tem
| sessao toda segunda 14h" e dar acoes inline (cancelar/reativar).
|
| Mutations espelham o pattern de MelissaRecorrencias.vue.
|--------------------------------------------------------------------------
*/
import { ref, computed } from 'vue';
import { supabase } from '@/lib/supabase/client';
export function usePatientRecurrences() {
const rules = ref([]);
const loading = ref(false);
const error = ref('');
const busy = ref(false);
let _lastPatientId = null;
async function load(patientId) {
_lastPatientId = patientId || null;
if (!patientId) {
rules.value = [];
return;
}
loading.value = true;
error.value = '';
rules.value = [];
try {
const { data, error: err } = await supabase
.from('recurrence_rules')
.select('*')
.eq('patient_id', patientId)
.order('start_date', { ascending: false });
if (err) throw err;
rules.value = data || [];
} catch (e) {
error.value = e?.message || 'Falha ao carregar recorrencias.';
rules.value = [];
} finally {
loading.value = false;
}
}
async function cancel(ruleId) {
if (!ruleId || busy.value) return { ok: false, error: 'busy' };
busy.value = true;
try {
const { error: err } = await supabase
.from('recurrence_rules')
.update({ status: 'cancelado', updated_at: new Date().toISOString() })
.eq('id', ruleId);
if (err) throw err;
if (_lastPatientId) await load(_lastPatientId);
return { ok: true };
} catch (e) {
return { ok: false, error: e?.message || 'Erro ao cancelar' };
} finally {
busy.value = false;
}
}
async function reactivate(ruleId) {
if (!ruleId || busy.value) return { ok: false, error: 'busy' };
busy.value = true;
try {
const { error: err } = await supabase
.from('recurrence_rules')
.update({ status: 'ativo', updated_at: new Date().toISOString() })
.eq('id', ruleId);
if (err) throw err;
if (_lastPatientId) await load(_lastPatientId);
return { ok: true };
} catch (e) {
return { ok: false, error: e?.message || 'Erro ao reativar' };
} finally {
busy.value = false;
}
}
const ativas = computed(() => rules.value.filter((r) => r.status === 'ativo'));
const canceladas = computed(() => rules.value.filter((r) => r.status === 'cancelado'));
const totalAtivas = computed(() => ativas.value.length);
const totalCanceladas = computed(() => canceladas.value.length);
return {
rules,
loading,
error,
busy,
load,
cancel,
reactivate,
ativas,
canceladas,
totalAtivas,
totalCanceladas
};
}