feat: sync all plugins, skills, agents updates

New plugins: core-tools
New skills: auto-expense, ticket-triage, design, security-check,
  aiktop-tasks, daily-digest, imap-triage, index-update, mindmap,
  notebooklm, proc-creator, tasks-overview, validate-component,
  perfex-module, report, calendar-manager
New agents: design-critic, design-generator, design-lead,
  design-prompt-architect, design-researcher, compliance-auditor,
  metabase-analyst, gitea-integration-specialist
Updated: all plugin configs, knowledge datasets, existing skills

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 17:16:15 +00:00
parent f2b5171ea2
commit 9404af7ac9
184 changed files with 20865 additions and 1993 deletions

View File

@@ -0,0 +1,295 @@
---
name: jetengine
description: >
JetEngine development: Custom Post Types (CPT), Custom Content Types (CCT), Meta Boxes, Taxonomies,
Relations, Query Builder, Listing Grid, Listing Map, Dynamic Content, Dynamic Tags, Macros, Profile Builder,
REST API, and headless/programmatic automation for AI agents and n8n integration.
This skill should be used when the user asks about "jetengine", "jet engine", "custom post type jetengine",
"meta box jetengine", "relations jetengine", "query builder jetengine", "listing grid", "listing map",
"jetengine dynamic content", "jetengine macros", "profile builder jetengine", "cct jetengine",
"custom content type", "jetengine cct", "jetengine rest api", "jetengine n8n", "jetengine sql",
"jetengine automação", "jetengine headless", "wp_jet_rel_default", "wp_jet_cct", "jetengine relations",
"conteúdo dinâmico elementor jetengine", "jetengine filtros", "jetengine repeater".
author: Descomplicar® Crescimento Digital
version: 1.0.0
user_invocable: true
tags: [wordpress, elementor, crocoblock, jetengine, cpt, cct, listings, dynamic-content]
allowed-tools: Read, Write, Edit, Bash, mcp__ssh-unified__ssh_execute, mcp__notebooklm__notebook_query, mcp__dify-kb__dify_kb_retrieve_segments
category: dev
quality_score: 85
updated: "2026-02-18T00:00:00Z"
---
# /jetengine — JetEngine Development
JetEngine é o núcleo do ecossistema Crocoblock: cria CPT, CCT, Meta Boxes, Relations, Queries e Listings com conteúdo dinâmico para Elementor.
## CPT vs CCT — Escolha Crítica
| | Custom Post Type (CPT) | Custom Content Type (CCT) |
|--|----------------------|--------------------------|
| **Storage** | `wp_posts` + `wp_postmeta` | Tabela SQL própria `wp_jet_cct_{slug}` |
| **Performance** | Lenta em queries complexas | Muito rápida (colunas reais) |
| **Compatibilidade** | Total WP ecosystem | Limitada (sem taxonomies nativas) |
| **REST API** | `/wp-json/wp/v2/{slug}` | `/wp-json/jet-cct/{slug}` |
| **Automação** | `wp_insert_post()` | `CCT_Data_Handler` ou SQL directo |
| **Usar quando** | Conteúdo editorial, SEO, plugins WP | Alta performance, CRUD automatizado, dashboards |
**Regra prática:** CPT para conteúdo público/SEO; CCT para dados internos, integrações e alta escala.
## Criar CPT
```
JetEngine > Post Types > Add New
├── Slug (único, sem espaços, lowercase)
├── Labels (Singular / Plural)
├── Suporte: title, editor, thumbnail, excerpt, custom-fields
├── REST API: Activar para integração externa
├── Archive: Activar se precisar de página de listagem
└── Admin Columns: Mostrar meta fields na tabela admin
```
## Meta Boxes e Campos
### Tipos de Campos
| Tipo | Uso |
|------|-----|
| `text` | Texto simples |
| `textarea` | Texto longo |
| `wysiwyg` | Editor rich text |
| `number` | Valor numérico |
| `select` | Dropdown de opções |
| `checkbox` | Verdadeiro/falso |
| `checkbox-list` | Múltipla selecção |
| `radio` | Opção única |
| `date` | Data (timestamp) |
| `media` | Upload de ficheiro/imagem |
| `gallery` | Múltiplas imagens |
| `repeater` | Campo repetível (sub-campos) |
| `posts` | Relação simples (seleccionar posts) |
| `color-picker` | Cor HEX |
| `map` | Coordenadas geográficas |
| `html` | HTML estático (apenas display) |
### Repeater — Estrutura
```
Repeater Field "galeria_servicos":
├── sub-campo: imagem (media)
├── sub-campo: titulo (text)
└── sub-campo: descricao (textarea)
Aceder em PHP:
$items = get_post_meta($post_id, 'galeria_servicos', true);
foreach ($items as $item) { ... }
```
## Relations
### Tipos de Relação
| Tipo | Exemplo |
|------|---------|
| One-to-Many | 1 Empresa → N Colaboradores |
| Many-to-Many | N Projectos ↔ N Tags custom |
| Self-relation | Artigo relacionado com outros artigos |
| User-to-Post | Utilizador → N Reservas |
### Tabela SQL das Relações
```sql
-- wp_jet_rel_default
-- parent_object_id | child_object_id | rel_id (hash da relação)
-- Ler todos os filhos de um pai
SELECT child_object_id
FROM wp_jet_rel_default
WHERE parent_object_id = 123
AND rel_id = 'relacao_empresa_colaboradores';
-- Ler o pai a partir de um filho
SELECT parent_object_id
FROM wp_jet_rel_default
WHERE child_object_id = 456
AND rel_id = 'relacao_empresa_colaboradores';
```
**Importante:** Nunca editar esta tabela directamente via SQL para criar relações — usar a API PHP do JetEngine (ver `references/automation.md`).
## Query Builder
Substitui o `WP_Query` com interface visual. Ligar ao Listing Grid.
### Tipos de Query
| Tipo | Usa |
|------|-----|
| Posts | CPT, posts, páginas |
| Terms | Taxonomias |
| Users | Utilizadores WP |
| Custom Content Type | Tabelas CCT |
| REST API | Endpoint externo |
### Configurar Query Posts
```
JetEngine > Query Builder > Add New
├── Query Type: Posts
├── Post Type: [seleccionar CPT]
├── Order By: date / meta_value / rand / menu_order
├── Meta Query: adicionar condições de meta fields
├── Tax Query: filtrar por taxonomia
└── Author: específico ou current user
```
### Usar no Listing Grid
```
Elemento Elementor: Listing Grid
├── Listing Item: [seleccionar Listing Template]
├── Use Custom Query: YES → seleccionar Query criado acima
├── Items per page: N
└── Pagination: Activar se necessário
```
## Listing Grid e Listing Template
### Criar Listing Template
```
JetEngine > Listings > Add New
├── Template Type: Default / Popup
├── Content From: Posts / CCT / User / Term
└── Editar com Elementor → usar Dynamic Tags
```
### Dynamic Tags no Listing Template
```
Post Title → {{ post_title }}
Field de Meta → Meta Field → seleccionar nome do campo
Imagem → Featured Image → meta field media
Link → Post URL / Custom Field URL
Relação → Relation Field
```
### Listing Map
```
JetEngine > Listings > Add New > Template Type: Map
├── Configurar campo de coordenadas (tipo: map no meta box)
├── Google Maps API Key: WP Admin > JetEngine > Settings
└── Ligar ao Query Builder
```
## Dynamic Content e Macros
### Macros Comuns
```
%current_user_id% # ID do utilizador logado
%current_post_id% # ID do post actual
%queried_object_id% # ID do objecto em arquivo/single
%request_field|name% # Parâmetro URL (?name=valor)
%current_meta|field_name% # Meta field do post actual
%user_meta|field_name% # Meta field do utilizador logado
```
### Usar Macros em Widgets Elementor
```
Textos: escrever macro directamente no campo de texto
Links: usar em URL field
Queries: usar em Meta Query → Valor = macro
```
### Profile Builder
```
JetEngine > Profile Builder > Add New
├── Criar páginas de conta (Dashboard, Submissões, etc.)
├── Cada página = Elementor Template
├── Acesso: utilizadores logados, por role, por condição
└── Menu de Profile: widget JetEngine Profile Menu
```
## Dify KB — Consultar Antes de Implementar
```javascript
// Dataset Crocoblock (JetEngine incluído)
mcp__dify-kb__dify_kb_retrieve_segments({
dataset_id: "bdf85c26-1824-4021-92d1-be20501b35ac",
query: "[funcionalidade JetEngine]"
})
```
## Troubleshooting
| Problema | Causa | Solução |
|----------|-------|---------|
| Listing não mostra resultados | Query sem posts ou condição errada | Debug Query: activar `WP_DEBUG` e verificar |
| Dynamic Tag retorna vazio | Campo meta não preenchido ou nome errado | Verificar key do meta field em JetEngine > Meta Boxes |
| Relações não aparecem | rel_id errado ou sem items relacionados | Verificar nome da relação em JetEngine > Relations |
| CCT não aparece no REST | REST API não activada | JetEngine > Content Types > Edit > Enable REST API |
| Filtros não funcionam | Listing ID errado no filtro | JetEngine Listing Grid > copiar ID do widget |
## Bridge Plugin — Deploy para Integração n8n
Para integração headless com n8n ou agentes IA, fazer deploy do MU plugin incluído:
```bash
PHP="/opt/alt/php-fpm83/usr/bin/php"
WP="$PHP /usr/local/bin/wp --allow-root --path=/home/USER/public_html"
# Copiar bridge plugin para mu-plugins (via SSH)
# O ficheiro está em: assets/descomplicar-jet-bridge.php (nesta skill)
scp descomplicar-jet-bridge.php server.descomplicar.pt:/home/USER/public_html/wp-content/mu-plugins/
# Verificar que está activo (MU plugins carregam automaticamente)
$WP plugin list --skip-plugins --skip-themes # MU plugins não aparecem aqui — normal
```
**Endpoint exposto:** `POST /wp-json/descomplicar/v1/manage`
**Acções disponíveis:**
| Acção | Descrição |
|-------|-----------|
| `create_cct_item` | Criar item num CCT |
| `update_cct_item` | Actualizar item (`_ID` em item_data) |
| `get_cct_item` | Ler item por ID |
| `delete_cct_item` | Eliminar item |
| `manage_relation` | Criar/remover relação (connect: true/false) |
```bash
# Testar endpoint (exemplo: criar fatura)
curl -X POST https://site.pt/wp-json/descomplicar/v1/manage \
-H "Authorization: Basic $(echo -n 'user:app-password' | base64)" \
-H "Content-Type: application/json" \
-d '{
"action": "create_cct_item",
"cct_slug": "faturas",
"item_data": { "titulo": "Fatura #001", "valor": 500 }
}'
```
## JetEngine MCP Server (v3.8.0+)
A partir da versão 3.8.0, o JetEngine tem **MCP Server nativo**:
```
WP Admin > JetEngine > AI Command Center > Enable MCP Server
→ Agentes IA podem inspecionar e criar CPTs/CCTs/Relations via linguagem natural
→ Claude consegue ler o schema completo e instanciar estruturas directamente
```
## Referências Adicionais
- **`assets/descomplicar-jet-bridge.php`** — MU Plugin pronto a deployar: CRUD CCT + Relations via REST API
- **`references/automation.md`** — BD completa (CCT, Relations meta, Custom Meta Storage), WP-CLI batch, PHP hooks, REST API avançada (store_items_type, filtros CCT), cache e guardrails
- **`references/query-listings.md`** — Query Builder avançado, Listing Map, paginação, ordenação dinâmica
- **`references/dynamic-content.md`** — Dynamic Tags, Profile Builder, Macros, Conditional Fields
---
**Versão**: 1.1.0 | **Autor**: Descomplicar® | **Data**: 18-02-2026

