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:
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user