query($sql)->fetchAll(PDO::FETCH_ASSOC); } function db_modalidades_get(PDO $pdo, int $id): ?array { $stmt = $pdo->prepare("SELECT * FROM modalidades WHERE m_id = ? AND gym_id = ? LIMIT 1"); $stmt->execute([$id, admin_gym_id()]); return $stmt->fetch(PDO::FETCH_ASSOC) ?: null; } function db_modalidades_create(PDO $pdo, array $data): int { $stmt = $pdo->prepare(" INSERT INTO modalidades (gym_id, nome, cor, icon, descricao, item_order) VALUES (?, ?, ?, ?, ?, ?) "); $stmt->execute([ admin_gym_id(), $data['nome'], $data['cor'] ?? '#808080', $data['icon'] ?? '', $data['descricao'] ?? '', (int) ($data['item_order'] ?? 0), ]); return (int) $pdo->lastInsertId(); } function db_modalidades_update(PDO $pdo, int $id, array $data): bool { $stmt = $pdo->prepare(" UPDATE modalidades SET nome = ?, cor = ?, icon = ?, descricao = ?, item_order = ? WHERE m_id = ? "); $stmt->execute([ $data['nome'], $data['cor'] ?? '#808080', $data['icon'] ?? '', $data['descricao'] ?? '', (int) ($data['item_order'] ?? 0), $id, ]); return $stmt->rowCount() > 0; } // Soft delete para a lixeira (config/functions_trash.php). function db_modalidades_delete(PDO $pdo, int $id): bool { return trash_delete($pdo, 'modalidade', $id); } function db_modalidades_restore(PDO $pdo, int $id): bool { return trash_restore($pdo, 'modalidade', $id); } // All flaticon names (populated from the flaticon table). function db_flaticon_list(PDO $pdo): array { try { return $pdo->query("SELECT flat_id, name FROM flaticon ORDER BY name")->fetchAll(PDO::FETCH_ASSOC); } catch (Throwable $e) { return []; } } // ─── Icon image upload / removal ─────────────────────────────────────────────── // The `icon` column holds a filename served from uploads/modalidades/icons/ on // both the backoffice list and the public site. That folder doubles as a shared // flaticon library (numeric-prefixed names like 012-yoga.png) referenced by the // "Ícones disponíveis" picker, so this CRUD must never delete a file it didn't // create. Custom uploads are saved with an `upl_` prefix; modalidades_icon_remove() // only ever unlinks `upl_*` files, leaving the library untouched. function modalidades_icon_dir(): string { return __DIR__ . '/../uploads/modalidades/icons/'; } // Moves an uploaded icon into the icons dir and returns the stored filename. // Returns null when no file was submitted (not an error) or on failure — in the // failure case $error is set with a user-facing reason. function modalidades_icon_upload(array $file, ?string &$error = null): ?string { $error = null; if (($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) { return null; // nothing uploaded — leave the existing icon alone } if ($file['error'] !== UPLOAD_ERR_OK) { $error = 'Falha no upload da imagem (código ' . (int) $file['error'] . ').'; return null; } if (($file['size'] ?? 0) > 2 * 1024 * 1024) { $error = 'A imagem excede o limite de 2 MB.'; return null; } if (empty($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) { $error = 'Upload inválido.'; return null; } $allowed = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg']; // Tipo decidido pelo conteúdo, não pelo nome; SVG só sem scripts // (config/functions_images.php). $ext = upload_image_ext($file, $allowed); if ($ext === null) { $error = 'Formato inválido ou ficheiro que não é uma imagem válida. Permitidos: ' . implode(', ', $allowed) . '.'; return null; } $dir = modalidades_icon_dir(); if (!is_dir($dir)) @mkdir($dir, 0775, true); // Readable, collision-proof, recognisable-as-custom filename. $slug = preg_replace('/[^a-z0-9]+/', '-', strtolower(pathinfo($file['name'], PATHINFO_FILENAME))); $slug = substr(trim((string) $slug, '-'), 0, 40); if ($slug === '') $slug = 'icon'; $filename = 'upl_' . $slug . '_' . bin2hex(random_bytes(4)) . '.' . $ext; if (!move_uploaded_file($file['tmp_name'], $dir . $filename)) { $error = 'Não foi possível guardar a imagem.'; return null; } img_optimize($dir . $filename, IMG_MAX_MODALIDADE); // no-op em SVG return $filename; } // Deletes a custom-uploaded icon from disk. No-ops for empty names and for // shared-library icons (only `upl_*` files are ever removed). basename() guards // against path traversal in stored values. function modalidades_icon_remove(?string $filename): void { $filename = basename((string) $filename); if ($filename === '' || strncmp($filename, 'upl_', 4) !== 0) return; $path = modalidades_icon_dir() . $filename; if (is_file($path)) @unlink($path); }