View File

@@ -0,0 +1,226 @@
<?php
/**
* Plugin Name: Descomplicar JetEngine Bridge
* Description: API Endpoint customizado para manipulação atómica de CCTs e Relações JetEngine.
* Expõe /descomplicar/v1/manage para integração com n8n e agentes IA.
* Version: 1.1.0
* Author: Descomplicar® Crescimento Digital
* Author URI: https://descomplicar.pt
*
* Instalar em: /wp-content/mu-plugins/descomplicar-jet-bridge.php
* Autenticação: WordPress Application Passwords
*/
if ( ! defined( 'ABSPATH' ) ) exit;
class Descomplicar_Jet_Bridge {
public function __construct() {
add_action( 'rest_api_init', [ $this, 'register_routes' ] );
}
public function register_routes() {
register_rest_route( 'descomplicar/v1', '/manage', [
'methods' => 'POST',
'callback' => [ $this, 'handle_request' ],
'permission_callback' => function() {
// Autenticar via WordPress Application Passwords no n8n:
// Header: Authorization: Basic base64(user:app-password)
return current_user_can( 'edit_posts' );
},
]);
}
public function handle_request( WP_REST_Request $request ) {
$params = $request->get_json_params();
$action = sanitize_text_field( $params['action'] ?? '' );
if ( ! function_exists( 'jet_engine' ) || ! class_exists( 'Jet_Engine' ) ) {
return new WP_Error( 'no_jet_engine', 'JetEngine não está activo.', [ 'status' => 500 ] );
}
switch ( $action ) {
case 'create_cct_item':
case 'update_cct_item':
return $this->handle_cct_write( $params );
case 'get_cct_item':
return $this->handle_cct_get( $params );
case 'delete_cct_item':
return $this->handle_cct_delete( $params );
case 'manage_relation':
return $this->handle_relation( $params );
default:
return new WP_Error(
'invalid_action',
'Acção inválida. Disponíveis: create_cct_item, update_cct_item, get_cct_item, delete_cct_item, manage_relation',
[ 'status' => 400 ]
);
}
}
/**
* Criar ou actualizar item CCT
*
* Payload: {
* "action": "create_cct_item",
* "cct_slug": "faturas",
* "item_data": {
* "titulo": "Fatura #1024",
* "valor": 500,
* "estado": "pendente"
* }
* }
* Para update, incluir "_ID": 42 em item_data.
*/
private function handle_cct_write( array $params ) {
$cct_slug = sanitize_key( $params['cct_slug'] ?? '' );
$item_data = $params['item_data'] ?? [];
if ( empty( $cct_slug ) ) {
return new WP_Error( 'missing_slug', 'cct_slug é obrigatório.', [ 'status' => 400 ] );
}
if ( empty( $item_data ) ) {
return new WP_Error( 'missing_data', 'item_data é obrigatório.', [ 'status' => 400 ] );
}
$handler = $this->get_cct_handler( $cct_slug );
if ( is_wp_error( $handler ) ) return $handler;
$result_id = $handler->update_item( $item_data );
if ( ! $result_id ) {
return new WP_Error( 'db_error', 'Falha ao guardar item na base de dados.', [ 'status' => 500 ] );
}
return rest_ensure_response( [
'success' => true,
'item_id' => $result_id,
'action' => isset( $item_data['_ID'] ) ? 'updated' : 'created',
] );
}
/**
* Ler item CCT por ID
*
* Payload: { "action": "get_cct_item", "cct_slug": "faturas", "item_id": 42 }
*/
private function handle_cct_get( array $params ) {
$cct_slug = sanitize_key( $params['cct_slug'] ?? '' );
$item_id = absint( $params['item_id'] ?? 0 );
if ( empty( $cct_slug ) || ! $item_id ) {
return new WP_Error( 'missing_data', 'cct_slug e item_id são obrigatórios.', [ 'status' => 400 ] );
}
$handler = $this->get_cct_handler( $cct_slug );
if ( is_wp_error( $handler ) ) return $handler;
$item = $handler->get_item( $item_id );
if ( ! $item ) {
return new WP_Error( 'not_found', "Item {$item_id} não encontrado no CCT '{$cct_slug}'.", [ 'status' => 404 ] );
}
return rest_ensure_response( [
'success' => true,
'item' => $item,
] );
}
/**
* Eliminar item CCT
*
* Payload: { "action": "delete_cct_item", "cct_slug": "faturas", "item_id": 42 }
*/
private function handle_cct_delete( array $params ) {
$cct_slug = sanitize_key( $params['cct_slug'] ?? '' );
$item_id = absint( $params['item_id'] ?? 0 );
if ( empty( $cct_slug ) || ! $item_id ) {
return new WP_Error( 'missing_data', 'cct_slug e item_id são obrigatórios.', [ 'status' => 400 ] );
}
$handler = $this->get_cct_handler( $cct_slug );
if ( is_wp_error( $handler ) ) return $handler;
$result = $handler->delete_item( $item_id );
return rest_ensure_response( [
'success' => (bool) $result,
'item_id' => $item_id,
'deleted' => (bool) $result,
] );
}
/**
* Criar ou remover relação entre dois items
*
* Payload: {
* "action": "manage_relation",
* "rel_key": "clientes-projetos",
* "parent_id": 10,
* "child_id": 20,
* "connect": true
* }
*/
private function handle_relation( array $params ) {
$rel_key = sanitize_text_field( $params['rel_key'] ?? '' );
$parent_id = absint( $params['parent_id'] ?? 0 );
$child_id = absint( $params['child_id'] ?? 0 );
$connect = (bool) ( $params['connect'] ?? true );
if ( empty( $rel_key ) || ! $parent_id || ! $child_id ) {
return new WP_Error(
'missing_data',
'rel_key, parent_id e child_id são obrigatórios.',
[ 'status' => 400 ]
);
}
if ( ! class_exists( '\Jet_Engine\Relations\Manager' ) ) {
return new WP_Error( 'no_relations', 'Módulo de Relações JetEngine não disponível.', [ 'status' => 500 ] );
}
\Jet_Engine\Relations\Manager::get_instance()->process_relation(
$rel_key,
$parent_id,
$child_id,
$connect
);
return rest_ensure_response( [
'success' => true,
'action' => $connect ? 'connected' : 'disconnected',
'rel_key' => $rel_key,
'parent' => $parent_id,
'child' => $child_id,
] );
}
/**
* Obter CCT handler com verificação de erro
*/
private function get_cct_handler( string $cct_slug ) {
$cct_module = jet_engine()->modules->get_module( 'custom-content-types' );
if ( ! $cct_module ) {
return new WP_Error( 'no_cct_module', 'Módulo CCT não está activo no JetEngine.', [ 'status' => 500 ] );
}
$manager = $cct_module->instance->manager;
$type = $manager->get_content_type_instance( $cct_slug );
if ( ! $type ) {
return new WP_Error( 'invalid_cct', "CCT '{$cct_slug}' não encontrado.", [ 'status' => 404 ] );
}
return $type->get_handler_instance();
}
}
new Descomplicar_Jet_Bridge();

