v-sistec-features 1.31.12 → 1.31.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,8 @@
2
2
  ========
3
3
  Este arquivo representa o componente de calendário visual utilizando a biblioteca vue-cal.
4
4
  Ele exibe e gerencia os eventos e reservas na interface, permitindo a criação de novos agendamentos,
5
- a abertura do modal de detalhes ao clicar em reservas existentes e a navegação entre datas.
5
+ a abertura do modal de detalhes ao clicar em reservas existentes, a navegação entre datas e a exposição
6
+ da lista contendo o recurso atualmente em exibição.
6
7
  ========
7
8
  -->
8
9
  <script setup lang="ts">
@@ -10,6 +11,7 @@ import { ref, computed, inject } from 'vue';
10
11
  import { VueCal } from 'vue-cal';
11
12
  import 'vue-cal/style.css';
12
13
  import { useAgenda } from '../composables/useAgenda';
14
+ import { extrairDataHoraLocal, formatarDataIso } from '../composables/useGradeCompartilhada';
13
15
  import ModalReserva from './ModalReserva.vue';
14
16
  import ModalDetalhesReserva from './ModalDetalhesReserva.vue';
15
17
 
@@ -22,10 +24,11 @@ const props = defineProps({
22
24
  ocultarTituloModal: { type: Boolean, default: false }
23
25
  });
24
26
 
25
- const emit = defineEmits(['before-save']);
27
+ defineEmits(['before-save']);
26
28
 
