Files

307 lines
17 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require __DIR__ . '/../../config/db.php';
require __DIR__ . '/../../config/functions.php';
$user = require_sysadmin(); // zona de sysadmin: o link só aparece a sysadmin e o servidor tem de concordar
$logo_dir = __DIR__ . '/../../uploads/public/logo/'; // convenção da plataforma (uploads/.htaccess bloqueia PHP)
$allowed_ext = ['png', 'jpg', 'jpeg', 'svg', 'webp'];
$max_upload = 2 * 1024 * 1024; // 2 MB
$errors = [];
// `app_base_url` lives only in .env (per-install topology). It used to be
// editable here but the DB-backed value caused tenants to drift when a dump
// was cloned, so the field was removed — change APP_BASE_URL in .env instead.
$fields = [
'app_name' => ['label' => 'Nome da aplicação', 'type' => 'text', 'required' => true],
'email_from' => ['label' => 'Email remetente', 'type' => 'email', 'required' => true],
'email_from_name' => ['label' => 'Nome do remetente', 'type' => 'text', 'required' => true],
'email_subject_prefix' => ['label' => 'Prefixo do assunto', 'type' => 'text', 'required' => false],
'reminder_hours' => ['label' => 'Horas de reminder', 'type' => 'number','required' => true],
// Public contacts are NOT here: the site reads them from fitme3070_info,
// edited in Gestão → Contactos (views/admin/info/edit.php). The fleet
// template's contact_* / social_* settings were unused and got edited by
// mistake, so they were removed from this form.
// SEO defaults — used by site/includes/head.php when a page doesn't
// set its own $page_title / $page_description / $page_image override.
'seo_default_title' => ['label' => 'SEO — título por defeito', 'type' => 'text', 'required' => false],
'seo_default_description' => ['label' => 'SEO — descrição por defeito', 'type' => 'text', 'required' => false],
'seo_keywords' => ['label' => 'SEO — palavras-chave', 'type' => 'text', 'required' => false],
'seo_twitter_handle' => ['label' => 'SEO — Twitter handle', 'type' => 'text', 'required' => false],
'seo_locale' => ['label' => 'SEO — locale (og:locale)', 'type' => 'text', 'required' => false],
'seo_title_separator' => ['label' => 'SEO — separador do título', 'type' => 'text', 'required' => false],
];
$current = settings_all();
$values = [];
foreach ($fields as $key => $_) {
$values[$key] = (string) ($current[$key] ?? '');
}
$logo_light = (string) ($current['logo_light_filename'] ?? '');
$logo_dark = (string) ($current['logo_dark_filename'] ?? '');
$seo_image = (string) ($current['seo_default_image_filename'] ?? '');
$seo_dir = __DIR__ . '/../../assets/img/seo/';
function _settings_handle_logo_upload(array $file, string $dir, array $allowed, int $max, ?string $previous, array &$errors, string $kind): ?string {
if (!isset($file['error']) || $file['error'] === UPLOAD_ERR_NO_FILE) {
return null;
}
if ($file['error'] !== UPLOAD_ERR_OK) {
$errors[] = "Falha no upload do logo {$kind}.";
return null;
}
if ($file['size'] > $max) {
$errors[] = "O logo {$kind} excede 2 MB.";
return null;
}
// 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) {
$errors[] = "Tipo de ficheiro não permitido (ou SVG com conteúdo ativo) para o logo {$kind}.";
return null;
}
if (!is_dir($dir) && !@mkdir($dir, 0755, true) && !is_dir($dir)) {
$errors[] = "Não foi possível criar a pasta do logo {$kind}.";
return null;
}
$name = bin2hex(random_bytes(16)) . '.' . $ext;
if (!move_uploaded_file($file['tmp_name'], $dir . $name)) {
$errors[] = "Não foi possível guardar o logo {$kind}.";
return null;
}
if ($previous && $previous !== $name) {
@unlink($dir . $previous);
}
return $name;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_check();
foreach ($fields as $key => $meta) {
$raw = trim((string) ($_POST[$key] ?? ''));
$values[$key] = $raw;
if ($meta['required'] && $raw === '') {
$errors[] = "O campo \"{$meta['label']}\" é obrigatório.";
continue;
}
if ($raw === '') continue;
if ($meta['type'] === 'email' && !filter_var($raw, FILTER_VALIDATE_EMAIL)) {
$errors[] = "Email inválido em \"{$meta['label']}\".";
}
if ($meta['type'] === 'url' && !filter_var($raw, FILTER_VALIDATE_URL)) {
$errors[] = "URL inválido em \"{$meta['label']}\".";
}
if ($meta['type'] === 'number' && !ctype_digit($raw)) {
$errors[] = "\"{$meta['label']}\" tem de ser um número inteiro.";
}
}
$new_light = _settings_handle_logo_upload($_FILES['logo_light'] ?? [], $logo_dir, $allowed_ext, $max_upload, $logo_light, $errors, 'claro');
$new_dark = _settings_handle_logo_upload($_FILES['logo_dark'] ?? [], $logo_dir, $allowed_ext, $max_upload, $logo_dark, $errors, 'escuro');
// SEO share image — same upload helper, separate target directory.
if (!is_dir($seo_dir)) @mkdir($seo_dir, 0755, true);
$seo_allowed = ['png', 'jpg', 'jpeg', 'webp'];
$new_seo_image = _settings_handle_logo_upload($_FILES['seo_default_image'] ?? [], $seo_dir, $seo_allowed, $max_upload, $seo_image, $errors, 'SEO');
if (!$errors) {
$uid = (int) $_SESSION['user_id'];
foreach ($fields as $key => $meta) {
db_update_setting($pdo, $key, $values[$key], $uid);
}
if ($new_light !== null) {
db_update_setting($pdo, 'logo_light_filename', $new_light, $uid);
$logo_light = $new_light;
}
if ($new_dark !== null) {
db_update_setting($pdo, 'logo_dark_filename', $new_dark, $uid);
$logo_dark = $new_dark;
}
if ($new_seo_image !== null) {
db_update_setting($pdo, 'seo_default_image_filename', $new_seo_image, $uid);
$seo_image = $new_seo_image;
}
header('Location: ' . base_url('views/sysadmin/system_settings.php?updated=1'));
exit;
}
}
include __DIR__ . '/../../includes/header.php';
?>
<div class="container py-4" style="max-width:900px;">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
<h4 class="mb-0"><i class="bi bi-sliders me-2"></i>Definições do sistema</h4>
<a href="<?= base_url('views/admin/dashboard.php') ?>" class="btn btn-outline-secondary btn-sm">Voltar</a>
</div>
<?php if (isset($_GET['updated'])): ?>
<div class="alert alert-success">Definições atualizadas.</div>
<?php endif; ?>
<?php foreach ($errors as $e): ?>
<div class="alert alert-danger"><?= htmlspecialchars($e) ?></div>
<?php endforeach; ?>
<form method="POST" enctype="multipart/form-data">
<?= csrf_field() ?>
<div class="card shadow-sm mb-3">
<div class="card-header"><strong>Identidade</strong></div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label small">Nome da aplicação</label>
<input type="text" name="app_name" class="form-control" required maxlength="150"
value="<?= htmlspecialchars($values['app_name']) ?>">
</div>
<div class="col-md-6">
<label class="form-label small">URL base da instalação</label>
<input type="url" class="form-control" readonly disabled
value="<?= htmlspecialchars((string) env('APP_BASE_URL')) ?>">
<div class="form-text">
<i class="bi bi-info-circle me-1"></i>
Definido em <code>.env</code> (<code>APP_BASE_URL</code>).
Edite o ficheiro para alterar — não é gerido aqui.
</div>
</div>
<div class="col-md-6">
<label class="form-label small">Logo claro (para fundos escuros — navbar)</label>
<?php if ($logo_light): ?>
<div class="mb-2 p-2 rounded" style="background:#161c18;">
<img src="<?= base_url(is_file($logo_dir . basename($logo_light)) ? 'uploads/public/logo/' . rawurlencode($logo_light) : 'assets/img/logo/' . rawurlencode($logo_light)) ?>" alt="Logo claro" style="max-height:48px;">
</div>
<?php endif; ?>
<input type="file" name="logo_light" class="form-control" accept=".png,.jpg,.jpeg,.svg,.webp">
<div class="form-text">PNG, JPG, SVG ou WebP. Máx. 2 MB.</div>
</div>
<div class="col-md-6">
<label class="form-label small">Logo escuro (para fundos claros — emails)</label>
<?php if ($logo_dark): ?>
<div class="mb-2 p-2 rounded border" style="background:#ffffff;">
<img src="<?= base_url(is_file($logo_dir . basename($logo_dark)) ? 'uploads/public/logo/' . rawurlencode($logo_dark) : 'assets/img/logo/' . rawurlencode($logo_dark)) ?>" alt="Logo escuro" style="max-height:48px;">
</div>
<?php endif; ?>
<input type="file" name="logo_dark" class="form-control" accept=".png,.jpg,.jpeg,.svg,.webp">
<div class="form-text">Usado no cabeçalho dos emails.</div>
</div>
</div>
</div>
</div>
<div class="card shadow-sm mb-3">
<div class="card-header"><strong>Email</strong></div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label small">Email remetente</label>
<input type="email" name="email_from" class="form-control" required
value="<?= htmlspecialchars($values['email_from']) ?>">
</div>
<div class="col-md-6">
<label class="form-label small">Nome do remetente</label>
<input type="text" name="email_from_name" class="form-control" required maxlength="150"
value="<?= htmlspecialchars($values['email_from_name']) ?>">
</div>
<div class="col-12">
<label class="form-label small">Prefixo do assunto</label>
<input type="text" name="email_subject_prefix" class="form-control" maxlength="100"
value="<?= htmlspecialchars($values['email_subject_prefix']) ?>">
<div class="form-text">Prefixado a todos os assuntos enviados pelo sistema.</div>
</div>
</div>
</div>
</div>
<div class="card shadow-sm mb-3">
<div class="card-header"><strong>Notificações</strong></div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label small">Horas de reminder</label>
<input type="number" name="reminder_hours" class="form-control" required min="1" max="168"
value="<?= htmlspecialchars($values['reminder_hours']) ?>">
<div class="form-text">Antes da sessão.</div>
</div>
</div>
</div>
</div>
<div class="alert alert-info d-flex align-items-center gap-2 mb-3">
<i class="bi bi-telephone-fill"></i>
<div>Telefone, WhatsApp, e-mail, morada e redes sociais do site editam-se em
<a href="<?= base_url('views/admin/info/edit.php') ?>" class="alert-link">Gestão → Contactos</a>.</div>
</div>
<div class="card shadow-sm mb-3">
<div class="card-header"><strong>SEO &amp; partilha social</strong></div>
<div class="card-body">
<p class="text-muted small mb-3">
Valores por defeito usados pelas páginas públicas quando não definem o seu próprio título ou descrição.
O nome do site (<code>app_name</code>) é usado automaticamente no <code>og:site_name</code> e no fim dos títulos.
</p>
<div class="row g-3">
<div class="col-md-8">
<label class="form-label small">Título por defeito</label>
<input type="text" name="seo_default_title" class="form-control" maxlength="150"
value="<?= htmlspecialchars($values['seo_default_title']) ?>"
placeholder="Ex: Fotografia profissional em Portugal">
<div class="form-text">Usado em <code>&lt;title&gt;</code> / <code>og:title</code> quando a página não define um.</div>
</div>
<div class="col-md-4">
<label class="form-label small">Separador do título</label>
<input type="text" name="seo_title_separator" class="form-control" maxlength="10"
value="<?= htmlspecialchars($values['seo_title_separator']) ?>"
placeholder="—">
<div class="form-text">Entre o título da página e o nome do site (ex: <code>—</code>, <code>|</code>).</div>
</div>
<div class="col-12">
<label class="form-label small">Descrição por defeito</label>
<textarea name="seo_default_description" class="form-control" rows="2" maxlength="300"
placeholder="Frase curta (até 160 caracteres) que descreve o site."><?= htmlspecialchars($values['seo_default_description']) ?></textarea>
<div class="form-text">Usado em <code>meta description</code> / <code>og:description</code>. Ideal: 120–160 caracteres.</div>
</div>
<div class="col-md-6">
<label class="form-label small">Palavras-chave (opcional)</label>
<input type="text" name="seo_keywords" class="form-control" maxlength="255"
value="<?= htmlspecialchars($values['seo_keywords']) ?>"
placeholder="fotografia, batizado, família, eventos">
</div>
<div class="col-md-3">
<label class="form-label small">Twitter handle</label>
<input type="text" name="seo_twitter_handle" class="form-control" maxlength="50"
value="<?= htmlspecialchars($values['seo_twitter_handle']) ?>"
placeholder="@andremoreirapt">
</div>
<div class="col-md-3">
<label class="form-label small">Locale</label>
<input type="text" name="seo_locale" class="form-control" maxlength="10"
value="<?= htmlspecialchars($values['seo_locale']) ?>"
placeholder="pt_PT">
<div class="form-text">Formato <code>xx_XX</code>.</div>
</div>
<div class="col-12">
<label class="form-label small">Imagem de partilha (Open Graph)</label>
<?php if ($seo_image): ?>
<div class="mb-2">
<img src="<?= base_url('assets/img/seo/' . rawurlencode($seo_image)) ?>"
alt="OG image" style="max-height:160px; max-width:100%; border-radius:4px;">
</div>
<?php endif; ?>
<input type="file" name="seo_default_image" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<div class="form-text">
PNG, JPG ou WebP. Recomendado 1200×630 px (proporção 1.91:1). Máx. 2 MB.
Mostrada quando alguém partilha uma página do site no Facebook, WhatsApp, LinkedIn, etc.
</div>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-end gap-2">
<button type="submit" class="btn btn-primary"><i class="bi bi-check2 me-1"></i>Guardar alterações</button>
</div>
</form>
</div>
<?php include __DIR__ . '/../../includes/footer.php'; ?>