View File

@@ -0,0 +1,668 @@
# JetEngine — Automação Headless e Integração com Agentes IA
Arquitectura técnica para manipular o JetEngine sem wp-admin: via PHP interno, REST API, SQL e n8n.
---
## 1. Estrutura de Dados — Base de Dados
### A. Custom Post Types (CPT) — Estrutura Standard WP
```sql
-- Dados em wp_posts
SELECT * FROM wp_posts WHERE post_type = 'meu_cpt' AND post_status = 'publish';
-- Meta fields em wp_postmeta
SELECT meta_key, meta_value FROM wp_postmeta WHERE post_id = 123;
```
**Limitação:** Queries complexas com múltiplos meta fields são lentas (JOIN por cada meta key).
### B. Custom Content Types (CCT) — Tabelas SQL Próprias
Cada CCT tem tabela dedicada: `wp_jet_cct_{slug_do_cct}`
```sql
-- Exemplo: CCT "faturas" → tabela wp_jet_cct_faturas
-- Colunas são os campos definidos no JetEngine
-- Ler todos os itens activos
SELECT * FROM wp_jet_cct_faturas WHERE cct_status = 'active';
-- Filtrar por campo
SELECT * FROM wp_jet_cct_clientes
WHERE cidade = 'Lisboa'
AND cct_status = 'publish'
ORDER BY _created_at DESC
LIMIT 20;
-- Campos especiais em todos os CCT:
-- _ID → ID numérico único
-- cct_status → publish / draft
-- cct_author_id → ID do utilizador criador
-- _created_at → Unix timestamp de criação
-- _modified → Unix timestamp de modificação
```
**Vantagem:** SQL directo é seguro e extremamente rápido. Ideal para leitura em dashboards e n8n.
### C. Relations — Tabelas wp_jet_rel_default e _meta
O JetEngine NÃO guarda relações em `wp_postmeta`. Tem tabela própria.
```sql
-- wp_jet_rel_default: relações básicas
-- parent_object_id | child_object_id | rel_id (hash/slug da relação)
-- Ler filhos de um pai
SELECT child_object_id
FROM wp_jet_rel_default
WHERE parent_object_id = 123
AND rel_id = 'relacao_clientes_projetos';
-- Ler pai a partir de filho
SELECT parent_object_id
FROM wp_jet_rel_default
WHERE child_object_id = 456
AND rel_id = 'relacao_clientes_projetos';
-- Contar relações
SELECT parent_object_id, COUNT(*) as total
FROM wp_jet_rel_default
WHERE rel_id = 'relacao_empresa_colaboradores'
GROUP BY parent_object_id;
```
**wp_jet_rel_default_meta** — metadados específicos de uma relação (ex: papel do actor num filme):
```sql
-- Exemplo: condição de uso de um veículo numa relação utilizador↔veículo
SELECT meta_key, meta_value
FROM wp_jet_rel_default_meta
WHERE rel_id = 8
AND parent_object_id = 45
AND child_object_id = 1020;
-- UPSERT massivo para sincronizações ETL (com integridade referencial)
START TRANSACTION;
INSERT INTO wp_jet_rel_default (rel_id, parent_object_id, child_object_id)
VALUES (8, 45, 1020), (8, 45, 1021), (8, 92, 1055)
ON DUPLICATE KEY UPDATE parent_object_id = VALUES(parent_object_id);
INSERT INTO wp_jet_rel_default_meta (rel_id, parent_object_id, child_object_id, meta_key, meta_value)
VALUES (8, 45, 1020, 'condicao_uso', 'Alugado')
ON DUPLICATE KEY UPDATE meta_value = VALUES(meta_value);
COMMIT;
-- OBRIGATÓRIO após SQL directo: chamar wp_cache_flush() + jet_smart_filters()->indexer->index_all()
```
**Nota:** Em relações críticas com muito tráfego, o JetEngine pode isolar por relação em tabela própria (`wp_jet_rel_81_meta`), reduzindo table locks.
**Guardrail:** NUNCA inserir directamente via SQL para criar relações em ambientes activos. Usar API PHP (ver secção 2). SQL directo só para sincronizações ETL massivas (centenas de milhares de linhas).
### D. Custom Meta Storage para CPT (Tabela Dedicada)
Alternativa ao `wp_postmeta` para CPTs com muitos campos e consultas complexas.
```
JetEngine > Post Types > Edit > Custom Meta Storage: Activar
→ Cria tabela dedicada: wp_{cpt_slug}_meta
(ex: wp_imoveis_meta com colunas tipadas por campo)
```
```sql
-- Com Custom Meta Storage activo, cada campo é uma coluna
SELECT preco, area_m2, tipologia, localizacao
FROM wp_imoveis_meta
WHERE preco BETWEEN 150000 AND 350000
AND tipologia IN ('T2', 'T3')
ORDER BY preco ASC;
-- Joins com wp_posts permanecem necessários para title/date
SELECT p.ID, p.post_title, m.preco, m.area_m2
FROM wp_posts p
JOIN wp_imoveis_meta m ON m.post_id = p.ID
WHERE p.post_status = 'publish'
AND m.preco < 300000;
```
**Quando usar:** CPTs com >10 campos numéricos/date, queries com múltiplos filtros simultâneos, alternativa a CCT quando se precisa de taxonomias e comentários nativos WP.
### E. Estrutura na wp_options
```sql
-- Configurações de CPTs
SELECT option_value FROM wp_options WHERE option_name = 'jet_engine_cpt';
-- Taxonomias
SELECT option_value FROM wp_options WHERE option_name = 'jet_engine_tax';
-- Relações
SELECT option_value FROM wp_options WHERE option_name = 'jet_engine_relations';
-- Meta Boxes
SELECT option_value FROM wp_options WHERE option_name = 'jet_engine_meta_boxes';
```
**Aviso:** São PHP serialized arrays. NUNCA editar via SQL string replace — vai corromper. Usar sempre `unserialize → edit → serialize` em PHP.
---
## 2. PHP — API Interna JetEngine
### Criar/Actualizar Item CCT
```php
<?php
// Usar CCT Data Handler (mantém hooks, validações e cache)
// Mais seguro que SQL directo para escrita
$cct_slug = 'faturas';
$item_data = [
'titulo' => 'Fatura #1024',
'valor' => 500.00,
'status' => 'pendente',
'cliente' => 'Empresa X Lda',
// Para update, incluir _ID existente:
// '_ID' => 15
];
// Aceder ao módulo CCT
if (function_exists('jet_engine')) {
$cct_module = jet_engine()->modules->get_module('custom-content-types');
if ($cct_module) {
$manager = $cct_module->instance->manager;
$type = $manager->get_content_type_instance($cct_slug);
if ($type) {
$result = $type->get_handler_instance()->update_item($item_data);
// $result = ID do item criado, ou false em caso de erro
}
}
}
```
### Gerir Relações Programaticamente
```php
<?php
// Método 1: process_relation (simples, para connect/disconnect)
// IMPORTANTE: Executar após o hook 'init' com prioridade > 12
if (class_exists('\Jet_Engine\Relations\Manager')) {
$relations_manager = \Jet_Engine\Relations\Manager::get_instance();
$relations_manager->process_relation(
$rel_id, // ex: 'relacao_empresa_colaboradores'
$parent_id,
$child_id,
true // true = conectar, false = desconectar
);
}
// Método 2: API completa (update com contexto — mais robusto)
function ia_conectar_com_contexto($cliente_id, $documento_id) {
$rel_id_numerico = 15; // ID numérico da relação (ver JetEngine > Relations)
$relation = jet_engine()->relations->get_active_relations($rel_id_numerico);
if ($relation) {
$relation->set_update_context('parent'); // perspectiva: 'parent' ou 'child'
$relation->update($cliente_id, $documento_id);
// Limpar cache do Query Builder após actualizar relações
\Jet_Engine\Query_Builder\Manager::instance()->listings_cache->clear();
}
}
```
### Criar/Actualizar CPT Post
```php
<?php
// CPTs usam funções standard do WordPress
$post_data = [
'post_title' => 'Novo Projecto',
'post_content' => '',
'post_status' => 'publish',
'post_type' => 'projecto',
];
$post_id = wp_insert_post($post_data);
if (!is_wp_error($post_id)) {
update_post_meta($post_id, 'cliente_id', 45);
update_post_meta($post_id, 'valor_contrato', 5000);
update_post_meta($post_id, 'data_inicio', '2026-03-01');
}
```
### Manipular Estrutura (Onboarding Programático)
```php
<?php
// Adicionar novo CPT à configuração do JetEngine
// CUIDADO: substitui configurações existentes se não fizer merge
$current_cpts = get_option('jet_engine_cpt', []);
$new_cpt = [
'slug' => 'imovel',
'singular_name' => 'Imóvel',
'name' => 'Imóveis',
'show_in_rest' => true,
'supports' => ['title', 'editor', 'thumbnail', 'custom-fields'],
// ... outras configurações
];
$current_cpts[] = $new_cpt;
update_option('jet_engine_cpt', $current_cpts);
// Obrigatório: regenerar permalinks após alterar CPTs
flush_rewrite_rules();
```
### Registar Meta Box Programaticamente (Hook)
```php
<?php
// Hook: jet-engine/meta-boxes/register-instances
// Ideal para plugins de agência (controlo por código, imutável pelo cliente)
add_action('jet-engine/meta-boxes/register-instances', function($meta_manager) {
$meta_fields = [
[
'title' => 'ID Remoto de Sincronização',
'name' => '_ia_sync_id',
'object_type' => 'field',
'type' => 'text',
'width' => '50%',
],
[
'title' => 'Score de Auditoria',
'name' => '_score_auditoria',
'object_type' => 'field',
'type' => 'number',
'width' => '50%',
],
];
$post_type = 'documento_legal';
if (!class_exists('Jet_Engine_CPT_Meta')) {
require $meta_manager->component_path('post.php');
}
$meta_manager->store_fields($post_type, $meta_fields, 'post_type');
new Jet_Engine_CPT_Meta($post_type, $meta_fields, 'Controlo IA', 'normal', 'high', [
'object_type' => 'post',
'allowed_post_type' => [$post_type],
'name' => 'Controlo de Metadados IA',
]);
});
```
### WP-CLI Batch Import para CCT
```php
<?php
// Ficheiro: import_cct_data.php
// Executar: wp eval-file import_cct_data.php
if (!class_exists('\Jet_Engine\Modules\Custom_Content_Types\Module')) {
WP_CLI::error('Módulo CCT não está activo.');
}
$cct_slug = 'veiculos';
$manager = \Jet_Engine\Modules\Custom_Content_Types\Module::instance()->manager;
$content_type = $manager->get_content_types($cct_slug);
if (!$content_type) {
WP_CLI::error("CCT '{$cct_slug}' não encontrado.");
}
$registos = json_decode(file_get_contents('/tmp/veiculos.json'), true);
$sucesso = 0;
foreach ($registos as $dados) {
// db->insert() respeita sanitizações e integridade do CCT
if ($content_type->db->insert($dados)) {
$sucesso++;
}
// Libertar memória em loops grandes
if ($sucesso % 500 === 0) {
wp_cache_flush();
}
}
wp_cache_flush();
WP_CLI::success("Importação concluída. Registos criados: {$sucesso}");
```
```bash
# Execução no servidor CWP:
PHP="/opt/alt/php-fpm83/usr/bin/php"
WP="$PHP /usr/local/bin/wp --allow-root --path=/home/USER/public_html"
$WP eval-file /tmp/import_cct_data.php
```
---
## 3. REST API JetEngine
### Endpoints
| Tipo | Endpoint | Método |
|------|----------|--------|
| CPT | `/wp-json/wp/v2/{cpt_slug}` | GET / POST / PUT / DELETE |
| CCT | `/wp-json/jet-cct/{cct_slug}` | GET / POST / PUT / DELETE |
| Relação | `/wp-json/jet-rel/{relation_id}` | GET / POST |
**Pré-requisito:** Activar REST API nas configurações do CPT/CCT/Relação em JetEngine.
### Autenticação
```bash
# Application Passwords (WP 5.6+) — recomendado para n8n
# Criar em: WP Admin > Users > Edit > Application Passwords
# Header de autenticação
Authorization: Basic $(echo -n 'username:app-password' | base64)
```
### Criar/Ler Item CCT via REST
```bash
# Criar registo no CCT "clientes"
curl -X POST https://site.pt/wp-json/jet-cct/clientes \
-H "Authorization: Basic dXNlcjpwYXNz..." \
-H "Content-Type: application/json" \
-d '{
"nome_cliente": "Empresa X Lda",
"email": "contacto@empresax.com",
"nif": "123456789",
"cct_status": "publish"
}'
# Resposta: { "_ID": 42, ... }
# Pesquisa com filtros — parâmetros CCT REST API
# _limit, _offset: paginação
# _cct_search: pesquisa de texto
# _cct_search_by: campo específico
curl "https://site.pt/wp-json/jet-cct/clientes?_limit=20&_offset=0&_cct_search=Lisboa&_cct_search_by=cidade"
# Header de resposta inclui: Jet-Query-Total (total de registos para paginação)
```
### Actualizar Relação via REST API
```bash
# POST para /wp-json/jet-rel/{relation_id}
# store_items_type: "update" (aditivo) | "replace" (destrutivo!) | "disconnect" (remover)
curl -X POST https://site.pt/wp-json/jet-rel/22 \
-H "Authorization: Basic dXNlcjpwYXNz..." \
-H "Content-Type: application/json" \
-d '{
"parent_id": 4500,
"child_id": 1020,
"context": "child",
"store_items_type": "update",
"meta": {
"etapa_validacao": "Aprovado",
"score_auditoria": 88.5,
"data_processamento": "2026-02-18T08:30"
}
}'
```
**Atenção `store_items_type`:**
- `update` — adiciona à relação existente (safe)
- `replace`**DESTRÓI todas as relações existentes** e substitui (perigoso!)
- `disconnect` — remove a relação específica
### Criar Post (CPT) via REST
```bash
curl -X POST https://site.pt/wp-json/wp/v2/projecto \
-H "Authorization: Basic dXNlcjpwYXNz..." \
-H "Content-Type: application/json" \
-d '{
"title": "Projecto Website ABC",
"status": "publish",
"meta": {
"cliente_id": 45,
"valor_contrato": 5000
}
}'
```
### Campos Repeater via REST
```json
{
"galeria_servicos": [
{ "titulo": "Serviço A", "descricao": "Descrição A" },
{ "titulo": "Serviço B", "descricao": "Descrição B" }
]
}
```
---
## 4. Integração n8n
### Leitura (MySQL Node — mais rápido)
```javascript
// n8n MySQL Node — ler CCT directamente da BD
// Muito mais rápido que REST API para leitura em massa
// Query example:
SELECT
_ID,
nome_cliente,
email,
cidade,
cct_status,
_created_at
FROM wp_jet_cct_clientes
WHERE cct_status = 'publish'
ORDER BY _created_at DESC
LIMIT 100;
```
**Vantagem:** 100x mais rápido que HTTP Request ao WP REST API.
### Escrita (HTTP Request Node — via REST)
```javascript
// n8n HTTP Request Node
// Method: POST
// URL: https://site.pt/wp-json/jet-cct/faturas
// Authentication: Basic Auth
// Body (JSON):
{
"numero": "{{ $json.fatura_numero }}",
"cliente_id": "{{ $json.cliente_id }}",
"valor": "{{ $json.total }}",
"cct_status": "publish"
}
```
**Usar REST para escrita** (garante hooks JetEngine e validações).
### Webhooks via JetFormBuilder
```
JetFormBuilder pode disparar Webhook para n8n:
1. Criar formulário no JetFormBuilder
2. Post-submit Action: Webhook
3. URL: https://n8n.descomplicar.pt/webhook/jetengine
4. Formulários silenciosos: receber POST externos que passam pelas validações JetEngine
```
---
## 5. Tabela de Métodos
| Método | Complexidade | Performance | Risco | Caso de Uso |
|--------|-------------|-------------|-------|-------------|
| REST API | Baixa | Média | Baixo | Inserir dados externos via n8n |
| PHP (Classes Internas) | Média | Alta | Baixo | Plugins custom, scripts de migração |
| SQL Directo (CCT) | Alta | Muito Alta | Médio | Bulk updates, leitura analytics |
| SQL Directo (Meta) | Alta | Baixa | **Alto** | **Evitar** — corrupção de arrays |
| WP-CLI Custom | Média | Alta | Baixo | Manutenção, cron jobs |
---
## 6. Guardrails Críticos
### Cache JetEngine
```bash
# Após alterar estruturas via PHP/SQL na wp_options,
# forçar regeneração de cache:
PHP="/opt/alt/php-fpm83/usr/bin/php"
WP="$PHP /usr/local/bin/wp --allow-root --path=/home/USER/public_html"
$WP cache flush
$WP rewrite flush
$WP elementor flush-css --regenerate
```
**Cache do Query Builder** — obrigatório após alterações SQL directas:
```php
<?php
// Limpar cache de listagens após SQL bulk update
\Jet_Engine\Query_Builder\Manager::instance()->listings_cache->clear();
// Recalcular Indexer do JetSmartFilters (se filtros activos)
if (function_exists('jet_smart_filters')) {
jet_smart_filters()->indexer->index_all();
}
// Cache do WordPress
wp_cache_flush();
```
**Regra crítica:** Alterações SQL directas em `wp_jet_cct_*` ou `wp_jet_rel_default` NÃO invalidam automaticamente o Object Cache nem os transients do Query Builder. O site apresentará dados obsoletos até ao flush.
### NUNCA Editar Campos Serializados via SQL String Replace
```sql
-- ERRADO — corrompe o array PHP serializado
UPDATE wp_options
SET option_value = REPLACE(option_value, 'Imóveis', 'Propriedades')
WHERE option_name = 'jet_engine_cpt';
-- CORRECTO — usar PHP
$cpts = get_option('jet_engine_cpt');
// Fazer alterações via PHP
update_option('jet_engine_cpt', $cpts);
```
### IDs de Relações em Migrações
```
Problema: Ao migrar dados entre ambientes, IDs podem mudar → relações quebram
Solução: Usar Slugs como referência no código; converter para ID antes de gravar
Usar: get_posts(['name' => 'slug-do-post', 'post_type' => 'meu_cpt'])
```
### Repeater Fields via SQL
```
Repeater fields são PHP serialize() ou JSON dependendo da configuração.
NUNCA fazer string replace em SQL.
Usar sempre: unserialize() → editar array → serialize() → update_post_meta()
```
---
## 7. MU-Plugin: Wrapper de Endpoints Custom
Para expor endpoints seguros ao n8n sem passar pela REST API standard:
```php
<?php
/**
* Plugin Name: JetEngine AI Agent Endpoints
* Description: Endpoints seguros para criação de CCTs e Relações via n8n
* Must Use: sim — colocar em /wp-content/mu-plugins/
*/
add_action('rest_api_init', function() {
// Endpoint para criar item CCT
register_rest_route('jet-agent/v1', '/cct/(?P<slug>[a-z_]+)', [
'methods' => 'POST',
'callback' => 'jet_agent_create_cct_item',
'permission_callback' => function() {
return current_user_can('edit_posts');
},
'args' => [
'slug' => ['required' => true, 'sanitize_callback' => 'sanitize_key'],
]
]);
// Endpoint para gerir relações
register_rest_route('jet-agent/v1', '/relation', [
'methods' => 'POST',
'callback' => 'jet_agent_manage_relation',
'permission_callback' => function() {
return current_user_can('edit_posts');
}
]);
});
function jet_agent_create_cct_item(WP_REST_Request $request) {
$slug = $request->get_param('slug');
$data = $request->get_json_params();
// Sanitizar dados
$data = array_map('sanitize_text_field', $data);
// Usar handler interno do JetEngine
if (!function_exists('jet_engine')) {
return new WP_Error('jet_engine_missing', 'JetEngine não activo', ['status' => 500]);
}
$cct_module = jet_engine()->modules->get_module('custom-content-types');
$type = $cct_module->instance->manager->get_content_type_instance($slug);
if (!$type) {
return new WP_Error('cct_not_found', "CCT '{$slug}' não encontrado", ['status' => 404]);
}
$result = $type->get_handler_instance()->update_item($data);
return rest_ensure_response([
'success' => (bool)$result,
'id' => $result,
]);
}
function jet_agent_manage_relation(WP_REST_Request $request) {
$params = $request->get_json_params();
$rel_id = sanitize_text_field($params['rel_id'] ?? '');
$parent_id = absint($params['parent_id'] ?? 0);
$child_id = absint($params['child_id'] ?? 0);
$connect = (bool)($params['connect'] ?? true);
if (!$rel_id || !$parent_id || !$child_id) {
return new WP_Error('missing_params', 'rel_id, parent_id e child_id são obrigatórios', ['status' => 400]);
}
if (class_exists('\Jet_Engine\Relations\Manager')) {
\Jet_Engine\Relations\Manager::get_instance()->process_relation(
$rel_id, $parent_id, $child_id, $connect
);
return rest_ensure_response(['success' => true]);
}
return new WP_Error('relations_missing', 'Módulo Relations não disponível', ['status' => 500]);
}
```
---
*JetEngine Automation | Descomplicar® | 18-02-2026*

