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
@@ -179,6 +179,58 @@ export const DOC_TYPE_LABEL = {
outro: 'Outro'
};
/**
* Map de dia da semana (0=Domingo) -> label pt-br.
*/
export const WEEKDAY_LABEL = ['Domingo', 'Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta', 'Sábado'];
export const WEEKDAY_LABEL_SHORT = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'];
/**
* Label legivel da regra de recorrencia.
* Ex: "Toda segunda às 14:00", "A cada 2 semanas, terça às 09:00",
* "Quartas e sextas às 16:00", "Mensal no dia 15".
*/
export function fmtRecurrenceLabel(rule) {
if (!rule) return '—';
const time = String(rule.start_time || '').slice(0, 5);
const interval = Number(rule.interval) || 1;
if (rule.type === 'weekly' || (rule.type === 'biweekly' && interval === 1)) {
const dow = (rule.weekdays || [])[0];
if (dow == null) return time ? `Semanal às ${time}` : 'Semanal';
const dayLbl = WEEKDAY_LABEL[dow] || '?';
if (rule.type === 'biweekly') {
return time ? `Quinzenal · ${dayLbl} às ${time}` : `Quinzenal · ${dayLbl}`;
}
return time ? `Toda ${dayLbl.toLowerCase()} às ${time}` : `Toda ${dayLbl.toLowerCase()}`;
}
if (rule.type === 'custom_weekdays') {
const dows = (rule.weekdays || []).map((d) => WEEKDAY_LABEL_SHORT[d]).filter(Boolean);
const dayList = dows.length ? dows.join(', ') : '?';
return time ? `${dayList} às ${time}` : dayList;
}
if (rule.type === 'monthly') {
return time ? `Mensal às ${time}` : 'Mensal';
}
if (rule.type === 'yearly') {
return time ? `Anual às ${time}` : 'Anual';
}
return rule.type || 'Recorrência';
}
/**
* Label pro fim da regra: "Sem data de fim", "Até DD/MM/YYYY", "N sessões no total".
*/
export function fmtRecurrenceFim(rule) {
if (!rule) return '';
if (rule.end_date) return `Até ${fmtDateBR(rule.end_date)}`;
if (rule.max_occurrences) {
const n = Number(rule.max_occurrences);
return `${n} ${n === 1 ? 'sessão' : 'sessões'} no total`;
}
return 'Sem data de fim';
}
/**
* Channel label pra conversa: whatsapp -> WhatsApp, sms -> SMS, email -> E-mail.
*/