feat(sidebar): fleet sidebar standard (header row, user in sidebar, collapsible groups, mini mode, no-flash transitions)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b73c23bf55
commit
aa55d44406
8 files changed
+644
-215
No files matched your search
@@ -137,3 +137,18 @@ or adapted for production, and these accounts must never exist in a production d
|
|||||||
- DEV ONLY, do platform-tools (`functions_dev.php`, README "Dev tooling"; produção segue `main`, que não tem o ficheiro: tudo degrada para "não é dev").
|
- DEV ONLY, do platform-tools (`functions_dev.php`, README "Dev tooling"; produção segue `main`, que não tem o ficheiro: tudo degrada para "não é dev").
|
||||||
- Ficheiros: `config/dev_guard.php` (shim), `includes/dev_toolbar.php` (incluído no fim de `includes/footer.php`), `dev_login.php` (na RAIZ: o site não tem `public/`; `.htaccess` tem a regra `^dev_login/?$` com `-f`), `scripts/dev_accounts.php` (`platform_dev_accounts_run`).
|
- Ficheiros: `config/dev_guard.php` (shim), `includes/dev_toolbar.php` (incluído no fim de `includes/footer.php`), `dev_login.php` (na RAIZ: o site não tem `public/`; `.htaccess` tem a regra `^dev_login/?$` com `-f`), `scripts/dev_accounts.php` (`platform_dev_accounts_run`).
|
||||||
- Contas `admin@mail.pt` / `sysadmin@mail.pt` (pw `123`). O login usa `auth_login()` + `fit_stamp_login_ua()` como o login real; CSRF do site = campo `_csrf` / `$_SESSION['csrf_token']` (verificação sem `exit`).
|
- Contas `admin@mail.pt` / `sysadmin@mail.pt` (pw `123`). O login usa `auth_login()` + `fit_stamp_login_ua()` como o login real; CSRF do site = campo `_csrf` / `$_SESSION['csrf_token']` (verificação sem `exit`).
|
||||||
|
|
||||||
|
## Ambiente de dev: moldura e login de um clique (2026-10-13)
|
||||||
|
- `includes/env_ribbon.php` (moldura às riscas + etiqueta DEV, só fora de `APP_ENV=prod`) incluído no `<head>` de `includes/header.php`, `includes/auth_header.php` e `site/includes/include.php`.
|
||||||
|
- A barra de login de dev (`includes/dev_toolbar.php`) vai também em `includes/auth_footer.php` e `site/includes/footer.php`. Botões Cliente / Staff / Admin / Sysadmin (`client@mail.pt` e `staff@mail.pt` criados por `scripts/dev_accounts.php`; o painel do staff é `views/staff/dashboard.php`, criado em 2026-10-13).
|
||||||
|
- Auditoria da sidebar: `docs/audits/2026-10-13-fitme3070-sidebar.md` (em Y:/GitTea; aplicada, ver «Barra lateral» abaixo).
|
||||||
|
|
||||||
|
## Barra lateral (2026-10-13)
|
||||||
|
- Padrão da frota (`.claude/skills/sysadmin-navbar-audit`). CSS todo em `assets/css/backoffice.css` (tokens `--bo-*`, casca, mini, view transitions; carregado com `?v=filemtime` por `_asset_versioned`); já não há `<style>` inline no `header.php`. Prefixo `bo-` nas peças novas; `.sidebar`, `.topbar`, `.nav-link` mantêm-se.
|
||||||
|
- Cabeçalho fixo `[☰][marca][↗ site público]` (↗ abre `base_url('/')` em nova aba, único sítio), utilizador logo abaixo (avatar com iniciais, nome, crachá de papel, menu Perfil / Terminar sessão). A topbar fica só com o seletor «A editar», o tema e, em telemóvel, o botão que abre a gaveta. Um só manipulador (`data-sidebar-toggle`, em `includes/footer.php`): desktop alterna mini (80px, `localStorage bo-sidebar-mini`), telemóvel abre o off-canvas.
|
||||||
|
- Menu em `includes/header.php` com os auxiliares de `includes/nav_helpers.php`: `bo_nav_link()` (ativo por sufixo do `SCRIPT_NAME`, `aria-current`, title + aria-label), `bo_nav_group_start/end()` (grupo recolhível, `data-short` = 4 letras), `bo_dashboard_path()`. Ordem: Painel (admin: `views/admin/dashboard.php`; staff: `views/staff/dashboard.php`; cliente: `views/user/dashboard.php`), Gestão (subgrupos Site, Horários, Comercial, Legal), Utilizadores, Sistema por último. Cada ligação mantém o seu `is_admin()`/`is_sysadmin()`.
|
||||||
|
- Estado sem flash: script no `<head>` põe `html.bo-mini` e `data-bo-groups`; estado dos grupos em `localStorage bo-nav-groups`; scroll da lista em `sessionStorage bo-nav-scroll`. Grupo novo = chave nova na lista `:is(...)` de grupos fechados em `backoffice.css`.
|
||||||
|
- Mini: categorias em texto (`data-short`), nunca ícones. Ícones: só Bootstrap Icons de contorno.
|
||||||
|
- `includes/navbar.php` (barra antiga) foi apagado; `system_settings.php` já não o inclui.
|
||||||
|
- `views/staff/dashboard.php`: saudação mínima (o staff ainda não alcança nada em `views/admin/`).
|
||||||
|
- Gotcha de dev: a tabela `trial_leads` não existe na BD de dev, por isso `views/admin/trials/index.php` dá 500 (migração por aplicar; anterior a esta mudança).
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
/* Backoffice FitMe 3070: tokens da marca, casca (barra lateral, topbar, gaveta
|
||||||
|
em telemóvel) e acertos dos componentes. Carregado por includes/header.php
|
||||||
|
depois do Bootstrap. O Bootstrap nunca se edita: o que muda faz-se com
|
||||||
|
variáveis e com classes próprias (prefixo bo-). */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bo-accent: #C2D500;
|
||||||
|
--bo-accent-rgb: 194,213,0;
|
||||||
|
--bo-dark: #0a0a0a;
|
||||||
|
--bo-dark-2: #151515;
|
||||||
|
--bo-surface: #eef0f4;
|
||||||
|
--bo-sidebar-w: 264px;
|
||||||
|
--bo-sidebar-w-mini: 80px;
|
||||||
|
--bo-topbar-h: 56px;
|
||||||
|
--bo-ease: .2s ease;
|
||||||
|
/* Texto da barra lateral (escura nos dois temas) */
|
||||||
|
--bo-nav-text: rgba(255,255,255,.62);
|
||||||
|
--bo-nav-text-muted: rgba(255,255,255,.5);
|
||||||
|
--bo-nav-line: rgba(255,255,255,.1);
|
||||||
|
--bo-nav-strong: #fff;
|
||||||
|
}
|
||||||
|
[data-bs-theme="dark"] { --bo-surface: #121416; }
|
||||||
|
|
||||||
|
body { background: var(--bo-surface); font-size: .925rem; }
|
||||||
|
a { color: var(--bs-emphasis-color); }
|
||||||
|
main { margin-left: var(--bo-sidebar-w); padding-top: calc(var(--bo-topbar-h) + 12px); }
|
||||||
|
|
||||||
|
/* ── Barra lateral ───────────────────────────────────────────────────────── */
|
||||||
|
/* Sem overflow aqui: o menu do utilizador sai da barra no modo mini.
|
||||||
|
Quem rola é só .bo-sidebar__nav; cabeçalho e utilizador ficam fixos. */
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0 auto 0 0;
|
||||||
|
z-index: 100;
|
||||||
|
width: var(--bo-sidebar-w);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--bo-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cabeçalho numa linha: [☰] [marca] [↗ site público] */
|
||||||
|
.bo-sidebar__head {
|
||||||
|
flex: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .5rem;
|
||||||
|
height: var(--bo-topbar-h);
|
||||||
|
padding: 0 .75rem;
|
||||||
|
border-bottom: 1px solid var(--bo-nav-line);
|
||||||
|
}
|
||||||
|
.bo-sidebar__toggle,
|
||||||
|
.bo-sidebar__site {
|
||||||
|
flex: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 40px;
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--bo-nav-text);
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background var(--bo-ease), color var(--bo-ease);
|
||||||
|
}
|
||||||
|
.bo-sidebar__site { font-size: 1rem; }
|
||||||
|
.bo-sidebar__toggle:hover,
|
||||||
|
.bo-sidebar__site:hover { background: rgba(255,255,255,.08); color: var(--bo-nav-strong); }
|
||||||
|
.bo-sidebar__brand {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--bo-nav-strong);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
letter-spacing: .5px;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.bo-sidebar__brand i { color: var(--bo-accent); }
|
||||||
|
|
||||||
|
/* Utilizador logo abaixo do cabeçalho */
|
||||||
|
.bo-sidebar__user {
|
||||||
|
flex: none;
|
||||||
|
padding: .6rem .75rem;
|
||||||
|
border-bottom: 1px solid var(--bo-nav-line);
|
||||||
|
}
|
||||||
|
.bo-user-menu {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .6rem;
|
||||||
|
padding: .35rem .45rem;
|
||||||
|
border: 1px solid var(--bo-nav-line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--bo-nav-strong);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.bo-user-menu:hover,
|
||||||
|
.bo-user-menu.show { background: rgba(255,255,255,.08); color: var(--bo-nav-strong); }
|
||||||
|
.bo-user-menu::after { margin-left: auto; flex: none; }
|
||||||
|
.bo-user-menu__avatar {
|
||||||
|
flex: none;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--bo-accent);
|
||||||
|
color: var(--bo-dark);
|
||||||
|
font-size: .8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.bo-user-menu__text { min-width: 0; display: flex; flex-direction: column; align-items: flex-start; line-height: 1.25; }
|
||||||
|
.bo-user-menu__name { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: .85rem; font-weight: 600; }
|
||||||
|
.bo-user-menu__text .badge { font-size: .62rem; font-weight: 600; }
|
||||||
|
.bo-sidebar__user .dropdown-menu { min-width: 14rem; z-index: 1110; }
|
||||||
|
|
||||||
|
/* Lista de navegação: a única parte que rola */
|
||||||
|
.bo-sidebar__nav {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
padding: .5rem 0 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .bo-nav-item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .6rem;
|
||||||
|
min-height: 40px;
|
||||||
|
padding: .5rem 1.25rem;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
color: var(--bo-nav-text);
|
||||||
|
font-size: .875rem;
|
||||||
|
transition: background var(--bo-ease), color var(--bo-ease), border-color var(--bo-ease);
|
||||||
|
}
|
||||||
|
.sidebar .bo-nav-item i { flex: none; width: 1.25rem; text-align: center; font-size: 1.05rem; }
|
||||||
|
.sidebar .bo-nav-item:hover { color: var(--bo-nav-strong); background: rgba(255,255,255,.06); border-left-color: rgba(255,255,255,.2); }
|
||||||
|
.sidebar .bo-nav-item.active { color: var(--bo-nav-strong); background: rgba(var(--bo-accent-rgb),.12); border-left-color: var(--bo-accent); }
|
||||||
|
.sidebar .bo-nav-item.active i { color: var(--bo-accent); }
|
||||||
|
.sidebar .bo-nav-item--primary i { font-size: 1.25rem; }
|
||||||
|
/* Indentação = cadeia de dados: filhos de um grupo, e de um subgrupo. */
|
||||||
|
.sidebar .bo-nav-item--sub { padding-left: 2.3rem; font-size: .84rem; }
|
||||||
|
.sidebar .bo-nav-item--sub2 { padding-left: 3.2rem; font-size: .84rem; }
|
||||||
|
.bo-nav-item__label { min-width: 0; }
|
||||||
|
|
||||||
|
.sidebar .bo-nav-item:focus-visible,
|
||||||
|
.bo-nav-group__label:focus-visible,
|
||||||
|
.bo-sidebar__toggle:focus-visible,
|
||||||
|
.bo-sidebar__site:focus-visible,
|
||||||
|
.bo-user-menu:focus-visible,
|
||||||
|
.bo-sidebar__brand:focus-visible { outline: 2px solid var(--bo-accent); outline-offset: -2px; }
|
||||||
|
|
||||||
|
/* Grupos recolhíveis */
|
||||||
|
.bo-nav-group { margin-top: .25rem; }
|
||||||
|
.bo-nav-group__label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .6rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: .55rem 1.25rem;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--bo-nav-text-muted);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: .68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .14em;
|
||||||
|
text-align: left;
|
||||||
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.bo-nav-group__label:hover { color: var(--bo-nav-strong); background: rgba(255,255,255,.04); }
|
||||||
|
.bo-nav-group__icon { flex: none; width: 1.25rem; text-align: center; font-size: 1.1rem; letter-spacing: 0; }
|
||||||
|
.bo-nav-group__text { flex: 1; min-width: 0; }
|
||||||
|
.bo-nav-group__chevron { font-size: .75rem; letter-spacing: 0; transition: rotate var(--bo-ease); }
|
||||||
|
.bo-nav-group__label[aria-expanded="false"] .bo-nav-group__chevron { rotate: -90deg; }
|
||||||
|
.bo-nav-group--sub > .bo-nav-group__label { padding-left: 1.9rem; }
|
||||||
|
|
||||||
|
/* Corpo recolhível: grelha 1fr -> 0fr anima a altura sem JavaScript a medir. */
|
||||||
|
.bo-nav-group__body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
transition: grid-template-rows var(--bo-ease), visibility var(--bo-ease);
|
||||||
|
}
|
||||||
|
.bo-nav-group__inner {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
/* Fechado = classe (clique no grupo da página ativa) ou chave no atributo do
|
||||||
|
<html> posto pelo script do <head> antes da pintura. O grupo da página ativa
|
||||||
|
só fecha por clique (.is-closed), nunca pelo estado guardado. visibility
|
||||||
|
tira os links fechados da ordem de tabulação. Grupo novo = chave nova aqui. */
|
||||||
|
:is(.bo-nav-group.is-closed,
|
||||||
|
html[data-bo-groups~="gestao"] .bo-nav-group[data-group="gestao"]:not(.has-active),
|
||||||
|
html[data-bo-groups~="site"] .bo-nav-group[data-group="site"]:not(.has-active),
|
||||||
|
html[data-bo-groups~="horarios"] .bo-nav-group[data-group="horarios"]:not(.has-active),
|
||||||
|
html[data-bo-groups~="comercial"] .bo-nav-group[data-group="comercial"]:not(.has-active),
|
||||||
|
html[data-bo-groups~="legal"] .bo-nav-group[data-group="legal"]:not(.has-active),
|
||||||
|
html[data-bo-groups~="utilizadores"] .bo-nav-group[data-group="utilizadores"]:not(.has-active),
|
||||||
|
html[data-bo-groups~="sistema"] .bo-nav-group[data-group="sistema"]:not(.has-active)) > .bo-nav-group__body {
|
||||||
|
grid-template-rows: 0fr;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Véu por trás da gaveta em telemóvel */
|
||||||
|
.sidebar-backdrop { display: none; position: fixed; inset: 0; z-index: 1090; background: rgba(0,0,0,.45); }
|
||||||
|
.sidebar-backdrop.show { display: block; }
|
||||||
|
|
||||||
|
/* ── Topbar ──────────────────────────────────────────────────────────────── */
|
||||||
|
.topbar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: var(--bo-sidebar-w);
|
||||||
|
right: 0;
|
||||||
|
z-index: 99;
|
||||||
|
height: var(--bo-topbar-h);
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .5rem;
|
||||||
|
background: var(--bs-body-bg);
|
||||||
|
border-bottom: 1px solid var(--bs-border-color);
|
||||||
|
}
|
||||||
|
.bo-topbar__tools { display: flex; align-items: center; gap: .5rem; margin-left: auto; }
|
||||||
|
.menu-toggle {
|
||||||
|
display: none;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--bs-border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bs-body-bg);
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
font-size: 1.35rem;
|
||||||
|
line-height: 1;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.theme-toggle { min-width: 44px; min-height: 44px; }
|
||||||
|
|
||||||
|
/* ── Marca nos componentes ───────────────────────────────────────────────── */
|
||||||
|
.badge-role-sysadmin { background: var(--bo-accent); color: var(--bo-dark); }
|
||||||
|
.badge-role-admin { background: var(--bo-accent); color: var(--bo-dark); opacity: .85; }
|
||||||
|
.badge-role-staff { background: #6f42c1; }
|
||||||
|
.badge-role-client { background: #adb5bd; color: #212529; }
|
||||||
|
.btn-outline-primary { color: var(--bo-accent); border-color: var(--bo-accent); }
|
||||||
|
.btn-outline-primary:hover { background: var(--bo-accent); border-color: var(--bo-accent); color: var(--bo-dark); }
|
||||||
|
.btn-primary { background: var(--bo-accent); border-color: var(--bo-accent); color: var(--bo-dark); }
|
||||||
|
.btn-primary:hover { background: #a8ba00; border-color: #a8ba00; color: var(--bo-dark); }
|
||||||
|
.card { border: none; box-shadow: 0 1px 3px rgba(0,0,0,.04); }
|
||||||
|
[data-bs-theme="dark"] .table-light { --bs-table-bg: var(--bs-tertiary-bg); --bs-table-color: var(--bs-body-color); }
|
||||||
|
|
||||||
|
/* Summernote (WYSIWYG): alinhado com o aspeto do backoffice */
|
||||||
|
.note-editor.note-frame { border-color: #ced4da; border-radius: .375rem; }
|
||||||
|
.note-editor .note-editing-area .note-editable { font-size: .95rem; }
|
||||||
|
.note-editor.note-frame.note-focused { border-color: var(--bo-accent); box-shadow: 0 0 0 .2rem rgba(var(--bo-accent-rgb), .25); }
|
||||||
|
.note-toolbar { background: var(--bs-tertiary-bg); }
|
||||||
|
.note-btn.btn-primary { background: var(--bo-accent); border-color: var(--bo-accent); color: var(--bo-dark); }
|
||||||
|
|
||||||
|
/* ── Telemóvel: barra lateral em gaveta ──────────────────────────────────── */
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
.sidebar {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform .25s ease;
|
||||||
|
z-index: 1100;
|
||||||
|
box-shadow: 2px 0 18px rgba(0,0,0,.3);
|
||||||
|
}
|
||||||
|
.sidebar.open { transform: translateX(0); }
|
||||||
|
main { margin-left: 0; }
|
||||||
|
.topbar { left: 0; padding: 0 .75rem; }
|
||||||
|
.menu-toggle { display: inline-flex; }
|
||||||
|
/* Tabelas largas rolam dentro do cartão em vez de alargar a página */
|
||||||
|
main .card > .card-body { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
||||||
|
}
|
||||||
|
@media (max-width: 575.98px) {
|
||||||
|
/* Cabeçalhos de página (título + botões) empilham em vez de transbordar */
|
||||||
|
main .d-flex.justify-content-between.align-items-center { flex-wrap: wrap; gap: .5rem; }
|
||||||
|
/* 16px nos campos: o iOS não faz zoom ao focar */
|
||||||
|
.form-control, .form-select { font-size: 16px; }
|
||||||
|
/* Margens confortáveis em telemóvel */
|
||||||
|
/* !important: .p-3 é uma utilitária do Bootstrap e tem !important de origem */
|
||||||
|
.p-3 { padding: .85rem !important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Modo mini (só ecrã grande; classe posta no <html> pelo script do <head>) ── */
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
html.bo-mini { --bo-sidebar-w: var(--bo-sidebar-w-mini); }
|
||||||
|
html.bo-mini .bo-sidebar__head {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
height: auto;
|
||||||
|
padding: .5rem 0;
|
||||||
|
gap: .25rem;
|
||||||
|
}
|
||||||
|
html.bo-mini .bo-sidebar__brand { display: none; }
|
||||||
|
html.bo-mini .bo-sidebar__user { padding: .5rem; }
|
||||||
|
html.bo-mini .bo-user-menu { justify-content: center; padding: .25rem; }
|
||||||
|
html.bo-mini .bo-user-menu__text,
|
||||||
|
html.bo-mini .bo-user-menu::after { display: none; }
|
||||||
|
html.bo-mini .bo-sidebar__nav { padding: .5rem 0; }
|
||||||
|
html.bo-mini .sidebar .bo-nav-item,
|
||||||
|
html.bo-mini .sidebar .bo-nav-item--sub,
|
||||||
|
html.bo-mini .sidebar .bo-nav-item--sub2 { justify-content: center; padding: .5rem; }
|
||||||
|
html.bo-mini .bo-nav-item__label { display: none; }
|
||||||
|
html.bo-mini .bo-nav-item__badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 8px;
|
||||||
|
margin: 0;
|
||||||
|
padding: .15em .4em;
|
||||||
|
font-size: .6rem;
|
||||||
|
}
|
||||||
|
/* Categoria no mini: sigla de texto (data-short), sem ícone nem chevron. */
|
||||||
|
html.bo-mini .bo-nav-group__label,
|
||||||
|
html.bo-mini .bo-nav-group--sub > .bo-nav-group__label { justify-content: center; padding: .55rem .25rem; font-size: 0; }
|
||||||
|
html.bo-mini .bo-nav-group__label::before { content: attr(data-short); font-size: .66rem; font-weight: 700; letter-spacing: .08em; }
|
||||||
|
html.bo-mini .bo-nav-group__text,
|
||||||
|
html.bo-mini .bo-nav-group__icon,
|
||||||
|
html.bo-mini .bo-nav-group__chevron { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Navegação sem piscar: só o conteúdo faz fade; barra e topbar ficam ── */
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
@view-transition { navigation: auto; }
|
||||||
|
.sidebar { view-transition-name: bo-sidebar; }
|
||||||
|
.topbar { view-transition-name: bo-topbar; }
|
||||||
|
::view-transition-old(root),
|
||||||
|
::view-transition-new(root) { animation-duration: .14s; }
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.sidebar,
|
||||||
|
.sidebar .bo-nav-item,
|
||||||
|
.bo-nav-group__body,
|
||||||
|
.bo-nav-group__chevron { transition: none; }
|
||||||
|
}
|
||||||
+83
-13
@@ -78,23 +78,93 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
mq.addEventListener('change', function () { if (pref === 'auto') apply('auto'); });
|
mq.addEventListener('change', function () { if (pref === 'auto') apply('auto'); });
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Mobile off-canvas sidebar: hamburger toggles .sidebar.open + the backdrop;
|
// Barra lateral. Em ecrã grande o hamburger alterna entre completa e mini (so
|
||||||
// tapping the backdrop, a nav link, Escape, or resizing to desktop closes it.
|
// o texto das categorias, lembrado em localStorage); em telemovel abre/fecha a
|
||||||
|
// gaveta por cima do conteudo. Um so manipulador para os dois botoes. O CSS
|
||||||
|
// decide o que cada estado mostra; o estado inicial ja veio do <head>.
|
||||||
(function () {
|
(function () {
|
||||||
var sb = document.getElementById('sidebar');
|
var sb = document.getElementById('sidebar');
|
||||||
var tg = document.getElementById('menuToggle');
|
|
||||||
var bd = document.getElementById('sidebarBackdrop');
|
var bd = document.getElementById('sidebarBackdrop');
|
||||||
if (!sb || !tg) return;
|
var toggles = document.querySelectorAll('[data-sidebar-toggle]');
|
||||||
var mq = window.matchMedia('(max-width: 991.98px)');
|
var root = document.documentElement;
|
||||||
function open() { sb.classList.add('open'); if (bd) bd.classList.add('show'); document.body.style.overflow = 'hidden'; tg.setAttribute('aria-expanded', 'true'); }
|
var nav = sb && sb.querySelector('.bo-sidebar__nav');
|
||||||
function close() { sb.classList.remove('open'); if (bd) bd.classList.remove('show'); document.body.style.overflow = ''; tg.setAttribute('aria-expanded', 'false'); }
|
if (!sb || !toggles.length) return;
|
||||||
tg.addEventListener('click', function () { sb.classList.contains('open') ? close() : open(); });
|
var mq = window.matchMedia('(min-width: 992px)');
|
||||||
if (bd) bd.addEventListener('click', close);
|
function store(k, v) { try { localStorage.setItem(k, v); } catch (e) {} }
|
||||||
sb.querySelectorAll('.nav-link').forEach(function (a) {
|
|
||||||
a.addEventListener('click', function () { if (mq.matches) close(); });
|
function sync() {
|
||||||
|
var desktop = mq.matches;
|
||||||
|
var open = desktop ? !root.classList.contains('bo-mini') : sb.classList.contains('open');
|
||||||
|
toggles.forEach(function (t) {
|
||||||
|
t.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||||
|
t.setAttribute('aria-label', open ? (desktop ? 'Reduzir menu' : 'Fechar menu') : (desktop ? 'Expandir menu' : 'Abrir menu'));
|
||||||
});
|
});
|
||||||
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
|
if (bd) bd.classList.toggle('show', !desktop && open);
|
||||||
window.addEventListener('resize', function () { if (!mq.matches) close(); });
|
document.body.style.overflow = (!desktop && open) ? 'hidden' : '';
|
||||||
|
}
|
||||||
|
function setDrawer(open) { sb.classList.toggle('open', open); sync(); }
|
||||||
|
|
||||||
|
toggles.forEach(function (t) {
|
||||||
|
t.addEventListener('click', function () {
|
||||||
|
if (mq.matches) {
|
||||||
|
store('bo-sidebar-mini', root.classList.toggle('bo-mini') ? '1' : '0');
|
||||||
|
sync();
|
||||||
|
} else {
|
||||||
|
setDrawer(!sb.classList.contains('open'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (bd) bd.addEventListener('click', function () { setDrawer(false); });
|
||||||
|
sb.querySelectorAll('.bo-nav-item').forEach(function (a) {
|
||||||
|
a.addEventListener('click', function () { if (!mq.matches) setDrawer(false); });
|
||||||
|
});
|
||||||
|
document.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key === 'Escape' && sb.classList.contains('open')) {
|
||||||
|
setDrawer(false);
|
||||||
|
var o = document.querySelector('.menu-toggle');
|
||||||
|
if (o && o.offsetParent) o.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (mq.addEventListener) mq.addEventListener('change', function () { setDrawer(false); });
|
||||||
|
sync();
|
||||||
|
|
||||||
|
// Grupos recolhiveis. O estado inicial veio do <head> (atributo no <html>);
|
||||||
|
// aqui so se liga o clique e se mantem atributo, armazenamento e aria alinhados.
|
||||||
|
var saved = {};
|
||||||
|
try { saved = JSON.parse(localStorage.getItem('bo-nav-groups') || '{}'); } catch (e) {}
|
||||||
|
function groupOpen(g) {
|
||||||
|
if (g.classList.contains('has-active')) return !g.classList.contains('is-closed');
|
||||||
|
return !saved[g.getAttribute('data-group')];
|
||||||
|
}
|
||||||
|
function paint(g) {
|
||||||
|
g.querySelector('[data-group-toggle]').setAttribute('aria-expanded', groupOpen(g) ? 'true' : 'false');
|
||||||
|
}
|
||||||
|
var groups = sb.querySelectorAll('.bo-nav-group[data-group]');
|
||||||
|
groups.forEach(function (g) {
|
||||||
|
g.querySelector('[data-group-toggle]').addEventListener('click', function () {
|
||||||
|
var key = g.getAttribute('data-group');
|
||||||
|
var willClose = groupOpen(g);
|
||||||
|
if (g.classList.contains('has-active')) g.classList.toggle('is-closed', willClose);
|
||||||
|
if (willClose) saved[key] = true; else delete saved[key];
|
||||||
|
store('bo-nav-groups', JSON.stringify(saved));
|
||||||
|
root.setAttribute('data-bo-groups', Object.keys(saved).join(' '));
|
||||||
|
groups.forEach(paint);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
groups.forEach(paint);
|
||||||
|
|
||||||
|
// A lista rola sozinha: lembrar a posicao entre paginas e mostrar o item ativo.
|
||||||
|
if (nav) {
|
||||||
|
try { nav.scrollTop = +sessionStorage.getItem('bo-nav-scroll') || 0; } catch (e) {}
|
||||||
|
var cur = nav.querySelector('[aria-current="page"]');
|
||||||
|
if (cur) {
|
||||||
|
var r = cur.getBoundingClientRect(), n = nav.getBoundingClientRect();
|
||||||
|
if (r.top < n.top || r.bottom > n.bottom) cur.scrollIntoView({ block: 'center' });
|
||||||
|
}
|
||||||
|
addEventListener('pagehide', function () {
|
||||||
|
try { sessionStorage.setItem('bo-nav-scroll', nav.scrollTop); } catch (e) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
+79
-183
@@ -25,203 +25,105 @@ $app_name = defined('APP_NAME') ? APP_NAME : 'FitMe 3070';
|
|||||||
document.documentElement.setAttribute('data-theme-pref', pref);
|
document.documentElement.setAttribute('data-theme-pref', pref);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
<script>
|
||||||
|
// Estado da barra lateral antes da primeira pintura (senão cada página salta
|
||||||
|
// de 264 para 80px): modo mini no <html> e grupos fechados em data-bo-groups.
|
||||||
|
try{var d=document.documentElement;if(localStorage.getItem('bo-sidebar-mini')==='1')d.classList.add('bo-mini');
|
||||||
|
var g=JSON.parse(localStorage.getItem('bo-nav-groups')||'{}');d.setAttribute('data-bo-groups',Object.keys(g).filter(function(k){return g[k]}).join(' '));}catch(e){}
|
||||||
|
</script>
|
||||||
<link href="<?= base_url('assets/vendor/bootstrap5/css/bootstrap.min.css') ?>" rel="stylesheet">
|
<link href="<?= base_url('assets/vendor/bootstrap5/css/bootstrap.min.css') ?>" rel="stylesheet">
|
||||||
<link href="<?= base_url('assets/vendor/bootstrap-icons/bootstrap-icons.css') ?>" rel="stylesheet">
|
<link href="<?= base_url('assets/vendor/bootstrap-icons/bootstrap-icons.css') ?>" rel="stylesheet">
|
||||||
<link href="<?= base_url('assets/vendor/summernote/summernote-bs5.min.css') ?>" rel="stylesheet">
|
<link href="<?= base_url('assets/vendor/summernote/summernote-bs5.min.css') ?>" rel="stylesheet">
|
||||||
<style>
|
<link href="<?= htmlspecialchars(_asset_versioned('assets/css/backoffice.css'), ENT_QUOTES) ?>" rel="stylesheet">
|
||||||
:root {
|
<?php include __DIR__ . '/env_ribbon.php'; // moldura de aviso fora de produção ?>
|
||||||
--bo-accent: #C2D500;
|
|
||||||
--bo-accent-rgb: 194,213,0;
|
|
||||||
--bo-dark: #0a0a0a;
|
|
||||||
--bo-dark-2: #151515;
|
|
||||||
--bo-surface: #eef0f4;
|
|
||||||
}
|
|
||||||
[data-bs-theme="dark"] { --bo-surface: #121416; }
|
|
||||||
body { background: var(--bo-surface); font-size: .925rem; }
|
|
||||||
.sidebar { position: fixed; top: 0; bottom: 0; left: 0; z-index: 100;
|
|
||||||
padding: 68px 0 0; width: 240px; background: var(--bo-dark); overflow-y: auto; }
|
|
||||||
.sidebar .nav-link { color: rgba(255,255,255,.55); padding: .55rem 1.25rem; font-size: .875rem;
|
|
||||||
border-left: 3px solid transparent; transition: all .2s; }
|
|
||||||
.sidebar .nav-link:hover { color: #fff; background: rgba(255,255,255,.06); border-left-color: rgba(255,255,255,.2); }
|
|
||||||
.sidebar .nav-link.active { color: #fff; background: rgba(var(--bo-accent-rgb),.12); border-left-color: var(--bo-accent); }
|
|
||||||
.sidebar .nav-link i { width: 20px; margin-right: .5rem; text-align: center; }
|
|
||||||
.sidebar .sidebar-brand { position: fixed; top: 0; left: 0; width: 240px; padding: 1rem 1.25rem;
|
|
||||||
color: #fff; font-weight: 700; font-size: 1.1rem; background: var(--bo-dark);
|
|
||||||
z-index: 101; letter-spacing: .5px; border-bottom: 1px solid rgba(255,255,255,.06); }
|
|
||||||
.sidebar .sidebar-brand i { color: var(--bo-accent); }
|
|
||||||
.sidebar .sidebar-section { color: rgba(255,255,255,.3); font-size: .65rem; text-transform: uppercase;
|
|
||||||
letter-spacing: 1.5px; padding: 1.25rem 1.25rem .35rem; }
|
|
||||||
main { margin-left: 240px; padding-top: 68px; }
|
|
||||||
.topbar { position: fixed; top: 0; left: 240px; right: 0; z-index: 99; height: 56px;
|
|
||||||
background: var(--bs-body-bg); border-bottom: 1px solid var(--bs-border-color); padding: 0 1.5rem;
|
|
||||||
display: flex; align-items: center; justify-content: space-between; }
|
|
||||||
.topbar .user-dropdown { display: flex; align-items: center; gap: .5rem; margin-left: auto; }
|
|
||||||
.badge-role-sysadmin { background: var(--bo-accent); color: var(--bo-dark); }
|
|
||||||
.badge-role-admin { background: var(--bo-accent); color: var(--bo-dark); opacity: .85; }
|
|
||||||
.badge-role-staff { background: #6f42c1; }
|
|
||||||
.badge-role-client { background: #adb5bd; color: #212529; }
|
|
||||||
.btn-outline-primary { color: var(--bo-accent); border-color: var(--bo-accent); }
|
|
||||||
.btn-outline-primary:hover { background: var(--bo-accent); border-color: var(--bo-accent); color: var(--bo-dark); }
|
|
||||||
.btn-primary { background: var(--bo-accent); border-color: var(--bo-accent); color: var(--bo-dark); }
|
|
||||||
.btn-primary:hover { background: #a8ba00; border-color: #a8ba00; color: var(--bo-dark); }
|
|
||||||
.card { border: none; box-shadow: 0 1px 3px rgba(0,0,0,.04); }
|
|
||||||
[data-bs-theme="dark"] .table-light { --bs-table-bg: var(--bs-tertiary-bg); --bs-table-color: var(--bs-body-color); }
|
|
||||||
.theme-toggle { min-width: 44px; min-height: 44px; }
|
|
||||||
a { color: var(--bs-emphasis-color); }
|
|
||||||
/* Summernote (WYSIWYG) — align with the backoffice look */
|
|
||||||
.note-editor.note-frame { border-color: #ced4da; border-radius: .375rem; }
|
|
||||||
.note-editor .note-editing-area .note-editable { font-size: .95rem; }
|
|
||||||
.note-editor.note-frame.note-focused { border-color: var(--bo-accent); box-shadow: 0 0 0 .2rem rgba(var(--bo-accent-rgb), .25); }
|
|
||||||
.note-toolbar { background: var(--bs-tertiary-bg); }
|
|
||||||
.note-btn.btn-primary { background: var(--bo-accent); border-color: var(--bo-accent); color: var(--bo-dark); }
|
|
||||||
/* ── Mobile: off-canvas sidebar + hamburger ──────────────────────────── */
|
|
||||||
.sidebar { transition: transform .25s ease; }
|
|
||||||
.menu-toggle { display: none; width: 44px; height: 44px; padding: 0; border: 1px solid var(--bs-border-color);
|
|
||||||
background: var(--bs-body-bg); color: var(--bs-body-color); border-radius: 8px; font-size: 1.35rem;
|
|
||||||
line-height: 1; align-items: center; justify-content: center; }
|
|
||||||
.sidebar-backdrop { display: none; position: fixed; inset: 0; z-index: 1090; background: rgba(0,0,0,.45); }
|
|
||||||
.sidebar-backdrop.show { display: block; }
|
|
||||||
@media (max-width: 991.98px) {
|
|
||||||
.sidebar { transform: translateX(-100%); width: 264px; z-index: 1100;
|
|
||||||
box-shadow: 2px 0 18px rgba(0,0,0,.3); }
|
|
||||||
.sidebar .sidebar-brand { width: 264px; z-index: 1101; }
|
|
||||||
.sidebar.open { transform: translateX(0); }
|
|
||||||
main { margin-left: 0; }
|
|
||||||
.topbar { left: 0; padding: 0 .75rem; }
|
|
||||||
.menu-toggle { display: inline-flex; }
|
|
||||||
/* Wide tables scroll inside their card instead of overflowing the page */
|
|
||||||
main .card > .card-body { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
|
||||||
}
|
|
||||||
@media (max-width: 575.98px) {
|
|
||||||
.topbar .user-dropdown .badge,
|
|
||||||
.topbar .user-dropdown .u-name { display: none; }
|
|
||||||
/* Page-header rows (title + action buttons) stack instead of overflowing */
|
|
||||||
main .d-flex.justify-content-between.align-items-center { flex-wrap: wrap; gap: .5rem; }
|
|
||||||
/* 16px inputs stop iOS from auto-zooming on focus */
|
|
||||||
.form-control, .form-select { font-size: 16px; }
|
|
||||||
/* Comfortable content gutters on phones */
|
|
||||||
.p-3 { padding: .85rem !important; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav class="sidebar" id="sidebar">
|
<?php require_once __DIR__ . '/nav_helpers.php'; $dash = bo_dashboard_path(); ?>
|
||||||
<div class="sidebar-brand"><i class="bi bi-lightning-charge-fill me-2"></i><?= htmlspecialchars($app_name) ?></div>
|
<aside class="sidebar" id="sidebar" aria-label="Menu principal">
|
||||||
<div class="sidebar-section">Principal</div>
|
<div class="bo-sidebar__head">
|
||||||
<ul class="nav flex-column">
|
<button type="button" class="bo-sidebar__toggle" data-sidebar-toggle
|
||||||
<li class="nav-item">
|
aria-label="Reduzir menu" aria-expanded="true" aria-controls="sidebar">
|
||||||
<a class="nav-link<?= ($_SERVER['SCRIPT_NAME'] ?? '') === '/views/admin/dashboard.php' ? ' active' : '' ?>"
|
<i class="bi bi-list" aria-hidden="true"></i>
|
||||||
href="<?= base_url('views/admin/dashboard.php') ?>"><i class="bi bi-speedometer2"></i>Painel</a>
|
</button>
|
||||||
</li>
|
<a class="bo-sidebar__brand" href="<?= base_url($dash) ?>"><i class="bi bi-lightning-charge me-2" aria-hidden="true"></i><?= htmlspecialchars($app_name) ?></a>
|
||||||
<?php if (is_staff()): ?>
|
<?php // Único sítio que abre o site público (nova aba). ?>
|
||||||
<li class="nav-item">
|
<a class="bo-sidebar__site" href="<?= base_url('/') ?>" target="_blank" rel="noopener"
|
||||||
<a class="nav-link" href="<?= base_url('/') ?>" target="_blank" rel="noopener"><i class="bi bi-box-arrow-up-right"></i>Site público</a>
|
title="Ver site público" aria-label="Ver site público"><i class="bi bi-box-arrow-up-right" aria-hidden="true"></i></a>
|
||||||
</li>
|
</div>
|
||||||
<?php endif; ?>
|
|
||||||
|
<div class="dropdown bo-sidebar__user">
|
||||||
|
<button class="bo-user-menu dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"
|
||||||
|
title="<?= htmlspecialchars($user['name']) ?>">
|
||||||
|
<span class="bo-user-menu__avatar" aria-hidden="true"><?= htmlspecialchars(bo_initials((string) $user['name'])) ?></span>
|
||||||
|
<span class="bo-user-menu__text">
|
||||||
|
<span class="bo-user-menu__name"><?= htmlspecialchars($user['name']) ?></span>
|
||||||
|
<span class="badge rounded-pill badge-role-<?= htmlspecialchars($user['role']) ?>"><?= htmlspecialchars(role_label($user['role'])) ?></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li class="px-3 pt-2 pb-1"><div class="fw-semibold small lh-sm"><?= htmlspecialchars($user['name']) ?></div></li>
|
||||||
|
<li><hr class="dropdown-divider my-1"></li>
|
||||||
|
<li><a class="dropdown-item" href="<?= base_url('views/user/profile.php') ?>"><i class="bi bi-person me-2" aria-hidden="true"></i>Perfil</a></li>
|
||||||
|
<li><a class="dropdown-item text-danger" href="<?= base_url('views/auth/logout.php') ?>"><i class="bi bi-box-arrow-right me-2" aria-hidden="true"></i>Terminar sessão</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="bo-sidebar__nav" aria-label="Navegação">
|
||||||
|
<?= bo_nav_link($dash, 'bi-speedometer2', 'Painel', ['mod' => 'primary']) ?>
|
||||||
|
|
||||||
<?php if (is_admin()): ?>
|
<?php if (is_admin()): ?>
|
||||||
<div class="sidebar-section">Gestão</div>
|
<?php bo_nav_group_start('gestao', 'Gestão', 'bi-briefcase'); ?>
|
||||||
<ul class="nav flex-column">
|
<?php bo_nav_group_start('site', 'Site', 'bi-window', true); ?>
|
||||||
<li class="nav-item">
|
<?= bo_nav_link('views/admin/gyms/index.php', 'bi-buildings', 'Ginásios', ['mod' => 'sub2', 'match' => 'views/admin/gyms/']) ?>
|
||||||
<a class="nav-link<?= (strpos($_SERVER['SCRIPT_NAME'] ?? '', '/views/admin/gyms/') !== false) ? ' active' : '' ?>"
|
<?= bo_nav_link('views/admin/modalidades/index.php', 'bi-grid-3x3-gap', 'Modalidades', ['mod' => 'sub2', 'match' => 'views/admin/modalidades/']) ?>
|
||||||
href="<?= base_url('views/admin/gyms/index.php') ?>"><i class="bi bi-buildings-fill"></i>Ginásios</a>
|
<?= bo_nav_link('views/admin/galerias/index.php', 'bi-images', 'Galerias', ['mod' => 'sub2', 'match' => 'views/admin/galerias/']) ?>
|
||||||
</li>
|
<?= bo_nav_link('views/admin/events/index.php', 'bi-megaphone', 'Eventos', ['mod' => 'sub2', 'match' => 'views/admin/events/']) ?>
|
||||||
<li class="nav-item">
|
<?= bo_nav_link('views/admin/info/edit.php', 'bi-telephone', 'Contactos', ['mod' => 'sub2', 'match' => 'views/admin/info/']) ?>
|
||||||
<a class="nav-link<?= (strpos($_SERVER['SCRIPT_NAME'] ?? '', '/views/admin/team/') !== false) ? ' active' : '' ?>"
|
<?= bo_nav_link('views/admin/reviews/index.php', 'bi-star', 'Reviews Google', ['mod' => 'sub2', 'match' => 'views/admin/reviews/']) ?>
|
||||||
href="<?= base_url('views/admin/team/index.php') ?>"><i class="bi bi-people-fill"></i>Equipa</a>
|
<?php bo_nav_group_end(); ?>
|
||||||
</li>
|
<?php bo_nav_group_start('horarios', 'Horários', 'bi-clock-history', true); ?>
|
||||||
<li class="nav-item">
|
<?= bo_nav_link('views/admin/work_hours/index.php', 'bi-clock', 'Horário de Funcionamento', ['mod' => 'sub2', 'match' => 'views/admin/work_hours/']) ?>
|
||||||
<a class="nav-link<?= (strpos($_SERVER['SCRIPT_NAME'] ?? '', '/views/admin/modalidades/') !== false) ? ' active' : '' ?>"
|
<?= bo_nav_link('views/admin/horario/index.php', 'bi-calendar-week', 'Horário de Aulas', ['mod' => 'sub2', 'match' => 'views/admin/horario/']) ?>
|
||||||
href="<?= base_url('views/admin/modalidades/index.php') ?>"><i class="bi bi-grid-3x3-gap-fill"></i>Modalidades</a>
|
<?php bo_nav_group_end(); ?>
|
||||||
</li>
|
<?php bo_nav_group_start('comercial', 'Comercial', 'bi-cash-coin', true); ?>
|
||||||
<li class="nav-item">
|
<?= bo_nav_link('views/admin/memberships/index.php', 'bi-credit-card-2-front', 'Mensalidades', ['mod' => 'sub2', 'match' => 'views/admin/memberships/']) ?>
|
||||||
<a class="nav-link<?= (strpos($_SERVER['SCRIPT_NAME'] ?? '', '/views/admin/galerias/') !== false) ? ' active' : '' ?>"
|
<?php $__tn = function_exists('db_trial_leads_count_new') ? db_trial_leads_count_new($pdo, admin_gym_id()) : 0; ?>
|
||||||
href="<?= base_url('views/admin/galerias/index.php') ?>"><i class="bi bi-images"></i>Galerias</a>
|
<?= bo_nav_link('views/admin/trials/index.php', 'bi-person-check', 'Aulas experimentais', ['mod' => 'sub2', 'match' => 'views/admin/trials/', 'badge' => $__tn]) ?>
|
||||||
</li>
|
<?= bo_nav_link('views/admin/team/index.php', 'bi-person-workspace', 'Equipa', ['mod' => 'sub2', 'match' => 'views/admin/team/']) ?>
|
||||||
<li class="nav-item">
|
<?php bo_nav_group_end(); ?>
|
||||||
<a class="nav-link<?= (strpos($_SERVER['SCRIPT_NAME'] ?? '', '/views/admin/events/') !== false) ? ' active' : '' ?>"
|
<?php bo_nav_group_start('legal', 'Legal', 'bi-shield-check', true); ?>
|
||||||
href="<?= base_url('views/admin/events/index.php') ?>"><i class="bi bi-megaphone-fill"></i>Eventos</a>
|
<?= bo_nav_link('views/admin/rgpd/index.php', 'bi-file-earmark-lock', 'Pedidos RGPD', ['mod' => 'sub2', 'match' => 'views/admin/rgpd/']) ?>
|
||||||
</li>
|
<?= bo_nav_link('views/admin/trash/index.php', 'bi-trash3', 'Lixeira', ['mod' => 'sub2', 'match' => 'views/admin/trash/']) ?>
|
||||||
<li class="nav-item">
|
<?php bo_nav_group_end(); ?>
|
||||||
<a class="nav-link<?= (strpos($_SERVER['SCRIPT_NAME'] ?? '', '/views/admin/work_hours/') !== false) ? ' active' : '' ?>"
|
<?php bo_nav_group_end(); ?>
|
||||||
href="<?= base_url('views/admin/work_hours/index.php') ?>"><i class="bi bi-clock-fill"></i>Horário de Funcionamento</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link<?= (strpos($_SERVER['SCRIPT_NAME'] ?? '', '/views/admin/horario/') !== false) ? ' active' : '' ?>"
|
|
||||||
href="<?= base_url('views/admin/horario/index.php') ?>"><i class="bi bi-calendar-week-fill"></i>Horário de Aulas</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link<?= (strpos($_SERVER['SCRIPT_NAME'] ?? '', '/views/admin/memberships/') !== false) ? ' active' : '' ?>"
|
|
||||||
href="<?= base_url('views/admin/memberships/index.php') ?>"><i class="bi bi-credit-card-2-front-fill"></i>Mensalidades</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link<?= (strpos($_SERVER['SCRIPT_NAME'] ?? '', '/views/admin/trials/') !== false) ? ' active' : '' ?>"
|
|
||||||
href="<?= base_url('views/admin/trials/index.php') ?>"><i class="bi bi-person-check-fill"></i>Aulas experimentais<?php
|
|
||||||
$__tn = function_exists('db_trial_leads_count_new') ? db_trial_leads_count_new($pdo, admin_gym_id()) : 0;
|
|
||||||
if ($__tn): ?> <span class="badge bg-danger ms-1"><?= (int) $__tn ?></span><?php endif; ?></a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link<?= (strpos($_SERVER['SCRIPT_NAME'] ?? '', '/views/admin/reviews/') !== false) ? ' active' : '' ?>"
|
|
||||||
href="<?= base_url('views/admin/reviews/index.php') ?>"><i class="bi bi-star-fill"></i>Reviews Google</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link<?= (strpos($_SERVER['SCRIPT_NAME'] ?? '', '/views/admin/info/') !== false) ? ' active' : '' ?>"
|
|
||||||
href="<?= base_url('views/admin/info/edit.php') ?>"><i class="bi bi-telephone-fill"></i>Contactos</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link<?= (strpos($_SERVER['SCRIPT_NAME'] ?? '', '/views/admin/trash/') !== false) ? ' active' : '' ?>"
|
|
||||||
href="<?= base_url('views/admin/trash/index.php') ?>"><i class="bi bi-trash3-fill"></i>Lixeira</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link<?= (strpos($_SERVER['SCRIPT_NAME'] ?? '', '/views/admin/rgpd/') !== false) ? ' active' : '' ?>"
|
|
||||||
href="<?= base_url('views/admin/rgpd/index.php') ?>"><i class="bi bi-shield-lock-fill"></i>Pedidos RGPD</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if (is_admin()): ?>
|
<?php bo_nav_group_start('utilizadores', 'Utilizadores', 'bi-people'); ?>
|
||||||
<div class="sidebar-section">Utilizadores</div>
|
<?= bo_nav_link('views/user/index.php', 'bi-person-vcard', 'Utilizadores', ['mod' => 'sub', 'match' => ['views/user/index.php', 'views/user/edit.php']]) ?>
|
||||||
<ul class="nav flex-column">
|
<?php bo_nav_group_end(); ?>
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link<?= ($_SERVER['SCRIPT_NAME'] ?? '') === '/views/user/index.php' ? ' active' : '' ?>"
|
|
||||||
href="<?= base_url('views/user/index.php') ?>"><i class="bi bi-person-badge"></i>Utilizadores</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (is_sysadmin()): ?>
|
<?php if (is_sysadmin()): ?>
|
||||||
<div class="sidebar-section">Sistema</div>
|
<?php bo_nav_group_start('sistema', 'Sistema', 'bi-gear'); ?>
|
||||||
<ul class="nav flex-column">
|
<?= bo_nav_link('views/sysadmin/system_settings.php', 'bi-sliders', 'Definições', ['mod' => 'sub']) ?>
|
||||||
<li class="nav-item">
|
<?= bo_nav_link('views/sysadmin/messages.php', 'bi-envelope-paper', 'Mensagens', ['mod' => 'sub']) ?>
|
||||||
<a class="nav-link<?= ($_SERVER['SCRIPT_NAME'] ?? '') === '/views/sysadmin/system_settings.php' ? ' active' : '' ?>"
|
<?= bo_nav_link('views/sysadmin/comms.php', 'bi-send', 'Comunicações', ['mod' => 'sub']) ?>
|
||||||
href="<?= base_url('views/sysadmin/system_settings.php') ?>"><i class="bi bi-sliders"></i>Definições</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link<?= ($_SERVER['SCRIPT_NAME'] ?? '') === '/views/sysadmin/messages.php' ? ' active' : '' ?>"
|
|
||||||
href="<?= base_url('views/sysadmin/messages.php') ?>"><i class="bi bi-envelope-paper"></i>Mensagens</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link<?= ($_SERVER['SCRIPT_NAME'] ?? '') === '/views/sysadmin/comms.php' ? ' active' : '' ?>"
|
|
||||||
href="<?= base_url('views/sysadmin/comms.php') ?>"><i class="bi bi-send"></i>Comunicações</a>
|
|
||||||
</li>
|
|
||||||
<?php // Backups/Auditoria/IP bans/Geo/Origem das visitas/Atualizações/Hub: um só
|
<?php // Backups/Auditoria/IP bans/Geo/Origem das visitas/Atualizações/Hub: um só
|
||||||
// link para o índice partilhado do platform-tools (como a fotografia/design/
|
// link para o índice partilhado do platform-tools (como a fotografia/design/
|
||||||
// dance fazem), em vez de um por ferramenta — uma ferramenta nova no
|
// dance fazem), em vez de um por ferramenta — uma ferramenta nova no
|
||||||
// platform-tools passa a aparecer sozinha, sem precisar de link novo aqui. ?>
|
// platform-tools passa a aparecer sozinha, sem precisar de link novo aqui. ?>
|
||||||
<li class="nav-item">
|
<?= bo_nav_link('platform.php?tool=index', 'bi-cpu', 'Sistema', ['mod' => 'sub',
|
||||||
<a class="nav-link<?= (($_GET['tool'] ?? 'index') === 'index' && str_contains($_SERVER['REQUEST_URI'] ?? '', 'platform.php')) ? ' active' : '' ?>"
|
'active' => (($_GET['tool'] ?? 'index') === 'index' && str_contains($_SERVER['REQUEST_URI'] ?? '', 'platform.php'))]) ?>
|
||||||
href="<?= base_url('platform.php?tool=index') ?>"><i class="bi bi-shield-lock"></i>Sistema</a>
|
<?php bo_nav_group_end(); ?>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</nav>
|
</nav>
|
||||||
|
</aside>
|
||||||
<div class="sidebar-backdrop" id="sidebarBackdrop" aria-hidden="true"></div>
|
<div class="sidebar-backdrop" id="sidebarBackdrop" aria-hidden="true"></div>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<div class="topbar">
|
<div class="topbar">
|
||||||
<button type="button" class="menu-toggle" id="menuToggle" aria-label="Abrir menu" aria-controls="sidebar">
|
<?php // Só em telemóvel: abre a gaveta (a barra está fora do ecrã). ?>
|
||||||
<i class="bi bi-list"></i>
|
<button type="button" class="menu-toggle" data-sidebar-toggle aria-label="Abrir menu" aria-expanded="false" aria-controls="sidebar">
|
||||||
|
<i class="bi bi-list" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<?php
|
<?php
|
||||||
// Which gym the content pages edit (functions_gyms.php). Switching goes back
|
// Which gym the content pages edit (functions_gyms.php). Switching goes back
|
||||||
@@ -244,7 +146,7 @@ $app_name = defined('APP_NAME') ? APP_NAME : 'FitMe 3070';
|
|||||||
<noscript><button class="btn btn-sm btn-outline-secondary">Mudar</button></noscript>
|
<noscript><button class="btn btn-sm btn-outline-secondary">Mudar</button></noscript>
|
||||||
</form>
|
</form>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<div class="user-dropdown">
|
<div class="bo-topbar__tools">
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button class="btn btn-sm btn-outline-secondary theme-toggle" type="button" data-bs-toggle="dropdown"
|
<button class="btn btn-sm btn-outline-secondary theme-toggle" type="button" data-bs-toggle="dropdown"
|
||||||
aria-expanded="false" aria-label="Tema: automático" id="themeToggle">
|
aria-expanded="false" aria-label="Tema: automático" id="themeToggle">
|
||||||
@@ -256,12 +158,6 @@ $app_name = defined('APP_NAME') ? APP_NAME : 'FitMe 3070';
|
|||||||
<li><button type="button" class="dropdown-item" data-theme-set="auto"><i class="bi bi-circle-half me-2" aria-hidden="true"></i>Automático</button></li>
|
<li><button type="button" class="dropdown-item" data-theme-set="auto"><i class="bi bi-circle-half me-2" aria-hidden="true"></i>Automático</button></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<span class="badge rounded-pill badge-role-<?= htmlspecialchars($user['role']) ?>">
|
|
||||||
<?= htmlspecialchars(role_label($user['role'])) ?>
|
|
||||||
</span>
|
|
||||||
<span class="u-name"><?= htmlspecialchars($user['name']) ?></span>
|
|
||||||
<a href="<?= base_url('views/user/profile.php') ?>" class="btn btn-sm btn-outline-secondary"><i class="bi bi-person"></i></a>
|
|
||||||
<a href="<?= base_url('views/auth/logout.php') ?>" class="btn btn-sm btn-outline-danger"><i class="bi bi-box-arrow-right"></i></a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
// Auxiliares da barra lateral do backoffice (incluído por includes/header.php).
|
||||||
|
// Ficam aqui, e não no HTML, para o menu ser só uma lista de itens: o ativo,
|
||||||
|
// o aria-current, o title/aria-label (o modo mini só mostra o ícone) e a
|
||||||
|
// abertura dos grupos decidem-se num sítio.
|
||||||
|
|
||||||
|
/** Painel de cada papel: o admin tem o seu, o staff o dele, o cliente a área de sócio. */
|
||||||
|
function bo_dashboard_path(): string
|
||||||
|
{
|
||||||
|
if (is_admin()) return 'views/admin/dashboard.php';
|
||||||
|
if (is_staff()) return 'views/staff/dashboard.php';
|
||||||
|
return 'views/user/dashboard.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* O pedido atual está nesta página? Compara pelo FIM do caminho (e não com ===):
|
||||||
|
* em dev o site vive num subcaminho do host (/GitTea/repos/fitme3070.pt/...).
|
||||||
|
* Um caminho terminado em «/» é uma pasta inteira (inclui edit.php, create.php...).
|
||||||
|
* Aceita uma lista: basta uma coincidir.
|
||||||
|
*
|
||||||
|
* @param string|string[] $paths
|
||||||
|
*/
|
||||||
|
function bo_nav_active($paths): bool
|
||||||
|
{
|
||||||
|
$script = str_replace('\\', '/', (string) ($_SERVER['SCRIPT_NAME'] ?? ''));
|
||||||
|
foreach ((array) $paths as $p) {
|
||||||
|
$p = '/' . ltrim($p, '/');
|
||||||
|
if (str_ends_with($p, '/') ? str_contains($script, $p) : str_ends_with($script, $p)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uma ligação da barra lateral.
|
||||||
|
* Opções: match (caminho/s para o «ativo», por omissão o próprio href), mod
|
||||||
|
* ('sub' | 'sub2' | 'primary'), badge (inteiro; só aparece se > 0).
|
||||||
|
*/
|
||||||
|
function bo_nav_link(string $href, string $icon, string $label, array $o = []): string
|
||||||
|
{
|
||||||
|
$active = array_key_exists('active', $o) ? (bool) $o['active'] : bo_nav_active($o['match'] ?? strtok($href, '?'));
|
||||||
|
$mod = isset($o['mod']) ? ' bo-nav-item--' . $o['mod'] : '';
|
||||||
|
$l = htmlspecialchars($label, ENT_QUOTES, 'UTF-8');
|
||||||
|
$badge = (int) ($o['badge'] ?? 0);
|
||||||
|
return '<a class="nav-link bo-nav-item' . $mod . ($active ? ' active' : '') . '" href="' . htmlspecialchars(base_url($href), ENT_QUOTES, 'UTF-8') . '"'
|
||||||
|
. ' title="' . $l . '" aria-label="' . $l . '"' . ($active ? ' aria-current="page"' : '') . '>'
|
||||||
|
. '<i class="bi ' . htmlspecialchars($icon, ENT_QUOTES, 'UTF-8') . '" aria-hidden="true"></i>'
|
||||||
|
. '<span class="bo-nav-item__label">' . $l . '</span>'
|
||||||
|
. ($badge > 0 ? '<span class="badge bg-danger ms-auto bo-nav-item__badge">' . $badge . '</span>' : '')
|
||||||
|
. '</a>' . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abre um grupo recolhível. O conteúdo fica em buffer até bo_nav_group_end(),
|
||||||
|
* que precisa de saber se algum item é o ativo (o grupo da página atual abre
|
||||||
|
* sempre). $sub = subcategoria dentro de outro grupo (recuada).
|
||||||
|
*/
|
||||||
|
function bo_nav_group_start(string $key, string $label, string $icon, bool $sub = false): void
|
||||||
|
{
|
||||||
|
$GLOBALS['bo_nav_group_stack'][] = [$key, $label, $icon, $sub];
|
||||||
|
ob_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fecha o grupo aberto por bo_nav_group_start() e escreve o markup final. */
|
||||||
|
function bo_nav_group_end(): void
|
||||||
|
{
|
||||||
|
$items = (string) ob_get_clean();
|
||||||
|
[$key, $label, $icon, $sub] = array_pop($GLOBALS['bo_nav_group_stack']);
|
||||||
|
$hasActive = str_contains($items, 'aria-current="page"');
|
||||||
|
$id = 'bo-grp-' . $key;
|
||||||
|
$k = htmlspecialchars($key, ENT_QUOTES, 'UTF-8');
|
||||||
|
$l = htmlspecialchars($label, ENT_QUOTES, 'UTF-8');
|
||||||
|
$i = htmlspecialchars($icon, ENT_QUOTES, 'UTF-8');
|
||||||
|
$short = htmlspecialchars(mb_strtoupper(mb_substr($label, 0, 4)), ENT_QUOTES, 'UTF-8');
|
||||||
|
echo '<div class="bo-nav-group' . ($sub ? ' bo-nav-group--sub' : '') . ($hasActive ? ' has-active' : '') . '" data-group="' . $k . '">'
|
||||||
|
. '<button class="bo-nav-group__label" type="button" data-short="' . $short . '" aria-expanded="true"'
|
||||||
|
. ' aria-controls="' . $id . '" data-group-toggle="' . $k . '" title="' . $l . '" aria-label="' . $l . '">'
|
||||||
|
. '<i class="bi ' . $i . ' bo-nav-group__icon" aria-hidden="true"></i>'
|
||||||
|
. '<span class="bo-nav-group__text">' . $l . '</span>'
|
||||||
|
. '<i class="bi bi-chevron-down bo-nav-group__chevron" aria-hidden="true"></i></button>'
|
||||||
|
. '<div class="bo-nav-group__body" id="' . $id . '"><div class="bo-nav-group__inner">' . $items . '</div></div>'
|
||||||
|
. '</div>' . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Iniciais para o avatar (até duas letras: primeiro e último nome). */
|
||||||
|
function bo_initials(string $name): string
|
||||||
|
{
|
||||||
|
// Só palavras que começam por letra: «Staff (dev)» dá «SD»... não «S(».
|
||||||
|
preg_match_all('/\p{L}[\p{L}\p{M}-]*/u', $name, $m);
|
||||||
|
$parts = $m[0];
|
||||||
|
if (!$parts) return '?';
|
||||||
|
$first = mb_substr($parts[0], 0, 1);
|
||||||
|
$last = count($parts) > 1 ? mb_substr($parts[count($parts) - 1], 0, 1) : '';
|
||||||
|
return mb_strtoupper($first . $last);
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Standalone top navbar — only outputs when header.php hasn't been included yet.
|
|
||||||
// (system_settings.php includes both header.php + navbar.php, so this defers.)
|
|
||||||
if (defined('ADMIN_SHELL_OPENED') && ADMIN_SHELL_OPENED) return;
|
|
||||||
?>
|
|
||||||
<link href="<?= base_url('assets/vendor/bootstrap5/css/bootstrap.min.css') ?>" rel="stylesheet">
|
|
||||||
<link href="<?= base_url('assets/vendor/bootstrap-icons/bootstrap-icons.css') ?>" rel="stylesheet">
|
|
||||||
<nav class="navbar navbar-expand navbar-dark px-3 mb-0" style="position:sticky;top:0;z-index:99;background:var(--bo-accent, #C2D500);">
|
|
||||||
<a class="navbar-brand fw-bold text-dark" href="<?= base_url('views/admin/dashboard.php') ?>">
|
|
||||||
<i class="bi bi-lightning-charge-fill me-2"></i><?= htmlspecialchars(defined('APP_NAME') ? APP_NAME : 'FitMe 3070') ?>
|
|
||||||
</a>
|
|
||||||
<ul class="navbar-nav ms-auto">
|
|
||||||
<li class="nav-item">
|
|
||||||
<span class="nav-link text-dark"><?= htmlspecialchars($user['name'] ?? '') ?></span>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item"><a class="nav-link text-dark" href="<?= base_url('views/auth/logout.php') ?>"><i class="bi bi-box-arrow-right"></i></a></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
// Painel da equipa (role staff). O staff ainda não tem páginas de gestão:
|
||||||
|
// tudo em views/admin/ exige admin. Fica a saudação e o acesso ao perfil.
|
||||||
|
require __DIR__ . '/../../config/db.php';
|
||||||
|
require __DIR__ . '/../../config/functions.php';
|
||||||
|
$user = require_staff();
|
||||||
|
|
||||||
|
$page_title = 'Painel';
|
||||||
|
include __DIR__ . '/../../includes/header.php';
|
||||||
|
?>
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="h3 mb-4">Olá, <?= htmlspecialchars($user['name']) ?></h1>
|
||||||
|
|
||||||
|
<div class="card shadow-sm"><div class="card-body">
|
||||||
|
<p class="mb-3">Bem-vindo à área da equipa FitMe 3070.</p>
|
||||||
|
<a class="btn btn-outline-primary" href="<?= base_url('views/user/profile.php') ?>"><i class="bi bi-person me-1"></i>O meu perfil</a>
|
||||||
|
<p class="text-muted small mt-3 mb-0">As ferramentas da equipa serão disponibilizadas num passo seguinte.</p>
|
||||||
|
</div></div>
|
||||||
|
</div>
|
||||||
|
<?php include __DIR__ . '/../../includes/footer.php'; ?>
|
||||||
@@ -128,7 +128,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
include __DIR__ . '/../../includes/header.php';
|
include __DIR__ . '/../../includes/header.php';
|
||||||
include __DIR__ . '/../../includes/navbar.php';
|
|
||||||
?>
|
?>
|
||||||
<div class="container py-4" style="max-width:900px;">
|
<div class="container py-4" style="max-width:900px;">
|
||||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
|
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
|
||||||
|
|||||||
Reference in new issue
Block a user