View File

@@ -0,0 +1,271 @@
# JetEngine — Dynamic Content, Macros e Profile Builder
## Dynamic Tags no Elementor
### Categorias de Dynamic Tags JetEngine
| Categoria | Tags Disponíveis |
|-----------|-----------------|
| **Post** | Post Title, Post URL, Post Date, Post Author, Featured Image, Post Terms |
| **Meta** | Meta Field (qualquer campo JetEngine) |
| **User** | User Name, User Email, User Role, User Meta |
| **Relations** | Relation Field (conteúdo relacionado) |
| **Options** | Option Field (JetEngine Global Options Pages) |
| **Macros** | JetEngine Macros (ver secção abaixo) |
| **Query** | Query Variable (parâmetros URL) |
### Usar Meta Field Tag
```
Elemento Elementor > Campo de texto > ícone Dynamic Tags
→ JetEngine > Meta Field
→ Seleccionar meta key name
→ Field Type: auto / text / image / url / ...
Para imagens (campo tipo media):
→ Field Type: Image
→ Image Size: thumbnail / medium / large / full
Para URLs em botões:
→ Link > Dynamic Tag > Meta Field > Field Type: URL
```
### Tags em CSS Custom (background, cores)
```
Elemento > Style > Background > Dynamic Tag
→ Meta Field → campo cor HEX
→ Útil para cards coloridos por categoria
```
---
## Macros
### Macros Built-in
```
%current_user_id% # ID do utilizador logado
%current_user_name% # Nome do utilizador
%current_user_email% # Email do utilizador
%current_post_id% # ID do post actual
%current_post_type% # Post type do post actual
%queried_object_id% # ID em arquivo/single
%queried_object_slug% # Slug em arquivo/single
%request_field|param_name% # Parâmetro URL: ?param_name=valor
%current_meta|field_name% # Meta field do post actual
%user_meta|field_name% # Meta field do utilizador logado
%current_taxonomy|tax_name% # Taxonomy do post actual
%current_timestamp% # Unix timestamp actual
%site_url% # URL do site
```
### Usar Macros em Query Builder
```
Exemplo: Listar imóveis favoritos do utilizador
Meta Query:
├── Field: favorito_de (campo que guarda array de IDs de utilizadores)
├── Operator: LIKE
└── Value: %current_user_id%
Exemplo: Listar items criados pelo utilizador
Meta Query:
├── Field: cct_author_id (campo automático do CCT)
├── Operator: =
└── Value: %current_user_id%
```
### Macros em Textos
```
Texto: "Bem-vindo, %current_user_name%!"
→ Em runtime: "Bem-vindo, João Silva!"
Em Heading Widget: activar Dynamic Tag > JetEngine Macros
→ Escrever a macro no campo Macro Value
```
### Criar Macro Custom (PHP)
```php
<?php
// Registar macro custom
add_filter('jet-engine/listings/macros-list', function($macros) {
$macros[] = [
'id' => 'minha_macro',
'name' => 'Minha Macro Custom',
'args' => [], // parâmetros opcionais
];
return $macros;
});
add_filter('jet-engine/listings/macros-result', function($result, $macro, $args) {
if ($macro === 'minha_macro') {
// Lógica da macro
$result = 'valor calculado';
}
return $result;
}, 10, 3);
```
---
## Conditional Fields (Campos Condicionais)
### Em Meta Boxes
```
Campo B só aparece quando Campo A = "sim"
Meta Box > Campo B > Conditions:
├── Field: nome_campo_a
├── Operator: =
└── Value: sim
```
### Em Listing Templates (Visibility)
```
Elemento Elementor > Advanced > JetEngine Conditions:
├── Condition: Meta Field | Equals | "featured"
└── Effect: Show / Hide
```
---
## Global Options Pages
Para configurações globais do site (telefone, email, horários, redes sociais).
### Criar Options Page
```
JetEngine > Options Pages > Add New
├── Page Title: Informações Globais
├── Slug: informacoes-globais
└── Campos: Meta Box associado
```
### Usar em Elementor
```
Dynamic Tag > JetEngine > Option
→ Seleccionar Options Page
→ Seleccionar campo
Exemplo: footer com telefone dinâmico
→ Options Page: Informações Globais > campo: telefone
→ Editar telefone em 1 lugar, actualiza em todo o site
```
---
## Profile Builder
Sistema de áreas de conta para utilizadores logados.
### Estrutura
```
JetEngine > Profile Builder
├── Pages (áreas da conta):
│ ├── Dashboard (página principal)
│ ├── Submissões (posts do utilizador)
│ ├── Favoritos
│ ├── Configurações (editar perfil)
│ └── [custom pages]
├── Access Rules:
│ ├── Login Required: YES
│ ├── User Roles: subscriber / author / ...
│ └── Redirect não-logados: página de login
└── Profile Menu Widget (Elementor)
```
### Criar Página de Perfil
```
1. JetEngine > Profile Builder > Add Page
Nome: Dashboard | Slug: dashboard
Template: Criar com Elementor
2. No Template, usar Dynamic Tags:
→ User Name, User Email, User Avatar
→ Query Builder filtrado por %current_user_id%
3. Adicionar Profile Menu Widget ao header/sidebar
→ Mostra links para cada página do perfil
→ Altera links consoante se está logado ou não
```
### Submissão de Conteúdo por Utilizadores
```
JetFormBuilder + Profile Builder:
1. Criar formulário com acção "Create Post" (CPT)
2. Acção: Set Author = Current User
3. Colocar formulário na página de perfil
4. Query Builder na página de listagem: autor = current_user_id
```
### Editar Perfil com JetFormBuilder
```php
// JetFormBuilder Form:
// Acção: Update User
// Campos mapeados:
// ├── first_name → user first_name
// ├── last_name → user last_name
// ├── email → user email
// └── bio_custom → user_meta: bio_custom
// Pre-fill automático: activar "Use data from:" → Current User
```
---
## Conditional Content por Utilizador
### Mostrar Conteúdo Só a Logados
```
Elementor > Advanced > JetEngine Conditions:
Condition: Is User Logged In | Equals | True
Effect: Show (esconde elemento para não logados)
```
### Por Role
```
Condition: User Role | Contains | administrator
Effect: Show (só admins vêem este bloco)
```
### Por Meta do Utilizador
```
Condition: User Meta | Field: plano_subscricao | Equals | premium
Effect: Show (só utilizadores premium vêem este conteúdo)
```
---
## Repeater em Frontend
### Listar Items de um Repeater
```
Meta Box com campo Repeater "galeria_destaques":
├── Sub-campo: imagem (media)
├── Sub-campo: titulo (text)
└── Sub-campo: link (url)
Em Listing Template:
Adicionar widget "Dynamic Repeater"
→ Source: Meta Field → galeria_destaques
→ Criar template para cada item do repeater
```
---
*JetEngine Dynamic Content | Descomplicar® | 18-02-2026*

