Correcao Sidebar Classico e Rail, Correcao Layout, Ajuste de Breakpoint para Tailwind, Ajuste AppTopbar, Ajuste Menu PopOver, Recriado Paleta de Cores, Inserido algumas animações leves, Reajuste Cor items NOVOS da tabela, Drawer Ajuda Corrigido no Logout, Whatsapp, sms, email, recursos extras

This commit is contained in:
Leonardo
2026-03-24 21:26:58 -03:00
parent a89d1f5560
commit 53a4980396
453 changed files with 121427 additions and 174407 deletions
@@ -15,151 +15,104 @@
|--------------------------------------------------------------------------
-->
<script setup>
import { computed, ref, watch } from 'vue'
import { computed, ref, watch } from 'vue';
const props = defineProps({
title: { type: String, default: 'Agenda' },
view: { type: String, default: 'day' }, // 'day' | 'week'
mode: { type: String, default: 'work_hours' } // 'full_24h' | 'work_hours'
})
title: { type: String, default: 'Agenda' },
view: { type: String, default: 'day' }, // 'day' | 'week'
mode: { type: String, default: 'work_hours' } // 'full_24h' | 'work_hours'
});
const emit = defineEmits([
'today',
'prev',
'next',
'changeView',
'toggleMode',
'createSession',
'createBlock',
'search'
])
const emit = defineEmits(['today', 'prev', 'next', 'changeView', 'toggleMode', 'createSession', 'createBlock', 'search']);
// UX: busca com debounce simples
const searchLocal = ref('')
let t = null
const searchLocal = ref('');
let t = null;
watch(searchLocal, (v) => {
clearTimeout(t)
t = setTimeout(() => emit('search', v), 220)
})
clearTimeout(t);
t = setTimeout(() => emit('search', v), 220);
});
// SelectButtons (executivo, direto)
const viewOptions = [
{ label: 'Dia', value: 'day' },
{ label: 'Semana', value: 'week' }
]
{ label: 'Dia', value: 'day' },
{ label: 'Semana', value: 'week' }
];
const modeOptions = [
{ label: 'Horário de funcionamento', value: 'work_hours' },
{ label: 'Dia completo (24h)', value: 'full_24h' }
]
{ label: 'Horário de funcionamento', value: 'work_hours' },
{ label: 'Dia completo (24h)', value: 'full_24h' }
];
const modeTag = computed(() => {
return props.mode === 'full_24h'
? { severity: 'info', text: '24h' }
: { severity: 'success', text: 'Funcionamento' }
})
return props.mode === 'full_24h' ? { severity: 'info', text: '24h' } : { severity: 'success', text: 'Funcionamento' };
});
function onChangeView(val) {
emit('changeView', val)
emit('changeView', val);
}
function onToggleMode(val) {
emit('toggleMode', val)
emit('toggleMode', val);
}
</script>
<template>
<Card class="mb-3 md:mb-4">
<template #content>
<div class="flex flex-column gap-3">
<Card class="mb-3 md:mb-4">
<template #content>
<div class="flex flex-column gap-3">
<!-- Top row -->
<div class="flex align-items-start justify-content-between gap-3 flex-wrap">
<div class="min-w-0">
<div class="flex align-items-center gap-2 flex-wrap">
<div class="text-xl md:text-2xl font-semibold leading-tight">
{{ title }}
</div>
<Tag :severity="modeTag.severity" :value="modeTag.text" />
</div>
<div class="text-sm mt-1" style="color: var(--text-color-secondary)">Navegue, filtre e crie compromissos com velocidade sem perder controle clínico.</div>
</div>
<!-- Top row -->
<div class="flex align-items-start justify-content-between gap-3 flex-wrap">
<div class="min-w-0">
<div class="flex align-items-center gap-2 flex-wrap">
<div class="text-xl md:text-2xl font-semibold leading-tight">
{{ title }}
</div>
<Tag :severity="modeTag.severity" :value="modeTag.text" />
<div class="flex align-items-center gap-2 flex-wrap">
<Button size="small" icon="pi pi-plus" label="Sessão" @click="emit('createSession')" />
<Button size="small" icon="pi pi-lock" severity="secondary" label="Bloqueio" @click="emit('createBlock')" />
</div>
</div>
<Divider class="my-0" />
<!-- Controls row -->
<div class="grid gap-3 md:gap-4" style="grid-template-columns: 1fr">
<div class="grid gap-3 md:gap-4 items-center" style="grid-template-columns: 1fr">
<div class="flex align-items-center justify-content-between gap-3 flex-wrap">
<!-- Nav -->
<div class="flex align-items-center gap-2 flex-wrap">
<Button size="small" text icon="pi pi-angle-left" @click="emit('prev')" />
<Button size="small" text icon="pi pi-angle-right" @click="emit('next')" />
<Button size="small" icon="pi pi-calendar" label="Hoje" @click="emit('today')" />
</div>
<!-- View + Mode -->
<div class="flex align-items-center gap-2 flex-wrap">
<SelectButton :modelValue="view" :options="viewOptions" optionLabel="label" optionValue="value" @update:modelValue="onChangeView" />
<SelectButton :modelValue="mode" :options="modeOptions" optionLabel="label" optionValue="value" @update:modelValue="onToggleMode" />
</div>
</div>
<!-- Search -->
<div class="flex align-items-center gap-2 flex-wrap">
<div class="flex-1 min-w-[260px]">
<FloatLabel>
<InputText id="agenda-search" v-model="searchLocal" class="w-full" />
<label for="agenda-search">Buscar por título / observação</label>
</FloatLabel>
</div>
<Button size="small" text icon="pi pi-times" :disabled="!searchLocal" @click="searchLocal = ''" />
</div>
</div>
</div>
</div>
<div class="text-sm mt-1" style="color: var(--text-color-secondary);">
Navegue, filtre e crie compromissos com velocidade sem perder controle clínico.
</div>
</div>
<div class="flex align-items-center gap-2 flex-wrap">
<Button
size="small"
icon="pi pi-plus"
label="Sessão"
@click="emit('createSession')"
/>
<Button
size="small"
icon="pi pi-lock"
severity="secondary"
label="Bloqueio"
@click="emit('createBlock')"
/>
</div>
</div>
<Divider class="my-0" />
<!-- Controls row -->
<div class="grid gap-3 md:gap-4" style="grid-template-columns: 1fr;">
<div class="grid gap-3 md:gap-4 items-center" style="grid-template-columns: 1fr;">
<div class="flex align-items-center justify-content-between gap-3 flex-wrap">
<!-- Nav -->
<div class="flex align-items-center gap-2 flex-wrap">
<Button size="small" text icon="pi pi-angle-left" @click="emit('prev')" />
<Button size="small" text icon="pi pi-angle-right" @click="emit('next')" />
<Button size="small" icon="pi pi-calendar" label="Hoje" @click="emit('today')" />
</div>
<!-- View + Mode -->
<div class="flex align-items-center gap-2 flex-wrap">
<SelectButton
:modelValue="view"
:options="viewOptions"
optionLabel="label"
optionValue="value"
@update:modelValue="onChangeView"
/>
<SelectButton
:modelValue="mode"
:options="modeOptions"
optionLabel="label"
optionValue="value"
@update:modelValue="onToggleMode"
/>
</div>
</div>
<!-- Search -->
<div class="flex align-items-center gap-2 flex-wrap">
<div class="flex-1 min-w-[260px]">
<FloatLabel>
<InputText id="agenda-search" v-model="searchLocal" class="w-full" />
<label for="agenda-search">Buscar por título / observação</label>
</FloatLabel>
</div>
<Button
size="small"
text
icon="pi pi-times"
:disabled="!searchLocal"
@click="searchLocal = ''"
/>
</div>
</div>
</div>
</div>
</template>
</Card>
</template>
</template>
</Card>
</template>
@@ -15,139 +15,123 @@
|--------------------------------------------------------------------------
-->
<script setup>
import { computed } from 'vue'
import { computed } from 'vue';
const props = defineProps({
stats: { type: Object, default: () => ({}) }
})
stats: { type: Object, default: () => ({}) }
});
const emit = defineEmits(['quickBlock', 'quickCreate'])
const emit = defineEmits(['quickBlock', 'quickCreate']);
const dados_de_exemplo = {
totalSessions: 6,
totalMinutes: 300,
biggestFreeWindow: '2h 10m',
pending: 0,
reschedules: 1,
attentions: 1,
suggested1: '14:00',
suggested2: '16:30',
nextBreak: '12:00'
}
totalSessions: 6,
totalMinutes: 300,
biggestFreeWindow: '2h 10m',
pending: 0,
reschedules: 1,
attentions: 1,
suggested1: '14:00',
suggested2: '16:30',
nextBreak: '12:00'
};
const s = computed(() => {
const base = props.stats && Object.keys(props.stats).length ? props.stats : dados_de_exemplo
return {
totalSessions: base.totalSessions ?? 0,
totalMinutes: base.totalMinutes ?? 0,
biggestFreeWindow: base.biggestFreeWindow ?? '—',
pending: base.pending ?? 0,
reschedules: base.reschedules ?? 0,
attentions: base.attentions ?? 0,
suggested1: base.suggested1 ?? '—',
suggested2: base.suggested2 ?? '—',
nextBreak: base.nextBreak ?? '—'
}
})
const base = props.stats && Object.keys(props.stats).length ? props.stats : dados_de_exemplo;
return {
totalSessions: base.totalSessions ?? 0,
totalMinutes: base.totalMinutes ?? 0,
biggestFreeWindow: base.biggestFreeWindow ?? '—',
pending: base.pending ?? 0,
reschedules: base.reschedules ?? 0,
attentions: base.attentions ?? 0,
suggested1: base.suggested1 ?? '—',
suggested2: base.suggested2 ?? '—',
nextBreak: base.nextBreak ?? '—'
};
});
function minutesToHuman(min) {
const n = Number(min || 0)
const h = Math.floor(n / 60)
const m = n % 60
if (h <= 0) return `${m}m`
if (m <= 0) return `${h}h`
return `${h}h ${m}m`
const n = Number(min || 0);
const h = Math.floor(n / 60);
const m = n % 60;
if (h <= 0) return `${m}m`;
if (m <= 0) return `${h}h`;
return `${h}h ${m}m`;
}
const totalTimeHuman = computed(() => minutesToHuman(s.value.totalMinutes))
const totalTimeHuman = computed(() => minutesToHuman(s.value.totalMinutes));
const attentionSeverity = computed(() => {
if (s.value.attentions >= 3) return 'danger'
if (s.value.attentions >= 1) return 'warning'
return 'success'
})
if (s.value.attentions >= 3) return 'danger';
if (s.value.attentions >= 1) return 'warning';
return 'success';
});
</script>
<template>
<Card>
<template #title>
<div class="flex align-items-center justify-content-between gap-2 flex-wrap">
<div class="flex flex-column">
<span>Pulso da agenda</span>
<span class="text-xs" style="color: var(--text-color-secondary);">
Indicadores rápidos para decisão imediata.
</span>
</div>
<Card>
<template #title>
<div class="flex align-items-center justify-content-between gap-2 flex-wrap">
<div class="flex flex-column">
<span>Pulso da agenda</span>
<span class="text-xs" style="color: var(--text-color-secondary)"> Indicadores rápidos para decisão imediata. </span>
</div>
<div class="flex align-items-center gap-2">
<Tag :severity="attentionSeverity" :value="`Atenções: ${s.attentions}`" />
</div>
</div>
</template>
<template #content>
<div class="grid gap-2">
<!-- Linha 1: métricas -->
<div class="grid gap-2" style="grid-template-columns: 1fr 1fr;">
<div class="p-3 border-round-xl" style="border: 1px solid var(--surface-border); background: var(--surface-card);">
<div class="text-xs" style="color: var(--text-color-secondary);">Sessões (no filtro)</div>
<div class="text-xl font-semibold mt-1">{{ s.totalSessions }}</div>
</div>
<div class="p-3 border-round-xl" style="border: 1px solid var(--surface-border); background: var(--surface-card);">
<div class="text-xs" style="color: var(--text-color-secondary);">Tempo total</div>
<div class="text-xl font-semibold mt-1">{{ totalTimeHuman }}</div>
</div>
</div>
<!-- Linha 2: janelas e atenção -->
<div class="grid gap-2" style="grid-template-columns: 1fr 1fr;">
<div class="p-3 border-round-xl" style="border: 1px solid var(--surface-border); background: var(--surface-card);">
<div class="text-xs" style="color: var(--text-color-secondary);">Maior janela livre</div>
<div class="text-base font-semibold mt-1">{{ s.biggestFreeWindow }}</div>
<div class="text-xs mt-2" style="color: var(--text-color-secondary);">
Sugestões: <b>{{ s.suggested1 }}</b> e <b>{{ s.suggested2 }}</b>
<div class="flex align-items-center gap-2">
<Tag :severity="attentionSeverity" :value="`Atenções: ${s.attentions}`" />
</div>
</div>
</div>
</template>
<div class="p-3 border-round-xl" style="border: 1px solid var(--surface-border); background: var(--surface-card);">
<div class="text-xs" style="color: var(--text-color-secondary);">Pontos de atenção</div>
<div class="flex align-items-center gap-2 mt-2 flex-wrap">
<Tag severity="warning" :value="`Pendências: ${s.pending}`" />
<Tag severity="info" :value="`Remarcar: ${s.reschedules}`" />
<template #content>
<div class="grid gap-2">
<!-- Linha 1: métricas -->
<div class="grid gap-2" style="grid-template-columns: 1fr 1fr">
<div class="p-3 border-round-xl" style="border: 1px solid var(--surface-border); background: var(--surface-card)">
<div class="text-xs" style="color: var(--text-color-secondary)">Sessões (no filtro)</div>
<div class="text-xl font-semibold mt-1">{{ s.totalSessions }}</div>
</div>
<div class="p-3 border-round-xl" style="border: 1px solid var(--surface-border); background: var(--surface-card)">
<div class="text-xs" style="color: var(--text-color-secondary)">Tempo total</div>
<div class="text-xl font-semibold mt-1">{{ totalTimeHuman }}</div>
</div>
</div>
<!-- Linha 2: janelas e atenção -->
<div class="grid gap-2" style="grid-template-columns: 1fr 1fr">
<div class="p-3 border-round-xl" style="border: 1px solid var(--surface-border); background: var(--surface-card)">
<div class="text-xs" style="color: var(--text-color-secondary)">Maior janela livre</div>
<div class="text-base font-semibold mt-1">{{ s.biggestFreeWindow }}</div>
<div class="text-xs mt-2" style="color: var(--text-color-secondary)">
Sugestões: <b>{{ s.suggested1 }}</b> e <b>{{ s.suggested2 }}</b>
</div>
</div>
<div class="p-3 border-round-xl" style="border: 1px solid var(--surface-border); background: var(--surface-card)">
<div class="text-xs" style="color: var(--text-color-secondary)">Pontos de atenção</div>
<div class="flex align-items-center gap-2 mt-2 flex-wrap">
<Tag severity="warning" :value="`Pendências: ${s.pending}`" />
<Tag severity="info" :value="`Remarcar: ${s.reschedules}`" />
</div>
<div class="text-xs mt-2" style="color: var(--text-color-secondary)">
Próxima pausa: <b>{{ s.nextBreak }}</b>
</div>
</div>
</div>
<Divider class="my-2" />
<!-- Ações rápidas -->
<div class="flex align-items-center justify-content-between gap-2 flex-wrap">
<div class="text-xs" style="color: var(--text-color-secondary)">Ações rápidas (criação sempre abre modal nada nasce "direto").</div>
<div class="flex align-items-center gap-2">
<Button size="small" icon="pi pi-plus" label="Nova sessão" @click="emit('quickCreate')" />
<Button size="small" icon="pi pi-lock" severity="secondary" label="Bloquear horário" @click="emit('quickBlock')" />
</div>
</div>
</div>
<div class="text-xs mt-2" style="color: var(--text-color-secondary);">
Próxima pausa: <b>{{ s.nextBreak }}</b>
</div>
</div>
</div>
<Divider class="my-2" />
<!-- Ações rápidas -->
<div class="flex align-items-center justify-content-between gap-2 flex-wrap">
<div class="text-xs" style="color: var(--text-color-secondary);">
Ações rápidas (criação sempre abre modal nada nasce "direto").
</div>
<div class="flex align-items-center gap-2">
<Button
size="small"
icon="pi pi-plus"
label="Nova sessão"
@click="emit('quickCreate')"
/>
<Button
size="small"
icon="pi pi-lock"
severity="secondary"
label="Bloquear horário"
@click="emit('quickBlock')"
/>
</div>
</div>
</div>
</template>
</Card>
</template>
</template>
</Card>
</template>