نمونه افزونه سفارشی هماهنگ با CRM

مثال‌های کامل نشان‌دهنده نحوه ترکیب چند هوک LA24Core در یک افزونه CRM کار‌آمد.


مثال ۱: پلاگین کامل با منو، صفحه و دسترسی

این مثال یک پلاگین مستقل می‌سازد که بخش «معاملات مشتری» را به CRM اضافه می‌کند.
**ساختار پلاگین:**
javascript
my-deals-plugin/
├── my-deals-plugin.php
├── includes/
│   ├── class-deals-hook.php
│   ├── controllers/
│   │   └── DealsList.php
│   └── settings/
│       └── DealsSettings.php
└── templates/
    └── deals-list.php
**`my-deals-plugin.php`:**
php
<?php
/**
 * Plugin Name: معاملات مشتری
 * Description: اضافه کردن ماژول معاملات به LA24Core CRM
 * Version:     1.0.0
 * Text Domain: my-deals
 */

defined( 'ABSPATH' ) || exit;

define( 'MY_DEALS_PATH', plugin_dir_path( __FILE__ ) );
define( 'MY_DEALS_URL',  plugin_dir_url( __FILE__ ) );

add_action( 'plugins_loaded', function () {
    if ( ! function_exists( 'la24_get_option' ) ) return;
    require_once MY_DEALS_PATH . 'includes/class-deals-hook.php';
    new Deals_Hook();
} );
**`includes/class-deals-hook.php`:**
php
<?php

defined( 'ABSPATH' ) || exit;

class Deals_Hook {

    public function __construct() {
        add_action( 'la24_dashboard_menu_items', array( $this, 'register_menu'         ) );
        add_filter( 'la24_endpoints',            array( $this, 'register_endpoint' ), 10 );
        add_filter( 'la24_capability',           array( $this, 'register_capability' ), 10 );
        add_filter( 'la24_groups_permission',    array( $this, 'register_group'     ), 10 );
        add_filter( 'la24_register_setting',     array( $this, 'register_setting'   ), 10 );
    }

    public function register_menu() {
        LA24()->Menu()::multi_add( array(
            'DealsSection' => array(
                'name'      => __( 'معاملات', 'my-deals' ),
                'icon'      => 'fa fa-handshake-o',
                'parent'    => '',
                'priority'  => 60,
                'is_toggle' => true,
            ),
            'DealsList' => array(
                'name'      => __( 'همه معاملات', 'my-deals' ),
                'icon'      => 'fa fa-list',
                'parent'    => 'DealsSection',
                'priority'  => 10,
                'is_toggle' => false,
            ),
        ) );
    }

    public function register_endpoint( $endpoints ) {
        $endpoints['DealsList'] = array(
            'name'     => __( 'لیست معاملات', 'my-deals' ),
            'callback' => '\MyDeals\Controllers\DealsList',
        );
        return $endpoints;
    }

    public function register_capability( $capability ) {
        $capability['my_deals_view'] = array(
            'name'  => __( 'مجوز مشاهده معاملات', 'my-deals' ),
            'gp_id' => 'deals-management',
        );
        $capability['my_deals_edit'] = array(
            'name'  => __( 'مجوز ویرایش معاملات', 'my-deals' ),
            'gp_id' => 'deals-management',
        );
        return $capability;
    }

    public function register_group( $group ) {
        $group['deals-management'] = __( 'مدیریت معاملات', 'my-deals' );
        return $group;
    }

    public function register_setting( $settings ) {
        $settings['my_deals'] = array(
            'priority'  => 45,
            'name'      => __( 'تنظیمات معاملات', 'my-deals' ),
            'call_back' => '\MyDeals\Settings\DealsSettings',
        );
        return $settings;
    }
}

مثال ۲: ردیابی تاریخچه پروفایل کاربر

