9966b5f175
CHROME COMPARTILHADO + 18 PAGINAS NATIVAS
- MelissaConfigPage: chrome unico (header, drawer mobile, sidebar com Configuracoes
+ FAQ slot, main com Suspense). Replica fake-dialog right rule e fica flush
com o config-aside global.
- 18 wrappers finos (~25 linhas cada): cfg-precificacao, cfg-descontos,
cfg-excecoes, cfg-convenios, cfg-wa, cfg-wa-pessoal, cfg-wa-oficial,
cfg-wa-templates, cfg-conversas-tags/autoreply/optouts/sla/bots,
cfg-lembretes, cfg-creditos-wa, cfg-sms, cfg-email-templates,
cfg-recursos-extras, cfg-recursos-extras-extrato, cfg-auditoria.
- Cada wrapper usa defineAsyncComponent + Suspense pra evitar race com
tenantStore no boot (loading travado em alguns chooser-style pages).
- MelissaLayout: imports + SECOES + MELISSA_NON_CONFIG_SLUGS + render
conditions atualizados pra cobrir os 18 slugs.
PAGINAS LEGADAS DETECTAM CONTEXTO MELISSA
- ConfiguracoesWhatsappChooserPage, WhatsappPage, TwilioWhatsappPage,
SmsPage, RecursosExtrasPage, EmailTemplatesPage, AddonsExtratoPage,
AgendadorPage, ConversasAutoreplyPage: route.startsWith('/melissa')
decide se router.push vai pro slug Melissa ou /configuracoes legado.
- Anchors <a href="/configuracoes/..."> (que recarregavam pagina e
vazavam o usuario do Melissa) trocados por RouterLink context-aware.
- MelissaAgenda.goSettings agora vai pra /melissa/agenda-config.
PERSONALIZAR > TEMAS
- melissaThemes.js: catalogo Freud/Klein/Jung (wallpaper + cor primaria
+ preset Lara/Nora + surface).
- Toggle de tema aplica tudo de uma vez; persistido em melissa_prefs.themeName.
- Boot resolve themeName -> imagem via fetch + data URL (sem guardar
data URL gigante no DB).
- onCustomFileChange/onClearBg invalidam themeName quando user mexe no bg.
PERSONALIZAR > FUNDO NOS TEXTOS
- Pref textBgEnabled em melissa_prefs.
- MelissaHeroClock: prop textBg envolve relogio/data/saudacao/resumo
em <span class="hero-text"> que ganha bg branco/preto 60% + borda
+ padding + radius quando o toggle esta on.
- Vars --m-hero-text-bg / --m-hero-text-border flipam com light/dark.
TOP + DOCK COM GRADIENT HORIZONTAL
- Var --m-band: preto 80% (dark) / branco 80% (light).
- .melissa-topbar-band: gradiente cor->transparente (right->left) atras
dos botoes do topo.
- .melissa-dock: gradiente cor->transparente (left->right) atras dos pins.
MELISSANEGOCIO ABSORVE MINHA EMPRESA
- Adiciona logo upload + preview "cartao de visita" (computeds
enderecoLinhas/redesValidas/temDados/logoDisplay + redeIcon helper).
- Normaliza dados legados do cfg-empresa: redes_sociais.{rede} virou {name}.
- Preview teleporta entre 3 destinos baseado no viewport:
mobile -> drawer; mid-desktop -> sidebar; wide-desktop (>=1340px) ->
painel flutuante FORA do fake dialog (ancora no right edge + 14px gap,
altura segue conteudo, header alinhado com header do dialog).
- Remove cfg-empresa de melissaConfigGrupos.js + COMPONENT_MAP do
MelissaConfiguracoes; grupo "Empresa & Plataforma" -> "Plataforma".
CRONOMETRO -> SESSAO AGENDADA
- MelissaCronometro emite session-end ao parar com paciente selecionado
(threshold 5s pra ignorar start/stop acidental).
- MelissaLayout.onCronometroSessionEnd busca agenda_eventos do paciente
no dia (tipo='sessao'), pega o mais recente e grava em
extra_fields.cronometro_duracao_seg + cronometro_parado_em.
- Toast: sucesso ("X min salvos") ou warn ("sessao nao encontrada").
CONVERSATIONDRAWER WHATSAPP-LIKE
- Nova imagem whatsapp-bg.jpg (renomeada de hash random) usada como
tile (380px) no .cd-msgs.
- Light: bege #efeae2 + multiply blend.
- Dark: #0b141a + camada 78% sobre o doodle.
- Bubbles WA-style (verde out / branco-dark in com tails) ja existiam.
EXTRATO RECURSOS EXTRAS
- Filtros 2-por-linha em Melissa (vs 1/4 no /configuracoes).
- Cards de Resumo teleportam pro #cfg-page-side em Melissa
(1-col empilhado no drawer; 4-col inline no /configuracoes).
- Botoes de exportar com flex-1 distribuidos em uma unica linha em
desktop, wrap no mobile.
- DataTable scrollable em ambos os layouts.
OUTROS AJUSTES MENORES
- Cfg-conversas-autoreply: dias semana 4-cols em Melissa (vs 7-cols
no /configuracoes).
- Cfg-creditos-wa: 1/2 por linha (vs 1/2/4) em Melissa.
- Cfg-recursos-extras: pacotes 1/2 (vs 1/2/4); "Em breve" 1-col.
- WhatsAppPage aba Templates: guia de formatacao teleporta pro side
drawer em Melissa, deixando textareas full-width.
- ConfigPage chrome agora tem #cfg-page-actions target pros Teleport
de acoes (refresh button etc).
- Imagens renomeadas em src/assets/themes/ (freudwebp/melainewebp/
jungwebp.webp) e src/assets/whatsapp-bg.jpg.
- JoditTextEditor.vue novo (wrapper Jodit generico, sem features de email).
- MelissaConfigList.vue novo (lista compartilhada de configs pro drawer).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1510 lines
53 KiB
Vue
1510 lines
53 KiB
Vue
<script setup>
|
||
/*
|
||
* MelissaBloqueios — Pagina nativa Melissa pra "Bloqueios e Feriados".
|
||
*
|
||
* Substitui o embed cfg-bloqueios que vivia dentro do MelissaConfiguracoes.
|
||
* Layout 2-col:
|
||
* - COL 1 (sidebar) — Card "Resumo" (3 stats + ano nav) + Card
|
||
* "Acoes rapidas" (Novo bloqueio + Feriado municipal) + Card
|
||
* "Como funciona" (explicacao dos 3 tipos)
|
||
* - COL 2 (main) — 3 cards: Feriados Nacionais (read-only) +
|
||
* Feriados Municipais (CRUD) + Bloqueios (CRUD). Em desktop
|
||
* 50/50 com Bloqueios full-width abaixo.
|
||
*
|
||
* Logica espelhada do BloqueiosPage.vue (composable useFeriados +
|
||
* tabela agenda_bloqueios).
|
||
*/
|
||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
||
import MelissaConfigList from './MelissaConfigList.vue';
|
||
import { useToast } from 'primevue/usetoast';
|
||
import { useConfirm } from 'primevue/useconfirm';
|
||
import { supabase } from '@/lib/supabase/client';
|
||
import { useTenantStore } from '@/stores/tenantStore';
|
||
import { useFeriados } from '@/composables/useFeriados';
|
||
// DatePicker/Tag/Skeleton/Dialog: auto via PrimeVueResolver
|
||
|
||
const emit = defineEmits(['close']);
|
||
|
||
const toast = useToast();
|
||
const confirm = useConfirm();
|
||
const tenantStore = useTenantStore();
|
||
|
||
const {
|
||
nacionais,
|
||
municipais,
|
||
loading: loadingF,
|
||
load: loadFeriados,
|
||
criar: criarFeriado,
|
||
remover: removerFeriado,
|
||
isDuplicata
|
||
} = useFeriados();
|
||
|
||
// ── Breakpoints + drawer ───────────────────────────────────
|
||
const drawerOpen = ref(false);
|
||
const isMobile = ref(false);
|
||
let _mqMobile = null;
|
||
function _onMqMobileChange(e) {
|
||
isMobile.value = e.matches;
|
||
if (!e.matches) drawerOpen.value = false;
|
||
}
|
||
function toggleDrawer() { drawerOpen.value = !drawerOpen.value; }
|
||
function fecharDrawer() { drawerOpen.value = false; }
|
||
|
||
// Toggle entre cards (default) e lista de configs (alterna inline na sidebar)
|
||
const cfgOpen = ref(false);
|
||
function toggleCfg() { cfgOpen.value = !cfgOpen.value; }
|
||
function fecharCfg() { cfgOpen.value = false; }
|
||
|
||
// ── Estado ─────────────────────────────────────────────────
|
||
const loadingB = ref(true);
|
||
const saving = ref(false);
|
||
const ownerId = ref(null);
|
||
const tenantId = ref(null);
|
||
const ano = ref(new Date().getFullYear());
|
||
const bloqueios = ref([]);
|
||
|
||
// Dialog bloqueio
|
||
const dlgOpen = ref(false);
|
||
const dlgMode = ref('add');
|
||
const form = ref(emptyForm());
|
||
|
||
// Dialog feriado municipal
|
||
const fdlgOpen = ref(false);
|
||
const fsaving = ref(false);
|
||
const fform = ref(emptyFForm());
|
||
|
||
function emptyForm() {
|
||
return {
|
||
id: null,
|
||
titulo: '',
|
||
data_inicio: null,
|
||
data_fim: null,
|
||
hora_inicio: null,
|
||
hora_fim: null,
|
||
recorrente: false,
|
||
dia_semana: null,
|
||
observacao: ''
|
||
};
|
||
}
|
||
function emptyFForm() {
|
||
return { nome: '', data: null, observacao: '', bloqueia_sessoes: false };
|
||
}
|
||
|
||
// ── Helpers ────────────────────────────────────────────────
|
||
function dateToISO(d) {
|
||
if (!d) return null;
|
||
const dt = d instanceof Date ? d : new Date(d);
|
||
return `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, '0')}-${String(dt.getDate()).padStart(2, '0')}`;
|
||
}
|
||
function isoToDate(s) {
|
||
if (!s) return null;
|
||
const [y, m, d] = String(s).split('-').map(Number);
|
||
return new Date(y, m - 1, d);
|
||
}
|
||
function dateToHHMM(d) {
|
||
if (!d || !(d instanceof Date)) return null;
|
||
return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
|
||
}
|
||
function hhmmToDate(hhmm) {
|
||
if (!hhmm) return null;
|
||
const [h, m] = String(hhmm).split(':').map(Number);
|
||
const d = new Date();
|
||
d.setHours(h, m, 0, 0);
|
||
return d;
|
||
}
|
||
function fmtDate(iso) {
|
||
if (!iso) return '—';
|
||
const [y, m, d] = String(iso).split('-');
|
||
return `${d}/${m}/${y}`;
|
||
}
|
||
function fmtDateShort(iso) {
|
||
if (!iso) return '';
|
||
const [, m, d] = String(iso).split('-');
|
||
return `${d}/${m}`;
|
||
}
|
||
function fmtHora(t) {
|
||
if (!t) return null;
|
||
return String(t).slice(0, 5);
|
||
}
|
||
function fmtPeriodo(b) {
|
||
if (b.recorrente && b.dia_semana != null) {
|
||
const dias = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'];
|
||
return `Toda ${dias[b.dia_semana]}`;
|
||
}
|
||
const ini = fmtDate(b.data_inicio);
|
||
if (!b.data_fim || b.data_fim === b.data_inicio) {
|
||
const hora = b.hora_inicio
|
||
? ` · ${fmtHora(b.hora_inicio)}–${fmtHora(b.hora_fim) || '?'}`
|
||
: ' · Dia inteiro';
|
||
return ini + hora;
|
||
}
|
||
return `${ini} até ${fmtDate(b.data_fim)}`;
|
||
}
|
||
|
||
// ── Boot + load ────────────────────────────────────────────
|
||
async function boot() {
|
||
const { data } = await supabase.auth.getUser();
|
||
ownerId.value = data?.user?.id || null;
|
||
tenantId.value = tenantStore.activeTenantId || tenantStore.tenantId || tenantStore.tenant?.id || null;
|
||
if (tenantId.value) await loadFeriados(tenantId.value);
|
||
await loadBloqueios();
|
||
}
|
||
|
||
async function loadBloqueios() {
|
||
if (!ownerId.value) return;
|
||
loadingB.value = true;
|
||
try {
|
||
const { data, error } = await supabase
|
||
.from('agenda_bloqueios')
|
||
.select('*')
|
||
.eq('owner_id', ownerId.value)
|
||
.gte('data_inicio', `${ano.value}-01-01`)
|
||
.lte('data_inicio', `${ano.value}-12-31`)
|
||
.order('data_inicio');
|
||
if (error) throw error;
|
||
bloqueios.value = data || [];
|
||
} catch (e) {
|
||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 3500 });
|
||
} finally {
|
||
loadingB.value = false;
|
||
}
|
||
}
|
||
|
||
async function anoAnterior() {
|
||
ano.value--;
|
||
if (tenantId.value) await loadFeriados(tenantId.value, ano.value);
|
||
await loadBloqueios();
|
||
}
|
||
async function anoProximo() {
|
||
ano.value++;
|
||
if (tenantId.value) await loadFeriados(tenantId.value, ano.value);
|
||
await loadBloqueios();
|
||
}
|
||
|
||
// ── Feriado municipal CRUD ─────────────────────────────────
|
||
function abrirFeriadoMunicipal() {
|
||
fform.value = emptyFForm();
|
||
fdlgOpen.value = true;
|
||
}
|
||
|
||
const fformValid = computed(() => !!fform.value.nome.trim() && !!fform.value.data);
|
||
const fformDuplicate = computed(() => {
|
||
if (!fform.value.data || !fform.value.nome) return false;
|
||
return isDuplicata(dateToISO(fform.value.data), fform.value.nome);
|
||
});
|
||
|
||
async function salvarFeriado() {
|
||
if (!fformValid.value) return;
|
||
const iso = dateToISO(fform.value.data);
|
||
if (isDuplicata(iso, fform.value.nome)) {
|
||
toast.add({
|
||
severity: 'warn',
|
||
summary: 'Duplicado',
|
||
detail: 'Já existe um feriado com esse nome nessa data.',
|
||
life: 3000
|
||
});
|
||
return;
|
||
}
|
||
fsaving.value = true;
|
||
try {
|
||
await criarFeriado({
|
||
tenant_id: tenantId.value,
|
||
owner_id: ownerId.value,
|
||
tipo: 'municipal',
|
||
nome: fform.value.nome.trim(),
|
||
data: iso,
|
||
observacao: fform.value.observacao || null,
|
||
bloqueia_sessoes: fform.value.bloqueia_sessoes
|
||
});
|
||
toast.add({ severity: 'success', summary: 'Feriado cadastrado', life: 1800 });
|
||
fdlgOpen.value = false;
|
||
} catch (e) {
|
||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 3500 });
|
||
} finally {
|
||
fsaving.value = false;
|
||
}
|
||
}
|
||
|
||
function excluirFeriado(id) {
|
||
confirm.require({
|
||
message: 'Deseja remover este feriado municipal?',
|
||
header: 'Confirmar remoção',
|
||
icon: 'pi pi-exclamation-triangle',
|
||
rejectLabel: 'Cancelar',
|
||
acceptLabel: 'Remover',
|
||
acceptClass: 'p-button-danger',
|
||
accept: async () => {
|
||
try {
|
||
await removerFeriado(id);
|
||
toast.add({ severity: 'success', summary: 'Removido', life: 1500 });
|
||
} catch (e) {
|
||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 3000 });
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// ── Bloqueio CRUD ──────────────────────────────────────────
|
||
function abrirAddBloqueio() {
|
||
form.value = emptyForm();
|
||
dlgMode.value = 'add';
|
||
dlgOpen.value = true;
|
||
}
|
||
|
||
function abrirEditBloqueio(b) {
|
||
form.value = {
|
||
id: b.id,
|
||
titulo: b.titulo,
|
||
data_inicio: isoToDate(b.data_inicio),
|
||
data_fim: isoToDate(b.data_fim),
|
||
hora_inicio: hhmmToDate(fmtHora(b.hora_inicio)),
|
||
hora_fim: hhmmToDate(fmtHora(b.hora_fim)),
|
||
recorrente: !!b.recorrente,
|
||
dia_semana: b.dia_semana ?? null,
|
||
observacao: b.observacao || ''
|
||
};
|
||
dlgMode.value = 'edit';
|
||
dlgOpen.value = true;
|
||
}
|
||
|
||
const formValid = computed(() => !!form.value.titulo.trim() && !!form.value.data_inicio);
|
||
|
||
async function salvarBloqueio() {
|
||
if (!formValid.value) return;
|
||
saving.value = true;
|
||
try {
|
||
const payload = {
|
||
owner_id: ownerId.value,
|
||
tenant_id: tenantId.value,
|
||
tipo: 'bloqueio',
|
||
titulo: form.value.titulo.trim(),
|
||
data_inicio: dateToISO(form.value.data_inicio),
|
||
data_fim: dateToISO(form.value.data_fim) || null,
|
||
hora_inicio: dateToHHMM(form.value.hora_inicio) || null,
|
||
hora_fim: dateToHHMM(form.value.hora_fim) || null,
|
||
recorrente: form.value.recorrente,
|
||
dia_semana: form.value.dia_semana ?? null,
|
||
observacao: form.value.observacao || null,
|
||
origem: 'manual'
|
||
};
|
||
if (dlgMode.value === 'edit') {
|
||
const { error } = await supabase
|
||
.from('agenda_bloqueios')
|
||
.update(payload)
|
||
.eq('id', form.value.id);
|
||
if (error) throw error;
|
||
toast.add({ severity: 'success', summary: 'Salvo', detail: 'Bloqueio atualizado.', life: 1800 });
|
||
} else {
|
||
const { error } = await supabase.from('agenda_bloqueios').insert(payload);
|
||
if (error) throw error;
|
||
toast.add({ severity: 'success', summary: 'Criado', detail: 'Bloqueio adicionado.', life: 1800 });
|
||
}
|
||
dlgOpen.value = false;
|
||
await loadBloqueios();
|
||
} catch (e) {
|
||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 3500 });
|
||
} finally {
|
||
saving.value = false;
|
||
}
|
||
}
|
||
|
||
function excluirBloqueio(id) {
|
||
const b = bloqueios.value.find((x) => x.id === id);
|
||
confirm.require({
|
||
message: b?.titulo ? `Remover o bloqueio "${b.titulo}"?` : 'Deseja remover este bloqueio?',
|
||
header: 'Confirmar remoção',
|
||
icon: 'pi pi-exclamation-triangle',
|
||
rejectLabel: 'Cancelar',
|
||
acceptLabel: 'Remover',
|
||
acceptClass: 'p-button-danger',
|
||
accept: async () => {
|
||
try {
|
||
const { error } = await supabase
|
||
.from('agenda_bloqueios')
|
||
.delete()
|
||
.eq('id', id);
|
||
if (error) throw error;
|
||
bloqueios.value = bloqueios.value.filter((b) => b.id !== id);
|
||
toast.add({ severity: 'success', summary: 'Removido', life: 1500 });
|
||
} catch (e) {
|
||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 3000 });
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
const loading = computed(() => loadingF.value || loadingB.value);
|
||
|
||
// ── Lifecycle ──────────────────────────────────────────────
|
||
onMounted(async () => {
|
||
if (typeof window !== 'undefined' && window.matchMedia) {
|
||
_mqMobile = window.matchMedia('(max-width: 1023px)');
|
||
isMobile.value = _mqMobile.matches;
|
||
try { _mqMobile.addEventListener('change', _onMqMobileChange); }
|
||
catch { _mqMobile.addListener(_onMqMobileChange); }
|
||
}
|
||
await tenantStore.ensureLoaded();
|
||
tenantId.value = tenantStore.activeTenantId || tenantStore.tenantId || tenantStore.tenant?.id || null;
|
||
await boot();
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
if (_mqMobile) {
|
||
try { _mqMobile.removeEventListener('change', _onMqMobileChange); }
|
||
catch { _mqMobile.removeListener(_onMqMobileChange); }
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<Transition name="mbq-drawer-fade">
|
||
<div
|
||
v-show="isMobile && drawerOpen"
|
||
class="mbq-mobile-drawer"
|
||
:class="{ 'is-open': drawerOpen }"
|
||
>
|
||
<div id="mbq-mobile-drawer-target" class="mbq-mobile-drawer__scroll" />
|
||
</div>
|
||
</Transition>
|
||
<Transition name="mbq-drawer-fade">
|
||
<div
|
||
v-show="isMobile && drawerOpen"
|
||
class="mbq-mobile-drawer__backdrop"
|
||
@click="fecharDrawer"
|
||
/>
|
||
</Transition>
|
||
|
||
<ConfirmDialog />
|
||
|
||
<section class="mbq-page">
|
||
<header class="mbq-page__head">
|
||
<button
|
||
class="mbq-menu-btn mbq-menu-btn--mobile-only"
|
||
v-tooltip.bottom="'Resumo & Acoes'"
|
||
@click="toggleDrawer"
|
||
>
|
||
<i class="pi pi-bars" />
|
||
<span>Menu</span>
|
||
</button>
|
||
<div class="mbq-page__title">
|
||
<i class="pi pi-ban mbq-page__title-icon" />
|
||
<span>Bloqueios e Feriados</span>
|
||
<span class="mbq-page__year">{{ ano }}</span>
|
||
</div>
|
||
<div class="mbq-page__actions">
|
||
<button
|
||
class="mbq-act-btn"
|
||
v-tooltip.bottom="'Atualizar'"
|
||
:disabled="loading"
|
||
@click="boot"
|
||
>
|
||
<i :class="loading ? 'pi pi-spin pi-spinner' : 'pi pi-refresh'" />
|
||
</button>
|
||
<button class="mbq-close" v-tooltip.bottom="'Voltar (Esc)'" @click="emit('close')">
|
||
<i class="pi pi-times" />
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="mbq-subheader">
|
||
<i class="pi pi-info-circle mbq-subheader__icon" />
|
||
<span class="mbq-subheader__text">
|
||
Feriados nacionais sao automaticos. Adicione feriados municipais
|
||
e periodos bloqueados (ferias, recesso, licencas) que afetam
|
||
a sua agenda.
|
||
</span>
|
||
</div>
|
||
|
||
<div class="mbq-body">
|
||
<Teleport to="#mbq-mobile-drawer-target" :disabled="!isMobile">
|
||
<aside class="mbq-side">
|
||
<button class="mbq-cfg-btn" :class="{ 'is-open': cfgOpen }" @click="toggleCfg">
|
||
<i :class="cfgOpen ? 'pi pi-arrow-left' : 'pi pi-cog'" />
|
||
<span>{{ cfgOpen ? 'Voltar' : 'Configurações' }}</span>
|
||
<i v-if="!cfgOpen" class="pi pi-chevron-down mbq-cfg-btn__chev" />
|
||
</button>
|
||
<div v-if="cfgOpen" class="mbq-side__scroll mbq-side__scroll--cfg">
|
||
<MelissaConfigList @select="fecharCfg" />
|
||
</div>
|
||
<div v-else class="mbq-side__scroll">
|
||
<!-- Card: Resumo -->
|
||
<div class="mbq-w mbq-w--side">
|
||
<div class="mbq-w__head">
|
||
<div class="mbq-w__icon"><i class="pi pi-chart-pie" /></div>
|
||
<div class="mbq-w__title">
|
||
<div class="mbq-w__title-text">Resumo</div>
|
||
<div class="mbq-w__sub">Quantidades e ano</div>
|
||
</div>
|
||
</div>
|
||
<div class="mbq-w__body">
|
||
<div class="mbq-stats">
|
||
<div class="mbq-stat mbq-stat--blue">
|
||
<div class="mbq-stat__val">{{ nacionais.length }}</div>
|
||
<div class="mbq-stat__lbl">Nacionais</div>
|
||
</div>
|
||
<div class="mbq-stat mbq-stat--orange">
|
||
<div class="mbq-stat__val">{{ municipais.length }}</div>
|
||
<div class="mbq-stat__lbl">Municipais</div>
|
||
</div>
|
||
<div class="mbq-stat mbq-stat--red">
|
||
<div class="mbq-stat__val">{{ bloqueios.length }}</div>
|
||
<div class="mbq-stat__lbl">Bloqueios</div>
|
||
</div>
|
||
</div>
|
||
<div class="mbq-yearnav">
|
||
<button class="mbq-btn mbq-btn--icon" :disabled="loading" @click="anoAnterior">
|
||
<i class="pi pi-chevron-left" />
|
||
</button>
|
||
<span class="mbq-yearnav__label">{{ ano }}</span>
|
||
<button class="mbq-btn mbq-btn--icon" :disabled="loading" @click="anoProximo">
|
||
<i class="pi pi-chevron-right" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Card: Acoes -->
|
||
<div class="mbq-w mbq-w--side">
|
||
<div class="mbq-w__head">
|
||
<div class="mbq-w__icon"><i class="pi pi-plus" /></div>
|
||
<div class="mbq-w__title">
|
||
<div class="mbq-w__title-text">Adicionar</div>
|
||
<div class="mbq-w__sub">Feriado municipal ou bloqueio</div>
|
||
</div>
|
||
</div>
|
||
<div class="mbq-w__body">
|
||
<button class="mbq-btn mbq-btn--full" @click="abrirFeriadoMunicipal">
|
||
<i class="pi pi-map-marker" />
|
||
<span>Feriado municipal</span>
|
||
</button>
|
||
<button class="mbq-btn mbq-btn--primary mbq-btn--full" @click="abrirAddBloqueio">
|
||
<i class="pi pi-ban" />
|
||
<span>Novo bloqueio</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Card: Como funciona -->
|
||
<div class="mbq-w mbq-w--side">
|
||
<div class="mbq-w__head">
|
||
<div class="mbq-w__icon"><i class="pi pi-question-circle" /></div>
|
||
<div class="mbq-w__title">
|
||
<div class="mbq-w__title-text">Como funciona</div>
|
||
<div class="mbq-w__sub">Tipos de impedimento</div>
|
||
</div>
|
||
</div>
|
||
<div class="mbq-w__body">
|
||
<ul class="mbq-faq">
|
||
<li class="mbq-faq__item">
|
||
<span class="mbq-faq__bullet" style="background: #3b82f6" />
|
||
<div>
|
||
<strong>Nacionais:</strong>
|
||
gerados automaticamente (Carnaval, Pascoa, Natal etc).
|
||
</div>
|
||
</li>
|
||
<li class="mbq-faq__item">
|
||
<span class="mbq-faq__bullet" style="background: #f97316" />
|
||
<div>
|
||
<strong>Municipais:</strong>
|
||
voce cadastra (aniversario da cidade, padroeiro etc).
|
||
</div>
|
||
</li>
|
||
<li class="mbq-faq__item">
|
||
<span class="mbq-faq__bullet" style="background: #ef4444" />
|
||
<div>
|
||
<strong>Bloqueios:</strong>
|
||
ferias, recesso, licenca, ou janela horaria especifica.
|
||
Pode ser recorrente.
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
</Teleport>
|
||
|
||
<div class="mbq-main">
|
||
<!-- Loading -->
|
||
<template v-if="loading">
|
||
<div class="mbq-w" v-for="n in 3" :key="`sk-${n}`">
|
||
<div class="mbq-w__body">
|
||
<Skeleton width="40%" height="20px" class="mb-3" />
|
||
<Skeleton v-for="m in 4" :key="`sk-${n}-${m}`" width="100%" height="32px" class="mb-2" />
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else>
|
||
<!-- ── Feriados Nacionais ── -->
|
||
<div class="mbq-w">
|
||
<div class="mbq-w__head">
|
||
<div class="mbq-w__icon mbq-w__icon--blue"><i class="pi pi-flag" /></div>
|
||
<div class="mbq-w__title">
|
||
<div class="mbq-w__title-text">Feriados Nacionais</div>
|
||
<div class="mbq-w__sub">Gerados automaticamente</div>
|
||
</div>
|
||
<span v-if="nacionais.length" class="mbq-w__count">{{ nacionais.length }}</span>
|
||
</div>
|
||
<div class="mbq-w__body">
|
||
<div v-if="!nacionais.length" class="mbq-empty">
|
||
<i class="pi pi-info-circle" />
|
||
<span>Nenhum feriado nacional para {{ ano }}.</span>
|
||
</div>
|
||
<div v-else class="mbq-list">
|
||
<div
|
||
v-for="f in nacionais"
|
||
:key="f.data + f.nome"
|
||
class="mbq-item mbq-item--nat"
|
||
>
|
||
<div class="mbq-item__date">{{ fmtDateShort(f.data) }}</div>
|
||
<div class="mbq-item__title">{{ f.nome }}</div>
|
||
<Tag
|
||
v-if="f.movel"
|
||
value="Móvel"
|
||
severity="secondary"
|
||
class="mbq-item__tag"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Feriados Municipais ── -->
|
||
<div class="mbq-w">
|
||
<div class="mbq-w__head">
|
||
<div class="mbq-w__icon mbq-w__icon--orange"><i class="pi pi-map-marker" /></div>
|
||
<div class="mbq-w__title">
|
||
<div class="mbq-w__title-text">Feriados Municipais</div>
|
||
<div class="mbq-w__sub">Cadastrados manualmente</div>
|
||
</div>
|
||
<button class="mbq-w__action" @click="abrirFeriadoMunicipal">
|
||
<i class="pi pi-plus" />
|
||
<span>Adicionar</span>
|
||
</button>
|
||
</div>
|
||
<div class="mbq-w__body">
|
||
<div v-if="!municipais.length" class="mbq-empty">
|
||
<i class="pi pi-map-marker" />
|
||
<span>Nenhum feriado municipal cadastrado para {{ ano }}.</span>
|
||
</div>
|
||
<div v-else class="mbq-list">
|
||
<div
|
||
v-for="f in municipais"
|
||
:key="f.id"
|
||
class="mbq-item mbq-item--mun"
|
||
>
|
||
<div class="mbq-item__date">{{ fmtDate(f.data) }}</div>
|
||
<div class="mbq-item__title">
|
||
{{ f.nome }}
|
||
<span v-if="f.observacao" class="mbq-item__obs">— {{ f.observacao }}</span>
|
||
</div>
|
||
<button
|
||
class="mbq-icon-btn mbq-icon-btn--danger"
|
||
v-tooltip.left="'Remover'"
|
||
@click="excluirFeriado(f.id)"
|
||
>
|
||
<i class="pi pi-trash" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Bloqueios ── -->
|
||
<div class="mbq-w mbq-w--full">
|
||
<div class="mbq-w__head">
|
||
<div class="mbq-w__icon mbq-w__icon--red"><i class="pi pi-ban" /></div>
|
||
<div class="mbq-w__title">
|
||
<div class="mbq-w__title-text">Bloqueios</div>
|
||
<div class="mbq-w__sub">Férias, recesso, licença ou janela horária</div>
|
||
</div>
|
||
<button class="mbq-w__action" @click="abrirAddBloqueio">
|
||
<i class="pi pi-plus" />
|
||
<span>Novo</span>
|
||
</button>
|
||
</div>
|
||
<div class="mbq-w__body">
|
||
<div v-if="!bloqueios.length" class="mbq-empty">
|
||
<i class="pi pi-ban" />
|
||
<span>Nenhum bloqueio cadastrado para {{ ano }}.</span>
|
||
</div>
|
||
<div v-else class="mbq-list">
|
||
<div
|
||
v-for="b in bloqueios"
|
||
:key="b.id"
|
||
class="mbq-item mbq-item--blk"
|
||
>
|
||
<div class="mbq-item__date">{{ fmtPeriodo(b) }}</div>
|
||
<div class="mbq-item__title">
|
||
{{ b.titulo }}
|
||
<Tag
|
||
v-if="b.recorrente"
|
||
value="Recorrente"
|
||
severity="warn"
|
||
class="mbq-item__tag"
|
||
/>
|
||
<span v-if="b.observacao" class="mbq-item__obs">— {{ b.observacao }}</span>
|
||
</div>
|
||
<div class="mbq-item__actions">
|
||
<button
|
||
class="mbq-icon-btn"
|
||
v-tooltip.top="'Editar'"
|
||
@click="abrirEditBloqueio(b)"
|
||
>
|
||
<i class="pi pi-pencil" />
|
||
</button>
|
||
<button
|
||
class="mbq-icon-btn mbq-icon-btn--danger"
|
||
v-tooltip.top="'Remover'"
|
||
@click="excluirBloqueio(b.id)"
|
||
>
|
||
<i class="pi pi-trash" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Dialog feriado municipal -->
|
||
<Dialog
|
||
v-model:visible="fdlgOpen"
|
||
modal
|
||
:draggable="false"
|
||
pt:mask:class="backdrop-blur-xs"
|
||
header="Cadastrar feriado municipal"
|
||
:style="{ width: '460px', maxWidth: '95vw' }"
|
||
>
|
||
<div class="mbq-dlg">
|
||
<div class="mbq-dlg__field">
|
||
<label class="mbq-dlg__label">Nome do feriado *</label>
|
||
<InputText
|
||
v-model="fform.nome"
|
||
class="w-full"
|
||
placeholder="Ex.: Aniversário da cidade, Padroeiro…"
|
||
/>
|
||
</div>
|
||
<div class="mbq-dlg__field">
|
||
<label class="mbq-dlg__label">Data *</label>
|
||
<DatePicker
|
||
v-model="fform.data"
|
||
showIcon
|
||
fluid
|
||
iconDisplay="input"
|
||
dateFormat="dd/mm/yy"
|
||
:manualInput="false"
|
||
>
|
||
<template #inputicon="sp">
|
||
<i class="pi pi-calendar" @click="sp.clickCallback" />
|
||
</template>
|
||
</DatePicker>
|
||
</div>
|
||
<div class="mbq-dlg__field">
|
||
<label class="mbq-dlg__label">Observação <span class="mbq-dlg__optional">(opcional)</span></label>
|
||
<Textarea
|
||
v-model="fform.observacao"
|
||
class="w-full"
|
||
rows="2"
|
||
autoResize
|
||
placeholder="Nota interna…"
|
||
/>
|
||
</div>
|
||
<div v-if="fformDuplicate" class="mbq-dlg__warn">
|
||
<i class="pi pi-exclamation-triangle" />
|
||
Já existe um feriado com esse nome nessa data.
|
||
</div>
|
||
</div>
|
||
<template #footer>
|
||
<button class="mbq-btn" @click="fdlgOpen = false">
|
||
<span>Cancelar</span>
|
||
</button>
|
||
<button
|
||
class="mbq-btn mbq-btn--primary"
|
||
:disabled="!fformValid || fformDuplicate"
|
||
@click="salvarFeriado"
|
||
>
|
||
<i :class="fsaving ? 'pi pi-spin pi-spinner' : 'pi pi-check'" />
|
||
<span>Cadastrar</span>
|
||
</button>
|
||
</template>
|
||
</Dialog>
|
||
|
||
<!-- Dialog bloqueio -->
|
||
<Dialog
|
||
v-model:visible="dlgOpen"
|
||
modal
|
||
:draggable="false"
|
||
pt:mask:class="backdrop-blur-xs"
|
||
:header="dlgMode === 'edit' ? 'Editar bloqueio' : 'Novo bloqueio'"
|
||
:style="{ width: '500px', maxWidth: '95vw' }"
|
||
>
|
||
<div class="mbq-dlg">
|
||
<div class="mbq-dlg__field">
|
||
<label class="mbq-dlg__label">Título *</label>
|
||
<InputText
|
||
v-model="form.titulo"
|
||
class="w-full"
|
||
placeholder="Ex.: Recesso, Férias, Licença…"
|
||
/>
|
||
</div>
|
||
<div class="mbq-dlg__row">
|
||
<div class="mbq-dlg__field">
|
||
<label class="mbq-dlg__label">Data início *</label>
|
||
<DatePicker
|
||
v-model="form.data_inicio"
|
||
showIcon
|
||
fluid
|
||
iconDisplay="input"
|
||
dateFormat="dd/mm/yy"
|
||
:manualInput="false"
|
||
>
|
||
<template #inputicon="sp">
|
||
<i class="pi pi-calendar" @click="sp.clickCallback" />
|
||
</template>
|
||
</DatePicker>
|
||
</div>
|
||
<div class="mbq-dlg__field">
|
||
<label class="mbq-dlg__label">Data fim <span class="mbq-dlg__optional">(opcional)</span></label>
|
||
<DatePicker
|
||
v-model="form.data_fim"
|
||
showIcon
|
||
fluid
|
||
iconDisplay="input"
|
||
dateFormat="dd/mm/yy"
|
||
:manualInput="false"
|
||
:minDate="form.data_inicio || undefined"
|
||
>
|
||
<template #inputicon="sp">
|
||
<i class="pi pi-calendar" @click="sp.clickCallback" />
|
||
</template>
|
||
</DatePicker>
|
||
</div>
|
||
</div>
|
||
<div class="mbq-dlg__row">
|
||
<div class="mbq-dlg__field">
|
||
<label class="mbq-dlg__label">Hora início <span class="mbq-dlg__optional">(opcional)</span></label>
|
||
<DatePicker
|
||
v-model="form.hora_inicio"
|
||
showIcon
|
||
fluid
|
||
iconDisplay="input"
|
||
timeOnly
|
||
hourFormat="24"
|
||
:stepMinute="15"
|
||
:manualInput="false"
|
||
>
|
||
<template #inputicon="sp">
|
||
<i class="pi pi-clock" @click="sp.clickCallback" />
|
||
</template>
|
||
</DatePicker>
|
||
</div>
|
||
<div class="mbq-dlg__field">
|
||
<label class="mbq-dlg__label">Hora fim <span class="mbq-dlg__optional">(opcional)</span></label>
|
||
<DatePicker
|
||
v-model="form.hora_fim"
|
||
showIcon
|
||
fluid
|
||
iconDisplay="input"
|
||
timeOnly
|
||
hourFormat="24"
|
||
:stepMinute="15"
|
||
:manualInput="false"
|
||
>
|
||
<template #inputicon="sp">
|
||
<i class="pi pi-clock" @click="sp.clickCallback" />
|
||
</template>
|
||
</DatePicker>
|
||
</div>
|
||
</div>
|
||
<div class="mbq-dlg__field">
|
||
<label class="mbq-dlg__label">Observação <span class="mbq-dlg__optional">(opcional)</span></label>
|
||
<Textarea
|
||
v-model="form.observacao"
|
||
class="w-full"
|
||
rows="2"
|
||
autoResize
|
||
/>
|
||
</div>
|
||
</div>
|
||
<template #footer>
|
||
<button class="mbq-btn" @click="dlgOpen = false">
|
||
<span>Cancelar</span>
|
||
</button>
|
||
<button
|
||
class="mbq-btn mbq-btn--primary"
|
||
:disabled="!formValid"
|
||
@click="salvarBloqueio"
|
||
>
|
||
<i :class="saving ? 'pi pi-spin pi-spinner' : 'pi pi-check'" />
|
||
<span>{{ dlgMode === 'edit' ? 'Salvar' : 'Adicionar' }}</span>
|
||
</button>
|
||
</template>
|
||
</Dialog>
|
||
</section>
|
||
</template>
|
||
|
||
<style scoped>
|
||
/* ═══════ Page chrome ═══════ */
|
||
.mbq-page {
|
||
position: absolute;
|
||
inset: 6px 6px calc(var(--m-dock-h, 76px) + 6px) var(--m-config-aside-left, 6px);
|
||
z-index: 40;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: var(--m-bg-medium);
|
||
backdrop-filter: blur(32px) saturate(160%);
|
||
-webkit-backdrop-filter: blur(32px) saturate(160%);
|
||
border: 1px solid var(--m-border);
|
||
border-radius: 18px;
|
||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4);
|
||
overflow: hidden;
|
||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||
color: var(--m-text);
|
||
animation: mbq-page-enter 240ms cubic-bezier(0.2, 0.7, 0.3, 1);
|
||
}
|
||
@keyframes mbq-page-enter {
|
||
from { opacity: 0; transform: scale(0.985); }
|
||
to { opacity: 1; transform: scale(1); }
|
||
}
|
||
|
||
.mbq-page__head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 14px 18px;
|
||
border-bottom: 1px solid var(--m-border);
|
||
flex-shrink: 0;
|
||
gap: 10px;
|
||
}
|
||
.mbq-page__title {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
font-size: 1rem;
|
||
font-weight: 700;
|
||
color: var(--m-text);
|
||
}
|
||
.mbq-page__title-icon { color: var(--p-primary-color); font-size: 1.05rem; }
|
||
.mbq-page__year {
|
||
margin-left: auto;
|
||
background: color-mix(in srgb, var(--p-primary-color) 18%, transparent);
|
||
color: var(--p-primary-color);
|
||
border-radius: 999px;
|
||
padding: 3px 10px;
|
||
font-size: 0.74rem;
|
||
font-weight: 700;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.mbq-page__actions { display: flex; align-items: center; gap: 6px; }
|
||
|
||
.mbq-act-btn {
|
||
width: 32px;
|
||
height: 32px;
|
||
display: grid;
|
||
place-items: center;
|
||
background: transparent;
|
||
border: 1px solid var(--m-border);
|
||
color: var(--m-text-muted);
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: background-color 120ms ease, color 120ms ease;
|
||
}
|
||
.mbq-act-btn:hover { background: var(--m-bg-soft-hover); color: var(--m-text); }
|
||
.mbq-act-btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
||
.mbq-close {
|
||
width: 32px;
|
||
height: 32px;
|
||
display: grid;
|
||
place-items: center;
|
||
background: transparent;
|
||
border: 1px solid var(--m-border);
|
||
color: var(--m-text-muted);
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: background-color 120ms ease, color 120ms ease;
|
||
}
|
||
.mbq-close:hover { background: var(--m-bg-soft-hover); color: var(--m-text); }
|
||
.mbq-menu-btn {
|
||
display: none;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 7px 12px;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--m-border);
|
||
background: var(--m-bg-soft);
|
||
color: var(--m-text);
|
||
cursor: pointer;
|
||
font-size: 0.82rem;
|
||
font-weight: 600;
|
||
flex-shrink: 0;
|
||
}
|
||
.mbq-menu-btn:hover { background: var(--m-bg-soft-hover); }
|
||
|
||
.mbq-subheader {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
padding: 10px 18px;
|
||
background: var(--m-bg-soft);
|
||
border-bottom: 1px solid var(--m-border);
|
||
color: var(--m-text-muted);
|
||
font-size: 0.78rem;
|
||
flex-shrink: 0;
|
||
}
|
||
.mbq-subheader__icon {
|
||
color: var(--p-primary-color);
|
||
font-size: 0.85rem;
|
||
margin-top: 2px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* ═══════ Body 2-col ═══════ */
|
||
.mbq-body {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
overflow: hidden;
|
||
}
|
||
.mbq-side {
|
||
width: 320px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: var(--m-bg-soft);
|
||
border-right: 1px solid var(--m-border);
|
||
overflow: hidden;
|
||
}
|
||
.mbq-side__scroll {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
padding: 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
scrollbar-width: thin;
|
||
scrollbar-color: var(--m-border-strong) transparent;
|
||
}
|
||
.mbq-side__scroll::-webkit-scrollbar { width: 5px; }
|
||
.mbq-side__scroll::-webkit-scrollbar-thumb {
|
||
background: var(--m-border-strong);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
/* Botao "Configuracoes" no topo da .mbq-side. Click alterna entre
|
||
cards (default) e lista de configs (estado is-open: vira "Voltar"). */
|
||
.mbq-cfg-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
width: calc(100% - 24px);
|
||
margin: 12px 12px 0;
|
||
padding: 10px 12px;
|
||
background: var(--m-bg-medium);
|
||
border: 1px solid var(--m-border);
|
||
border-radius: 9px;
|
||
color: var(--m-text);
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
font-size: 0.84rem;
|
||
font-weight: 600;
|
||
text-align: left;
|
||
transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
|
||
flex-shrink: 0;
|
||
}
|
||
.mbq-cfg-btn:hover {
|
||
background: var(--m-bg-soft-hover);
|
||
border-color: var(--m-border-strong);
|
||
}
|
||
.mbq-cfg-btn.is-open {
|
||
background: color-mix(in srgb, var(--p-primary-color) 14%, transparent);
|
||
border-color: color-mix(in srgb, var(--p-primary-color) 38%, transparent);
|
||
color: var(--p-primary-color);
|
||
}
|
||
.mbq-cfg-btn > i:first-child {
|
||
color: var(--p-primary-color);
|
||
font-size: 0.92rem;
|
||
}
|
||
.mbq-cfg-btn > span { flex: 1; }
|
||
.mbq-cfg-btn__chev {
|
||
color: var(--m-text-muted);
|
||
font-size: 0.7rem;
|
||
}
|
||
.mbq-side__scroll--cfg {
|
||
padding: 8px;
|
||
gap: 0;
|
||
}
|
||
|
||
.mbq-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
padding: 16px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
scrollbar-width: thin;
|
||
scrollbar-color: var(--m-border-strong) transparent;
|
||
}
|
||
.mbq-main::-webkit-scrollbar { width: 5px; }
|
||
.mbq-main::-webkit-scrollbar-thumb {
|
||
background: var(--m-border-strong);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
/* Desktop (>=1024): main em grid 2-col, Bloqueios spans 2 cols.
|
||
Cards min-h 300 + max-h 100% + body scroll (lições aprendidas). */
|
||
@media (min-width: 1024px) {
|
||
.mbq-main {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 12px;
|
||
align-items: start;
|
||
align-content: start;
|
||
}
|
||
.mbq-main > .mbq-w {
|
||
min-height: 300px;
|
||
max-height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.mbq-main > .mbq-w--full { grid-column: 1 / -1; }
|
||
.mbq-side > .mbq-side__scroll > .mbq-w--side {
|
||
min-height: 300px;
|
||
max-height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.mbq-main > .mbq-w > .mbq-w__body,
|
||
.mbq-side .mbq-w--side > .mbq-w__body {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
scrollbar-width: thin;
|
||
scrollbar-color: var(--m-border-strong) transparent;
|
||
}
|
||
.mbq-main > .mbq-w > .mbq-w__body::-webkit-scrollbar,
|
||
.mbq-side .mbq-w--side > .mbq-w__body::-webkit-scrollbar { width: 5px; }
|
||
.mbq-main > .mbq-w > .mbq-w__body::-webkit-scrollbar-thumb,
|
||
.mbq-side .mbq-w--side > .mbq-w__body::-webkit-scrollbar-thumb {
|
||
background: var(--m-border-strong);
|
||
border-radius: 3px;
|
||
}
|
||
}
|
||
|
||
/* ═══════ Card-base ═══════ */
|
||
.mbq-w {
|
||
background: var(--m-bg-soft);
|
||
border: 1px solid var(--m-border);
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.mbq-w--side {
|
||
background: var(--m-bg-medium);
|
||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16);
|
||
}
|
||
.mbq-w__head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 12px 14px;
|
||
border-bottom: 1px solid var(--m-border);
|
||
}
|
||
.mbq-w__icon {
|
||
width: 36px;
|
||
height: 36px;
|
||
display: grid;
|
||
place-items: center;
|
||
border-radius: 9px;
|
||
flex-shrink: 0;
|
||
background: color-mix(in srgb, var(--p-primary-color) 15%, transparent);
|
||
color: var(--p-primary-color);
|
||
}
|
||
.mbq-w__icon > i { font-size: 0.95rem; }
|
||
.mbq-w__icon--blue {
|
||
background: color-mix(in srgb, #3b82f6 15%, transparent);
|
||
color: #3b82f6;
|
||
}
|
||
.mbq-w__icon--orange {
|
||
background: color-mix(in srgb, #f97316 15%, transparent);
|
||
color: #f97316;
|
||
}
|
||
.mbq-w__icon--red {
|
||
background: color-mix(in srgb, #ef4444 15%, transparent);
|
||
color: #ef4444;
|
||
}
|
||
.mbq-w__title { flex: 1; min-width: 0; }
|
||
.mbq-w__title-text {
|
||
font-size: 0.92rem;
|
||
font-weight: 700;
|
||
color: var(--m-text);
|
||
line-height: 1.2;
|
||
}
|
||
.mbq-w__sub {
|
||
font-size: 0.74rem;
|
||
color: var(--m-text-muted);
|
||
margin-top: 2px;
|
||
line-height: 1.3;
|
||
}
|
||
.mbq-w__count {
|
||
flex-shrink: 0;
|
||
background: color-mix(in srgb, var(--p-primary-color) 18%, transparent);
|
||
color: var(--p-primary-color);
|
||
border-radius: 999px;
|
||
padding: 3px 10px;
|
||
font-size: 0.74rem;
|
||
font-weight: 700;
|
||
}
|
||
.mbq-w__action {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 5px 10px;
|
||
border-radius: 7px;
|
||
border: 1px solid var(--m-border);
|
||
background: var(--m-bg-medium);
|
||
color: var(--m-text);
|
||
cursor: pointer;
|
||
font-size: 0.74rem;
|
||
font-weight: 600;
|
||
flex-shrink: 0;
|
||
transition: background-color 120ms ease;
|
||
}
|
||
.mbq-w__action:hover { background: var(--m-bg-soft-hover); }
|
||
.mbq-w__action > i { font-size: 0.72rem; color: var(--p-primary-color); }
|
||
.mbq-w__body {
|
||
padding: 14px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
/* ═══════ Sidebar: Resumo (mini stats) ═══════ */
|
||
.mbq-stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 8px;
|
||
}
|
||
.mbq-stat {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 2px;
|
||
padding: 10px 6px;
|
||
border-radius: 9px;
|
||
background: var(--m-bg-soft);
|
||
border: 1px solid var(--m-border);
|
||
text-align: center;
|
||
}
|
||
.mbq-stat__val {
|
||
font-size: 1.2rem;
|
||
font-weight: 800;
|
||
line-height: 1.1;
|
||
}
|
||
.mbq-stat__lbl {
|
||
font-size: 0.66rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
color: var(--m-text-muted);
|
||
}
|
||
.mbq-stat--blue .mbq-stat__val { color: #3b82f6; }
|
||
.mbq-stat--orange .mbq-stat__val { color: #f97316; }
|
||
.mbq-stat--red .mbq-stat__val { color: #ef4444; }
|
||
|
||
.mbq-yearnav {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
}
|
||
.mbq-yearnav__label {
|
||
flex: 1;
|
||
text-align: center;
|
||
font-size: 1.05rem;
|
||
font-weight: 800;
|
||
color: var(--m-text);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
/* ═══════ Sidebar: FAQ ═══════ */
|
||
.mbq-faq {
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
.mbq-faq__item {
|
||
display: flex;
|
||
gap: 10px;
|
||
font-size: 0.78rem;
|
||
color: var(--m-text-muted);
|
||
line-height: 1.4;
|
||
}
|
||
.mbq-faq__item > div { flex: 1; min-width: 0; }
|
||
.mbq-faq__item strong { color: var(--m-text); font-weight: 700; }
|
||
.mbq-faq__bullet {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
margin-top: 6px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* ═══════ Buttons ═══════ */
|
||
.mbq-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
padding: 8px 14px;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--m-border);
|
||
background: var(--m-bg-soft);
|
||
color: var(--m-text);
|
||
cursor: pointer;
|
||
font-size: 0.82rem;
|
||
font-weight: 600;
|
||
font-family: inherit;
|
||
transition: background-color 120ms ease, opacity 120ms ease;
|
||
}
|
||
.mbq-btn:hover { background: var(--m-bg-soft-hover); }
|
||
.mbq-btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
||
.mbq-btn--primary {
|
||
background: var(--p-primary-color);
|
||
border-color: var(--p-primary-color);
|
||
color: var(--p-primary-contrast-color, #fff);
|
||
}
|
||
.mbq-btn--primary:hover {
|
||
background: color-mix(in srgb, var(--p-primary-color) 88%, black);
|
||
}
|
||
.mbq-btn--full { width: 100%; }
|
||
.mbq-btn--icon {
|
||
width: 32px;
|
||
height: 32px;
|
||
padding: 0;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.mbq-icon-btn {
|
||
width: 28px;
|
||
height: 28px;
|
||
display: grid;
|
||
place-items: center;
|
||
background: transparent;
|
||
border: 1px solid var(--m-border);
|
||
border-radius: 6px;
|
||
color: var(--m-text-muted);
|
||
cursor: pointer;
|
||
flex-shrink: 0;
|
||
transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
|
||
}
|
||
.mbq-icon-btn:hover {
|
||
background: var(--m-bg-soft-hover);
|
||
color: var(--m-text);
|
||
}
|
||
.mbq-icon-btn--danger:hover {
|
||
background: rgba(220, 38, 38, 0.12);
|
||
color: rgb(220, 38, 38);
|
||
border-color: rgba(220, 38, 38, 0.40);
|
||
}
|
||
|
||
/* ═══════ Lists / Items ═══════ */
|
||
.mbq-empty {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 14px;
|
||
border-radius: 9px;
|
||
background: var(--m-bg-medium);
|
||
border: 1px dashed var(--m-border);
|
||
color: var(--m-text-muted);
|
||
font-size: 0.82rem;
|
||
}
|
||
.mbq-empty > i { color: var(--p-primary-color); font-size: 0.92rem; }
|
||
|
||
.mbq-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
.mbq-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 8px 10px;
|
||
border-radius: 8px;
|
||
background: var(--m-bg-medium);
|
||
border: 1px solid var(--m-border);
|
||
font-size: 0.82rem;
|
||
flex-wrap: wrap;
|
||
transition: background-color 120ms ease;
|
||
}
|
||
.mbq-item:hover { background: var(--m-bg-soft-hover); }
|
||
.mbq-item--nat { border-left: 3px solid #3b82f6; }
|
||
.mbq-item--mun { border-left: 3px solid #f97316; }
|
||
.mbq-item--blk { border-left: 3px solid #ef4444; }
|
||
.mbq-item__date {
|
||
font-size: 0.74rem;
|
||
color: var(--m-text-muted);
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
min-width: 80px;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.mbq-item__title {
|
||
flex: 1;
|
||
min-width: 0;
|
||
font-weight: 600;
|
||
color: var(--m-text);
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.mbq-item__obs {
|
||
font-size: 0.72rem;
|
||
font-weight: 400;
|
||
color: var(--m-text-muted);
|
||
font-style: italic;
|
||
}
|
||
.mbq-item__tag {
|
||
font-size: 0.66rem !important;
|
||
flex-shrink: 0;
|
||
}
|
||
.mbq-item__actions {
|
||
display: flex;
|
||
gap: 4px;
|
||
margin-left: auto;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* ═══════ Dialog ═══════ */
|
||
.mbq-dlg {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
padding-top: 4px;
|
||
}
|
||
.mbq-dlg__row {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
.mbq-dlg__row > .mbq-dlg__field { flex: 1; min-width: 0; }
|
||
.mbq-dlg__field {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
.mbq-dlg__label {
|
||
font-size: 0.78rem;
|
||
font-weight: 700;
|
||
color: var(--m-text);
|
||
}
|
||
.mbq-dlg__optional {
|
||
font-weight: 400;
|
||
color: var(--m-text-muted);
|
||
opacity: 0.7;
|
||
}
|
||
.mbq-dlg__warn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 8px 10px;
|
||
border-radius: 7px;
|
||
background: rgba(220, 38, 38, 0.10);
|
||
border: 1px solid rgba(220, 38, 38, 0.30);
|
||
color: rgb(220, 38, 38);
|
||
font-size: 0.78rem;
|
||
}
|
||
|
||
/* ═══════ Mobile drawer ═══════ */
|
||
.mbq-mobile-drawer {
|
||
position: fixed;
|
||
top: 0; left: 0;
|
||
height: 100vh;
|
||
height: 100dvh;
|
||
width: min(360px, 88vw);
|
||
z-index: 80;
|
||
background: var(--m-bg-medium);
|
||
backdrop-filter: blur(28px) saturate(160%);
|
||
-webkit-backdrop-filter: blur(28px) saturate(160%);
|
||
border-right: 1px solid var(--m-border);
|
||
transform: translateX(-100%);
|
||
transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||
color: var(--m-text);
|
||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.mbq-mobile-drawer.is-open { transform: translateX(0); }
|
||
.mbq-mobile-drawer__scroll {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
padding: 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
scrollbar-width: thin;
|
||
scrollbar-color: var(--m-border-strong) transparent;
|
||
}
|
||
.mbq-mobile-drawer__scroll::-webkit-scrollbar { width: 5px; }
|
||
.mbq-mobile-drawer__scroll::-webkit-scrollbar-thumb {
|
||
background: var(--m-border-strong);
|
||
border-radius: 3px;
|
||
}
|
||
/* No mobile a .mbq-side e teleportada pra dentro do drawer scroll. */
|
||
.mbq-mobile-drawer__scroll .mbq-side {
|
||
width: 100%;
|
||
border-right: none;
|
||
}
|
||
|
||
.mbq-mobile-drawer__backdrop {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.45);
|
||
backdrop-filter: blur(4px);
|
||
-webkit-backdrop-filter: blur(4px);
|
||
z-index: 79;
|
||
}
|
||
.mbq-drawer-fade-enter-active,
|
||
.mbq-drawer-fade-leave-active { transition: opacity 200ms ease; }
|
||
.mbq-drawer-fade-enter-from,
|
||
.mbq-drawer-fade-leave-to { opacity: 0; }
|
||
|
||
/* ═══════ Mobile (<1024px) ═══════ */
|
||
@media (max-width: 1023px) {
|
||
.mbq-body { flex-direction: column; padding: 0; }
|
||
.mbq-body > .mbq-side { display: none; }
|
||
.mbq-main { width: 100%; padding: 8px; }
|
||
.mbq-main .mbq-w {
|
||
height: auto;
|
||
flex: 0 0 auto;
|
||
align-self: stretch;
|
||
}
|
||
.mbq-page__title > span:first-of-type { display: none; }
|
||
.mbq-page__title-icon { display: none; }
|
||
.mbq-menu-btn--mobile-only { display: inline-flex; }
|
||
.mbq-dlg__row { flex-direction: column; gap: 14px; }
|
||
.mbq-item { padding: 10px; }
|
||
}
|
||
</style>
|