- 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>
81 lines
3.9 KiB
PHP
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);
|
|
}
|