first commit

This commit is contained in:
Leonardo
2026-02-18 22:36:45 -03:00
parent ec6b6ef53a
commit 676042268b
122 changed files with 26354 additions and 1615 deletions

View File

@@ -0,0 +1,220 @@
<!-- src/components/agenda/AgendaOnlineGradeCard.vue -->
<script setup>
import { computed, onMounted, ref } from 'vue'
import TabView from 'primevue/tabview'
import TabPanel from 'primevue/tabpanel'
import Tag from 'primevue/tag'
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>