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
+141 -144
View File
@@ -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>