diff --git a/config/functions.php b/config/functions.php index 4d4742b..69f524d 100644 --- a/config/functions.php +++ b/config/functions.php @@ -37,6 +37,8 @@ require_once __DIR__ . '/functions_events.php'; // time-limited events + require_once __DIR__ . '/functions_info.php'; // contactos (fitme3070_info) editor require_once __DIR__ . '/functions_memberships.php'; // mensalidades + pedidos de inscrição require_once __DIR__ . '/functions_reviews.php'; // reviews Google (Places API) + cache +require_once __DIR__ . '/functions_trials.php'; // pedidos de aula experimental +require_once __DIR__ . '/functions_forms.php'; // anti-spam + bloqueio de IP dos formulários públicos // ─── DB-backed settings → legacy constants ─────────────────────────────────── // Resolved once from the settings cache so call sites read plain constants. diff --git a/config/functions_audit.php b/config/functions_audit.php index b7d36b8..66a3747 100644 --- a/config/functions_audit.php +++ b/config/functions_audit.php @@ -23,3 +23,37 @@ function audit_mask_email(string $email): string { [$u, $d] = explode('@', $email, 2); return mb_substr($u, 0, 1) . '…@' . $d; } + +// ─── Escritas de conteúdo ──────────────────────────────────────────────────── +// Os modelos (config/functions_*.php) chamam audit_row() antes de alterar e +// audit_change() depois: o registo leva o antes/depois campo a campo +// (platform_audit_diff) ou, numa criação, os campos criados. Tabela e chave vêm +// do código, nunca do pedido, e ainda assim são validadas. + +function audit_row(PDO $pdo, string $table, string $pk, int $id): ?array { + if (!preg_match('/^[A-Za-z0-9_]+$/', $table) || !preg_match('/^[A-Za-z0-9_]+$/', $pk)) return null; + try { + $st = $pdo->prepare("SELECT * FROM `{$table}` WHERE `{$pk}` = ? LIMIT 1"); + $st->execute([$id]); + $row = $st->fetch(PDO::FETCH_ASSOC); + return $row ?: null; + } catch (Throwable $e) { + return null; + } +} + +function audit_change(PDO $pdo, string $entity, string $table, string $pk, int $id, string $verb, + ?array $before = null, string $labelCol = 'name'): void { + if (!function_exists('platform_audit') || $id < 1) return; + $after = audit_row($pdo, $table, $pk, $id) ?? []; + $changes = $before !== null + ? platform_audit_diff($before, $after, ['updated_at', 'created_at', 'updated_by']) + : platform_audit_created(array_diff_key($after, array_flip(['created_at', 'updated_at']))); + if ($verb !== 'create' && $before !== null && !$changes) return; // gravou sem mudar nada + platform_audit($pdo, $entity . '.' . $verb, [ + 'target_type' => $entity, + 'target_id' => $id, + 'target_label' => (string) ($after[$labelCol] ?? $before[$labelCol] ?? ''), + 'changes' => $changes, + ]); +} diff --git a/config/functions_events.php b/config/functions_events.php index e55f53c..948e0b8 100644 --- a/config/functions_events.php +++ b/config/functions_events.php @@ -128,10 +128,13 @@ function db_events_create(PDO $pdo, array $data): int { str_replace('T', ' ', (string) $d['publish_until']), (int) $d['is_active'], $d['cta_label'], $d['cta_url'], (int) $d['show_lead_form'], ]); - return (int) $pdo->lastInsertId(); + $__id = (int) $pdo->lastInsertId(); + audit_change($pdo, 'event', 'events', 'e_id', (int) $__id, 'create', null, 'title'); + return $__id; } function db_events_update(PDO $pdo, int $id, array $data): bool { + $__before = audit_row($pdo, 'events', 'e_id', (int) $id); $d = array_merge(events_defaults(), $data); $stmt = $pdo->prepare(" UPDATE events SET @@ -150,7 +153,9 @@ function db_events_update(PDO $pdo, int $id, array $data): bool { (int) $d['is_active'], $d['cta_label'], $d['cta_url'], (int) $d['show_lead_form'], $id, ]); - return $stmt->rowCount() > 0; + $__r = $stmt->rowCount() > 0; + audit_change($pdo, 'event', 'events', 'e_id', (int) $id, 'update', $__before, 'title'); + return $__r; } // Soft delete para a lixeira (config/functions_trash.php). diff --git a/config/functions_forms.php b/config/functions_forms.php new file mode 100644 index 0000000..a90f947 --- /dev/null +++ b/config/functions_forms.php @@ -0,0 +1,47 @@ + 'fitme-antispam|', + 'log_table' => 'blocked_submissions', + 'keywords' => ['seo', 'backlink', 'backlinks', 'crypto', 'bitcoin', 'casino', 'viagra', 'loan', + 'guest post', 'web design', 'website traffic', 'ranking', 'forex', 'investment opportunity'], + // Marcas do português: quem as usa não é pontuado como «texto estrangeiro». + 'native' => '/\b(ol[aá]|bom dia|boa (tarde|noite)|gostaria|queria|aulas?|gin[aá]sio|treino|obrigad[oa]|marcar|experimental|hor[aá]rio)\b/u', +]); + +// Devolve o motivo da recusa (já registado) ou null para seguir. +function public_form_screen(PDO $pdo, string $form, string $table, string $name, string $text): ?string { + try { + platform_ip_guard($pdo); // sai com 403 se o IP estiver banido + return platform_antispam_check($pdo, $form, $table, ['name' => $name, 'text' => $text]); + } catch (Throwable $e) { + error_log('[antispam] ' . $form . ': ' . $e->getMessage()); + return null; + } +} + +// Os campos escondidos que o formulário tem de levar (honeypot + carimbo + +// prova de JS). Sem eles o carimbo falha e a submissão é recusada. +function public_form_trap(string $form): string { + try { + return platform_antispam_trap_field($form, $GLOBALS['pdo'] ?? null); + } catch (Throwable $e) { + error_log('[antispam] trap ' . $form . ': ' . $e->getMessage()); + return ''; + } +} diff --git a/config/functions_galerias.php b/config/functions_galerias.php index f1342c9..9e862b7 100644 --- a/config/functions_galerias.php +++ b/config/functions_galerias.php @@ -17,13 +17,18 @@ function db_gallery_get(PDO $pdo, int $id): ?array { function db_gallery_create(PDO $pdo, string $name): int { $stmt = $pdo->prepare("INSERT INTO galleries (name) VALUES (?)"); $stmt->execute([$name]); - return (int) $pdo->lastInsertId(); + $__id = (int) $pdo->lastInsertId(); + audit_change($pdo, 'gallery', 'galleries', 'id', (int) $__id, 'create', null, 'name'); + return $__id; } function db_gallery_update(PDO $pdo, int $id, string $name): bool { + $__before = audit_row($pdo, 'galleries', 'id', (int) $id); $stmt = $pdo->prepare("UPDATE galleries SET name = ? WHERE id = ?"); $stmt->execute([$name, $id]); - return $stmt->rowCount() > 0; + $__r = $stmt->rowCount() > 0; + audit_change($pdo, 'gallery', 'galleries', 'id', (int) $id, 'update', $__before, 'name'); + return $__r; } // Soft delete para a lixeira (config/functions_trash.php). As imagens ficam diff --git a/config/functions_gyms.php b/config/functions_gyms.php index 9fa8232..01aec4e 100644 --- a/config/functions_gyms.php +++ b/config/functions_gyms.php @@ -199,10 +199,13 @@ function db_gyms_create(PDO $pdo, array $d): int { facebook, messenger, email, website, google_maps, google_maps_link) VALUES (?, ?, '', '', '', '', '', '', '', '', '', '', '')")->execute([$id, $d['name']]); $pdo->commit(); - return $id; + $__id = $id; + audit_change($pdo, 'gym', 'gyms', 'id', (int) $__id, 'create', null, 'name'); + return $__id; } function db_gyms_update(PDO $pdo, int $id, array $d): bool { + $__before = audit_row($pdo, 'gyms', 'id', (int) $id); $pdo->beginTransaction(); $params = _gyms_params($d); $params[] = $id; $st = $pdo->prepare("UPDATE gyms SET slug = ?, name = ?, locality = ?, postal_code = ?, is_published = ?, @@ -214,7 +217,9 @@ function db_gyms_update(PDO $pdo, int $id, array $d): bool { $changed = $st->rowCount() > 0; if ($d['is_default']) $pdo->prepare("UPDATE gyms SET is_default = 0 WHERE id <> ?")->execute([$id]); $pdo->commit(); - return $changed; + $__r = $changed; + audit_change($pdo, 'gym', 'gyms', 'id', (int) $id, 'update', $__before, 'name'); + return $__r; } // Soft delete. The default gym can't go (the root URL needs somewhere to land). diff --git a/config/functions_horario.php b/config/functions_horario.php index da5396a..e19b4a5 100644 --- a/config/functions_horario.php +++ b/config/functions_horario.php @@ -42,10 +42,13 @@ function db_horario_create(PDO $pdo, array $data): int { $data['hora_fim'], $data['local'] ?? '', ]); - return (int) $pdo->lastInsertId(); + $__id = (int) $pdo->lastInsertId(); + audit_change($pdo, 'horario', 'horario', 'id', (int) $__id, 'create', null, 'dia_semana'); + return $__id; } function db_horario_update(PDO $pdo, int $id, array $data): bool { + $__before = audit_row($pdo, 'horario', 'id', (int) $id); $stmt = $pdo->prepare(" UPDATE horario SET id_modalidade = ?, dia_semana = ?, hora_inicio = ?, hora_fim = ?, local = ? @@ -59,7 +62,9 @@ function db_horario_update(PDO $pdo, int $id, array $data): bool { $data['local'] ?? '', $id, ]); - return $stmt->rowCount() > 0; + $__r = $stmt->rowCount() > 0; + audit_change($pdo, 'horario', 'horario', 'id', (int) $id, 'update', $__before, 'dia_semana'); + return $__r; } // Soft delete para a lixeira (config/functions_trash.php). @@ -71,11 +76,14 @@ function db_horario_delete(PDO $pdo, int $id): bool { // Modalidade and local are left untouched. Times must be HH:MM[:SS]; the caller // is responsible for validating the day against the allowed set. function db_horario_move(PDO $pdo, int $id, string $dia_semana, string $hora_inicio, string $hora_fim): bool { + $__before = audit_row($pdo, 'horario', 'id', (int) $id); $stmt = $pdo->prepare(" UPDATE horario SET dia_semana = ?, hora_inicio = ?, hora_fim = ? WHERE id = ? "); $stmt->execute([$dia_semana, $hora_inicio, $hora_fim, $id]); - return $stmt->rowCount() >= 0; + $__r = $stmt->rowCount() >= 0; + audit_change($pdo, 'horario', 'horario', 'id', (int) $id, 'update', $__before, 'dia_semana'); + return $__r; } // Modalidades dropdown (id => nome + cor) for horario create/edit forms. diff --git a/config/functions_info.php b/config/functions_info.php index 2c6a28a..b97918b 100644 --- a/config/functions_info.php +++ b/config/functions_info.php @@ -45,6 +45,7 @@ function db_info_save(PDO $pdo, array $data): bool { $cols = array_keys(info_fields()); // One row per gym (id = gyms.id): the gym being edited in the backoffice. $gid = admin_gym_id(); + $__before = audit_row($pdo, 'fitme3070_info', 'id', (int) $gid); $exists = (bool) $pdo->query("SELECT COUNT(*) FROM fitme3070_info WHERE id = " . $gid)->fetchColumn(); $vals = array_map(fn($c) => $data[$c] ?? '', $cols); if ($exists) { @@ -56,5 +57,7 @@ function db_info_save(PDO $pdo, array $data): bool { $stmt = $pdo->prepare("INSERT INTO fitme3070_info (id, $names, updated_at) VALUES (" . $gid . ", $marks, NOW())"); } $stmt->execute($vals); - return $stmt->rowCount() > 0; + $__r = $stmt->rowCount() > 0; + audit_change($pdo, 'info', 'fitme3070_info', 'id', (int) $gid, $__before ? 'update' : 'create', $__before, 'name'); + return $__r; } diff --git a/config/functions_memberships.php b/config/functions_memberships.php index b6aa02a..0c0534a 100644 --- a/config/functions_memberships.php +++ b/config/functions_memberships.php @@ -93,10 +93,13 @@ function db_memberships_create(PDO $pdo, array $d): int { $id = (int) $pdo->lastInsertId(); if ($d['is_featured']) _memberships_clear_featured($pdo, $id); $pdo->commit(); - return $id; + $__id = $id; + audit_change($pdo, 'membership', 'memberships', 'id', (int) $__id, 'create', null, 'name'); + return $__id; } function db_memberships_update(PDO $pdo, int $id, array $d): bool { + $__before = audit_row($pdo, 'memberships', 'id', (int) $id); $pdo->beginTransaction(); $stmt = $pdo->prepare(" UPDATE memberships SET name = ?, subtitle = ?, price = ?, is_addon = ?, price_suffix = ?, @@ -110,7 +113,9 @@ function db_memberships_update(PDO $pdo, int $id, array $d): bool { $changed = $stmt->rowCount() > 0; if ($d['is_featured']) _memberships_clear_featured($pdo, $id); $pdo->commit(); - return $changed; + $__r = $changed; + audit_change($pdo, 'membership', 'memberships', 'id', (int) $id, 'update', $__before, 'name'); + return $__r; } // Soft delete para a lixeira (config/functions_trash.php). Deixa de ser o @@ -187,10 +192,13 @@ function db_membership_lead_create(PDO $pdo, array $d): int { } function db_membership_lead_update(PDO $pdo, int $id, string $status, string $notes): bool { + $__before = audit_row($pdo, 'membership_leads', 'id', (int) $id); if (!isset(membership_lead_statuses()[$status])) return false; $stmt = $pdo->prepare("UPDATE membership_leads SET status = ?, admin_notes = ?, updated_at = NOW() WHERE id = ?"); $stmt->execute([$status, $notes !== '' ? $notes : null, $id]); - return $stmt->rowCount() > 0; + $__r = $stmt->rowCount() > 0; + audit_change($pdo, 'membership_lead', 'membership_leads', 'id', (int) $id, 'update', $__before, 'name'); + return $__r; } // Vai para a lixeira (config/functions_trash.php). diff --git a/config/functions_modalidades.php b/config/functions_modalidades.php index 3e44ddb..38aed45 100644 --- a/config/functions_modalidades.php +++ b/config/functions_modalidades.php @@ -30,10 +30,13 @@ function db_modalidades_create(PDO $pdo, array $data): int { $data['descricao'] ?? '', (int) ($data['item_order'] ?? 0), ]); - return (int) $pdo->lastInsertId(); + $__id = (int) $pdo->lastInsertId(); + audit_change($pdo, 'modalidade', 'modalidades', 'm_id', (int) $__id, 'create', null, 'nome'); + return $__id; } function db_modalidades_update(PDO $pdo, int $id, array $data): bool { + $__before = audit_row($pdo, 'modalidades', 'm_id', (int) $id); $stmt = $pdo->prepare(" UPDATE modalidades SET nome = ?, cor = ?, icon = ?, descricao = ?, item_order = ? @@ -47,7 +50,9 @@ function db_modalidades_update(PDO $pdo, int $id, array $data): bool { (int) ($data['item_order'] ?? 0), $id, ]); - return $stmt->rowCount() > 0; + $__r = $stmt->rowCount() > 0; + audit_change($pdo, 'modalidade', 'modalidades', 'm_id', (int) $id, 'update', $__before, 'nome'); + return $__r; } // Soft delete para a lixeira (config/functions_trash.php). diff --git a/config/functions_settings.php b/config/functions_settings.php index fac775c..9d0245c 100644 --- a/config/functions_settings.php +++ b/config/functions_settings.php @@ -101,7 +101,7 @@ function db_update_setting(PDO $pdo, string $key, string $value, ?int $userId = if ($old !== $value && function_exists('platform_audit')) { platform_audit($pdo, 'settings.update', [ 'target_type' => 'setting', 'target_id' => $key, 'target_label' => $key, - 'changes' => [$key => ['antes' => $old, 'depois' => $value]], + 'changes' => [$key => ['from' => $old, 'to' => $value]], ]); } } diff --git a/config/functions_team.php b/config/functions_team.php index 5cf595e..4afee41 100644 --- a/config/functions_team.php +++ b/config/functions_team.php @@ -34,10 +34,13 @@ function db_team_create(PDO $pdo, array $data): int { ($data['certifications'] ?? '') !== '' ? $data['certifications'] : null, $data['photo'] ?? null, ]); - return (int) $pdo->lastInsertId(); + $__id = (int) $pdo->lastInsertId(); + audit_change($pdo, 'team', 'team', 'id', (int) $__id, 'create', null, 'name'); + return $__id; } function db_team_update(PDO $pdo, int $id, array $data, ?string $photo = null): bool { + $__before = audit_row($pdo, 'team', 'id', (int) $id); $sets = ["name = ?", "title = ?", "birthday = ?", "`at` = ?", "status = ?", "description = ?", "skills = ?", "certifications = ?", "updated_at = NOW()"]; $params = [ @@ -57,7 +60,9 @@ function db_team_update(PDO $pdo, int $id, array $data, ?string $photo = null): $params[] = $id; $stmt = $pdo->prepare("UPDATE team SET " . implode(', ', $sets) . " WHERE id = ?"); $stmt->execute($params); - return $stmt->rowCount() > 0; + $__r = $stmt->rowCount() > 0; + audit_change($pdo, 'team', 'team', 'id', (int) $id, 'update', $__before, 'name'); + return $__r; } // Soft delete para a lixeira (config/functions_trash.php). diff --git a/config/functions_trash.php b/config/functions_trash.php index 17fbb1b..bd32875 100644 --- a/config/functions_trash.php +++ b/config/functions_trash.php @@ -59,6 +59,11 @@ function trash_types(): array { 'title' => "CONCAT(t.name, COALESCE(CONCAT(' — ', (SELECT e.title FROM events e WHERE e.e_id = t.e_id)), ''))", 'edit' => null, 'personal' => true, ], + 'trial_lead' => [ + 'label' => 'Pedido de aula experimental', 'table' => 'trial_leads', 'pk' => 'id', + 'title' => "CONCAT(t.name, IF(t.phone <> '', CONCAT(' — ', t.phone), ''))", + 'edit' => null, 'personal' => true, + ], 'membership_lead' => [ 'label' => 'Pedido de inscrição', 'table' => 'membership_leads', 'pk' => 'id', 'title' => "CONCAT(t.name, IF(t.membership_name <> '', CONCAT(' — ', t.membership_name), ''))", diff --git a/config/functions_trials.php b/config/functions_trials.php new file mode 100644 index 0000000..83d994c --- /dev/null +++ b/config/functions_trials.php @@ -0,0 +1,80 @@ + '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); +} diff --git a/config/functions_work_hours.php b/config/functions_work_hours.php index c0cc1a8..50b5e84 100644 --- a/config/functions_work_hours.php +++ b/config/functions_work_hours.php @@ -19,13 +19,18 @@ function db_whours_get(PDO $pdo, int $id): ?array { function db_whours_create(PDO $pdo, array $data): int { $stmt = $pdo->prepare("INSERT INTO work_hours (gym_id, week_day, open, close) VALUES (?, ?, ?, ?)"); $stmt->execute([admin_gym_id(), $data['week_day'], $data['open'], $data['close']]); - return (int) $pdo->lastInsertId(); + $__id = (int) $pdo->lastInsertId(); + audit_change($pdo, 'work_hours', 'work_hours', 'id', (int) $__id, 'create', null, 'week_day'); + return $__id; } function db_whours_update(PDO $pdo, int $id, array $data): bool { + $__before = audit_row($pdo, 'work_hours', 'id', (int) $id); $stmt = $pdo->prepare("UPDATE work_hours SET week_day = ?, open = ?, close = ? WHERE id = ?"); $stmt->execute([$data['week_day'], $data['open'], $data['close'], $id]); - return $stmt->rowCount() > 0; + $__r = $stmt->rowCount() > 0; + audit_change($pdo, 'work_hours', 'work_hours', 'id', (int) $id, 'update', $__before, 'week_day'); + return $__r; } // Soft delete para a lixeira (config/functions_trash.php). diff --git a/database/migration_20260926_trial_leads.sql b/database/migration_20260926_trial_leads.sql new file mode 100644 index 0000000..95cd1f9 --- /dev/null +++ b/database/migration_20260926_trial_leads.sql @@ -0,0 +1,51 @@ +-- ─── Pedidos de aula experimental + registo do anti-spam ──────────────────── +-- Até aqui o pedido de aula experimental só existia como email: sem SMTP (ou +-- com o SMTP em baixo) o visitante via «erro» e o pedido perdia-se. Agora fica +-- guardado primeiro, como as inscrições, e o email é só um aviso. +-- +-- blocked_submissions: onde o anti-spam partilhado (platform-tools, +-- functions_antispam_forms.php) guarda o que recusou — um falso positivo é uma +-- lead verdadeira, e tem de se poder ler. Esquema igual ao do dance. +-- +-- Só cria tabelas. Safe to re-run. + +CREATE TABLE IF NOT EXISTS `trial_leads` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `gym_id` INT NOT NULL DEFAULT 1, + `name` VARCHAR(150) NOT NULL, + `email` VARCHAR(190) NOT NULL DEFAULT '', + `phone` VARCHAR(40) NOT NULL DEFAULT '', + `preferred_date` DATE NULL DEFAULT NULL, + `period` VARCHAR(40) NOT NULL DEFAULT '', + `message` TEXT NULL, + `consent` TINYINT(1) NOT NULL DEFAULT 0, + `consent_text` VARCHAR(255) NOT NULL DEFAULT '', + `ip` VARCHAR(45) NOT NULL DEFAULT '', + `user_agent` VARCHAR(255) NOT NULL DEFAULT '', + `status` VARCHAR(20) NOT NULL DEFAULT 'novo', + `admin_notes` TEXT NULL, + `email_sent` TINYINT(1) NOT NULL DEFAULT 0, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `updated_by` INT UNSIGNED NULL DEFAULT NULL, + `deleted_at` DATETIME NULL DEFAULT NULL, + `deleted_by` INT UNSIGNED NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_trial_leads_gym` (`gym_id`, `deleted_at`, `status`), + KEY `idx_trial_leads_ip` (`ip`, `created_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `blocked_submissions` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `form` VARCHAR(40) NOT NULL COMMENT 'trial, inscricao, evento', + `reason` VARCHAR(190) NOT NULL COMMENT 'Regra que travou, ou os sinais somados', + `score` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Pontuação de conteúdo; 0 nas recusas diretas', + `ip` VARCHAR(45) NOT NULL DEFAULT '', + `user_agent` VARCHAR(250) NOT NULL DEFAULT '', + `payload` TEXT DEFAULT NULL COMMENT 'JSON do que foi submetido, sem o token CSRF', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_bs_form` (`form`), + KEY `idx_bs_ip` (`ip`), + KEY `idx_bs_created` (`created_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/includes/header.php b/includes/header.php index b507b82..2d0dd44 100644 --- a/includes/header.php +++ b/includes/header.php @@ -138,6 +138,12 @@ $app_name = defined('APP_NAME') ? APP_NAME : 'FitMe 3070'; Mensalidades +