feat: adiciona 12 plugins Descomplicar ao marketplace

Plugins: automacao, crm-ops, design-media, dev-tools, gestao,
infraestrutura, marketing, negocio, perfex-dev, project-manager,
wordpress + hello-plugin (existente).

Totais: 83 skills, 44 agents, 12 datasets.json

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 21:41:24 +00:00
parent bcce928beb
commit 2cb3210962
209 changed files with 50869 additions and 0 deletions

View File

@@ -0,0 +1,421 @@
---
name: perfex-controllers
description: Perfex CRM module controllers. AdminController, ClientsController, routing, ValidatesContact trait. Based on official documentation only. Use when user mentions "perfex controller", "AdminController", "ClientsController", "routing perfex".
author: Descomplicar® Crescimento Digital
version: 1.0.0
quality_score: 70
user_invocable: true
desk_task: null
---
# /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_modulo_model->get($id);
// Verificar se pertence ao cliente
if (!$item || $item->client_id != get_client_user_id()) {
show_404();
}
$this->data(['item' => $item]);
$this->title($item->name);
$this->view('meu_modulo/client_view');
$this->layout();
}
}
```
---
## ValidatesContact Trait (v2.3.3+)
Para exigir contacto autenticado com email verificado.
```php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
use app\services\ValidatesContact;
class Meu_modulo extends ClientsController
{
use ValidatesContact;
public function __construct()
{
parent::__construct();
// Todos os métodos requerem contacto validado
$this->validateContactInController();
}
public function index()
{
// Só executa se contacto logado E email verificado
// ...
}
}
```
---
## URLs e Routing
### Admin Controllers
| URL | Controller | Método |
|-----|------------|--------|
| `/admin/meu_modulo` | Meu_modulo | index() |
| `/admin/meu_modulo/create` | Meu_modulo | create() |
| `/admin/meu_modulo/edit/123` | Meu_modulo | edit(123) |
| `/admin/meu_modulo/delete/123` | Meu_modulo | delete(123) |
### Client Controllers
| URL | Controller | Método |
|-----|------------|--------|
| `/meu_modulo` | Meu_modulo | index() |
| `/meu_modulo/view/123` | Meu_modulo | view(123) |
### Quando Nome Controller = Nome Módulo
Se o controller tem o mesmo nome do módulo, URLs simplificadas:
```
/admin/meu_modulo → modules/meu_modulo/controllers/Meu_modulo.php::index()
/meu_modulo → modules/meu_modulo/controllers/Meu_modulo.php::index()
```
### Controllers com Nomes Diferentes
```
/admin/meu_modulo/outro_controller/metodo
→ modules/meu_modulo/controllers/Outro_controller.php::metodo()
```
---
## Funções Helper Úteis
### Admin URLs
```php
// Gerar URL admin
admin_url('meu_modulo'); // /admin/meu_modulo
admin_url('meu_modulo/edit/123'); // /admin/meu_modulo/edit/123
// Redirecionar
redirect(admin_url('meu_modulo'));
```
### Site URLs
```php
// Gerar URL site/cliente
site_url('meu_modulo'); // /meu_modulo
site_url('meu_modulo/view/123'); // /meu_modulo/view/123
```
### Alertas
```php
// Definir alerta (mostra na próxima página)
set_alert('success', 'Operação concluída!');
set_alert('warning', 'Atenção!');
set_alert('danger', 'Erro!');
```
### Verificações de Login
```php
// Staff
is_staff_logged_in(); // bool
get_staff_user_id(); // int
// Cliente/Contacto
is_client_logged_in(); // bool
get_client_user_id(); // int (customer_id)
get_contact_user_id(); // int (contact_id)
```
### Acesso Negado
```php
// Redireciona com mensagem de acesso negado
access_denied('meu_modulo');
```
---
## AJAX Responses
### Retornar JSON
```php
public function ajax_get_item($id)
{
if (!$this->input->is_ajax_request()) {
show_404();
}
$item = $this->meu_modulo_model->get($id);
echo json_encode([
'success' => (bool) $item,
'data' => $item,
]);
}
```
### Com Validação CSRF
```php
public function ajax_save()
{
if (!$this->input->is_ajax_request()) {
show_404();
}
// CSRF validado automaticamente se usar jQuery
$data = $this->input->post();
$id = $this->meu_modulo_model->add($data);
echo json_encode([
'success' => (bool) $id,
'id' => $id,
'message' => $id ? _l('added_successfully') : _l('error_occurred'),
]);
}
```
---
## Anti-Patterns (NUNCA FAZER)
| Anti-Pattern | Risco | Alternativa |
|--------------|-------|-------------|
| Controller sem verificar permissões | Acesso não autorizado | `staff_can()` sempre |
| Aceder dados de outros clientes | Data breach | Validar ownership |
| Nome ficheiro lowercase | Não carrega | Primeira letra MAIÚSCULA |
| echo em controllers admin | Output malformado | Usar views |
| Não validar AJAX requests | CSRF vulnerável | `is_ajax_request()` |
---
## Checklist Controller
```
1. [ ] defined('BASEPATH') no topo
2. [ ] Extends classe correcta (Admin/Clients/App_Controller)
3. [ ] Nome ficheiro com primeira letra maiúscula
4. [ ] Permissões verificadas em cada método
5. [ ] Ownership validado para dados de clientes
6. [ ] Inputs via $this->input->post/get
7. [ ] Alertas definidos antes de redirect
8. [ ] AJAX methods verificam is_ajax_request()
```
---
**Versão:** 1.0.0 | **Autor:** Descomplicar®
**Fonte:** help.perfexcrm.com/module-controllers

View File

@@ -0,0 +1,469 @@
---
name: perfex-database
description: Perfex CRM database operations. db_prefix(), options, queries, Active Record. Based on official documentation only. Use when user mentions "perfex database", "db_prefix", "query perfex", "options perfex".
author: Descomplicar® Crescimento Digital
version: 1.0.0
quality_score: 70
user_invocable: true
desk_task: null
---
# /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_option('meu_modulo_enabled', '0');
update_option('meu_modulo_last_sync', date('Y-m-d H:i:s'));
```
### delete_option()
Remove opção.
```php
delete_option('meu_modulo_enabled');
```
### Boas Práticas Opções
```php
// SEMPRE prefixar com nome do módulo
add_option('meu_modulo_version', '1.0.0');
add_option('meu_modulo_api_key', '');
add_option('meu_modulo_enabled', '1');
// NUNCA usar nomes genéricos
add_option('enabled', '1'); // ERRADO
add_option('api_key', '...'); // ERRADO
```
---
## Model Exemplo Completo
```php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class Meu_modulo_model extends App_Model
{
private $table;
public function __construct()
{
parent::__construct();
$this->table = db_prefix() . 'meu_modulo';
}
/**
* Obter todos os registos
*/
public function get_all($where = [])
{
if (!empty($where)) {
$this->db->where($where);
}
$this->db->order_by('created_at', 'DESC');
return $this->db->get($this->table)->result();
}
/**
* Obter por ID
*/
public function get($id)
{
$this->db->where('id', $id);
return $this->db->get($this->table)->row();
}
/**
* Obter por cliente
*/
public function get_by_client($client_id)
{
$this->db->where('client_id', $client_id);
$this->db->order_by('created_at', 'DESC');
return $this->db->get($this->table)->result();
}
/**
* Criar novo registo
*/
public function add($data)
{
$data['created_at'] = date('Y-m-d H:i:s');
// Trigger hook antes
$data = hooks()->apply_filters('before_meu_modulo_added', $data);
$this->db->insert($this->table, $data);
$insert_id = $this->db->insert_id();
if ($insert_id) {
// Trigger hook após
hooks()->do_action('after_meu_modulo_added', $insert_id);
log_activity('Meu Módulo criado [ID: ' . $insert_id . ']');
}
return $insert_id;
}
/**
* Actualizar registo
*/
public function update($id, $data)
{
$data['updated_at'] = date('Y-m-d H:i:s');
// Trigger hook antes
$data = hooks()->apply_filters('before_meu_modulo_updated', $data, $id);
$this->db->where('id', $id);
$this->db->update($this->table, $data);
if ($this->db->affected_rows() > 0) {
// Trigger hook após
hooks()->do_action('after_meu_modulo_updated', $id);
log_activity('Meu Módulo actualizado [ID: ' . $id . ']');
return true;
}
return false;
}
/**
* Apagar registo
*/
public function delete($id)
{
// Trigger hook antes
hooks()->do_action('before_meu_modulo_deleted', $id);
$this->db->where('id', $id);
$this->db->delete($this->table);
if ($this->db->affected_rows() > 0) {
// Trigger hook após
hooks()->do_action('after_meu_modulo_deleted', $id);
log_activity('Meu Módulo apagado [ID: ' . $id . ']');
return true;
}
return false;
}
/**
* Contar por status
*/
public function count_by_status($status)
{
$this->db->where('status', $status);
return $this->db->count_all_results($this->table);
}
/**
* Pesquisa
*/
public function search($term)
{
$this->db->group_start();
$this->db->like('name', $term);
$this->db->or_like('description', $term);
$this->db->group_end();
return $this->db->get($this->table)->result();
}
}
```
---
## Transações
Para operações que devem ser atómicas:
```php
$this->db->trans_begin();
try {
$this->db->insert(db_prefix() . 'meu_modulo', $data1);
$id = $this->db->insert_id();
$this->db->insert(db_prefix() . 'meu_modulo_items', [
'parent_id' => $id,
'item' => $item,
]);
if ($this->db->trans_status() === false) {
$this->db->trans_rollback();
return false;
}
$this->db->trans_commit();
return $id;
} catch (Exception $e) {
$this->db->trans_rollback();
log_activity('Erro transação: ' . $e->getMessage());
return false;
}
```
---
## Funções Úteis
### log_activity()
Registar actividade no sistema.
```php
log_activity('Descrição da actividade');
log_activity('Item criado [ID: ' . $id . ', Cliente: ' . $client_id . ']');
```
### Datas
```php
// Data actual formatada
$now = date('Y-m-d H:i:s');
// Formatar para display
$formatted = _d($row->created_at); // Data
$formatted = _dt($row->created_at); // Data e hora
```
---
## Anti-Patterns (NUNCA FAZER)
| Anti-Pattern | Risco | Alternativa |
|--------------|-------|-------------|
| Query sem db_prefix() | Falha em instalações custom | `db_prefix()` sempre |
| SQL directo com input user | SQL Injection | Query Builder |
| Opções sem prefixo módulo | Conflitos | Prefixar sempre |
| Não usar transações em multi-insert | Dados inconsistentes | `trans_begin/commit` |
| Hardcode charset | Problemas encoding | `$CI->db->char_set` |
---
## Checklist Database
```
1. [ ] db_prefix() em TODAS as queries
2. [ ] $CI->db->char_set na criação de tabelas
3. [ ] Opções com prefixo do módulo
4. [ ] Query Builder para inputs de utilizador
5. [ ] Transações para operações múltiplas
6. [ ] log_activity() para auditoria
7. [ ] Hooks em operações CRUD
8. [ ] Índices em colunas de pesquisa
```
---
**Versão:** 1.0.0 | **Autor:** Descomplicar®
**Fonte:** help.perfexcrm.com/module-basics

