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,14 +2,25 @@
2
2
  ========
3
3
  Este arquivo implementa o modelo visual 'tabela-horario' com os turnos na vertical à esquerda,
4
4
  colunas de horários seguidas pelos dias da semana e suporte a colunas de horários específicas (como Sábado)
5
- com altura proporcional, mesclagem vertical de reservas contínuas e seleção vertical via BlocoArrastavel.
5
+ com altura proporcional, mesclagem vertical de reservas contínuas, seleção vertical via BlocoArrastavel
6
+ e exposição da lista contendo o recurso atualmente em exibição.
6
7
  ========
7
8
  -->
8
9
  <script setup lang="ts">
9
- import { computed, onMounted, watch, ref, inject } from 'vue';
10
+ import { computed, watch, inject } from 'vue';
10
11
  import { useAgenda } from '../composables/useAgenda';
11
12
  import { useCoresAgenda } from '../composables/useCoresAgenda';
12
13
  import { useSelecaoArrasto } from '../composables/useSelecaoArrasto';
14
+ import {
15
+ SIGLAS_DIAS_JS,
16
+ TITULOS_DIAS_CABECALHO,
17
+ extrairDataHoraLocal,
18
+ extrairHoraFormatada,
19
+ criarDataLocal,
20
+ mesclarSlotsReservados,
21
+ useGradeSemanal,
22
+ useSincronizarRecursoUnico
23
+ } from '../composables/useGradeCompartilhada';
13
24
  import BlocoArrastavel from './BlocoArrastavel.vue';
14
25
  import ModalReserva from './ModalReserva.vue';
15
26
  import ModalDetalhesReserva from './ModalDetalhesReserva.vue';
@@ -35,111 +46,23 @@ const props = defineProps<{
35
46
  }>();
36
47
 
37
48
  const api = inject('api-instance-agenda') as any;
38
- const emit = defineEmits(['before-save']);
39
-
40
- const {
41
- dadosMatrix,
42
- buscarSlotsMatrix,
43
- buscarReservas,
44
- agendasDisponiveis,
45
- agendaSelecionada,
46
- recursosDisponiveis,
47
- recursoSelecionado,
48
- dataSelecionada,
49
- converterParaUtc
50
- } = useAgenda(api);
49
+ defineEmits(['before-save']);
51
50
 
51
+ const { agendaSelecionada, recursosDisponiveis, recursoSelecionado } = useAgenda(api);
52
52
  const { theme, obterCoresBloco } = useCoresAgenda();
53
53
 
