agenda Fase A: extrai utils puros pra features/agenda/utils
Decomposicao da agenda em prep pra replicar Rail/Clinica. 4 arquivos novos em src/features/agenda/utils/: - eventoTipo.js -> EVENTO_TIPO + normalize/derive + MAX_SESSION_MINUTES - dbFields.js -> pickDbFields whitelist (memoria pickdbfields_whitelist) - timeHelpers.js -> isUuid + addMinutesToTime + isoToDecimalHour + dateToISO - colors.js -> pickColor (status+tipo+isOccurrence) useMelissaAgenda.js (2863L -> 2792L): removeu definicoes locais (83 linhas), passou a importar dos utils. Aliases _addMinutesToTime e _dateToISO mantidos no escopo via import "as" pra nao mexer em 70+ callsites internos. Fase A = baseline zero-comportamental pra Rail/Clinica adotarem os mesmos helpers. Fase B (service de billing — applyStatusDecisions, createPackageContract, materializeAndCharge) vem em seguida. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI — paleta de cores da agenda
|
||||
|--------------------------------------------------------------------------
|
||||
| Mapping (tipo, status, isOccurrence) → hex color. Usado pelo card do
|
||||
| FullCalendar (borderColor/backgroundColor) e popovers de evento.
|
||||
|
|
||||
| Status manda mais do que tipo: realizado/faltou/cancelado têm cores
|
||||
| dedicadas (emerald/red/slate) independentes do tipo.
|
||||
|
|
||||
| Extraído de useMelissaAgenda.js (Fase A) pra reuso em Rail/Clínica.
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export function pickColor(tipo, status, isOccurrence) {
|
||||
const s = String(status || '').toLowerCase();
|
||||
if (s === 'realizado' || s === 'realizada') return '#10b981'; // emerald-500
|
||||
if (s === 'faltou') return '#ef4444'; // red-500
|
||||
if (s === 'cancelado' || s === 'cancelada') return '#94a3b8'; // slate-400
|
||||
|
||||
const t = String(tipo || '').toLowerCase();
|
||||
if (t === 'bloqueio') return '#64748b'; // slate-500
|
||||
if (t === 'supervisao' || t === 'supervisão') return '#a855f7'; // purple-500
|
||||
if (t === 'reuniao' || t === 'reunião') return '#0ea5e9'; // sky-500
|
||||
|
||||
// Sessão default — distingue virtual (violet-500) vs real (indigo-500)
|
||||
return isOccurrence ? '#8b5cf6' : '#6366f1';
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI — whitelist de campos do agenda_eventos
|
||||
|--------------------------------------------------------------------------
|
||||
| Whitelist canônica de campos aceitos na tabela agenda_eventos pra INSERT/
|
||||
| UPDATE via cliente. Filtra qualquer chave não-prevista (defesa contra bug
|
||||
| onde payload acidentalmente carrega field defaultado pelo banco — como
|
||||
| modalidade='presencial' do bug de 2026-05-16).
|
||||
|
|
||||
| Memoria: project_pickdbfields_whitelist.md — antes era inline em
|
||||
| useMelissaAgenda.js. Extraído na Fase A.
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const ALLOWED_FIELDS = [
|
||||
'tenant_id', 'owner_id', 'terapeuta_id', 'patient_id',
|
||||
'tipo', 'status', 'titulo', 'observacoes', 'modalidade',
|
||||
'inicio_em', 'fim_em', 'visibility_scope',
|
||||
'mirror_of_event_id', 'mirror_source',
|
||||
'determined_commitment_id', 'titulo_custom', 'extra_fields',
|
||||
'recurrence_id', 'recurrence_date',
|
||||
'price', 'insurance_plan_id', 'insurance_guide_number',
|
||||
'insurance_value', 'insurance_plan_service_id'
|
||||
];
|
||||
|
||||
export function pickDbFields(obj) {
|
||||
const out = {};
|
||||
for (const k of ALLOWED_FIELDS) {
|
||||
if (obj[k] !== undefined) out[k] = obj[k];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export { ALLOWED_FIELDS };
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI — utils de tipo de evento (agenda)
|
||||
|--------------------------------------------------------------------------
|
||||
| Helpers puros pra classificar/normalizar tipo de evento. Extraídos de
|
||||
| useMelissaAgenda.js (Fase A da decomposição agenda) pra reuso em
|
||||
| Rail/Clínica + utility puro testável.
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export const EVENTO_TIPO = Object.freeze({ SESSAO: 'sessao', BLOQUEIO: 'bloqueio' });
|
||||
export const EVENTO_TIPOS_VALIDOS = new Set([EVENTO_TIPO.SESSAO, EVENTO_TIPO.BLOQUEIO]);
|
||||
|
||||
// Limite máximo de duração de sessão (regra de negócio Melissa). DB constraint
|
||||
// session_duration_min_chk permite 10–240; convencionamos 120 (2h) aqui pra
|
||||
// evitar slots gigantes acidentais. Futuro: ler de agenda_configuracoes se
|
||||
// max_session_duration_min for adicionado.
|
||||
export const MAX_SESSION_MINUTES = 120;
|
||||
|
||||
export function normalizeEventoTipo(t, fallback = EVENTO_TIPO.SESSAO) {
|
||||
const s = String(t || '').trim().toLowerCase();
|
||||
if (!s) return fallback;
|
||||
if (s.includes('sess')) return EVENTO_TIPO.SESSAO;
|
||||
if (s.includes('bloq') || s.includes('ocup')) return EVENTO_TIPO.BLOQUEIO;
|
||||
return EVENTO_TIPOS_VALIDOS.has(s) ? s : fallback;
|
||||
}
|
||||
|
||||
export function deriveEventoTipoForNewEvent(payload) {
|
||||
const vis = String(payload?.visibility_scope || '').toLowerCase();
|
||||
const title = String(payload?.titulo || '').toLowerCase();
|
||||
if (vis === 'busy_only' || title.includes('ocup')) return EVENTO_TIPO.BLOQUEIO;
|
||||
return EVENTO_TIPO.SESSAO;
|
||||
}
|
||||
|
||||
export function deriveTituloDefaultByTipo(tipo) {
|
||||
return normalizeEventoTipo(tipo, EVENTO_TIPO.SESSAO) === EVENTO_TIPO.BLOQUEIO ? 'Ocupado' : 'Sessão';
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI — utils de tempo/data (agenda)
|
||||
|--------------------------------------------------------------------------
|
||||
| Helpers puros pra manipulação de tempo na agenda. Extraídos de
|
||||
| useMelissaAgenda.js (Fase A) pra reuso em Rail/Clínica.
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const UUID_RX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
|
||||
export function isUuid(v) {
|
||||
return UUID_RX.test(String(v || ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Soma minutos a um time "HH:MM" e retorna "HH:MM:SS".
|
||||
* Tolerante a input vazio (default 09:00).
|
||||
*/
|
||||
export function addMinutesToTime(timeStr, minutes) {
|
||||
const [h, m] = String(timeStr || '09:00').split(':').map(Number);
|
||||
const total = h * 60 + m + Number(minutes || 0);
|
||||
const hh = Math.floor(total / 60);
|
||||
const mm = total % 60;
|
||||
return `${String(hh).padStart(2, '0')}:${String(mm).padStart(2, '0')}:00`;
|
||||
}
|
||||
|
||||
/**
|
||||
* ISO timestamp → hora decimal (ex: "2026-05-21T14:30:00Z" → 14.5).
|
||||
* Usa hora local (não UTC) — propósito de exibição no calendário.
|
||||
*/
|
||||
export function isoToDecimalHour(iso) {
|
||||
if (!iso) return 0;
|
||||
const d = new Date(iso);
|
||||
return d.getHours() + d.getMinutes() / 60;
|
||||
}
|
||||
|
||||
/**
|
||||
* Date object → "YYYY-MM-DD" (formato ISO date sem hora).
|
||||
*/
|
||||
export function dateToISO(d) {
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${dd}`;
|
||||
}
|
||||
@@ -38,84 +38,19 @@ import { useCommitmentServices } from '@/features/agenda/composables/useCommitme
|
||||
import { useFeriados } from '@/composables/useFeriados';
|
||||
import { useAgendaBloqueios } from '@/features/agenda/composables/useAgendaBloqueios';
|
||||
|
||||
// ─── Constantes do domínio (espelhadas de AgendaTerapeutaPage) ──────────────
|
||||
const EVENTO_TIPO = Object.freeze({ SESSAO: 'sessao', BLOQUEIO: 'bloqueio' });
|
||||
const EVENTO_TIPOS_VALIDOS = new Set([EVENTO_TIPO.SESSAO, EVENTO_TIPO.BLOQUEIO]);
|
||||
|
||||
// Limite máximo de duração de sessão (regra de negócio Melissa). DB constraint
|
||||
// `session_duration_min_chk` permite 10–240; convencionamos 120 (2h) aqui pra
|
||||
// evitar slots gigantes acidentais. Futuro: ler de `agenda_configuracoes` se
|
||||
// `max_session_duration_min` for adicionado.
|
||||
const MAX_SESSION_MINUTES = 120;
|
||||
|
||||
function isUuid(v) {
|
||||
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(String(v || ''));
|
||||
}
|
||||
|
||||
function normalizeEventoTipo(t, fallback = EVENTO_TIPO.SESSAO) {
|
||||
const s = String(t || '').trim().toLowerCase();
|
||||
if (!s) return fallback;
|
||||
if (s.includes('sess')) return EVENTO_TIPO.SESSAO;
|
||||
if (s.includes('bloq') || s.includes('ocup')) return EVENTO_TIPO.BLOQUEIO;
|
||||
return EVENTO_TIPOS_VALIDOS.has(s) ? s : fallback;
|
||||
}
|
||||
|
||||
function deriveEventoTipoForNewEvent(payload) {
|
||||
const vis = String(payload?.visibility_scope || '').toLowerCase();
|
||||
const title = String(payload?.titulo || '').toLowerCase();
|
||||
if (vis === 'busy_only' || title.includes('ocup')) return EVENTO_TIPO.BLOQUEIO;
|
||||
return EVENTO_TIPO.SESSAO;
|
||||
}
|
||||
|
||||
function deriveTituloDefaultByTipo(tipo) {
|
||||
return normalizeEventoTipo(tipo, EVENTO_TIPO.SESSAO) === EVENTO_TIPO.BLOQUEIO ? 'Ocupado' : 'Sessão';
|
||||
}
|
||||
|
||||
function pickDbFields(obj) {
|
||||
const allowed = [
|
||||
'tenant_id', 'owner_id', 'terapeuta_id', 'patient_id',
|
||||
'tipo', 'status', 'titulo', 'observacoes', 'modalidade',
|
||||
'inicio_em', 'fim_em', 'visibility_scope',
|
||||
'mirror_of_event_id', 'mirror_source',
|
||||
'determined_commitment_id', 'titulo_custom', 'extra_fields',
|
||||
'recurrence_id', 'recurrence_date',
|
||||
'price', 'insurance_plan_id', 'insurance_guide_number',
|
||||
'insurance_value', 'insurance_plan_service_id'
|
||||
];
|
||||
const out = {};
|
||||
for (const k of allowed) {
|
||||
if (obj[k] !== undefined) out[k] = obj[k];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function _addMinutesToTime(timeStr, minutes) {
|
||||
const [h, m] = String(timeStr || '09:00').split(':').map(Number);
|
||||
const total = h * 60 + m + Number(minutes || 0);
|
||||
const hh = Math.floor(total / 60);
|
||||
const mm = total % 60;
|
||||
return `${String(hh).padStart(2, '0')}:${String(mm).padStart(2, '0')}:00`;
|
||||
}
|
||||
|
||||
// ─── Melissa-style normalize (color, label, startH/endH, dateKey, _raw) ─────
|
||||
function pickColor(tipo, status, isOccurrence) {
|
||||
const s = String(status || '').toLowerCase();
|
||||
if (s === 'realizado' || s === 'realizada') return '#10b981';
|
||||
if (s === 'faltou') return '#ef4444';
|
||||
if (s === 'cancelado' || s === 'cancelada') return '#94a3b8';
|
||||
|
||||
const t = String(tipo || '').toLowerCase();
|
||||
if (t === 'bloqueio') return '#64748b';
|
||||
if (t === 'supervisao' || t === 'supervisão') return '#a855f7';
|
||||
if (t === 'reuniao' || t === 'reunião') return '#0ea5e9';
|
||||
return isOccurrence ? '#8b5cf6' : '#6366f1'; // virtual: violet, real: indigo
|
||||
}
|
||||
|
||||
function isoToDecimalHour(iso) {
|
||||
if (!iso) return 0;
|
||||
const d = new Date(iso);
|
||||
return d.getHours() + d.getMinutes() / 60;
|
||||
}
|
||||
// ─── Utilities puros (extraídos na Fase A da decomposição agenda) ───────────
|
||||
// Mantidos em features/agenda/utils/ pra reuso em Rail/Clínica.
|
||||
import {
|
||||
EVENTO_TIPO,
|
||||
EVENTO_TIPOS_VALIDOS,
|
||||
MAX_SESSION_MINUTES,
|
||||
normalizeEventoTipo,
|
||||
deriveEventoTipoForNewEvent,
|
||||
deriveTituloDefaultByTipo
|
||||
} from '@/features/agenda/utils/eventoTipo';
|
||||
import { pickDbFields } from '@/features/agenda/utils/dbFields';
|
||||
import { isUuid, addMinutesToTime as _addMinutesToTime, isoToDecimalHour, dateToISO as _dateToISO } from '@/features/agenda/utils/timeHelpers';
|
||||
import { pickColor } from '@/features/agenda/utils/colors';
|
||||
|
||||
function normalizeForMelissa(r, paymentStateMap = null, paymentAmountMap = null, rulePaymentMap = null, ruleContractMap = null) {
|
||||
// r pode vir de useAgendaEvents (linha real do banco) ou de loadAndExpand
|
||||
@@ -2854,10 +2789,4 @@ function _generateOccurrenceDates(rule, max, exceptionDates) {
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
|
||||
function _dateToISO(d) {
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
// _dateToISO foi extraído pra @/features/agenda/utils/timeHelpers — import no topo.
|
||||
|
||||
Reference in New Issue
Block a user