Ajuste em Massa - Paciente, Terapeuta, Clinica e Admin - Inicio agenda
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
<script setup>
|
||||
defineProps({})
|
||||
defineEmits([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4 rounded-xl border border-[var(--surface-border)] bg-[var(--surface-card)]">
|
||||
<b>AgendaCalendar (placeholder)</b>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,100 @@
|
||||
<!-- src/features/agenda/components/AgendaClinicCalendar.vue -->
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import FullCalendar from '@fullcalendar/vue3'
|
||||
import resourceTimeGridPlugin from '@fullcalendar/resource-timegrid'
|
||||
import interactionPlugin from '@fullcalendar/interaction'
|
||||
|
||||
const props = defineProps({
|
||||
view: { type: String, default: 'day' }, // 'day' | 'week'
|
||||
timezone: { type: String, default: 'America/Sao_Paulo' },
|
||||
|
||||
mode: { type: String, default: 'work_hours' }, // 'full_24h' | 'work_hours'
|
||||
slotDuration: { type: String, default: '00:30:00' },
|
||||
slotMinTime: { type: String, default: '06:00:00' },
|
||||
slotMaxTime: { type: String, default: '22:00:00' },
|
||||
|
||||
resources: { type: Array, default: () => [] }, // [{ id, title }]
|
||||
events: { type: Array, default: () => [] }, // event.resourceId = resource.id
|
||||
|
||||
loading: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'rangeChange',
|
||||
'eventClick',
|
||||
'eventDrop',
|
||||
'eventResize'
|
||||
])
|
||||
|
||||
const calendarRef = ref(null)
|
||||
|
||||
const initialView = computed(() => (props.view === 'week' ? 'resourceTimeGridWeek' : 'resourceTimeGridDay'))
|
||||
|
||||
const computedSlotMinTime = computed(() => (props.mode === 'full_24h' ? '00:00:00' : props.slotMinTime))
|
||||
const computedSlotMaxTime = computed(() => (props.mode === 'full_24h' ? '24:00:00' : props.slotMaxTime))
|
||||
|
||||
const options = computed(() => ({
|
||||
plugins: [resourceTimeGridPlugin, interactionPlugin],
|
||||
initialView: initialView.value,
|
||||
timeZone: props.timezone,
|
||||
|
||||
headerToolbar: false,
|
||||
|
||||
nowIndicator: true,
|
||||
editable: true,
|
||||
|
||||
slotDuration: props.slotDuration,
|
||||
slotMinTime: computedSlotMinTime.value,
|
||||
slotMaxTime: computedSlotMaxTime.value,
|
||||
|
||||
resourceAreaWidth: '280px',
|
||||
resourceAreaHeaderContent: 'Profissionais',
|
||||
resources: props.resources,
|
||||
|
||||
events: props.events,
|
||||
|
||||
datesSet(arg) {
|
||||
emit('rangeChange', {
|
||||
start: arg.start,
|
||||
end: arg.end,
|
||||
startStr: arg.startStr,
|
||||
endStr: arg.endStr,
|
||||
viewType: arg.view.type
|
||||
})
|
||||
},
|
||||
|
||||
eventClick(info) { emit('eventClick', info) },
|
||||
eventDrop(info) { emit('eventDrop', info) },
|
||||
eventResize(info) { emit('eventResize', info) },
|
||||
|
||||
height: 'auto',
|
||||
expandRows: true,
|
||||
allDaySlot: false
|
||||
}))
|
||||
|
||||
function api () {
|
||||
const fc = calendarRef.value
|
||||
return fc?.getApi?.()
|
||||
}
|
||||
|
||||
function goToday () { api()?.today() }
|
||||
function prev () { api()?.prev() }
|
||||
function next () { api()?.next() }
|
||||
function setView (v) { api()?.changeView(v === 'week' ? 'resourceTimeGridWeek' : 'resourceTimeGridDay') }
|
||||
|
||||
defineExpose({ goToday, prev, next, setView })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-[1.5rem] border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden">
|
||||
<div v-if="loading" class="p-4 text-sm opacity-70 border-b border-[var(--surface-border)]">
|
||||
Carregando agenda da clínica…
|
||||
</div>
|
||||
|
||||
<div class="p-2 md:p-3">
|
||||
<FullCalendar ref="calendarRef" :options="options" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,192 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch, nextTick } from 'vue'
|
||||
|
||||
import FullCalendar from '@fullcalendar/vue3'
|
||||
import timeGridPlugin from '@fullcalendar/timegrid'
|
||||
import interactionPlugin from '@fullcalendar/interaction'
|
||||
|
||||
const props = defineProps({
|
||||
view: { type: String, default: 'day' }, // 'day' | 'week'
|
||||
mode: { type: String, default: 'work_hours' }, // 'full_24h' | 'work_hours'
|
||||
timezone: { type: String, default: 'America/Sao_Paulo' },
|
||||
|
||||
slotDuration: { type: String, default: '00:30:00' },
|
||||
slotMinTime: { type: String, default: '06:00:00' },
|
||||
slotMaxTime: { type: String, default: '22:00:00' },
|
||||
|
||||
// [{ id, title }]
|
||||
staff: { type: Array, default: () => [] },
|
||||
|
||||
// todos os eventos (com extendedProps.owner_id)
|
||||
events: { type: Array, default: () => [] },
|
||||
|
||||
loading: { type: Boolean, default: false },
|
||||
|
||||
// controla quantas colunas "visíveis" por vez (resto vai por scroll horizontal)
|
||||
minColWidth: { type: Number, default: 360 }
|
||||
})
|
||||
|
||||
// ✅ rangeChange = mudança de range (carregar eventos)
|
||||
// ✅ slotSelect = seleção de intervalo em uma coluna específica (criar evento)
|
||||
// ✅ eventClick/Drop/Resize = ações em evento
|
||||
const emit = defineEmits(['rangeChange', 'slotSelect', 'eventClick', 'eventDrop', 'eventResize'])
|
||||
|
||||
const calendarRefs = ref([])
|
||||
|
||||
function setCalendarRef (el, idx) {
|
||||
if (!el) return
|
||||
calendarRefs.value[idx] = el
|
||||
}
|
||||
|
||||
const initialView = computed(() => (props.view === 'week' ? 'timeGridWeek' : 'timeGridDay'))
|
||||
|
||||
const computedSlotMinTime = computed(() => (props.mode === 'full_24h' ? '00:00:00' : props.slotMinTime))
|
||||
// ✅ 23:59:59 para evitar edge-case de 24:00:00
|
||||
const computedSlotMaxTime = computed(() => (props.mode === 'full_24h' ? '23:59:59' : props.slotMaxTime))
|
||||
|
||||
function apiAt (idx) {
|
||||
const fc = calendarRefs.value[idx]
|
||||
return fc?.getApi?.()
|
||||
}
|
||||
|
||||
function forEachApi (fn) {
|
||||
for (let i = 0; i < calendarRefs.value.length; i++) {
|
||||
const api = apiAt(i)
|
||||
if (api) fn(api, i)
|
||||
}
|
||||
}
|
||||
|
||||
function goToday () { forEachApi(api => api.today()) }
|
||||
function prev () { forEachApi(api => api.prev()) }
|
||||
function next () { forEachApi(api => api.next()) }
|
||||
|
||||
function setView (v) {
|
||||
const target = v === 'week' ? 'timeGridWeek' : 'timeGridDay'
|
||||
forEachApi(api => api.changeView(target))
|
||||
}
|
||||
|
||||
defineExpose({ goToday, prev, next, setView })
|
||||
|
||||
// Eventos por profissional (owner)
|
||||
function eventsFor (ownerId) {
|
||||
const list = props.events || []
|
||||
return list.filter(e => e?.extendedProps?.owner_id === ownerId)
|
||||
}
|
||||
|
||||
// ---- range sync ----
|
||||
let lastRangeKey = ''
|
||||
let suppressSync = false
|
||||
|
||||
function onDatesSet (arg) {
|
||||
const key = `${arg.startStr}__${arg.endStr}__${arg.view?.type || ''}`
|
||||
if (key === lastRangeKey) return
|
||||
lastRangeKey = key
|
||||
|
||||
// dispara carregamento no pai
|
||||
emit('rangeChange', {
|
||||
start: arg.start,
|
||||
end: arg.end,
|
||||
startStr: arg.startStr,
|
||||
endStr: arg.endStr,
|
||||
viewType: arg.view.type
|
||||
})
|
||||
|
||||
// mantém todos os calendários na mesma data
|
||||
if (suppressSync) return
|
||||
suppressSync = true
|
||||
|
||||
const masterDate = arg.start
|
||||
forEachApi((api) => {
|
||||
const cur = api.view?.currentStart
|
||||
if (!cur) return
|
||||
if (cur.getTime() !== masterDate.getTime()) api.gotoDate(masterDate)
|
||||
})
|
||||
|
||||
// libera no próximo tick (evita loops)
|
||||
Promise.resolve().then(() => { suppressSync = false })
|
||||
}
|
||||
|
||||
// Se trocar view, garante que todos estão no mesmo
|
||||
watch(() => props.view, async () => {
|
||||
await nextTick()
|
||||
setView(props.view)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-[1.5rem] border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden">
|
||||
<div v-if="loading" class="p-4 text-sm opacity-70 border-b border-[var(--surface-border)]">
|
||||
Carregando agenda da clínica…
|
||||
</div>
|
||||
|
||||
<!-- Mosaic -->
|
||||
<div
|
||||
class="p-2 md:p-3 overflow-x-auto"
|
||||
:style="{ display: 'grid', gridAutoFlow: 'column', gridAutoColumns: `minmax(${minColWidth}px, 1fr)`, gap: '12px' }"
|
||||
>
|
||||
<div
|
||||
v-for="(p, idx) in staff"
|
||||
:key="p.id"
|
||||
class="rounded-[1.25rem] border border-[var(--surface-border)] bg-[color-mix(in_srgb,var(--surface-card),transparent_12%)] overflow-hidden"
|
||||
>
|
||||
<!-- Header da coluna -->
|
||||
<div class="p-3 border-b border-[var(--surface-border)] flex items-center justify-between gap-2">
|
||||
<div class="min-w-0">
|
||||
<div class="font-semibold truncate">{{ p.title }}</div>
|
||||
<div class="text-xs opacity-70 truncate">Visão diária operacional</div>
|
||||
</div>
|
||||
<div class="text-xs opacity-70 whitespace-nowrap">
|
||||
{{ mode === 'full_24h' ? '24h' : 'Horário' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-2">
|
||||
<FullCalendar
|
||||
:ref="(el) => setCalendarRef(el, idx)"
|
||||
:options="{
|
||||
plugins: [timeGridPlugin, interactionPlugin],
|
||||
initialView: initialView,
|
||||
timeZone: timezone,
|
||||
|
||||
headerToolbar: false,
|
||||
nowIndicator: true,
|
||||
|
||||
editable: true,
|
||||
|
||||
// ✅ seleção para criar evento (por coluna)
|
||||
selectable: true,
|
||||
selectMirror: true,
|
||||
select: (selection) => {
|
||||
emit('slotSelect', {
|
||||
ownerId: p.id,
|
||||
start: selection.start,
|
||||
end: selection.end,
|
||||
startStr: selection.startStr,
|
||||
endStr: selection.endStr,
|
||||
jsEvent: selection.jsEvent || null,
|
||||
viewType: selection.view?.type || initialView
|
||||
})
|
||||
},
|
||||
|
||||
slotDuration: slotDuration,
|
||||
slotMinTime: computedSlotMinTime,
|
||||
slotMaxTime: computedSlotMaxTime,
|
||||
|
||||
height: 'auto',
|
||||
expandRows: true,
|
||||
allDaySlot: false,
|
||||
|
||||
events: eventsFor(p.id),
|
||||
|
||||
datesSet: onDatesSet,
|
||||
|
||||
eventClick: (info) => emit('eventClick', info),
|
||||
eventDrop: (info) => emit('eventDrop', info),
|
||||
eventResize: (info) => emit('eventResize', info)
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,243 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import Dialog from 'primevue/dialog'
|
||||
import Button from 'primevue/button'
|
||||
import Dropdown from 'primevue/dropdown'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Textarea from 'primevue/textarea'
|
||||
import FloatLabel from 'primevue/floatlabel'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
|
||||
// Para editar
|
||||
eventRow: { type: Object, default: null },
|
||||
|
||||
// Para criar via seleção no calendário
|
||||
initialStartISO: { type: String, default: '' },
|
||||
initialEndISO: { type: String, default: '' },
|
||||
|
||||
// Quem é o dono da agenda (owner_id)
|
||||
ownerId: { type: String, default: '' },
|
||||
|
||||
// Se estiver criando na visão clínica e quiser atribuir a um owner específico
|
||||
allowOwnerEdit: { type: Boolean, default: false },
|
||||
ownerOptions: { type: Array, default: () => [] } // [{ label, value }]
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'save', 'delete'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emit('update:modelValue', v)
|
||||
})
|
||||
|
||||
const tipoOptions = [
|
||||
{ label: 'Sessão', value: 'sessao' },
|
||||
{ label: 'Bloqueio', value: 'bloqueio' },
|
||||
{ label: 'Pessoal', value: 'pessoal' },
|
||||
{ label: 'Clínica', value: 'clinica' }
|
||||
]
|
||||
|
||||
const statusOptions = [
|
||||
{ label: 'Agendado', value: 'agendado' },
|
||||
{ label: 'Realizado', value: 'realizado' },
|
||||
{ label: 'Faltou', value: 'faltou' },
|
||||
{ label: 'Cancelado', value: 'cancelado' }
|
||||
]
|
||||
|
||||
const form = ref(resetForm())
|
||||
|
||||
watch(
|
||||
() => [props.eventRow, props.initialStartISO, props.initialEndISO, props.ownerId],
|
||||
() => {
|
||||
form.value = resetForm()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function resetForm () {
|
||||
const r = props.eventRow
|
||||
|
||||
// ISO strings (timestamptz)
|
||||
const startISO = r?.inicio_em || props.initialStartISO || ''
|
||||
const endISO = r?.fim_em || props.initialEndISO || ''
|
||||
|
||||
return {
|
||||
id: r?.id || null,
|
||||
owner_id: r?.owner_id || props.ownerId || '',
|
||||
terapeuta_id: r?.terapeuta_id ?? null,
|
||||
paciente_id: r?.paciente_id ?? null,
|
||||
|
||||
tipo: r?.tipo || 'sessao',
|
||||
status: r?.status || 'agendado',
|
||||
titulo: r?.titulo || '',
|
||||
observacoes: r?.observacoes || '',
|
||||
|
||||
inicio_em: startISO,
|
||||
fim_em: endISO,
|
||||
|
||||
// ajuda de UX (minutos) caso você queira editar duração fácil
|
||||
duracaoMin: calcMinutes(startISO, endISO) || 50
|
||||
}
|
||||
}
|
||||
|
||||
function calcMinutes (a, b) {
|
||||
try {
|
||||
if (!a || !b) return null
|
||||
const ms = new Date(b).getTime() - new Date(a).getTime()
|
||||
return Math.max(0, Math.round(ms / 60000))
|
||||
} catch { return null }
|
||||
}
|
||||
|
||||
function addMinutesISO (iso, min) {
|
||||
const d = new Date(iso)
|
||||
d.setMinutes(d.getMinutes() + Number(min || 0))
|
||||
return d.toISOString()
|
||||
}
|
||||
|
||||
const isEdit = computed(() => !!form.value.id)
|
||||
|
||||
const canSave = computed(() => {
|
||||
if (!form.value.owner_id) return false
|
||||
if (!form.value.inicio_em) return false
|
||||
if (!form.value.fim_em) return false
|
||||
const a = new Date(form.value.inicio_em).getTime()
|
||||
const b = new Date(form.value.fim_em).getTime()
|
||||
return b > a
|
||||
})
|
||||
|
||||
function applyDuration () {
|
||||
if (!form.value.inicio_em) return
|
||||
form.value.fim_em = addMinutesISO(form.value.inicio_em, form.value.duracaoMin || 50)
|
||||
}
|
||||
|
||||
function onSave () {
|
||||
if (!canSave.value) return
|
||||
|
||||
const payload = {
|
||||
owner_id: form.value.owner_id,
|
||||
terapeuta_id: form.value.terapeuta_id,
|
||||
paciente_id: form.value.paciente_id,
|
||||
|
||||
tipo: form.value.tipo,
|
||||
status: form.value.status,
|
||||
titulo: form.value.titulo || null,
|
||||
observacoes: form.value.observacoes || null,
|
||||
|
||||
inicio_em: form.value.inicio_em,
|
||||
fim_em: form.value.fim_em
|
||||
}
|
||||
|
||||
emit('save', { id: form.value.id, payload })
|
||||
}
|
||||
|
||||
function onDelete () {
|
||||
if (!form.value.id) return
|
||||
emit('delete', form.value.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:visible="visible" modal :style="{ width: '720px', maxWidth: '95vw' }" :header="isEdit ? 'Editar evento' : 'Novo evento'">
|
||||
<div class="grid gap-4">
|
||||
<div class="grid md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<FloatLabel>
|
||||
<Dropdown
|
||||
id="tipo"
|
||||
class="w-full"
|
||||
:options="tipoOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
v-model="form.tipo"
|
||||
/>
|
||||
<label for="tipo">Tipo</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FloatLabel>
|
||||
<Dropdown
|
||||
id="status"
|
||||
class="w-full"
|
||||
:options="statusOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
v-model="form.status"
|
||||
/>
|
||||
<label for="status">Status</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div v-if="allowOwnerEdit">
|
||||
<FloatLabel>
|
||||
<Dropdown
|
||||
id="owner"
|
||||
class="w-full"
|
||||
:options="ownerOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
v-model="form.owner_id"
|
||||
/>
|
||||
<label for="owner">Profissional</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FloatLabel>
|
||||
<InputText id="titulo" class="w-full" v-model="form.titulo" placeholder=" " />
|
||||
<label for="titulo">Título</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<FloatLabel>
|
||||
<InputText id="inicio" class="w-full" v-model="form.inicio_em" placeholder=" " />
|
||||
<label for="inicio">Início (ISO)</label>
|
||||
</FloatLabel>
|
||||
<div class="text-xs opacity-70 mt-1">Por enquanto em ISO. Depois trocamos para DatePicker bonito.</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FloatLabel>
|
||||
<InputText id="fim" class="w-full" v-model="form.fim_em" placeholder=" " />
|
||||
<label for="fim">Fim (ISO)</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-3 items-end">
|
||||
<div>
|
||||
<FloatLabel>
|
||||
<InputNumber id="dur" class="w-full" v-model="form.duracaoMin" :min="5" :max="480" />
|
||||
<label for="dur">Duração (min)</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button label="Aplicar duração" severity="secondary" outlined icon="pi pi-clock" @click="applyDuration" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FloatLabel>
|
||||
<Textarea id="obs" class="w-full" autoResize rows="3" v-model="form.observacoes" placeholder=" " />
|
||||
<label for="obs">Observações</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center pt-2">
|
||||
<Button v-if="isEdit" label="Excluir" icon="pi pi-trash" severity="danger" outlined @click="onDelete" />
|
||||
|
||||
<div class="flex gap-2 ml-auto">
|
||||
<Button label="Cancelar" severity="secondary" outlined @click="visible = false" />
|
||||
<Button label="Salvar" icon="pi pi-check" :disabled="!canSave" @click="onSave" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup>
|
||||
defineProps({})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4 rounded-xl border border-[var(--surface-border)] bg-[var(--surface-card)]">
|
||||
<b>AgendaRightPanel (placeholder)</b>
|
||||
<div class="mt-3">
|
||||
<slot name="top" />
|
||||
<slot name="bottom" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,115 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import Button from 'primevue/button'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import ToggleButton from 'primevue/togglebutton'
|
||||
import FloatLabel from 'primevue/floatlabel'
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import InputText from 'primevue/inputtext'
|
||||
|
||||
const props = defineProps({
|
||||
title: { type: String, default: 'Agenda' },
|
||||
|
||||
// 'day' | 'week'
|
||||
view: { type: String, default: 'day' },
|
||||
|
||||
// 'full_24h' | 'work_hours'
|
||||
mode: { type: String, default: 'work_hours' },
|
||||
|
||||
showSearch: { type: Boolean, default: true },
|
||||
searchPlaceholder: { type: String, default: ' ' },
|
||||
|
||||
// controla se exibe botões de ação
|
||||
showActions: { type: Boolean, default: true }
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'today',
|
||||
'prev',
|
||||
'next',
|
||||
'changeView',
|
||||
'toggleMode',
|
||||
'createSession',
|
||||
'createBlock',
|
||||
'search'
|
||||
])
|
||||
|
||||
const viewOptions = [
|
||||
{ label: 'Dia', value: 'day' },
|
||||
{ label: 'Semana', value: 'week' }
|
||||
]
|
||||
|
||||
const search = ref('')
|
||||
|
||||
watch(search, (v) => emit('search', v))
|
||||
|
||||
const modeLabel = computed(() => (props.mode === 'full_24h' ? '24h' : 'Horário'))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-4 overflow-hidden rounded-[1.5rem] border border-[var(--surface-border)] bg-[var(--surface-card)]">
|
||||
<div class="p-4 md:p-5 flex flex-col gap-3">
|
||||
<!-- topo -->
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<div class="text-lg md:text-xl font-semibold truncate">{{ title }}</div>
|
||||
<div class="text-sm opacity-70">Operação do dia com visão e ação.</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Button label="Hoje" icon="pi pi-calendar" severity="secondary" outlined @click="$emit('today')" />
|
||||
<Button icon="pi pi-chevron-left" severity="secondary" outlined @click="$emit('prev')" />
|
||||
<Button icon="pi pi-chevron-right" severity="secondary" outlined @click="$emit('next')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- controles -->
|
||||
<div class="flex flex-col md:flex-row md:items-center gap-3 md:justify-between">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<SelectButton
|
||||
:modelValue="view"
|
||||
:options="viewOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
@update:modelValue="$emit('changeView', $event)"
|
||||
/>
|
||||
|
||||
<ToggleButton
|
||||
:modelValue="mode === 'full_24h'"
|
||||
onLabel="24h"
|
||||
offLabel="Horário"
|
||||
@update:modelValue="$emit('toggleMode', $event ? 'full_24h' : 'work_hours')"
|
||||
/>
|
||||
|
||||
<div class="text-sm opacity-70">
|
||||
Modo: <b>{{ modeLabel }}</b>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row items-stretch md:items-center gap-2">
|
||||
<template v-if="showActions">
|
||||
<Button label="Nova sessão" icon="pi pi-plus" @click="$emit('createSession')" />
|
||||
<Button label="Bloquear" icon="pi pi-lock" severity="secondary" outlined @click="$emit('createBlock')" />
|
||||
</template>
|
||||
|
||||
<div v-if="showSearch" class="md:w-72 w-full">
|
||||
<FloatLabel>
|
||||
<IconField>
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputText
|
||||
id="agendaSearch"
|
||||
class="w-full"
|
||||
v-model="search"
|
||||
:placeholder="searchPlaceholder"
|
||||
/>
|
||||
</IconField>
|
||||
<label for="agendaSearch">Buscar por paciente/título</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
items: { type: Array, default: () => [] }
|
||||
})
|
||||
defineEmits(['open', 'confirm', 'reschedule'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-3 border rounded-lg">
|
||||
<b>AgendaNextSessionsCardList (placeholder)</b>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
stats: { type: Object, default: () => ({}) }
|
||||
})
|
||||
defineEmits(['quickBlock', 'quickCreate'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-3 border rounded-lg">
|
||||
<b>AgendaPulseCardGrid (placeholder)</b>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user