feat(perfex-dev): adiciona plugin perfex-dev (agents, skills, knowledge e metadados do projeto)
This commit is contained in:
@@ -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)
|
||||
@@ -0,0 +1,5 @@
|
||||
# CHANGELOG — skills
|
||||
|
||||
## 0.1.0 — 2026-06-30
|
||||
### Adicionado
|
||||
- Estrutura padrão (PROC-DEV-STANDARD)
|
||||
@@ -0,0 +1,4 @@
|
||||
# MEMORY.md — skills
|
||||
|
||||
## 2026-06-30
|
||||
- Estrutura padrão aplicada via propagate-structure.sh
|
||||
@@ -0,0 +1,3 @@
|
||||
# skills
|
||||
|
||||
Subprojecto skills do ecossistema Descomplicar.
|
||||
@@ -0,0 +1,8 @@
|
||||
# STATUS.md — skills
|
||||
|
||||
**Versão:** 0.1 | **Data:** 2026-06-30 | **Agente:** —
|
||||
|
||||
## Estado actual
|
||||
- Em curso: —
|
||||
- Bloqueios: —
|
||||
- Próximos passos: —
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"task_id": null,
|
||||
"project_id": null,
|
||||
"customer_id": null,
|
||||
"gitea_repo": null
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# skills
|
||||
|
||||
## Documentação
|
||||
|
||||
- [README](README.md)
|
||||
- [AGENTS](AGENTS.md)
|
||||
- [STATUS](STATUS.md)
|
||||
- [MEMORY](MEMORY.md)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
```
|
||||
@@ -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_
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user