Files
fitme3070.pt/config/functions_trials.php
andreandClaude Opus 5.5 04c8c83e66 feat: aulas experimentais guardadas, anti-spam partilhado nos formulários, audit das escritas
- Pedido de aula experimental passa a ficar em trial_leads (por ginásio)
  ANTES do email: sem SMTP o visitante já não vê «erro» e o pedido não se
  perde. Gestão → Aulas experimentais: estados, notas, CSV (auditado),
  apagar para a lixeira; aviso «sem email» quando o aviso não saiu.
- Aula experimental, inscrição e eventos usam o anti-spam do platform-tools
  (honeypot, carimbo assinado com tempo mínimo, prova de JS, limite por IP,
  pontuação de conteúdo) e o bloqueio de IP, em vez de honeypots locais.
  Recusas ficam em blocked_submissions; resposta igual à de sucesso.
  Falha aberto se o módulo rebentar.
- Checkbox de consentimento da aula experimental passa a ser guardada.
- Audit das escritas de conteúdo: criar/editar equipa, modalidades,
  eventos, mensalidades, pedidos de inscrição, galerias, horário (incl.
  arrastar no calendário), horas, contactos e ginásios, com antes/depois
  campo a campo (audit_row/audit_change). Exportação CSV de inscrições
  auditada.
- .table-responsive em todas as tabelas do backoffice.
- Migração: database/migration_20260926_trial_leads.sql (só cria tabelas).

Testado numa cópia da BD de produção: 63/63 verificações HTTP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:33:24 +01:00

81 lines
3.9 KiB
PHP

<?php
// ─── Pedidos de aula experimental ────────────────────────────────────────────
// Guardados por site/agendar.php ANTES de se tentar o email, para nenhum
// pedido depender do SMTP. Geridos em Gestão → Aulas experimentais
// (views/admin/trials/index.php), por ginásio. Apagar vai para a lixeira.
// Schema: database/migration_20260926_trial_leads.sql.
function trial_lead_statuses(): array {
return ['novo' => 'Novo', 'contactado' => 'Contactado', 'marcado' => 'Aula marcada', 'descartado' => 'Descartado'];
}
function db_trial_lead_create(PDO $pdo, array $d): int {
$stmt = $pdo->prepare("
INSERT INTO trial_leads (gym_id, name, email, phone, preferred_date, period, message,
consent, consent_text, ip, user_agent)
VALUES (?,?,?,?,?,?,?,?,?,?,?)
");
$stmt->execute([
(int) ($d['gym_id'] ?? 1),
mb_substr(trim((string) ($d['name'] ?? '')), 0, 150),
mb_substr(trim((string) ($d['email'] ?? '')), 0, 190),
mb_substr(trim((string) ($d['phone'] ?? '')), 0, 40),
($d['preferred_date'] ?? '') !== '' ? $d['preferred_date'] : null,
mb_substr(trim((string) ($d['period'] ?? '')), 0, 40),
mb_substr(trim((string) ($d['message'] ?? '')), 0, 2000),
!empty($d['consent']) ? 1 : 0,
mb_substr((string) ($d['consent_text'] ?? ''), 0, 255),
mb_substr((string) ($d['ip'] ?? ''), 0, 45),
mb_substr((string) ($d['user_agent'] ?? ''), 0, 255),
]);
return (int) $pdo->lastInsertId();
}
function db_trial_lead_mark_emailed(PDO $pdo, int $id): void {
$pdo->prepare("UPDATE trial_leads SET email_sent = 1 WHERE id = ?")->execute([$id]);
}
function db_trial_leads_list(PDO $pdo, int $gymId, string $status = ''): array {
$sql = "SELECT * FROM trial_leads WHERE deleted_at IS NULL AND gym_id = ?";
$args = [$gymId];
if ($status !== '' && isset(trial_lead_statuses()[$status])) { $sql .= " AND status = ?"; $args[] = $status; }
$sql .= " ORDER BY created_at DESC, id DESC LIMIT 1000";
$stmt = $pdo->prepare($sql);
$stmt->execute($args);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
function db_trial_leads_count_new(PDO $pdo, int $gymId): int {
try {
$st = $pdo->prepare("SELECT COUNT(*) FROM trial_leads WHERE status = 'novo' AND deleted_at IS NULL AND gym_id = ?");
$st->execute([$gymId]);
return (int) $st->fetchColumn();
} catch (Throwable $e) { return 0; }
}
function db_trial_lead_update(PDO $pdo, int $id, int $gymId, string $status, string $notes): bool {
if (!isset(trial_lead_statuses()[$status])) return false;
$before = $pdo->prepare("SELECT status, admin_notes, name FROM trial_leads WHERE id = ? AND gym_id = ? AND deleted_at IS NULL");
$before->execute([$id, $gymId]);
$old = $before->fetch(PDO::FETCH_ASSOC);
if (!$old) return false;
$stmt = $pdo->prepare("UPDATE trial_leads SET status = ?, admin_notes = ?, updated_by = ? WHERE id = ? AND gym_id = ?");
$stmt->execute([$status, $notes !== '' ? $notes : null, (int) ($_SESSION['user_id'] ?? 0) ?: null, $id, $gymId]);
audit('trial_lead.update', [
'target_type' => 'trial_lead', 'target_id' => $id, 'target_label' => (string) $old['name'],
'changes' => platform_audit_diff(
['status' => $old['status'], 'admin_notes' => (string) $old['admin_notes']],
['status' => $status, 'admin_notes' => $notes]
),
]);
return $stmt->rowCount() > 0;
}
// Vai para a lixeira — só se pertencer ao ginásio que se está a editar.
function db_trial_lead_delete(PDO $pdo, int $id, int $gymId): bool {
$own = $pdo->prepare("SELECT 1 FROM trial_leads WHERE id = ? AND gym_id = ?");
$own->execute([$id, $gymId]);
if (!$own->fetchColumn()) return false;
return trash_delete($pdo, 'trial_lead', $id);
}