27
29
  const {
28
30
  reservasBrutas,
31
+ recursosDisponiveis,
29
32
  recursoSelecionado,
30
33
  dataSelecionada,
31
34
  dataVisao,
@@ -36,38 +39,34 @@ const {
36
39
  buscarReservas
37
40
  } = useAgenda(api);
38
41
 
39
- // Armazena a referência direta à instância do componente vue-cal.
40
42
  const calendarioRef = ref<any>(null);
41
-
42
- // Controla o estado de visibilidade da janela modal de criação de reservas.
43
43
  const modalAberto = ref(false);
44
-
45
- // Armazena a função de resolução interna (Promise) fornecida pelo vue-cal durante a criação de um evento.
46
44
  const resolverCriacaoVueCal = ref<any>(null);
47
-
48
- // Armazena os dados processados resultantes de um clique na grade para alimentar os campos do modal de criação.
49
45
  const dadosParaModal = ref({});
50
-
51
- // Controla a visibilidade do modal de detalhes de uma reserva já existente.
52
46
  const modalDetalhesAberto = ref(false);
53
-
54
- // Armazena o objeto bruto da reserva clicada no calendário para exibição detalhada.
55
47
  const reservaSelecionadaDetalhes = ref<any>(null);
56
48
 
49
+ const recursosEmExibicao = computed(() => {
50
+ if (!recursoSelecionado.value) return [];
51
+ const encontrado = recursosDisponiveis.value.find(
52
+ (r: any) => Number(r.id_recurso) === Number(recursoSelecionado.value)
53
+ );
54
+ return encontrado ? [encontrado] : [];
55
+ });
56
+
57
57
  defineExpose({
58
+ recursosEmExibicao,
58
59
  irParaHoje: () => calendarioRef.value?.view?.goToToday(),
59
60
  periodoAnterior: () => calendarioRef.value?.view?.previous(),
60
61
  periodoProximo: () => calendarioRef.value?.view?.next(),
61
62
  mudarVisao: (v: string) => { visaoAtiva.value = v; calendarioRef.value?.view?.switch(v); }
62
63
  });
63
64
 
64
- // Recebe um objeto de evento opcional e não devolve retorno.
65
- // É usada para deduzir o período atualmente em exibição no calendário e buscar as reservas correspondentes na API.
66
65
  const atualizarVisualizacao = (evento: any = null) => {
67
66
  let dataInicio: Date;
68
67
  let dataFim: Date;
69
68
 
70
- if (evento && evento.startDate && evento.endDate) {
69
+ if (evento?.startDate && evento?.endDate) {
71
70
  dataInicio = new Date(evento.startDate);
72
71
  dataFim = new Date(evento.endDate);
73
72
  } else {
@@ -84,35 +83,9 @@ const atualizarVisualizacao = (evento: any = null) => {
84
83
  }
85
84
  }
86
85
 
87
- const formatar = (d: Date) => {
88
- const ano = d.getFullYear();
89
- const mes = String(d.getMonth() + 1).padStart(2, '0');
90
- const dia = String(d.getDate()).padStart(2, '0');
91
- return `${ano}-${mes}-${dia}`;
92
- };
93
-
94
- buscarReservas(`${formatar(dataInicio)}T00:00:00`, `${formatar(dataFim)}T23:59:59`);
95
- };
96
-
97
- // Recebe um objeto Date nativo e devolve uma string estruturada no formato YYYY-MM-DD.
98
- // É usada para adequar instâncias de data ao formato exigido pelos inputs tipo "date" do HTML5.
99
- const formatarDataInput = (d: Date) => {
100
- const ano = d.getFullYear();
101
- const mes = String(d.getMonth() + 1).padStart(2, '0');
102
- const dia = String(d.getDate()).padStart(2, '0');
103
- return `${ano}-${mes}-${dia}`;
104
- };
105
-
106
- // Recebe um objeto Date nativo e devolve uma string estruturada no formato HH:mm.
107
- // É usada para adequar instâncias de data ao formato exigido pelos inputs tipo "time" do HTML5.
108
- const formatarHoraInput = (d: Date) => {
109
- const horas = String(d.getHours()).padStart(2, '0');
110
- const minutos = String(d.getMinutes()).padStart(2, '0');
111
- return `${horas}:${minutos}`;
86
+ buscarReservas(`${formatarDataIso(dataInicio)}T00:00:00`, `${formatarDataIso(dataFim)}T23:59:59`);
112
87
  };
113
88
 
114
- // Recebe um objeto contendo os detalhes gerados pelo vue-cal e a função finalizadora da promessa.
115
- // É usada para interceptar a criação de evento na grade e abrir o modal com os horários preenchidos.
116
89
  const aoCriarEvento = ({ event, resolve }: any) => {
117
90
  resolverCriacaoVueCal.value = resolve;
118
91
 
@@ -122,20 +95,21 @@ const aoCriarEvento = ({ event, resolve }: any) => {
122
95
  if (isNaN(start.getTime())) start = new Date();
123
96
  if (isNaN(end.getTime())) end = new Date();
124
97
 
98
+ const inicio = extrairDataHoraLocal(start.toISOString());
99
+ const fim = extrairDataHoraLocal(end.toISOString());
100
+
125
101
  dadosParaModal.value = {
126
102
  id_recurso: recursoSelecionado.value,
127
103
  titulo_reserva: '',
128
- data_inicio: formatarDataInput(start),
129
- hora_inicio: formatarHoraInput(start),
130
- data_fim: formatarDataInput(end),
131
- hora_fim: formatarHoraInput(end)
104
+ data_inicio: inicio.data,
105
+ hora_inicio: inicio.hora,
106
+ data_fim: fim.data,
107
+ hora_fim: fim.hora
132
108
  };
133
109
 
134
110
  modalAberto.value = true;
135
111
  };
136
112
 
137
- // Recebe o objeto de evento disparado pelo clique em uma reserva no vue-cal e não devolve retorno.
138
- // É usada para localizar a reserva correspondente em reservasBrutas e abrir o modal de detalhes.
139
113
  const aoClicarEvento = (eventoVueCal: any) => {
140
114
  const idClicado = eventoVueCal?.id || eventoVueCal?.event?.id;
141
115
  if (!idClicado) return;
@@ -150,88 +124,56 @@ const aoClicarEvento = (eventoVueCal: any) => {
150
124
  }
151
125
  };
152
126
 
153
- // Não recebe parâmetros e não devolve retorno.
154
- // É usada para encerrar a promessa pendente do vue-cal e atualizar a grade após salvar uma nova reserva.
155
- const aoSalvar = () => {
127
+ const encerrarCriacaoProvisoria = () => {
156
128
  if (resolverCriacaoVueCal.value) {
157
129
  resolverCriacaoVueCal.value(false);
158
130
  resolverCriacaoVueCal.value = null;
159
131
  }
160
- atualizarVisualizacao();
161
132
  };
162
133
 
163
- // Não recebe parâmetros e não devolve retorno.
164
- // É usada para descartar o bloco provisório desenhado no calendário quando o usuário cancela o modal.
165
- const aoCancelar = () => {
166
- if (resolverCriacaoVueCal.value) {
167
- resolverCriacaoVueCal.value(false);
168
- resolverCriacaoVueCal.value = null;
169
- }
134
+ const aoSalvar = () => {
135
+ encerrarCriacaoProvisoria();
136
+ atualizarVisualizacao();
170
137
  };
171
138
 
172
139
  type FormatoDataVueCal = `${number}${number}${number}${number}-${number}${number}-${number}${number} ${number}${number}:${number}${number}`;
173
140
 
174
- // Recebe a lista de reservas padronizadas da API e devolve a coleção formatada para o componente vue-cal.
175
- // É usada para converter os horários UTC para o fuso local do navegador e definir o estilo visual conforme a autoria.
176
- const mapearEventosVisuais = (reservas: any[]) => {
177
- const formatarDataLocal = (dataIsoUtc: string): FormatoDataVueCal => {
178
- const d = new Date(dataIsoUtc);
179
- const ano = d.getFullYear();
180
- const mes = String(d.getMonth() + 1).padStart(2, '0');
181
- const dia = String(d.getDate()).padStart(2, '0');
182
- const horas = String(d.getHours()).padStart(2, '0');
183
- const minutos = String(d.getMinutes()).padStart(2, '0');
184
- return `${ano}-${mes}-${dia} ${horas}:${minutos}` as FormatoDataVueCal;
185
- };
186
-
187
- return reservas.map(reserva => {
188
- const identificadorAutor = reserva.email_responsavel_reserva || reserva.usuario_responsavel_reserva;
189
- const ehMeu = Boolean(
190
- identificadorAutor &&
191
- props.usuarioLogado &&
192
- String(identificadorAutor).toLowerCase() === props.usuarioLogado.toLowerCase()
193
- );
194
- const tituloLimpo = reserva.titulo_reserva ? String(reserva.titulo_reserva).trim() : '';
195
-
196
- return {
197
- id: reserva.codigo_referencia_reserva,
198
- title: tituloLimpo || props.tituloPadrao,
199
- start: formatarDataLocal(reserva.data_inicio_reserva),
200
- end: formatarDataLocal(reserva.data_fim_reserva),
201
- id_recurso: reserva.id_recurso,
202
- draggable: ehMeu,
203
- resizable: ehMeu,
204
- deletable: ehMeu,
205
- class: ehMeu ? 'reserva-minha' : 'reserva-bloqueada'
206
- };
207
- });
208
- };
209
-
210
- // Armazena a lista reativa de reservas filtradas exclusivamente para o recurso selecionado no momento.
211
141
  const eventosProcessados = computed(() => {
212
142
  if (!recursoSelecionado.value) return [];
213
143
 
214
- return mapearEventosVisuais(reservasBrutas.value).filter(evento =>
215
- Number(evento.id_recurso) === Number(recursoSelecionado.value)
216
- );
144
+ return reservasBrutas.value
145
+ .filter(reserva => Number(reserva.id_recurso) === Number(recursoSelecionado.value))
146
+ .map(reserva => {
147
+ const identificadorAutor = reserva.email_responsavel_reserva || reserva.usuario_responsavel_reserva;
148
+ const ehMeu = Boolean(
149
+ identificadorAutor &&
150
+ props.usuarioLogado &&
151
+ String(identificadorAutor).toLowerCase() === props.usuarioLogado.toLowerCase()
152
+ );
153
+ const ini = extrairDataHoraLocal(reserva.data_inicio_reserva);
154
+ const fim = extrairDataHoraLocal(reserva.data_fim_reserva);
155
+
156
+ return {
157
+ id: reserva.codigo_referencia_reserva,
158
+ title: String(reserva.titulo_reserva || '').trim() || props.tituloPadrao,
159
+ start: `${ini.data} ${ini.hora}` as FormatoDataVueCal,
160
+ end: `${fim.data} ${fim.hora}` as FormatoDataVueCal,
161
+ id_recurso: reserva.id_recurso,
162
+ draggable: ehMeu,
163
+ resizable: ehMeu,
164
+ deletable: ehMeu,
165
+ class: ehMeu ? 'reserva-minha' : 'reserva-bloqueada'
166
+ };
167
+ });
217
168
  });
218
169
 
219
- // Recebe uma data do cabeçalho do calendário e devolve um booleano indicando se é a data selecionada.
220
- // É usada para destacar visualmente o dia ativo no topo da grade.
221
170
  const ehDataSelecionada = (data: any) => {
222
171
  if (!dataSelecionada.value || !data) return false;
223
- const dSelecionada = new Date(dataSelecionada.value);
224
- const dCabecalho = new Date(data);
225
- return dCabecalho.toDateString() === dSelecionada.toDateString();
172
+ return new Date(data).toDateString() === new Date(dataSelecionada.value).toDateString();
226
173
  };
227
174
 
228
- // Recebe um objeto Date e devolve a abreviação de três letras do dia da semana em português.
229
- // É usada na renderização customizada do cabeçalho dos dias.
230
- const obterNomeDiaCurto = (data: Date) => {
231
- return ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'][data.getDay()];
232
- };
175
+ const obterNomeDiaCurto = (data: Date) => ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'][data.getDay()];
233
176
 
234
- // Armazena a configuração de horários fora do expediente para bloqueio visual no componente vue-cal.
235
177
  const horasBloqueadasNativas = computed(() => {
236
178
  const bloqueios = [
237
179
  { from: 0, to: minutosInicio.value, allowEvents: false, class: 'fora-do-expediente' },
@@ -242,76 +184,71 @@ const horasBloqueadasNativas = computed(() => {
242
184
  </script>
243
185
 
244
186
  <template>
245
- <div class="wrapper-calendario">
246
- <ModalReserva
247
- v-model="modalAberto"
248
- :dados-iniciais="dadosParaModal"
249
- :usa-interceptador="usaInterceptador"
250
- :ocultar-titulo-modal="props.ocultarTituloModal"
251
- @before-save="$emit('before-save',$event)"
252
- @salvo="aoSalvar"
253
- @cancelado="aoCancelar"
254
- >
255
- <template #campos-extras>
256
- <slot name="campos-extras"></slot>
257
- </template>
258
- </ModalReserva>
259
-
260
- <ModalDetalhesReserva
261
- v-model="modalDetalhesAberto"
262
- :reserva="reservaSelecionadaDetalhes"
263
- :titulo-padrao="props.tituloPadrao"
264
- @excluido="atualizarVisualizacao"
265
- />
266
-
267
- <vue-cal
268
- ref="calendarioRef"
269
- class="calendario-principal vuecal--blue-theme"
270
- :events="eventosProcessados"
271
- v-model:active-view="visaoAtiva"
272
- v-model:selected-date="dataSelecionada"
273
- v-model:view-date="dataVisao"
274
- @update:view-date="dataVisao = $event"
275
- :views="['day', 'week']"
276
- :hide-view-selector="true"
277
- :hide-title-bar="true"
278
- :click-to-navigate="false"
279
- locale="pt-br"
280
- :time-from="minutosInicio"
281
- :time-to="minutosFim"
282
- :disable-days="datasDesabilitadas"
283
- :special-hours="horasBloqueadasNativas"
284
- :snap-to-interval="15"
285
- :start-week-on-sunday="true"
286
- :editable-events="{ drag: true, resize: true, delete: true, create: true }"
287
- @ready="atualizarVisualizacao"
288
- @view-change="atualizarVisualizacao"
289
- @event-create="aoCriarEvento"
290
- @event-click="aoClicarEvento"
291
- >
292
- <template #weekday-heading="{ date }">
293
- <div
294
- class="cabecalho-dia-customizado"
295
- @click="dataSelecionada = date; dataVisao = date"
296
- style="cursor: pointer;"
187
+ <div class="wrapper-calendario">
188
+ <ModalReserva
189
+ v-model="modalAberto"
190
+ :dados-iniciais="dadosParaModal"
191
+ :usa-interceptador="usaInterceptador"
192
+ :ocultar-titulo-modal="props.ocultarTituloModal"
193
+ @before-save="$emit('before-save', $event)"
194
+ @salvo="aoSalvar"
195
+ @cancelado="encerrarCriacaoProvisoria"
297
196
  >
298
- <span class="nome-dia">{{ obterNomeDiaCurto(date) }}</span>
299
- <span
300
- class="numero-dia"
301
- :class="{ 'dia-hoje': date.toDateString() === new Date().toDateString(), 'dia-selecionado': ehDataSelecionada(date) }"
302
- >
303
- {{ date.getDate() }}
304
- </span>
305
- </div>
306
- </template>
307
- </vue-cal>
308
- </div>
197
+ <template v-for="(_, nomeSlot) in $slots" :key="nomeSlot" #[nomeSlot]="slotProps">
198
+ <slot :name="nomeSlot" v-bind="slotProps ?? {}"></slot>
199
+ </template>
200
+ </ModalReserva>
201
+
202
+ <ModalDetalhesReserva
203
+ v-model="modalDetalhesAberto"
204
+ :reserva="reservaSelecionadaDetalhes"
205
+ :titulo-padrao="props.tituloPadrao"
206
+ @excluido="atualizarVisualizacao"
207
+ />
208
+
209
+ <vue-cal
210
+ ref="calendarioRef"
211
+ class="calendario-principal vuecal--blue-theme"
212
+ :events="eventosProcessados"
213
+ v-model:active-view="visaoAtiva"
214
+ v-model:selected-date="dataSelecionada"
215
+ v-model:view-date="dataVisao"
216
+ @update:view-date="dataVisao = $event"
217
+ :views="['day', 'week']"
218
+ :hide-view-selector="true"
219
+ :hide-title-bar="true"
220
+ :click-to-navigate="false"
221
+ locale="pt-br"
222
+ :time-from="minutosInicio"
223
+ :time-to="minutosFim"
224
+ :disable-days="datasDesabilitadas"
225
+ :special-hours="horasBloqueadasNativas"
226
+ :snap-to-interval="15"
227
+ :start-week-on-sunday="true"
228
+ :editable-events="{ drag: true, resize: true, delete: true, create: true }"
229
+ @ready="atualizarVisualizacao"
230
+ @view-change="atualizarVisualizacao"
231
+ @event-create="aoCriarEvento"
232
+ @event-click="aoClicarEvento"
233
+ >
234
+ <template #weekday-heading="{ date }">
235
+ <div class="cabecalho-dia-customizado" @click="dataSelecionada = date; dataVisao = date" style="cursor: pointer;">
236
+ <span class="nome-dia">{{ obterNomeDiaCurto(date) }}</span>
237
+ <span
238
+ class="numero-dia"
239
+ :class="{ 'dia-hoje': date.toDateString() === new Date().toDateString(), 'dia-selecionado': ehDataSelecionada(date) }"
240
+ >
241
+ {{ date.getDate() }}
242
+ </span>
243
+ </div>
244
+ </template>
245
+ </vue-cal>
246
+ </div>
309
247
  </template>
310
248
 
311
249
  <style scoped>
312
250
  .wrapper-calendario { display: flex; flex-direction: column; flex-grow: 1; height: 100%; overflow: hidden; background: var(--bg-primary); }
313
251
  .calendario-principal { --vuecal-weekday-bar-size: 75px !important; background: var(--bg-primary); flex-grow: 1; height: 100%; overflow: hidden; border-radius: 0 !important; border: none !important; box-shadow: none !important; }
314
-
315
252
  :deep(.vuecal__header), :deep(.vuecal__title-bar) { display: none !important; }
316
253
  :deep(.vuecal__weekdays-headings) { border-bottom: 1px solid var(--border-light); background-color: var(--bg-primary); color: var(--text-primary); }
317
254
  :deep(.vuecal__weekday) { background-color: var(--bg-primary); justify-content: center; border-right: 1px solid var(--border-light); padding: 0; }
@@ -331,28 +268,10 @@ const horasBloqueadasNativas = computed(() => {
331
268
  :deep(.vuecal__event:hover), :deep(.vuecal__event--focus) { box-shadow: var(--shadow-float); filter: brightness(1.1); }
332
269
  :deep(.vuecal__event.reserva-minha) { background-color: var(--color-success) !important; border-color: var(--color-success) !important; color: var(--text-inverse) !important; }
333
270
  :deep(.vuecal__event.reserva-bloqueada) { background-color: var(--border-editor) !important; border-color: var(--border-light) !important; color: var(--text-primary) !important; font-weight: bold; opacity: 0.75; cursor: pointer !important; background-image: repeating-linear-gradient(45deg, transparent, transparent 10px, rgba(0, 0, 0, 0.1) 10px, rgba(0, 0, 0, 0.1) 20px) !important; }
334
- :deep(.vuecal__time-column) {
335
- background-color: var(--bg-primary) !important;
336
- border-right: 1px solid var(--border-editor) !important;
337
- }
338
- :deep(.vuecal__time-cell) {
339
- color: var(--text-secondary) !important;
340
- }
341
- :deep(.vuecal__time-cell-line::before),
342
- :deep(.vuecal__cell::before) {
343
- border-color: var(--border-editor) !important;
344
- }
345
- :deep(.vuecal__cell) {
346
- background-color: transparent !important;
347
- border-right: 1px solid var(--border-editor) !important;
348
- }
349
- :deep(.vuecal__now-line) {
350
- color: var(--text-primary) !important;
351
- }
352
- :deep(.vuecal__now-line span) {
353
- color: var(--text-primary) !important;
354
- }
355
- :deep(.vuecal__cell::before) {
356
- border-color: var(--border-light) !important;
357
- }
271
+ :deep(.vuecal__time-column) { background-color: var(--bg-primary) !important; border-right: 1px solid var(--border-editor) !important; }
272
+ :deep(.vuecal__time-cell) { color: var(--text-secondary) !important; }
273
+ :deep(.vuecal__time-cell-line::before) { border-color: var(--border-editor) !important; }
274
+ :deep(.vuecal__cell) { background-color: transparent !important; border-right: 1px solid var(--border-editor) !important; }
275
+ :deep(.vuecal__now-line), :deep(.vuecal__now-line span) { color: var(--text-primary) !important; }
276
+ :deep(.vuecal__cell::before) { border-color: var(--border-light) !important; }
358
277
  </style>