Files
agenciapsilmno/src/views/pages/preview/AgendaDialogV2Preview.vue
T
Leonardo 6d9b36d592 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>
2026-05-06 09:13:22 -03:00

309 lines
9.8 KiB
Vue

<!--
|--------------------------------------------------------------------------
| 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>