54
- // Mapeamento das siglas dos dias da semana conforme índice do Date.getDay() (0=DOM a 6=SAB).
55
- const SIGLAS_DIAS_JS = ['DOM', 'SEG', 'TER', 'QUA', 'QUI', 'SEX', 'SAB'];
56
-
57
- // Mapeamento dos títulos de cabeçalho de cada dia da semana conforme índice do Date.getDay().
58
- const TITULOS_DIAS_CABECALHO = [
59
- 'DOMINGO',
60
- '2ª FEIRA',
61
- '3ª FEIRA',
62
- '4ª FEIRA',
63
- '5ª FEIRA',
64
- '6ª FEIRA',
65
- 'SÁBADO'
66
- ];
67
-
68
- // Indica se os dados da grade estão sendo carregados da API.
69
- const carregando = ref(false);
70
-
71
- // Controla a abertura do modal de criação de reserva.
72
- const modalAberto = ref(false);
73
-
74
- // Armazena os dados preenchidos pela seleção na grade para enviar ao modal de criação.
75
- const dadosParaModal = ref({});
76
-
77
- // Controla a abertura do modal de detalhes de uma reserva existente.
78
- const modalDetalhesAberto = ref(false);
79
-
80
- // Armazena a reserva selecionada ao clicar em um bloco ocupado.
81
- const reservaSelecionadaDetalhes = ref<any>(null);
82
-
83
- // Recebe uma string ISO UTC e devolve objeto com data (YYYY-MM-DD) e hora (HH:MM) locais.
84
- const extrairDataHoraLocal = (isoString: string) => {
85
- const d = new Date(isoString);
86
- const ano = d.getFullYear();
87
- const mes = String(d.getMonth() + 1).padStart(2, '0');
88
- const dia = String(d.getDate()).padStart(2, '0');
89
- const horas = String(d.getHours()).padStart(2, '0');
90
- const min = String(d.getMinutes()).padStart(2, '0');
91
- return { data: `${ano}-${mes}-${dia}`, hora: `${horas}:${min}` };
92
- };
93
-
94
- // Instância do controlador genérico de arrasto configurada para operar nas colunas verticais de cada dia.
95
- const controladorArrastoVertical = useSelecaoArrasto({
96
- aoClicarItemIndisponivel: (celula) => {
97
- if (celula?.reservado_slot && celula?.detalhes_reserva) {
98
- reservaSelecionadaDetalhes.value = celula.detalhes_reserva;
99
- modalDetalhesAberto.value = true;
100
- }
101
- },
102
- aoFinalizarSelecao: ({ itensSelecionados }) => {
103
- if (!idRecursoAtivo.value) return;
104
- const slotInicial = itensSelecionados[0];
105
- const slotFinal = itensSelecionados[itensSelecionados.length - 1];
106
- if (!slotInicial || !slotFinal) return;
107
-
108
- const inicio = extrairDataHoraLocal(slotInicial.data_inicio_slot);
109
- const fim = extrairDataHoraLocal(slotFinal.data_fim_slot);
110
-
111
- dadosParaModal.value = {
112
- id_recurso: idRecursoAtivo.value,
113
- titulo_reserva: '',
114
- data_inicio: inicio.data,
115
- hora_inicio: inicio.hora,
116
- data_fim: fim.data,
117
- hora_fim: fim.hora
118
- };
119
-
120
- modalAberto.value = true;
121
- }
122
- });
123
-
124
- // Sincroniza o recurso e a agenda caso o componente tenha recebido a propriedade idRecursoUnico.
125
- const sincronizarRecursoUnico = () => {
126
- if (!props.idRecursoUnico || recursosDisponiveis.value.length === 0) return;
127
-
128
- const encontrado = recursosDisponiveis.value.find(
129
- (r: any) => Number(r.id_recurso) === Number(props.idRecursoUnico)
130
- );
54
+ useSincronizarRecursoUnico(api, () => props.idRecursoUnico);
131
55
 
132
- if (encontrado) {
133
- recursoSelecionado.value = Number(encontrado.id_recurso);
134
- if (encontrado.id_agenda && agendaSelecionada.value !== encontrado.id_agenda) {
135
- agendaSelecionada.value = encontrado.id_agenda;
136
- }
137
- }
138
- };
139
-
140
- watch([() => props.idRecursoUnico, recursosDisponiveis], () => {
141
- sincronizarRecursoUnico();
142
- }, { immediate: true });
56
+ const {
57
+ dadosMatrix,
58
+ carregando,
59
+ modalAberto,
60
+ dadosParaModal,
61
+ modalDetalhesAberto,
62
+ reservaSelecionadaDetalhes,
63
+ obterBlocosDoDia,
64
+ dispararBuscaSemanal
65
+ } = useGradeSemanal(api, () => props.configuracaoBlocos);
143
66
 
144
67
  // Quando a agenda muda no seletor e não há idRecursoUnico fixo, garante que o recursoSelecionado pertença à agenda ativa.
