diff --git a/O-que-foi-feito.txt b/O-que-foi-feito.txt deleted file mode 100644 index 3fd539b..0000000 --- a/O-que-foi-feito.txt +++ /dev/null @@ -1,101 +0,0 @@ -O que foi feito (até agora) -Usuários de teste criados - -admin@agenciapsi.com.br - — senha: 123Mudar@ - -patient@agenciapsi.com.br - — senha: 123Mudar@ - -therapist@agenciapsi.com.br - — senha: 123Mudar@ - -Base funcionando - -✅ Auth (Supabase) está funcionando -✅ Tabela profiles criada e ok -✅ Trigger automático cria profile após signup -✅ Campo role definido (admin | therapist | patient) -✅ RLS básico ativo -✅ Login funcionando -✅ Logout funcionando -✅ Guard de rota implementado e ativo -✅ RBAC básico operando via meta.role + redirect para painel correto -✅ Home pública / com 3 cards (Admin | Therapist | Patient) levando ao login -✅ Pós-login: busca profiles.role e redireciona para: - -/admin - -/therapist - -/patient - -Estrutura implementada agora (menus e sessão para o Sakai) -Sessão central (evita menu errado e if(role) espalhado) - -✅ Criado src/app/session.js com: - -sessionUser, sessionRole, sessionReady (refs globais) - -initSession() (carrega user + role antes de renderizar o layout) - -listenAuthChanges() (atualiza sessão ao logar/deslogar) - -✅ Ajustado src/main.js para usar bootstrap async: - -chama await initSession() antes de app.mount() - -liga listenAuthChanges() - -mantém PrimeVue, tema Aura, ToastService e ConfirmationService - -mantém imports de CSS existentes - -Menu dinâmico por role no Sakai - -✅ Menus foram estruturados no formato do Sakai (sections com label + items) e separados por role: - -src/navigation/menus/admin.menu.js - -src/navigation/menus/therapist.menu.js - -src/navigation/menus/patient.menu.js - -✅ Criado src/navigation/index.js com getMenuByRole(role) para centralizar a escolha do menu (sem if(role) em componentes). - -✅ Ajustado o AppMenu.vue (menu do Sakai) para: - -usar computed() com sessionRole/sessionReady - -carregar dinamicamente getMenuByRole(sessionRole.value) - -evitar “piscar” menu errado antes de carregar (sessionReady) - -Menu demo do Sakai mantido sem quebrar o produto - -✅ Mantivemos o menu demo (UIKit/Blocks/Start etc.) em arquivo separado para não perder as páginas do template: - -src/navigation/menus/sakai.demo.menu.js (conteúdo original do template) - -✅ Estratégia adotada: - -Admin pode ver o menu demo (idealmente só em DEV) - -Therapist/Patient ficam com menu limpo (clínico) - -Rotas demo do Sakai corrigidas (arquivos com sufixo Doc) - -✅ Problema resolvido: itens do menu demo davam 404 porque as rotas/imports não existiam com os nomes esperados (Input.vue etc.). -✅ Ajuste aplicado: rotas demo apontam para arquivos *Doc.vue (ex.: ButtonDoc.vue, InputDoc.vue). - -📌 Criado/ajustado src/router/routes.demo.js para mapear: - -/uikit/* → @/views/uikit/*Doc.vue - -e demais demos conforme existirem - -✅ Incluído demoRoutes no router principal para o menu demo funcionar. - -Testes - -✅ Confirmado que localStorage.clear() limpa sessão para testar outros usuários/roles rapidamente. \ No newline at end of file diff --git a/checklist-novo-chat.txt b/checklist-novo-chat.txt deleted file mode 100644 index 6f95736..0000000 --- a/checklist-novo-chat.txt +++ /dev/null @@ -1,47 +0,0 @@ -🔁 CONTEXTO DO PROJETO (SaaS multi-tenant) - -Stack: -- Supabase -- Multi-tenant por clinic/tenant -- Assinaturas por tenant (subscriptions.tenant_id) -- Controle de features: features, plan_features, subscription_intents, entitlementsStore, view v_tenant_entitlements -- Ativação manual: activate_subscription_from_intent() -- Merge concluído: agenda_online → online_scheduling.manage -- Entitlements e bloqueio PRO no menu funcionando -- Signup + intent funcionando; ativação cria subscription ativa; view retorna feature correta - -Modelo de “Contas” decidido: -- Auth user (login) ≠ Clínica (tenant) -- Clínica = tenant; Usuário pode ser dono/admin de clínica e também profissional -- Clínica convida usuários (tenant_members). Usuário pode aceitar/recusar. -- Profissional pode trabalhar anos e depois sair: clínica mantém registros; profissional mantém histórico (audit trail), sem acesso após saída. - -Regras de offboarding: -- Profissional só pode sair se NÃO houver agenda futura atribuída a ele. -- Se houver, cria “pedido de saída” e admin precisa realocar/cancelar; depois finaliza saída. - -Tabelas existentes: -- tenant_members: (id uuid pk, tenant_id uuid, user_id uuid, role text, status text, created_at timestamptz) - - UNIQUE (tenant_id, user_id) atualmente -- Agenda: agenda_eventos, agenda_excecoes, agenda_configuracoes, agenda_regras_semanais -- Outros: subscriptions, subscription_intents, plan_features, features, subscription_events - -O que estamos fazendo agora: -- Ajustar modelo de membership lifecycle e offboarding (exit_requests) -- Garantir integridade: histórico de vínculos + auditoria + bloqueio de saída com agenda futura -- Implementar SQL + RPC + RLS + UI (passo a passo) - -✔ subscriptions -Representa o plano da clínica (tenant) -✔ tenant_members -Define quais usuários pertencem à clínica -✔ entitlements - -Define o que aquela clínica pode usar - -Dados que faltam confirmar: -1) Estrutura de agenda_eventos (colunas e como relaciona com profissional) -2) Valores usados em tenant_members.status (active/invited/etc) -3) Estratégia de reentrada: remover UNIQUE (tenant_id,user_id) e usar unique parcial por status ativo/convite -4) Se existe tabela public.users como espelho do auth.users - diff --git a/package-lock.json b/package-lock.json index 58bb217..390382c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,13 @@ "name": "sakai-vue", "version": "5.0.0", "dependencies": { + "@fullcalendar/core": "^6.1.20", + "@fullcalendar/daygrid": "^6.1.20", + "@fullcalendar/interaction": "^6.1.20", + "@fullcalendar/resource": "^6.1.20", + "@fullcalendar/resource-timegrid": "^6.1.20", + "@fullcalendar/timegrid": "^6.1.20", + "@fullcalendar/vue3": "^6.1.20", "@primeuix/themes": "^2.0.0", "@supabase/supabase-js": "^2.95.3", "chart.js": "3.3.2", @@ -513,6 +520,96 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fullcalendar/core": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.20.tgz", + "integrity": "sha512-1cukXLlePFiJ8YKXn/4tMKsy0etxYLCkXk8nUCFi11nRONF2Ba2CD5b21/ovtOO2tL6afTJfwmc1ed3HG7eB1g==", + "dependencies": { + "preact": "~10.12.1" + } + }, + "node_modules/@fullcalendar/daygrid": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.20.tgz", + "integrity": "sha512-AO9vqhkLP77EesmJzuU+IGXgxNulsA8mgQHynclJ8U70vSwAVnbcLG9qftiTAFSlZjiY/NvhE7sflve6cJelyQ==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.20" + } + }, + "node_modules/@fullcalendar/interaction": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.20.tgz", + "integrity": "sha512-p6txmc5txL0bMiPaJxe2ip6o0T384TyoD2KGdsU6UjZ5yoBlaY+dg7kxfnYKpYMzEJLG58n+URrHr2PgNL2fyA==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.20" + } + }, + "node_modules/@fullcalendar/premium-common": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/premium-common/-/premium-common-6.1.20.tgz", + "integrity": "sha512-rT+AitNnRyZuFEtYvsB1OJ2g1Bq2jmTR6qdn/dEU6LwkIj/4L499goLtMOena/JyJ31VBztdHrccX//36QrY3w==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.20" + } + }, + "node_modules/@fullcalendar/resource": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/resource/-/resource-6.1.20.tgz", + "integrity": "sha512-vpQs1eYJbc1zGOzF3obVVr+XsHTMTG7STKVQBEGy3AeFgfosRkUz+3DUawmy98vSjJUYOAQHO+pWW0ek0n5g0w==", + "dependencies": { + "@fullcalendar/premium-common": "~6.1.20" + }, + "peerDependencies": { + "@fullcalendar/core": "~6.1.20" + } + }, + "node_modules/@fullcalendar/resource-daygrid": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/resource-daygrid/-/resource-daygrid-6.1.20.tgz", + "integrity": "sha512-g1rhNsTiGyx6U/01MCjRjQfpmkHpJABoTLS9TR2jcMa7X0SJd2xNd88phoMhIkYdfp+cZ29VOjhwN+3Xg6aohg==", + "dependencies": { + "@fullcalendar/daygrid": "~6.1.20", + "@fullcalendar/premium-common": "~6.1.20" + }, + "peerDependencies": { + "@fullcalendar/core": "~6.1.20", + "@fullcalendar/resource": "~6.1.20" + } + }, + "node_modules/@fullcalendar/resource-timegrid": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/resource-timegrid/-/resource-timegrid-6.1.20.tgz", + "integrity": "sha512-uMf9ERh1c/WeYHg5CPNGxYorkamDzfwUh2o9XS+9fR+KypIIovH1ArflOZF42XFsdrvQx61vDF0alt6/cOqT8Q==", + "dependencies": { + "@fullcalendar/premium-common": "~6.1.20", + "@fullcalendar/resource-daygrid": "~6.1.20", + "@fullcalendar/timegrid": "~6.1.20" + }, + "peerDependencies": { + "@fullcalendar/core": "~6.1.20", + "@fullcalendar/resource": "~6.1.20" + } + }, + "node_modules/@fullcalendar/timegrid": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.20.tgz", + "integrity": "sha512-4H+/MWbz3ntA50lrPif+7TsvMeX3R1GSYjiLULz0+zEJ7/Yfd9pupZmAwUs/PBpA6aAcFmeRr0laWfcz1a9V1A==", + "dependencies": { + "@fullcalendar/daygrid": "~6.1.20" + }, + "peerDependencies": { + "@fullcalendar/core": "~6.1.20" + } + }, + "node_modules/@fullcalendar/vue3": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/vue3/-/vue3-6.1.20.tgz", + "integrity": "sha512-8qg6pS27II9QBwFkkJC+7SfflMpWqOe7i3ii5ODq9KpLAjwQAd/zjfq8RvKR1Yryoh5UmMCmvRbMB7i4RGtqog==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.20", + "vue": "^3.0.11" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -3645,6 +3742,15 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/preact": { + "version": "10.12.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", + "integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4672,6 +4778,73 @@ "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true }, + "@fullcalendar/core": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.20.tgz", + "integrity": "sha512-1cukXLlePFiJ8YKXn/4tMKsy0etxYLCkXk8nUCFi11nRONF2Ba2CD5b21/ovtOO2tL6afTJfwmc1ed3HG7eB1g==", + "requires": { + "preact": "~10.12.1" + } + }, + "@fullcalendar/daygrid": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.20.tgz", + "integrity": "sha512-AO9vqhkLP77EesmJzuU+IGXgxNulsA8mgQHynclJ8U70vSwAVnbcLG9qftiTAFSlZjiY/NvhE7sflve6cJelyQ==", + "requires": {} + }, + "@fullcalendar/interaction": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.20.tgz", + "integrity": "sha512-p6txmc5txL0bMiPaJxe2ip6o0T384TyoD2KGdsU6UjZ5yoBlaY+dg7kxfnYKpYMzEJLG58n+URrHr2PgNL2fyA==", + "requires": {} + }, + "@fullcalendar/premium-common": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/premium-common/-/premium-common-6.1.20.tgz", + "integrity": "sha512-rT+AitNnRyZuFEtYvsB1OJ2g1Bq2jmTR6qdn/dEU6LwkIj/4L499goLtMOena/JyJ31VBztdHrccX//36QrY3w==", + "requires": {} + }, + "@fullcalendar/resource": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/resource/-/resource-6.1.20.tgz", + "integrity": "sha512-vpQs1eYJbc1zGOzF3obVVr+XsHTMTG7STKVQBEGy3AeFgfosRkUz+3DUawmy98vSjJUYOAQHO+pWW0ek0n5g0w==", + "requires": { + "@fullcalendar/premium-common": "~6.1.20" + } + }, + "@fullcalendar/resource-daygrid": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/resource-daygrid/-/resource-daygrid-6.1.20.tgz", + "integrity": "sha512-g1rhNsTiGyx6U/01MCjRjQfpmkHpJABoTLS9TR2jcMa7X0SJd2xNd88phoMhIkYdfp+cZ29VOjhwN+3Xg6aohg==", + "requires": { + "@fullcalendar/daygrid": "~6.1.20", + "@fullcalendar/premium-common": "~6.1.20" + } + }, + "@fullcalendar/resource-timegrid": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/resource-timegrid/-/resource-timegrid-6.1.20.tgz", + "integrity": "sha512-uMf9ERh1c/WeYHg5CPNGxYorkamDzfwUh2o9XS+9fR+KypIIovH1ArflOZF42XFsdrvQx61vDF0alt6/cOqT8Q==", + "requires": { + "@fullcalendar/premium-common": "~6.1.20", + "@fullcalendar/resource-daygrid": "~6.1.20", + "@fullcalendar/timegrid": "~6.1.20" + } + }, + "@fullcalendar/timegrid": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.20.tgz", + "integrity": "sha512-4H+/MWbz3ntA50lrPif+7TsvMeX3R1GSYjiLULz0+zEJ7/Yfd9pupZmAwUs/PBpA6aAcFmeRr0laWfcz1a9V1A==", + "requires": { + "@fullcalendar/daygrid": "~6.1.20" + } + }, + "@fullcalendar/vue3": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/vue3/-/vue3-6.1.20.tgz", + "integrity": "sha512-8qg6pS27II9QBwFkkJC+7SfflMpWqOe7i3ii5ODq9KpLAjwQAd/zjfq8RvKR1Yryoh5UmMCmvRbMB7i4RGtqog==", + "requires": {} + }, "@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -6664,6 +6837,11 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "preact": { + "version": "10.12.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", + "integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==" + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/package.json b/package.json index 4e88faf..a947a66 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,13 @@ "lint": "eslint --fix . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore" }, "dependencies": { + "@fullcalendar/core": "^6.1.20", + "@fullcalendar/daygrid": "^6.1.20", + "@fullcalendar/interaction": "^6.1.20", + "@fullcalendar/resource": "^6.1.20", + "@fullcalendar/resource-timegrid": "^6.1.20", + "@fullcalendar/timegrid": "^6.1.20", + "@fullcalendar/vue3": "^6.1.20", "@primeuix/themes": "^2.0.0", "@supabase/supabase-js": "^2.95.3", "chart.js": "3.3.2", diff --git a/src/App.vue b/src/App.vue index ca93f6d..43cb4d2 100644 --- a/src/App.vue +++ b/src/App.vue @@ -6,20 +6,35 @@ import { useEntitlementsStore } from '@/stores/entitlementsStore' const route = useRoute() -const tenant = useTenantStore() -const ent = useEntitlementsStore() +const tenantStore = useTenantStore() +const entStore = useEntitlementsStore() onMounted(async () => { - await tenant.loadSessionAndTenant() - await ent.loadForTenant(tenant.activeTenantId) + // 1) carrega sessão + tenant ativo (do seu fluxo atual) + await tenantStore.loadSessionAndTenant() - // pode remover esses logs depois - console.log('tenant.activeTenantId', tenant.activeTenantId) - console.log('role', tenant.activeRole) - console.log('can online_scheduling.manage?', ent.can('online_scheduling.manage')) + // 2) carrega permissões do tenant ativo (se existir) + if (tenantStore.activeTenantId) { + await entStore.loadForTenant(tenantStore.activeTenantId) + } + + // 3) debug: localStorage com rótulos + console.groupCollapsed('[Debug] Tenant localStorage') + console.log('tenant_id:', localStorage.getItem('tenant_id')) + console.log('currentTenantId:', localStorage.getItem('currentTenantId')) + console.log('tenant:', localStorage.getItem('tenant')) + console.groupEnd() + + // 4) debug: stores + console.groupCollapsed('[Debug] Tenant stores') + console.log('route:', route.fullPath) + console.log('activeTenantId:', tenantStore.activeTenantId) + console.log('activeRole:', tenantStore.activeRole) + console.log("can('online_scheduling.manage'):", entStore.can('online_scheduling.manage')) + console.groupEnd() }) + \ No newline at end of file diff --git a/src/app/session.js b/src/app/session.js index 2776226..65dc672 100644 --- a/src/app/session.js +++ b/src/app/session.js @@ -219,4 +219,4 @@ export function stopAuthChanges () { authSubscription.unsubscribe() authSubscription = null } -} +} \ No newline at end of file diff --git a/src/components/agenda/PausasChipsEditor.vue b/src/components/agenda/PausasChipsEditor.vue new file mode 100644 index 0000000..b978def --- /dev/null +++ b/src/components/agenda/PausasChipsEditor.vue @@ -0,0 +1,246 @@ + + + + \ No newline at end of file diff --git a/src/constants/roles.js b/src/constants/roles.js index b783efa..3bac3c5 100644 --- a/src/constants/roles.js +++ b/src/constants/roles.js @@ -1,11 +1,49 @@ +// src/constants/roles.js + +/** + * Roles canônicas do sistema (tenant-level) + * Esses valores devem refletir exatamente o que existe no banco. + */ export const ROLES = { - ADMIN: 'admin', + CLINIC_ADMIN: 'clinic_admin', + TENANT_ADMIN: 'tenant_admin', // legado (compatibilidade) THERAPIST: 'therapist', PATIENT: 'patient' } -export const ROLE_HOME = { - admin: '/admin', - therapist: '/therapist', - patient: '/patient' + +/** + * Retorna a rota base (home) de cada role. + * Usado após login, guards e redirecionamentos. + */ +export function roleToHome(role) { + switch (role) { + case ROLES.CLINIC_ADMIN: + case ROLES.TENANT_ADMIN: // compatibilidade + return '/admin' + + case ROLES.THERAPIST: + return '/therapist' + + case ROLES.PATIENT: + return '/portal' + + default: + return '/' + } } + + +/** + * Lista todas as roles válidas + * Útil para validações e guards + */ +export const ALL_ROLES = Object.values(ROLES) + + +/** + * Verifica se uma role é válida + */ +export function isValidRole(role) { + return ALL_ROLES.includes(role) +} \ No newline at end of file diff --git a/src/features/agenda/components/AdicionarCompromissoPage.vue b/src/features/agenda/components/AdicionarCompromissoPage.vue new file mode 100644 index 0000000..7915af7 --- /dev/null +++ b/src/features/agenda/components/AdicionarCompromissoPage.vue @@ -0,0 +1,1004 @@ + + + + + \ No newline at end of file diff --git a/src/features/agenda/components/AgendaCalendar.vue b/src/features/agenda/components/AgendaCalendar.vue new file mode 100644 index 0000000..abce031 --- /dev/null +++ b/src/features/agenda/components/AgendaCalendar.vue @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/src/features/agenda/components/AgendaClinicCalendar.vue b/src/features/agenda/components/AgendaClinicCalendar.vue new file mode 100644 index 0000000..a18fd72 --- /dev/null +++ b/src/features/agenda/components/AgendaClinicCalendar.vue @@ -0,0 +1,100 @@ + + + + \ No newline at end of file diff --git a/src/features/agenda/components/AgendaClinicMosaic.vue b/src/features/agenda/components/AgendaClinicMosaic.vue new file mode 100644 index 0000000..3a17a8c --- /dev/null +++ b/src/features/agenda/components/AgendaClinicMosaic.vue @@ -0,0 +1,192 @@ + + + \ No newline at end of file diff --git a/src/features/agenda/components/AgendaEventDialog.vue b/src/features/agenda/components/AgendaEventDialog.vue new file mode 100644 index 0000000..d9b373a --- /dev/null +++ b/src/features/agenda/components/AgendaEventDialog.vue @@ -0,0 +1,243 @@ + + +