View File

@@ -0,0 +1,562 @@
---
name: perfex-forms
description: Perfex CRM forms handling. form_open(), CSRF tokens, validation, AJAX. Based on official documentation only. Use when user mentions "perfex form", "form_open", "csrf token", "validation perfex".
author: Descomplicar® Crescimento Digital
version: 1.0.0
quality_score: 70
user_invocable: true
desk_task: null
---
# /perfex-forms - Formulários Perfex CRM
Gestão de formulários em módulos. **Zero assumptions, zero hallucinations** - apenas documentação oficial.
---
## Documentação 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:**
```html
<!-- ERRADO - Sem CSRF protection -->
<form action="..." method="post">
```
---
## Sintaxe form_open()
```php
// Básico
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 formulário
echo form_close();
```
---
## Elementos de Formulário
### 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; ?>">
```
---
## Validação 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 Validação
| Regra | Descrição |
|-------|-----------|
| `required: true` | Campo obrigatório |
| `email: true` | Email válido |
| `number: true` | Número |
| `digits: true` | Apenas dígitos |
| `minlength: N` | Mínimo N caracteres |
| `maxlength: N` | Máximo N caracteres |
| `min: N` | Valor mínimo |
| `max: N` | Valor máximo |
| `equalTo: '#field'` | Igual a outro campo |
---
## Validação Server-Side
```php
public function save()
{
if (!$this->input->post()) {
redirect(admin_url('meu_modulo'));
}
// Validação
$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) {
// Erro de validação
set_alert('danger', validation_errors());
redirect(admin_url('meu_modulo/create'));
}
// Dados válidos, processar
$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);
}
});
```
### Com $.ajax()
```javascript
$.ajax({
url: admin_url + 'meu_modulo/ajax_save',
type: 'POST',
data: $('#form-meu-modulo').serialize(),
dataType: 'json',
success: function(response) {
if (response.success) {
alert_float('success', response.message);
// Redirecionar ou actualizar UI
} else {
alert_float('danger', response.message);
}
},
error: function() {
alert_float('danger', 'Erro de comunicação');
}
});
```
### 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
];
```
---
## Formulário Completo Exemplo
### 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>
<!-- Descrição -->
<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>
<!-- Botões -->
<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(){
// Validação
appValidateForm($('#form-meu-modulo'), {
name: {
required: true,
minlength: 3
},
amount: {
number: true,
min: 0
}
});
// Inicializar datepicker (já auto via classe)
// Inicializar selectpicker (já auto via classe)
});
</script>
</body>
</html>
```
---
## Anti-Patterns (NUNCA FAZER)
| Anti-Pattern | Risco | Alternativa |
|--------------|-------|-------------|
| `<form>` HTML directo | CSRF bypass | `form_open()` |
| Valores sem `html_escape()` | XSS | Escape sempre |
| Validação só client-side | Bypass | Validar server também |
| AJAX sem verificar `is_ajax_request()` | Acesso directo | Verificar sempre |
---
## Checklist Formulários
```
1. [ ] form_open() para todos os formulários
2. [ ] html_escape() em todos os values
3. [ ] Validação client-side (appValidateForm)
4. [ ] Validação server-side (form_validation)
5. [ ] Permissões verificadas no controller
6. [ ] CSRF excluído apenas para webhooks
7. [ ] form_open_multipart() para uploads
8. [ ] is_ajax_request() em endpoints AJAX
```
---
**Versão:** 1.0.0 | **Autor:** Descomplicar®
**Fonte:** help.perfexcrm.com/working-with-forms

View File

@@ -0,0 +1,380 @@
---
name: perfex-hooks
description: Perfex CRM hooks system. Actions, filters, add_action, add_filter, do_action, apply_filters. Complete hook list by category. Based on official documentation only. Use when user mentions "perfex hooks", "action hook", "filter perfex", "extend perfex".
author: Descomplicar® Crescimento Digital
version: 1.0.0
quality_score: 70
user_invocable: true
desk_task: null
---
# /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_sent` | Orçamento enviado | $estimate_id |
| `estimate_accepted` | Aceite pelo cliente | $estimate_id |
| `estimate_declined` | Rejeitado | $estimate_id |
| `estimate_converted_to_invoice` | Convertido em factura | $estimate_id, $invoice_id |
### Propostas
| Hook | Trigger | Argumentos |
|------|---------|------------|
| `proposal_created` | Proposta criada | $proposal_id |
| `after_proposal_updated` | Após actualizar | $proposal_id |
| `proposal_sent` | Proposta enviada | $proposal_id |
| `proposal_accepted` | Aceite | $proposal_id |
| `proposal_declined` | Rejeitada | $proposal_id |
| `proposal_converted_to_invoice` | → Factura | $proposal_id |
| `proposal_converted_to_estimate` | → Orçamento | $proposal_id |
### Leads
| Hook | Trigger | Argumentos |
|------|---------|------------|
| `lead_created` | Lead criado | $lead_id |
| `before_lead_deleted` | Antes de apagar | $lead_id |
| `lead_marked_as_lost` | Marcado perdido | $lead_id |
| `lead_marked_as_junk` | Marcado spam | $lead_id |
| `lead_converted_to_customer` | Convertido | $lead_id, $customer_id |
### Tarefas
| Hook | Trigger | Argumentos |
|------|---------|------------|
| `after_add_task` | Tarefa criada | $task_id |
| `after_update_task` | Tarefa actualizada | $task_id |
| `task_status_changed` | Status alterado | $task_id, $status |
| `task_deleted` | Tarefa apagada | $task_id |
| `task_timer_started` | Timer iniciado | $task_id, $staff_id |
| `task_timer_deleted` | Timer apagado | $timer_id |
| `task_comment_added` | Comentário adicionado | $comment_id, $task_id |
| `task_checklist_item_created` | Checklist item criado | $item_id |
| `task_checklist_item_finished` | Checklist item concluído | $item_id |
### Projectos
| Hook | Trigger | Argumentos |
|------|---------|------------|
| `after_add_project` | Projecto criado | $project_id |
| `after_update_project` | Projecto actualizado | $project_id |
| `before_remove_project_file` | Antes remover ficheiro | $file_id |
### Staff
| Hook | Trigger | Argumentos |
|------|---------|------------|
| `staff_member_created` | Staff criado | $staff_id |
| `staff_member_updated` | Staff actualizado | $staff_id |
| `staff_member_profile_updated` | Perfil actualizado | $staff_id |
### Pagamentos
| Hook | Trigger | Argumentos |
|------|---------|------------|
| `after_payment_added` | Pagamento registado | $payment_id |
| `customer_subscribed_to_subscription` | Subscrição activada | $subscription_id |
| `credit_note_refund_created` | Reembolso criado | $refund_id |
### Contratos
| Hook | Trigger | Argumentos |
|------|---------|------------|
| `after_contract_added` | Contrato criado | $contract_id |
| `after_contract_updated` | Contrato actualizado | $contract_id |
| `before_contract_deleted` | Antes de apagar | $contract_id |
| `contract_html_viewed` | Contrato visualizado | $contract_id |
### Tickets
| Hook | Trigger | Argumentos |
|------|---------|------------|
| `ticket_created` | Ticket criado | $ticket_id |
| `before_ticket_deleted` | Antes de apagar | $ticket_id |
| `before_delete_ticket_reply` | Antes apagar resposta | $reply_id |
### Módulos
| Hook | Trigger | Argumentos |
|------|---------|------------|
| `modules_loaded` | Módulos carregados | - |
| `pre_activate_module` | Antes de activar | $module_name |
| `module_activated` | Módulo activado | $module_name |
| `pre_deactivate_module` | Antes de desactivar | $module_name |
| `module_deactivated` | Módulo desactivado | $module_name |
| `module_installed` | Módulo instalado | $module_name |
| `pre_uninstall_module` | Antes de desinstalar | $module_name |
| `module_uninstalled` | Módulo desinstalado | $module_name |
### UI / Templates
| Hook | Trigger | Argumentos |
|------|---------|------------|
| `app_admin_head` | Header admin | - |
| `app_admin_footer` | Footer admin | - |
| `before_start_render_dashboard_content` | Antes dashboard | - |
| `after_dashboard` | Após dashboard | - |
| `before_render_aside_menu` | Antes menu lateral | - |
| `after_render_aside_menu` | Após menu lateral | - |
---
## Exemplos Práticos
### 1. Integração Externa Após Cliente Criado
```php
hooks()->add_action('after_client_added', 'sync_to_external_crm');
function sync_to_external_crm($clientId)
{
$CI = &get_instance();
$CI->load->model('clients_model');
$client = $CI->clients_model->get($clientId);
// Enviar para API externa
$response = wp_remote_post('https://api.externa.com/customers', [
'body' => json_encode([
'name' => $client->company,
'email' => $client->email,
'vat' => $client->vat,
])
]);
log_activity('Cliente sincronizado: ' . $clientId);
}
```
### 2. Validar Dados Antes de Criar Factura
```php
hooks()->add_filter('before_invoice_added', 'validate_invoice_data');
function validate_invoice_data($data)
{
// Forçar data de vencimento mínima 30 dias
if (isset($data['duedate'])) {
$due = strtotime($data['duedate']);
$min = strtotime('+30 days');
if ($due < $min) {
$data['duedate'] = date('Y-m-d', $min);
}
}
return $data; // OBRIGATÓRIO retornar
}
```
### 3. Adicionar Conteúdo ao Dashboard
```php
hooks()->add_action('before_start_render_dashboard_content', 'add_custom_widget');
function add_custom_widget()
{
$CI = &get_instance();
$CI->load->view('meu_modulo/dashboard_widget');
}
```
---
## Anti-Patterns (NUNCA FAZER)
| Anti-Pattern | Risco | Alternativa |
|--------------|-------|-------------|
| Não retornar valor em filter | Dados perdidos | SEMPRE retornar |
| Modificar core em vez de hooks | Quebra em updates | Usar hooks |
| Hooks sem prefixo único | Conflitos | Prefixar callbacks |
| Ignorar prioridade | Ordem errada | Definir prioridade |
| Exceptions não tratadas em hooks | Quebra sistema | Try/catch |
---
## Checklist Implementação de Hook
```
1. [ ] Hook correcto identificado (action vs filter)
2. [ ] Callback com prefixo único
3. [ ] Número de argumentos correcto
4. [ ] Filter retorna dados (OBRIGATÓRIO)
5. [ ] Try/catch para operações externas
6. [ ] Testado em desenvolvimento
7. [ ] Log de actividade para debug
```
---
**Versão:** 1.0.0 | **Autor:** Descomplicar®
**Fonte:** help.perfexcrm.com/action-hooks

