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:
@@ -15,213 +15,210 @@
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { computed, ref, watch, onMounted } from 'vue'
|
||||
import { computed, ref, watch, onMounted } from 'vue';
|
||||
|
||||
import FullCalendar from '@fullcalendar/vue3'
|
||||
import timeGridPlugin from '@fullcalendar/timegrid'
|
||||
import interactionPlugin from '@fullcalendar/interaction'
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||
import FullCalendar from '@fullcalendar/vue3';
|
||||
import timeGridPlugin from '@fullcalendar/timegrid';
|
||||
import interactionPlugin from '@fullcalendar/interaction';
|
||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import ProgressSpinner from 'primevue/progressspinner';
|
||||
|
||||
const props = defineProps({
|
||||
// UI
|
||||
view: { type: String, default: 'day' }, // 'day' | 'week'
|
||||
mode: { type: String, default: 'work_hours' }, // 'full_24h' | 'work_hours'
|
||||
// UI
|
||||
view: { type: String, default: 'day' }, // 'day' | 'week'
|
||||
mode: { type: String, default: 'work_hours' }, // 'full_24h' | 'work_hours'
|
||||
|
||||
// calendar behavior
|
||||
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' },
|
||||
businessHours: { type: [Array, Object], default: () => [] },
|
||||
// calendar behavior
|
||||
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' },
|
||||
businessHours: { type: [Array, Object], default: () => [] },
|
||||
|
||||
// data
|
||||
events: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false }
|
||||
})
|
||||
// data
|
||||
events: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false }
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'rangeChange',
|
||||
'selectTime',
|
||||
'eventClick',
|
||||
'eventDrop',
|
||||
'eventResize'
|
||||
])
|
||||
const emit = defineEmits(['rangeChange', 'selectTime', 'eventClick', 'eventDrop', 'eventResize']);
|
||||
|
||||
const fcRef = ref(null)
|
||||
const fcRef = ref(null);
|
||||
|
||||
const initialView = computed(() => (props.view === 'week' ? 'timeGridWeek' : 'timeGridDay'))
|
||||
const initialView = computed(() => (props.view === 'week' ? 'timeGridWeek' : 'timeGridDay'));
|
||||
|
||||
function getApi () {
|
||||
const inst = fcRef.value
|
||||
return inst?.getApi?.() || null
|
||||
function getApi() {
|
||||
const inst = fcRef.value;
|
||||
return inst?.getApi?.() || null;
|
||||
}
|
||||
|
||||
function emitRange () {
|
||||
const api = getApi()
|
||||
if (!api) return
|
||||
const v = api.view
|
||||
emit('rangeChange', { start: v.activeStart, end: v.activeEnd })
|
||||
function emitRange() {
|
||||
const api = getApi();
|
||||
if (!api) return;
|
||||
const v = api.view;
|
||||
emit('rangeChange', { start: v.activeStart, end: v.activeEnd });
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// Calendar options
|
||||
// -----------------------------
|
||||
const calendarOptions = computed(() => {
|
||||
const isWorkHours = props.mode !== 'full_24h'
|
||||
const isWorkHours = props.mode !== 'full_24h';
|
||||
|
||||
// No modo 24h, não recorta — mas mantemos min/max caso você queira ainda controlar
|
||||
const minTime = isWorkHours ? props.slotMinTime : '00:00:00'
|
||||
// FullCalendar timeGrid costuma aceitar 24:00:00, mas 23:59:59 evita edge-case
|
||||
const maxTime = isWorkHours ? props.slotMaxTime : '23:59:59'
|
||||
// No modo 24h, não recorta — mas mantemos min/max caso você queira ainda controlar
|
||||
const minTime = isWorkHours ? props.slotMinTime : '00:00:00';
|
||||
// FullCalendar timeGrid costuma aceitar 24:00:00, mas 23:59:59 evita edge-case
|
||||
const maxTime = isWorkHours ? props.slotMaxTime : '23:59:59';
|
||||
|
||||
return {
|
||||
plugins: [timeGridPlugin, interactionPlugin, dayGridPlugin],
|
||||
initialView: initialView.value,
|
||||
return {
|
||||
plugins: [timeGridPlugin, interactionPlugin, dayGridPlugin],
|
||||
initialView: initialView.value,
|
||||
|
||||
timeZone: props.timezone,
|
||||
timeZone: props.timezone,
|
||||
|
||||
// Header desativado (você controla no Toolbar)
|
||||
headerToolbar: false,
|
||||
// Header desativado (você controla no Toolbar)
|
||||
headerToolbar: false,
|
||||
|
||||
// Visão "produto": blocos com linhas suaves
|
||||
nowIndicator: true,
|
||||
allDaySlot: false,
|
||||
expandRows: true,
|
||||
height: 'auto',
|
||||
// Visão "produto": blocos com linhas suaves
|
||||
nowIndicator: true,
|
||||
allDaySlot: false,
|
||||
expandRows: true,
|
||||
height: 'auto',
|
||||
|
||||
// Seleção / DnD / Resize
|
||||
selectable: true,
|
||||
selectMirror: true,
|
||||
editable: true,
|
||||
eventStartEditable: true,
|
||||
eventDurationEditable: true,
|
||||
// Seleção / DnD / Resize
|
||||
selectable: true,
|
||||
selectMirror: true,
|
||||
editable: true,
|
||||
eventStartEditable: true,
|
||||
eventDurationEditable: true,
|
||||
|
||||
// Intervalos visuais
|
||||
slotDuration: props.slotDuration,
|
||||
slotMinTime: minTime,
|
||||
slotMaxTime: maxTime,
|
||||
slotLabelFormat: {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
},
|
||||
// Intervalos visuais
|
||||
slotDuration: props.slotDuration,
|
||||
slotMinTime: minTime,
|
||||
slotMaxTime: maxTime,
|
||||
slotLabelFormat: {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
},
|
||||
|
||||
// Horário "verdadeiro" de funcionamento (se você usar)
|
||||
businessHours: props.businessHours,
|
||||
// Horário "verdadeiro" de funcionamento (se você usar)
|
||||
businessHours: props.businessHours,
|
||||
|
||||
// Dados
|
||||
events: props.events,
|
||||
// Dados
|
||||
events: props.events,
|
||||
|
||||
// Melhor UX
|
||||
weekends: true,
|
||||
firstDay: 1, // segunda
|
||||
// Melhor UX
|
||||
weekends: true,
|
||||
firstDay: 1, // segunda
|
||||
|
||||
// Callbacks
|
||||
datesSet: () => {
|
||||
// dispara quando muda o intervalo exibido (prev/next/today/view)
|
||||
emitRange()
|
||||
},
|
||||
// Callbacks
|
||||
datesSet: () => {
|
||||
// dispara quando muda o intervalo exibido (prev/next/today/view)
|
||||
emitRange();
|
||||
},
|
||||
|
||||
select: (selection) => {
|
||||
// selection: { start, end, allDay, ... }
|
||||
emit('selectTime', selection)
|
||||
},
|
||||
select: (selection) => {
|
||||
// selection: { start, end, allDay, ... }
|
||||
emit('selectTime', selection);
|
||||
},
|
||||
|
||||
eventClick: (info) => emit('eventClick', info),
|
||||
eventDrop: (info) => emit('eventDrop', info),
|
||||
eventResize: (info) => emit('eventResize', info)
|
||||
}
|
||||
})
|
||||
eventClick: (info) => emit('eventClick', info),
|
||||
eventDrop: (info) => emit('eventDrop', info),
|
||||
eventResize: (info) => emit('eventResize', info)
|
||||
};
|
||||
});
|
||||
|
||||
// -----------------------------
|
||||
// Exposed methods (para Toolbar/Page)
|
||||
// -----------------------------
|
||||
function goToday () { getApi()?.today?.() }
|
||||
function prev () { getApi()?.prev?.() }
|
||||
function next () { getApi()?.next?.() }
|
||||
|
||||
function setView (v) {
|
||||
const api = getApi()
|
||||
if (!api) return
|
||||
api.changeView(v === 'week' ? 'timeGridWeek' : 'timeGridDay')
|
||||
function goToday() {
|
||||
getApi()?.today?.();
|
||||
}
|
||||
function prev() {
|
||||
getApi()?.prev?.();
|
||||
}
|
||||
function next() {
|
||||
getApi()?.next?.();
|
||||
}
|
||||
|
||||
defineExpose({ goToday, prev, next, setView })
|
||||
function setView(v) {
|
||||
const api = getApi();
|
||||
if (!api) return;
|
||||
api.changeView(v === 'week' ? 'timeGridWeek' : 'timeGridDay');
|
||||
}
|
||||
|
||||
defineExpose({ goToday, prev, next, setView });
|
||||
|
||||
// Se a prop view mudar, sincroniza
|
||||
watch(
|
||||
() => props.view,
|
||||
(v) => setView(v)
|
||||
)
|
||||
() => props.view,
|
||||
(v) => setView(v)
|
||||
);
|
||||
|
||||
// Emite o range inicial assim que montar
|
||||
onMounted(() => {
|
||||
// garante que o FullCalendar já criou a view
|
||||
setTimeout(() => emitRange(), 0)
|
||||
})
|
||||
// garante que o FullCalendar já criou a view
|
||||
setTimeout(() => emitRange(), 0);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="agenda-calendar-wrap">
|
||||
<div v-if="loading" class="agenda-calendar-loading">
|
||||
<div class="flex flex-col gap-2 w-full px-2 py-2">
|
||||
<Skeleton height="2rem" class="mb-1" />
|
||||
<div class="grid grid-cols-7 gap-1">
|
||||
<Skeleton v-for="n in 7" :key="n" height="1.25rem" />
|
||||
<div class="agenda-calendar-wrap">
|
||||
<div v-if="loading" class="agenda-calendar-loading">
|
||||
<div class="flex flex-col gap-2 w-full px-2 py-2">
|
||||
<Skeleton height="2rem" class="mb-1" />
|
||||
<div class="grid grid-cols-7 gap-1">
|
||||
<Skeleton v-for="n in 7" :key="n" height="1.25rem" />
|
||||
</div>
|
||||
<Skeleton v-for="n in 8" :key="'row' + n" height="3rem" />
|
||||
</div>
|
||||
</div>
|
||||
<Skeleton v-for="n in 8" :key="'row' + n" height="3rem" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FullCalendar
|
||||
ref="fcRef"
|
||||
:options="calendarOptions"
|
||||
/>
|
||||
</div>
|
||||
<FullCalendar ref="fcRef" :options="calendarOptions" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.agenda-calendar-wrap{
|
||||
position: relative;
|
||||
width: 100%;
|
||||
.agenda-calendar-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.agenda-calendar-loading{
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(3px);
|
||||
background: color-mix(in srgb, var(--surface-card) 85%, transparent);
|
||||
border-radius: 16px;
|
||||
.agenda-calendar-loading {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(3px);
|
||||
background: color-mix(in srgb, var(--surface-card) 85%, transparent);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
/* Deixa o calendário "respirar" dentro de cards/layouts */
|
||||
:deep(.fc){
|
||||
font-size: 0.95rem;
|
||||
:deep(.fc) {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
:deep(.fc .fc-timegrid-slot){
|
||||
height: 2.2rem;
|
||||
:deep(.fc .fc-timegrid-slot) {
|
||||
height: 2.2rem;
|
||||
}
|
||||
|
||||
:deep(.fc .fc-timegrid-now-indicator-line){
|
||||
opacity: .75;
|
||||
:deep(.fc .fc-timegrid-now-indicator-line) {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
:deep(.fc .fc-scrollgrid){
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--surface-border);
|
||||
:deep(.fc .fc-scrollgrid) {
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--surface-border);
|
||||
}
|
||||
|
||||
:deep(.fc .fc-timegrid-axis-cushion),
|
||||
:deep(.fc .fc-timegrid-slot-label-cushion){
|
||||
color: var(--text-color-secondary);
|
||||
:deep(.fc .fc-timegrid-slot-label-cushion) {
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user