Files
agenciapsilmno/src/components/agenda/AgendaOnlineGradeCard.vue

235 lines
8.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/components/agenda/AgendaOnlineGradeCard.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { computed, onMounted, ref } from 'vue'
import TabView from 'primevue/tabview'
import TabPanel from 'primevue/tabpanel'
import ProgressSpinner from 'primevue/progressspinner'
import { useToast } from 'primevue/usetoast'
import { fetchSlotsRegras } from '@/services/agendaConfigService'
import { fetchSlotsBloqueados, setSlotBloqueado } from '@/services/agendaSlotsBloqueadosService'
import { gerarSlotsDoDia } from '@/utils/slotsGenerator'
import { supabase } from '@/lib/supabase/client'
const toast = useToast()
const props = defineProps({
ownerId: { type: String, required: true }
})
const diasSemana = [
{ label: 'Seg', value: 1 },
{ label: 'Ter', value: 2 },
{ label: 'Qua', value: 3 },
{ label: 'Qui', value: 4 },
{ label: 'Sex', value: 5 },
{ label: 'Sáb', value: 6 },
{ label: 'Dom', value: 0 }
]
const loading = ref(false)
const savingSlot = ref(false)
const slotsRegras = ref([]) // agenda_slots_regras
const regrasSemanais = ref([]) // agenda_regras_semanais
const bloqueadosByDia = ref({}) // {dia: Set('09:00'...)}
async function loadRegrasSemanais() {
const { data, error } = await supabase
.from('agenda_regras_semanais')
.select('*')
.eq('owner_id', props.ownerId)
.order('dia_semana', { ascending: true })
.order('hora_inicio', { ascending: true })
if (error) throw error
regrasSemanais.value = data || []
}
async function load() {
loading.value = true
try {
await Promise.all([
loadRegrasSemanais(),
(async () => { slotsRegras.value = await fetchSlotsRegras(props.ownerId) })()
])
// carregue bloqueados de todos os dias
const map = {}
for (const d of diasSemana.map(x => x.value)) {
const rows = await fetchSlotsBloqueados(props.ownerId, d)
map[d] = new Set(rows.map(r => String(r.hora_inicio).slice(0, 5)))
}
bloqueadosByDia.value = map
} catch (e) {
console.error(e)
toast.add({ severity: 'error', summary: 'Falha', detail: e?.message || 'Não foi possível carregar a grade.', life: 3200 })
} finally {
loading.value = false
}
}
function regraDoDia(dia) {
return slotsRegras.value.find(r => r.dia_semana === dia) || null
}
function janelasDoDia(dia) {
return (regrasSemanais.value || []).filter(r => r.dia_semana === dia && r.ativo !== false)
}
function slotsDoDia(dia) {
const regra = regraDoDia(dia)
if (!regra || regra.ativo === false) return []
return gerarSlotsDoDia(janelasDoDia(dia), regra)
}
function isBloqueado(dia, hhmm) {
return !!bloqueadosByDia.value?.[dia]?.has(hhmm)
}
async function toggleSlot(dia, hhmm) {
savingSlot.value = true
try {
const blocked = isBloqueado(dia, hhmm)
await setSlotBloqueado(props.ownerId, dia, hhmm, !blocked)
if (!bloqueadosByDia.value[dia]) bloqueadosByDia.value[dia] = new Set()
if (blocked) bloqueadosByDia.value[dia].delete(hhmm)
else bloqueadosByDia.value[dia].add(hhmm)
} catch (e) {
console.error(e)
toast.add({ severity: 'error', summary: 'Falha', detail: e?.message || 'Não foi possível atualizar o horário.', life: 3200 })
} finally {
savingSlot.value = false
}
}
const resumo = computed(() => {
// só um resumo simples — depois refinamos
const diasAtivos = diasSemana.filter(d => (regraDoDia(d.value)?.ativo !== false)).length
return { diasAtivos }
})
onMounted(load)
</script>
<template>
<Card class="overflow-hidden">
<template #title>
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2">
<i class="pi pi-globe" />
<span>Grade do online (estilo Altegio)</span>
</div>
<div class="flex gap-2">
<Button icon="pi pi-refresh" text rounded :disabled="loading" @click="load" />
</div>
</div>
</template>
<template #content>
<div v-if="loading" class="flex items-center gap-3 text-600">
<ProgressSpinner style="width:22px;height:22px" />
Carregando
</div>
<div v-else>
<!-- Resumo tipo "cards" -->
<div class="grid grid-cols-12 gap-3 mb-4">
<div class="col-span-12 md:col-span-4 p-3 rounded-xl border border-[var(--surface-border)]">
<div class="text-600 text-sm">Tipo de slots</div>
<div class="text-900 font-semibold mt-1">Fixo</div>
</div>
<div class="col-span-12 md:col-span-4 p-3 rounded-xl border border-[var(--surface-border)]">
<div class="text-600 text-sm">Dias ativos</div>
<div class="text-900 font-semibold mt-1">{{ resumo.diasAtivos }}</div>
</div>
<div class="col-span-12 md:col-span-4 p-3 rounded-xl border border-[var(--surface-border)]">
<div class="text-600 text-sm">Dica</div>
<div class="text-900 font-semibold mt-1">Clique em um horário para ocultar/exibir</div>
</div>
</div>
<TabView>
<TabPanel v-for="d in diasSemana" :key="d.value" :header="d.label">
<div class="grid grid-cols-12 gap-4">
<!-- Jornada -->
<div class="col-span-12">
<div class="flex items-center justify-between">
<div class="text-900 font-medium">Jornada do dia</div>
<div class="text-600 text-sm">
(Isso vem das suas "janelas semanais")
</div>
</div>
<div class="mt-2 flex flex-wrap gap-2">
<template v-if="janelasDoDia(d.value).length">
<Tag
v-for="j in janelasDoDia(d.value)"
:key="j.id"
:value="`${String(j.hora_inicio).slice(0,5)}${String(j.hora_fim).slice(0,5)}`"
/>
</template>
<template v-else>
<span class="text-600 text-sm">Sem jornada ativa neste dia.</span>
</template>
</div>
</div>
<!-- Chips -->
<div class="col-span-12">
<div class="flex items-center justify-between">
<div class="text-900 font-medium">Horários publicados</div>
<div class="text-600 text-sm" v-if="savingSlot">Salvando</div>
</div>
<div class="mt-3 flex flex-wrap gap-2">
<template v-if="slotsDoDia(d.value).length">
<button
v-for="hh in slotsDoDia(d.value)"
:key="hh"
class="px-3 py-2 rounded-lg border text-sm transition"
:class="isBloqueado(d.value, hh)
? 'border-[var(--surface-border)] text-600 bg-[var(--surface-ground)] line-through opacity-70'
: 'border-[var(--surface-border)] text-900 bg-[var(--surface-card)] hover:bg-[var(--surface-ground)]'"
@click="toggleSlot(d.value, hh)"
>
{{ hh }}
</button>
</template>
<template v-else>
<span class="text-600 text-sm">
Nada para publicar (verifique: jornada do dia + regra de slots ativa).
</span>
</template>
</div>
<div class="text-600 text-sm mt-3 leading-relaxed">
Se algum horário não deve aparecer para o paciente, clique para <b>desativar</b>.
Isso não altera sua agenda interna a disponibilidade do online.
</div>
</div>
</div>
</TabPanel>
</TabView>
</div>
</template>
</Card>
</template>