نمایش خط زمانی فعالیت‌های سفارشی در صفحه پروفایل کاربر:
php
add_filter( 'la24_history_user_profile', function ( $tabs ) {

    $tabs['deals_history'] = array(
        'priority' => 5,
        'name'     => __( 'تاریخچه معاملات', 'my-deals' ),
        'callback' => '\MyDeals\Controllers\DealsHistory',
    );

    return $tabs;
} );
**کنترلر `DealsHistory`:**
php
<?php

namespace MyDeals\Controllers;

use LoyalAxis\abstracts\BaseViewController;

defined( 'ABSPATH' ) || exit;

class DealsHistory extends BaseViewController {

    public function content() {
        $profile_id = (int) ( $_GET['id'] ?? 0 );
        if ( ! $profile_id ) return;

        if ( ! la24_user_can( get_current_user_id(), 'my_deals_view' ) ) {
            echo '<p class="text-muted">' . esc_html__( 'دسترسی ندارید.', 'my-deals' ) . '</p>';
            return;
        }

        global $wpdb;
        $deals = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM {$wpdb->prefix}my_deals WHERE user_id = %d ORDER BY created_at DESC",
                $profile_id
            )
        );

        if ( empty( $deals ) ) {
            echo '<p class="text-muted">' . esc_html__( 'هیچ معامله‌ای یافت نشد.', 'my-deals' ) . '</p>';
            return;
        }

        foreach ( $deals as $deal ) {
            printf(
                '<div class="la24-history-item d-flex justify-content-between align-items-center border-bottom py-2">
                    <div>
                        <strong>%s</strong>
                        <small class="text-muted ms-2">%s</small>
                    </div>
                    <span class="badge bg-%s">%s</span>
                </div>',
                esc_html( $deal->title ),
                esc_html( $deal->created_at ),
                esc_attr( $deal->status === 'won' ? 'success' : ( $deal->status === 'lost' ? 'danger' : 'secondary' ) ),
                esc_html( $deal->status )
            );
        }
    }
}

مثال ۳: ویجت داشبورد سفارشی

کلاس‌های ویجت باید از کلاس پایه انتزاعی `Widget` در LA24Core (‏`\LoyalAxis\classes\services\widgets\Widget`) ارث‌بری کنند و از یک شناسه عددی **۹۰۰ یا بالاتر** استفاده کنند — ماژول‌های داخلی خود LA24Core شناسه‌های زیر ۹۰۰ را اشغال کرده‌اند، پس بازه `900+` برای افزونه‌های سفارشی/شخص‌ثالث رزرو شده است. ویجت را با یک یا چند برچسب `category` (`administrative`، `woocommerce_panel`، `operational`، `reporting`، `general`) مشخص کنید تا در چیپ‌های جست‌وجو/فیلتر مودال «افزودن ویجت به داشبورد» به‌درستی نمایش داده شود — برای قرارداد کامل به [راهنمای `la24_widgets`](developer-guide-la24_widgets_fa.md) مراجعه کنید.
php
add_filter( 'la24_widgets', function ( $widgets ) {
    $widgets['900'] = array(
        'name'       => __( 'خلاصه قیف فروش', 'my-deals' ),
        'class_icon' => 'fa fa-filter',
        'category'   => array( 'reporting' ),
        'callback'   => '\MyDeals\Widgets\PipelineWidget',
    );
    return $widgets;
} );
**کلاس `PipelineWidget`:**
php
<?php

namespace MyDeals\Widgets;

defined( 'ABSPATH' ) || exit;

use LoyalAxis\classes\services\widgets\Widget;
use LoyalAxis\classes\utilities\Form;

class PipelineWidget extends Widget {

