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>
This commit is contained in:
1 parent
0e8b53f620
commit
04c8c83e66
29 files changed
+480
-29
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
@@ -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).
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
// ─── Formulários públicos: anti-spam e bloqueio de IP (platform-tools) ──────
|
||||
// Aula experimental, inscrição e eventos usam as defesas partilhadas da frota
|
||||
// em vez de um honeypot local por formulário:
|
||||
// - platform_ip_guard(): um IP banido não chega ao formulário;
|
||||
// - platform_antispam_check(): honeypot, carimbo assinado com tempo mínimo
|
||||
// (time-trap), prova de JavaScript, limite por IP e pontuação de conteúdo.
|
||||
// Uma submissão recusada recebe a mesma resposta de sucesso (o bot não aprende
|
||||
// nada) e fica em `blocked_submissions`, para um falso positivo se poder ler.
|
||||
//
|
||||
// Falha aberto: se o módulo rebentar, o formulário continua a funcionar —
|
||||
// segurança que derruba o formulário é uma avaria, não uma proteção.
|
||||
|
||||
require_once __DIR__ . '/../shared/platform-tools/functions_ip_ban.php';
|
||||
require_once __DIR__ . '/../shared/platform-tools/functions_antispam_forms.php';
|
||||
|
||||
platform_antispam_config([
|
||||
// NÃO mudar num site a correr: invalida os formulários abertos nesse momento.
|
||||
'key_domain' => '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 '';
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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).
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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]],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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).
|
||||
|
||||
@@ -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), ''))",
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<?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);
|
||||
}
|
||||
@@ -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).
|
||||
|
||||
Reference in new issue
Block a user