feat(perfex-dev): adiciona plugin perfex-dev (agents, skills, knowledge e metadados do projeto)

This commit is contained in:
2026-10-28 16:09:35 +00:00
parent b446fe25fe
commit 77c33e4d96
31 changed files with 2722 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
{
"name": "perfex-dev",
"description": "Perfex CRM module development - controllers, views, forms, hooks, menus, migrations, permissions, security and database. Backed by NotebookLM notebooks.",
"version": "1.0.0",
"author": {
"name": "Descomplicar - Crescimento Digital",
"url": "https://descomplicar.pt"
},
"homepage": "https://git.descomplicar.pt/ealmeida/descomplicar-plugins",
"license": "MIT",
"keywords": ["perfex", "crm", "modulos", "codeigniter", "desenvolvimento"]
}
+17
View File
@@ -0,0 +1,17 @@
# AGENTS.md — perfex-dev
Directrizes específicas deste projecto para agentes IA.
## Referências obrigatórias
- `ECOSYSTEM.md` — Hub/06-Operacoes/Documentacao/ECOSYSTEM.md
- `PROC-DEV-STANDARD` — D7-SIS-006
- `PROC-AUDIT-STANDARD` — D7-SIS-007
## Protocolo de sessão
Início: ler MEMORY.md + STATUS.md. Fim: actualizar ambos.
## Convenções específicas
- (a definir)
+5
View File
@@ -0,0 +1,5 @@
# CHANGELOG — perfex-dev
## 0.1.0 — 2026-06-30
### Adicionado
- Estrutura padrão (PROC-DEV-STANDARD)
+4
View File
@@ -0,0 +1,4 @@
# MEMORY.md — perfex-dev
## 2026-06-30
- Estrutura padrão aplicada via propagate-structure.sh
+3
View File
@@ -0,0 +1,3 @@
# perfex-dev
Subprojecto perfex-dev do ecossistema Descomplicar.
+8
View File
@@ -0,0 +1,8 @@
# STATUS.md — perfex-dev
**Versão:** 0.1 | **Data:** 2026-06-30 | **Agente:** —
## Estado actual
- Em curso: —
- Bloqueios: —
- Próximos passos: —
@@ -0,0 +1,169 @@
---
name: perfex-crm-module-developer
description: Desenvolvimento de modulos personalizados para Perfex CRM com expertise
em automacao de workflows e integracoes
role: Desenvolvimento de modulos personalizados para Perfex CRM com expertise em automacao
de workflows e integracoes
domain: Dev
model: sonnet
tools: Read, Write, Edit, Bash, Glob, Grep, ToolSearch
# Dependencies
primary_mcps:
- desk-crm-v3
- ssh-unified
- gitea
recommended_mcps:
- context7
- filesystem
skills:
- _core
- perfex-module-basics
- perfex-hooks
- perfex-controllers
- perfex-views
- perfex-menus
- perfex-permissions
- perfex-database
- perfex-security
- perfex-forms
- perfex-migrations
desk_task: 1510
desk_project: 65
milestone: 274
tags:
- agent
- stackworkflow
- claude-code
- perfex
version: '2.0'
status: active
quality_score: 70
compliance:
sacred_rules: true
excellence_standards: true
data_sources: true
knowledge_first: true
created: '2025-01-13'
updated: '2026-02-04'
author: Descomplicar®
layer: wiki
---
# Perfex CRM Module Developer Descomplicar
Especialista em desenvolvimento de modulos customizados, automacao de workflows e criacao de integracoes para sistemas Perfex CRM.
## Responsabilidades
- Desenvolvimento de modulos Perfex CRM seguindo convencoes do framework
- Design e implementacao de extensoes de schema de base de dados
- Criacao de workflows automatizados para lead management e vendas
- Desenvolvimento de ferramentas de reporting e dashboards customizados
- Implementacao de integracoes com servicos terceiros (pagamentos, comunicacao)
## Knowledge Sources (Consultar SEMPRE)
### NotebookLM (Primario - usar PRIMEIRO)
```
mcp__notebooklm__notebook_query notebook_id:"df4688bb-c2c0-4aba-98c1-38c3b50a353c" query:"module development hooks customization"
mcp__notebooklm__notebook_query notebook_id:"24947ffa-0019-448a-a340-2f4a275d2eb1" query:"PHP MVC CRM development"
```
## System Prompt
### Papel
Especialista em desenvolvimento de modulos customizados, automacao de workflows e criacao de integracoes para sistemas Perfex CRM.
### Regras Obrigatórias
1. SEMPRE seguir estrutura de módulos Perfex (hooks, views, controllers)
2. Namespace adequado (`modules/<nome>`)
3. Versionamento semântico (CHANGELOG.md)
4. Database migrations em `/install.php` e `/upgrade/`
5. NUNCA modificar core do Perfex
6. Testes em ambiente staging antes de produção
### Output Format
- Código PHP PSR-4 compliant
- README com instalação e configuração
- CHANGELOG.md actualizado
- Database schema documentado
- Hooks utilizados listados
## Perfex Skills (Official Documentation Only)
**Zero assumptions, zero hallucinations** - Todas baseadas em documentação oficial help.perfexcrm.com.
| Skill | Função |
|-------|--------|
| **/perfex-module-basics** | Estrutura base, init file, file headers |
| **/perfex-hooks** | Actions, filters, lista 80+ hooks |
| **/perfex-controllers** | AdminController, ClientsController, routing |
| **/perfex-views** | Templates, init_head(), init_tail() |
| **/perfex-menus** | Sidebar menus, client menus, posições |
| **/perfex-permissions** | staff_can(), register_staff_capabilities |
| **/perfex-database** | db_prefix(), options, queries seguras |
| **/perfex-security** | CSRF, XSS, input validation |
| **/perfex-forms** | form_open(), AJAX, validation |
| **/perfex-migrations** | Versioning, migrations, upgrades |
## Workflows
### Workflow 1: Criar Novo Módulo Perfex
1. Estrutura pastas (`modules/<nome>`, views, controllers, models)
2. `install.php` (DB schema, permissions, menu)
3. Hooks registration
4. Views e controllers
5. Testes funcionais
6. Documentação
### Workflow 2: Integração API Externa
1. Configurar credentials (settings module)
2. Helper class para API calls
3. Error handling e retry logic
4. Webhook receiver (se aplicável)
5. Logging de requests
6. Testes end-to-end
### Workflow 3: Custom Dashboard Widget
1. Hook `admin_widgets`
2. Controller para data fetching
3. View com gráfico/tabela
4. Permissions check
5. Responsive design
6. Cache layer
## MCPs Relevantes
- `desk-crm-v3`: Acesso directo à BD Perfex para testes
- `notebooklm`: KB primaria (Gemini 2.5 RAG) | ``: KB PerfexCRM, Desenvolvimento Software
- `ssh-unified`: Deploy de módulos em servidores
- `gitea`: Versionamento de código
## Colaboracao
- Reports to: Development Lead
- Colabora com: Database specialists, CRM administrators, Business analysts
## Your Available MCPs
### Primary MCPs (Your Domain)
✓ **desk-crm-v3** (business)
- Clientes, projectos, facturas, time tracking
- Usage: `mcp__desk-crm-v3__*`
✓ **gitea** (dev)
- Repositórios Git, issues, PRs
- Usage: `mcp__gitea__*`
✓ **ssh-unified** (infra)
- SSH, SFTP, servidor management
- Usage: `mcp__ssh-unified__*`
✓ **notebooklm** (knowledge primaria)
✓ **** (knowledge fallback)
- Knowledge base AI
- Usage: `mcp____*`
### Recommended for dev
- **context7** - Context documentation
- **n8n** - Workflows automation
- **filesystem** - Ficheiros locais
- **mag
+6
View File
@@ -0,0 +1,6 @@
{
"task_id": null,
"project_id": null,
"customer_id": null,
"gitea_repo": null
}
+8
View File
@@ -0,0 +1,8 @@
# perfex-dev
## Documentação
- [README](README.md)
- [AGENTS](AGENTS.md)
- [STATUS](STATUS.md)
- [MEMORY](MEMORY.md)
+18
View File
@@ -0,0 +1,18 @@
{
"description": "Knowledge sources for Perfex CRM Development domain",
"sources": {
"notebooklm": {
"description": "NotebookLM - conhecimento curado profundo via Gemini 2.5 RAG (PRIMARIO)",
"query_tool": "mcp__notebooklm__notebook_query",
"notebooks": [
{
"id": "df4688bb-c2c0-4aba-98c1-38c3b50a353c",
"title": "Perfex CRM: Gestao de Clientes e Projetos",
"topics": [
"perfexcrm"
]
}
]
}
}
}
+1
View File
@@ -0,0 +1 @@
## 2026-06-30 init | Estrutura padrão
+17
View File
@@ -0,0 +1,17 @@
# AGENTS.md — skills
Directrizes específicas deste projecto para agentes IA.
## Referências obrigatórias
- `ECOSYSTEM.md` — Hub/06-Operacoes/Documentacao/ECOSYSTEM.md
- `PROC-DEV-STANDARD` — D7-SIS-006
- `PROC-AUDIT-STANDARD` — D7-SIS-007
## Protocolo de sessão
Início: ler MEMORY.md + STATUS.md. Fim: actualizar ambos.
## Convenções específicas
- (a definir)
+5
View File
@@ -0,0 +1,5 @@
# CHANGELOG — skills
## 0.1.0 — 2026-06-30
### Adicionado
- Estrutura padrão (PROC-DEV-STANDARD)
+4
View File
@@ -0,0 +1,4 @@
# MEMORY.md — skills
## 2026-06-30
- Estrutura padrão aplicada via propagate-structure.sh
+3
View File
@@ -0,0 +1,3 @@
# skills
Subprojecto skills do ecossistema Descomplicar.
+8
View File
@@ -0,0 +1,8 @@
# STATUS.md — skills
**Versão:** 0.1 | **Data:** 2026-06-30 | **Agente:** —
## Estado actual
- Em curso: —
- Bloqueios: —
- Próximos passos: —
+6
View File
@@ -0,0 +1,6 @@
{
"task_id": null,
"project_id": null,
"customer_id": null,
"gitea_repo": null
}
+8
View File
@@ -0,0 +1,8 @@
# skills
## Documentação
- [README](README.md)
- [AGENTS](AGENTS.md)
- [STATUS](STATUS.md)
- [MEMORY](MEMORY.md)
+1
View File
@@ -0,0 +1 @@
## 2026-06-30 init | Estrutura padrão
@@ -0,0 +1,208 @@
---
name: perfex-controllers
description: Controllers para módulos Perfex CRM — AdminController, ClientsController, routing e trait ValidatesContact. Baseado apenas na documentação oficial.
layer: wiki
---
# /perfex-controllers - Controllers Perfex CRM
Controllers para módulos Perfex CRM. **Zero assumptions, zero hallucinations** - apenas documentação oficial.
---
## Documentação Base
- [Module Controllers](https://help.perfexcrm.com/module-controllers/)
- [CodeIgniter Controllers](https://codeigniter.com/userguide3/general/controllers.html)
---
## Estrutura de Pastas
```
modules/meu_modulo/
└── controllers/
├── Meu_modulo.php # Controller principal
├── Admin_controller.php # Controller admin adicional
└── Client_controller.php # Controller cliente
```
---
## Classes Base Disponíveis
| Classe | Uso | Validação Automática |
|--------|-----|----------------------|
| `AdminController` | Área admin/staff | Login staff obrigatório |
| `ClientsController` | Área cliente | Tema cliente aplicado |
| `App_Controller` | Base genérica | Nenhuma |
---
## AdminController (Área Admin)
Para funcionalidades exclusivas de staff/administradores.
### Template
```php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class Meu_modulo extends AdminController
{
public function __construct()
{
parent::__construct();
// Carregar models, helpers, etc.
$this->load->model('meu_modulo/meu_modulo_model');
}
/**
* Página principal do módulo
* URL: /admin/meu_modulo ou /admin/meu_modulo/index
*/
public function index()
{
// Verificar permissão
if (!staff_can('view', 'meu_modulo')) {
access_denied('meu_modulo');
}
// Preparar dados
$data['items'] = $this->meu_modulo_model->get_all();
$data['title'] = _l('meu_modulo_title');
// Carregar view
$this->load->view('meu_modulo/index', $data);
}
/**
* Formulário de criação
* URL: /admin/meu_modulo/create
*/
public function create()
{
if (!staff_can('create', 'meu_modulo')) {
access_denied('meu_modulo');
}
if ($this->input->post()) {
// Processar formulário
$data = $this->input->post();
$id = $this->meu_modulo_model->add($data);
if ($id) {
set_alert('success', _l('added_successfully', _l('meu_modulo_item')));
redirect(admin_url('meu_modulo'));
}
}
$this->load->view('meu_modulo/form');
}
/**
* Editar item
* URL: /admin/meu_modulo/edit/123
*/
public function edit($id)
{
if (!staff_can('edit', 'meu_modulo')) {
access_denied('meu_modulo');
}
$data['item'] = $this->meu_modulo_model->get($id);
if (!$data['item']) {
show_404();
}
if ($this->input->post()) {
$success = $this->meu_modulo_model->update($id, $this->input->post());
if ($success) {
set_alert('success', _l('updated_successfully', _l('meu_modulo_item')));
redirect(admin_url('meu_modulo'));
}
}
$this->load->view('meu_modulo/form', $data);
}
/**
* Apagar item
* URL: /admin/meu_modulo/delete/123
*/
public function delete($id)
{
if (!staff_can('delete', 'meu_modulo')) {
access_denied('meu_modulo');
}
if ($this->meu_modulo_model->delete($id)) {
set_alert('success', _l('deleted', _l('meu_modulo_item')));
}
redirect(admin_url('meu_modulo'));
}
}
```
---
## ClientsController (Área Cliente)
Para funcionalidades na área de clientes.
### Template
```php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class Meu_modulo extends ClientsController
{
public function __construct()
{
parent::__construct();
$this->load->model('meu_modulo/meu_modulo_model');
}
/**
* Página principal cliente
* URL: /meu_modulo ou /meu_modulo/index
*/
public function index()
{
// Verificar se cliente logado
if (!is_client_logged_in()) {
redirect(site_url('authentication/login'));
}
$contact_id = get_contact_user_id();
$client_id = get_client_user_id();
$data = [
'items' => $this->meu_modulo_model->get_by_client($client_id),
];
// Métodos específicos de ClientsController
$this->data($data);
$this->title(_l('meu_modulo_title'));
$this->view('meu_modulo/client_index');
$this->layout();
}
/**
* Ver detalhe
* URL: /meu_modulo/view/123
*/
public function view($id)
{
if (!is_client_logged_in()) {
redirect(site_url('authentication/login'));
}
$item = $this->meu_mod
+213
View File
@@ -0,0 +1,213 @@
---
name: perfex-database
description: Operações de base de dados em módulos Perfex CRM — db_prefix(), options, queries e Active Record. Baseado apenas na documentação oficial.
layer: wiki
---
# /perfex-database - Base de Dados Perfex CRM
Operações de base de dados em módulos. **Zero assumptions, zero hallucinations** - apenas documentação oficial.
---
## Documentação Base
- [Module Basics](https://help.perfexcrm.com/module-basics/)
- [CodeIgniter Query Builder](https://codeigniter.com/userguide3/database/query_builder.html)
---
## Regra Fundamental: db_prefix()
**SEMPRE usar `db_prefix()` em todas as queries.**
O Perfex CRM suporta prefixos de tabela customizados. O default é `tbl`, mas pode ser alterado.
```php
// CORRECTO
$CI->db->get(db_prefix() . 'meu_modulo');
// ERRADO - quebra se prefix não for "tbl"
$CI->db->get('tblmeu_modulo');
```
---
## Aceder à Base de Dados
### Em Controllers/Models
```php
// Acesso directo
$this->db->get(db_prefix() . 'clients');
```
### Fora de Controllers/Models
```php
$CI = &get_instance();
$CI->db->get(db_prefix() . 'clients');
```
---
## Criar Tabelas (Activation Hook)
```php
function meu_modulo_activation_hook()
{
$CI = &get_instance();
// Verificar se tabela existe
if (!$CI->db->table_exists(db_prefix() . 'meu_modulo')) {
$CI->db->query('
CREATE TABLE `' . db_prefix() . 'meu_modulo` (
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`client_id` INT(11) UNSIGNED NOT NULL,
`name` VARCHAR(255) NOT NULL,
`description` TEXT,
`amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`status` VARCHAR(50) NOT NULL DEFAULT "pending",
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `client_id` (`client_id`),
KEY `status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=' . $CI->db->char_set . ';
');
}
}
```
---
## Query Builder (Active Record)
### SELECT
```php
// Todos os registos
$result = $this->db->get(db_prefix() . 'meu_modulo')->result();
// Com condições
$this->db->where('status', 'active');
$this->db->where('client_id', $client_id);
$result = $this->db->get(db_prefix() . 'meu_modulo')->result();
// Seleccionar campos específicos
$this->db->select('id, name, amount');
$this->db->where('status', 'active');
$result = $this->db->get(db_prefix() . 'meu_modulo')->result();
// Um registo
$this->db->where('id', $id);
$row = $this->db->get(db_prefix() . 'meu_modulo')->row();
// Com LIKE
$this->db->like('name', $search);
$result = $this->db->get(db_prefix() . 'meu_modulo')->result();
// Com ORDER BY
$this->db->order_by('created_at', 'DESC');
$result = $this->db->get(db_prefix() . 'meu_modulo')->result();
// Com LIMIT
$this->db->limit(10, 0); // 10 registos, offset 0
$result = $this->db->get(db_prefix() . 'meu_modulo')->result();
// JOIN
$this->db->select('m.*, c.company as client_name');
$this->db->from(db_prefix() . 'meu_modulo as m');
$this->db->join(db_prefix() . 'clients as c', 'c.userid = m.client_id', 'left');
$result = $this->db->get()->result();
```
### INSERT
```php
$data = [
'client_id' => $client_id,
'name' => $this->input->post('name'),
'description' => $this->input->post('description'),
'amount' => $this->input->post('amount'),
'status' => 'pending',
'created_at' => date('Y-m-d H:i:s'),
];
$this->db->insert(db_prefix() . 'meu_modulo', $data);
$insert_id = $this->db->insert_id();
```
### UPDATE
```php
$data = [
'name' => $this->input->post('name'),
'description' => $this->input->post('description'),
'amount' => $this->input->post('amount'),
'updated_at' => date('Y-m-d H:i:s'),
];
$this->db->where('id', $id);
$this->db->update(db_prefix() . 'meu_modulo', $data);
// Verificar se actualizou
$affected = $this->db->affected_rows();
```
### DELETE
```php
$this->db->where('id', $id);
$this->db->delete(db_prefix() . 'meu_modulo');
```
### COUNT
```php
// Contar todos
$count = $this->db->count_all(db_prefix() . 'meu_modulo');
// Contar com condições
$this->db->where('status', 'active');
$count = $this->db->count_all_results(db_prefix() . 'meu_modulo');
```
---
## Sistema de Opções
Para guardar configurações simples (key-value).
### add_option()
Cria opção nova. **NÃO sobrescreve se existir.**
```php
add_option('meu_modulo_enabled', '1');
add_option('meu_modulo_config', serialize(['key' => 'value']));
// Com autoload (0 = não, 1 = sim)
add_option('meu_modulo_setting', 'valor', 0);
```
### get_option()
Obtém valor da opção.
```php
$enabled = get_option('meu_modulo_enabled');
$config = unserialize(get_option('meu_modulo_config'));
// Retorna string vazia se não existir
if (get_option('meu_modulo_setting') === '') {
// Opção não existe
}
```
### update_option()
Actualiza ou cria opção (v2.3.3+).
```php
update
+225
View File
@@ -0,0 +1,225 @@
---
name: perfex-forms
description: Gestao de formularios em modulos Perfex CRM -- form_open(), tokens CSRF, validacao e AJAX. Baseado apenas na documentacao oficial.
layer: wiki
---
# /perfex-forms - Formularios Perfex CRM
Gestao de formularios em modulos. **Zero assumptions, zero hallucinations** - apenas documentacao oficial.
---
## Documentacao Base
- [Working With Forms](https://help.perfexcrm.com/working-with-forms/)
- [CodeIgniter Form Helper](https://codeigniter.com/userguide3/helpers/form_helper.html)
---
## Regra Fundamental: form_open()
**SEMPRE usar `form_open()` para gerar token CSRF automaticamente.**
```php
<?php echo form_open(admin_url('meu_modulo/save')); ?>
<!-- Inputs aqui -->
<?php echo form_close(); ?>
```
**NUNCA usar `<form>` HTML directo** - sem CSRF protection.
---
## Sintaxe form_open()
```php
// Basico
echo form_open(admin_url('meu_modulo/save'));
// Com atributos
echo form_open(admin_url('meu_modulo/save'), [
'id' => 'form-meu-modulo',
'class' => 'form-horizontal',
]);
// Com upload de ficheiros
echo form_open_multipart(admin_url('meu_modulo/upload'), [
'id' => 'form-upload',
]);
// Fechar formulario
echo form_close();
```
---
## Elementos de Formulario
Todos os elementos com exemplos completos em: `references/elementos-formulario.md`
Elementos disponiveis: Input Text, Textarea, Select, Select AJAX, Checkbox, Radio, Date Picker, DateTime Picker, File Upload, Hidden.
**Padrao geral:**
```php
<div class="form-group">
<label for="CAMPO" class="control-label">
<?php echo _l('CAMPO'); ?>
<span class="text-danger">*</span> <!-- se obrigatorio -->
</label>
<input type="text" id="CAMPO" name="CAMPO" class="form-control"
value="<?php echo isset($item) ? html_escape($item->CAMPO) : ''; ?>">
</div>
```
---
## Validacao Client-Side
O Perfex usa jQuery Validation. Usar `appValidateForm()`:
```php
<?php init_tail(); ?>
<script>
$(function(){
appValidateForm($('#form-meu-modulo'), {
name: {
required: true,
minlength: 3
},
email: {
required: true,
email: true
},
amount: {
required: true,
number: true,
min: 0
}
});
});
</script>
</body>
</html>
```
### Regras de Validacao
| Regra | Descricao |
|-------|-----------|
| `required: true` | Campo obrigatorio |
| `email: true` | Email valido |
| `number: true` | Numero |
| `digits: true` | Apenas digitos |
| `minlength: N` | Minimo N caracteres |
| `maxlength: N` | Maximo N caracteres |
| `min: N` | Valor minimo |
| `max: N` | Valor maximo |
| `equalTo: '#field'` | Igual a outro campo |
---
## Validacao Server-Side
```php
public function save()
{
if (!$this->input->post()) {
redirect(admin_url('meu_modulo'));
}
$this->load->library('form_validation');
$this->form_validation->set_rules('name', _l('name'), 'required|min_length[3]');
$this->form_validation->set_rules('email', _l('email'), 'required|valid_email');
$this->form_validation->set_rules('amount', _l('amount'), 'required|numeric');
if ($this->form_validation->run() === false) {
set_alert('danger', validation_errors());
redirect(admin_url('meu_modulo/create'));
}
$data = [
'name' => $this->input->post('name'),
'email' => $this->input->post('email'),
'amount' => $this->input->post('amount'),
];
// ... guardar
}
```
---
## AJAX com jQuery
### POST Simples
```javascript
$.post(admin_url + 'meu_modulo/ajax_save', {
name: $('#name').val(),
description: $('#description').val()
}).done(function(response) {
response = JSON.parse(response);
if (response.success) {
alert_float('success', response.message);
} else {
alert_float('danger', response.message);
}
});
```
### Controller AJAX
```php
public function ajax_save()
{
if (!$this->input->is_ajax_request()) {
show_404();
}
if (!staff_can('create', 'meu_modulo')) {
echo json_encode(['success' => false, 'message' => _l('access_denied')]);
return;
}
$data = [
'name' => $this->input->post('name'),
'description' => $this->input->post('description'),
];
$id = $this->meu_modulo_model->add($data);
echo json_encode([
'success' => (bool) $id,
'id' => $id,
'message' => $id ? _l('added_successfully') : _l('error_occurred'),
]);
}
```
---
## Excluir CSRF para Webhooks
Para endpoints que recebem callbacks externos:
```php
// modules/meu_modulo/config/csrf_exclude_uris.php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
return [
'meu_modulo/webhook',
'meu_modulo/callback',
'meu_modulo/api/.*', // Regex
];
```
---
## Anti-Patterns (NUNCA FAZER)
| Anti-Pattern | Risco | Alternativa |
|--------------|-------|-------------|
| `<form>` HTML directo | CSRF bypass
@@ -0,0 +1,159 @@
# Elementos de Formulario Perfex CRM
Referencia completa de todos os elementos HTML/PHP para formularios em modulos Perfex.
---
## Input Text
```php
<div class="form-group">
<label for="name" class="control-label">
<?php echo _l('name'); ?>
<span class="text-danger">*</span>
</label>
<input type="text"
id="name"
name="name"
class="form-control"
value="<?php echo isset($item) ? html_escape($item->name) : ''; ?>"
required>
</div>
```
## Textarea
```php
<div class="form-group">
<label for="description" class="control-label">
<?php echo _l('description'); ?>
</label>
<textarea id="description"
name="description"
class="form-control"
rows="4"><?php echo isset($item) ? html_escape($item->description) : ''; ?></textarea>
</div>
```
## Select
```php
<div class="form-group">
<label for="client_id" class="control-label">
<?php echo _l('client'); ?>
</label>
<select id="client_id" name="client_id" class="selectpicker"
data-live-search="true" data-width="100%">
<option value=""><?php echo _l('select'); ?></option>
<?php foreach ($clients as $client): ?>
<option value="<?php echo $client['userid']; ?>"
<?php echo (isset($item) && $item->client_id == $client['userid']) ? 'selected' : ''; ?>>
<?php echo html_escape($client['company']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
```
## Select com AJAX (Clientes)
```php
<div class="form-group">
<label for="clientid" class="control-label">
<?php echo _l('client'); ?>
</label>
<select id="clientid" name="clientid" data-live-search="true"
data-width="100%" class="ajax-search"
data-none-selected-text="<?php echo _l('dropdown_non_selected_tex'); ?>">
<?php if (isset($item) && $item->clientid): ?>
<option value="<?php echo $item->clientid; ?>" selected>
<?php echo html_escape(get_company_name($item->clientid)); ?>
</option>
<?php endif; ?>
</select>
</div>
```
## Checkbox
```php
<div class="checkbox checkbox-primary">
<input type="checkbox"
id="is_active"
name="is_active"
value="1"
<?php echo (isset($item) && $item->is_active == 1) ? 'checked' : ''; ?>>
<label for="is_active"><?php echo _l('active'); ?></label>
</div>
```
## Radio
```php
<div class="form-group">
<label class="control-label"><?php echo _l('status'); ?></label>
<div class="radio radio-primary">
<input type="radio" name="status" id="status_pending" value="pending"
<?php echo (!isset($item) || $item->status == 'pending') ? 'checked' : ''; ?>>
<label for="status_pending"><?php echo _l('pending'); ?></label>
</div>
<div class="radio radio-primary">
<input type="radio" name="status" id="status_active" value="active"
<?php echo (isset($item) && $item->status == 'active') ? 'checked' : ''; ?>>
<label for="status_active"><?php echo _l('active'); ?></label>
</div>
</div>
```
## Date Picker
```php
<div class="form-group">
<label for="date" class="control-label">
<?php echo _l('date'); ?>
</label>
<input type="text"
id="date"
name="date"
class="form-control datepicker"
value="<?php echo isset($item) ? _d($item->date) : _d(date('Y-m-d')); ?>"
autocomplete="off">
</div>
```
## Date Time Picker
```php
<div class="form-group">
<label for="datetime" class="control-label">
<?php echo _l('date_time'); ?>
</label>
<input type="text"
id="datetime"
name="datetime"
class="form-control datetimepicker"
value="<?php echo isset($item) ? _dt($item->datetime) : ''; ?>"
autocomplete="off">
</div>
```
## File Upload
```php
<?php echo form_open_multipart(admin_url('meu_modulo/upload')); ?>
<div class="form-group">
<label for="attachment" class="control-label">
<?php echo _l('attachment'); ?>
</label>
<input type="file" id="attachment" name="attachment" class="form-control">
</div>
<?php echo form_close(); ?>
```
## Hidden
```php
<input type="hidden" name="id" value="<?php echo $item->id; ?>">
```
@@ -0,0 +1,141 @@
# Formulario Completo Exemplo - Perfex CRM
Exemplo completo de view com formulario, validacao client-side e todos os elementos tipicos.
---
## View
```php
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<?php init_head(); ?>
<div id="wrapper">
<div class="content">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel_s">
<div class="panel-body">
<h4 class="no-margin">
<?php echo isset($item) ? _l('edit') : _l('create'); ?>
<?php echo _l('meu_modulo_item'); ?>
</h4>
<hr class="hr-panel-heading" />
<?php
$action = isset($item)
? admin_url('meu_modulo/save/' . $item->id)
: admin_url('meu_modulo/save');
echo form_open($action, ['id' => 'form-meu-modulo']);
?>
<!-- Nome -->
<div class="form-group">
<label for="name" class="control-label">
<?php echo _l('name'); ?>
<span class="text-danger">*</span>
</label>
<input type="text" id="name" name="name"
class="form-control"
value="<?php echo isset($item) ? html_escape($item->name) : ''; ?>">
</div>
<!-- Cliente -->
<div class="form-group">
<label for="client_id" class="control-label">
<?php echo _l('client'); ?>
</label>
<select id="client_id" name="client_id"
class="selectpicker"
data-live-search="true"
data-width="100%">
<option value=""><?php echo _l('select'); ?></option>
<?php foreach ($clients as $client): ?>
<option value="<?php echo $client['userid']; ?>"
<?php echo (isset($item) && $item->client_id == $client['userid']) ? 'selected' : ''; ?>>
<?php echo html_escape($client['company']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<!-- Valor -->
<div class="form-group">
<label for="amount" class="control-label">
<?php echo _l('amount'); ?>
</label>
<input type="number" id="amount" name="amount"
class="form-control" step="0.01" min="0"
value="<?php echo isset($item) ? $item->amount : '0.00'; ?>">
</div>
<!-- Data -->
<div class="form-group">
<label for="date" class="control-label">
<?php echo _l('date'); ?>
</label>
<input type="text" id="date" name="date"
class="form-control datepicker"
value="<?php echo isset($item) ? _d($item->date) : _d(date('Y-m-d')); ?>"
autocomplete="off">
</div>
<!-- Descricao -->
<div class="form-group">
<label for="description" class="control-label">
<?php echo _l('description'); ?>
</label>
<textarea id="description" name="description"
class="form-control"
rows="4"><?php echo isset($item) ? html_escape($item->description) : ''; ?></textarea>
</div>
<!-- Activo -->
<div class="checkbox checkbox-primary">
<input type="checkbox" id="is_active" name="is_active" value="1"
<?php echo (!isset($item) || $item->is_active == 1) ? 'checked' : ''; ?>>
<label for="is_active"><?php echo _l('active'); ?></label>
</div>
<!-- Botoes -->
<div class="btn-bottom-toolbar text-right">
<button type="submit" class="btn btn-primary">
<?php echo _l('save'); ?>
</button>
<a href="<?php echo admin_url('meu_modulo'); ?>" class="btn btn-default">
<?php echo _l('cancel'); ?>
</a>
</div>
<?php echo form_close(); ?>
</div>
</div>
</div>
</div>
</div>
</div>
<?php init_tail(); ?>
<script>
$(function(){
// Validacao
appValidateForm($('#form-meu-modulo'), {
name: {
required: true,
minlength: 3
},
amount: {
number: true,
min: 0
}
});
// Inicializar datepicker (ja auto via classe)
// Inicializar selectpicker (ja auto via classe)
});
</script>
</body>
</html>
```
+180
View File
@@ -0,0 +1,180 @@
---
name: perfex-hooks
description: Sistema de hooks Perfex CRM — actions, filters, add_action, add_filter, do_action, apply_filters com lista completa por categoria. Baseado apenas na documentação oficial.
layer: wiki
---
# /perfex-hooks - Sistema de Hooks Perfex CRM
Sistema de extensibilidade via actions e filters. **Zero assumptions, zero hallucinations** - apenas documentação oficial.
---
## Documentação Base
- [Action Hooks](https://help.perfexcrm.com/action-hooks/)
- [GitHub Hook List](https://gist.github.com/JamesSimpson/4eae4ba2d6d7072eca9f0fae58b8887c)
---
## Conceitos Fundamentais
| Tipo | Função | Retorno | Uso |
|------|--------|---------|-----|
| **Action** | Executa lógica após eventos | void | Side effects (logs, emails, etc.) |
| **Filter** | Modifica dados antes de processamento | Dados modificados | Alterar valores |
---
## Sintaxe (v2.3.0+)
### Registar Action
```php
hooks()->add_action($tag, $callback, $priority = 10, $accepted_args = 1);
```
**Parâmetros:**
- `$tag` (string): Nome do hook
- `$callback` (callable): Função a executar
- `$priority` (int): Ordem de execução (menor = primeiro). Default: 10
- `$accepted_args` (int): Número de argumentos. Default: 1
**Exemplo:**
```php
// No init file do módulo
hooks()->add_action('after_client_added', 'meu_modulo_after_client_added');
function meu_modulo_after_client_added($clientId)
{
// Lógica após cliente criado
log_activity('Novo cliente criado: ' . $clientId);
// Enviar notificação
$CI = &get_instance();
$CI->load->model('clients_model');
$client = $CI->clients_model->get($clientId);
// Integração externa, etc.
}
```
### Registar Filter
```php
hooks()->add_filter($tag, $callback, $priority = 10, $accepted_args = 1);
```
**Exemplo:**
```php
hooks()->add_filter('before_client_added', 'meu_modulo_before_client_added');
function meu_modulo_before_client_added($data)
{
// Modificar dados antes de guardar
if (empty($data['country'])) {
$data['country'] = 177; // Portugal
}
// OBRIGATÓRIO: retornar dados modificados
return $data;
}
```
### Executar Action (criar hooks próprios)
```php
hooks()->do_action($tag, $arg = '');
```
**Exemplo:**
```php
// No teu módulo, permitir que outros módulos executem código
hooks()->do_action('meu_modulo_after_process', $resultId);
```
### Aplicar Filter (criar filters próprios)
```php
hooks()->apply_filters($tag, $value, $additionalParams = []);
```
**Exemplo:**
```php
// Permitir que outros módulos modifiquem dados
$data = hooks()->apply_filters('meu_modulo_data', $data);
```
---
## Onde Colocar Hooks
### Opção 1: Init File do Módulo (Recomendado)
```php
// modules/meu_modulo/meu_modulo.php
hooks()->add_action('after_client_added', 'meu_modulo_client_handler');
```
### Opção 2: my_functions_helper.php (Customizações Simples)
```php
// application/helpers/my_functions_helper.php
hooks()->add_action('after_client_added', 'custom_client_handler');
```
---
## Lista Completa de Hooks por Categoria
### Autenticação
| Hook | Trigger | Argumentos |
|------|---------|------------|
| `admin_auth_init` | Início auth admin | - |
| `after_staff_login` | Após login staff | $staff_id |
| `after_user_logout` | Após logout | - |
| `after_contact_login` | Após login contacto | $contact_id |
| `after_client_register` | Após registo cliente | $client_id |
| `after_client_logout` | Após logout cliente | - |
### Clientes
| Hook | Trigger | Argumentos |
|------|---------|------------|
| `before_client_added` | **FILTER** - Antes de criar | $data |
| `after_client_added` | Após criar cliente | $client_id |
| `before_client_updated` | **FILTER** - Antes de actualizar | $data, $client_id |
| `after_client_updated` | Após actualizar | $client_id |
| `before_client_deleted` | Antes de apagar | $client_id |
| `after_client_deleted` | Após apagar | $client_id |
| `contact_created` | Contacto criado | $contact_id |
| `contact_updated` | Contacto actualizado | $contact_id |
| `before_delete_contact` | Antes apagar contacto | $contact_id |
| `contact_deleted` | Contacto apagado | $contact_id |
### Facturas
| Hook | Trigger | Argumentos |
|------|---------|------------|
| `before_invoice_added` | **FILTER** - Antes de criar | $data |
| `after_invoice_added` | Após criar | $invoice_id |
| `before_invoice_updated` | **FILTER** - Antes de actualizar | $data, $invoice_id |
| `after_invoice_updated` | Após actualizar | $invoice_id |
| `invoice_sent` | Factura enviada | $invoice_id |
| `invoice_status_changed` | Status alterado | $invoice_id, $status |
| `before_invoice_deleted` | Antes de apagar | $invoice_id |
| `invoice_marked_as_cancelled` | Factura cancelada | $invoice_id |
### Orçamentos (Estimates)
| Hook | Trigger | Argumentos |
|------|---------|------------|
| `before_estimate_added` | **FILTER** - Antes de criar | $data |
| `after_estimate_added` | Após criar | $estimate_id |
| `after_estimate_updated` | Após actualizar | $estimate_id |
| `estimate_
+223
View File
@@ -0,0 +1,223 @@
---
name: perfex-menus
description: Criação de menus para módulos Perfex CRM — sidebar, menus de cliente, add_sidebar_menu_item(), posições e ícones. Baseado apenas na documentação oficial.
layer: wiki
---
# /perfex-menus - Menus Perfex CRM
Criação de menus para módulos. **Zero assumptions, zero hallucinations** - apenas documentação oficial.
---
## Documentação Base
- [Create Menu Items](https://help.perfexcrm.com/create-menu-items/)
---
## Menu Admin (Sidebar)
### Adicionar Item Simples
```php
// No init file do módulo
hooks()->add_action('admin_init', 'meu_modulo_init_menu');
function meu_modulo_init_menu()
{
$CI = &get_instance();
$CI->app_menu->add_sidebar_menu_item('meu-modulo-menu', [
'name' => _l('meu_modulo_title'),
'href' => admin_url('meu_modulo'),
'position' => 25,
'icon' => 'fa fa-cube',
]);
}
```
### Parâmetros
| Parâmetro | Tipo | Descrição |
|-----------|------|-----------|
| `name` | string | Texto do menu (usar _l() para tradução) |
| `href` | string | URL destino |
| `position` | int | Posição no menu (menor = mais acima) |
| `icon` | string | Classe Font Awesome |
| `collapse` | bool | Se tem sub-items (default: false) |
| `badge` | array | Badge com contagem |
---
## Menu com Sub-Items (Collapsible)
```php
hooks()->add_action('admin_init', 'meu_modulo_init_menu');
function meu_modulo_init_menu()
{
$CI = &get_instance();
// Menu pai (collapsible)
$CI->app_menu->add_sidebar_menu_item('meu-modulo-menu', [
'name' => _l('meu_modulo_title'),
'collapse' => true,
'position' => 25,
'icon' => 'fa fa-cube',
]);
// Sub-item 1
$CI->app_menu->add_sidebar_children_item('meu-modulo-menu', [
'slug' => 'meu-modulo-lista',
'name' => _l('list'),
'href' => admin_url('meu_modulo'),
'position' => 1,
'icon' => 'fa fa-list',
]);
// Sub-item 2
$CI->app_menu->add_sidebar_children_item('meu-modulo-menu', [
'slug' => 'meu-modulo-criar',
'name' => _l('create'),
'href' => admin_url('meu_modulo/create'),
'position' => 2,
'icon' => 'fa fa-plus',
]);
// Sub-item 3
$CI->app_menu->add_sidebar_children_item('meu-modulo-menu', [
'slug' => 'meu-modulo-settings',
'name' => _l('settings'),
'href' => admin_url('meu_modulo/settings'),
'position' => 3,
'icon' => 'fa fa-cog',
]);
}
```
---
## Posições Padrão Admin
| Posição | Menu Item |
|---------|-----------|
| 1 | Dashboard |
| 5 | Customers |
| 10 | Sales |
| 15 | Subscriptions |
| 20 | Expenses |
| 25 | Contracts |
| 30 | Projects |
| 35 | Tasks |
| 40 | Tickets |
| 45 | Leads |
| 50 | Knowledge Base |
| 55 | Utilities |
| 60 | Reports |
**Dica:** Usar posições intermédias (ex: 27, 32) para inserir entre items existentes.
---
## Badge (Contador)
```php
$CI->app_menu->add_sidebar_menu_item('meu-modulo-menu', [
'name' => _l('meu_modulo_title'),
'href' => admin_url('meu_modulo'),
'position' => 25,
'icon' => 'fa fa-cube',
'badge' => [
'value' => meu_modulo_count_pending(),
'type' => 'danger', // primary, success, warning, danger, info
'show_on_sidebar' => true,
],
]);
// Função de contagem
function meu_modulo_count_pending()
{
$CI = &get_instance();
$CI->db->where('status', 'pending');
return $CI->db->count_all_results(db_prefix() . 'meu_modulo');
}
```
---
## Menu Condicional (Permissões)
```php
function meu_modulo_init_menu()
{
// Só mostrar se staff tem permissão
if (!staff_can('view', 'meu_modulo')) {
return;
}
$CI = &get_instance();
$CI->app_menu->add_sidebar_menu_item('meu-modulo-menu', [
'name' => _l('meu_modulo_title'),
'href' => admin_url('meu_modulo'),
'position' => 25,
'icon' => 'fa fa-cube',
]);
}
```
---
## Menu Cliente (Área Cliente)
### Adicionar Item
```php
hooks()->add_action('clients_init', 'meu_modulo_client_menu');
function meu_modulo_client_menu()
{
// Só mostrar se cliente logado
if (!is_client_logged_in()) {
return;
}
add_theme_menu_item('meu-modulo', [
'name' => _l('meu_modulo_title'),
'href' => site_url('meu_modulo'),
'position' => 35,
'icon' => 'fa fa-cube',
]);
}
```
### Posições Padrão Cliente
| Posição | Menu Item |
|---------|-----------|
| 5 | Knowledge Base |
| 10 | Projects |
| 15 | Invoices |
| 20 | Contracts |
| 25 | Estimates |
| 30 | Proposals |
| 40 | Subscriptions |
| 45 | Support |
| 99 | Registration |
| 100 | Login |
---
## Menu no Setup (Administração)
Para adicionar configurações ao menu Setup:
```php
hooks()->add_action('admin_init', 'meu_modulo_setup_menu');
function meu_modulo_setup_menu()
{
$CI = &get_instance();
// Adicionar ao sub-menu de Setup
@@ -0,0 +1,211 @@
---
name: perfex-migrations
description: Migrações e upgrades de módulos Perfex CRM — gestão de versões, ficheiros de migração e actualizações de base de dados. Baseado apenas na documentação oficial.
layer: wiki
---
# /perfex-migrations - Migrations Perfex CRM
Sistema de migrações e upgrades de módulos. **Zero assumptions, zero hallucinations** - apenas documentação oficial.
---
## Documentação Base
- [Preparing Module Upgrade](https://help.perfexcrm.com/preparing-module-upgrade/)
- [Module Basics](https://help.perfexcrm.com/module-basics/)
---
## Como Funciona
O Perfex CRM compara:
- **Versão no init file** (header `Version:`)
- **Versão na base de dados** (tabela `tblmodules`)
Se forem diferentes → Sistema mostra "Database upgrade required".
---
## Estrutura de Migrations
```
modules/meu_modulo/
├── meu_modulo.php # Init file com Version: X.Y.Z
└── migrations/
├── 001_version_100.php # v1.0.0 → v1.0.1
├── 002_version_101.php # v1.0.1 → v1.0.2
├── 100_version_100.php # v1.0.0 inicial (alternativa)
├── 110_version_110.php # v1.1.0
└── 200_version_200.php # v2.0.0
```
---
## Formato do Ficheiro
### Nomenclatura (CRÍTICO)
O Perfex usa **Sequential Migration** (não Timestamp).
```
NNN_version_NNN.php
│ │ │
│ │ └── Versão sem pontos (110 = 1.1.0)
│ └── Literal "version"
└── Número sequencial (3 dígitos, sem gaps)
```
**Exemplos:**
| Versão | Ficheiro |
|--------|----------|
| 1.0.0 | `001_version_100.php` ou `100_version_100.php` |
| 1.0.1 | `002_version_101.php` ou `101_version_101.php` |
| 1.1.0 | `003_version_110.php` ou `110_version_110.php` |
| 2.0.0 | `004_version_200.php` ou `200_version_200.php` |
**Convenção recomendada:** Usar versão como prefixo (100, 110, 200...).
---
## Template Migration
```php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class Migration_Version_110 extends App_module_migration
{
/**
* Executado durante upgrade
*/
public function up()
{
// Alterações de base de dados aqui
}
}
```
### Notas Importantes
1. **Classe:** `Migration_Version_NNN` (NNN = versão sem pontos)
2. **Extends:** `App_module_migration`
3. **Método:** Apenas `up()` - não há `down()` (downgrades não suportados)
4. **Pode ser vazio:** Se não houver alterações de BD, deixar `up()` vazio
---
## Exemplos de Migrations
### Adicionar Coluna
```php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class Migration_Version_110 extends App_module_migration
{
public function up()
{
$CI = &get_instance();
// Adicionar coluna se não existir
if (!$CI->db->field_exists('new_column', db_prefix() . 'meu_modulo')) {
$CI->db->query('ALTER TABLE `' . db_prefix() . 'meu_modulo`
ADD COLUMN `new_column` VARCHAR(255) DEFAULT NULL AFTER `name`');
}
}
}
```
### Criar Nova Tabela
```php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class Migration_Version_120 extends App_module_migration
{
public function up()
{
$CI = &get_instance();
if (!$CI->db->table_exists(db_prefix() . 'meu_modulo_items')) {
$CI->db->query('
CREATE TABLE `' . db_prefix() . 'meu_modulo_items` (
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`parent_id` INT(11) UNSIGNED NOT NULL,
`name` VARCHAR(255) NOT NULL,
`quantity` INT(11) NOT NULL DEFAULT 1,
PRIMARY KEY (`id`),
KEY `parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=' . $CI->db->char_set . ';
');
}
}
}
```
### Modificar Coluna
```php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class Migration_Version_130 extends App_module_migration
{
public function up()
{
$CI = &get_instance();
// Alterar tipo de coluna
$CI->db->query('ALTER TABLE `' . db_prefix() . 'meu_modulo`
MODIFY COLUMN `amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00');
}
}
```
### Adicionar Índice
```php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class Migration_Version_140 extends App_module_migration
{
public function up()
{
$CI = &get_instance();
// Verificar se índice existe (evitar erro)
$indexes = $CI->db->query('SHOW INDEX FROM `' . db_prefix() . 'meu_modulo`
WHERE Key_name = "idx_status"')->result();
if (count($indexes) == 0) {
$CI->db->query('ALTER TABLE `' . db_prefix() . 'meu_modulo`
ADD INDEX `idx_status` (`status`)');
}
}
}
```
### Migração de Dados
```php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class Migration_Version_150 extends App_module_migration
{
public function up()
{
$CI = &get_instance();
// Migrar dados existentes
@@ -0,0 +1,215 @@
---
name: perfex-module-basics
description: Fundamentos de desenvolvimento de módulos Perfex CRM — estrutura de pastas, ficheiro init, headers e integração CodeIgniter. Baseado apenas na documentação oficial.
layer: wiki
---
# /perfex-module-basics - Estrutura Base Módulos Perfex CRM
Fundamentos para desenvolvimento de módulos Perfex CRM. **Zero assumptions, zero hallucinations** - apenas documentação oficial.
---
## Documentação Base
- [Module Basics](https://help.perfexcrm.com/module-basics/)
- [Introduction to Modules](https://help.perfexcrm.com/introduction-to-perfex-crm-modules/)
- [Module File Headers](https://help.perfexcrm.com/module-file-headers/)
---
## Estrutura de Pastas (Obrigatória)
```
modules/
└── meu_modulo/
├── meu_modulo.php # Init file (OBRIGATÓRIO - mesmo nome da pasta)
├── controllers/
│ └── Meu_modulo.php
├── models/
│ └── Meu_modulo_model.php
├── views/
│ ├── index.php
│ └── form.php
├── libraries/
├── helpers/
├── language/
│ └── english/
│ └── meu_modulo_lang.php
├── migrations/
│ └── 001_version_100.php
├── config/
└── index.html # SEGURANÇA - prevenir directory listing
```
---
## Init File (Obrigatório)
O ficheiro init é o ponto de entrada do módulo. **DEVE ter o mesmo nome da pasta.**
### Template Mínimo
```php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
/*
Module Name: Meu Módulo
Description: Descrição do módulo
Version: 1.0.0
Requires at least: 2.3.*
Author: Descomplicar
Author URI: https://descomplicar.pt
*/
// Registar hooks de activação/desactivação
register_activation_hook('meu_modulo', 'meu_modulo_activation_hook');
register_deactivation_hook('meu_modulo', 'meu_modulo_deactivation_hook');
register_uninstall_hook('meu_modulo', 'meu_modulo_uninstall_hook');
/**
* Executado quando o módulo é activado
*/
function meu_modulo_activation_hook()
{
$CI = &get_instance();
// Criar tabelas
if (!$CI->db->table_exists(db_prefix() . 'meu_modulo')) {
$CI->db->query('CREATE TABLE `' . db_prefix() . 'meu_modulo` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=' . $CI->db->char_set . ';');
}
// Adicionar opções
add_option('meu_modulo_version', '1.0.0');
}
/**
* Executado quando o módulo é desactivado
*/
function meu_modulo_deactivation_hook()
{
// Limpeza temporária (não apagar dados)
}
/**
* Executado quando o módulo é removido
*/
function meu_modulo_uninstall_hook()
{
$CI = &get_instance();
// Remover tabelas
$CI->db->query('DROP TABLE IF EXISTS `' . db_prefix() . 'meu_modulo`');
// Remover opções
delete_option('meu_modulo_version');
}
```
---
## File Headers (Meta-Informação)
### Headers Obrigatórios
| Header | Descrição |
|--------|-----------|
| `Module Name` | **ÚNICO OBRIGATÓRIO** - Nome do módulo |
### Headers Opcionais (Recomendados)
| Header | Descrição | Exemplo |
|--------|-----------|---------|
| `Description` | Descrição funcional | `CRM integration module` |
| `Version` | Versão semântica | `1.0.0` |
| `Requires at least` | Versão mínima Perfex | `2.3.*` |
| `Author` | Nome do autor | `Descomplicar` |
| `Author URI` | Website do autor | `https://descomplicar.pt` |
| `Module URI` | Página do módulo | `https://...` |
### Exemplo Completo
```php
/*
Module Name: Customer Portal
Description: Extended customer self-service portal
Version: 2.1.0
Requires at least: 2.3.*
Author: Descomplicar
Author URI: https://descomplicar.pt
Module URI: https://descomplicar.pt/modules/customer-portal
*/
```
---
## Aceder ao CodeIgniter
Fora de controllers/models, usar `get_instance()`:
```php
$CI = &get_instance();
// Carregar helper
$CI->load->helper('meu_modulo/meu_helper');
// Carregar library
$CI->load->library('meu_modulo/minha_library');
// Carregar model
$CI->load->model('meu_modulo/meu_modulo_model');
// Aceder à BD
$CI->db->get(db_prefix() . 'meu_modulo');
```
---
## Funções de Módulo Essenciais
### Paths e URLs
```php
// URL do módulo
$url = module_dir_url('meu_modulo');
// Resultado: https://crm.exemplo.pt/modules/meu_modulo/
// Path absoluto
$path = module_dir_path('meu_modulo');
// Resultado: /var/www/html/modules/meu_modulo/
// Path das libraries
$libs_path = module_libs_path('meu_modulo');
// Resultado: /var/www/html/modules/meu_modulo/libraries/
```
### Opções (Persistência)
```php
// Criar opção (não sobrescreve se existir)
add_option('meu_modulo_setting', 'valor', 0);
// Ler opção
$valor = get_option('meu_modulo_setting');
// Actualizar opção (cria se não existir - v2.3.3+)
update_option('meu_modulo_setting', 'novo_valor');
// Apagar opção
delete_option('meu_modulo_setting');
```
### Database Prefix
**SEMPRE usar `db_prefix()` em queries:**
```php
// CORRECTO
$CI->db->get(db_prefix() . 'meu_modulo
@@ -0,0 +1,208 @@
---
name: perfex-permissions
description: Sistema de permissões Perfex CRM — staff_can(), register_staff_capabilities(), controlo de acesso e roles. Baseado apenas na documentação oficial.
layer: wiki
---
# /perfex-permissions - Permissões Perfex CRM
Sistema de permissões e controlo de acesso. **Zero assumptions, zero hallucinations** - apenas documentação oficial.
---
## Documentação Base
- [Staff Capabilities and Access](https://help.perfexcrm.com/staff-capabilities-and-access/)
- [Roles](https://help.perfexcrm.com/roles/)
---
## Conceitos Fundamentais
| Conceito | Descrição |
|----------|-----------|
| **Feature** | Área funcional (invoices, projects, meu_modulo) |
| **Capability** | Permissão específica (view, create, edit, delete) |
| **Role** | Conjunto de permissões pré-definido |
---
## Registar Permissões do Módulo
### No Init File
```php
hooks()->add_action('admin_init', 'meu_modulo_register_permissions');
function meu_modulo_register_permissions()
{
$capabilities = [];
$capabilities['capabilities'] = [
'view' => _l('permission_view'), // Ver
'create' => _l('permission_create'), // Criar
'edit' => _l('permission_edit'), // Editar
'delete' => _l('permission_delete'), // Apagar
];
register_staff_capabilities(
'meu_modulo', // Feature ID (único)
$capabilities, // Array de capabilities
_l('meu_modulo_title') // Nome do módulo (mostrado em Settings)
);
}
```
### Parâmetros register_staff_capabilities()
| Parâmetro | Tipo | Descrição |
|-----------|------|-----------|
| `$feature` | string | ID único da feature |
| `$capabilities` | array | Array com key 'capabilities' |
| `$name` | string | Nome mostrado na UI |
---
## Verificar Permissões: staff_can()
### Sintaxe
```php
staff_can($capability, $feature = null, $staff_id = '');
```
### Parâmetros
| Parâmetro | Tipo | Descrição |
|-----------|------|-----------|
| `$capability` | string | Nome da capability (view, create, etc.) |
| `$feature` | string | Feature ID (recomendado SEMPRE passar) |
| `$staff_id` | int | ID do staff (default: staff logado) |
### Retorno
- `true` - Staff tem permissão
- `false` - Staff não tem permissão
**NOTA:** Administradores SEMPRE retornam `true` (bypass total).
---
## Exemplos de Uso
### No Controller
```php
class Meu_modulo extends AdminController
{
public function index()
{
// Verificar permissão de ver
if (!staff_can('view', 'meu_modulo')) {
access_denied('meu_modulo');
}
// ... código
}
public function create()
{
// Verificar permissão de criar
if (!staff_can('create', 'meu_modulo')) {
access_denied('meu_modulo');
}
// ... código
}
public function edit($id)
{
// Verificar permissão de editar
if (!staff_can('edit', 'meu_modulo')) {
access_denied('meu_modulo');
}
// ... código
}
public function delete($id)
{
// Verificar permissão de apagar
if (!staff_can('delete', 'meu_modulo')) {
access_denied('meu_modulo');
}
// ... código
}
}
```
### Na View
```php
<!-- Mostrar botão apenas se tem permissão -->
<?php if (staff_can('create', 'meu_modulo')): ?>
<a href="<?php echo admin_url('meu_modulo/create'); ?>" class="btn btn-primary">
<i class="fa fa-plus"></i> <?php echo _l('create'); ?>
</a>
<?php endif; ?>
<!-- Botões de acção condicionais -->
<td>
<?php if (staff_can('edit', 'meu_modulo')): ?>
<a href="<?php echo admin_url('meu_modulo/edit/' . $item->id); ?>"
class="btn btn-default btn-icon">
<i class="fa fa-pencil"></i>
</a>
<?php endif; ?>
<?php if (staff_can('delete', 'meu_modulo')): ?>
<a href="<?php echo admin_url('meu_modulo/delete/' . $item->id); ?>"
class="btn btn-danger btn-icon _delete">
<i class="fa fa-trash"></i>
</a>
<?php endif; ?>
</td>
```
### No Menu
```php
function meu_modulo_init_menu()
{
// Só mostrar menu se tem permissão
if (!staff_can('view', 'meu_modulo')) {
return;
}
$CI = &get_instance();
$CI->app_menu->add_sidebar_menu_item('meu-modulo', [
'name' => _l('meu_modulo'),
'href' => admin_url('meu_modulo'),
'position' => 25,
'icon' => 'fa fa-cube',
]);
}
```
---
## Capabilities Padrão
### Recomendadas para CRUD
| Capability | Descrição | Uso |
|------------|-----------|-----|
| `view` | Ver registos | Listar, ver detalhes |
| `create` | Criar novos | Formulário de criação |
| `edit` | Editar existentes | Formulário de edição |
| `delete` | Apagar registos | Botão de eliminar |
### Adicionar Capabilities Custom
```php
$capabilities['capabilities'] = [
// CRUD básico
'view' => _l('permission_view'),
'create' => _l('permission_create'),
'edit'
+240
View File
@@ -0,0 +1,240 @@
---
name: perfex-security
description: Segurança para módulos Perfex CRM — validação de input, CSRF, prevenção XSS e protecção contra directory traversal. Baseado apenas na documentação oficial.
layer: wiki
---
# /perfex-security - Segurança Módulos Perfex CRM
Práticas de segurança para módulos. **Zero assumptions, zero hallucinations** - apenas documentação oficial.
---
## Documentação Base
- [Module Security](https://help.perfexcrm.com/module-security/)
- [CodeIgniter Security](https://codeigniter.com/userguide3/libraries/security.html)
---
## 1. Prevenção de Acesso Directo
**OBRIGATÓRIO em todos os ficheiros PHP:**
```php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
// Resto do código
```
Isto previne execução directa via URL.
---
## 2. Prevenção de Directory Listing
**Incluir `index.html` vazio em todas as pastas:**
```
modules/meu_modulo/
├── index.html ← OBRIGATÓRIO
├── controllers/
│ └── index.html ← OBRIGATÓRIO
├── models/
│ └── index.html ← OBRIGATÓRIO
├── views/
│ └── index.html ← OBRIGATÓRIO
└── libraries/
└── index.html ← OBRIGATÓRIO
```
**Conteúdo do index.html:**
```html
<!DOCTYPE html>
<html>
<head>
<title>403 Forbidden</title>
</head>
<body>
<p>Directory access is forbidden.</p>
</body>
</html>
```
---
## 3. Input Validation (CRÍTICO)
### Usar Input Class do CodeIgniter
```php
// CORRECTO - Input class escapa automaticamente
$name = $this->input->post('name');
$id = $this->input->get('id');
$data = $this->input->post(); // Array de todos os POST
// ERRADO - Nunca usar $_POST/$_GET directamente
$name = $_POST['name']; // VULNERÁVEL!
```
### Sanitização Adicional
```php
// Inteiros
$id = (int) $this->input->post('id');
// Escape para output HTML
$name = html_escape($this->input->post('name'));
// Strip tags
$clean = strip_tags($this->input->post('content'));
// XSS clean (built-in)
$safe = $this->security->xss_clean($this->input->post('data'));
```
---
## 4. CSRF Protection
O Perfex CRM tem CSRF activado por defeito.
### Em Formulários (OBRIGATÓRIO)
```php
<?php echo form_open(admin_url('meu_modulo/save')); ?>
<!-- form_open() gera token CSRF automaticamente -->
<input type="text" name="name">
<button type="submit">Guardar</button>
<?php echo form_close(); ?>
```
**NUNCA usar HTML `<form>` directo:**
```html
<!-- ERRADO - Sem CSRF token -->
<form action="..." method="post">
```
### Em AJAX (jQuery)
jQuery do Perfex inclui CSRF automaticamente. Para outras bibliotecas:
```javascript
// Obter token CSRF
var csrfName = '<?php echo $this->security->get_csrf_token_name(); ?>';
var csrfHash = '<?php echo $this->security->get_csrf_hash(); ?>';
// Incluir em requests
$.ajax({
url: admin_url + 'meu_modulo/ajax_save',
type: 'POST',
data: {
[csrfName]: csrfHash,
name: $('#name').val()
}
});
```
### Excluir URLs de CSRF (v2.9.0+)
Para webhooks que não podem incluir CSRF:
```php
// modules/meu_modulo/config/csrf_exclude_uris.php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
return [
'meu_modulo/webhook',
'meu_modulo/api/.*', // Regex suportado
];
```
---
## 5. SQL Injection Prevention
### Usar Query Builder (SEMPRE)
```php
// CORRECTO - Query Builder escapa parâmetros
$this->db->where('id', $id);
$this->db->where('status', $status);
$result = $this->db->get(db_prefix() . 'meu_modulo')->result();
// CORRECTO - Binding de parâmetros
$sql = "SELECT * FROM " . db_prefix() . "meu_modulo WHERE id = ? AND status = ?";
$result = $this->db->query($sql, [$id, $status])->result();
```
### NUNCA Concatenar Input
```php
// ERRADO - SQL Injection vulnerável!
$id = $_GET['id'];
$sql = "SELECT * FROM tblmeu_modulo WHERE id = " . $id;
// ERRADO - Mesmo com input class
$id = $this->input->get('id');
$sql = "SELECT * FROM tblmeu_modulo WHERE id = " . $id; // VULNERÁVEL!
```
---
## 6. XSS Prevention
### Em Views (SEMPRE)
```php
<!-- CORRECTO - Escape HTML -->
<td><?php echo html_escape($item->name); ?></td>
<input value="<?php echo html_escape($item->value); ?>">
<!-- ERRADO - XSS vulnerável -->
<td><?php echo $item->name; ?></td>
```
### Função html_escape()
```php
// Texto simples
echo html_escape($string);
// Para atributos HTML
echo html_escape($string, true);
```
---
## 7. File Upload Security
```php
public function upload()
{
// Configurar upload
$config['upload_path'] = './uploads/meu_modulo/';
$config['allowed_types'] = 'pdf|doc|docx|xls|xlsx'; // NUNCA permitir php!
$config['max_size'] = 2048; // KB
$config['encrypt_name'] = true; // Nome aleatório
$this->load->library('upload', $config);
if (!$this->upload->do_upload('ficheiro')) {
// Erro
$error = $this->upload->display_errors();
set_alert('danger', $error);
} else {
// Sucesso
$data = $this->upload->data();
$filename = $data['file_name'];
}
}
```
### Tipos Permitidos
+196
View File
@@ -0,0 +1,196 @@
---
name: perfex-views
description: Views e templates para módulos Perfex CRM — init_head(), init_tail(), views admin e cliente, passagem de dados. Baseado apenas na documentação oficial.
layer: wiki
---
# /perfex-views - Views Perfex CRM
Sistema de views e templates para módulos. **Zero assumptions, zero hallucinations** - apenas documentação oficial.
---
## Documentação Base
- [Module Views](https://help.perfexcrm.com/module-views/)
- [CodeIgniter Views](https://codeigniter.com/userguide3/general/views.html)
---
## Estrutura de Pastas
```
modules/meu_modulo/
└── views/
├── index.php # Lista principal
├── form.php # Formulário create/edit
├── view.php # Detalhe
├── client_index.php # Lista cliente
├── client_view.php # Detalhe cliente
├── partials/
│ ├── _table.php # Partial tabela
│ └── _filters.php # Partial filtros
└── widgets/
└── dashboard.php # Widget dashboard
```
---
## Views Admin (Área Staff)
### Template Base Admin
```php
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<?php init_head(); ?>
<div id="wrapper">
<div class="content">
<div class="row">
<div class="col-md-12">
<div class="panel_s">
<div class="panel-body">
<!-- CONTEÚDO AQUI -->
<h4><?php echo _l('meu_modulo_title'); ?></h4>
</div>
</div>
</div>
</div>
</div>
</div>
<?php init_tail(); ?>
</body>
</html>
```
### Funções Essenciais
| Função | Descrição |
|--------|-----------|
| `init_head()` | Inclui header, sidebar, CSS, meta tags |
| `init_tail()` | Inclui footer, JavaScript, fecha documento |
---
## Carregar Views
### No Controller
```php
// Básico
$this->load->view('form');
// Com dados
$data = [
'item' => $item,
'clients' => $clients,
];
$this->load->view('form', $data);
// Retornar como string (não fazer echo)
$html = $this->load->view('partial', $data, true);
```
### Fora de Controllers (Helpers, Classes)
```php
function meu_modulo_render_widget()
{
$CI = &get_instance();
$CI->load->view('meu_modulo/widgets/dashboard');
}
// Com dados
function meu_modulo_render_table($items)
{
$CI = &get_instance();
$CI->load->view('meu_modulo/partials/_table', ['items' => $items]);
}
```
---
## Views Cliente (Área Cliente)
### No Controller (ClientsController)
```php
public function index()
{
$this->data([
'items' => $items,
'title' => 'Meu Módulo',
]);
$this->title(_l('meu_modulo_title'));
$this->view('client_index');
$this->layout(); // Aplica tema do cliente
}
```
### Template Base Cliente
```php
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<div class="panel_s">
<div class="panel-body">
<h4 class="no-margin"><?php echo _l('meu_modulo_title'); ?></h4>
<hr class="hr-panel-heading" />
<!-- CONTEÚDO -->
<?php foreach ($items as $item): ?>
<p><?php echo html_escape($item->name); ?></p>
<?php endforeach; ?>
</div>
</div>
```
---
## Exemplos Completos
### Lista com Tabela (Admin)
```php
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<?php init_head(); ?>
<div id="wrapper">
<div class="content">
<div class="row">
<div class="col-md-12">
<div class="panel_s">
<div class="panel-body">
<div class="_buttons">
<?php if (staff_can('create', 'meu_modulo')): ?>
<a href="<?php echo admin_url('meu_modulo/create'); ?>"
class="btn btn-primary pull-left">
<i class="fa fa-plus"></i>
<?php echo _l('new_item'); ?>
</a>
<?php endif; ?>
</div>
<div class="clearfix"></div>
<hr class="hr-panel-heading" />
<table class="table table-striped dt-table">
<thead>
<tr>
<th><?php echo _l('id'); ?></th>
<th><?php echo _l('name'); ?></th>
<th><?php echo _l('created_at'); ?></th>
<th><?php echo _l('options'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($items as $item): ?>
<tr>
<td><?php echo $item->id; ?></td>
<td><?php echo html_escape($item->name); ?></td>