A66 WIP: AgendaEventDialog quebrado em 5 composables + 265 specs + V2 esqueleto
Sub-sessao 1 entregue (composables): - agendaEventHelpers (262L) — utilitarios puros (date, format, parse) - useAgendaEventComposer (485L) — montagem do form + validacao - useAgendaEventActions (387L) — save/delete/cancel/move actions - useAgendaEventPickerBilling (378L) — pickers (terapeuta, servico, convenio) + calculo de billing - useAgendaEventLifecycle (474L) — open/close/dirty state + autosave - 5 specs em __tests__/ (75+76+28+43+43 = 265 testes), 495/495 passing AgendaEventDialog: 3522 -> 2632 linhas (-25%) consumindo os composables. Backup byte-identico em AgendaEventDialog.vue.bak pra rollback. Sub-sessao 2 entregue (esqueleto, NAO TESTADO): - AgendaEventDialogV2 (~1100L, 3 zonas: PACIENTE/QUANDO/O QUE) - Preview em /preview/agenda-dialog-v2 com 5 cenarios - Rota em routes.misc.js - User testou e nao gostou do design — aguarda feedback especifico pra iteracao na sub-sessao 3 (migracao nos 9 consumers). Dialogs auxiliares novos pro AgendaEventDialog: - InsurancePlanQuickCreateDialog (criar convenio inline) - ServiceQuickCreateDialog (criar tipo de sessao inline) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Preview de AgendaEventDialogV2 com fixtures.
|
||||
| Rota: /preview/agenda-dialog-v2
|
||||
| Uso: visualizar e iterar o template V2 sem subir pra produção.
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import AgendaEventDialogV2 from '@/features/agenda/components/AgendaEventDialogV2.vue';
|
||||
|
||||
const open = ref(false);
|
||||
const scenario = ref('novo-vazio');
|
||||
|
||||
// ── Fixtures ──────────────────────────────────────────────
|
||||
const commitmentOptions = [
|
||||
{ id: 'cm-sessao', name: 'Sessão', description: 'Atendimento clínico padrão', native_type: 'session', bg_color: '6366f1', fields: [] },
|
||||
{ id: 'cm-bloqueio', name: 'Bloqueio', description: 'Indisponibilidade', native_type: 'bloqueio', bg_color: 'ef4444', fields: [] },
|
||||
{ id: 'cm-supervisao', name: 'Supervisão', description: 'Encontro com supervisor', bg_color: 'f59e0b', fields: [] },
|
||||
{ id: 'cm-evento', name: 'Evento', description: 'Reunião, palestra, etc.', bg_color: '10b981', fields: [] }
|
||||
];
|
||||
|
||||
const workRules = [
|
||||
{ dia_semana: 1, hora_inicio: '08:00', hora_fim: '18:00', ativo: true },
|
||||
{ dia_semana: 2, hora_inicio: '08:00', hora_fim: '18:00', ativo: true },
|
||||
{ dia_semana: 3, hora_inicio: '08:00', hora_fim: '18:00', ativo: true },
|
||||
{ dia_semana: 4, hora_inicio: '08:00', hora_fim: '18:00', ativo: true },
|
||||
{ dia_semana: 5, hora_inicio: '08:00', hora_fim: '17:00', ativo: true }
|
||||
];
|
||||
|
||||
const agendaSettings = {
|
||||
session_duration_min: 50,
|
||||
session_break_min: 10,
|
||||
slot_mode: 'fixed',
|
||||
online_ativo: true
|
||||
};
|
||||
|
||||
const allEvents = [];
|
||||
const pausasSemanais = [];
|
||||
const blockedDates = [];
|
||||
|
||||
// Cenários
|
||||
const scenarios = {
|
||||
'novo-vazio': {
|
||||
label: 'Novo (vazio)',
|
||||
eventRow: null,
|
||||
presetCommitmentId: null
|
||||
},
|
||||
'novo-sessao': {
|
||||
label: 'Novo (preset Sessão)',
|
||||
eventRow: null,
|
||||
presetCommitmentId: 'cm-sessao'
|
||||
},
|
||||
'edit-avulsa': {
|
||||
label: 'Editar avulsa',
|
||||
eventRow: {
|
||||
id: 'evt-edit-1',
|
||||
commitment_id: 'cm-sessao',
|
||||
paciente_id: 'p-1',
|
||||
paciente_nome: 'Marina Souza',
|
||||
paciente_status: 'Ativo',
|
||||
paciente_avatar: null,
|
||||
inicio_em: '2026-05-09T14:00:00',
|
||||
fim_em: '2026-05-09T14:50:00',
|
||||
modalidade: 'presencial',
|
||||
status: 'agendado',
|
||||
price: 200,
|
||||
observacoes: 'Sessão de retorno',
|
||||
recurrence_id: null,
|
||||
serie_id: null
|
||||
},
|
||||
presetCommitmentId: null
|
||||
},
|
||||
'edit-serie': {
|
||||
label: 'Editar série',
|
||||
eventRow: {
|
||||
id: 'evt-serie-1',
|
||||
commitment_id: 'cm-sessao',
|
||||
paciente_id: 'p-2',
|
||||
paciente_nome: 'Carlos Mendes',
|
||||
paciente_status: 'Ativo',
|
||||
paciente_avatar: null,
|
||||
inicio_em: '2026-05-12T10:00:00',
|
||||
fim_em: '2026-05-12T10:50:00',
|
||||
modalidade: 'online',
|
||||
status: 'agendado',
|
||||
price: 220,
|
||||
recurrence_id: 'rec-1',
|
||||
serie_id: 'rec-1',
|
||||
serie_dia_semana: 2,
|
||||
serie_hora: '10:00'
|
||||
},
|
||||
presetCommitmentId: null
|
||||
},
|
||||
'paciente-inativo': {
|
||||
label: 'Paciente inativo (lock)',
|
||||
eventRow: {
|
||||
id: 'evt-inativo-1',
|
||||
commitment_id: 'cm-sessao',
|
||||
paciente_id: 'p-3',
|
||||
paciente_nome: 'João Pereira',
|
||||
paciente_status: 'Inativo',
|
||||
inicio_em: '2026-06-01T15:00:00',
|
||||
fim_em: '2026-06-01T15:50:00',
|
||||
modalidade: 'presencial',
|
||||
status: 'agendado'
|
||||
},
|
||||
presetCommitmentId: null
|
||||
}
|
||||
};
|
||||
|
||||
const dialogProps = ref({
|
||||
eventRow: null,
|
||||
presetCommitmentId: null
|
||||
});
|
||||
|
||||
function abrir(key) {
|
||||
scenario.value = key;
|
||||
const sc = scenarios[key];
|
||||
dialogProps.value = {
|
||||
eventRow: sc.eventRow,
|
||||
presetCommitmentId: sc.presetCommitmentId
|
||||
};
|
||||
open.value = true;
|
||||
}
|
||||
|
||||
// ── Logs de events ───────────────────────────────────────
|
||||
const log = reactive({ entries: [] });
|
||||
function pushLog(label, payload) {
|
||||
log.entries.unshift({
|
||||
ts: new Date().toLocaleTimeString('pt-BR'),
|
||||
label,
|
||||
payload: JSON.stringify(payload, null, 2)
|
||||
});
|
||||
if (log.entries.length > 8) log.entries.pop();
|
||||
}
|
||||
|
||||
function onSave(payload) { pushLog('save', payload); }
|
||||
function onDelete(payload) { pushLog('delete', payload); }
|
||||
function onUpdateSeries(payload) { pushLog('updateSeriesEvent', payload); }
|
||||
function onEditSeriesOccurrence(payload) { pushLog('editSeriesOccurrence', payload); }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="aev2-preview-page">
|
||||
<header class="aev2-preview-header">
|
||||
<div>
|
||||
<h1 class="text-xl font-bold">AgendaEventDialogV2 — Preview</h1>
|
||||
<p class="text-sm opacity-70">A66 sub-sessão 2. Iterar visual antes de migrar consumers (sub-sessão 3).</p>
|
||||
</div>
|
||||
<div class="text-xs opacity-60">
|
||||
Cenário atual: <code>{{ scenario }}</code>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="aev2-preview-grid">
|
||||
<section>
|
||||
<h2 class="aev2-preview-section-title">Cenários</h2>
|
||||
<div class="aev2-preview-buttons">
|
||||
<button
|
||||
v-for="(sc, key) in scenarios"
|
||||
:key="key"
|
||||
type="button"
|
||||
class="aev2-preview-btn"
|
||||
:class="{ 'aev2-preview-btn--active': scenario === key }"
|
||||
@click="abrir(key)"
|
||||
>
|
||||
{{ sc.label }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="aev2-preview-section-title">Eventos emitidos</h2>
|
||||
<div class="aev2-preview-log">
|
||||
<div v-if="!log.entries.length" class="opacity-50 text-sm">
|
||||
Abra o dialog e dispare ações pra ver os emits aqui.
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-for="(e, i) in log.entries" :key="i" class="aev2-preview-log-entry">
|
||||
<div class="aev2-preview-log-meta">
|
||||
<span class="font-mono text-xs opacity-60">{{ e.ts }}</span>
|
||||
<span class="aev2-preview-log-label">{{ e.label }}</span>
|
||||
</div>
|
||||
<pre class="aev2-preview-log-payload">{{ e.payload }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<AgendaEventDialogV2
|
||||
v-model="open"
|
||||
:event-row="dialogProps.eventRow"
|
||||
:preset-commitment-id="dialogProps.presetCommitmentId"
|
||||
owner-id="owner-preview"
|
||||
tenant-id="tenant-preview"
|
||||
:commitment-options="commitmentOptions"
|
||||
:work-rules="workRules"
|
||||
:agenda-settings="agendaSettings"
|
||||
:all-events="allEvents"
|
||||
:pausas-semanais="pausasSemanais"
|
||||
:blocked-dates="blockedDates"
|
||||
@save="onSave"
|
||||
@delete="onDelete"
|
||||
@update-series-event="onUpdateSeries"
|
||||
@edit-series-occurrence="onEditSeriesOccurrence"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.aev2-preview-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1.5rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
.aev2-preview-header {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
}
|
||||
.aev2-preview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
.aev2-preview-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
.aev2-preview-section-title {
|
||||
font-size: .8rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .04em;
|
||||
opacity: .7;
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
.aev2-preview-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .35rem;
|
||||
}
|
||||
.aev2-preview-btn {
|
||||
text-align: left;
|
||||
border: 1px solid var(--surface-border);
|
||||
background: var(--surface-card);
|
||||
color: var(--text-color);
|
||||
padding: .65rem .85rem;
|
||||
border-radius: 10px;
|
||||
font-size: .85rem;
|
||||
cursor: pointer;
|
||||
transition: all .15s;
|
||||
}
|
||||
.aev2-preview-btn:hover {
|
||||
border-color: var(--p-primary-500, #6366f1);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
.aev2-preview-btn--active {
|
||||
background: var(--p-primary-500, #6366f1);
|
||||
color: #fff;
|
||||
border-color: var(--p-primary-500, #6366f1);
|
||||
font-weight: 600;
|
||||
}
|
||||
.aev2-preview-log {
|
||||
background: var(--surface-card);
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
max-height: 480px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.aev2-preview-log-entry {
|
||||
border-top: 1px solid var(--surface-border);
|
||||
padding: .5rem 0;
|
||||
}
|
||||
.aev2-preview-log-entry:first-child { border-top: 0; padding-top: 0; }
|
||||
.aev2-preview-log-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
.aev2-preview-log-label {
|
||||
background: var(--p-primary-500, #6366f1);
|
||||
color: #fff;
|
||||
padding: 1px 7px;
|
||||
border-radius: 4px;
|
||||
font-size: .7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.aev2-preview-log-payload {
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: .72rem;
|
||||
background: var(--surface-100);
|
||||
padding: .5rem;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user