    public function content( $settings ) {
        global $wpdb;

        $stats = $wpdb->get_results(
            "SELECT status, COUNT(*) as total, SUM(value) as total_value
             FROM {$wpdb->prefix}my_deals
             GROUP BY status"
        );

        echo '<div class="p-3" style="background-color: ' . esc_attr( $settings['bg_color_900'] ) . '">';
        echo '<h6 class="fw-bold mb-3">' . esc_html( $settings['head_text_900'] ) . '</h6>';

        foreach ( $stats as $row ) {
            $color = match( $row->status ) {
                'won'         => 'success',
                'lost'        => 'danger',
                'negotiation' => 'warning',
                default       => 'secondary',
            };
            printf(
                '<div class="d-flex justify-content-between mb-2">
                    <span class="badge bg-%s">%s</span>
                    <span>%d معامله — %s تومان</span>
                </div>',
                esc_attr( $color ),
                esc_html( ucfirst( $row->status ) ),
                (int) $row->total,
                number_format( (float) $row->total_value )
            );
        }

        echo '</div>';
    }

    public static function desktop_size() {
        return array(
            'size_x' => 3, 'size_y' => 2,
            'minx'   => 2, 'miny'   => 1,
            'maxx'   => 6, 'maxy'   => 6,
        );
    }

    public static function mobile_size() {
        return array(
            'size_x' => 2, 'size_y' => 2,
            'minx'   => 1, 'miny'   => 1,
            'maxx'   => 2, 'maxy'   => 6,
        );
    }

    public function default_settings(): array {
        return array(
            'head_text_900' => __( 'خلاصه قیف فروش', 'my-deals' ),
            'bg_color_900'  => 'rgba(255,255,255,1)',
        );
    }

    public function widget_title() {
        return $this->settings['head_text_900'] ?? '';
    }

    public function widget_bg_color() {
        return $this->settings['bg_color_900'] ?? '';
    }

    public function style_settings( $settings ) {
        echo Form::start_form( 0, 'la24-section-box' );
        echo Form::header( __( 'تنظیمات ظاهری:', 'my-deals' ), 12, 'la24-titr' );
        echo Form::color_picker( __( 'رنگ پس‌زمینه ویجت', 'my-deals' ), 6, 'bg_color_900', 'bg_color_900', 'center', $settings['bg_color_900'], 0, '' );
        echo Form::close_form();
    }

    public function master_settings( $settings ) {
        echo Form::start_form( 0, 'la24-section-box' );
        echo Form::header( __( 'تنظیمات مدیریتی:', 'my-deals' ), 12, 'la24-titr' );
        echo Form::text_input( __( 'عنوان', 'my-deals' ), 6, 'head_text_900', 'head_text_900', '', $settings['head_text_900'], '', 0 );
        echo Form::close_form();
    }
}

مثال ۴: دکمه عملیات پروفایل

دکمه سریع به صفحه معاملات برای کاربر مشخص:
php
add_filter( 'la24_render_profile_action_btn', function ( $actions, $is_clue ) {

    $profile_id = (int) ( $_GET['id'] ?? 0 );

    if ( $profile_id && la24_user_can( get_current_user_id(), 'my_deals_view' ) ) {
        $url = la24_get_permalink_panel( 'DealsList/?user_id=' . $profile_id );

        $actions['view_deals_btn']['content'] = sprintf(
            '<a href="%s" class="btn btn-sm btn-outline-success me-1">
                <i class="fa fa-handshake-o me-1"></i>%s
            </a>',
            esc_url( $url ),
            esc_html__( 'معاملات', 'my-deals' )
        );
    }

    return $actions;

}, 10, 2 );

مثال ۵: پنل تنظیمات چندفیلدی