View File

@@ -0,0 +1,225 @@
# JetEngine — Query Builder e Listings
## Query Builder Avançado
### Tipos de Query e Configuração
#### Query Posts
```
Campos principais:
├── Post Type: CPT ou post/page
├── Post Status: publish / draft / any
├── Posts per page: N (ou -1 para todos)
├── Order: ASC / DESC
├── Order By: date / title / menu_order / meta_value / meta_value_num / rand
│ └── Se meta_value: seleccionar meta key
├── Meta Query (múltiplas condições):
│ ├── Field: meta key name
│ ├── Type: CHAR / NUMERIC / DATE
│ ├── Operator: = / != / > / < / BETWEEN / LIKE / IN / NOT IN / EXISTS
│ └── Value: valor ou macro (%current_user_id%)
└── Tax Query:
├── Taxonomy: seleccionar taxonomy
├── Operator: IN / NOT IN / AND
└── Terms: valores ou relação dinâmica
```
#### Query CCT
```
Campos principais:
├── Content Type: seleccionar CCT
├── Items per page: N
├── Order By: qualquer coluna do CCT
├── Filters (equivalente a Meta Query):
│ ├── Field: coluna do CCT
│ ├── Operator: = / != / > / LIKE / IN
│ └── Value: valor ou macro
└── Show items only for current user: activar para perfis
```
#### Query Relação
```
Objetivo: listar items relacionados ao post/item actual
├── Relation: seleccionar relação criada no JetEngine
├── Get children of: post actual / ID específico / macro
└── Post Type: tipo dos items relacionados
```
### Meta Query com Macros
```
Exemplo: listar projectos do utilizador logado
Meta Query:
├── Field: cliente_user_id
├── Operator: =
└── Value: %current_user_id%
Exemplo: listar items de hoje
Meta Query:
├── Field: data_evento (timestamp)
├── Type: NUMERIC
├── Operator: >=
└── Value: %current_timestamp%
```
### Combinar Múltiplas Condições
```
Relation: AND (todas as condições verdadeiras)
OR (pelo menos uma verdadeira)
Exemplo: imóveis em Lisboa com preço < 300k
Tax Query 1: Localização = Lisboa
Meta Query 1: Preço < 300000
Relation: AND
```
---
## Listing Grid — Configuração Detalhada
### Criar Listing Template
```
JetEngine > Listings > Add New
Nome: [CPT]-listing-card
Listing From: Posts / Terms / Users / CCT
Post Type: [seleccionar]
Editar com Elementor:
├── Usar Dynamic Tags em todos os campos
├── Post Title Widget → ou Text Widget com dynamic tag
├── Meta Field → adicionar Dynamic Tag > JetEngine > Meta Field
├── Post Featured Image → ou Image Widget com dynamic tag
└── Button → URL com dynamic tag > Post URL
```
### Listing Grid Widget — Parâmetros
```
Listing Item: [seleccionar template]
Use Custom Query: YES → seleccionar Query Builder
Items per page: 12 (padrão)
Not Found Text: "Sem resultados"
Loader: activar para UX com filtros
Columns: 1/2/3/4 (responsivo)
Equal Columns Height: YES para cards uniformes
```
### Paginação
```
Listing Grid > Pagination:
├── Type: Default WP / Load More / Infinity Scroll
├── Load More Button Text: "Carregar mais"
├── On Scroll: activar para infinity
└── Starting From: 1
```
### Ordenação Dinâmica (Frontend)
```
Adicionar widget: JetSmartFilters > Sorting
├── Ligar ao mesmo Listing Grid
└── Opções: Date ASC/DESC, Title, Price, Custom Field
```
---
## Listing Map
### Configuração
```
1. Activar Google Maps API Key:
JetEngine > Settings > Google Maps API Key
2. Criar Meta Box com campo tipo Map no CPT
(O utilizador define latitude/longitude via picker no editor)
3. Criar Listing Template tipo: Map Marker
Adicionar: Post Title, info a mostrar no popup
4. Adicionar widget Listing Map ao Elementor
├── Listing Item: [marker template]
├── Use Custom Query: YES
├── Map Field: seleccionar campo coordenadas
└── Map Options: zoom, altura, cluster
```
### Mapa + Listing Grid Sincronizados
```
Adicionar os dois widgets na mesma página
Ligar ao mesmo Query Builder
Activar "Synchronize with Listing Grid": YES no Map
→ Ao fazer hover no card, destaca pin no mapa
→ Ao fazer hover no pin, destaca card na lista
```
---
## Archive e Single Templates
### Single CPT com Elementor (JetThemeCore ou Elementor Pro)
```
JetThemeCore > Templates > Add New > Single
Conditions: Post Type = [meu_cpt]
Editar com Elementor usando Dynamic Tags
OU
Elementor > Templates > Theme Builder > Single
Conditions: igual
```
### Archive Page
```
JetThemeCore > Templates > Add New > Archive
Conditions: Post Type Archive = [meu_cpt]
Adicionar:
├── Archive Title Widget
├── Listing Grid Widget (sem Custom Query — usa WP_Query do arquivo)
└── JetSmartFilters (ligado ao mesmo archive)
```
---
## JetSmartFilters — Integração Avançada
### AJAX Reload vs Full Page
```
Provider: Listing Grid → AJAX (sem reload de página)
Provider: Archive → Pode ser AJAX ou URL based
Provider: WooCommerce → AJAX
```
### URL Based Filters (SEO Friendly)
```
JetSmartFilters > Filters > Edit
├── Query Variable: activar
├── URL Based: activar
└── Clean URL: activar
Resultado: site.pt/imoveis/?cidade=Lisboa&preco=200000-400000
(Em vez de AJAX puro — melhor para SEO e partilha de URL)
```
### Hierarquical Taxonomy Filter
```
Exemplo: País → Distrito → Concelho
├── Criar taxonomia com hierarquia
├── Filtro: Checkboxes com hierarquia activada
└── "Show children of selected": activar
```
---
*JetEngine Query & Listings | Descomplicar® | 18-02-2026*