Correcao Sidebar Classico e Rail, Correcao Layout, Ajuste de Breakpoint para Tailwind, Ajuste AppTopbar, Ajuste Menu PopOver, Recriado Paleta de Cores, Inserido algumas animações leves, Reajuste Cor items NOVOS da tabela, Drawer Ajuda Corrigido no Logout, Whatsapp, sms, email, recursos extras

This commit is contained in:
Leonardo
2026-03-24 21:26:58 -03:00
parent a89d1f5560
commit 53a4980396
453 changed files with 121427 additions and 174407 deletions
+114 -108
View File
@@ -14,135 +14,141 @@
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client'
import { useLayout } from '@/layout/composables/layout'
import { $t, updatePreset, updateSurfacePalette } from '@primeuix/themes'
import Aura from '@primeuix/themes/aura'
import Lara from '@primeuix/themes/lara'
import Nora from '@primeuix/themes/nora'
import { supabase } from '@/lib/supabase/client';
import { useLayout } from '@/layout/composables/layout';
import { $t, updatePreset, updateSurfacePalette } from '@primeuix/themes';
import Aura from '@primeuix/themes/aura';
import Lara from '@primeuix/themes/lara';
import Nora from '@primeuix/themes/nora';
const presets = { Aura, Lara, Nora }
const presets = { Aura, Lara, Nora };
function safeEq (a, b) {
return String(a || '').trim() === String(b || '').trim()
function safeEq(a, b) {
return String(a || '').trim() === String(b || '').trim();
}
// copia do seu getPresetExt (ou exporta ele do Perfil pra reutilizar)
function getPresetExt(primaryColors, layoutConfig) {
const color = primaryColors.find((c) => c.name === layoutConfig.primary) || { name: 'noir', palette: {} }
const color = primaryColors.find((c) => c.name === layoutConfig.primary) || { name: 'noir', palette: {} };
if (color.name === 'noir') {
return {
semantic: {
primary: {
50: '{surface.50}',
100: '{surface.100}',
200: '{surface.200}',
300: '{surface.300}',
400: '{surface.400}',
500: '{surface.500}',
600: '{surface.600}',
700: '{surface.700}',
800: '{surface.800}',
900: '{surface.900}',
950: '{surface.950}'
},
colorScheme: {
light: {
primary: { color: '{primary.950}', contrastColor: '#ffffff', hoverColor: '{primary.800}', activeColor: '{primary.700}' },
highlight: { background: '{primary.950}', focusBackground: '{primary.700}', color: '#ffffff', focusColor: '#ffffff' }
},
dark: {
primary: { color: '{primary.50}', contrastColor: '{primary.950}', hoverColor: '{primary.200}', activeColor: '{primary.300}' },
highlight: { background: '{primary.50}', focusBackground: '{primary.300}', color: '{primary.950}', focusColor: '{primary.950}' }
}
}
}
};
}
if (color.name === 'noir') {
return {
semantic: {
primary: {
50: '{surface.50}', 100: '{surface.100}', 200: '{surface.200}', 300: '{surface.300}',
400: '{surface.400}', 500: '{surface.500}', 600: '{surface.600}', 700: '{surface.700}',
800: '{surface.800}', 900: '{surface.900}', 950: '{surface.950}'
},
colorScheme: {
light: {
primary: { color: '{primary.950}', contrastColor: '#ffffff', hoverColor: '{primary.800}', activeColor: '{primary.700}' },
highlight: { background: '{primary.950}', focusBackground: '{primary.700}', color: '#ffffff', focusColor: '#ffffff' }
},
dark: {
primary: { color: '{primary.50}', contrastColor: '{primary.950}', hoverColor: '{primary.200}', activeColor: '{primary.300}' },
highlight: { background: '{primary.50}', focusBackground: '{primary.300}', color: '{primary.950}', focusColor: '{primary.950}' }
}
semantic: {
primary: color.palette,
colorScheme: {
light: {
primary: { color: '{primary.500}', contrastColor: '#ffffff', hoverColor: '{primary.600}', activeColor: '{primary.700}' },
highlight: { background: '{primary.50}', focusBackground: '{primary.100}', color: '{primary.700}', focusColor: '{primary.800}' }
},
dark: {
primary: { color: '{primary.400}', contrastColor: '{surface.900}', hoverColor: '{primary.300}', activeColor: '{primary.200}' },
highlight: {
background: 'color-mix(in srgb, {primary.400}, transparent 84%)',
focusBackground: 'color-mix(in srgb, {primary.400}, transparent 76%)',
color: 'rgba(255,255,255,.87)',
focusColor: 'rgba(255,255,255,.87)'
}
}
}
}
}
}
}
return {
semantic: {
primary: color.palette,
colorScheme: {
light: {
primary: { color: '{primary.500}', contrastColor: '#ffffff', hoverColor: '{primary.600}', activeColor: '{primary.700}' },
highlight: { background: '{primary.50}', focusBackground: '{primary.100}', color: '{primary.700}', focusColor: '{primary.800}' }
},
dark: {
primary: { color: '{primary.400}', contrastColor: '{surface.900}', hoverColor: '{primary.300}', activeColor: '{primary.200}' },
highlight: {
background: 'color-mix(in srgb, {primary.400}, transparent 84%)',
focusBackground: 'color-mix(in srgb, {primary.400}, transparent 76%)',
color: 'rgba(255,255,255,.87)',
focusColor: 'rgba(255,255,255,.87)'
}
}
}
}
}
};
}
export async function bootstrapUserSettings({
primaryColors = [], // passe a lista do seu Perfil (ou uma versão reduzida)
surfaces = [] // idem
primaryColors = [], // passe a lista do seu Perfil (ou uma versão reduzida)
surfaces = [] // idem
} = {}) {
const { layoutConfig, isDarkTheme, toggleDarkMode, changeMenuMode, setVariant } = useLayout()
const { layoutConfig, isDarkTheme, toggleDarkMode, changeMenuMode, setVariant } = useLayout();
const { data: uRes, error: uErr } = await supabase.auth.getUser()
if (uErr) return
const user = uRes?.user
if (!user) return
const { data: uRes, error: uErr } = await supabase.auth.getUser();
if (uErr) return;
const user = uRes?.user;
if (!user) return;
const { data: settings, error } = await supabase
.from('user_settings')
.select('theme_mode, preset, primary_color, surface_color, menu_mode, layout_variant')
.eq('user_id', user.id)
.maybeSingle()
const { data: settings, error } = await supabase.from('user_settings').select('theme_mode, preset, primary_color, surface_color, menu_mode, layout_variant').eq('user_id', user.id).maybeSingle();
if (error || !settings) return
if (error || !settings) return;
// layout variant: respeita a preferência já gravada no localStorage.
// Se localStorage está vazio, só aplica 'rail' do banco (confirma o padrão).
// Nunca aplica 'classic' automaticamente quando não há preferência local —
// dado antigo no banco não deve sobrescrever o padrão do app.
const _lsV = (() => {
try {
const v = localStorage.getItem('layout_variant')
return (v === 'rail' || v === 'classic') ? v : null
} catch { return null }
})()
// layout variant: respeita a preferência já gravada no localStorage.
// Se localStorage está vazio, só aplica 'rail' do banco (confirma o padrão).
// Nunca aplica 'classic' automaticamente quando não há preferência local —
// dado antigo no banco não deve sobrescrever o padrão do app.
const _lsV = (() => {
try {
const v = localStorage.getItem('layout_variant');
return v === 'rail' || v === 'classic' ? v : null;
} catch {
return null;
}
})();
if (_lsV !== null) {
// localStorage já tem valor → aplica ele (garante coerência com layoutConfig)
if (_lsV !== (settings.layout_variant ?? _lsV)) setVariant(_lsV)
} else if (settings.layout_variant === 'rail') {
// localStorage vazio + banco tem 'rail' → aplica e grava no localStorage
setVariant('rail')
}
// localStorage vazio + banco tem 'classic' → mantém padrão 'rail' (não aplica)
if (_lsV !== null) {
// localStorage já tem valor → aplica ele (garante coerência com layoutConfig)
if (_lsV !== (settings.layout_variant ?? _lsV)) setVariant(_lsV);
} else if (settings.layout_variant === 'rail') {
// localStorage vazio + banco tem 'rail' → aplica e grava no localStorage
setVariant('rail');
}
// localStorage vazio + banco tem 'classic' → mantém padrão 'rail' (não aplica)
// menu mode
if (settings.menu_mode && settings.menu_mode !== layoutConfig.menuMode) {
layoutConfig.menuMode = settings.menu_mode
changeMenuMode()
}
// menu mode
if (settings.menu_mode && settings.menu_mode !== layoutConfig.menuMode) {
layoutConfig.menuMode = settings.menu_mode;
changeMenuMode();
}
// preset
if (settings.preset && settings.preset !== layoutConfig.preset) {
layoutConfig.preset = settings.preset
const presetValue = presets[settings.preset] || presets.Aura
const surfacePalette = surfaces.find(s => s.name === layoutConfig.surface)?.palette
$t().preset(presetValue).preset(getPresetExt(primaryColors, layoutConfig)).surfacePalette(surfacePalette).use({ useDefaultOptions: true })
}
// preset
if (settings.preset && settings.preset !== layoutConfig.preset) {
layoutConfig.preset = settings.preset;
const presetValue = presets[settings.preset] || presets.Aura;
const surfacePalette = surfaces.find((s) => s.name === layoutConfig.surface)?.palette;
$t().preset(presetValue).preset(getPresetExt(primaryColors, layoutConfig)).surfacePalette(surfacePalette).use({ useDefaultOptions: true });
}
// colors
if (settings.primary_color && !safeEq(settings.primary_color, layoutConfig.primary)) {
layoutConfig.primary = settings.primary_color
updatePreset(getPresetExt(primaryColors, layoutConfig))
}
// colors
if (settings.primary_color && !safeEq(settings.primary_color, layoutConfig.primary)) {
layoutConfig.primary = settings.primary_color;
updatePreset(getPresetExt(primaryColors, layoutConfig));
}
if (settings.surface_color && !safeEq(settings.surface_color, layoutConfig.surface)) {
layoutConfig.surface = settings.surface_color
const surface = surfaces.find(s => s.name === settings.surface_color)
if (surface) updateSurfacePalette(surface.palette)
}
if (settings.surface_color && !safeEq(settings.surface_color, layoutConfig.surface)) {
layoutConfig.surface = settings.surface_color;
const surface = surfaces.find((s) => s.name === settings.surface_color);
if (surface) updateSurfacePalette(surface.palette);
}
// dark/light
if (settings.theme_mode) {
const shouldBeDark = settings.theme_mode === 'dark'
if (shouldBeDark !== isDarkTheme) toggleDarkMode()
}
// dark/light
if (settings.theme_mode) {
const shouldBeDark = settings.theme_mode === 'dark';
if (shouldBeDark !== isDarkTheme) toggleDarkMode();
}
}
+133 -141
View File
@@ -14,8 +14,8 @@
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
import { ref } from 'vue'
import { supabase } from '@/lib/supabase/client'
import { ref } from 'vue';
import { supabase } from '@/lib/supabase/client';
/**
* ⚠️ IMPORTANTE — ESTABILIDADE DE NAVEGAÇÃO
@@ -42,197 +42,189 @@ import { supabase } from '@/lib/supabase/client'
* entre sessão, guard e carregamento de tenant/entitlements.
*/
export const sessionUser = ref(null)
export const sessionRole = ref(null)
export const sessionIsSaasAdmin = ref(false)
export const sessionUser = ref(null);
export const sessionRole = ref(null);
export const sessionIsSaasAdmin = ref(false);
// só no primeiro boot
export const sessionReady = ref(false)
export const sessionReady = ref(false);
// refresh leve (troca de aba / refresh token) sem desmontar UI
export const sessionRefreshing = ref(false)
export const sessionRefreshing = ref(false);
let onSignedOutCallback = null
export function setOnSignedOut (cb) {
onSignedOutCallback = typeof cb === 'function' ? cb : null
let onSignedOutCallback = null;
export function setOnSignedOut(cb) {
onSignedOutCallback = typeof cb === 'function' ? cb : null;
}
// evita init concorrente
let initPromise = null
let initPromise = null;
async function fetchRole (userId) {
const { data, error } = await supabase
.from('profiles')
.select('role')
.eq('id', userId)
.single()
async function fetchRole(userId) {
const { data, error } = await supabase.from('profiles').select('role').eq('id', userId).single();
if (error) return null
return data?.role || null
if (error) return null;
return data?.role || null;
}
async function fetchIsSaasAdmin (userId) {
const { data, error } = await supabase
.from('saas_admins')
.select('user_id')
.eq('user_id', userId)
.maybeSingle()
async function fetchIsSaasAdmin(userId) {
const { data, error } = await supabase.from('saas_admins').select('user_id').eq('user_id', userId).maybeSingle();
if (error) return false
return !!data
if (error) return false;
return !!data;
}
/**
* Atualiza estado a partir de uma session "confiável" (getSession() ou callback do auth).
* ⚠️ NÃO zera user/role durante refresh enquanto existir sessão.
*/
async function hydrateFromSession (sess) {
const user = sess?.user || null
if (!user?.id) return false
async function hydrateFromSession(sess) {
const user = sess?.user || null;
if (!user?.id) return false;
const prevUid = sessionUser.value?.id || null
const uid = user.id
const prevUid = sessionUser.value?.id || null;
const uid = user.id;
// ✅ pega primeiro hydrate e troca de usuário
const userChanged = prevUid !== uid
// ✅ pega primeiro hydrate e troca de usuário
const userChanged = prevUid !== uid;
// atualiza user imediatamente (sem flicker)
sessionUser.value = user
// atualiza user imediatamente (sem flicker)
sessionUser.value = user;
// ✅ saas admin: calcula no primeiro hydrate e sempre que trocar de user
// (no primeiro hydrate prevUid é null, então userChanged = true)
if (userChanged) {
sessionIsSaasAdmin.value = await fetchIsSaasAdmin(uid)
}
// ✅ saas admin: calcula no primeiro hydrate e sempre que trocar de user
// (no primeiro hydrate prevUid é null, então userChanged = true)
if (userChanged) {
sessionIsSaasAdmin.value = await fetchIsSaasAdmin(uid);
}
// role: busca se não tem, ou se mudou user
if (!sessionRole.value || userChanged) {
sessionRole.value = await fetchRole(uid)
}
// role: busca se não tem, ou se mudou user
if (!sessionRole.value || userChanged) {
sessionRole.value = await fetchRole(uid);
}
return true
return true;
}
/**
* Boot inicial (pode bloquear UI) ou refresh (não pode derrubar menu).
*/
export async function initSession ({ initial = false } = {}) {
if (initPromise) return initPromise
export async function initSession({ initial = false } = {}) {
if (initPromise) return initPromise;
if (initial) sessionReady.value = false
else sessionRefreshing.value = true
if (initial) sessionReady.value = false;
else sessionRefreshing.value = true;
initPromise = (async () => {
try {
const { data, error } = await supabase.auth.getSession();
if (error) throw error;
const sess = data?.session || null;
const ok = await hydrateFromSession(sess);
// se não tem sessão, zera estado (aqui pode, porque é init/refresh controlado)
if (!ok) {
sessionUser.value = null;
sessionRole.value = null;
sessionIsSaasAdmin.value = false;
}
} catch (e) {
console.warn('[initSession] getSession falhou (tratando como sem sessão):', e);
// não deixa estourar pro router guard
sessionUser.value = null;
sessionRole.value = null;
sessionIsSaasAdmin.value = false;
}
})();
initPromise = (async () => {
try {
const { data, error } = await supabase.auth.getSession()
if (error) throw error
const sess = data?.session || null
const ok = await hydrateFromSession(sess)
// se não tem sessão, zera estado (aqui pode, porque é init/refresh controlado)
if (!ok) {
sessionUser.value = null
sessionRole.value = null
sessionIsSaasAdmin.value = false
}
} catch (e) {
console.warn('[initSession] getSession falhou (tratando como sem sessão):', e)
// não deixa estourar pro router guard
sessionUser.value = null
sessionRole.value = null
sessionIsSaasAdmin.value = false
await initPromise;
} finally {
initPromise = null;
if (initial) sessionReady.value = true;
sessionRefreshing.value = false;
}
})()
try {
await initPromise
} finally {
initPromise = null
if (initial) sessionReady.value = true
sessionRefreshing.value = false
}
}
// refresh leve (troca de aba etc.)
export async function refreshSession () {
// ✅ evita corrida: se já está refreshing/init, não dispara outro
if (sessionRefreshing.value || initPromise) return
export async function refreshSession() {
// ✅ evita corrida: se já está refreshing/init, não dispara outro
if (sessionRefreshing.value || initPromise) return;
const { data, error } = await supabase.auth.getSession()
if (error) return
const { data, error } = await supabase.auth.getSession();
if (error) return;
const sess = data?.session || null
const uid = sess?.user?.id || null
const sess = data?.session || null;
const uid = sess?.user?.id || null;
// se não tem sessão, não zera aqui (deixa SIGNED_OUT cuidar)
if (!uid) return
// se não tem sessão, não zera aqui (deixa SIGNED_OUT cuidar)
if (!uid) return;
// se já está consistente, não faz nada
if (sessionUser.value?.id === uid && sessionRole.value) return
// se já está consistente, não faz nada
if (sessionUser.value?.id === uid && sessionRole.value) return;
await initSession({ initial: false })
await initSession({ initial: false });
}
// evita múltiplos listeners
let authSubscription = null
let authSubscription = null;
export function listenAuthChanges () {
if (authSubscription) return
export function listenAuthChanges() {
if (authSubscription) return;
const { data } = supabase.auth.onAuthStateChange(async (event, sess) => {
console.log('[AUTH EVENT]', event)
const { data } = supabase.auth.onAuthStateChange(async (event, sess) => {
console.log('[AUTH EVENT]', event);
// ✅ SIGNED_OUT: zera e chama callback
if (event === 'SIGNED_OUT') {
sessionUser.value = null
sessionRole.value = null
sessionIsSaasAdmin.value = false
sessionRefreshing.value = false
sessionReady.value = true
if (onSignedOutCallback) onSignedOutCallback()
return
}
// ✅ SIGNED_OUT: zera e chama callback
if (event === 'SIGNED_OUT') {
sessionUser.value = null;
sessionRole.value = null;
sessionIsSaasAdmin.value = false;
sessionRefreshing.value = false;
sessionReady.value = true;
if (onSignedOutCallback) onSignedOutCallback();
return;
}
// ✅ se já está consistente, ignora SIGNED_IN redundante
if (event === 'SIGNED_IN') {
const uid = sess?.user?.id || null
if (uid && sessionReady.value && sessionUser.value?.id === uid && sessionRole.value) {
return
}
}
// ✅ se já está consistente, ignora SIGNED_IN redundante
if (event === 'SIGNED_IN') {
const uid = sess?.user?.id || null;
if (uid && sessionReady.value && sessionUser.value?.id === uid && sessionRole.value) {
return;
}
}
// ✅ use a session fornecida no callback
if (sess?.user?.id) {
// evita reentrância
if (sessionRefreshing.value) return
// ✅ use a session fornecida no callback
if (sess?.user?.id) {
// evita reentrância
if (sessionRefreshing.value) return;
sessionRefreshing.value = true
try {
await hydrateFromSession(sess)
sessionReady.value = true
} catch (e) {
console.warn('[auth hydrate error]', e)
} finally {
sessionRefreshing.value = false
}
return
}
sessionRefreshing.value = true;
try {
await hydrateFromSession(sess);
sessionReady.value = true;
} catch (e) {
console.warn('[auth hydrate error]', e);
} finally {
sessionRefreshing.value = false;
}
return;
}
// fallback: refresh leve
try {
await refreshSession()
} catch (e) {
console.error('[refreshSession error]', e)
}
})
// fallback: refresh leve
try {
await refreshSession();
} catch (e) {
console.error('[refreshSession error]', e);
}
});
authSubscription = data?.subscription || null
authSubscription = data?.subscription || null;
}
export function stopAuthChanges () {
if (authSubscription) {
authSubscription.unsubscribe()
authSubscription = null
}
}
export function stopAuthChanges() {
if (authSubscription) {
authSubscription.unsubscribe();
authSubscription = null;
}
}