145
68
  watch(agendaSelecionada, (novoIdAgenda) => {
@@ -157,66 +80,6 @@ watch(agendaSelecionada, (novoIdAgenda) => {
157
80
  }
158
81
  });
159
82
 
160
- // Calcula a data de início da semana (Segunda-feira por padrão) com base na data selecionada.
161
- const obterInicioDaSemana = (dataRef: Date, idAgenda: number | null) => {
162
- const d = new Date(dataRef.getFullYear(), dataRef.getMonth(), dataRef.getDate());
163
- const agendaObj = agendasDisponiveis.value.find((a: any) => Number(a.id_agenda) === Number(idAgenda));
164
- const diaInicioPython = agendaObj?.detalhes?.dia_inicio_semana_agenda ?? 0;
165
- const diaInicioJs = (diaInicioPython + 1) % 7;
166
- const diff = (d.getDay() - diaInicioJs + 7) % 7;
167
- d.setDate(d.getDate() - diff);
168
- return d;
169
- };
170
-
171
- // Busca os slots da matriz e as reservas para os 7 dias da semana ordenada.
172
- const dispararBuscaGrade = async () => {
173
- const idAgenda = agendaSelecionada.value;
174
- if (!idAgenda) return;
175
-
176
- carregando.value = true;
177
-
178
- const valorData = dataSelecionada.value || new Date();
179
- let dataBase: Date;
180
-
181
- if (typeof valorData === 'string') {
182
- const apenasData = String(valorData).substring(0, 10);
183
- const [ano, mes, dia] = apenasData.split('-');
184
- dataBase = new Date(Number(ano), Number(mes) - 1, Number(dia));
185
- } else {
186
- dataBase = new Date(valorData);
187
- }
188
-
189
- const dataInicio = obterInicioDaSemana(dataBase, idAgenda);
190
- const dataFim = new Date(dataInicio);
191
- dataFim.setDate(dataInicio.getDate() + 6);
192
-
193
- const formatar = (dt: Date) => {
194
- const ano = dt.getFullYear();
195
- const mes = String(dt.getMonth() + 1).padStart(2, '0');
196
- const dia = String(dt.getDate()).padStart(2, '0');
197
- return `${ano}-${mes}-${dia}`;
198
- };
199
-
200
- const strInicio = formatar(dataInicio);
201
- const strFim = formatar(dataFim);
202
-
203
- const inicioUtc = encodeURIComponent(converterParaUtc(strInicio, '00:00'));
204
- const fimUtc = encodeURIComponent(converterParaUtc(strFim, '23:59'));
205
-
206
- await buscarSlotsMatrix(idAgenda, inicioUtc, fimUtc);
207
- await buscarReservas(`${strInicio}T00:00:00`, `${strFim}T23:59:59`);
208
- carregando.value = false;
209
- };
210
-
211
- onMounted(() => {
212
- dispararBuscaGrade();
213
- });
214
-
215
- watch([dataSelecionada, agendaSelecionada], () => {
216
- dispararBuscaGrade();
217
- });
218
-
219
- // Devolve a lista de recursos pertencentes à agenda selecionada para o seletor de salas.
220
83
  const recursosDaAgendaAtual = computed(() => {
221
84
  if (!agendaSelecionada.value) return recursosDisponiveis.value;
222
85
  return recursosDisponiveis.value.filter(
@@ -224,7 +87,6 @@ const recursosDaAgendaAtual = computed(() => {
224
87
  );
225
88
  });
226
89
 
227
- // Devolve o ID numérico do recurso ativo que está sendo exibido na grade.
228
90
  const idRecursoAtivo = computed(() => {
229
91
  if (props.idRecursoUnico) return Number(props.idRecursoUnico);
230
92
  if (recursoSelecionado.value) return Number(recursoSelecionado.value);
@@ -232,54 +94,49 @@ const idRecursoAtivo = computed(() => {
232
94
  return primeiro ? Number(primeiro.id_recurso) : null;
233
95
  });
234
96
 
235
- // Recebe uma string ISO UTC e devolve a hora local formatada em HH:MM.
236
- const extrairHoraFormatada = (isoStr: string) => {
237
- const d = new Date(isoStr);
238
- const horas = String(d.getHours()).padStart(2, '0');
239
- const minutos = String(d.getMinutes()).padStart(2, '0');
240
- return `${horas}:${minutos}`;
241
- };
97
+ const recursosEmExibicao = computed(() => {
98
+ if (!idRecursoAtivo.value) return [];
99
+ const encontrado = recursosDisponiveis.value.find(
100
+ (r: any) => Number(r.id_recurso) === Number(idRecursoAtivo.value)
101
+ );
102
+ return encontrado ? [encontrado] : [];
103
+ });
242
104
 
243
- // Recebe uma string YYYY-MM-DD e devolve o objeto Date local.
244
- const criarDataLocal = (isoData: string) => {
245
- const apenasData = String(isoData).substring(0, 10);
246
- const [ano, mes, dia] = apenasData.split('-');
247
- return new Date(Number(ano), Number(mes) - 1, Number(dia));
248
- };
105
+ defineExpose({ recursosEmExibicao });
249
106
 
250
- // Localiza a configuração de blocos enviada via prop para a agenda selecionada no momento.
251
- const configuracaoAgendaAtual = computed(() => {
252
- if (!props.configuracaoBlocos || !agendaSelecionada.value) return null;
253
- const idAtual = Number(agendaSelecionada.value);
107
+ const controladorArrastoVertical = useSelecaoArrasto({
108
+ aoClicarItemIndisponivel: (celula) => {
109
+ if (celula?.reservado_slot && celula?.detalhes_reserva) {
110
+ reservaSelecionadaDetalhes.value = celula.detalhes_reserva;
111
+ modalDetalhesAberto.value = true;
112
+ }
113
+ },
114
+ aoFinalizarSelecao: ({ itensSelecionados }) => {
115
+ if (!idRecursoAtivo.value) return;
116
+ const slotInicial = itensSelecionados[0];
117
+ const slotFinal = itensSelecionados[itensSelecionados.length - 1];
118
+ if (!slotInicial || !slotFinal) return;
254
119
 
255
- if (Array.isArray(props.configuracaoBlocos)) {
256
- return props.configuracaoBlocos.find((c: any) => Number(c.id_agenda) === idAtual) || null;
257
- }
120
+ const inicio = extrairDataHoraLocal(slotInicial.data_inicio_slot);
121
+ const fim = extrairDataHoraLocal(slotFinal.data_fim_slot);
258
122
 
259
- if (typeof props.configuracaoBlocos === 'object') {
260
- if (Number(props.configuracaoBlocos.id_agenda) === idAtual) {
261
- return props.configuracaoBlocos;
262
- }
263
- if (props.configuracaoBlocos[idAtual]) {
264
- return props.configuracaoBlocos[idAtual];
265
- }
123
+ dadosParaModal.value = {
124
+ id_recurso: idRecursoAtivo.value,
125
+ titulo_reserva: '',
126
+ data_inicio: inicio.data,
127
+ hora_inicio: inicio.hora,
128
+ data_fim: fim.data,
129
+ hora_fim: fim.hora
130
+ };
131
+
132
+ modalAberto.value = true;
266
133
  }
267
- return null;
268
134
  });
269
135
 
270
136
  // Processa a lista de horários de um grupo de dias e gera os metadados de cada linha (bloco lateral, cor e bordas de subdivisão).
271
137
  const mapearLinhasComBlocos = (horariosBase: { horaInicio: string; horaFim: string }[], siglaDia: string) => {
272
138
  const total = horariosBase.length;
273
- const cfg = configuracaoAgendaAtual.value;
274
-
275
- let listaBlocos: any[] | null = null;
276
- if (cfg) {
277
- if (cfg.por_dia && cfg.por_dia[siglaDia]) {
278
- listaBlocos = cfg.por_dia[siglaDia];
279
- } else if (Array.isArray(cfg.blocos)) {
280
- listaBlocos = cfg.blocos;
281
- }
282
- }
139
+ const listaBlocos = obterBlocosDoDia(siglaDia);
283
140
 
284
141
  const metadadosLinhas: MetadadoLinha[] = horariosBase.map((h) => ({
285
142
  horaInicio: h.horaInicio,
@@ -302,8 +159,6 @@ const mapearLinhasComBlocos = (horariosBase: { horaInicio: string; horaFim: stri
302
159
  listaBlocos.forEach((blocoCfg: any, idxBloco: number) => {
303
160
  if (cursor >= total) return;
304
161
  const qtdSolicitada = Number(blocoCfg.quantidade) || 0;
305
- if (qtdSolicitada <= 0) return;
306
-
307
162
  const qtdReal = Math.min(qtdSolicitada, total - cursor);
308
163
  if (qtdReal <= 0) return;
309
164
 
@@ -331,37 +186,27 @@ const mapearLinhasComBlocos = (horariosBase: { horaInicio: string; horaFim: stri
331
186
  let consumido = 0;
332
187
  subLista.forEach((tam: number) => {
333
188
  if (consumido >= qtdReal) return;
334
- const tamEfetivo = Math.min(Number(tam) || 1, qtdReal - consumido);
335
- consumido += tamEfetivo;
189
+ consumido += Math.min(Number(tam) || 1, qtdReal - consumido);
336
190
  const metaFimSub = metadadosLinhas[cursor + consumido - 1];
337
- if (metaFimSub) {
338
- metaFimSub.fimDeSubbloco = true;
339
- }
191
+ if (metaFimSub) metaFimSub.fimDeSubbloco = true;
340
192
  });
341
193
  } else if (subFixo > 0) {
342
194
  let consumido = 0;
343
195
  while (consumido < qtdReal) {
344
- const tamEfetivo = Math.min(subFixo, qtdReal - consumido);
345
- consumido += tamEfetivo;
196
+ consumido += Math.min(subFixo, qtdReal - consumido);
346
197
  const metaFimSub = metadadosLinhas[cursor + consumido - 1];
347
- if (metaFimSub) {
348
- metaFimSub.fimDeSubbloco = true;
349
- }
198
+ if (metaFimSub) metaFimSub.fimDeSubbloco = true;
350
199
  }
351
200
  }
352
201
 
353
202
  const metaFimPrincipal = metadadosLinhas[cursor + qtdReal - 1];
354
- if (metaFimPrincipal) {
355
- metaFimPrincipal.fimDeBlocoPrincipal = true;
356
- }
203
+ if (metaFimPrincipal) metaFimPrincipal.fimDeBlocoPrincipal = true;
357
204
  cursor += qtdReal;
358
205
  });
359
206
 
360
207
  if (cursor < total) {
361
208
  const restante = total - cursor;
362
- const idxExtra = listaBlocos.length;
363
-
364
- const { corFundo, corBorda } = obterCoresBloco(idxExtra);
209
+ const { corFundo, corBorda } = obterCoresBloco(listaBlocos.length);
365
210
 
366
211
  const metaRestante = metadadosLinhas[cursor];
367
212
  if (metaRestante) {
@@ -379,15 +224,13 @@ const mapearLinhasComBlocos = (horariosBase: { horaInicio: string; horaFim: stri
379
224
  }
380
225
 
381
226
  const metaUltimo = metadadosLinhas[total - 1];
382
- if (metaUltimo) {
383
- metaUltimo.fimDeBlocoPrincipal = true;
384
- }
227
+ if (metaUltimo) metaUltimo.fimDeBlocoPrincipal = true;
385
228
  }
386
229
 
387
230
  return { temColunaTurno: true, linhas: metadadosLinhas };
388
231
  };
389
232
 
390
- // Constrói a estrutura completa de grupos de colunas (ex: 2ª a 6ª feira + coluna de Horas e Sábado separados) e calcula o rowspan das reservas.
233
+ // Constrói a estrutura completa de grupos de colunas e calcula o rowspan das reservas.
391
234
  const estruturaGradeHorario = computed(() => {
392
235
  if (!dadosMatrix.value?.datas_matriz || !idRecursoAtivo.value) {
393
236
  return { grupos: [], maxLinhas: 0, temColunaTurnoPrincipal: false };
@@ -430,57 +273,11 @@ const estruturaGradeHorario = computed(() => {
430
273
  });
431
274
  }
432
275
 
433
- const grupo = gruposPorAssinatura.get(assinatura);
434
-
435
- // Calcula a mesclagem vertical (rowspan) para reservas que ocupam múltiplos horários consecutivos.
436
- const celulasProcessadas: any[] = [];
437
- let i = 0;
438
- while (i < slotsReservaveis.length) {
439
- const slotAtual = slotsReservaveis[i];
440
- const codRef = slotAtual?.detalhes_reserva?.codigo_referencia_reserva;
441
-
442
- if (slotAtual?.reservado_slot && codRef) {
443
- let span = 1;
444
- while (
445
- i + span < slotsReservaveis.length &&
446
- slotsReservaveis[i + span]?.reservado_slot &&
447
- slotsReservaveis[i + span]?.detalhes_reserva?.codigo_referencia_reserva === codRef
448
- ) {
449
- span++;
450
- }
451
-
452
- celulasProcessadas.push({
453
- ...slotAtual,
454
- indiceLinha: i,
455
- rowspan: span,
456
- ocultaPorRowspan: false
457
- });
458
-
459
- for (let k = 1; k < span; k++) {
460
- celulasProcessadas.push({
461
- ...slotsReservaveis[i + k],
462
- indiceLinha: i + k,
463
- rowspan: 1,
464
- ocultaPorRowspan: true
465
- });
466
- }
467
- i += span;
468
- } else {
469
- celulasProcessadas.push({
470
- ...slotAtual,
471
- indiceLinha: i,
472
- rowspan: 1,
473
- ocultaPorRowspan: false
474
- });
475
- i++;
476
- }
477
- }
478
-
479
- grupo.dias.push({
276
+ gruposPorAssinatura.get(assinatura).dias.push({
480
277
  data_slot: dia.data_slot,
481
278
  tituloDia,
482
279
  dataCurta,
483
- celulas: celulasProcessadas
280
+ celulas: mesclarSlotsReservados(slotsReservaveis, 'rowspan')
484
281
  });
485
282
  });
486
283
 
@@ -504,10 +301,10 @@ const estruturaGradeHorario = computed(() => {
504
301
  :usa-interceptador="props.usaInterceptador"
505
302
  :ocultar-titulo-modal="props.ocultarTituloModal"
506
303
  @before-save="$emit('before-save', $event)"
507
- @salvo="dispararBuscaGrade"
304
+ @salvo="dispararBuscaSemanal"
508
305
  >
509
- <template #campos-extras>
510
- <slot name="campos-extras"></slot>
306
+ <template v-for="(_, nomeSlot) in $slots" :key="nomeSlot" #[nomeSlot]="slotProps">
307
+ <slot :name="nomeSlot" v-bind="slotProps ?? {}"></slot>
511
308
  </template>
512
309
  </ModalReserva>
513
310
 
@@ -515,7 +312,7 @@ const estruturaGradeHorario = computed(() => {
515
312
  v-model="modalDetalhesAberto"
516
313
  :reserva="reservaSelecionadaDetalhes"
517
314
  :titulo-padrao="props.tituloPadrao"
518
- @excluido="dispararBuscaGrade"
315
+ @excluido="dispararBuscaSemanal"
519
316
  />
520
317
 
521
318
  <div class="aviso-vazio" v-if="carregando">
@@ -654,246 +451,45 @@ const estruturaGradeHorario = computed(() => {
654
451
  </template>
655
452
 
656
453
  <style scoped>
657
- .tabela-horario-container {
658
- width: 100%;
659
- height: 100%;
660
- background: var(--bg-primary);
661
- display: flex;
662
- flex-direction: column;
663
- user-select: none;
664
- }
665
-
666
- .horario-conteudo {
667
- display: flex;
668
- flex-direction: column;
669
- height: 100%;
670
- overflow: hidden;
671
- }
672
-
673
- .barra-topo-recurso {
674
- display: flex;
675
- align-items: center;
676
- gap: 12px;
677
- padding: 10px 20px;
678
- background: var(--bg-primary);
679
- border-bottom: 1px solid var(--border-light);
680
- font-size: 13.5px;
681
- font-weight: 600;
682
- color: var(--text-primary);
683
- flex-shrink: 0;
684
- }
685
-
686
- .barra-topo-recurso select {
687
- padding: 6px 12px;
688
- border-radius: 6px;
689
- border: 1px solid var(--border-light);
690
- background: var(--bg-secondary);
691
- color: var(--text-primary);
692
- font-weight: 600;
693
- cursor: pointer;
694
- }
695
-
696
- .dica-arrasto {
697
- margin-left: auto;
698
- font-size: 12px;
699
- font-weight: 500;
700
- color: var(--text-secondary);
701
- }
702
-
703
- .grade-scroll-wrapper {
704
- flex-grow: 1;
705
- overflow: auto;
706
- padding: 16px;
707
- background: var(--bg-primary);
708
- }
709
-
710
- .tabela-estilo-puc {
711
- width: 100%;
712
- border-collapse: collapse;
713
- table-layout: fixed;
714
- font-size: 12.5px;
715
- text-align: center;
716
- }
717
-
718
- .th-canto-vazio {
719
- width: 36px;
720
- min-width: 36px;
721
- max-width: 36px;
722
- background: transparent !important;
723
- border: none !important;
724
- }
725
-
726
- .th-col-horas {
727
- width: 78px;
728
- min-width: 78px;
729
- max-width: 78px;
730
- }
731
-
732
- .th-cabecalho-padrao {
733
- background-color: #0066B3 !important;
734
- color: #FFFFFF !important;
735
- font-weight: 800;
736
- padding: 8px 6px;
737
- border: 1px solid #005291;
738
- letter-spacing: 0.4px;
739
- position: sticky;
740
- top: 0;
741
- z-index: 4;
742
- }
743
-
744
- .th-cabecalho-especifico {
745
- background-color: #808080 !important;
746
- color: #FFFFFF !important;
747
- font-weight: 800;
748
- padding: 8px 6px;
749
- border: 1px solid #666666;
750
- letter-spacing: 0.4px;
751
- position: sticky;
752
- top: 0;
753
- z-index: 4;
754
- }
755
-
756
- .th-dia-nome {
757
- font-size: 13px;
758
- line-height: 1.2;
759
- }
760
-
761
- .th-dia-data {
762
- font-size: 10.5px;
763
- font-weight: 600;
764
- opacity: 0.9;
765
- }
766
-
767
- .td-turno-vertical {
768
- width: 36px;
769
- min-width: 36px;
770
- max-width: 36px;
771
- border: 1.5px solid;
772
- padding: 4px 0;
773
- vertical-align: middle;
774
- text-align: center;
775
- color: #111827 !important;
776
- }
777
-
778
- .texto-turno-vertical {
779
- writing-mode: vertical-rl;
780
- transform: rotate(180deg);
781
- font-weight: 800;
782
- font-size: 13.5px;
783
- letter-spacing: 1px;
784
- text-transform: uppercase;
785
- margin: 0 auto;
786
- white-space: nowrap;
787
- }
788
-
789
- .td-celula-horas {
790
- border: 1px solid;
791
- padding: 4px 2px;
792
- height: 40px;
793
- color: #111827 !important;
794
- font-weight: 700;
795
- font-size: 11.5px;
796
- }
797
-
798
- .horas-empilhadas {
799
- display: flex;
800
- flex-direction: column;
801
- align-items: center;
802
- justify-content: center;
803
- line-height: 1.15;
804
- }
805
-
806
- .td-slot-agendavel {
807
- border: 1px solid;
808
- padding: 4px 6px;
809
- height: 40px;
810
- cursor: pointer;
811
- transition: filter 0.12s ease, box-shadow 0.12s ease;
812
- vertical-align: middle;
813
- color: #111827 !important;
814
- }
815
-
816
- .td-slot-agendavel:hover {
817
- filter: brightness(0.93);
818
- box-shadow: inset 0 0 0 1.5px var(--color-brand);
819
- }
820
-
821
- .slot-selecionado-arrasto {
822
- box-shadow: inset 0 0 0 2px var(--color-brand) !important;
823
- }
824
-
454
+ .tabela-horario-container { width: 100%; height: 100%; background: var(--bg-primary); display: flex; flex-direction: column; user-select: none; }
455
+ .horario-conteudo { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
456
+ .barra-topo-recurso { display: flex; align-items: center; gap: 12px; padding: 10px 20px; background: var(--bg-primary); border-bottom: 1px solid var(--border-light); font-size: 13.5px; font-weight: 600; color: var(--text-primary); flex-shrink: 0; }
457
+ .barra-topo-recurso select { padding: 6px 12px; border-radius: 6px; border: 1px solid var(--border-light); background: var(--bg-secondary); color: var(--text-primary); font-weight: 600; cursor: pointer; }
458
+ .dica-arrasto { margin-left: auto; font-size: 12px; font-weight: 500; color: var(--text-secondary); }
459
+ .grade-scroll-wrapper { flex-grow: 1; overflow: auto; padding: 16px; background: var(--bg-primary); }
460
+ .tabela-estilo-puc { width: 100%; border-collapse: collapse; table-layout: fixed; font-size: 12.5px; text-align: center; }
461
+
462
+ .th-canto-vazio { width: 36px; min-width: 36px; max-width: 36px; background: transparent !important; border: none !important; }
463
+ .th-col-horas { width: 78px; min-width: 78px; max-width: 78px; }
464
+ .th-cabecalho-padrao { background-color: #0066B3 !important; color: #FFFFFF !important; font-weight: 800; padding: 8px 6px; border: 1px solid #005291; letter-spacing: 0.4px; position: sticky; top: 0; z-index: 4; }
465
+ .th-cabecalho-especifico { background-color: #808080 !important; color: #FFFFFF !important; font-weight: 800; padding: 8px 6px; border: 1px solid #666666; letter-spacing: 0.4px; position: sticky; top: 0; z-index: 4; }
466
+ .th-dia-nome { font-size: 13px; line-height: 1.2; }
467
+ .th-dia-data { font-size: 10.5px; font-weight: 600; opacity: 0.9; }
468
+
469
+ .td-turno-vertical { width: 36px; min-width: 36px; max-width: 36px; border: 1.5px solid; padding: 4px 0; vertical-align: middle; text-align: center; color: #111827 !important; }
470
+ .texto-turno-vertical { writing-mode: vertical-rl; transform: rotate(180deg); font-weight: 800; font-size: 13.5px; letter-spacing: 1px; text-transform: uppercase; margin: 0 auto; white-space: nowrap; }
471
+ .td-celula-horas { border: 1px solid; padding: 4px 2px; height: 40px; color: #111827 !important; font-weight: 700; font-size: 11.5px; }
472
+ .horas-empilhadas { display: flex; flex-direction: column; align-items: center; justify-content: center; line-height: 1.15; }
473
+ .td-slot-agendavel { border: 1px solid; padding: 4px 6px; height: 40px; cursor: pointer; transition: filter 0.12s ease, box-shadow 0.12s ease; vertical-align: middle; color: #111827 !important; }
474
+ .td-slot-agendavel:hover { filter: brightness(0.93); box-shadow: inset 0 0 0 1.5px var(--color-brand); }
475
+ .slot-selecionado-arrasto { box-shadow: inset 0 0 0 2px var(--color-brand) !important; }
825
476
  .slot-ocupado {
826
477
  font-weight: 700;
827
- /* Define uma cor de fundo cinza clara para destacar do resto do turno */
828
478
  background-color: #e5e7eb !important;
829
- /* Cria o efeito de listrinhas diagonais */
830
- background-image: repeating-linear-gradient(
831
- 45deg,
832
- rgba(0, 0, 0, 0.06),
833
- rgba(0, 0, 0, 0.06) 10px,
834
- transparent 10px,
835
- transparent 20px
836
- ) !important;
837
- }
838
-
839
- .conteudo-slot-horario {
840
- display: flex;
841
- align-items: center;
842
- justify-content: center;
843
- width: 100%;
844
- height: 100%;
845
- }
846
-
847
- .titulo-reserva-grade {
848
- font-size: 12px;
849
- font-weight: 700;
850
- color: #111827;
851
- line-height: 1.25;
852
- word-break: break-word;
853
- }
854
-
855
- .borda-subbloco {
856
- border-bottom-width: 2px !important;
479
+ background-image: repeating-linear-gradient(45deg, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.06) 10px, transparent 10px, transparent 20px) !important;
857
480
  }
858
481
 
859
- .borda-bloco-principal {
860
- border-bottom-width: 2.5px !important;
861
- }
862
-
863
- .td-espaco-vazio-relativo {
864
- background: transparent !important;
865
- border: none !important;
866
- pointer-events: none;
867
- }
868
-
869
- .aviso-vazio {
870
- display: flex;
871
- flex-direction: column;
872
- justify-content: center;
873
- align-items: center;
874
- flex-grow: 1;
875
- color: var(--text-secondary);
876
- font-weight: 500;
877
- font-size: 1rem;
878
- gap: 12px;
879
- }
880
-
881
- .spinner {
882
- width: 32px;
883
- height: 32px;
884
- border: 3px solid var(--border-light);
885
- border-top-color: var(--color-brand);
886
- border-radius: 50%;
887
- animation: spin 1s linear infinite;
888
- }
889
-
890
- @keyframes spin {
891
- to { transform: rotate(360deg); }
892
- }
482
+ .conteudo-slot-horario { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }
483
+ .titulo-reserva-grade { font-size: 12px; font-weight: 700; color: #111827; line-height: 1.25; word-break: break-word; }
484
+ .borda-subbloco { border-bottom-width: 2px !important; }
485
+ .borda-bloco-principal { border-bottom-width: 2.5px !important; }
486
+ .td-espaco-vazio-relativo { background: transparent !important; border: none !important; pointer-events: none; }
487
+ .aviso-vazio { display: flex; flex-direction: column; justify-content: center; align-items: center; flex-grow: 1; color: var(--text-secondary); font-weight: 500; font-size: 1rem; gap: 12px; }
488
+ .spinner { width: 32px; height: 32px; border: 3px solid var(--border-light); border-top-color: var(--color-brand); border-radius: 50%; animation: spin 1s linear infinite; }
489
+ @keyframes spin { to { transform: rotate(360deg); } }
893
490
  </style>
894
491
 
895
492
  <style>
896
- /* CSS injetado globalmente para os blocos da tabela horário escurecerem o texto no dark theme */
897
493
  [data-bs-theme="dark"] .td-turno-vertical,
898
494
  [data-bs-theme="dark"] .texto-turno-vertical,
899
495
  [data-bs-theme="dark"] .td-celula-horas,
@@ -905,12 +501,6 @@ const estruturaGradeHorario = computed(() => {
905
501
 
906
502
  [data-bs-theme="dark"] .slot-ocupado {
907
503
  background-color: #374151 !important;
908
- background-image: repeating-linear-gradient(
909
- 45deg,
910
- rgba(255, 255, 255, 0.03),
911
- rgba(255, 255, 255, 0.03) 10px,
912
- transparent 10px,
913
- transparent 20px
914
- ) !important;
504
+ background-image: repeating-linear-gradient(45deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.03) 10px, transparent 10px, transparent 20px) !important;
915
505
  }
916
506
  </style>