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:
12
perfex-dev/.claude-plugin/plugin.json
Normal file
12
perfex-dev/.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "perfex-dev",
|
||||
"description": "Perfex CRM module development - controllers, views, forms, hooks, menus, migrations, permissions, security and database. Backed by 1 Dify KB dataset.",
|
||||
"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"]
|
||||
}
|
||||
234
perfex-dev/agents/perfex-crm-module-developer.md
Normal file
234
perfex-dev/agents/perfex-crm-module-developer.md
Normal file
@@ -0,0 +1,234 @@
|
||||
---
|
||||
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
|
||||
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®
|
||||
---
|
||||
|
||||
|
||||
# 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)
|
||||
|
||||
## Datasets Dify (Consultar SEMPRE)
|
||||
```
|
||||
mcp__dify-kb__dify_kb_retrieve_segments dataset:"PerfexCRM" query:"module development hooks customization"
|
||||
mcp__dify-kb__dify_kb_retrieve_segments dataset:"Desenvolvimento de Software" 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
|
||||
- `dify-kb`: 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__*`
|
||||
|
||||
✓ **dify-kb** (knowledge)
|
||||
- Knowledge base AI
|
||||
- Usage: `mcp__dify-kb__*`
|
||||
|
||||
### Recommended for dev
|
||||
- **context7** - Context documentation
|
||||
- **n8n** - Workflows automation
|
||||
- **filesystem** - Ficheiros locais
|
||||
- **magic** - AI-powered UI component generation (tipo v0.dev)
|
||||
- **memory-supabase** - Memória longo prazo
|
||||
- **cwp** - CentOS Web Panel
|
||||
- **puppeteer** - Browser automation
|
||||
|
||||
### All Available (33 total)
|
||||
moloni, google-analytics, google-workspace, imap, outline-api, youtube-research, youtube-uploader, wikijs, gsc, lighthouse, mcp-time, mcp-mermaid, mcp-echarts, powerpoint, penpot, pixabay, pexels, tavily, elevenlabs, vimeo, design-systems, replicate
|
||||
|
||||
**Discovery:** Use ToolSearch to find specific tools.
|
||||
**Example:** `ToolSearch("ssh upload")` finds SSH upload tools.
|
||||
|
||||
|
||||
## Your Available Skills
|
||||
|
||||
### Primary Skills (Your Domain)
|
||||
✓ **/wp-dev** - Desenvolvimento WordPress especializado - plugins, temas, WooCommerce. Usar para
|
||||
- Invoke: `/wp-dev`
|
||||
|
||||
✓ **/php-dev** - Desenvolvimento PHP fullstack - Laravel, Symfony, APIs RESTful, arquitectura bac
|
||||
- Invoke: `/php-dev`
|
||||
|
||||
✓ **/db-design** - Design de bases de dados - schema, optimização queries, MySQL/PostgreSQL, arquit
|
||||
- Invoke: `/db-design`
|
||||
|
||||
✓ **/mcp-dev** - Desenvolvimento de servidores MCP - criar, configurar, testar e documentar MCPs
|
||||
- Invoke: `/mcp-dev`
|
||||
|
||||
### Recommended for dev
|
||||
- **/react-patterns** - Padrões React modernos - Hooks, Server Components, State Man
|
||||
- **/nextjs** - Desenvolvimento Next.js moderno com App Router, Server Compo
|
||||
- **/elementor** - Desenvolvimento avançado com Elementor Pro e Crocoblock - wi
|
||||
- **/woocommerce** - Desenvolvimento e optimização de lojas WooCommerce - checkou
|
||||
- **/crm-admin** - Administração e desenvolvimento Perfex CRM - gestão dados, m
|
||||
- **/video** - Criar vídeos programáticos com Remotion e React. Gera intros
|
||||
- **/doc-sync** - Sincronização automática de documentação Stack/Vault. Garant
|
||||
- **/delegate** - Delegar tarefas dev para outros chats Claude com workflow co
|
||||
- **/time** - Time tracking integrado com Desk CRM. Inicia e para timers e
|
||||
|
||||
### Core Skills (All Agents)
|
||||
- **/reflect** - Auto-reflexão e melhoria contínua do sistema. Analisa sessõe
|
||||
- **/worklog** - Registo automático de trabalho - tarefas, problemas, soluçõe
|
||||
- **/_core** - Padrões fundamentais Descomplicar® - Sacred Rules, Excellenc
|
||||
- **/knowledge** - Gestão unificada de conhecimento - pesquisa inteligente com
|
||||
- **/desk** - Integração com Desk CRM via ficheiro .desk-project. Auto-det
|
||||
|
||||
### All Available (54 total)
|
||||
/billing-check, /crm-ops, /ecommerce, /lead-approach, /orcamento, /saas, /content-marketing-pt, /remotion-video, /seo-content-optimization, /social-media, /ui-ux-pro-max-repo, /brand-voice-generator, /frontend-design, /pptx-generator, /ui-ux-pro-max, /backup-strategies, /security-audit, /server-health, /wp-performance, /wp-update, /second-brain-repo, /ads, /marketing-strategy, /product, /skill-creator, /sop-creator, /calendar-manager, /interview, /today, /research, /youtube, /seo-audit, /seo-report, /archive, /metrics, /sdk
|
||||
|
||||
**Discovery:** Use the Skill tool to invoke skills.
|
||||
**Example:** `Skill("skill-name")` invokes the skill.
|
||||
|
||||
|
||||
## Your Team & Responsibilities
|
||||
|
||||
You are part of **3 SDKs** (TaskForce teams):
|
||||
|
||||
### TaskForce Claude Agents
|
||||
|
||||
**Purpose:** NULL
|
||||
|
||||
**Your responsibilities in this TaskForce:**
|
||||
|
||||
- **Sistema de agentes especializados para delegacao de tarefas via Task tool com consulta automatica de datasets Dify.**: NULL
|
||||
|
||||
### TaskForce DeskDev
|
||||
|
||||
**Purpose:** NULL
|
||||
|
||||
### TaskForce Gestão Administrativa e Financeira
|
||||
|
||||
**Purpose:** NULL
|
||||
|
||||
**Collaboration:**
|
||||
- Work with other agents in your TaskForce teams
|
||||
- Share knowledge and context across team members
|
||||
- Leverage team-specific skills and MCPs
|
||||
- Contribute to team goals and deliverables
|
||||
|
||||
7
perfex-dev/knowledge/datasets.json
Normal file
7
perfex-dev/knowledge/datasets.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"description": "Dify KB datasets for Perfex CRM Development domain",
|
||||
"query_tool": "mcp__dify-kb__dify_kb_retrieve_segments",
|
||||
"datasets": [
|
||||
{"id": "43354eb6-f0b2-40cc-aa53-44e375ab347c", "name": "PerfexCRM", "priority": 1, "document_count": 144, "word_count": 1547186}
|
||||
]
|
||||
}
|
||||
421
perfex-dev/skills/perfex-controllers/SKILL.md
Normal file
421
perfex-dev/skills/perfex-controllers/SKILL.md
Normal 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
|
||||
469
perfex-dev/skills/perfex-database/SKILL.md
Normal file
469
perfex-dev/skills/perfex-database/SKILL.md
Normal 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
|
||||
562
perfex-dev/skills/perfex-forms/SKILL.md
Normal file
562
perfex-dev/skills/perfex-forms/SKILL.md
Normal 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
|
||||
380
perfex-dev/skills/perfex-hooks/SKILL.md
Normal file
380
perfex-dev/skills/perfex-hooks/SKILL.md
Normal 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
|
||||
400
perfex-dev/skills/perfex-menus/SKILL.md
Normal file
400
perfex-dev/skills/perfex-menus/SKILL.md
Normal 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
|
||||
426
perfex-dev/skills/perfex-migrations/SKILL.md
Normal file
426
perfex-dev/skills/perfex-migrations/SKILL.md
Normal 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
|
||||
300
perfex-dev/skills/perfex-module-basics/SKILL.md
Normal file
300
perfex-dev/skills/perfex-module-basics/SKILL.md
Normal 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
|
||||
467
perfex-dev/skills/perfex-permissions/SKILL.md
Normal file
467
perfex-dev/skills/perfex-permissions/SKILL.md
Normal 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
|
||||
468
perfex-dev/skills/perfex-security/SKILL.md
Normal file
468
perfex-dev/skills/perfex-security/SKILL.md
Normal 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
|
||||
463
perfex-dev/skills/perfex-views/SKILL.md
Normal file
463
perfex-dev/skills/perfex-views/SKILL.md
Normal 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
|
||||
Reference in New Issue
Block a user