View File

@@ -0,0 +1,400 @@
---
name: perfex-menus
description: Perfex CRM menu creation. Sidebar menus, client menus, add_sidebar_menu_item(), positions, icons. Based on official documentation only. Use when user mentions "perfex menu", "sidebar perfex", "menu item", "navigation perfex".
author: Descomplicar® Crescimento Digital
version: 1.0.0
quality_score: 70
user_invocable: true
desk_task: null
---
# /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
$CI->app_menu->add_setup_menu_item('meu-modulo-settings', [
'name' => _l('meu_modulo_settings'),
'href' => admin_url('meu_modulo/settings'),
'position' => 50,
'icon' => 'fa fa-cog',
]);
}
```
---
## Ícones Font Awesome 4
### Mais Usados
| Ícone | Classe |
|-------|--------|
| Lista | `fa fa-list` |
| Adicionar | `fa fa-plus` |
| Editar | `fa fa-edit` / `fa fa-pencil` |
| Apagar | `fa fa-trash` |
| Configurações | `fa fa-cog` / `fa fa-cogs` |
| Utilizador | `fa fa-user` |
| Utilizadores | `fa fa-users` |
| Ficheiro | `fa fa-file` / `fa fa-file-o` |
| Pasta | `fa fa-folder` / `fa fa-folder-o` |
| Email | `fa fa-envelope` / `fa fa-envelope-o` |
| Calendário | `fa fa-calendar` |
| Gráfico | `fa fa-bar-chart` / `fa fa-line-chart` |
| Dinheiro | `fa fa-money` |
| Cubo | `fa fa-cube` |
| Sino | `fa fa-bell` |
| Check | `fa fa-check` |
| Warning | `fa fa-exclamation-triangle` |
| Info | `fa fa-info-circle` |
### Referência Completa
[Font Awesome 4.7 Icons](https://fontawesome.com/v4/icons/)
---
## Exemplos Completos
### Módulo com Menu Completo
```php
// modules/meu_modulo/meu_modulo.php
hooks()->add_action('admin_init', 'meu_modulo_init_menus');
hooks()->add_action('clients_init', 'meu_modulo_client_menu');
/**
* Menus Admin
*/
function meu_modulo_init_menus()
{
if (!staff_can('view', 'meu_modulo')) {
return;
}
$CI = &get_instance();
// Menu principal com sub-items
$CI->app_menu->add_sidebar_menu_item('meu-modulo', [
'name' => _l('meu_modulo'),
'collapse' => true,
'position' => 27,
'icon' => 'fa fa-cube',
'badge' => [
'value' => meu_modulo_count_pending(),
'type' => 'warning',
'show_on_sidebar' => true,
],
]);
// Sub-items
$CI->app_menu->add_sidebar_children_item('meu-modulo', [
'slug' => 'meu-modulo-dashboard',
'name' => _l('dashboard'),
'href' => admin_url('meu_modulo'),
'position' => 1,
'icon' => 'fa fa-tachometer',
]);
$CI->app_menu->add_sidebar_children_item('meu-modulo', [
'slug' => 'meu-modulo-items',
'name' => _l('items'),
'href' => admin_url('meu_modulo/items'),
'position' => 2,
'icon' => 'fa fa-list',
]);
if (staff_can('create', 'meu_modulo')) {
$CI->app_menu->add_sidebar_children_item('meu-modulo', [
'slug' => 'meu-modulo-create',
'name' => _l('create'),
'href' => admin_url('meu_modulo/create'),
'position' => 3,
'icon' => 'fa fa-plus',
]);
}
// Settings no Setup
if (is_admin()) {
$CI->app_menu->add_setup_menu_item('meu-modulo-settings', [
'name' => _l('meu_modulo_settings'),
'href' => admin_url('meu_modulo/settings'),
'position' => 80,
'icon' => 'fa fa-cog',
]);
}
}
/**
* Menu Cliente
*/
function meu_modulo_client_menu()
{
if (!is_client_logged_in()) {
return;
}
add_theme_menu_item('meu-modulo', [
'name' => _l('meu_modulo'),
'href' => site_url('meu_modulo'),
'position' => 32,
'icon' => 'fa fa-cube',
]);
}
/**
* Contador para badge
*/
function meu_modulo_count_pending()
{
$CI = &get_instance();
$CI->db->where('status', 'pending');
return $CI->db->count_all_results(db_prefix() . 'meu_modulo');
}
```
---
## Anti-Patterns (NUNCA FAZER)
| Anti-Pattern | Risco | Alternativa |
|--------------|-------|-------------|
| Menu sem verificar permissões | Acesso não autorizado | `staff_can()` |
| Posição fixa sem considerar conflitos | Sobreposição | Posições intermédias |
| Hardcode nome do menu | Não traduzível | Usar `_l()` |
| ID duplicado | Menu não aparece | IDs únicos com prefixo |
| Badge com query pesada | Performance | Cache ou contagem eficiente |
---
## Checklist Menu
```
1. [ ] Hook correcto (admin_init / clients_init)
2. [ ] ID único com prefixo do módulo
3. [ ] Permissões verificadas
4. [ ] _l() para nome do menu
5. [ ] Posição apropriada (não conflitar)
6. [ ] Ícone Font Awesome 4 válido
7. [ ] Badge eficiente (se usado)
8. [ ] Sub-items com slugs únicos
```
---
**Versão:** 1.0.0 | **Autor:** Descomplicar®
**Fonte:** help.perfexcrm.com/create-menu-items

View File

@@ -0,0 +1,426 @@
---
name: perfex-migrations
description: Perfex CRM module migrations and upgrades. Version management, migration files, database updates. Based on official documentation only. Use when user mentions "perfex migration", "module upgrade", "version perfex", "database migration".
author: Descomplicar® Crescimento Digital
version: 1.0.0
quality_score: 70
user_invocable: true
desk_task: null
---
# /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
$CI->db->query('UPDATE `' . db_prefix() . 'meu_modulo`
SET `status` = "active"
WHERE `status` = "1"');
$CI->db->query('UPDATE `' . db_prefix() . 'meu_modulo`
SET `status` = "inactive"
WHERE `status` = "0"');
}
}
```
### Adicionar Opção
```php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class Migration_Version_160 extends App_module_migration
{
public function up()
{
// Adicionar nova opção (add_option não sobrescreve)
add_option('meu_modulo_new_feature', '1');
add_option('meu_modulo_default_status', 'pending');
}
}
```
### Migration Vazia (Só actualiza versão)
```php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class Migration_Version_170 extends App_module_migration
{
public function up()
{
// Sem alterações de BD nesta versão
// O Perfex apenas actualiza o número da versão
}
}
```
---
## Sincronizar Activation Hook
**CRÍTICO:** O activation hook deve SEMPRE conter o schema mais recente.
```php
// modules/meu_modulo/meu_modulo.php
function meu_modulo_activation_hook()
{
$CI = &get_instance();
// Schema completo e actualizado (v1.7.0)
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,
`new_column` VARCHAR(255) DEFAULT NULL, -- Adicionado v1.1.0
`description` TEXT,
`amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00, -- Modificado v1.3.0
`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 `idx_status` (`status`) -- Adicionado v1.4.0
) ENGINE=InnoDB DEFAULT CHARSET=' . $CI->db->char_set . ';
');
}
// Tabela items (adicionada v1.2.0)
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 . ';
');
}
// Opções (versão mais recente)
add_option('meu_modulo_version', '1.7.0');
add_option('meu_modulo_new_feature', '1'); // v1.6.0
add_option('meu_modulo_default_status', 'pending'); // v1.6.0
}
```
---
## Workflow de Release
### 1. Desenvolver Alterações
```php
// Exemplo: Adicionar campo "priority" na v1.8.0
```
### 2. Criar Migration
```php
// migrations/180_version_180.php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class Migration_Version_180 extends App_module_migration
{
public function up()
{
$CI = &get_instance();
if (!$CI->db->field_exists('priority', db_prefix() . 'meu_modulo')) {
$CI->db->query('ALTER TABLE `' . db_prefix() . 'meu_modulo`
ADD COLUMN `priority` TINYINT(1) NOT NULL DEFAULT 0 AFTER `status`');
}
}
}
```
### 3. Actualizar Activation Hook
```php
// Adicionar campo ao CREATE TABLE no activation hook
`priority` TINYINT(1) NOT NULL DEFAULT 0,
```
### 4. Actualizar Versão no Header
```php
/*
Module Name: Meu Módulo
Version: 1.8.0 <!-- ACTUALIZAR -->
Requires at least: 2.3.*
*/
```
### 5. Testar
1. Instalação limpa (activation hook)
2. Upgrade de versão anterior (migration)
---
## Verificações de Segurança em Migrations
```php
public function up()
{
$CI = &get_instance();
// Verificar se tabela existe antes de ALTER
if (!$CI->db->table_exists(db_prefix() . 'meu_modulo')) {
return; // Tabela não existe, nada a fazer
}
// Verificar se coluna existe antes de ADD
if ($CI->db->field_exists('new_column', db_prefix() . 'meu_modulo')) {
return; // Coluna já existe
}
// Seguro para executar
$CI->db->query('ALTER TABLE ...');
}
```
---
## Anti-Patterns (NUNCA FAZER)
| Anti-Pattern | Risco | Alternativa |
|--------------|-------|-------------|
| Gaps na sequência (001, 003) | Migrations saltadas | Sequência contínua |
| Não actualizar activation hook | Instalações novas incompletas | Sincronizar sempre |
| ALTER sem verificar existência | Erro se já existe | `field_exists()` |
| DROP sem backup | Dados perdidos | Backup ou soft delete |
| Hardcode prefix "tbl" | Falha em instalações custom | `db_prefix()` |
---
## Checklist Migrations
```
1. [ ] Ficheiro com nome correcto (NNN_version_NNN.php)
2. [ ] Classe com nome correcto (Migration_Version_NNN)
3. [ ] Extends App_module_migration
4. [ ] Verificações de existência antes de ALTER
5. [ ] db_prefix() em todas as queries
6. [ ] Activation hook actualizado com schema completo
7. [ ] Header Version actualizado
8. [ ] Testado: instalação limpa
9. [ ] Testado: upgrade de versão anterior
10. [ ] Sem gaps na sequência de migrations
```
---
**Versão:** 1.0.0 | **Autor:** Descomplicar®
**Fonte:** help.perfexcrm.com/preparing-module-upgrade

