Files
fitme3070.pt/views/admin/reviews/index.php
T
andreandClaude Opus 5.5 f95c9ebc52 feat: contactos editáveis, equipa com título/aptidões, mensalidades e reviews Google
- Contactos: nova página no backoffice (fitme3070_info) e 964 201 137 como
  telefone principal; o telefone secundário só aparece quando existe.
- Equipa: título, aptidões/funções e formações; o cartão mostra foto, nome e
  título e abre um cartão horizontal de detalhe (hover no desktop, toque no
  telemóvel) sem mexer na grelha.
- Mensalidades: cartões editáveis com um destaque, modal de inscrição que
  guarda o pedido (RGPD + estado + campos reservados para o CRM) e avisa o
  ginásio por e-mail; lista de pedidos com estados, notas e CSV.
- Reviews Google via Places API (New), filtradas por classificação mínima e
  em cache configurável; página de configuração com pesquisa do Place ID.
- Assets com ?v=<mtime>: site/assets é servido com cache imutável de 1 ano.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 14:58:57 +01:00

162 lines
10 KiB
PHP

<?php
require __DIR__ . '/../../../config/db.php';
require __DIR__ . '/../../../config/functions.php';
$user = require_admin();
$search = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_check();
$uid = (int) ($user['id'] ?? 0) ?: null;
$action = (string) ($_POST['action'] ?? '');
if ($action === 'save') {
// Key: blank keeps the stored one; the "remover" box clears it.
$key = trim((string) ($_POST['google_places_api_key'] ?? ''));
if (!empty($_POST['clear_key'])) {
db_update_setting($pdo, 'google_places_api_key', '', $uid);
} elseif ($key !== '') {
db_update_setting($pdo, 'google_places_api_key', secret_encrypt($key), $uid);
}
db_update_setting($pdo, 'google_place_id', trim((string) ($_POST['google_place_id'] ?? '')), $uid);
db_update_setting($pdo, 'google_reviews_min_rating', (string) max(1, min(5, (int) ($_POST['min_rating'] ?? 4))), $uid);
db_update_setting($pdo, 'google_reviews_max', (string) max(1, min(5, (int) ($_POST['max'] ?? 3))), $uid);
db_update_setting($pdo, 'google_reviews_cache_hours', (string) max(1, (int) ($_POST['cache_hours'] ?? 24)), $uid);
reviews_cache_clear(); // new settings → next view fetches fresh
session()->setFlashdata('success', 'Definições guardadas.');
header('Location: ' . base_url('views/admin/reviews/index.php'));
exit;
}
if ($action === 'refresh') {
$r = reviews_refresh();
session()->setFlashdata(!empty($r['error']) ? 'error' : 'success',
!empty($r['error']) ? 'Falhou: ' . $r['error'] : 'Reviews atualizadas (' . count($r['reviews'] ?? []) . ' recebidas da Google).');
header('Location: ' . base_url('views/admin/reviews/index.php'));
exit;
}
if ($action === 'search') {
$apiKey = reviews_config()['api_key'];
$q = trim((string) ($_POST['query'] ?? ''));
$search = ($apiKey === '') ? ['error' => 'Guarda primeiro a chave da API.', 'places' => []]
: ($q === '' ? ['error' => 'Escreve o nome do ginásio.', 'places' => []] : reviews_find_place($q, $apiKey));
}
}
$cfg = reviews_config();
$env_key = (string) env('GOOGLE_PLACES_API_KEY', '') !== '';
$cache = reviews_cache_read();
$preview = site_reviews();
$info = site_info($pdo);
$page_title = 'Reviews Google';
include __DIR__ . '/../../../includes/header.php';
?>
<div class="container" style="max-width:900px;">
<div class="d-flex justify-content-between align-items-center mb-3">
<h1 class="h3 mb-0">Reviews Google</h1>
<a class="btn btn-outline-secondary btn-sm" href="<?= base_url('home#reviews') ?>" target="_blank" rel="noopener"><i class="bi bi-box-arrow-up-right me-1"></i>Ver no site</a>
</div>
<p class="text-muted small">A secção mostra as melhores reviews do ginásio no Google, obtidas pela Places API. A Google devolve no máximo 5 reviews (escolhidas por relevância); o site mostra as que têm a classificação mínima definida. Se nenhuma se qualificar, a secção fica escondida.</p>
<div class="card shadow-sm mb-4"><div class="card-body">
<h2 class="h5 mb-3">Configuração</h2>
<form method="POST">
<?= csrf_field() ?>
<input type="hidden" name="action" value="save">
<div class="row g-3">
<div class="col-md-7">
<label class="form-label">Chave da API (Places API — New)</label>
<?php if ($env_key): ?>
<input class="form-control" disabled value="Definida no .env (GOOGLE_PLACES_API_KEY)">
<?php else: ?>
<input class="form-control font-monospace" type="password" name="google_places_api_key" autocomplete="off"
placeholder="<?= $cfg['api_key'] !== '' ? 'Guardada — deixa vazio para manter' : 'AIza…' ?>">
<?php if ($cfg['api_key'] !== ''): ?>
<div class="form-check mt-1"><input class="form-check-input" type="checkbox" name="clear_key" id="clear_key" value="1">
<label class="form-check-label small" for="clear_key">Remover chave guardada</label></div>
<?php endif; ?>
<?php if (!secrets_available()): ?><div class="form-text text-warning">Sem chave de encriptação no .env — a chave fica guardada sem encriptação.</div><?php endif; ?>
<?php endif; ?>
<div class="form-text">Google Cloud Console → ativar «Places API (New)» → Credenciais. Restringe a chave a essa API.</div>
</div>
<div class="col-md-5">
<label class="form-label">Place ID</label>
<input class="form-control font-monospace" name="google_place_id" value="<?= htmlspecialchars($cfg['place_id']) ?>" placeholder="ChIJ…">
<div class="form-text">Não sabes? Usa a pesquisa abaixo.</div>
</div>
<div class="col-md-4">
<label class="form-label">Classificação mínima</label>
<select class="form-select" name="min_rating">
<?php for ($i = 5; $i >= 1; $i--): ?><option value="<?= $i ?>" <?= $cfg['min_rating'] === $i ? 'selected' : '' ?>><?= $i ?> ★<?= $i < 5 ? ' ou mais' : '' ?></option><?php endfor; ?>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Número de reviews</label>
<input type="number" class="form-control" name="max" min="1" max="5" value="<?= $cfg['max'] ?>">
</div>
<div class="col-md-4">
<label class="form-label">Atualizar a cada (horas)</label>
<input type="number" class="form-control" name="cache_hours" min="1" value="<?= $cfg['cache_hours'] ?>">
<div class="form-text">Cada atualização é um pedido pago à Google.</div>
</div>
</div>
<button class="btn btn-primary mt-3">Guardar</button>
</form>
</div></div>
<div class="card shadow-sm mb-4"><div class="card-body">
<h2 class="h5 mb-3">Encontrar o Place ID</h2>
<form method="POST" class="d-flex gap-2">
<?= csrf_field() ?>
<input type="hidden" name="action" value="search">
<input class="form-control" name="query" value="<?= htmlspecialchars((string) ($_POST['query'] ?? trim($info['name'] . ' ' . $info['adress']))) ?>">
<button class="btn btn-outline-primary text-nowrap"><i class="bi bi-search me-1"></i>Procurar</button>
</form>
<?php if ($search !== null): ?>
<?php if ($search['error'] !== ''): ?><div class="alert alert-danger mt-3 mb-0"><?= htmlspecialchars($search['error']) ?></div>
<?php elseif (!$search['places']): ?><div class="alert alert-warning mt-3 mb-0">Sem resultados.</div>
<?php else: ?>
<ul class="list-group mt-3">
<?php foreach ($search['places'] as $p): ?>
<li class="list-group-item d-flex justify-content-between align-items-center gap-2">
<div><strong><?= htmlspecialchars($p['name']) ?></strong>
<?php if ($p['rating'] !== null): ?><span class="small text-muted"> · <?= htmlspecialchars((string) $p['rating']) ?> ★ (<?= (int) $p['total'] ?>)</span><?php endif; ?>
<div class="small text-muted"><?= htmlspecialchars($p['address']) ?></div>
<code class="small"><?= htmlspecialchars($p['id']) ?></code></div>
<button type="button" class="btn btn-sm btn-outline-success text-nowrap" onclick="document.querySelector('[name=google_place_id]').value='<?= htmlspecialchars($p['id'], ENT_QUOTES) ?>';window.scrollTo(0,0);">Usar</button>
</li>
<?php endforeach; ?>
</ul>
<div class="form-text">Depois de «Usar», carrega em Guardar.</div>
<?php endif; ?>
<?php endif; ?>
</div></div>
<div class="card shadow-sm"><div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="h5 mb-0">Estado</h2>
<form method="POST" class="mb-0"><?= csrf_field() ?><input type="hidden" name="action" value="refresh">
<button class="btn btn-sm btn-outline-primary" <?= reviews_configured() ? '' : 'disabled' ?>><i class="bi bi-arrow-clockwise me-1"></i>Atualizar agora</button></form>
</div>
<?php if (!reviews_configured()): ?>
<p class="text-muted mb-0">Por configurar — a secção não aparece no site.</p>
<?php else: ?>
<p class="small mb-2">
Última atualização: <?= !empty($cache['fetched_at']) ? date('d/m/Y H:i', (int) $cache['fetched_at']) : '—' ?>
<?php if (isset($cache['rating'])): ?> · <?= htmlspecialchars(number_format((float) $cache['rating'], 1, ',', '')) ?> ★ em <?= (int) ($cache['total'] ?? 0) ?> avaliações<?php endif; ?>
</p>
<?php if (!empty($cache['error'])): ?><div class="alert alert-danger small">Último erro: <?= htmlspecialchars($cache['error']) ?></div><?php endif; ?>
<?php if ($preview): ?>
<p class="small text-muted mb-2">No site aparecem <?= count($preview['reviews']) ?>:</p>
<?php foreach ($preview['reviews'] as $r): ?>
<div class="border rounded p-2 mb-2 small">
<strong><?= htmlspecialchars($r['author']) ?></strong> · <?= str_repeat('★', $r['rating']) ?> · <span class="text-muted"><?= htmlspecialchars($r['relative']) ?></span>
<div><?= nl2br(htmlspecialchars(mb_strimwidth($r['text'], 0, 240, '…'))) ?></div>
</div>
<?php endforeach; ?>
<?php else: ?>
<p class="text-muted small mb-0">Nenhuma review qualifica neste momento — a secção está escondida.</p>
<?php endif; ?>
<?php endif; ?>
</div></div>
</div>
<?php include __DIR__ . '/../../../includes/footer.php';