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