کلاسی که از طریق `la24_register_setting` ثبت می‌شود (مثال ۱ را ببینید، جایی که `register_setting()` کلید `my_deals` را به `\MyDeals\Settings\DealsSettings` وصل می‌کند) باید **اینترفیس `\LoyalAxis\interfaces\Settings`** خود LA24Core را پیاده‌سازی کند — دو متد استاتیک، `request_process()` و `render_view()` — نه یک متد واحد `render()`. این همان قراردادی است که `LA24License_Settings.php` در داخل خودش رعایت می‌کند؛ برای توضیح کامل اینکه چرا این دو متد از هم جدا شده‌اند و با چه ترتیبی اجرا می‌شوند به [راهنمای `la24_register_setting`](developer-guide-la24_register_setting_fa.md#کلاس-تنظیمات) مراجعه کنید.
مقادیر با `la24_get_option()` / `la24_update_options()` خوانده و نوشته می‌شوند (این توابع هم در [بخش آموزشی اختصاصی همان راهنما](developer-guide-la24_register_setting_fa.md#آموزش-la24_get_option-و-la24_update_options) توضیح داده شده‌اند)، نه با `update_option()` / `get_option()`؛ در نتیجه تمام فیلدهای این پنل درون یک گروه واحد، `'my_deals_options'`، ذخیره می‌شوند.
php
<?php

namespace MyDeals\Settings;

use LoyalAxis\interfaces\Settings;
use LoyalAxis\traits\Notification;

defined( 'ABSPATH' ) || exit;

class DealsSettings implements Settings {

    use Notification;

    private const OPTION_GROUP = 'my_deals_options';

    public static function render_view() {
        $currency      = la24_get_option( 'currency', self::OPTION_GROUP ) ?: 'IRR';
        $default_stage = la24_get_option( 'default_stage', self::OPTION_GROUP ) ?: 'new';
        $notify_email  = la24_get_option( 'notify_email', self::OPTION_GROUP );
        ?>
        <form method="post" class="la24-settings-form">
            <div class="mb-3">
                <label class="form-label"><?php esc_html_e( 'واحد ارز', 'my-deals' ); ?></label>
                <input type="text" name="currency" value="<?php echo esc_attr( $currency ); ?>" class="form-control" style="max-width:120px" />
            </div>
            <div class="mb-3">
                <label class="form-label"><?php esc_html_e( 'مرحله پیش‌فرض', 'my-deals' ); ?></label>
                <select name="default_stage" class="form-select" style="max-width:200px">
                    <option value="new"         <?php selected( $default_stage, 'new' ); ?>><?php esc_html_e( 'جدید', 'my-deals' ); ?></option>
                    <option value="negotiation" <?php selected( $default_stage, 'negotiation' ); ?>><?php esc_html_e( 'مذاکره', 'my-deals' ); ?></option>
                    <option value="won"         <?php selected( $default_stage, 'won' ); ?>><?php esc_html_e( 'برنده شده', 'my-deals' ); ?></option>
                </select>
            </div>
            <div class="mb-3">
                <label class="form-label"><?php esc_html_e( 'ایمیل اطلاع‌رسانی', 'my-deals' ); ?></label>
                <input type="email" name="notify_email" value="<?php echo esc_attr( $notify_email ); ?>" class="form-control" style="max-width:300px" />
            </div>
            <button type="submit" name="my_deals_submit" class="btn btn-primary"><?php esc_html_e( 'ذخیره تنظیمات', 'my-deals' ); ?></button>
        </form>
        <?php
    }

    public static function request_process() {
        if ( isset( $_POST['my_deals_submit'] ) ) {
            la24_update_options( array(
                'currency'      => sanitize_text_field( $_POST['currency'] ?? 'IRR' ),
                'default_stage' => sanitize_text_field( $_POST['default_stage'] ?? 'new' ),
                'notify_email'  => sanitize_email( $_POST['notify_email'] ?? '' ),
            ), self::OPTION_GROUP );

            self::alert( __( 'تنظیمات معاملات با موفقیت ذخیره شد.', 'my-deals' ), true, 'success' );
        }
    }
}
`request_process()` ابتدا اجرا می‌شود — برای هر پنل ثبت‌شده، در هر بار بارگذاری صفحهٔ تنظیمات — و فقط زمانی می‌نویسد که دکمهٔ ارسال خودش (`my_deals_submit`) در `$_POST` وجود داشته باشد. `render_view()` پس از آن اجرا می‌شود و به‌سادگی مقادیر (که ممکن است همین الان به‌روزرسانی شده باشند) را با `la24_get_option()` می‌خواند، بنابراین فرم همیشه همان چیزی را نشان می‌دهد که واقعاً ذخیره شده است.