a7f6bcbe66
- useTenantDb composable + lib/supabase/tenantClient (tenantDb/tenantSchemaName)
- tenantStore: getters activeTenantSlug/activeTenantSchema; my_tenants() RPC
passa a devolver slug+nome (migration 07)
- codemod scripts/codemod-tenant-db.py: supabase.from('<84 tabelas + 6 views
tenant>') -> tenantDb().from(...) em 139 arquivos (777 chamadas), remove
.eq('tenant_id') das cadeias tenant (173)
- passada manual (4 agentes): remove tenant_id de payloads insert/upsert/update,
selects, .or/.is de defaults; onConflict ajustado pros uniques sem tenant_id
(singletons usam 'singleton'); realtime de tabelas tenant aponta pro schema
do tenant ativo; repos dropam tenant_id defensivamente de payloads externos
- agendaSelects: tenant_id fora do AGENDA_EVENT_SELECT (quebraria PostgREST)
- zero embeds cross-schema (todos FK embeds sao tenant->tenant ou global->global)
- build de producao passa; 67 .js checados
Pendente (fora do escopo F3, sao cross-tenant/anon -> F4/F6):
- AgendadorPublicoPage (anon, resolve tenant por link_slug)
- Saas{Feriados,NotificationTemplates,DocumentTemplates,Whatsapp}Page
(gerenciam defaults do sistema / views cross-tenant)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2299 lines
86 KiB
Vue
2299 lines
86 KiB
Vue
<script setup>
|
||
/*
|
||
* MelissaAgendaConfig — Pagina nativa Melissa pra "Configuracoes da Agenda".
|
||
*
|
||
* Substitui o embed cfg-agenda que vivia dentro do MelissaConfiguracoes.
|
||
* Layout 2-col:
|
||
* - COL 1 (sidebar) — Card "Status do setup" (3 mini stats: Jornada,
|
||
* Ritmo, Online com cor verde/amber/red) + Card "Atalhos" (3
|
||
* atalhos pra ancoras) + Card "Como funciona" (FAQ)
|
||
* - COL 2 (main) — 3 cards stackados: Jornada (fuso + dias +
|
||
* horarios igual/diferente + pausas) + Ritmo (presets + custom)
|
||
* + Online (toggle + slots por dia com periodos)
|
||
*
|
||
* Logica espelhada do ConfiguracoesAgendaPage.vue (tabelas
|
||
* agenda_configuracoes + agenda_regras_semanais + agenda_online_slots).
|
||
*
|
||
* SKIP: FullCalendar preview (visita a /melissa/agenda real).
|
||
*/
|
||
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
|
||
import MelissaConfigList from './MelissaConfigList.vue';
|
||
import { useToast } from 'primevue/usetoast';
|
||
import { supabase } from '@/lib/supabase/client';
|
||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||
import { useTenantStore } from '@/stores/tenantStore';
|
||
import PausasChipsEditor from '@/components/agenda/PausasChipsEditor.vue';
|
||
// DatePicker/Select/Skeleton/Tag/ToggleSwitch: auto via PrimeVueResolver
|
||
|
||
const emit = defineEmits(['close']);
|
||
|
||
const toast = useToast();
|
||
const tenantStore = useTenantStore();
|
||
|
||
// ── 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 loading = ref(true);
|
||
const hydrating = ref(true);
|
||
const ownerId = ref(null);
|
||
const savingCard = ref(null);
|
||
|
||
const cfg = ref({
|
||
owner_id: null,
|
||
agenda_view_mode: 'work_hours',
|
||
agenda_custom_start: null,
|
||
agenda_custom_end: null,
|
||
session_duration_min: 50,
|
||
session_break_min: 10,
|
||
pausas_semanais: [],
|
||
online_ativo: false,
|
||
setup_clinica_concluido: false,
|
||
setup_clinica_concluido_em: null,
|
||
jornada_igual_todos: true,
|
||
timezone: 'America/Sao_Paulo',
|
||
is_conveniado: false
|
||
});
|
||
|
||
const timezones = [
|
||
{ label: 'Brasília (BRT)', value: 'America/Sao_Paulo' },
|
||
{ label: 'Manaus (AMT)', value: 'America/Manaus' },
|
||
{ label: 'Fortaleza (BRT)', value: 'America/Fortaleza' },
|
||
{ label: 'Belém (BRT)', value: 'America/Belem' },
|
||
{ label: 'Rio Branco (ACT)', value: 'America/Rio_Branco' },
|
||
{ label: 'Noronha (FNT)', value: 'America/Noronha' },
|
||
{ label: 'Lisboa (WET)', value: 'Europe/Lisbon' }
|
||
];
|
||
|
||
const regras = ref([]);
|
||
const workDays = ref({ 1: false, 2: false, 3: false, 4: false, 5: false, 6: false, 0: false });
|
||
const jornadaStart = ref('08:00');
|
||
const jornadaEnd = ref('18:00');
|
||
const jornadaIgualTodos = ref(true);
|
||
|
||
const jornadaPorDia = ref({
|
||
1: { inicio: '08:00', fim: '18:00' },
|
||
2: { inicio: '08:00', fim: '18:00' },
|
||
3: { inicio: '08:00', fim: '18:00' },
|
||
4: { inicio: '08:00', fim: '18:00' },
|
||
5: { inicio: '08:00', fim: '18:00' },
|
||
6: { inicio: '08:00', fim: '18:00' },
|
||
0: { inicio: '08:00', fim: '18:00' }
|
||
});
|
||
const jornadaPorDiaSnapshot = ref(null);
|
||
|
||
const pausasGlobais = ref([]);
|
||
const pausasPorDia = ref({ 1: [], 2: [], 3: [], 4: [], 5: [], 6: [], 0: [] });
|
||
|
||
const onlineSlotsByDay = ref({ 1: new Set(), 2: new Set(), 3: new Set(), 4: new Set(), 5: new Set(), 6: new Set(), 0: new Set() });
|
||
|
||
function resetOnlineSlots() {
|
||
onlineSlotsByDay.value = { 1: new Set(), 2: new Set(), 3: new Set(), 4: new Set(), 5: new Set(), 6: new Set(), 0: new Set() };
|
||
}
|
||
|
||
const showAdvancedRitmo = ref(false);
|
||
const previewDay = ref(null);
|
||
|
||
// ── Constantes / helpers ───────────────────────────────────
|
||
const diasSemana = [
|
||
{ label: 'Segunda', short: 'Seg', value: 1 },
|
||
{ label: 'Terça', short: 'Ter', value: 2 },
|
||
{ label: 'Quarta', short: 'Qua', value: 3 },
|
||
{ label: 'Quinta', short: 'Qui', value: 4 },
|
||
{ label: 'Sexta', short: 'Sex', value: 5 },
|
||
{ label: 'Sábado', short: 'Sáb', value: 6 },
|
||
{ label: 'Domingo', short: 'Dom', value: 0 }
|
||
];
|
||
|
||
const selectedDays = computed(() => diasSemana.filter((d) => !!workDays.value[d.value]));
|
||
const selectedWeekdays = computed(() => diasSemana.filter((d) => d.value >= 1 && d.value <= 5 && !!workDays.value[d.value]));
|
||
|
||
function hhmmToMin(hhmm) {
|
||
const [h, m] = String(hhmm).split(':').map(Number);
|
||
return h * 60 + m;
|
||
}
|
||
function minToHHMM(min) {
|
||
const h = Math.floor(min / 60) % 24;
|
||
const m = min % 60;
|
||
return String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0');
|
||
}
|
||
function isValidHHMM(v) {
|
||
if (!v) return false;
|
||
return /^\d{2}:\d{2}$/.test(String(v).trim());
|
||
}
|
||
function normalizeTime(v) {
|
||
if (v == null || v === '') return null;
|
||
const s = String(v).trim();
|
||
if (/^\d{2}:\d{2}$/.test(s)) return s + ':00';
|
||
if (/^\d{2}:\d{2}:\d{2}$/.test(s)) return s;
|
||
return s;
|
||
}
|
||
function newId() {
|
||
return Math.random().toString(16).slice(2) + Date.now().toString(16);
|
||
}
|
||
function dateToHHMM(date) {
|
||
if (!date) return null;
|
||
const d = date instanceof Date ? date : new Date(date);
|
||
if (Number.isNaN(d.getTime())) return null;
|
||
return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
|
||
}
|
||
function hhmmToDate(hhmm) {
|
||
if (!isValidHHMM(hhmm)) return null;
|
||
const [h, m] = String(hhmm).split(':').map(Number);
|
||
const d = new Date();
|
||
d.setHours(h, m, 0, 0);
|
||
return d;
|
||
}
|
||
|
||
// ── Resumos ────────────────────────────────────────────────
|
||
const resumoJornada = computed(() => {
|
||
const days = selectedDays.value;
|
||
if (!days.length) return 'Nenhum dia configurado';
|
||
const labels = days.map((d) => d.short).join('·');
|
||
if (jornadaIgualTodos.value !== false) {
|
||
return `${labels} · ${jornadaStart.value}–${jornadaEnd.value}`;
|
||
}
|
||
return `${labels} · horários variados`;
|
||
});
|
||
|
||
const jornadaOk = computed(
|
||
() =>
|
||
selectedDays.value.length > 0 &&
|
||
isValidHHMM(jornadaStart.value) &&
|
||
isValidHHMM(jornadaEnd.value) &&
|
||
jornadaEnd.value > jornadaStart.value
|
||
);
|
||
|
||
const resumoRitmo = computed(() => {
|
||
const d = cfg.value.session_duration_min || 50;
|
||
const i = cfg.value.session_break_min || 0;
|
||
return `${d}min sessão · ${i > 0 ? `${i}min intervalo` : 'sem intervalo'}`;
|
||
});
|
||
|
||
const resumoOnline = computed(() => {
|
||
if (!cfg.value.online_ativo) return 'Desativado';
|
||
let total = 0;
|
||
let dias = 0;
|
||
for (const k of Object.keys(onlineSlotsByDay.value)) {
|
||
const size = onlineSlotsByDay.value[k]?.size || 0;
|
||
total += size;
|
||
if (size > 0) dias++;
|
||
}
|
||
return `${total} slot${total !== 1 ? 's' : ''} em ${dias} dia${dias !== 1 ? 's' : ''}`;
|
||
});
|
||
|
||
const orphanSlotDays = computed(() => {
|
||
const active = new Set(selectedDays.value.map((d) => d.value));
|
||
return diasSemana
|
||
.filter((d) => !active.has(d.value) && (onlineSlotsByDay.value[d.value]?.size || 0) > 0)
|
||
.map((d) => d.short);
|
||
});
|
||
|
||
// ── Sync horários igual ────────────────────────────────────
|
||
watch(
|
||
[selectedDays, jornadaStart, jornadaEnd],
|
||
() => {
|
||
if (!isValidHHMM(jornadaStart.value) || !isValidHHMM(jornadaEnd.value)) return;
|
||
if (jornadaIgualTodos.value !== false) {
|
||
selectedDays.value
|
||
.filter((d) => d.value >= 1 && d.value <= 5)
|
||
.forEach((d) => {
|
||
jornadaPorDia.value[d.value] = { inicio: jornadaStart.value, fim: jornadaEnd.value };
|
||
});
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
|
||
function getPausasForDay(dayValue) {
|
||
if (jornadaIgualTodos.value !== false) return pausasGlobais.value;
|
||
return pausasPorDia.value[dayValue] || [];
|
||
}
|
||
|
||
function switchToIgual() {
|
||
jornadaPorDiaSnapshot.value = {};
|
||
selectedDays.value.forEach((d) => {
|
||
if (jornadaPorDia.value[d.value]) {
|
||
jornadaPorDiaSnapshot.value[d.value] = { ...jornadaPorDia.value[d.value] };
|
||
}
|
||
});
|
||
if (isValidHHMM(jornadaStart.value) && isValidHHMM(jornadaEnd.value)) {
|
||
selectedDays.value
|
||
.filter((d) => d.value >= 1 && d.value <= 5)
|
||
.forEach((d) => {
|
||
jornadaPorDia.value[d.value] = { inicio: jornadaStart.value, fim: jornadaEnd.value };
|
||
});
|
||
[6, 0].forEach((v) => {
|
||
if (workDays.value[v] && !jornadaPorDia.value[v]?.inicio) {
|
||
jornadaPorDia.value[v] = { inicio: jornadaStart.value, fim: jornadaEnd.value };
|
||
}
|
||
});
|
||
}
|
||
selectedDays.value.forEach((d) => { pausasPorDia.value[d.value] = []; });
|
||
jornadaIgualTodos.value = true;
|
||
}
|
||
|
||
function switchToDiferente() {
|
||
selectedDays.value.forEach((d) => {
|
||
const isWeekend = d.value === 6 || d.value === 0;
|
||
const snap = jornadaPorDiaSnapshot.value?.[d.value];
|
||
if (snap && isValidHHMM(snap.inicio) && isValidHHMM(snap.fim)) {
|
||
jornadaPorDia.value[d.value] = { ...snap };
|
||
} else if (!isWeekend) {
|
||
jornadaPorDia.value[d.value] = { inicio: '08:00', fim: '18:00' };
|
||
}
|
||
});
|
||
selectedDays.value.forEach((d) => {
|
||
pausasPorDia.value[d.value] = pausasGlobais.value.map((p) => ({ ...p, id: newId() }));
|
||
});
|
||
jornadaIgualTodos.value = false;
|
||
}
|
||
|
||
function hydratePausasFromCfg() {
|
||
const byDay = { 1: [], 2: [], 3: [], 4: [], 5: [], 6: [], 0: [] };
|
||
for (const p of cfg.value.pausas_semanais || []) {
|
||
const d = p.dia_semana;
|
||
if (byDay[d]) byDay[d].push({ ...p, id: p.id || newId() });
|
||
}
|
||
pausasPorDia.value = byDay;
|
||
const first = selectedDays.value[0]?.value;
|
||
if (jornadaIgualTodos.value !== false) {
|
||
pausasGlobais.value = first != null ? (byDay[first] || []).map((p) => ({ ...p })) : [];
|
||
}
|
||
}
|
||
|
||
function hydrateWizardFromRegras(dbRegras) {
|
||
const map = { 1: false, 2: false, 3: false, 4: false, 5: false, 6: false, 0: false };
|
||
const byDay = { ...jornadaPorDia.value };
|
||
const actives = (dbRegras || []).filter((r) => r.ativo);
|
||
|
||
if (!actives.length) {
|
||
workDays.value = map;
|
||
jornadaIgualTodos.value = true;
|
||
regras.value = [];
|
||
return;
|
||
}
|
||
|
||
actives.forEach((r) => {
|
||
map[r.dia_semana] = true;
|
||
byDay[r.dia_semana] = {
|
||
inicio: String(r.hora_inicio || '').slice(0, 5),
|
||
fim: String(r.hora_fim || '').slice(0, 5)
|
||
};
|
||
});
|
||
|
||
workDays.value = map;
|
||
jornadaPorDia.value = byDay;
|
||
|
||
const weekdayActives = actives.filter((r) => r.dia_semana >= 1 && r.dia_semana <= 5);
|
||
const ref0 = weekdayActives.length ? weekdayActives[0] : actives[0];
|
||
|
||
const allSame =
|
||
weekdayActives.length >= 2
|
||
? weekdayActives.every(
|
||
(r) =>
|
||
String(r.hora_inicio || '').slice(0, 5) === String(ref0.hora_inicio || '').slice(0, 5) &&
|
||
String(r.hora_fim || '').slice(0, 5) === String(ref0.hora_fim || '').slice(0, 5)
|
||
)
|
||
: true;
|
||
|
||
jornadaIgualTodos.value = allSame;
|
||
jornadaStart.value = String(ref0.hora_inicio || '').slice(0, 5) || '08:00';
|
||
jornadaEnd.value = String(ref0.hora_fim || '').slice(0, 5) || '18:00';
|
||
|
||
if (!allSame) {
|
||
jornadaPorDiaSnapshot.value = {};
|
||
Object.keys(byDay).forEach((k) => {
|
||
jornadaPorDiaSnapshot.value[k] = { ...byDay[k] };
|
||
});
|
||
}
|
||
|
||
regras.value = actives.map((r) => ({
|
||
...r,
|
||
hora_inicio: String(r.hora_inicio || '').slice(0, 5),
|
||
hora_fim: String(r.hora_fim || '').slice(0, 5)
|
||
}));
|
||
}
|
||
|
||
// ── Auth / tenant ──────────────────────────────────────────
|
||
async function getOwnerId() {
|
||
const { data, error } = await supabase.auth.getUser();
|
||
if (error) throw error;
|
||
const uid = data?.user?.id;
|
||
if (!uid) throw new Error('Sessão inválida.');
|
||
return uid;
|
||
}
|
||
|
||
async function getActiveTenantId(uid) {
|
||
const fromStore =
|
||
tenantStore.activeTenantId ||
|
||
tenantStore.tenantId ||
|
||
tenantStore.currentTenantId ||
|
||
tenantStore.tenant?.id ||
|
||
null;
|
||
if (fromStore) return fromStore;
|
||
const { data } = await supabase
|
||
.from('tenant_members')
|
||
.select('tenant_id')
|
||
.eq('user_id', uid)
|
||
.eq('status', 'active')
|
||
.limit(1)
|
||
.maybeSingle();
|
||
return data?.tenant_id || null;
|
||
}
|
||
|
||
async function seedConfigIfMissing(uid) {
|
||
const { data: existing } = await tenantDb().from('agenda_configuracoes')
|
||
.select('owner_id')
|
||
.eq('owner_id', uid)
|
||
.maybeSingle();
|
||
if (existing) return;
|
||
const tenantId = await getActiveTenantId(uid);
|
||
const { error } = await tenantDb().from('agenda_configuracoes')
|
||
.insert({ owner_id: uid, session_duration_min: 50, session_break_min: 10 });
|
||
if (error) throw error;
|
||
}
|
||
|
||
// ── Load ───────────────────────────────────────────────────
|
||
async function loadConfig() {
|
||
const uid = await getOwnerId();
|
||
ownerId.value = uid;
|
||
cfg.value.owner_id = uid;
|
||
await seedConfigIfMissing(uid);
|
||
const { data, error } = await tenantDb().from('agenda_configuracoes')
|
||
.select('*')
|
||
.eq('owner_id', uid)
|
||
.order('created_at', { ascending: false })
|
||
.limit(1)
|
||
.maybeSingle();
|
||
if (error) throw error;
|
||
if (data) {
|
||
cfg.value = { ...cfg.value, ...data };
|
||
if (!Array.isArray(cfg.value.pausas_semanais)) cfg.value.pausas_semanais = [];
|
||
}
|
||
}
|
||
|
||
async function loadRegras() {
|
||
const uid = ownerId.value || (await getOwnerId());
|
||
ownerId.value = uid;
|
||
const { data, error } = await tenantDb().from('agenda_regras_semanais')
|
||
.select('*')
|
||
.eq('owner_id', uid)
|
||
.order('dia_semana')
|
||
.order('hora_inicio');
|
||
if (error) throw error;
|
||
const rows = (data || []).map((r) => ({
|
||
...r,
|
||
hora_inicio: String(r.hora_inicio || '').slice(0, 5),
|
||
hora_fim: String(r.hora_fim || '').slice(0, 5),
|
||
ativo: !!r.ativo
|
||
}));
|
||
hydrateWizardFromRegras(rows);
|
||
}
|
||
|
||
async function loadOnlineSlots() {
|
||
const uid = ownerId.value || (await getOwnerId());
|
||
ownerId.value = uid;
|
||
resetOnlineSlots();
|
||
const { data, error } = await tenantDb().from('agenda_online_slots')
|
||
.select('weekday,time,enabled')
|
||
.eq('owner_id', uid);
|
||
if (error) return;
|
||
for (const row of data || []) {
|
||
if (!row?.enabled) continue;
|
||
const d = Number(row.weekday);
|
||
const t = String(row.time || '').slice(0, 5);
|
||
if ([0, 1, 2, 3, 4, 5, 6].includes(d) && isValidHHMM(t)) onlineSlotsByDay.value[d].add(t);
|
||
}
|
||
}
|
||
|
||
async function boot() {
|
||
hydrating.value = true;
|
||
loading.value = true;
|
||
try {
|
||
await loadConfig();
|
||
await loadRegras();
|
||
if (cfg.value.jornada_igual_todos != null) {
|
||
jornadaIgualTodos.value = cfg.value.jornada_igual_todos;
|
||
}
|
||
hydratePausasFromCfg();
|
||
await loadOnlineSlots();
|
||
const first = selectedDays.value[0];
|
||
previewDay.value = first?.value ?? 1;
|
||
} catch (e) {
|
||
toast.add({ severity: 'error', summary: 'Erro ao carregar', detail: e?.message, life: 4000 });
|
||
} finally {
|
||
loading.value = false;
|
||
hydrating.value = false;
|
||
}
|
||
}
|
||
|
||
// ── Save por card ──────────────────────────────────────────
|
||
async function saveJornada() {
|
||
if (!jornadaOk.value) {
|
||
toast.add({
|
||
severity: 'warn',
|
||
summary: 'Jornada',
|
||
detail: 'Selecione ao menos um dia e informe horários válidos.',
|
||
life: 3500
|
||
});
|
||
return;
|
||
}
|
||
savingCard.value = 'jornada';
|
||
try {
|
||
const uid = ownerId.value || (await getOwnerId());
|
||
ownerId.value = uid;
|
||
const tenantId = await getActiveTenantId(uid);
|
||
|
||
const pausasToSave = [];
|
||
if (jornadaIgualTodos.value !== false) {
|
||
for (const d of selectedDays.value) {
|
||
for (const p of pausasGlobais.value) pausasToSave.push({ ...p, dia_semana: d.value });
|
||
}
|
||
} else {
|
||
for (const d of selectedDays.value) {
|
||
for (const p of pausasPorDia.value[d.value] || []) pausasToSave.push({ ...p, dia_semana: d.value });
|
||
}
|
||
}
|
||
cfg.value.pausas_semanais = pausasToSave;
|
||
|
||
const igualTodos = jornadaIgualTodos.value !== false;
|
||
const { error: cfgErr } = await tenantDb().from('agenda_configuracoes')
|
||
.upsert(
|
||
{
|
||
owner_id: uid,
|
||
pausas_semanais: pausasToSave,
|
||
jornada_igual_todos: igualTodos,
|
||
timezone: cfg.value.timezone || 'America/Sao_Paulo',
|
||
setup_clinica_concluido: true,
|
||
setup_clinica_concluido_em: cfg.value.setup_clinica_concluido_em || new Date().toISOString()
|
||
},
|
||
{ onConflict: 'owner_id' }
|
||
);
|
||
if (cfgErr) throw cfgErr;
|
||
|
||
const rows = selectedDays.value.map((d) => {
|
||
const isWeekend = d.value === 6 || d.value === 0;
|
||
const t =
|
||
jornadaIgualTodos.value === false || isWeekend
|
||
? jornadaPorDia.value[d.value] || { inicio: jornadaStart.value, fim: jornadaEnd.value }
|
||
: { inicio: jornadaStart.value, fim: jornadaEnd.value };
|
||
return {
|
||
owner_id: uid,
|
||
dia_semana: d.value,
|
||
hora_inicio: normalizeTime(t.inicio),
|
||
hora_fim: normalizeTime(t.fim),
|
||
modalidade: 'ambos',
|
||
ativo: true
|
||
};
|
||
});
|
||
|
||
const { error: delErr } = await tenantDb().from('agenda_regras_semanais').delete().eq('owner_id', uid);
|
||
if (delErr) throw delErr;
|
||
if (rows.length) {
|
||
const { error: insErr } = await tenantDb().from('agenda_regras_semanais').insert(rows);
|
||
if (insErr) throw insErr;
|
||
}
|
||
|
||
// Limpar slots online de dias removidos da jornada
|
||
const activeDays = new Set(selectedDays.value.map((d) => d.value));
|
||
const orphanDays = [0, 1, 2, 3, 4, 5, 6].filter((d) => !activeDays.has(d));
|
||
if (orphanDays.length) {
|
||
await tenantDb().from('agenda_online_slots').delete().eq('owner_id', uid).in('weekday', orphanDays);
|
||
for (const d of orphanDays) _setDay(d, new Set());
|
||
}
|
||
|
||
cfg.value.setup_clinica_concluido = true;
|
||
cfg.value.jornada_igual_todos = igualTodos;
|
||
toast.add({ severity: 'success', summary: 'Jornada salva', life: 2500 });
|
||
window.dispatchEvent(new CustomEvent('agenda:settings-saved', { detail: { source: 'jornada' } }));
|
||
} catch (e) {
|
||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao salvar jornada.', life: 3500 });
|
||
} finally {
|
||
savingCard.value = null;
|
||
}
|
||
}
|
||
|
||
async function saveRitmo() {
|
||
const dur = Number(cfg.value.session_duration_min || 0);
|
||
const gap = Number(cfg.value.session_break_min || 0);
|
||
if (dur < 10 || dur > 240) {
|
||
toast.add({ severity: 'warn', summary: 'Ritmo', detail: 'Duração entre 10 e 240 min.', life: 3500 });
|
||
return;
|
||
}
|
||
if (gap < 0 || gap > 60) {
|
||
toast.add({ severity: 'warn', summary: 'Ritmo', detail: 'Intervalo entre 0 e 60 min.', life: 3500 });
|
||
return;
|
||
}
|
||
savingCard.value = 'ritmo';
|
||
try {
|
||
const uid = ownerId.value || (await getOwnerId());
|
||
ownerId.value = uid;
|
||
const { error } = await tenantDb().from('agenda_configuracoes')
|
||
.upsert(
|
||
{ owner_id: uid, session_duration_min: dur, session_break_min: gap },
|
||
{ onConflict: 'owner_id' }
|
||
);
|
||
if (error) throw error;
|
||
toast.add({ severity: 'success', summary: 'Ritmo salvo', life: 1800 });
|
||
} catch (e) {
|
||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha.', life: 3500 });
|
||
} finally {
|
||
savingCard.value = null;
|
||
}
|
||
}
|
||
|
||
async function saveOnline() {
|
||
savingCard.value = 'online';
|
||
try {
|
||
const uid = ownerId.value || (await getOwnerId());
|
||
ownerId.value = uid;
|
||
const tenantId = await getActiveTenantId(uid);
|
||
|
||
const { error: cfgErr } = await tenantDb().from('agenda_configuracoes')
|
||
.upsert({ owner_id: uid, online_ativo: cfg.value.online_ativo }, { onConflict: 'owner_id' });
|
||
if (cfgErr) throw cfgErr;
|
||
|
||
const { error: delErr } = await tenantDb().from('agenda_online_slots').delete().eq('owner_id', uid);
|
||
if (delErr) throw delErr;
|
||
|
||
if (cfg.value.online_ativo) {
|
||
const rows = [];
|
||
for (const d of selectedDays.value) {
|
||
for (const hhmm of onlineSlotsByDay.value[d.value] || new Set()) {
|
||
if (isValidHHMM(hhmm)) {
|
||
rows.push({
|
||
owner_id: uid,
|
||
weekday: Number(d.value),
|
||
time: normalizeTime(hhmm),
|
||
enabled: true
|
||
});
|
||
}
|
||
}
|
||
}
|
||
if (rows.length) {
|
||
const { error: insErr } = await tenantDb().from('agenda_online_slots').insert(rows);
|
||
if (insErr) throw insErr;
|
||
}
|
||
}
|
||
|
||
toast.add({ severity: 'success', summary: 'Online salvo', life: 1800 });
|
||
} catch (e) {
|
||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha.', life: 3500 });
|
||
} finally {
|
||
savingCard.value = null;
|
||
}
|
||
}
|
||
|
||
// ── Live regras (UI) ───────────────────────────────────────
|
||
const liveRegras = computed(() =>
|
||
selectedDays.value.map((d) => {
|
||
const t = jornadaPorDia.value[d.value] || { inicio: jornadaStart.value, fim: jornadaEnd.value };
|
||
return { dia_semana: d.value, hora_inicio: t.inicio, hora_fim: t.fim, ativo: true };
|
||
})
|
||
);
|
||
|
||
// ── Slots online helpers ───────────────────────────────────
|
||
function generateSlotsForDay(dayValue) {
|
||
const windows = liveRegras.value
|
||
.filter((r) => r.ativo && r.dia_semana === dayValue)
|
||
.map((r) => ({ start: String(r.hora_inicio || '').slice(0, 5), end: String(r.hora_fim || '').slice(0, 5) }))
|
||
.sort((a, b) => a.start.localeCompare(b.start));
|
||
if (!windows.length) return [];
|
||
|
||
const breaks = getPausasForDay(dayValue).map((p) => ({
|
||
start: String(p.inicio || '').slice(0, 5),
|
||
end: String(p.fim || '').slice(0, 5)
|
||
}));
|
||
|
||
const duration = Number(cfg.value.session_duration_min || 50);
|
||
const gap = Number(cfg.value.session_break_min || 10);
|
||
const cycle = Math.max(1, duration + gap);
|
||
const out = [];
|
||
|
||
for (const w of windows) {
|
||
const wStart = hhmmToMin(w.start);
|
||
const wEnd = hhmmToMin(w.end);
|
||
let t = wStart;
|
||
while (t < wEnd) {
|
||
const aEnd = t + duration;
|
||
if (aEnd > wEnd) break;
|
||
const conflict = breaks.find((b) => {
|
||
const bS = hhmmToMin(b.start);
|
||
const bE = hhmmToMin(b.end);
|
||
return !(aEnd <= bS || t >= bE);
|
||
});
|
||
if (conflict) {
|
||
t = hhmmToMin(conflict.end);
|
||
continue;
|
||
}
|
||
out.push({ hhmm: minToHHMM(t), endHHMM: minToHHMM(aEnd) });
|
||
t += cycle;
|
||
}
|
||
}
|
||
|
||
const seen = new Set();
|
||
return out
|
||
.filter((s) => {
|
||
if (seen.has(s.hhmm)) return false;
|
||
seen.add(s.hhmm);
|
||
return true;
|
||
})
|
||
.sort((a, b) => a.hhmm.localeCompare(b.hhmm));
|
||
}
|
||
|
||
const slotsByDay = computed(() => {
|
||
const out = {};
|
||
for (const d of selectedDays.value) out[d.value] = generateSlotsForDay(d.value);
|
||
return out;
|
||
});
|
||
|
||
function isSlotEnabled(dayValue, hhmm) {
|
||
return !!onlineSlotsByDay.value?.[dayValue]?.has?.(hhmm);
|
||
}
|
||
|
||
function _setDay(dayValue, newSet) {
|
||
onlineSlotsByDay.value = { ...onlineSlotsByDay.value, [dayValue]: newSet };
|
||
}
|
||
function toggleSlot(dayValue, hhmm) {
|
||
const s = new Set(onlineSlotsByDay.value?.[dayValue] || []);
|
||
if (s.has(hhmm)) s.delete(hhmm);
|
||
else s.add(hhmm);
|
||
_setDay(dayValue, s);
|
||
}
|
||
function enableAllDay(dayValue) {
|
||
const set = new Set();
|
||
for (const s of slotsByDay.value?.[dayValue] || []) set.add(s.hhmm);
|
||
_setDay(dayValue, set);
|
||
}
|
||
function clearDay(dayValue) {
|
||
_setDay(dayValue, new Set());
|
||
}
|
||
|
||
const periodos = [
|
||
{ label: 'Manhã', icon: 'pi-sun', from: '05:00', to: '12:00' },
|
||
{ label: 'Tarde', icon: 'pi-cloud', from: '12:00', to: '18:00' },
|
||
{ label: 'Noite', icon: 'pi-moon', from: '18:00', to: '24:00' }
|
||
];
|
||
|
||
function isPeriodoAtivo(dayValue, periodo) {
|
||
const slots = (slotsByDay.value?.[dayValue] || []).filter((s) => s.hhmm >= periodo.from && s.hhmm < periodo.to);
|
||
if (!slots.length) return false;
|
||
return slots.every((s) => isSlotEnabled(dayValue, s.hhmm));
|
||
}
|
||
function togglePeriodo(dayValue, periodo) {
|
||
const slots = (slotsByDay.value?.[dayValue] || []).filter((s) => s.hhmm >= periodo.from && s.hhmm < periodo.to);
|
||
if (!slots.length) return;
|
||
const s = new Set(onlineSlotsByDay.value?.[dayValue] || []);
|
||
const allOn = slots.every((sl) => s.has(sl.hhmm));
|
||
if (allOn) for (const sl of slots) s.delete(sl.hhmm);
|
||
else for (const sl of slots) s.add(sl.hhmm);
|
||
_setDay(dayValue, s);
|
||
}
|
||
function hasSlotsInPeriodo(dayValue, periodo) {
|
||
return (slotsByDay.value?.[dayValue] || []).some((s) => s.hhmm >= periodo.from && s.hhmm < periodo.to);
|
||
}
|
||
|
||
// ── Presets de duração ─────────────────────────────────────
|
||
const durationPresets = [
|
||
{ label: '30 min', dur: 30, gap: 0 },
|
||
{ label: '45 min', dur: 45, gap: 15 },
|
||
{ label: '50 min', dur: 50, gap: 10 },
|
||
{ label: '60 min', dur: 60, gap: 0 },
|
||
{ label: '90 min', dur: 90, gap: 0 }
|
||
];
|
||
function applyDurationPreset(preset) {
|
||
cfg.value.session_duration_min = preset.dur;
|
||
cfg.value.session_break_min = preset.gap;
|
||
showAdvancedRitmo.value = false;
|
||
}
|
||
function isActivePreset(preset) {
|
||
return cfg.value.session_duration_min === preset.dur && cfg.value.session_break_min === preset.gap;
|
||
}
|
||
|
||
// DatePicker bridges
|
||
const durationDate = computed({
|
||
get: () => hhmmToDate(minToHHMM(Number(cfg.value.session_duration_min || 50))),
|
||
set: (v) => { if (v instanceof Date) cfg.value.session_duration_min = v.getHours() * 60 + v.getMinutes(); }
|
||
});
|
||
const breakDate = computed({
|
||
get: () => hhmmToDate(minToHHMM(Number(cfg.value.session_break_min || 0))),
|
||
set: (v) => { if (v instanceof Date) cfg.value.session_break_min = v.getHours() * 60 + v.getMinutes(); }
|
||
});
|
||
const jornadaStartDate = computed({
|
||
get: () => hhmmToDate(jornadaStart.value),
|
||
set: (v) => { const h = dateToHHMM(v); if (h) jornadaStart.value = h; }
|
||
});
|
||
const jornadaEndDate = computed({
|
||
get: () => hhmmToDate(jornadaEnd.value),
|
||
set: (v) => { const h = dateToHHMM(v); if (h) jornadaEnd.value = h; }
|
||
});
|
||
|
||
function setJornadaPorDiaInicio(day, v) {
|
||
const h = dateToHHMM(v);
|
||
if (h) jornadaPorDia.value[day] = { ...jornadaPorDia.value[day], inicio: h };
|
||
}
|
||
function setJornadaPorDiaFim(day, v) {
|
||
const h = dateToHHMM(v);
|
||
if (h) jornadaPorDia.value[day] = { ...jornadaPorDia.value[day], fim: h };
|
||
}
|
||
|
||
// ── Status indicators (sidebar) ────────────────────────────
|
||
const statusItems = computed(() => [
|
||
{
|
||
key: 'jornada',
|
||
label: 'Jornada',
|
||
icon: 'pi pi-calendar',
|
||
ok: jornadaOk.value,
|
||
resumo: resumoJornada.value
|
||
},
|
||
{
|
||
key: 'ritmo',
|
||
label: 'Ritmo',
|
||
icon: 'pi pi-stopwatch',
|
||
ok: cfg.value.session_duration_min > 0,
|
||
resumo: resumoRitmo.value
|
||
},
|
||
{
|
||
key: 'online',
|
||
label: 'Online',
|
||
icon: 'pi pi-globe',
|
||
ok: cfg.value.online_ativo,
|
||
resumo: resumoOnline.value
|
||
}
|
||
]);
|
||
|
||
function scrollToCard(key) {
|
||
if (isMobile.value) drawerOpen.value = false;
|
||
nextTick(() => {
|
||
const el = document.getElementById('mac-sec-' + key);
|
||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
});
|
||
}
|
||
|
||
// ── 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();
|
||
await boot();
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
if (_mqMobile) {
|
||
try { _mqMobile.removeEventListener('change', _onMqMobileChange); }
|
||
catch { _mqMobile.removeListener(_onMqMobileChange); }
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<Transition name="mac-drawer-fade">
|
||
<div
|
||
v-show="isMobile && drawerOpen"
|
||
class="mac-mobile-drawer"
|
||
:class="{ 'is-open': drawerOpen }"
|
||
>
|
||
<div id="mac-mobile-drawer-target" class="mac-mobile-drawer__scroll" />
|
||
</div>
|
||
</Transition>
|
||
<Transition name="mac-drawer-fade">
|
||
<div
|
||
v-show="isMobile && drawerOpen"
|
||
class="mac-mobile-drawer__backdrop"
|
||
@click="fecharDrawer"
|
||
/>
|
||
</Transition>
|
||
|
||
<section class="mac-page">
|
||
<header class="mac-page__head">
|
||
<button
|
||
class="mac-menu-btn mac-menu-btn--mobile-only"
|
||
v-tooltip.bottom="'Status & Atalhos'"
|
||
@click="toggleDrawer"
|
||
>
|
||
<i class="pi pi-bars" />
|
||
<span>Menu</span>
|
||
</button>
|
||
<div class="mac-page__title">
|
||
<i class="pi pi-calendar mac-page__title-icon" />
|
||
<span>Configurações da Agenda</span>
|
||
<Tag
|
||
v-if="cfg.setup_clinica_concluido"
|
||
value="Setup OK"
|
||
severity="success"
|
||
/>
|
||
<Tag
|
||
v-else
|
||
value="Setup pendente"
|
||
severity="warn"
|
||
/>
|
||
</div>
|
||
<div class="mac-page__actions">
|
||
<button class="mac-close" v-tooltip.bottom="'Voltar (Esc)'" @click="emit('close')">
|
||
<i class="pi pi-times" />
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="mac-subheader">
|
||
<i class="pi pi-info-circle mac-subheader__icon" />
|
||
<span class="mac-subheader__text">
|
||
Configure a jornada (dias e horários), o ritmo das sessões e o
|
||
agendamento online. Cada card abaixo tem seu próprio botão de salvar.
|
||
</span>
|
||
</div>
|
||
|
||
<div class="mac-body">
|
||
<Teleport to="#mac-mobile-drawer-target" :disabled="!isMobile">
|
||
<aside class="mac-side">
|
||
<button class="mac-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 mac-cfg-btn__chev" />
|
||
</button>
|
||
<div v-if="cfgOpen" class="mac-side__scroll mac-side__scroll--cfg">
|
||
<MelissaConfigList @select="fecharCfg" />
|
||
</div>
|
||
<div v-else class="mac-side__scroll">
|
||
<!-- Card: Status do setup -->
|
||
<div class="mac-w mac-w--side">
|
||
<div class="mac-w__head">
|
||
<div class="mac-w__icon"><i class="pi pi-check-circle" /></div>
|
||
<div class="mac-w__title">
|
||
<div class="mac-w__title-text">Status do setup</div>
|
||
<div class="mac-w__sub">Estado de cada seção</div>
|
||
</div>
|
||
</div>
|
||
<div class="mac-w__body">
|
||
<button
|
||
v-for="s in statusItems"
|
||
:key="s.key"
|
||
type="button"
|
||
class="mac-status-item"
|
||
@click="scrollToCard(s.key)"
|
||
>
|
||
<div
|
||
class="mac-status-item__icon"
|
||
:class="{ 'is-ok': s.ok, 'is-pending': !s.ok }"
|
||
>
|
||
<i :class="s.icon" />
|
||
</div>
|
||
<div class="mac-status-item__text">
|
||
<div class="mac-status-item__label">
|
||
{{ s.label }}
|
||
<span class="mac-status-item__tag" :class="{ 'is-ok': s.ok }">
|
||
{{ s.ok ? '✓' : '!' }}
|
||
</span>
|
||
</div>
|
||
<div class="mac-status-item__resumo">{{ s.resumo }}</div>
|
||
</div>
|
||
<i class="pi pi-chevron-right mac-status-item__chev" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Card: Como funciona -->
|
||
<div class="mac-w mac-w--side">
|
||
<div class="mac-w__head">
|
||
<div class="mac-w__icon"><i class="pi pi-question-circle" /></div>
|
||
<div class="mac-w__title">
|
||
<div class="mac-w__title-text">Como funciona</div>
|
||
<div class="mac-w__sub">Ordem sugerida</div>
|
||
</div>
|
||
</div>
|
||
<div class="mac-w__body">
|
||
<ul class="mac-faq">
|
||
<li class="mac-faq__item">
|
||
<span class="mac-faq__bullet" style="background: #3b82f6">1</span>
|
||
<div>
|
||
<strong>Jornada</strong> — escolha os dias e os horários de trabalho. Pode ser igual ou diferente por dia.
|
||
</div>
|
||
</li>
|
||
<li class="mac-faq__item">
|
||
<span class="mac-faq__bullet" style="background: #f59e0b">2</span>
|
||
<div>
|
||
<strong>Ritmo</strong> — duração de cada sessão e intervalo entre elas. Define a grade de slots.
|
||
</div>
|
||
</li>
|
||
<li class="mac-faq__item">
|
||
<span class="mac-faq__bullet" style="background: #10b981">3</span>
|
||
<div>
|
||
<strong>Online</strong> — ative o agendamento online e escolha quais slots ficam disponíveis pros pacientes.
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
</Teleport>
|
||
|
||
<div class="mac-main">
|
||
<template v-if="loading">
|
||
<div class="mac-w" v-for="n in 3" :key="`sk-${n}`">
|
||
<div class="mac-w__body">
|
||
<Skeleton width="40%" height="20px" class="mb-3" />
|
||
<Skeleton v-for="m in 4" :key="`sk-${n}-${m}`" width="100%" height="36px" class="mb-2" />
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else>
|
||
<!-- ── Jornada ── -->
|
||
<div id="mac-sec-jornada" class="mac-w">
|
||
<div class="mac-w__head">
|
||
<div class="mac-w__icon mac-w__icon--blue"><i class="pi pi-calendar" /></div>
|
||
<div class="mac-w__title">
|
||
<div class="mac-w__title-text">Jornada</div>
|
||
<div class="mac-w__sub">Dias e horários de trabalho</div>
|
||
</div>
|
||
<Tag
|
||
v-if="jornadaOk"
|
||
value="Configurado"
|
||
severity="success"
|
||
/>
|
||
<Tag
|
||
v-else
|
||
value="Pendente"
|
||
severity="warn"
|
||
/>
|
||
</div>
|
||
|
||
<div class="mac-w__body">
|
||
<!-- Fuso -->
|
||
<div class="mac-field">
|
||
<label class="mac-label">Fuso horário</label>
|
||
<Select
|
||
v-model="cfg.timezone"
|
||
:options="timezones"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
class="w-full"
|
||
placeholder="Selecione o fuso..."
|
||
/>
|
||
</div>
|
||
|
||
<!-- Dias -->
|
||
<div class="mac-field">
|
||
<label class="mac-label">Quais dias você trabalha?</label>
|
||
<div class="mac-days">
|
||
<button
|
||
v-for="d in diasSemana"
|
||
:key="d.value"
|
||
type="button"
|
||
class="mac-day"
|
||
:class="{ 'is-active': workDays[d.value] }"
|
||
@click="
|
||
workDays[d.value] = !workDays[d.value];
|
||
previewDay = selectedDays[0]?.value ?? previewDay;
|
||
"
|
||
>{{ d.short }}</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Horários -->
|
||
<div v-if="selectedDays.length > 0" class="mac-field">
|
||
<label class="mac-label">Qual é o seu horário?</label>
|
||
<div class="mac-toggle-mode">
|
||
<button
|
||
type="button"
|
||
class="mac-mode-btn"
|
||
:class="{ 'is-active': jornadaIgualTodos !== false }"
|
||
@click="switchToIgual"
|
||
>Igual para todos</button>
|
||
<button
|
||
type="button"
|
||
class="mac-mode-btn"
|
||
:class="{ 'is-active': jornadaIgualTodos === false }"
|
||
@click="switchToDiferente"
|
||
>Diferente por dia</button>
|
||
</div>
|
||
|
||
<!-- Modo igual -->
|
||
<div v-if="jornadaIgualTodos !== false" class="mac-equals">
|
||
<div v-if="selectedWeekdays.length > 0" class="mac-equal-row">
|
||
<div class="mac-equal-row__chips">
|
||
<span
|
||
v-for="d in selectedWeekdays"
|
||
:key="d.value"
|
||
class="mac-day is-active mac-day--sm"
|
||
>{{ d.short }}</span>
|
||
</div>
|
||
<div class="mac-time-row">
|
||
<span class="mac-time-row__lbl">Das</span>
|
||
<DatePicker
|
||
v-model="jornadaStartDate"
|
||
showIcon
|
||
fluid
|
||
iconDisplay="input"
|
||
timeOnly
|
||
hourFormat="24"
|
||
:stepMinute="15"
|
||
:manualInput="false"
|
||
class="mac-time"
|
||
>
|
||
<template #inputicon="sp">
|
||
<i class="pi pi-clock" @click="sp.clickCallback" />
|
||
</template>
|
||
</DatePicker>
|
||
<span class="mac-time-row__lbl">até</span>
|
||
<DatePicker
|
||
v-model="jornadaEndDate"
|
||
showIcon
|
||
fluid
|
||
iconDisplay="input"
|
||
timeOnly
|
||
hourFormat="24"
|
||
:stepMinute="15"
|
||
:manualInput="false"
|
||
class="mac-time"
|
||
>
|
||
<template #inputicon="sp">
|
||
<i class="pi pi-clock" @click="sp.clickCallback" />
|
||
</template>
|
||
</DatePicker>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="workDays[6]" class="mac-equal-row">
|
||
<div class="mac-equal-row__chips">
|
||
<span class="mac-day is-active mac-day--sm">Sáb</span>
|
||
</div>
|
||
<div class="mac-time-row">
|
||
<span class="mac-time-row__lbl">Das</span>
|
||
<DatePicker
|
||
:modelValue="hhmmToDate(jornadaPorDia[6]?.inicio)"
|
||
@update:modelValue="(v) => setJornadaPorDiaInicio(6, v)"
|
||
showIcon fluid iconDisplay="input" timeOnly hourFormat="24"
|
||
:stepMinute="15" :manualInput="false" class="mac-time"
|
||
>
|
||
<template #inputicon="sp">
|
||
<i class="pi pi-clock" @click="sp.clickCallback" />
|
||
</template>
|
||
</DatePicker>
|
||
<span class="mac-time-row__lbl">até</span>
|
||
<DatePicker
|
||
:modelValue="hhmmToDate(jornadaPorDia[6]?.fim)"
|
||
@update:modelValue="(v) => setJornadaPorDiaFim(6, v)"
|
||
showIcon fluid iconDisplay="input" timeOnly hourFormat="24"
|
||
:stepMinute="15" :manualInput="false" class="mac-time"
|
||
>
|
||
<template #inputicon="sp">
|
||
<i class="pi pi-clock" @click="sp.clickCallback" />
|
||
</template>
|
||
</DatePicker>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="workDays[0]" class="mac-equal-row">
|
||
<div class="mac-equal-row__chips">
|
||
<span class="mac-day is-active mac-day--sm">Dom</span>
|
||
</div>
|
||
<div class="mac-time-row">
|
||
<span class="mac-time-row__lbl">Das</span>
|
||
<DatePicker
|
||
:modelValue="hhmmToDate(jornadaPorDia[0]?.inicio)"
|
||
@update:modelValue="(v) => setJornadaPorDiaInicio(0, v)"
|
||
showIcon fluid iconDisplay="input" timeOnly hourFormat="24"
|
||
:stepMinute="15" :manualInput="false" class="mac-time"
|
||
>
|
||
<template #inputicon="sp">
|
||
<i class="pi pi-clock" @click="sp.clickCallback" />
|
||
</template>
|
||
</DatePicker>
|
||
<span class="mac-time-row__lbl">até</span>
|
||
<DatePicker
|
||
:modelValue="hhmmToDate(jornadaPorDia[0]?.fim)"
|
||
@update:modelValue="(v) => setJornadaPorDiaFim(0, v)"
|
||
showIcon fluid iconDisplay="input" timeOnly hourFormat="24"
|
||
:stepMinute="15" :manualInput="false" class="mac-time"
|
||
>
|
||
<template #inputicon="sp">
|
||
<i class="pi pi-clock" @click="sp.clickCallback" />
|
||
</template>
|
||
</DatePicker>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modo diferente -->
|
||
<div v-else class="mac-diff">
|
||
<div
|
||
v-for="d in selectedDays"
|
||
:key="d.value"
|
||
class="mac-diff-row"
|
||
>
|
||
<span class="mac-diff-row__day">{{ d.short }}</span>
|
||
<DatePicker
|
||
:modelValue="hhmmToDate(jornadaPorDia[d.value]?.inicio)"
|
||
@update:modelValue="(v) => { setJornadaPorDiaInicio(d.value, v); previewDay = d.value; }"
|
||
showIcon fluid iconDisplay="input" timeOnly hourFormat="24"
|
||
:stepMinute="15" :manualInput="false" class="mac-time"
|
||
>
|
||
<template #inputicon="sp">
|
||
<i class="pi pi-clock" @click="sp.clickCallback" />
|
||
</template>
|
||
</DatePicker>
|
||
<span class="mac-time-row__lbl">até</span>
|
||
<DatePicker
|
||
:modelValue="hhmmToDate(jornadaPorDia[d.value]?.fim)"
|
||
@update:modelValue="(v) => { setJornadaPorDiaFim(d.value, v); previewDay = d.value; }"
|
||
showIcon fluid iconDisplay="input" timeOnly hourFormat="24"
|
||
:stepMinute="15" :manualInput="false" class="mac-time"
|
||
>
|
||
<template #inputicon="sp">
|
||
<i class="pi pi-clock" @click="sp.clickCallback" />
|
||
</template>
|
||
</DatePicker>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Pausas -->
|
||
<div v-if="selectedDays.length > 0" class="mac-field">
|
||
<label class="mac-label">Pausas <span class="mac-label__opt">(opcional)</span></label>
|
||
<span class="mac-hint">Almoço, jantar, café — ficam fora dos slots disponíveis.</span>
|
||
|
||
<div v-if="jornadaIgualTodos !== false">
|
||
<PausasChipsEditor v-model="pausasGlobais" />
|
||
</div>
|
||
<div v-else class="mac-pausas-by-day">
|
||
<div v-for="d in selectedDays" :key="d.value">
|
||
<div class="mac-pausa-day-label">{{ d.label }}</div>
|
||
<PausasChipsEditor v-model="pausasPorDia[d.value]" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mac-card-actions">
|
||
<button
|
||
class="mac-btn mac-btn--primary"
|
||
:disabled="!jornadaOk || savingCard === 'jornada'"
|
||
@click="saveJornada"
|
||
>
|
||
<i :class="savingCard === 'jornada' ? 'pi pi-spin pi-spinner' : 'pi pi-check'" />
|
||
<span>Salvar jornada</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Ritmo ── -->
|
||
<div id="mac-sec-ritmo" class="mac-w">
|
||
<div class="mac-w__head">
|
||
<div class="mac-w__icon mac-w__icon--amber"><i class="pi pi-stopwatch" /></div>
|
||
<div class="mac-w__title">
|
||
<div class="mac-w__title-text">Ritmo das sessões</div>
|
||
<div class="mac-w__sub">Duração e intervalo</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mac-w__body">
|
||
<div class="mac-field">
|
||
<label class="mac-label">Duração padrão</label>
|
||
<div class="mac-presets">
|
||
<button
|
||
v-for="p in durationPresets"
|
||
:key="p.label"
|
||
type="button"
|
||
class="mac-preset"
|
||
:class="{ 'is-active': isActivePreset(p) }"
|
||
@click="applyDurationPreset(p)"
|
||
>
|
||
<span class="mac-preset__dur">{{ p.label }}</span>
|
||
<span class="mac-preset__gap">
|
||
{{ p.gap > 0 ? `· ${p.gap}min intervalo` : '· sem pausa' }}
|
||
</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="mac-preset"
|
||
:class="{ 'is-active': !durationPresets.some((p) => isActivePreset(p)) }"
|
||
@click="showAdvancedRitmo = true"
|
||
>
|
||
<span class="mac-preset__dur">Personalizar</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="showAdvancedRitmo || !durationPresets.some((p) => isActivePreset(p))" class="mac-custom">
|
||
<div class="mac-label">Personalizado</div>
|
||
<div class="mac-grid-2">
|
||
<div class="mac-field">
|
||
<label class="mac-label-sm">Duração</label>
|
||
<DatePicker
|
||
v-model="durationDate"
|
||
showIcon
|
||
fluid
|
||
iconDisplay="input"
|
||
timeOnly
|
||
hourFormat="24"
|
||
:stepMinute="5"
|
||
:manualInput="false"
|
||
>
|
||
<template #inputicon="sp">
|
||
<i class="pi pi-clock" @click="sp.clickCallback" />
|
||
</template>
|
||
</DatePicker>
|
||
</div>
|
||
<div class="mac-field">
|
||
<label class="mac-label-sm">Intervalo</label>
|
||
<DatePicker
|
||
v-model="breakDate"
|
||
showIcon
|
||
fluid
|
||
iconDisplay="input"
|
||
timeOnly
|
||
hourFormat="24"
|
||
:stepMinute="5"
|
||
:manualInput="false"
|
||
>
|
||
<template #inputicon="sp">
|
||
<i class="pi pi-clock" @click="sp.clickCallback" />
|
||
</template>
|
||
</DatePicker>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mac-card-actions">
|
||
<button
|
||
class="mac-btn mac-btn--primary"
|
||
:disabled="savingCard === 'ritmo'"
|
||
@click="saveRitmo"
|
||
>
|
||
<i :class="savingCard === 'ritmo' ? 'pi pi-spin pi-spinner' : 'pi pi-check'" />
|
||
<span>Salvar ritmo</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Online ── -->
|
||
<div id="mac-sec-online" class="mac-w">
|
||
<div class="mac-w__head">
|
||
<div class="mac-w__icon mac-w__icon--green"><i class="pi pi-globe" /></div>
|
||
<div class="mac-w__title">
|
||
<div class="mac-w__title-text">Agendamento Online</div>
|
||
<div class="mac-w__sub">Slots disponíveis pros pacientes</div>
|
||
</div>
|
||
<Tag
|
||
v-if="cfg.online_ativo"
|
||
value="Ativo"
|
||
severity="success"
|
||
/>
|
||
<Tag
|
||
v-else
|
||
value="Desativado"
|
||
severity="secondary"
|
||
/>
|
||
</div>
|
||
|
||
<div class="mac-w__body">
|
||
<!-- Aviso slots órfãos -->
|
||
<div v-if="orphanSlotDays.length" class="mac-warn">
|
||
<i class="pi pi-exclamation-triangle" />
|
||
<span>
|
||
Há slots configurados pra <b>{{ orphanSlotDays.join(', ') }}</b>,
|
||
mas esses dias saíram da sua jornada. Serão removidos ao salvar.
|
||
</span>
|
||
</div>
|
||
|
||
<div class="mac-toggle-row">
|
||
<div class="mac-toggle-row__text">
|
||
<div class="mac-toggle-row__label">Permitir agendamento online</div>
|
||
<div class="mac-toggle-row__hint">Você escolhe quais horários ficam disponíveis.</div>
|
||
</div>
|
||
<ToggleSwitch v-model="cfg.online_ativo" />
|
||
</div>
|
||
|
||
<!-- Slots por dia -->
|
||
<div v-if="cfg.online_ativo && selectedDays.length" class="mac-slots-area">
|
||
<label class="mac-label">Escolha os horários disponíveis</label>
|
||
|
||
<!-- Tabs de dias -->
|
||
<div class="mac-day-tabs">
|
||
<button
|
||
v-for="d in selectedDays"
|
||
:key="d.value"
|
||
type="button"
|
||
class="mac-day-tab"
|
||
:class="{
|
||
'is-active': previewDay === d.value,
|
||
'has-slots': previewDay !== d.value && (onlineSlotsByDay[d.value]?.size || 0) > 0
|
||
}"
|
||
@click="previewDay = d.value"
|
||
>{{ d.short }}</button>
|
||
</div>
|
||
|
||
<!-- Slots do dia selecionado -->
|
||
<div v-if="previewDay != null" class="mac-slots-day">
|
||
<div v-if="(slotsByDay[previewDay] || []).length === 0" class="mac-empty-slots">
|
||
Nenhum slot disponível pra este dia. Configure a jornada primeiro.
|
||
</div>
|
||
<div v-else>
|
||
<!-- Periodos quick actions -->
|
||
<div class="mac-periodos">
|
||
<button
|
||
v-for="p in periodos"
|
||
:key="p.label"
|
||
v-show="hasSlotsInPeriodo(previewDay, p)"
|
||
type="button"
|
||
class="mac-periodo-btn"
|
||
:class="{ 'is-on': isPeriodoAtivo(previewDay, p) }"
|
||
@click="togglePeriodo(previewDay, p)"
|
||
>
|
||
<i :class="['pi', p.icon]" />
|
||
<span>{{ p.label }}</span>
|
||
</button>
|
||
<span class="mac-periodos__sep" />
|
||
<button
|
||
type="button"
|
||
class="mac-periodo-btn"
|
||
@click="enableAllDay(previewDay)"
|
||
>
|
||
<i class="pi pi-check-circle" />
|
||
<span>Todos</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="mac-periodo-btn"
|
||
@click="clearDay(previewDay)"
|
||
>
|
||
<i class="pi pi-times-circle" />
|
||
<span>Limpar</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Slot chips -->
|
||
<div class="mac-slot-chips">
|
||
<button
|
||
v-for="s in slotsByDay[previewDay] || []"
|
||
:key="s.hhmm"
|
||
type="button"
|
||
class="mac-slot"
|
||
:class="{ 'is-on': isSlotEnabled(previewDay, s.hhmm) }"
|
||
@click="toggleSlot(previewDay, s.hhmm)"
|
||
>{{ s.hhmm }}</button>
|
||
</div>
|
||
|
||
<div class="mac-slots-info">
|
||
<i class="pi pi-info-circle" />
|
||
<span>
|
||
{{ onlineSlotsByDay[previewDay]?.size || 0 }} slot{{
|
||
(onlineSlotsByDay[previewDay]?.size || 0) !== 1 ? 's' : ''
|
||
}} disponíveis toda
|
||
{{ diasSemana.find((d) => d.value === previewDay)?.label?.toLowerCase() }}.
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else-if="!selectedDays.length" class="mac-empty-jornada">
|
||
<i class="pi pi-info-circle" />
|
||
<span>Configure sua jornada de trabalho primeiro.</span>
|
||
</div>
|
||
|
||
<div class="mac-card-actions">
|
||
<button
|
||
class="mac-btn mac-btn--primary"
|
||
:disabled="savingCard === 'online'"
|
||
@click="saveOnline"
|
||
>
|
||
<i :class="savingCard === 'online' ? 'pi pi-spin pi-spinner' : 'pi pi-check'" />
|
||
<span>Salvar online</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<style scoped>
|
||
/* ═══════ Page chrome ═══════ */
|
||
.mac-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: mac-page-enter 240ms cubic-bezier(0.2, 0.7, 0.3, 1);
|
||
}
|
||
@keyframes mac-page-enter {
|
||
from { opacity: 0; transform: scale(0.985); }
|
||
to { opacity: 1; transform: scale(1); }
|
||
}
|
||
|
||
.mac-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;
|
||
}
|
||
.mac-page__title {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
font-size: 1rem;
|
||
font-weight: 700;
|
||
color: var(--m-text);
|
||
flex-wrap: wrap;
|
||
}
|
||
.mac-page__title-icon { color: var(--p-primary-color); font-size: 1.05rem; }
|
||
.mac-page__actions { display: flex; align-items: center; gap: 6px; }
|
||
|
||
.mac-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;
|
||
}
|
||
.mac-close:hover { background: var(--m-bg-soft-hover); color: var(--m-text); }
|
||
.mac-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;
|
||
}
|
||
.mac-menu-btn:hover { background: var(--m-bg-soft-hover); }
|
||
|
||
.mac-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;
|
||
}
|
||
.mac-subheader__icon {
|
||
color: var(--p-primary-color);
|
||
font-size: 0.85rem;
|
||
margin-top: 2px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* ═══════ Body 2-col ═══════ */
|
||
.mac-body {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
overflow: hidden;
|
||
}
|
||
.mac-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;
|
||
}
|
||
.mac-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;
|
||
}
|
||
.mac-side__scroll::-webkit-scrollbar { width: 5px; }
|
||
.mac-side__scroll::-webkit-scrollbar-thumb {
|
||
background: var(--m-border-strong);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
/* Botao "Configuracoes" no topo da .mac-side. Click alterna entre
|
||
cards (default) e lista de configs (estado is-open: vira "Voltar"). */
|
||
.mac-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;
|
||
}
|
||
.mac-cfg-btn:hover {
|
||
background: var(--m-bg-soft-hover);
|
||
border-color: var(--m-border-strong);
|
||
}
|
||
.mac-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);
|
||
}
|
||
.mac-cfg-btn > i:first-child {
|
||
color: var(--p-primary-color);
|
||
font-size: 0.92rem;
|
||
}
|
||
.mac-cfg-btn > span { flex: 1; }
|
||
.mac-cfg-btn__chev {
|
||
color: var(--m-text-muted);
|
||
font-size: 0.7rem;
|
||
}
|
||
.mac-side__scroll--cfg {
|
||
padding: 8px;
|
||
gap: 0;
|
||
}
|
||
|
||
.mac-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;
|
||
}
|
||
.mac-main::-webkit-scrollbar { width: 5px; }
|
||
.mac-main::-webkit-scrollbar-thumb {
|
||
background: var(--m-border-strong);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
/* Desktop (>=1024): main coluna unica. Sem cap de altura nos cards —
|
||
cada um cresce com seu conteudo (Jornada vira maior em "Diferente
|
||
por dia", Online expande com slots, etc). Sem scroll interno: o
|
||
.mac-main / .mac-side__scroll fazem o scroll externo da pagina.
|
||
flex-shrink: 0 nos cards pra eles nao serem comprimidos quando o
|
||
total ultrapassa a altura do .mac-main — o scroll externo engata
|
||
no lugar disso. */
|
||
.mac-main > .mac-w,
|
||
.mac-side > .mac-side__scroll > .mac-w--side {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* ═══════ Card-base ═══════ */
|
||
.mac-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;
|
||
}
|
||
.mac-w--side {
|
||
background: var(--m-bg-medium);
|
||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16);
|
||
}
|
||
.mac-w__head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 12px 14px;
|
||
border-bottom: 1px solid var(--m-border);
|
||
}
|
||
.mac-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);
|
||
}
|
||
.mac-w__icon > i { font-size: 0.95rem; }
|
||
.mac-w__icon--blue { background: color-mix(in srgb, #3b82f6 15%, transparent); color: #3b82f6; }
|
||
.mac-w__icon--amber { background: color-mix(in srgb, #f59e0b 15%, transparent); color: #f59e0b; }
|
||
.mac-w__icon--green { background: color-mix(in srgb, #10b981 15%, transparent); color: #10b981; }
|
||
.mac-w__title { flex: 1; min-width: 0; }
|
||
.mac-w__title-text {
|
||
font-size: 0.92rem;
|
||
font-weight: 700;
|
||
color: var(--m-text);
|
||
line-height: 1.2;
|
||
}
|
||
.mac-w__sub {
|
||
font-size: 0.74rem;
|
||
color: var(--m-text-muted);
|
||
margin-top: 2px;
|
||
line-height: 1.3;
|
||
}
|
||
.mac-w__body {
|
||
padding: 14px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
}
|
||
|
||
/* ═══════ Sidebar: Status items ═══════ */
|
||
.mac-status-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 8px 10px;
|
||
border-radius: 8px;
|
||
background: var(--m-bg-soft);
|
||
border: 1px solid var(--m-border);
|
||
color: var(--m-text);
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
text-align: left;
|
||
transition: background-color 120ms ease, border-color 120ms ease;
|
||
}
|
||
.mac-status-item:hover {
|
||
background: var(--m-bg-soft-hover);
|
||
border-color: var(--m-border-strong);
|
||
}
|
||
.mac-status-item__icon {
|
||
width: 32px;
|
||
height: 32px;
|
||
display: grid;
|
||
place-items: center;
|
||
border-radius: 8px;
|
||
flex-shrink: 0;
|
||
}
|
||
.mac-status-item__icon.is-ok {
|
||
background: rgba(16, 185, 129, 0.14);
|
||
color: rgb(16, 185, 129);
|
||
}
|
||
.mac-status-item__icon.is-pending {
|
||
background: rgba(245, 158, 11, 0.14);
|
||
color: rgb(245, 158, 11);
|
||
}
|
||
.mac-status-item__text { flex: 1; min-width: 0; }
|
||
.mac-status-item__label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 0.82rem;
|
||
font-weight: 700;
|
||
color: var(--m-text);
|
||
}
|
||
.mac-status-item__tag {
|
||
width: 16px;
|
||
height: 16px;
|
||
display: grid;
|
||
place-items: center;
|
||
border-radius: 50%;
|
||
background: rgba(245, 158, 11, 0.20);
|
||
color: rgb(245, 158, 11);
|
||
font-size: 0.66rem;
|
||
font-weight: 800;
|
||
}
|
||
.mac-status-item__tag.is-ok {
|
||
background: rgba(16, 185, 129, 0.20);
|
||
color: rgb(16, 185, 129);
|
||
}
|
||
.mac-status-item__resumo {
|
||
font-size: 0.7rem;
|
||
color: var(--m-text-muted);
|
||
margin-top: 2px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.mac-status-item__chev {
|
||
color: var(--m-text-muted);
|
||
font-size: 0.7rem;
|
||
flex-shrink: 0;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
/* ═══════ Sidebar: FAQ ═══════ */
|
||
.mac-faq {
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
.mac-faq__item {
|
||
display: flex;
|
||
gap: 10px;
|
||
font-size: 0.78rem;
|
||
color: var(--m-text-muted);
|
||
line-height: 1.4;
|
||
}
|
||
.mac-faq__item > div { flex: 1; min-width: 0; }
|
||
.mac-faq__item strong { color: var(--m-text); font-weight: 700; }
|
||
.mac-faq__bullet {
|
||
width: 22px;
|
||
height: 22px;
|
||
border-radius: 50%;
|
||
margin-top: 0;
|
||
flex-shrink: 0;
|
||
display: grid;
|
||
place-items: center;
|
||
color: white;
|
||
font-weight: 700;
|
||
font-size: 0.72rem;
|
||
}
|
||
|
||
/* ═══════ Buttons ═══════ */
|
||
.mac-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;
|
||
}
|
||
.mac-btn:hover { background: var(--m-bg-soft-hover); }
|
||
.mac-btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
||
.mac-btn--primary {
|
||
background: var(--p-primary-color);
|
||
border-color: var(--p-primary-color);
|
||
color: var(--p-primary-contrast-color, #fff);
|
||
}
|
||
.mac-btn--primary:hover {
|
||
background: color-mix(in srgb, var(--p-primary-color) 88%, black);
|
||
}
|
||
|
||
.mac-card-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding-top: 4px;
|
||
border-top: 1px solid var(--m-border);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
/* ═══════ Form ═══════ */
|
||
.mac-field {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
.mac-label {
|
||
font-size: 0.82rem;
|
||
font-weight: 700;
|
||
color: var(--m-text);
|
||
}
|
||
.mac-label__opt {
|
||
font-weight: 400;
|
||
color: var(--m-text-muted);
|
||
opacity: 0.7;
|
||
}
|
||
.mac-label-sm {
|
||
font-size: 0.74rem;
|
||
color: var(--m-text-muted);
|
||
font-weight: 600;
|
||
}
|
||
.mac-hint {
|
||
font-size: 0.72rem;
|
||
color: var(--m-text-muted);
|
||
line-height: 1.3;
|
||
}
|
||
.mac-grid-2 {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 14px;
|
||
}
|
||
|
||
/* ═══════ Days chips ═══════ */
|
||
.mac-days {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
}
|
||
.mac-day {
|
||
padding: 8px 14px;
|
||
border-radius: 999px;
|
||
border: 1.5px solid var(--m-border);
|
||
background: var(--m-bg-soft);
|
||
color: var(--m-text);
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
font-size: 0.82rem;
|
||
font-weight: 700;
|
||
transition: all 120ms ease;
|
||
}
|
||
.mac-day:hover { border-color: var(--m-border-strong); }
|
||
.mac-day.is-active {
|
||
background: var(--p-primary-color);
|
||
border-color: var(--p-primary-color);
|
||
color: var(--p-primary-contrast-color, #fff);
|
||
}
|
||
.mac-day--sm {
|
||
padding: 4px 10px;
|
||
font-size: 0.74rem;
|
||
cursor: default;
|
||
}
|
||
|
||
/* ═══════ Toggle igual/diferente ═══════ */
|
||
.mac-toggle-mode {
|
||
display: flex;
|
||
gap: 4px;
|
||
padding: 4px;
|
||
background: var(--m-bg-medium);
|
||
border: 1px solid var(--m-border);
|
||
border-radius: 9px;
|
||
width: fit-content;
|
||
}
|
||
.mac-mode-btn {
|
||
padding: 6px 14px;
|
||
border-radius: 7px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--m-text-muted);
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
font-size: 0.78rem;
|
||
font-weight: 600;
|
||
transition: all 120ms ease;
|
||
}
|
||
.mac-mode-btn:hover { color: var(--m-text); }
|
||
.mac-mode-btn.is-active {
|
||
background: var(--p-primary-color);
|
||
color: var(--p-primary-contrast-color, #fff);
|
||
}
|
||
|
||
/* ═══════ Equal mode rows ═══════ */
|
||
.mac-equals {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
.mac-equal-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 10px 12px;
|
||
border-radius: 9px;
|
||
background: var(--m-bg-medium);
|
||
border: 1px solid var(--m-border);
|
||
flex-wrap: wrap;
|
||
}
|
||
.mac-equal-row__chips {
|
||
display: flex;
|
||
gap: 4px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.mac-time-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.mac-time-row__lbl {
|
||
font-size: 0.78rem;
|
||
color: var(--m-text-muted);
|
||
}
|
||
.mac-time { width: 110px; }
|
||
|
||
/* ═══════ Different mode rows ═══════ */
|
||
.mac-diff {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
.mac-diff-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 8px 10px;
|
||
border-radius: 8px;
|
||
background: var(--m-bg-medium);
|
||
border: 1px solid var(--m-border);
|
||
flex-wrap: wrap;
|
||
}
|
||
.mac-diff-row__day {
|
||
width: 36px;
|
||
font-size: 0.82rem;
|
||
font-weight: 700;
|
||
color: var(--m-text);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* ═══════ Pausas ═══════ */
|
||
.mac-pausas-by-day {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
.mac-pausa-day-label {
|
||
font-size: 0.74rem;
|
||
font-weight: 700;
|
||
color: var(--m-text-muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
/* ═══════ Presets ritmo ═══════ */
|
||
.mac-presets {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
}
|
||
.mac-preset {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 8px 14px;
|
||
border-radius: 999px;
|
||
border: 1.5px solid var(--m-border);
|
||
background: var(--m-bg-soft);
|
||
color: var(--m-text);
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
font-size: 0.78rem;
|
||
font-weight: 600;
|
||
transition: all 120ms ease;
|
||
}
|
||
.mac-preset:hover { border-color: var(--m-border-strong); }
|
||
.mac-preset.is-active {
|
||
background: var(--p-primary-color);
|
||
border-color: var(--p-primary-color);
|
||
color: var(--p-primary-contrast-color, #fff);
|
||
}
|
||
.mac-preset__dur { font-weight: 700; }
|
||
.mac-preset__gap {
|
||
font-size: 0.7rem;
|
||
opacity: 0.7;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.mac-custom {
|
||
padding: 12px;
|
||
border-radius: 9px;
|
||
background: var(--m-bg-medium);
|
||
border: 1px solid var(--m-border);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
/* ═══════ Toggle row (online) ═══════ */
|
||
.mac-toggle-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
padding: 10px 12px;
|
||
border-radius: 9px;
|
||
background: var(--m-bg-medium);
|
||
border: 1px solid var(--m-border);
|
||
}
|
||
.mac-toggle-row__text { flex: 1; min-width: 0; }
|
||
.mac-toggle-row__label {
|
||
font-size: 0.85rem;
|
||
font-weight: 700;
|
||
color: var(--m-text);
|
||
}
|
||
.mac-toggle-row__hint {
|
||
font-size: 0.72rem;
|
||
color: var(--m-text-muted);
|
||
margin-top: 2px;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.mac-warn {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
padding: 10px 12px;
|
||
border-radius: 9px;
|
||
background: rgba(245, 158, 11, 0.10);
|
||
border: 1px solid rgba(245, 158, 11, 0.30);
|
||
color: rgb(180, 83, 9);
|
||
font-size: 0.78rem;
|
||
line-height: 1.4;
|
||
}
|
||
.mac-warn > i { font-size: 0.92rem; margin-top: 2px; flex-shrink: 0; }
|
||
|
||
/* ═══════ Slots ═══════ */
|
||
.mac-slots-area {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
padding-top: 4px;
|
||
border-top: 1px solid var(--m-border);
|
||
}
|
||
.mac-day-tabs {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
}
|
||
.mac-day-tab {
|
||
padding: 6px 12px;
|
||
border-radius: 7px;
|
||
border: 1px solid var(--m-border);
|
||
background: var(--m-bg-soft);
|
||
color: var(--m-text);
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
font-size: 0.78rem;
|
||
font-weight: 600;
|
||
transition: all 120ms ease;
|
||
}
|
||
.mac-day-tab:hover { border-color: var(--m-border-strong); }
|
||
.mac-day-tab.is-active {
|
||
background: var(--p-primary-color);
|
||
border-color: var(--p-primary-color);
|
||
color: var(--p-primary-contrast-color, #fff);
|
||
}
|
||
.mac-day-tab.has-slots {
|
||
border-color: color-mix(in srgb, var(--p-primary-color) 50%, transparent);
|
||
}
|
||
.mac-day-tab.has-slots::after {
|
||
content: '·';
|
||
margin-left: 3px;
|
||
color: var(--p-primary-color);
|
||
font-weight: 800;
|
||
}
|
||
|
||
.mac-slots-day {
|
||
padding: 12px;
|
||
border-radius: 9px;
|
||
background: var(--m-bg-medium);
|
||
border: 1px solid var(--m-border);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.mac-empty-slots {
|
||
color: var(--m-text-muted);
|
||
font-size: 0.82rem;
|
||
text-align: center;
|
||
padding: 12px;
|
||
}
|
||
|
||
.mac-periodos {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
align-items: center;
|
||
}
|
||
.mac-periodos__sep {
|
||
width: 1px;
|
||
height: 20px;
|
||
background: var(--m-border);
|
||
margin: 0 4px;
|
||
}
|
||
.mac-periodo-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 5px 10px;
|
||
border-radius: 6px;
|
||
border: 1px solid var(--m-border);
|
||
background: var(--m-bg-soft);
|
||
color: var(--m-text);
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
font-size: 0.74rem;
|
||
font-weight: 600;
|
||
transition: all 120ms ease;
|
||
}
|
||
.mac-periodo-btn:hover {
|
||
background: var(--m-bg-soft-hover);
|
||
border-color: var(--m-border-strong);
|
||
}
|
||
.mac-periodo-btn.is-on {
|
||
background: color-mix(in srgb, var(--p-primary-color) 15%, transparent);
|
||
border-color: color-mix(in srgb, var(--p-primary-color) 50%, transparent);
|
||
color: var(--p-primary-color);
|
||
}
|
||
.mac-periodo-btn > i { font-size: 0.7rem; }
|
||
|
||
.mac-slot-chips {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
}
|
||
.mac-slot {
|
||
padding: 5px 10px;
|
||
border-radius: 999px;
|
||
border: 1px solid var(--m-border);
|
||
background: var(--m-bg-soft);
|
||
color: var(--m-text-muted);
|
||
cursor: pointer;
|
||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||
font-size: 0.72rem;
|
||
font-weight: 600;
|
||
transition: all 120ms ease;
|
||
}
|
||
.mac-slot:hover { border-color: var(--m-border-strong); }
|
||
.mac-slot.is-on {
|
||
background: var(--p-primary-color);
|
||
border-color: var(--p-primary-color);
|
||
color: var(--p-primary-contrast-color, #fff);
|
||
}
|
||
|
||
.mac-slots-info {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 6px;
|
||
font-size: 0.72rem;
|
||
color: var(--m-text-muted);
|
||
}
|
||
.mac-slots-info > i {
|
||
font-size: 0.78rem;
|
||
margin-top: 2px;
|
||
color: var(--p-primary-color);
|
||
}
|
||
|
||
.mac-empty-jornada {
|
||
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;
|
||
}
|
||
.mac-empty-jornada > i {
|
||
color: var(--p-primary-color);
|
||
}
|
||
|
||
/* ═══════ Mobile drawer ═══════ */
|
||
.mac-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;
|
||
}
|
||
.mac-mobile-drawer.is-open { transform: translateX(0); }
|
||
.mac-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;
|
||
}
|
||
.mac-mobile-drawer__scroll::-webkit-scrollbar { width: 5px; }
|
||
.mac-mobile-drawer__scroll::-webkit-scrollbar-thumb {
|
||
background: var(--m-border-strong);
|
||
border-radius: 3px;
|
||
}
|
||
/* No mobile a .mac-side e teleportada pra dentro do drawer scroll. */
|
||
.mac-mobile-drawer__scroll .mac-side {
|
||
width: 100%;
|
||
border-right: none;
|
||
}
|
||
|
||
.mac-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;
|
||
}
|
||
.mac-drawer-fade-enter-active,
|
||
.mac-drawer-fade-leave-active { transition: opacity 200ms ease; }
|
||
.mac-drawer-fade-enter-from,
|
||
.mac-drawer-fade-leave-to { opacity: 0; }
|
||
|
||
/* ═══════ Mobile (<1024px) ═══════ */
|
||
@media (max-width: 1023px) {
|
||
.mac-body { flex-direction: column; padding: 0; }
|
||
.mac-body > .mac-side { display: none; }
|
||
.mac-main { width: 100%; padding: 8px; }
|
||
.mac-main .mac-w {
|
||
height: auto;
|
||
flex: 0 0 auto;
|
||
align-self: stretch;
|
||
}
|
||
.mac-grid-2 { grid-template-columns: 1fr; }
|
||
.mac-page__title > span:first-of-type { display: none; }
|
||
.mac-page__title-icon { display: none; }
|
||
.mac-menu-btn--mobile-only { display: inline-flex; }
|
||
.mac-time-row { width: 100%; }
|
||
.mac-time { flex: 1; min-width: 0; }
|
||
}
|
||
</style>
|