feat: contactos editáveis, equipa com título/aptidões, mensalidades e reviews Google

- Contactos: nova página no backoffice (fitme3070_info) e 964 201 137 como
  telefone principal; o telefone secundário só aparece quando existe.
- Equipa: título, aptidões/funções e formações; o cartão mostra foto, nome e
  título e abre um cartão horizontal de detalhe (hover no desktop, toque no
  telemóvel) sem mexer na grelha.
- Mensalidades: cartões editáveis com um destaque, modal de inscrição que
  guarda o pedido (RGPD + estado + campos reservados para o CRM) e avisa o
  ginásio por e-mail; lista de pedidos com estados, notas e CSV.
- Reviews Google via Places API (New), filtradas por classificação mínima e
  em cache configurável; página de configuração com pesquisa do Place ID.
- Assets com ?v=<mtime>: site/assets é servido com cache imutável de 1 ano.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
andreandClaude Opus 5.5 committed 2026-09-24 14:58:57 +01:00
1 parent 8a494d59a5
commit f95c9ebc52
27 files changed
+2063 -16

No files matched your search

@@ -0,0 +1,8 @@
-- ─── Contactos: novo número principal ────────────────────────────────────────
-- 964 201 137 passa a ser o telefone principal (phone), substituindo o número
-- anterior, que sai do site. O telefone secundário (telephone) não muda.
-- A partir daqui os contactos editam-se no backoffice (Gestão → Contactos).
UPDATE `fitme3070_info`
SET `phone` = '964 201 137', `updated_at` = NOW()
WHERE `id` = 1;
@@ -0,0 +1,60 @@
-- ─── Mensalidades + pedidos de inscrição ────────────────────────────────────
-- memberships: cartões de mensalidade editáveis no backoffice. Um só pode estar
-- em destaque (garantido na aplicação). is_addon = acresce à mensalidade base
-- (mostra "+X €").
-- membership_leads: pedidos do modal "Inscrever". Guarda o consentimento RGPD
-- (texto + IP + UA), o estado de acompanhamento e, reservado para a futura
-- integração com o CRM do ginásio, crm_ref / crm_synced_at.
-- Safe to re-run.
CREATE TABLE IF NOT EXISTS `memberships` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,
`subtitle` VARCHAR(255) NOT NULL DEFAULT '',
`price` DECIMAL(8,2) DEFAULT NULL,
`is_addon` TINYINT(1) NOT NULL DEFAULT 0,
`price_suffix` VARCHAR(60) NOT NULL DEFAULT '/mês',
`features` TEXT DEFAULT NULL,
`is_featured` TINYINT(1) NOT NULL DEFAULT 0,
`featured_label` VARCHAR(40) NOT NULL DEFAULT 'Mais popular',
`cta_label` VARCHAR(40) NOT NULL DEFAULT 'Inscrever',
`item_order` INT(11) NOT NULL DEFAULT 0,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`deleted_at` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_memberships_list` (`deleted_at`, `is_active`, `item_order`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `membership_leads` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`membership_id` INT(11) DEFAULT NULL,
`membership_name` VARCHAR(100) NOT NULL DEFAULT '',
`name` VARCHAR(150) NOT NULL,
`email` VARCHAR(190) NOT NULL DEFAULT '',
`phone` VARCHAR(40) NOT NULL DEFAULT '',
`message` VARCHAR(1000) NOT NULL DEFAULT '',
`status` ENUM('novo','contactado','inscrito','descartado') NOT NULL DEFAULT 'novo',
`admin_notes` TEXT DEFAULT 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 '',
`crm_ref` VARCHAR(100) DEFAULT NULL,
`crm_synced_at` DATETIME DEFAULT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_membership_leads_status` (`status`, `created_at`),
KEY `idx_membership_leads_crm` (`crm_synced_at`),
CONSTRAINT `fk_membership_leads_membership`
FOREIGN KEY (`membership_id`) REFERENCES `memberships` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Cartão de exemplo (o preço fica por definir no backoffice).
INSERT INTO `memberships` (`name`, `subtitle`, `price`, `is_addon`, `price_suffix`, `features`, `item_order`)
SELECT 'Off-hours', 'Mensalidade para as horas baixas do ginásio.', NULL, 1, 'acresce à mensalidade',
'Acesso nas horas de menor afluência\nSala de musculação e cardio', 10
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM `memberships`);
@@ -0,0 +1,9 @@
-- ─── Equipa: título, aptidões/funções e formações ───────────────────────────
-- O cartão público mostra foto + nome + título; aptidões e formações (uma por
-- linha) aparecem no cartão de detalhe ao passar o rato / tocar.
-- Safe to re-run.
ALTER TABLE `team`
ADD COLUMN IF NOT EXISTS `title` VARCHAR(100) NOT NULL DEFAULT '' AFTER `name`,
ADD COLUMN IF NOT EXISTS `skills` TEXT DEFAULT NULL AFTER `description`,
ADD COLUMN IF NOT EXISTS `certifications` TEXT DEFAULT NULL AFTER `skills`;