359 lines
20 KiB
PHP
359 lines
20 KiB
PHP
<?php
|
||
require __DIR__ . '/../../config/db.php';
|
||
require __DIR__ . '/../../config/functions.php';
|
||
$user = require_admin();
|
||
|
||
$logo_dir = __DIR__ . '/../../assets/img/logo/';
|
||
$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],
|
||
'contact_email' => ['label' => 'Email de contacto público', 'type' => 'email', 'required' => false],
|
||
'contact_phone_display'=> ['label' => 'Telefone (texto exibido)', 'type' => 'text', 'required' => false],
|
||
'contact_phone_dial' => ['label' => 'Telefone (formato dial)', 'type' => 'text', 'required' => false],
|
||
'contact_phone_intl' => ['label' => 'Telefone (formato internacional, sem +)', 'type' => 'text', 'required' => false],
|
||
'contact_address_line1'=> ['label' => 'Morada — linha 1', 'type' => 'text', 'required' => false],
|
||
'contact_address_line2'=> ['label' => 'Morada — linha 2', 'type' => 'text', 'required' => false],
|
||
'social_facebook_url' => ['label' => 'Facebook URL', 'type' => 'url', 'required' => false],
|
||
'social_instagram_url' => ['label' => 'Instagram URL', 'type' => 'url', 'required' => false],
|
||
|
||
// 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;
|
||
}
|
||
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||
if (!in_array($ext, $allowed, true)) {
|
||
$errors[] = "Tipo de ficheiro não permitido para o 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';
|
||
include __DIR__ . '/../../includes/navbar.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('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('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="card shadow-sm mb-3">
|
||
<div class="card-header"><strong>Contactos públicos</strong></div>
|
||
<div class="card-body">
|
||
<div class="row g-3">
|
||
<div class="col-md-6">
|
||
<label class="form-label small">Email de contacto</label>
|
||
<input type="email" name="contact_email" class="form-control"
|
||
value="<?= htmlspecialchars($values['contact_email']) ?>"
|
||
placeholder="andre@andremoreira.pt">
|
||
<div class="form-text">Mostrado no site e recebe os pedidos de orçamento e contacto.</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label small">Telefone (texto exibido)</label>
|
||
<input type="text" name="contact_phone_display" class="form-control"
|
||
value="<?= htmlspecialchars($values['contact_phone_display']) ?>"
|
||
placeholder="915 435 034">
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label small">Telefone (dial / <code>tel:</code>)</label>
|
||
<input type="text" name="contact_phone_dial" class="form-control font-monospace"
|
||
value="<?= htmlspecialchars($values['contact_phone_dial']) ?>"
|
||
placeholder="+351915435034">
|
||
<div class="form-text">Formato E.164 (com <code>+</code>).</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label small">Telefone (formato WhatsApp <code>wa.me</code>)</label>
|
||
<input type="text" name="contact_phone_intl" class="form-control font-monospace"
|
||
value="<?= htmlspecialchars($values['contact_phone_intl']) ?>"
|
||
placeholder="351915435034">
|
||
<div class="form-text">Sem <code>+</code> nem espaços. Usado em <code>https://wa.me/<numero></code>.</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label small">Morada — linha 1</label>
|
||
<input type="text" name="contact_address_line1" class="form-control"
|
||
value="<?= htmlspecialchars($values['contact_address_line1']) ?>"
|
||
placeholder="Av. Cidade de Coimbra 159">
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label small">Morada — linha 2</label>
|
||
<input type="text" name="contact_address_line2" class="form-control"
|
||
value="<?= htmlspecialchars($values['contact_address_line2']) ?>"
|
||
placeholder="3070-761 Praia de Mira">
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label small">Facebook URL</label>
|
||
<input type="url" name="social_facebook_url" class="form-control"
|
||
value="<?= htmlspecialchars($values['social_facebook_url']) ?>"
|
||
placeholder="https://www.facebook.com/...">
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label small">Instagram URL</label>
|
||
<input type="url" name="social_instagram_url" class="form-control"
|
||
value="<?= htmlspecialchars($values['social_instagram_url']) ?>"
|
||
placeholder="https://www.instagram.com/...">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card shadow-sm mb-3">
|
||
<div class="card-header"><strong>SEO & 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><title></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'; ?>
|