View File

@@ -0,0 +1,300 @@
---
name: perfex-module-basics
description: Perfex CRM module development basics. Folder structure, init file, file headers, CodeIgniter integration. Based on official documentation only. Use when user mentions "perfex module", "criar modulo perfex", "init file perfex", "module structure".
author: Descomplicar® Crescimento Digital
version: 1.0.0
quality_score: 70
user_invocable: true
desk_task: null
---
# /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');
$CI->db->query('SELECT * FROM ' . db_prefix() . 'meu_modulo');
// ERRADO - vai falhar se prefix não for "tbl"
$CI->db->get('tblmeu_modulo');
```
---
## Convenções de Nomenclatura
### Prefixos (OBRIGATÓRIO)
Todas as funções e classes devem ter prefixo único para evitar conflitos:
```php
// CORRECTO
function meu_modulo_get_data() { }
class Meu_modulo_helper { }
// ERRADO - pode conflitar com outros módulos/core
function get_data() { }
class Helper { }
```
### Nomes de Ficheiros
| Tipo | Convenção | Exemplo |
|------|-----------|---------|
| Controller | Primeira letra maiúscula | `Meu_modulo.php` |
| Model | Sufixo `_model` | `Meu_modulo_model.php` |
| Helper | Sufixo `_helper` | `meu_modulo_helper.php` |
| Language | Sufixo `_lang` | `meu_modulo_lang.php` |
---
## Modo de Desenvolvimento
**SEMPRE activar durante desenvolvimento:**
1. Abrir `application/config/config.php`
2. Definir: `$config['development_mode'] = true;`
Isto mostra:
- Erros PHP
- Warnings de depreciação
- Stack traces
---
## Anti-Patterns (NUNCA FAZER)
| Anti-Pattern | Risco | Alternativa |
|--------------|-------|-------------|
| Modificar ficheiros core do Perfex | Quebra em updates | Usar hooks |
| Hardcode database prefix `tbl` | Falha em instalações custom | `db_prefix()` |
| Funções sem prefixo | Conflitos | Prefixar tudo |
| Init file com nome diferente da pasta | Módulo não detectado | Nomes iguais |
| Desenvolver sem development mode | Erros silenciosos | Activar sempre |
| Não incluir index.html nas pastas | Directory listing | Incluir sempre |
---
## Checklist Novo Módulo
```
1. [ ] Pasta em /modules/ com nome único
2. [ ] Init file com mesmo nome da pasta
3. [ ] Header "Module Name" presente
4. [ ] defined('BASEPATH') em todos os ficheiros PHP
5. [ ] index.html em todas as pastas
6. [ ] Funções com prefixo único
7. [ ] db_prefix() em todas as queries
8. [ ] Development mode activado para testes
9. [ ] Hooks de activation/deactivation/uninstall
10. [ ] Testado em ambiente staging
```
---
**Versão:** 1.0.0 | **Autor:** Descomplicar®
**Fonte:** help.perfexcrm.com/module-basics

View File

@@ -0,0 +1,467 @@
---
name: perfex-permissions
description: Perfex CRM permissions system. staff_can(), register_staff_capabilities(), access control. Based on official documentation only. Use when user mentions "perfex permissions", "staff_can", "access control", "capabilities perfex".
author: Descomplicar® Crescimento Digital
version: 1.0.0
quality_score: 70
user_invocable: true
desk_task: null
---
# /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' => _l('permission_edit'),
'delete' => _l('permission_delete'),
// Custom
'export' => _l('permission_export'), // Exportar dados
'import' => _l('permission_import'), // Importar dados
'send_email' => _l('permission_send_email'), // Enviar emails
'view_all' => _l('permission_view_all'), // Ver todos (não só próprios)
'approve' => _l('permission_approve'), // Aprovar items
];
```
---
## Adicionar Permissões a Features Existentes
Para adicionar capabilities a módulos do core (invoices, projects, etc.):
```php
hooks()->add_action('admin_init', 'meu_modulo_extend_permissions');
function meu_modulo_extend_permissions()
{
$capabilities = [];
$capabilities['capabilities'] = [
'meu_modulo_custom_action' => _l('meu_modulo_custom_action_label'),
];
// Adicionar à feature "invoices"
register_staff_capabilities('invoices', $capabilities);
}
```
**Uso:**
```php
if (staff_can('meu_modulo_custom_action', 'invoices')) {
// Acção custom em facturas
}
```
---
## Verificar Administrador
```php
// Verificar se é admin (tem todas as permissões)
if (is_admin()) {
// Utilizador é administrador
}
// Verificar se é admin específico (ID 1)
if (is_admin(1)) {
// Staff com ID 1 é admin
}
```
---
## Funções Auxiliares
### Acesso Negado
```php
// Redireciona com mensagem de acesso negado
access_denied('meu_modulo');
```
### Verificar Staff Logado
```php
// ID do staff actual
$staff_id = get_staff_user_id();
// Verificar se está logado
if (is_staff_logged_in()) {
// Staff autenticado
}
```
### Verificar Permissão de Outro Staff
```php
// Verificar se staff ID 5 pode editar
$can_edit = staff_can('edit', 'meu_modulo', 5);
```
---
## Exemplo Completo
### Init File
```php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
/*
Module Name: Gestão de Inventário
Description: Sistema de inventário com controlo de acesso
Version: 1.0.0
Requires at least: 2.3.*
*/
// Registar permissões
hooks()->add_action('admin_init', 'inventario_register_permissions');
hooks()->add_action('admin_init', 'inventario_init_menu');
function inventario_register_permissions()
{
$capabilities = [
'capabilities' => [
'view' => _l('permission_view'),
'create' => _l('permission_create'),
'edit' => _l('permission_edit'),
'delete' => _l('permission_delete'),
'export' => _l('inventory_permission_export'),
'adjust' => _l('inventory_permission_adjust'), // Ajustar stock
'view_costs' => _l('inventory_permission_view_costs'), // Ver custos
],
];
register_staff_capabilities(
'inventario',
$capabilities,
_l('inventory_module')
);
}
function inventario_init_menu()
{
if (!staff_can('view', 'inventario')) {
return;
}
$CI = &get_instance();
$CI->app_menu->add_sidebar_menu_item('inventario', [
'name' => _l('inventory'),
'collapse' => true,
'position' => 22,
'icon' => 'fa fa-cubes',
]);
$CI->app_menu->add_sidebar_children_item('inventario', [
'slug' => 'inventario-lista',
'name' => _l('products'),
'href' => admin_url('inventario'),
'position' => 1,
'icon' => 'fa fa-list',
]);
if (staff_can('adjust', 'inventario')) {
$CI->app_menu->add_sidebar_children_item('inventario', [
'slug' => 'inventario-ajustes',
'name' => _l('adjustments'),
'href' => admin_url('inventario/adjustments'),
'position' => 2,
'icon' => 'fa fa-exchange',
]);
}
if (staff_can('export', 'inventario')) {
$CI->app_menu->add_sidebar_children_item('inventario', [
'slug' => 'inventario-export',
'name' => _l('export'),
'href' => admin_url('inventario/export'),
'position' => 3,
'icon' => 'fa fa-download',
]);
}
}
```
### Controller
```php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class Inventario extends AdminController
{
public function __construct()
{
parent::__construct();
$this->load->model('inventario/inventario_model');
}
public function index()
{
if (!staff_can('view', 'inventario')) {
access_denied('inventario');
}
$data['products'] = $this->inventario_model->get_all();
// Se pode ver custos, incluir
if (staff_can('view_costs', 'inventario')) {
$data['show_costs'] = true;
}
$this->load->view('inventario/index', $data);
}
public function adjustments()
{
if (!staff_can('adjust', 'inventario')) {
access_denied('inventario');
}
// ... lógica de ajustes
}
public function export()
{
if (!staff_can('export', 'inventario')) {
access_denied('inventario');
}
// ... lógica de exportação
}
}
```
---
## Anti-Patterns (NUNCA FAZER)
| Anti-Pattern | Risco | Alternativa |
|--------------|-------|-------------|
| Não verificar permissões no controller | Acesso não autorizado | `staff_can()` sempre |
| Confiar apenas na UI | Bypass via URL | Verificar em backend |
| Feature ID duplicado | Conflitos | Prefixar com nome módulo |
| Capability sem _l() | Não traduzível | Usar traduções |
| Assumir que view esconde acção | Vulnerável | Verificar em ambos |
---
## Checklist Permissões
```
1. [ ] register_staff_capabilities no init file
2. [ ] Feature ID único
3. [ ] Capabilities com traduções (_l)
4. [ ] staff_can() em TODOS os métodos do controller
5. [ ] staff_can() nas views para UI condicional
6. [ ] staff_can() antes de mostrar menus
7. [ ] access_denied() para rejeitar acesso
8. [ ] Testado com utilizador não-admin
```
---
**Versão:** 1.0.0 | **Autor:** Descomplicar®
**Fonte:** help.perfexcrm.com/staff-capabilities-and-access

