feat(plugins): Salas como add-on por conta (pessoa-cêntrico) + Visualizar como biomédico

Superadmin / Visualizar como:
- adiciona BIOMÉDICO(A) à lista de preview do superadmin
- enterPreview ativa os plugins (RX/Salas/Profissionais) p/ teste; exitPreview restaura

Locação de Salas — ativação POR USUÁRIO (independe de clínica):
- backend: tabela usuario_plugins + GET/POST /api/me/plugins (authGuard, sem tenant)
- sala continua dona=owner_usuario_id (pessoa) + salaID, clinica_id opcional
- backend.ts: getMyPlugins/setMyPlugin/hydrateMyPlugins; login + App montam o hydrate
- isViewAllowed: 'salas' liberado se plugin ativo na conta; catálogo 'plugins' p/ não-paciente
- Sidebar: entrada ADD-ONS/PLUGINS p/ papéis operacionais ativarem
- PluginsView: toggle de locacao-salas persiste no servidor
- escopo: só locacao-salas é server-backed (RX/Profissionais seguem localStorage)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Builder
2026-06-13 15:04:25 +02:00
parent 98728b4e57
commit ca7eba4531
5 changed files with 108 additions and 1 deletions
+50
View File
@@ -75,6 +75,9 @@ export const HybridBackend = {
localStorage.setItem('SCOREODONTO_ACTIVE_WORKSPACE', JSON.stringify(data.workspaces[0]));
}
// Hidrata os plugins ativados na conta do usuário (fonte da verdade no servidor).
await HybridBackend.hydrateMyPlugins();
if (data.noWorkspace) {
return { ok: true, noWorkspace: true, userRole: data.userRole };
}
@@ -256,6 +259,12 @@ export const HybridBackend = {
getPreviewRole: (): string | null => localStorage.getItem('SCOREODONTO_PREVIEW_ROLE'),
enterPreview: (role: string) => {
// Na primeira entrada do preview, guarda os plugins reais do superadmin p/ restaurar no exit.
// (não sobrescreve o backup se já estiver previewando e só trocando de papel)
if (!localStorage.getItem('SCOREODONTO_PREVIEW_ROLE')) {
const prevPlugins = localStorage.getItem('SCOREODONTO_ACTIVE_PLUGINS');
localStorage.setItem('SCOREODONTO_PREVIEW_PREV_PLUGINS', prevPlugins === null ? '__none__' : prevPlugins);
}
const synthetic = {
id: '00000000-0000-0000-0000-000000000000', // UUID inexistente → consultas retornam vazio
nome: `VISUALIZAÇÃO: ${role.toUpperCase()}`,
@@ -266,6 +275,8 @@ export const HybridBackend = {
localStorage.setItem('SCOREODONTO_PREVIEW_ROLE', role);
localStorage.setItem('SCOREODONTO_ACTIVE_WORKSPACE', JSON.stringify(synthetic));
localStorage.setItem('SCOREODONTO_WORKSPACES', JSON.stringify([synthetic]));
// Preview ativa os plugins para o superadmin ver/testar as telas (RX, Salas, Profissionais).
localStorage.setItem('SCOREODONTO_ACTIVE_PLUGINS', JSON.stringify(['rx-scoreodonto', 'locacao-salas', 'profissionais-marketplace']));
const path = role === 'paciente' ? '/tratamentos' : '/dashboard';
window.location.assign(path);
},
@@ -274,9 +285,48 @@ export const HybridBackend = {
localStorage.removeItem('SCOREODONTO_PREVIEW_ROLE');
localStorage.removeItem('SCOREODONTO_ACTIVE_WORKSPACE');
localStorage.removeItem('SCOREODONTO_WORKSPACES');
// Restaura os plugins que o superadmin tinha antes do preview.
const prev = localStorage.getItem('SCOREODONTO_PREVIEW_PREV_PLUGINS');
if (prev === null || prev === '__none__') localStorage.removeItem('SCOREODONTO_ACTIVE_PLUGINS');
else localStorage.setItem('SCOREODONTO_ACTIVE_PLUGINS', prev);
localStorage.removeItem('SCOREODONTO_PREVIEW_PREV_PLUGINS');
window.location.assign('/superadmin/overview');
},
// --- PLUGINS POR USUÁRIO (add-on por conta; fonte da verdade no servidor) ---
getMyPlugins: async (): Promise<string[]> => {
try {
const res = await apiFetch(`${API_URL}/me/plugins`);
if (!res.ok) return [];
const d = await res.json();
return Array.isArray(d?.plugins) ? d.plugins : [];
} catch { return []; }
},
setMyPlugin: async (pluginId: string, ativo: boolean): Promise<boolean> => {
try {
const res = await apiFetch(`${API_URL}/me/plugins`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pluginId, ativo }),
});
return res.ok;
} catch { return false; }
},
// Reconcilia o cache local (SCOREODONTO_ACTIVE_PLUGINS) com o servidor.
// Escopo atual: só 'locacao-salas' é server-backed; demais plugins seguem locais.
// Não mexe durante o modo visualização (preview sobrescreve os plugins temporariamente).
hydrateMyPlugins: async (): Promise<void> => {
if (localStorage.getItem('SCOREODONTO_PREVIEW_ROLE')) return;
const server = await HybridBackend.getMyPlugins();
let local: string[] = [];
try { local = JSON.parse(localStorage.getItem('SCOREODONTO_ACTIVE_PLUGINS') || '[]'); } catch { local = []; }
const next = local.filter(p => p !== 'locacao-salas');
if (server.includes('locacao-salas')) next.push('locacao-salas');
localStorage.setItem('SCOREODONTO_ACTIVE_PLUGINS', JSON.stringify(next));
},
isAuthenticated: (): boolean => {
return !!localStorage.getItem('SCOREODONTO_AUTH_TOKEN');
},