View File

@@ -0,0 +1,468 @@
---
name: perfex-security
description: Perfex CRM module security. Input validation, CSRF, XSS prevention, directory traversal. Based on official documentation only. Use when user mentions "perfex security", "input validation", "csrf perfex", "xss protection".
author: Descomplicar® Crescimento Digital
version: 1.0.0
quality_score: 70
user_invocable: true
desk_task: null
---
# /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 (Seguros)
```php
// Documentos
'pdf|doc|docx|xls|xlsx|ppt|pptx|txt|csv'
// Imagens
'gif|jpg|jpeg|png|webp'
// NUNCA permitir
'php|php3|php4|php5|phtml|exe|sh|bat|js|html'
```
---
## 8. Verificação de Ownership
Sempre verificar se o utilizador tem acesso ao recurso:
```php
public function view($id)
{
$item = $this->meu_modulo_model->get($id);
// Verificar se existe
if (!$item) {
show_404();
}
// Se for área cliente, verificar ownership
if (!is_staff_logged_in()) {
if ($item->client_id != get_client_user_id()) {
show_404(); // Não revelar que existe
}
}
// Ou para staff com view_own apenas
if (!is_admin() && !staff_can('view_all', 'meu_modulo')) {
if ($item->created_by != get_staff_user_id()) {
access_denied('meu_modulo');
}
}
// ... mostrar dados
}
```
---
## 9. Session Security
```php
// Regenerar session após login (já feito pelo Perfex)
$this->session->sess_regenerate();
// Destruir session
$this->session->sess_destroy();
// Guardar dados em session
$this->session->set_userdata('key', 'value');
// Ler dados da session
$value = $this->session->userdata('key');
```
---
## 10. Password Handling
**NUNCA guardar passwords em plain text.**
```php
// Hash password (se necessário no módulo)
$hashed = password_hash($password, PASSWORD_DEFAULT);
// Verificar password
if (password_verify($input_password, $stored_hash)) {
// Válido
}
```
---
## Checklist de Segurança
### Ficheiros
```
[ ] defined('BASEPATH') em todos os PHP
[ ] index.html em todas as pastas
[ ] Nenhum ficheiro executável em uploads
```
### Input/Output
```
[ ] $this->input->post/get para todos os inputs
[ ] html_escape() em todos os outputs
[ ] Query Builder para todas as queries
[ ] form_open() para todos os formulários
```
### Acesso
```
[ ] staff_can() antes de operações
[ ] Ownership verificado para dados de cliente
[ ] CSRF token em formulários e AJAX
[ ] Tipos de ficheiro restritos em uploads
```
---
## Exemplo Seguro Completo
### Controller
```php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class Meu_modulo extends AdminController
{
public function __construct()
{
parent::__construct();
$this->load->model('meu_modulo/meu_modulo_model');
}
public function save()
{
// 1. Verificar permissão
if (!staff_can('create', 'meu_modulo')) {
access_denied('meu_modulo');
}
// 2. Verificar se é POST (CSRF verificado automaticamente)
if (!$this->input->post()) {
redirect(admin_url('meu_modulo'));
}
// 3. Validar e sanitizar input
$data = [
'name' => trim($this->input->post('name')),
'description' => $this->input->post('description'),
'amount' => (float) $this->input->post('amount'),
'client_id' => (int) $this->input->post('client_id'),
];
// 4. Validação de negócio
if (empty($data['name'])) {
set_alert('danger', _l('field_required', _l('name')));
redirect(admin_url('meu_modulo/create'));
}
// 5. Guardar via model (usa Query Builder)
$id = $this->meu_modulo_model->add($data);
if ($id) {
set_alert('success', _l('added_successfully'));
} else {
set_alert('danger', _l('error_occurred'));
}
redirect(admin_url('meu_modulo'));
}
}
```
### View
```php
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<?php init_head(); ?>
<div id="wrapper">
<div class="content">
<div class="panel_s">
<div class="panel-body">
<?php echo form_open(admin_url('meu_modulo/save')); ?>
<div class="form-group">
<label><?php echo _l('name'); ?></label>
<input type="text"
name="name"
class="form-control"
value="<?php echo isset($item) ? html_escape($item->name) : ''; ?>"
required>
</div>
<button type="submit" class="btn btn-primary">
<?php echo _l('save'); ?>
</button>
<?php echo form_close(); ?>
</div>
</div>
</div>
</div>
<?php init_tail(); ?>
</body>
</html>
```
---
## Anti-Patterns (NUNCA FAZER)
| Anti-Pattern | Risco | Alternativa |
|--------------|-------|-------------|
| `$_POST`/`$_GET` directo | XSS, Injection | `$this->input->post/get` |
| Echo sem escape | XSS | `html_escape()` |
| SQL concatenado | SQL Injection | Query Builder |
| `<form>` HTML | CSRF bypass | `form_open()` |
| Upload sem validação | RCE | `allowed_types` restrito |
| Sem verificar ownership | Data breach | Validar sempre |
---
**Versão:** 1.0.0 | **Autor:** Descomplicar®
**Fonte:** help.perfexcrm.com/module-security

View File

@@ -0,0 +1,463 @@
---
name: perfex-views
description: Perfex CRM module views. Templates, init_head(), init_tail(), admin and client views, passing data. Based on official documentation only. Use when user mentions "perfex views", "template perfex", "init_head", "init_tail".
author: Descomplicar® Crescimento Digital
version: 1.0.0
quality_score: 70
user_invocable: true
desk_task: null
---
# /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>
<td><?php echo _dt($item->created_at); ?></td>
<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>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<?php init_tail(); ?>
<script>
$(function(){
// DataTables já inicializado via classe dt-table
// Confirmação delete
$('._delete').on('click', function(e){
return confirm('<?php echo _l('confirm_delete'); ?>');
});
});
</script>
</body>
</html>
```
### Formulário (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-8 col-md-offset-2">
<div class="panel_s">
<div class="panel-body">
<h4>
<?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/edit/' . $item->id)
: admin_url('meu_modulo/create');
echo form_open($action);
?>
<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>
<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>
<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>
<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(){
// Validação formulário
appValidateForm($('form'), {
name: {
required: true,
minlength: 3
}
});
});
</script>
</body>
</html>
```
---
## Helpers de Output
### Escape HTML (OBRIGATÓRIO para dados user)
```php
// SEMPRE usar para dados de utilizador
echo html_escape($item->name);
// Para atributos HTML
echo html_escape($item->url, true);
```
### Formatação de Datas
```php
// Data formatada (configuração do sistema)
echo _d($item->date); // 25-01-2026
// Data e hora
echo _dt($item->created_at); // 25-01-2026 14:30
```
### Formatação de Valores
```php
// Moeda
echo app_format_money($item->amount, $currency);
// Número
echo app_format_number($item->quantity);
```
### Tradução
```php
// String de tradução
echo _l('meu_modulo_title');
// Com placeholder
echo _l('item_created_by', $staff_name);
```
---
## Classes CSS Disponíveis
### Painéis
```html
<div class="panel_s">
<div class="panel-body">
<!-- Conteúdo -->
</div>
</div>
```
### Tabelas
```html
<!-- Tabela com DataTables -->
<table class="table table-striped dt-table">
<!-- Tabela simples -->
<table class="table table-bordered">
```
### Botões
```html
<a class="btn btn-primary">Primary</a>
<a class="btn btn-success">Success</a>
<a class="btn btn-warning">Warning</a>
<a class="btn btn-danger">Danger</a>
<a class="btn btn-default">Default</a>
<!-- Botão ícone -->
<a class="btn btn-default btn-icon"><i class="fa fa-edit"></i></a>
```
### Ícones (Font Awesome 4)
```html
<i class="fa fa-plus"></i>
<i class="fa fa-edit"></i>
<i class="fa fa-trash"></i>
<i class="fa fa-search"></i>
<i class="fa fa-check"></i>
```
---
## Anti-Patterns (NUNCA FAZER)
| Anti-Pattern | Risco | Alternativa |
|--------------|-------|-------------|
| Echo dados sem escape | XSS | `html_escape()` sempre |
| Não usar form_open() | CSRF vulnerável | Usar form_open() |
| SQL em views | Má arquitectura | Preparar no controller |
| Esquecer init_head/tail | Layout quebrado | Incluir sempre (admin) |
| Hardcode strings | Não traduzível | Usar `_l()` |
---
## Checklist View
```
1. [ ] defined('BASEPATH') no topo
2. [ ] init_head() e init_tail() (admin)
3. [ ] $this->layout() (cliente)
4. [ ] html_escape() em todos os dados user
5. [ ] form_open() para formulários
6. [ ] Permissões verificadas antes de mostrar botões
7. [ ] _l() para todas as strings
8. [ ] Classes CSS standard do Perfex
```
---
**Versão:** 1.0.0 | **Autor:** Descomplicar®
**Fonte:** help.perfexcrm.com/module-views