Plugin Directory

Changeset 3460128


Ignore:
Timestamp:
02/12/2026 03:52:16 PM (7 weeks ago)
Author:
closemarketing
Message:

Update to version 4.3.0 from GitHub

Location:
formscrm
Files:
17 added
2 deleted
50 edited
1 copied

Legend:

Unmodified
Added
Removed
  • formscrm/tags/4.3.0/formscrm.php

    r3425471 r3460128  
    44 * Plugin URI : https://close.technology/wordpress-plugins/formscrm/
    55 * Description: Connects Forms with CRM, ERP and Email Marketing.
    6  * Version: 4.2.1
     6 * Version: 4.3.0
    77 * Author: CloseTechnology
    88 * Author URI: https://close.technology
     
    2424defined( 'ABSPATH' ) || die( 'No script kiddies please!' );
    2525
    26 define( 'FORMSCRM_VERSION', '4.2.1' );
     26define( 'FORMSCRM_VERSION', '4.3.0' );
    2727define( 'FORMSCRM_PLUGIN', __FILE__ );
    2828define( 'FORMSCRM_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
     
    9292// Include files.
    9393require_once FORMSCRM_PLUGIN_PATH . '/includes/admin/class-admin-options.php';
     94require_once FORMSCRM_PLUGIN_PATH . '/includes/admin/class-error-log.php';
     95require_once FORMSCRM_PLUGIN_PATH . '/includes/admin/class-error-log-page.php';
    9496require_once FORMSCRM_PLUGIN_PATH . '/includes/formscrm-library/loader.php';
  • formscrm/tags/4.3.0/includes/admin/class-admin-options.php

    r3424189 r3460128  
    3737            add_action( 'admin_menu', array( $this, 'add_plugin_page' ) );
    3838            add_action( 'formscrm_settings', array( $this, 'settings_page' ) );
     39            add_action( 'formscrm_notifications', array( $this, 'notifications_page' ) );
    3940            add_action( 'admin_init', array( $this, 'register_settings' ) );
    4041        }
     
    142143                            'action' => 'formscrm_settings',
    143144                        ),
     145                        array(
     146                            'tab'    => 'notifications',
     147                            'label'  => esc_html__( 'Notifications', 'formscrm' ),
     148                            'action' => 'formscrm_notifications',
     149                        ),
     150                        array(
     151                            'tab'    => 'error-log',
     152                            'label'  => esc_html__( 'Error Log', 'formscrm' ),
     153                            'action' => 'formscrm_error_log_content',
     154                        ),
    144155                    )
    145156                );
     
    210221
    211222        /**
    212          * Renders the settings page.
    213          *
    214          * Displays the FormsCRM settings form with Slack integration options.
    215          *
    216          * @return void
    217          */
    218         public function settings_page() {
    219             $source_shop_url          = 'es' === strtok( get_locale(), '_' ) ? 'https://close.technology/' : 'https://close.technology/en/';
    220             $utm_source               = '?utm_source=WordPress+Settings&utm_medium=plugin&utm_campaign=link';
     223         * Renders the notifications page.
     224         *
     225         * Displays error notification settings including Slack and Email options.
     226         *
     227         * @return void
     228         */
     229        public function notifications_page() {
    221230            $slack_webhook_url        = get_option( 'formscrm_slack_webhook_url', '' );
    222231            $error_notification_email = get_option( 'formscrm_error_notification_email', '' );
    223232            ?>
    224233
    225         <!-- Notifications Section -->
     234        <!-- Error Notifications Section -->
    226235        <div class="fcrm-section">
    227236            <div class="fcrm-section-header">
     
    306315            </div>
    307316        </div>
     317            <?php
     318        }
     319
     320        /**
     321         * Renders the settings page.
     322         *
     323         * Displays supported forms and CRM integrations.
     324         *
     325         * @return void
     326         */
     327        public function settings_page() {
     328            $source_shop_url = 'es' === strtok( get_locale(), '_' ) ? 'https://close.technology/' : 'https://close.technology/en/';
     329            $utm_source      = '?utm_source=WordPress+Settings&utm_medium=plugin&utm_campaign=link';
     330            ?>
    308331
    309332        <!-- Forms Supported Section -->
  • formscrm/tags/4.3.0/includes/assets/elementor-editor.js

    r3290078 r3460128  
    3030        };
    3131
    32         $form.html('Loading...');
     32        // Show loading state.
     33        $('#formscrm-connection-status').html(
     34            '<div style="padding: 12px; background: #f9f9f9; border-left: 4px solid #0073aa; border-radius: 4px;">' +
     35            '<div style="display: flex; align-items: center; gap: 8px;">' +
     36            '<strong style="color: #23282d;">' + 'API Connection Status:' + '</strong> ' +
     37            '<span style="display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 3px; background: #0073aa; color: white; font-size: 12px; font-weight: bold;">' +
     38            '<span style="margin-right: 5px;">⟳</span>' + 'Connecting...' +
     39            '</span>' +
     40            '</div></div>'
     41        );
     42        $form.html('<p style="text-align: center; padding: 20px; color: #666;">Loading...</p>');
    3343
    3444        $.ajax({
     
    3949            success: function(response) {
    4050                console.log('Response:', response);
    41                 $form.html(response.data);
     51               
     52                if (response.success) {
     53                    // Update connection status.
     54                    if (response.data.status_html) {
     55                        $('#formscrm-connection-status').html(response.data.status_html);
     56                    }
    4257
    43                 if ( !$('#fc_crm_module').val() ) {
    44                     // select first
    45                     let firstSelect = $form.find('select').first();
    46                     let firstOption = firstSelect.find('option').first();
    47                     firstSelect.val(firstOption.val());
     58                    // Update form content with modules and fields.
     59                    $form.html(response.data.form_html);
     60
     61                    if ( !$('#fc_crm_module').val() ) {
     62                        // select first
     63                        let firstSelect = $form.find('select').first();
     64                        let firstOption = firstSelect.find('option').first();
     65                        firstSelect.val(firstOption.val());
     66                    }
     67
     68                    $('.elementor-map-table[data-module="'+$('#fc_crm_module').val()+'"]').addClass('active');
     69                } else {
     70                    // Handle error response.
     71                    // Update connection status with error HTML if available.
     72                    if (response.data && response.data.status_html) {
     73                        $('#formscrm-connection-status').html(response.data.status_html);
     74                    } else {
     75                        let errorMessage = response.data && response.data.message ? response.data.message : (response.data || 'Connection failed');
     76                       
     77                        // Show error in status indicator.
     78                        $('#formscrm-connection-status').html(
     79                            '<div style="padding: 12px; background: #ffebee; border-left: 4px solid #dc3232; border-radius: 4px;">' +
     80                            '<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">' +
     81                            '<strong style="color: #23282d;">API Connection Status:</strong> ' +
     82                            '<span style="display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 3px; background: #dc3232; color: white; font-size: 12px; font-weight: bold;">' +
     83                            '<span style="margin-right: 5px;">✕</span>Error' +
     84                            '</span>' +
     85                            '</div>' +
     86                            '<p style="margin: 0; color: #dc3232; font-size: 12px;"><strong>Error:</strong> ' + errorMessage + '</p>' +
     87                            '</div>'
     88                        );
     89                    }
     90                    $form.html('');
    4891                }
    49 
    50                 $('.elementor-map-table[data-module="'+$('#fc_crm_module').val()+'"]').addClass('active');
    5192            },
    5293            error: function(xhr, status, error) {
    5394                console.error('AJAX Error:', error);
     95                // Show error in status indicator.
     96                $('#formscrm-connection-status').html(
     97                    '<div style="padding: 12px; background: #ffebee; border-left: 4px solid #dc3232; border-radius: 4px;">' +
     98                    '<div style="display: flex; align-items: center; gap: 8px;">' +
     99                    '<strong style="color: #23282d;">API Connection Status:</strong> ' +
     100                    '<span style="display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 3px; background: #dc3232; color: white; font-size: 12px; font-weight: bold;">' +
     101                    '<span style="margin-right: 5px;">✕</span>Error' +
     102                    '</span>' +
     103                    '</div>' +
     104                    '<p style="margin: 8px 0 0 0; color: #dc3232; font-size: 12px;"><strong>Error:</strong> Network error - ' + error + '</p>' +
     105                    '</div>'
     106                );
     107                $form.html('');
    54108            }
    55109        });
  • formscrm/tags/4.3.0/includes/assets/formscrm-admin.css

    r3424189 r3460128  
    114114/* Tabs Navigation */
    115115.fcrm-tabs-wrapper {
    116     margin-bottom: 2rem;
     116    margin-bottom: 1rem;
    117117    background: white;
    118118    border-radius: 1rem;
     
    413413    align-items: center;
    414414    justify-content: center;
    415     padding: 0.75rem 1.5rem;
     415    padding: 0.40rem 1rem;
    416416    font-size: 1rem;
    417417    font-weight: 600;
     
    555555    height: 1.5rem;
    556556}
     557
     558/* Error Log Specific Styles */
     559.fcrm-error-log-table-wrapper {
     560    overflow-x: auto;
     561    margin-top: 1.5rem;
     562}
     563
     564.fcrm-table {
     565    width: 100%;
     566    border-collapse: collapse;
     567    background: white;
     568}
     569
     570.fcrm-table thead {
     571    background: var(--fcrm-gray-50);
     572}
     573
     574.fcrm-table th {
     575    padding: 0.75rem 1rem;
     576    text-align: left;
     577    border-bottom: 2px solid var(--fcrm-gray-200);
     578    font-weight: 600;
     579    color: var(--fcrm-gray-700);
     580    font-size: 0.875rem;
     581    white-space: nowrap;
     582}
     583
     584.fcrm-table td {
     585    padding: 0.75rem 1rem;
     586    border-bottom: 1px solid var(--fcrm-gray-200);
     587    font-size: 0.875rem;
     588    color: var(--fcrm-gray-700);
     589}
     590
     591.fcrm-table tbody tr:hover {
     592    background: var(--fcrm-gray-50);
     593}
     594
     595.fcrm-button-small {
     596    padding: 0.375rem 0.75rem;
     597    font-size: 0.8125rem;
     598}
     599
     600.fcrm-button-danger {
     601    background: var(--fcrm-error);
     602    color: white;
     603}
     604
     605.fcrm-button-danger:hover {
     606    background: #dc2626;
     607    transform: translateY(-2px);
     608    box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
     609}
     610
     611.fcrm-status {
     612    display: inline-block;
     613    padding: 0.25rem 0.625rem;
     614    border-radius: 0.375rem;
     615    font-size: 0.75rem;
     616    font-weight: 600;
     617    text-transform: uppercase;
     618    letter-spacing: 0.025em;
     619}
     620
     621.fcrm-status-error {
     622    background: #fef2f2;
     623    color: #991b1b;
     624}
     625
     626.fcrm-status-success {
     627    background: #f0fdf4;
     628    color: #15803d;
     629}
     630
     631.fcrm-log-details {
     632    background: var(--fcrm-gray-50);
     633}
     634
     635.fcrm-log-details td {
     636    padding: 1.5rem;
     637}
     638
     639.fcrm-pagination {
     640    display: flex;
     641    justify-content: center;
     642    gap: 0.5rem;
     643    margin-top: 1.5rem;
     644    flex-wrap: wrap;
     645}
     646
     647.fcrm-notice-info {
     648    background: #eff6ff;
     649    border-left: 4px solid var(--fcrm-blue);
     650}
     651
     652.fcrm-notice-info .fcrm-notice-text {
     653    color: #1e40af;
     654}
     655
     656/* Error Log Filters */
     657.fcrm-error-log-filters {
     658    margin-bottom: 1.25rem;
     659    display: flex;
     660    gap: 1rem;
     661    align-items: center;
     662    justify-content: space-between;
     663    flex-wrap: wrap;
     664}
     665
     666.fcrm-error-log-filters-form {
     667    width: 84%;
     668    display: flex;
     669    gap: 0.625rem;
     670    align-items: center;
     671    flex-wrap: wrap;
     672}
     673
     674.wp-core-ui select.fcrm-filter-select {
     675    max-width: 200px;
     676    flex-shrink: 0;
     677}
     678
     679/* Stats Summary */
     680.fcrm-stats-summary {
     681    margin-bottom: 1.25rem;
     682    padding: 0.9375rem;
     683    background: var(--fcrm-gray-100);
     684    border-radius: 0.3125rem;
     685}
     686
     687/* Table Actions Column */
     688.fcrm-table-actions {
     689    text-align: center;
     690    width: 280px;
     691    min-width: 280px;
     692}
     693
     694.fcrm-table-actions .fcrm-button {
     695    margin-right: 0.3125rem;
     696}
     697
     698.fcrm-table-actions .fcrm-button:last-child {
     699    margin-right: 0;
     700}
     701
     702/* Form Subtitle */
     703.fcrm-form-subtitle {
     704    color: var(--fcrm-gray-600);
     705}
     706
     707/* Error Message Column */
     708.fcrm-error-message {
     709    max-width: 300px;
     710    overflow: hidden;
     711    text-overflow: ellipsis;
     712}
     713
     714/* Details Row */
     715.fcrm-log-details {
     716    display: none;
     717}
     718
     719.fcrm-details-cell {
     720    padding: 1.25rem;
     721    background: var(--fcrm-gray-50);
     722}
     723
     724.fcrm-details-grid {
     725    display: grid;
     726    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
     727    gap: 1.25rem;
     728}
     729
     730.fcrm-details-section h4 {
     731    margin-top: 0;
     732}
     733
     734.fcrm-details-box {
     735    background: white;
     736    padding: 0.9375rem;
     737    border-radius: 0.3125rem;
     738}
     739
     740.fcrm-details-box-scroll {
     741    max-height: 300px;
     742    overflow-y: auto;
     743}
     744
     745/* Lead Data Table */
     746.fcrm-lead-data-table {
     747    width: 100%;
     748}
     749
     750.fcrm-lead-data-name {
     751    padding: 0.3125rem;
     752    font-weight: 600;
     753}
     754
     755.fcrm-lead-data-value {
     756    padding: 0.3125rem;
     757}
     758
     759/* Code Blocks */
     760.fcrm-code {
     761    word-break: break-all;
     762    font-size: 0.6875rem;
     763}
     764
     765.fcrm-code-block {
     766    display: block;
     767    max-height: 150px;
     768    overflow-y: auto;
     769    background: var(--fcrm-gray-100);
     770    padding: 0.625rem;
     771    border-radius: 0.1875rem;
     772}
     773
     774/* Full Width Details */
     775.fcrm-details-full {
     776    grid-column: 1 / -1;
     777}
     778
     779/* Error Box */
     780.fcrm-error-box {
     781    background: #ffebee;
     782    padding: 0.9375rem;
     783    border-radius: 0.3125rem;
     784    border-left: 4px solid #d32f2f;
     785}
     786
     787@media (max-width: 768px) {
     788    .fcrm-error-log-filters {
     789        flex-direction: column;
     790        align-items: stretch !important;
     791    }
     792
     793    .fcrm-error-log-filters-form {
     794        flex-direction: column;
     795        width: 80%;
     796    }
     797
     798    .fcrm-filter-select {
     799        max-width: 100% !important;
     800    }
     801
     802    .fcrm-table {
     803        font-size: 0.75rem;
     804    }
     805
     806    .fcrm-table th,
     807    .fcrm-table td {
     808        padding: 0.5rem;
     809    }
     810
     811    .fcrm-table-actions {
     812        width: auto;
     813        min-width: auto;
     814    }
     815
     816    .fcrm-table-actions .fcrm-button {
     817        display: block;
     818        margin-bottom: 0.3125rem;
     819        margin-right: 0;
     820    }
     821
     822    .fcrm-details-grid {
     823        grid-template-columns: 1fr;
     824    }
     825}
     826
     827/* =================================================================
     828   Gravity Forms - Connected Feeds Column
     829   ================================================================= */
     830
     831/* Column Styling */
     832.gform-table .formscrm_feeds,
     833.wp-list-table .formscrm_feeds {
     834    min-width: 200px;
     835    max-width: 350px;
     836    vertical-align: top;
     837}
     838
     839.wp-list-table td.formscrm_feeds {
     840    padding: 10px;
     841}
     842
     843/* Status Badges - Using GravityForms native classes */
     844/* .gform-status-indicator.gform-status--active for connected state */
     845/* .gform-status-indicator.gform-status--inactive for disconnected/error state */
     846
     847/* Feeds Wrapper */
     848.formscrm_feeds {
     849
     850    .gform-status--active {
     851    border-radius: .75rem;
     852    gap: .25rem;
     853    padding-block: 0.0625rem;
     854    padding-inline: 0.375rem 0.5rem;
     855        font-size: 12px;
     856    }
     857}
     858
     859/* Feeds List */
     860.formscrm-feeds-list {
     861    display: flex;
     862    flex-direction: column;
     863    gap: 4px;
     864    margin-top: 8px;
     865    padding-top: 8px;
     866    border-top: 1px solid #e0e0e0;
     867}
     868
     869.formscrm-feed-item {
     870    display: flex;
     871    align-items: center;
     872    gap: 6px;
     873    line-height: 1.4;
     874}
     875
     876.formscrm-feed-name {
     877    font-weight: 500;
     878    color: #000;
     879}
     880
     881.formscrm-feed-crm {
     882    color: #666;
     883    font-size: 0.85em;
     884    font-style: italic;
     885}
     886
     887.formscrm-feed-total {
     888    margin-top: 4px;
     889    padding-top: 4px;
     890    border-top: 1px solid #ddd;
     891    color: #666;
     892    font-size: 0.85em;
     893}
     894
     895/* Mobile responsive */
     896@media screen and (max-width: 782px) {
     897    .gform-table .formscrm_feeds,
     898    .wp-list-table .formscrm_feeds {
     899        min-width: 150px;
     900    }
     901   
     902    .formscrm-status-badge {
     903        padding: 4px 8px;
     904        font-size: 0.85em;
     905    }
     906   
     907    .formscrm-feed-item {
     908        font-size: 0.9em;
     909    }
     910}
  • formscrm/tags/4.3.0/includes/assets/icons/icon-bell.svg

    r3424189 r3460128  
    22    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path>
    33</svg>
     4
  • formscrm/tags/4.3.0/includes/assets/icons/icon-check-simple.svg

    r3424189 r3460128  
    22    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
    33</svg>
     4
  • formscrm/tags/4.3.0/includes/assets/icons/icon-checkmark.svg

    r3424189 r3460128  
    22    <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
    33</svg>
     4
  • formscrm/tags/4.3.0/includes/assets/icons/icon-document.svg

    r3424189 r3460128  
    22    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
    33</svg>
     4
  • formscrm/tags/4.3.0/includes/assets/icons/icon-email.svg

    r3424189 r3460128  
    22    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
    33</svg>
     4
  • formscrm/tags/4.3.0/includes/assets/icons/icon-plus.svg

    r3424189 r3460128  
    22    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
    33</svg>
     4
  • formscrm/tags/4.3.0/includes/assets/icons/icon-slack.svg

    r3424189 r3460128  
    22    <path d="M6 8a2 2 0 1 0-4 0 2 2 0 0 0 4 0zm8 0a2 2 0 1 0-4 0 2 2 0 0 0 4 0zm8 0a2 2 0 1 0-4 0 2 2 0 0 0 4 0zM6 16a2 2 0 1 0-4 0 2 2 0 0 0 4 0zm8 0a2 2 0 1 0-4 0 2 2 0 0 0 4 0zm8 0a2 2 0 1 0-4 0 2 2 0 0 0 4 0z"/>
    33</svg>
     4
  • formscrm/tags/4.3.0/includes/assets/icons/icon-support.svg

    r3424189 r3460128  
    22    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192l-3.536 3.536M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z"></path>
    33</svg>
     4
  • formscrm/tags/4.3.0/includes/assets/icons/icon-users.svg

    r3424189 r3460128  
    22    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
    33</svg>
     4
  • formscrm/tags/4.3.0/includes/crm-library/class-crmlib-brevo.php

    r3415133 r3460128  
    238238        $list_id = isset( $settings['fc_crm_module'] ) ? (int) $settings['fc_crm_module'] : '';
    239239
     240        // List of standard Brevo contact fields that should be at root level.
     241        // All other fields are automatically treated as custom attributes.
     242        // See https://developers.brevo.com/reference/createcontact.
     243        $standard_fields = array(
     244            'email',
     245            'ext_id',
     246            'emailBlacklisted',
     247            'smsBlacklisted',
     248            'listIds',
     249            'unlinkListIds',
     250            'updateEnabled',
     251            'smtpBlacklistSender',
     252        );
     253
    240254        $subscriber            = array();
    241255        $subscriber['listIds'] = array( $list_id );
    242256        foreach ( $merge_vars as $element ) {
    243             if ( false === strpos( $element['name'], '|' ) ) {
    244                 $subscriber[ $element['name'] ] = $element['value'];
     257            $field_name  = $element['name'];
     258            $field_value = $element['value'];
     259
     260            // Check if field contains pipe separator (attributes|FIELDNAME).
     261            if ( false === strpos( $field_name, '|' ) ) {
     262                // No pipe - check if it's a standard field.
     263                if ( in_array( $field_name, $standard_fields, true ) ) {
     264                    // Standard field - add to root level.
     265                    $subscriber[ $field_name ] = $field_value;
     266                } else {
     267                    // Custom attribute - add to attributes object.
     268                    $subscriber['attributes'][ $field_name ] = $field_value;
     269                }
    245270            } else {
    246                 $key                              = str_replace( 'attributes|', '', $element['name'] );
    247                 $subscriber['attributes'][ $key ] = $element['value'];
     271                // Pipe found - extract attribute name and add to attributes.
     272                $key                              = str_replace( 'attributes|', '', $field_name );
     273                $subscriber['attributes'][ $key ] = $field_value;
    248274            }
    249275        }
  • formscrm/tags/4.3.0/includes/crm-library/class-crmlib-clientify.php

    r3424189 r3460128  
    33 * Clientify connect library
    44 *
    5  * Has functions to login, list fields and create leadº
     5 * Has functions to login, list fields and create lead
    66 *
    77 * @author   closemarketing
     
    874874            } elseif ( 'gdpr_accept' === $element['name'] || 'disclaimer' === $element['name'] ) {
    875875                $contact[ $element['name'] ] = empty( $element['value'] ) ? false : true;
     876            } elseif ( 'birthday' === $element['name'] ) {
     877                // Normalize birthday date format to YYYY-MM-DD.
     878                $normalized_date = formscrm_normalize_date_format( $element['value'] );
     879                if ( false !== $normalized_date ) {
     880                    $contact[ $element['name'] ] = $normalized_date;
     881                }
    876882            } else {
    877883                $contact[ $element['name'] ] = $element['value'];
  • formscrm/tags/4.3.0/includes/formscrm-library/class-contactform7.php

    r3415133 r3460128  
    3434        add_action( 'wpcf7_after_save', array( $this, 'crm_save_options' ) );
    3535        add_action( 'wpcf7_before_send_mail', array( $this, 'crm_process_entry' ) );
     36        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_autosubmit_assets' ) );
    3637    }
    3738
     
    6263        $cf7_crm_defaults = array();
    6364        $cf7_crm          = get_option( 'cf7_crm_' . $args->id(), $cf7_crm_defaults );
     65        $settings_module  = isset( $cf7_crm['fc_crm_module'] ) ? $cf7_crm['fc_crm_module'] : '';
    6466        ?>
    6567        <div class="metabox-holder">
    6668            <div class="cme-main-fields">
    6769                <p>
    68                     <select name="wpcf7-crm[fc_crm_type]" class="medium" onchange="jQuery(this).parents('form').submit();" id="fc_crm_type">
     70                    <label for="fc_crm_type"><?php esc_html_e( 'CRM Type:', 'formscrm' ); ?></label><br />
     71                    <select name="wpcf7-crm[fc_crm_type]" class="medium formscrm-autosubmit" id="fc_crm_type" data-formscrm-autosubmit="true">
    6972                        <?php
    7073                        foreach ( formscrm_get_choices() as $choice ) {
     
    7780                        ?>
    7881                    </select>
     82                    <span class="formscrm-saving-indicator" style="display:none; margin-left:10px; color:#46b450;">
     83                        <span class="dashicons dashicons-update-alt" style="animation: rotation 1s infinite linear;"></span>
     84                        <?php esc_html_e( 'Saving...', 'formscrm' ); ?>
     85                    </span>
    7986                </p>
    8087                <?php if ( isset( $cf7_crm['fc_crm_type'] ) && $cf7_crm['fc_crm_type'] ) { ?>
     
    126133                    ?>
    127134                    <p>
    128                         <select name="wpcf7-crm[fc_crm_module]" class="medium" onchange="jQuery(this).parents('form').submit();" id="fc_crm_module">
     135                        <label for="fc_crm_module"><?php esc_html_e( 'CRM Module:', 'formscrm' ); ?></label><br />
     136                        <select name="wpcf7-crm[fc_crm_module]" class="medium formscrm-autosubmit" id="fc_crm_module" data-formscrm-autosubmit="true">
    129137                            <?php
    130                             $settings_module = isset( $cf7_crm['fc_crm_module'] ) ? $cf7_crm['fc_crm_module'] : '';
    131                             foreach ( $this->crmlib->list_modules( $cf7_crm ) as $module ) {
     138                            $modules = $this->crmlib->list_modules( $cf7_crm );
     139                            foreach ( $modules as $module ) {
    132140                                $value = '';
    133141                                if ( ! empty( $module['value'] ) ) {
     
    145153                                echo '>' . esc_html( $module['label'] ) . '</option>';
    146154                            }
     155                            if ( empty( $settings_module ) || ! in_array( $settings_module, array_column( $modules, 'value' ), true ) ) {
     156                                $default_value            = ! empty( $modules[0]['value'] ) ? $modules[0]['value'] : '';
     157                                $settings_module          = $default_value;
     158                                $cf7_crm['fc_crm_module'] = $default_value;
     159                            }
    147160                            ?>
    148161                        </select>
     162                        <span class="formscrm-saving-indicator" style="display:none; margin-left:10px; color:#46b450;">
     163                            <span class="dashicons dashicons-update-alt" style="animation: rotation 1s infinite linear;"></span>
     164                            <?php esc_html_e( 'Saving...', 'formscrm' ); ?>
     165                        </span>
    149166                    </p>
    150167                    <p>
     
    155172            </div>
    156173            <?php
     174            // Show API connection status.
     175            if ( ! empty( $cf7_crm['fc_crm_type'] ) ) {
     176                formscrm_render_connection_status( $cf7_crm, 'html' );
     177            }
     178
    157179            if ( ! empty( $this->crmlib ) ) {
    158180                $login_crm = $this->crmlib->login( $cf7_crm );
    159181                if ( is_array( $login_crm ) && isset( $login_crm['status'] ) && 'error' === $login_crm['status'] ) {
    160                     echo '<p>' . esc_html__( 'We could not login to the CRM', 'formscrm' ) . ' ' . esc_html( $login_crm['message'] ) . '</p>';
    161182                    return;
    162183                }
    163184
    164185                if ( false === $login_crm ) {
    165                     echo '<p>' . esc_html__( 'We could not login to the CRM', 'formscrm' ) . '</p>';
    166186                    return;
    167187                }
    168188            }
    169189
    170             if ( isset( $cf7_crm['fc_crm_module'] ) && $cf7_crm['fc_crm_module'] ) {
    171                 $crm_fields  = $this->crmlib->list_fields( $cf7_crm, $cf7_crm['fc_crm_module'] );
     190            if ( $settings_module ) {
     191                $crm_fields  = $this->crmlib->list_fields( $cf7_crm, $settings_module );
    172192                $cf7_form    = WPCF7_ContactForm::get_instance( $args->id() );
    173193                $form_fields = ! empty( $cf7_form ) ? $cf7_form->scan_form_tags() : array();
     
    275295
    276296            $form_info = array(
    277                 'form_type' => 'Contact Form 7',
    278                 'form_id'   => $contact_form->id(),
    279                 'form_name' => $contact_form->title(),
     297                'form_type'       => 'contactform7',
     298                'form_type_title' => 'Contact Form 7',
     299                'form_id'         => $contact_form->id(),
     300                'form_name'       => $contact_form->title(),
    280301            );
    281302
     
    310331            }
    311332
     333            // Process dynamic values (shortcodes).
     334            $value = $this->fill_dynamic_value( $value, $submitted_data );
     335
    312336            $merge_vars[] = array(
    313337                'name'  => $crm_key,
     
    318342        return $merge_vars;
    319343    }
     344
     345    /**
     346     * Enqueue auto-submit assets for CF7 settings
     347     *
     348     * @param string $hook Hook suffix for the current admin page.
     349     * @return void
     350     */
     351    public function enqueue_autosubmit_assets( $hook ) {
     352        // Only load on CF7 edit pages.
     353        if ( 'toplevel_page_wpcf7' !== $hook ) {
     354            return;
     355        }
     356
     357        // Enqueue CSS (reusing admin styles for consistency).
     358        wp_enqueue_style(
     359            'formscrm-admin',
     360            FORMSCRM_PLUGIN_URL . 'includes/assets/formscrm-admin.css',
     361            array(),
     362            FORMSCRM_VERSION,
     363            'all'
     364        );
     365
     366        // Enqueue JavaScript.
     367        wp_enqueue_script(
     368            'formscrm-cf7-autosubmit',
     369            FORMSCRM_PLUGIN_URL . 'includes/assets/js/cf7-autosubmit.js',
     370            array(),
     371            FORMSCRM_VERSION,
     372            true
     373        );
     374    }
     375    /**
     376     * Fills dynamic value with shortcode support.
     377     *
     378     * Supports {id:field_name} syntax to reference other form field values.
     379     * Example: "Customer: {id:your-name} - {id:your-email}"
     380     *
     381     * @param string $field_value Field value that may contain shortcodes.
     382     * @param array  $submitted_data All submitted form data.
     383     * @return string Processed field value with shortcodes replaced.
     384     */
     385    private function fill_dynamic_value( $field_value, $submitted_data ) {
     386        if ( ! str_contains( $field_value, '{id:' ) ) {
     387            return $field_value;
     388        }
     389
     390        // Generate dynamic value.
     391        $matches = array();
     392        preg_match_all( '/{([^}]*)}/', $field_value, $matches );
     393        if ( empty( $matches[1] ) ) {
     394            return $field_value;
     395        }
     396
     397        foreach ( $matches[1] as $match ) {
     398            $field_options = explode( ':', $match );
     399            if ( ! isset( $field_options[1] ) || 'id' !== $field_options[0] ) {
     400                continue;
     401            }
     402
     403            $field_name = $field_options[1];
     404            if ( ! isset( $submitted_data[ $field_name ] ) ) {
     405                continue;
     406            }
     407
     408            // Get the value from submitted data.
     409            $entry_value = $submitted_data[ $field_name ];
     410
     411            // Handle array values (checkboxes, etc.).
     412            if ( is_array( $entry_value ) ) {
     413                $entry_value = implode( ', ', $entry_value );
     414            }
     415
     416            // Replace the shortcode with the actual value.
     417            $field_value = str_replace( '{' . $match . '}', $entry_value, $field_value );
     418        }
     419
     420        return $field_value;
     421    }
    320422}
    321423
  • formscrm/tags/4.3.0/includes/formscrm-library/class-elementor.php

    r3415133 r3460128  
    1616}
    1717
    18 use ElementorPro\Modules\Forms\Submissions\Database\Query;
    19 
    2018/**
    2119 * Action Class
     
    183181        );
    184182
     183        // API Connection Status indicator.
     184        $widget->add_control(
     185            'fc_connection_status_info',
     186            array(
     187                'type'            => \Elementor\Controls_Manager::RAW_HTML,
     188                'raw'             => '<div class="formscrm-elementor-status-container" id="formscrm-connection-status">' .
     189                    '<div style="padding: 12px; background: #f9f9f9; border-left: 4px solid #0073aa; border-radius: 4px; margin: 10px 0;">' .
     190                    '<div style="display: flex; align-items: center; gap: 8px;">' .
     191                    '<strong style="color: #23282d;">' . esc_html__( 'API Connection Status:', 'formscrm' ) . '</strong> ' .
     192                    '<span style="display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 3px; background: #999; color: white; font-size: 12px; font-weight: bold;">' .
     193                    '<span style="margin-right: 5px;">○</span>' . esc_html__( 'Not verified', 'formscrm' ) .
     194                    '</span>' .
     195                    '</div>' .
     196                    '<p style="margin: 8px 0 0 0; color: #666; font-size: 12px;">' . esc_html__( 'Click "Connect" button below to verify your CRM credentials', 'formscrm' ) . '</p>' .
     197                    '</div></div>',
     198                'content_classes' => 'formscrm-connection-status-wrapper',
     199                'separator'       => 'before',
     200            )
     201        );
     202
    185203        $widget->add_control(
    186204            'connect_crm',
     
    188206                'label'       => esc_html__( 'Connect CRM', 'formscrm' ),
    189207                'type'        => \Elementor\Controls_Manager::BUTTON,
    190                 'separator'   => 'before',
    191208                'button_type' => 'info',
    192209                'text'        => esc_html__( 'Connect', 'formscrm' ),
     
    281298
    282299            $form_info = array(
    283                 'form_type' => 'Elementor',
    284                 'form_id'   => isset( $settings['form_id'] ) ? $settings['form_id'] : ( isset( $settings['id'] ) ? $settings['id'] : '' ),
    285                 'form_name' => isset( $settings['form_name'] ) ? $settings['form_name'] : '',
     300                'form_type'       => 'elementor',
     301                'form_type_title' => 'Elementor',
     302                'form_id'         => isset( $settings['form_id'] ) ? $settings['form_id'] : ( isset( $settings['id'] ) ? $settings['id'] : '' ),
     303                'form_name'       => isset( $settings['form_name'] ) ? $settings['form_name'] : '',
    286304            );
    287305
  • formscrm/tags/4.3.0/includes/formscrm-library/class-gravityforms-widget.php

    r3415133 r3460128  
    2222    public function __construct() {
    2323        add_filter( 'gform_entry_detail_meta_boxes', array( $this, 'widget_resend_entries' ), 10, 3 );
     24        add_action( 'gform_post_add_feed', array( $this, 'clear_feeds_cache' ), 10, 2 );
     25        add_action( 'gform_post_update_feed', array( $this, 'clear_feeds_cache' ), 10, 2 );
     26        add_action( 'gform_post_delete_feed', array( $this, 'clear_feeds_cache' ), 10, 2 );
     27    }
     28
     29    /**
     30     * Get feeds with caching and error handling to improve performance.
     31     *
     32     * @param int $form_id Form ID.
     33     * @return array Array of feeds.
     34     */
     35    private function get_feeds_cached( $form_id ) {
     36        $cache_key = 'formscrm_feeds_' . $form_id;
     37        $feeds     = get_transient( $cache_key );
     38
     39        if ( false === $feeds ) {
     40            try {
     41                // Increase max execution time temporarily for this operation.
     42                $original_time_limit = ini_get( 'max_execution_time' );
     43                if ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) ) {
     44                    set_time_limit( 60 );
     45                }
     46
     47                $feeds = GFCRM::get_instance()->get_feeds( null, $form_id, 'formscrm', true );
     48
     49                // Restore original time limit.
     50                if ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) ) {
     51                    set_time_limit( (int) $original_time_limit );
     52                }
     53
     54                // Only cache if we got valid data.
     55                if ( is_array( $feeds ) ) {
     56                    set_transient( $cache_key, $feeds, 5 * MINUTE_IN_SECONDS );
     57                } else {
     58                    $feeds = array();
     59                }
     60            } catch ( Exception $e ) {
     61                $feeds = array();
     62            }
     63        }
     64
     65        return is_array( $feeds ) ? $feeds : array();
     66    }
     67
     68    /**
     69     * Clear feeds cache for a form.
     70     *
     71     * @param int   $feed_id Feed ID.
     72     * @param array $form_id Form ID.
     73     * @return void
     74     */
     75    public function clear_feeds_cache( $feed_id, $form_id ) {
     76        if ( ! empty( $form_id ) ) {
     77            $cache_key = 'formscrm_feeds_' . $form_id;
     78            delete_transient( $cache_key );
     79        }
    2480    }
    2581
     
    3490    public function widget_resend_entries( $meta_boxes, $entry, $form ) {
    3591        $meta_boxes['formscrm'] = array(
    36             'title'         => esc_html__( 'Resend Entry to CRM', 'formscrm' ),
     92            'title'         => esc_html__( 'FormsCRM: Resend', 'formscrm' ),
    3793            'callback'      => array( $this, 'resend_metabox' ),
    3894            'context'       => 'side',
     
    4298        return $meta_boxes;
    4399    }
     100
    44101    /**
    45102     * The callback used to echo the content to the meta box.
     
    48105     */
    49106    public function resend_metabox( $args ) {
    50         $html    = '';
    51         $action  = 'formscrm_process_feeds';
    52         $form    = ! empty( $args['form'] ) ? $args['form'] : array();
    53         $form_id = isset( $form['id'] ) ? (int) $form['id'] : 0;
    54         $entry   = ! empty( $args['entry'] ) ? $args['entry'] : array();
    55 
    56         $feeds = GFCRM::get_instance()->get_feeds( null, $form_id, 'formscrm', true );
    57 
    58         if ( rgpost( 'action' ) === $action ) {
    59             check_admin_referer( 'gforms_save_entry', 'gforms_save_entry' );
    60             $html .= '<p><strong>' . esc_html__( 'Feeds processed:', 'formscrm' ) . '</strong></p>';
    61             $html .= '<ul>';
    62 
     107        $html     = '';
     108        $action   = 'formscrm_process_feeds';
     109        $form     = ! empty( $args['form'] ) ? $args['form'] : array();
     110        $form_id  = isset( $form['id'] ) ? (int) $form['id'] : 0;
     111        $entry    = ! empty( $args['entry'] ) ? $args['entry'] : array();
     112        $entry_id = isset( $entry['id'] ) ? (int) $entry['id'] : 0;
     113
     114        // Use cached version with error handling.
     115        $feeds = $this->get_feeds_cached( $form_id );
     116
     117        // Check if action was triggered.
     118        $resend_action = isset( $_POST['formscrm_action'] ) ? sanitize_text_field( wp_unslash( $_POST['formscrm_action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified below.
     119
     120        if ( $action === $resend_action ) {
     121            // Verify nonce for security.
     122            if ( ! isset( $_POST['formscrm_resend_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['formscrm_resend_nonce'] ) ), 'formscrm_resend_entry_' . $entry_id ) ) {
     123                $html .= '<p style="color:red;">' . esc_html__( 'Security check failed. Please try again.', 'formscrm' ) . '</p>';
     124            } else {
     125                // Get selected feed(s).
     126                $selected_feeds = isset( $_POST['formscrm_selected_feeds'] ) && is_array( $_POST['formscrm_selected_feeds'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_POST['formscrm_selected_feeds'] ) ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above.
     127                $process_all    = in_array( 'all', $selected_feeds, true );
     128
     129                $html .= '<p><strong>' . esc_html__( 'Feeds processed:', 'formscrm' ) . '</strong></p>';
     130                $html .= '<ul>';
     131
     132                foreach ( $feeds as $feed ) {
     133                    if ( ! $feed['is_active'] || $form_id !== (int) $feed['form_id'] ) {
     134                        continue;
     135                    }
     136
     137                    // Process feed if "all" is selected or if this specific feed is selected.
     138                    if ( $process_all || in_array( (string) $feed['id'], $selected_feeds, true ) ) {
     139                        GFCRM::get_instance()->process_feed( $feed, $entry, $form );
     140                        $html .= '<li>';
     141                        $html .= sprintf(
     142                            // translators: %s is the name of the feed.
     143                            __( 'Feed: %s', 'formscrm' ),
     144                            isset( $feed['meta']['feedName'] ) ? $feed['meta']['feedName'] : $feed['id'],
     145                        );
     146                        $html .= '</li>';
     147                    }
     148                }
     149                $html .= '</ul>';
     150            }
     151        }
     152
     153        // Always show the form with available feeds.
     154        $html .= '<p>' . esc_html__( 'This will resend the entry to the CRM.', 'formscrm' ) . '</p>';
     155        $html .= '<p>' . esc_html__( 'Actual feeds actived:', 'formscrm' ) . '</p>';
     156        $html .= '<ul>';
     157
     158        $active_feeds = 0;
     159        foreach ( $feeds as $feed ) {
     160            if ( ! $feed['is_active'] || $form_id !== (int) $feed['form_id'] ) {
     161                continue;
     162            }
     163            ++$active_feeds;
     164            $html .= '<li>';
     165            $html .= sprintf(
     166                // translators: %s is the name of the feed.
     167                __( 'Feed: %s', 'formscrm' ),
     168                isset( $feed['meta']['feedName'] ) ? $feed['meta']['feedName'] : $feed['id'],
     169            );
     170            $html .= '</li>';
     171        }
     172        $html .= '</ul>';
     173
     174        if ( $active_feeds > 0 ) {
     175            $html .= '<br/>';
     176            $html .= '<form method="post" style="display:inline;">';
     177            $html .= wp_nonce_field( 'formscrm_resend_entry_' . $entry_id, 'formscrm_resend_nonce', true, false );
     178            $html .= '<input type="hidden" name="formscrm_action" value="' . esc_attr( $action ) . '" />';
     179            $html .= '<label for="formscrm_feed_select">' . esc_html__( 'Select Feeds to Resend', 'formscrm' ) . ':</label> ';
     180            $html .= '<select id="formscrm_feed_select" name="formscrm_selected_feeds[]" style="min-width:200px;">';
     181            $html .= '<option value="all">' . esc_html__( 'All feeds', 'formscrm' ) . '</option>';
    63182            foreach ( $feeds as $feed ) {
    64183                if ( ! $feed['is_active'] || $form_id !== (int) $feed['form_id'] ) {
    65184                    continue;
    66185                }
    67                 GFCRM::get_instance()->process_feed( $feed, $entry, $form );
    68                 $html .= '<li>';
    69                 $html .= sprintf(
    70                     // translators: %s is the name of the feed.
    71                     __( 'Feed: %s', 'formscrm' ),
    72                     isset( $feed['meta']['feedName'] ) ? $feed['meta']['feedName'] : $feed['id'],
     186                $feed_name = isset( $feed['meta']['feedName'] ) ? $feed['meta']['feedName'] : $feed['id'];
     187                $html     .= sprintf(
     188                    '<option value="%s">%s</option>',
     189                    esc_attr( $feed['id'] ),
     190                    esc_html( $feed_name )
    73191                );
    74                 $html .= '</li>';
    75             }
    76             $html .= '</ul>';
     192            }
     193            $html .= '</select><br/><br/>';
     194            $html .= sprintf(
     195                '<input type="submit" value="%s" class="button button-primary" />',
     196                esc_attr__( 'Resend Entry', 'formscrm' )
     197            );
     198            $html .= '</form>';
    77199        } else {
    78             $html .= '<p>' . esc_html__( 'This will resend the entry to the CRM.', 'formscrm' ) . '</p>';
    79             $html .= '<p>' . esc_html__( 'Actual feeds actived:', 'formscrm' ) . '</p>';
    80             $html .= '<ul>';
    81 
    82             foreach ( $feeds as $feed ) {
    83                 if ( ! $feed['is_active'] || $form_id !== (int) $feed['form_id'] ) {
    84                     continue;
    85                 }
    86                 $html .= '<li>';
    87                 $html .= sprintf(
    88                     // translators: %s is the name of the feed.
    89                     __( 'Feed: %s', 'formscrm' ),
    90                     isset( $feed['meta']['feedName'] ) ? $feed['meta']['feedName'] : $feed['id'],
    91                 );
    92                 $html .= '</li>';
    93             }
    94             $html .= '</ul>';
    95             $html .= '</br>';
    96             // Add the 'Process Feeds' button.
    97             $html .= sprintf(
    98                 '<input type="submit" value="%s" class="button" onclick="jQuery(\'#action\').val(\'%s\');" />',
    99                 __( 'Resend Entry', 'formscrm' ),
    100                 $action
    101             );
    102         }
    103         echo wp_kses_post( $html );
     200            $html .= '<p><em>' . esc_html__( 'No active feeds found for this form.', 'formscrm' ) . '</em></p>';
     201        }
     202
     203        echo wp_kses(
     204            $html,
     205            array(
     206                'p'      => array( 'style' => array() ),
     207                'strong' => array(),
     208                'ul'     => array(),
     209                'li'     => array(),
     210                'br'     => array(),
     211                'form'   => array(
     212                    'method' => array(),
     213                    'style'  => array(),
     214                ),
     215                'input'  => array(
     216                    'type'  => array(),
     217                    'name'  => array(),
     218                    'value' => array(),
     219                    'class' => array(),
     220                    'id'    => array(),
     221                ),
     222                'select' => array(
     223                    'name'     => array(),
     224                    'id'       => array(),
     225                    'multiple' => array(),
     226                    'style'    => array(),
     227                ),
     228                'option' => array(
     229                    'value' => array(),
     230                ),
     231                'label'  => array(
     232                    'for' => array(),
     233                ),
     234                'em'     => array(),
     235            )
     236        );
    104237    }
    105238}
  • formscrm/tags/4.3.0/includes/formscrm-library/class-gravityforms.php

    r3415133 r3460128  
    145145
    146146        $this->ensure_upgrade();
     147
     148        // Add custom columns to forms list.
     149        if ( is_admin() ) {
     150            add_filter( 'gform_form_list_columns', array( $this, 'add_feeds_column' ), 10 );
     151            add_action( 'gform_form_list_column_formscrm_feeds', array( $this, 'display_feeds_column' ), 10, 1 );
     152            add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_forms_list_styles' ) );
     153        }
     154    }
     155
     156    /**
     157     * Enqueue styles for forms list page
     158     *
     159     * @param string $hook Current admin page hook.
     160     * @return void
     161     */
     162    public function enqueue_forms_list_styles( $hook ) {
     163        // Only load on Gravity Forms pages.
     164        if ( 'toplevel_page_gf_edit_forms' === $hook || strpos( $hook, 'gf_' ) !== false ) {
     165            wp_enqueue_style(
     166                'formscrm-forms-list',
     167                FORMSCRM_PLUGIN_URL . 'includes/assets/formscrm-admin.css',
     168                array(),
     169                FORMSCRM_VERSION
     170            );
     171        }
    147172    }
    148173
     
    260285        $fields = $this->get_crm_fields( true, array(), 'settings' );
    261286
     287        // API Connection Status.
     288        $fields = array_merge(
     289            $fields,
     290            array(
     291                array(
     292                    'label' => __( 'API Connection Status', 'formscrm' ),
     293                    'type'  => 'connection_status',
     294                    'name'  => 'fc_crm_connection_status',
     295                ),
     296            ),
     297        );
     298
    262299        // Expert Mode.
    263300        $fields = array_merge(
     
    309346
    310347        return $api_key_field . '</br>' . $caption;
     348    }
     349
     350    /**
     351     * Settings Connection Status field.
     352     *
     353     * Renders the API connection status indicator for plugin settings.
     354     *
     355     * @param array $field   Field configuration.
     356     * @param bool  $display Whether to display or return the HTML.
     357     * @return string HTML output.
     358     */
     359    public function settings_connection_status( $field, $display = true ) {
     360        $settings  = $this->get_plugin_settings();
     361        $help_text = __( 'Save settings and reload the page to test the connection.', 'formscrm' );
     362        $html      = formscrm_get_connection_status_html( $settings, 'badge', $help_text );
     363
     364        if ( $display ) {
     365            formscrm_render_connection_status( $settings, 'badge', $help_text );
     366        }
     367
     368        return $html;
     369    }
     370
     371    /**
     372     * Settings Feed Connection Status field.
     373     *
     374     * Renders the API connection status indicator for feed settings.
     375     *
     376     * @param array $field   Field configuration.
     377     * @param bool  $display Whether to display or return the HTML.
     378     * @return string HTML output.
     379     */
     380    public function settings_feed_connection_status( $field, $display = true ) {
     381        $settings    = $this->get_api_settings_custom();
     382        $status_data = formscrm_check_connection_status( $settings );
     383        $help_text   = '';
     384
     385        // Show help text only for error states.
     386        if ( 'disconnected' === $status_data['status'] || 'error' === $status_data['status'] ) {
     387            $help_text = __( 'Please check your CRM credentials in the FormsCRM settings.', 'formscrm' );
     388        }
     389
     390        $html = formscrm_build_status_html( $status_data, 'badge', $help_text );
     391
     392        if ( $display ) {
     393            formscrm_render_connection_status( $settings, 'badge', $help_text );
     394        }
     395
     396        return $html;
    311397    }
    312398
     
    414500        $login_crm       = $this->login_api_crm();
    415501
     502        // Add connection status field.
     503        $crm_feed_fields[] = array(
     504            'name'  => 'fc_feed_connection_status',
     505            'label' => __( 'API Connection Status', 'formscrm' ),
     506            'type'  => 'feed_connection_status',
     507        );
     508
    416509        if ( is_array( $login_crm ) && isset( $login_crm['status'] ) && 'error' === $login_crm['status'] ) {
    417             $crm_feed_fields[] = array(
    418                 'name'  => 'fc_login_result',
    419                 'label' => __( 'We could not login to the CRM', 'formscrm' ) . ' ' . $login_crm['message'],
    420                 'type'  => 'hidden',
    421             );
    422510            return $crm_feed_fields;
    423511        }
    424512
    425513        if ( false === $login_crm ) {
    426             $crm_feed_fields[] = array(
    427                 'name'  => 'fc_login_result',
    428                 'label' => __( 'We could not login to the CRM', 'formscrm' ),
    429                 'type'  => 'hidden',
    430             );
     514            // Connection status field already added above, no additional fields needed.
     515            return $crm_feed_fields;
    431516        } else {
    432517            $module = $this->get_actual_feed_value( 'fc_crm_module', $feed_settings );
     
    576661        update_option( 'fc_crm_upgrade', 1 );
    577662        return true;
     663    }
     664
     665    /**
     666     * Add feeds column to forms list
     667     *
     668     * @param array $columns Existing columns.
     669     * @return array Modified columns.
     670     */
     671    public function add_feeds_column( $columns ) {
     672        $columns['formscrm_feeds'] = esc_html__( 'Connected Feeds', 'formscrm' );
     673        return $columns;
     674    }
     675
     676    /**
     677     * Display feeds column content
     678     *
     679     * @param array $form Form object.
     680     * @return void
     681     */
     682    public function display_feeds_column( $form ) {
     683        // Get form ID from array or object.
     684        $form_id = 0;
     685
     686        if ( is_array( $form ) ) {
     687            $form_id = isset( $form['id'] ) ? absint( $form['id'] ) : 0;
     688        } elseif ( is_object( $form ) ) {
     689            $form_id = isset( $form->id ) ? absint( $form->id ) : 0;
     690        }
     691
     692        // If no form ID, show disconnected.
     693        if ( ! $form_id ) {
     694            echo '<span class="gform-status-indicator gform-status--inactive">● ' . esc_html__( 'Disconnected', 'formscrm' ) . '</span>';
     695            return;
     696        }
     697
     698        try {
     699            // Get feeds for this form.
     700            $feeds = $this->get_feeds( $form_id );
     701
     702            // No feeds - show Disconnected.
     703            if ( empty( $feeds ) || ! is_array( $feeds ) ) {
     704                return;
     705            }
     706
     707            // Has feeds - show Connected.
     708            $feed_count = count( $feeds );
     709
     710            echo '<div class="formscrm-feeds-wrapper">';
     711            echo '<span class="gform-status-indicator gform-status--active">' . esc_html__( 'Connected', 'formscrm' ) . '</span>';
     712
     713            // Show feed details.
     714            echo '<div class="formscrm-feeds-list">';
     715
     716            foreach ( $feeds as $feed ) {
     717                if ( ! is_array( $feed ) || empty( $feed['meta'] ) ) {
     718                    continue;
     719                }
     720
     721                $feed_name = isset( $feed['meta']['feedName'] ) ? $feed['meta']['feedName'] : __( 'Unnamed Feed', 'formscrm' );
     722                $crm_type  = '';
     723
     724                // Get CRM type.
     725                if ( ! empty( $feed['meta']['fc_crm_custom_type'] ) && 'no' !== $feed['meta']['fc_crm_custom_type'] ) {
     726                    $crm_type = $feed['meta']['fc_crm_custom_type'];
     727                } else {
     728                    $settings = $this->get_plugin_settings();
     729                    if ( ! empty( $settings['fc_crm_type'] ) ) {
     730                        $crm_type = $settings['fc_crm_type'];
     731                    }
     732                }
     733
     734                $is_active = ! empty( $feed['is_active'] );
     735                $status    = $is_active ? '✓' : '✗';
     736                $color     = $is_active ? '#46b450' : '#dc3232';
     737                $title     = $is_active ? __( 'Active', 'formscrm' ) : __( 'Inactive', 'formscrm' );
     738
     739                echo '<div class="formscrm-feed-item">';
     740                printf(
     741                    '<span style="color: %s; font-weight: bold;" title="%s">%s</span> ',
     742                    esc_attr( $color ),
     743                    esc_attr( $title ),
     744                    esc_html( $status )
     745                );
     746                    echo '<span class="formscrm-feed-name">' . esc_html( $feed_name ) . '</span>';
     747
     748                if ( ! empty( $crm_type ) ) {
     749                    echo ' <span class="formscrm-feed-crm">(' . esc_html( ucfirst( $crm_type ) ) . ')</span>';
     750                }
     751                echo '</div>';
     752            }
     753
     754            // Show total if more than 1.
     755            if ( $feed_count > 1 ) {
     756                echo '<div class="formscrm-feed-total">';
     757                printf(
     758                    /* translators: %d: number of feeds */
     759                    esc_html__( 'Total: %d feeds', 'formscrm' ),
     760                    absint( $feed_count )
     761                );
     762                echo '</div>';
     763            }
     764
     765            echo '</div>'; // .formscrm-feeds-list
     766            echo '</div>'; // .formscrm-feeds-wrapper
     767        } catch ( Exception $e ) {
     768            echo '<span class="gform-status-indicator gform-status--inactive">● ' . esc_html__( 'Error', 'formscrm' ) . '</span>';
     769        }
    578770    }
    579771
     
    681873
    682874            $form_info = array(
    683                 'form_type' => 'Gravity Forms',
    684                 'form_id'   => isset( $form['id'] ) ? $form['id'] : '',
    685                 'form_name' => isset( $form['title'] ) ? $form['title'] : '',
    686                 'entry_id'  => isset( $entry['id'] ) ? $entry['id'] : '',
     875                'form_type'       => 'gravityforms',
     876                'form_type_title' => 'Gravity Forms',
     877                'form_id'         => isset( $form['id'] ) ? $form['id'] : '',
     878                'form_name'       => isset( $form['title'] ) ? $form['title'] : '',
     879                'entry_id'        => isset( $entry['id'] ) ? $entry['id'] : '',
    687880            );
    688881
  • formscrm/tags/4.3.0/includes/formscrm-library/class-woocommerce.php

    r3415133 r3460128  
    194194            'id'   => 'wc_settings_formscrm_section_end',
    195195        );
     196
     197        // Show API connection status.
     198        if ( ! empty( $wc_formscrm['fc_crm_type'] ) ) {
     199            formscrm_render_connection_status( $wc_formscrm, 'notice' );
     200        }
     201
    196202        if ( ! empty( $this->crmlib ) && ! empty( $wc_formscrm ) ) {
    197203            $login_crm = $this->crmlib->login( $wc_formscrm );
    198204            if ( is_array( $login_crm ) && isset( $login_crm['status'] ) && 'error' === $login_crm['status'] ) {
    199                 echo '<div class="notice notice-error"><p>' . esc_html__( 'We could not login to the CRM', 'formscrm' ) . ' ' . esc_html( $login_crm['message'] ) . '</p></div>';
    200205                return $settings_crm;
    201206            }
    202207
    203208            if ( false === $login_crm ) {
    204                 echo '<div class="notice notice-error"><p>' . esc_html__( 'We could not login to the CRM', 'formscrm' ) . '</p></div>';
    205209                return $settings_crm;
    206210            }
     
    261265            if ( 'error' === $response_result['status'] ) {
    262266                $form_info = array(
    263                     'form_type' => 'WooCommerce',
    264                     'form_id'   => 'checkout',
    265                     'form_name' => 'WooCommerce Checkout',
    266                     'entry_id'  => $order_id,
     267                    'form_type'       => 'woocommerce',
     268                    'form_type_title' => 'WooCommerce',
     269                    'form_id'         => 'checkout',
     270                    'form_name'       => 'WooCommerce Checkout',
     271                    'entry_id'        => $order_id,
    267272                );
    268273
  • formscrm/tags/4.3.0/includes/formscrm-library/class-wpforms.php

    r3415133 r3460128  
    174174                if ( 'error' === $api_status ) {
    175175                    $form_info = array(
    176                         'form_type' => 'WPForms',
    177                         'form_id'   => $form_id,
    178                         'form_name' => isset( $form_data['settings']['form_title'] ) ? $form_data['settings']['form_title'] : '',
    179                         'entry_id'  => $entry_id,
     176                        'form_type'       => 'wpforms',
     177                        'form_type_title' => 'WPForms',
     178                        'form_id'         => $form_id,
     179                        'form_name'       => isset( $form_data['settings']['form_title'] ) ? $form_data['settings']['form_title'] : '',
     180                        'entry_id'        => $entry_id,
    180181                    );
    181182                    formscrm_alert_error( $settings['fc_crm_type'], 'Error ' . $api_message, $merge_vars, '', '', $form_info );
     
    517518     */
    518519    public function output_options( $connection_id = '', $connection = array() ) {
    519 
    520         // Double opt in and a welcome email are defined in the List options on FormsCRM.
    521         // They can't be controlled via the API.
    522         return '';
     520        $account_id = ! empty( $connection['account_id'] ) ? $connection['account_id'] : '';
     521        $html       = '';
     522
     523        if ( ! empty( $account_id ) ) {
     524            $settings = $this->api_connect( $account_id );
     525            if ( is_array( $settings ) && ! empty( $settings['fc_crm_type'] ) ) {
     526                // Get connection status and display it prominently.
     527                $status_html = formscrm_get_connection_status_html( $settings, 'badge' );
     528
     529                // Wrap in a visible container with proper styling.
     530                $html  = '<div class="wpforms-provider-connection-status" style="margin: 15px 0; padding: 12px; background: #f9f9f9; border-radius: 4px; border-left: 4px solid #0073aa;">';
     531                $html .= '<div style="display: flex; align-items: center; justify-content: space-between;">';
     532                $html .= '<div>';
     533                $html .= '<strong style="display: block; margin-bottom: 5px; color: #23282d;">' . esc_html__( 'CRM Connection:', 'formscrm' ) . '</strong>';
     534                $html .= $status_html;
     535                $html .= '</div>';
     536
     537                // Add CRM type info.
     538                if ( ! empty( $settings['fc_crm_type'] ) ) {
     539                    $html .= '<div style="text-align: right;">';
     540                    $html .= '<span style="display: block; font-size: 11px; color: #666; text-transform: uppercase; letter-spacing: 0.5px;">' . esc_html__( 'CRM Type', 'formscrm' ) . '</span>';
     541                    $html .= '<strong style="font-size: 14px; color: #0073aa;">' . esc_html( ucfirst( $settings['fc_crm_type'] ) ) . '</strong>';
     542                    $html .= '</div>';
     543                }
     544
     545                $html .= '</div>';
     546                $html .= '</div>';
     547            }
     548        }
     549
     550        return $html;
    523551    }
    524552
  • formscrm/tags/4.3.0/includes/formscrm-library/elementor-ajax.php

    r3415133 r3460128  
    5151    // 1. Check connection to CRM
    5252    $crmtype = isset( $_POST['crmSettings']['fc_crm_type'] ) ? sanitize_text_field( wp_unslash( $_POST['crmSettings']['fc_crm_type'] ) ) : '';
    53     $crmlib  = null;
    5453
    5554    if ( empty( $crmtype ) ) {
     
    5756    }
    5857
    59     $crmname      = strtolower( $crmtype );
    60     $crmclassname = str_replace( ' ', '', $crmname );
    61     $crmclassname = 'CRMLIB_' . strtoupper( $crmclassname );
    62     $crmname      = str_replace( ' ', '_', $crmname );
    63 
    64     $array_path = formscrm_get_crmlib_path();
    65     if ( isset( $array_path[ $crmname ] ) ) {
    66         include_once $array_path[ $crmname ];
    67     }
    68 
    69     formscrm_debug_message( $array_path[ $crmname ] );
    70 
    71     if ( ! class_exists( $crmclassname ) ) {
    72         wp_send_json_error( __( 'Class not found', 'formscrm' ) );
    73     }
    74 
    75     $crmlib = new $crmclassname();
     58    // Load CRM library class using helper function.
     59    $crmlib = formscrm_get_api_class( $crmtype );
     60
     61    if ( ! $crmlib ) {
     62        wp_send_json_error( __( 'Could not load CRM library', 'formscrm' ) );
     63    }
    7664
    7765    $crm_settings_raw = isset( $_POST['crmSettings'] ) ? wp_unslash( $_POST['crmSettings'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized in formscrm_elementor_process_settings().
    7866    $post_data        = formscrm_elementor_process_settings( $crm_settings_raw );
     67
     68    // Check connection status.
     69    $status_data = formscrm_check_connection_status( $post_data );
     70
     71    // Store connection status HTML separately.
     72    $status_html = formscrm_get_connection_status_html( $post_data, 'elementor' );
     73
     74    // If connection failed, return error with status HTML.
     75    if ( 'connected' !== $status_data['status'] ) {
     76        $error_msg = __( 'Could not connect to CRM', 'formscrm' );
     77        if ( ! empty( $status_data['error_message'] ) ) {
     78            $error_msg .= ': ' . $status_data['error_message'];
     79        }
     80
     81        // Return error with status HTML for JavaScript to handle.
     82        wp_send_json_error(
     83            array(
     84                'message'     => $error_msg,
     85                'status_html' => $status_html,
     86            )
     87        );
     88    }
    7989
    8090    // 2. Show modules dropdown
     
    196206    }
    197207
    198     wp_send_json_success( ob_get_clean() );
     208    $form_html = ob_get_clean();
     209
     210    // Return success with both form HTML and status HTML.
     211    wp_send_json_success(
     212        array(
     213            'form_html'   => $form_html,
     214            'status_html' => $status_html,
     215        )
     216    );
    199217}
  • formscrm/tags/4.3.0/includes/formscrm-library/helpers-functions.php

    r3425471 r3460128  
    1616     *
    1717     * @param string $crm_type Type of CRM.
    18      * @return object|void
     18     * @return object|null
    1919     */
    2020    function formscrm_get_api_class( $crm_type ) {
    21         $crmname      = strtolower( $crm_type );
     21        // Normalize CRM type.
     22        $crmname      = strtolower( trim( $crm_type ) );
    2223        $crmclassname = str_replace( ' ', '', $crmname );
    23         $crmclassname = 'CRMLIB_' . strtoupper( $crmclassname );
     24        $crmclassname = 'CRMLIB_' . ucfirst( $crmclassname );
    2425        $crmname      = str_replace( ' ', '_', $crmname );
    2526
     27        formscrm_debug_message( 'Attempting to load CRM: ' . $crmname );
     28
    2629        $array_path = formscrm_get_crmlib_path();
    2730
     31        // Log available CRM paths for debugging.
     32        formscrm_debug_message( 'Available CRM paths: ' . wp_json_encode( array_keys( $array_path ) ) );
     33
    2834        if ( isset( $array_path[ $crmname ] ) ) {
    29             include_once $array_path[ $crmname ];
    30             formscrm_debug_message( $array_path[ $crmname ] );
    31         }
    32 
     35            $file_path = $array_path[ $crmname ];
     36
     37            // Verify file exists before including.
     38            if ( ! file_exists( $file_path ) ) {
     39                formscrm_debug_message( 'ERROR: CRM library file not found: ' . $file_path );
     40                return null;
     41            }
     42
     43            include_once $file_path;
     44            formscrm_debug_message( 'Included CRM library: ' . $file_path );
     45        } else {
     46            formscrm_debug_message( 'ERROR: CRM path not registered for: ' . $crmname );
     47            return null;
     48        }
     49
     50        // Verify class exists after including file.
    3351        if ( class_exists( $crmclassname ) ) {
     52            formscrm_debug_message( 'Successfully created instance of: ' . $crmclassname );
    3453            return new $crmclassname();
    3554        }
     55
     56        formscrm_debug_message( 'ERROR: CRM class not found: ' . $crmclassname );
     57        return null;
     58    }
     59}
     60
     61if ( ! function_exists( 'formscrm_get_crm_settings' ) ) {
     62    /**
     63     * Get CRM settings from WordPress options
     64     *
     65     * @param string $form_type Type of form (gravity, woocommerce, etc).
     66     * @return array Settings array.
     67     */
     68    function formscrm_get_crm_settings( $form_type = '' ) {
     69        $settings = array();
     70
     71        // Try to get settings based on form type.
     72        if ( 'gravity' === $form_type || 'gravityforms' === $form_type ) {
     73            $settings = get_option( 'gravityformsaddon_formscrm_settings', array() );
     74        } elseif ( 'woocommerce' === $form_type ) {
     75            $settings = get_option( 'wc_formscrm', array() );
     76        } else {
     77            // Default to Gravity Forms settings as fallback.
     78            $settings = get_option( 'gravityformsaddon_formscrm_settings', array() );
     79
     80            // Fallback to WooCommerce settings when Gravity Forms settings are empty.
     81            if ( empty( $settings ) ) {
     82                $settings = get_option( 'wc_formscrm', array() );
     83            }
     84        }
     85
     86        formscrm_debug_message( 'Retrieved CRM settings for form type: ' . $form_type );
     87
     88        return $settings;
    3689    }
    3790}
     
    106159     */
    107160    function formscrm_alert_error( $crm, $error, $data, $url = '', $json = '', $form_info = array() ) {
     161        // Log error to database.
     162        global $formscrm_error_log;
     163        if ( isset( $formscrm_error_log ) && method_exists( $formscrm_error_log, 'insert_log' ) ) {
     164            $formscrm_error_log->insert_log( $crm, $error, $data, $url, $json, $form_info );
     165        }
     166
    108167        // Get custom email or fallback to admin email.
    109168        $custom_email = get_option( 'formscrm_error_notification_email', '' );
     
    436495}
    437496
     497if ( ! function_exists( 'formscrm_normalize_date_format' ) ) {
     498    /**
     499     * Normalizes date to YYYY-MM-DD format required by APIs like Clientify.
     500     *
     501     * Supported input formats:
     502     * - dd/mm/yyyy (European format)
     503     * - dd-mm-yyyy (European format with dashes)
     504     * - dd.mm.yyyy (European format with dots)
     505     * - yyyy-mm-dd (ISO format - already correct)
     506     * - yyyy/mm/dd (ISO format with slashes)
     507     * - mm/dd/yyyy (US format)
     508     * - mm-dd-yyyy (US format with dashes)
     509     * - Unix timestamps
     510     *
     511     * @param string $date_value The date value to normalize.
     512     * @return string|false Normalized date in YYYY-MM-DD format or false if invalid.
     513     */
     514    function formscrm_normalize_date_format( $date_value ) {
     515        if ( empty( $date_value ) ) {
     516            return false;
     517        }
     518
     519        $date_value = trim( $date_value );
     520
     521        // Already in correct format YYYY-MM-DD.
     522        if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date_value ) ) {
     523            return $date_value;
     524        }
     525
     526        // Handle Unix timestamp.
     527        if ( is_numeric( $date_value ) && strlen( $date_value ) >= 8 ) {
     528            $timestamp = (int) $date_value;
     529            $date      = gmdate( 'Y-m-d', $timestamp );
     530            if ( false !== $date ) {
     531                return $date;
     532            }
     533        }
     534
     535        // European format: dd/mm/yyyy or dd-mm-yyyy or dd.mm.yyyy.
     536        if ( preg_match( '/^(\d{1,2})[\/\-\.](\d{1,2})[\/\-\.](\d{4})$/', $date_value, $matches ) ) {
     537            $day   = (int) $matches[1];
     538            $month = (int) $matches[2];
     539            $year  = (int) $matches[3];
     540
     541            // Validate date components.
     542            if ( $day > 31 || $month > 12 ) {
     543                // Could be US format mm/dd/yyyy, try swapping.
     544                if ( $month <= 31 && $day <= 12 ) {
     545                    $temp  = $day;
     546                    $day   = $month;
     547                    $month = $temp;
     548                }
     549            }
     550
     551            // Validate the date.
     552            if ( checkdate( $month, $day, $year ) ) {
     553                return sprintf( '%04d-%02d-%02d', $year, $month, $day );
     554            }
     555        }
     556
     557        // ISO format with slashes: yyyy/mm/dd.
     558        if ( preg_match( '/^(\d{4})[\/](\d{1,2})[\/](\d{1,2})$/', $date_value, $matches ) ) {
     559            $year  = (int) $matches[1];
     560            $month = (int) $matches[2];
     561            $day   = (int) $matches[3];
     562
     563            if ( checkdate( $month, $day, $year ) ) {
     564                return sprintf( '%04d-%02d-%02d', $year, $month, $day );
     565            }
     566        }
     567
     568        // Try PHP's strtotime as last resort for other formats.
     569        $timestamp = strtotime( $date_value );
     570        if ( false !== $timestamp && -1 !== $timestamp ) {
     571            return gmdate( 'Y-m-d', $timestamp );
     572        }
     573
     574        return false;
     575    }
     576}
     577
    438578if ( ! function_exists( 'formscrm_get_svg_icon' ) ) {
    439579    /**
     
    458598    }
    459599}
     600
     601if ( ! function_exists( 'formscrm_get_connection_status_html' ) ) {
     602    /**
     603     * Get HTML for API connection status indicator.
     604     *
     605     * @param array  $settings    CRM settings array with fc_crm_type and credentials.
     606     * @param string $output_type Output type: 'html', 'notice', 'badge', or 'elementor'.
     607     * @param string $help_text   Optional help text to display below status.
     608     * @return string HTML output for the connection status.
     609     */
     610    function formscrm_get_connection_status_html( $settings, $output_type = 'html', $help_text = '' ) {
     611        $status_data = formscrm_check_connection_status( $settings );
     612
     613        return formscrm_build_status_html( $status_data, $output_type, $help_text );
     614    }
     615}
     616
     617if ( ! function_exists( 'formscrm_check_connection_status' ) ) {
     618    /**
     619     * Check connection status and return status data array.
     620     *
     621     * @param array $settings CRM settings array with fc_crm_type and credentials.
     622     * @return array Status data with keys: status, text, color, icon, error_message, crm_type.
     623     */
     624    function formscrm_check_connection_status( $settings ) {
     625        $crm_type = isset( $settings['fc_crm_type'] ) ? $settings['fc_crm_type'] : '';
     626        $data     = array(
     627            'status'        => 'unknown',
     628            'text'          => __( 'Not configured', 'formscrm' ),
     629            'color'         => '#999999',
     630            'icon'          => '○',
     631            'error_message' => '',
     632            'crm_type'      => $crm_type,
     633        );
     634
     635        if ( empty( $crm_type ) ) {
     636            return $data;
     637        }
     638
     639        $crmlib = formscrm_get_api_class( $crm_type );
     640
     641        if ( ! isset( $crmlib ) || ! method_exists( $crmlib, 'login' ) ) {
     642            return $data;
     643        }
     644
     645        $login_result = $crmlib->login( $settings );
     646        $login_status = isset( $login_result['status'] ) ? $login_result['status'] : '';
     647
     648        if ( is_array( $login_result ) && 'error' === $login_status ) {
     649            $data['status']        = 'error';
     650            $data['text']          = __( 'Error', 'formscrm' );
     651            $data['color']         = '#dc3232';
     652            $data['icon']          = '✕';
     653            $data['error_message'] = isset( $login_result['message'] ) ? $login_result['message'] : '';
     654        } elseif ( true === $login_result || 'ok' === $login_status ) {
     655            $data['status'] = 'connected';
     656            $data['text']   = __( 'Connected', 'formscrm' );
     657            $data['color']  = '#46b450';
     658            $data['icon']   = '✓';
     659        } else {
     660            $data['status'] = 'disconnected';
     661            $data['text']   = __( 'Disconnected', 'formscrm' );
     662            $data['color']  = '#dc3232';
     663            $data['icon']   = '✕';
     664        }
     665
     666        return $data;
     667    }
     668}
     669
     670if ( ! function_exists( 'formscrm_build_status_html' ) ) {
     671    /**
     672     * Build HTML from status data.
     673     *
     674     * @param array  $status_data Status data from formscrm_check_connection_status().
     675     * @param string $output_type Output type: 'html', 'notice', 'badge', or 'elementor'.
     676     * @param string $help_text   Optional help text to display below status.
     677     * @return string HTML output.
     678     */
     679    function formscrm_build_status_html( $status_data, $output_type = 'html', $help_text = '' ) {
     680        $status        = $status_data['status'];
     681        $status_text   = $status_data['text'];
     682        $status_color  = $status_data['color'];
     683        $status_icon   = $status_data['icon'];
     684        $error_message = $status_data['error_message'];
     685        $crm_type      = $status_data['crm_type'];
     686        $html          = '';
     687
     688        switch ( $output_type ) {
     689            case 'notice':
     690                $notice_class = 'connected' === $status ? 'notice-success' : ( 'unknown' === $status ? 'notice-warning' : 'notice-error' );
     691                $html         = '<div class="notice ' . esc_attr( $notice_class ) . '" style="padding: 10px;">';
     692                $html        .= '<strong>' . esc_html__( 'API Connection Status:', 'formscrm' ) . '</strong> ';
     693                $html        .= '<span style="color: ' . esc_attr( $status_color ) . '; font-weight: bold;">';
     694                $html        .= esc_html( $status_icon ) . ' ' . esc_html( $status_text );
     695                $html        .= '</span>';
     696
     697                if ( ! empty( $crm_type ) ) {
     698                    $html .= ' <span style="color: #666;">(' . esc_html( ucfirst( $crm_type ) ) . ')</span>';
     699                }
     700
     701                if ( ! empty( $error_message ) ) {
     702                    $html .= '<br/><span style="color: #dc3232; font-size: 12px;">' . esc_html( $error_message ) . '</span>';
     703                }
     704
     705                $html .= '</div>';
     706                break;
     707
     708            case 'elementor':
     709                $bg_color     = 'connected' === $status ? '#f9f9f9' : ( 'unknown' === $status ? '#f9f9f9' : '#ffebee' );
     710                $border_color = 'connected' === $status ? '#46b450' : ( 'unknown' === $status ? '#0073aa' : '#dc3232' );
     711                $badge_color  = 'connected' === $status ? '#46b450' : ( 'unknown' === $status ? '#999' : '#dc3232' );
     712
     713                $html  = '<div style="padding: 12px; background: ' . esc_attr( $bg_color ) . '; border-left: 4px solid ' . esc_attr( $border_color ) . '; border-radius: 4px; margin-bottom: 15px;">';
     714                $html .= '<div style="display: flex; align-items: center; justify-content: space-between;">';
     715                $html .= '<div style="display: flex; align-items: center; gap: 8px;">';
     716                $html .= '<strong style="color: #23282d;">' . esc_html__( 'API Connection Status:', 'formscrm' ) . '</strong> ';
     717                $html .= '<span style="display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 3px; background: ' . esc_attr( $badge_color ) . '; color: white; font-size: 12px; font-weight: bold;">';
     718                $html .= '<span style="margin-right: 5px;">' . esc_html( $status_icon ) . '</span>' . esc_html( $status_text );
     719                $html .= '</span>';
     720                $html .= '</div>';
     721
     722                if ( ! empty( $crm_type ) ) {
     723                    $html .= '<div style="text-align: right;">';
     724                    $html .= '<span style="display: block; font-size: 11px; color: #666; text-transform: uppercase; letter-spacing: 0.5px;">' . esc_html__( 'CRM Type', 'formscrm' ) . '</span>';
     725                    $html .= '<strong style="font-size: 14px; color: #0073aa;">' . esc_html( ucfirst( $crm_type ) ) . '</strong>';
     726                    $html .= '</div>';
     727                }
     728
     729                $html .= '</div>';
     730
     731                if ( ! empty( $error_message ) ) {
     732                    $html .= '<p style="margin: 8px 0 0 0; padding-top: 8px; border-top: 1px solid #ddd; color: #dc3232; font-size: 12px;"><strong>' . esc_html__( 'Error:', 'formscrm' ) . '</strong> ' . esc_html( $error_message ) . '</p>';
     733                }
     734
     735                $html .= '</div>';
     736                break;
     737
     738            case 'badge':
     739                $html  = '<div class="formscrm-connection-status" style="display: flex; align-items: center; gap: 10px;">';
     740                $html .= sprintf(
     741                    '<span class="formscrm-status-badge" style="display: inline-flex; align-items: center; padding: 6px 12px; border-radius: 4px; background-color: %1$s; color: white; font-weight: bold; font-size: 13px;">',
     742                    esc_attr( $status_color )
     743                );
     744                $html .= '<span style="margin-right: 6px; font-size: 14px;">' . esc_html( $status_icon ) . '</span>';
     745                $html .= esc_html( $status_text );
     746                $html .= '</span>';
     747
     748                if ( ! empty( $crm_type ) ) {
     749                    $html .= sprintf(
     750                        '<span class="formscrm-crm-name" style="color: #666; font-size: 13px;">(%s)</span>',
     751                        esc_html( ucfirst( $crm_type ) )
     752                    );
     753                }
     754
     755                $html .= '</div>';
     756
     757                if ( ! empty( $error_message ) ) {
     758                    $html .= sprintf(
     759                        '<p class="formscrm-error-message" style="color: #dc3232; margin-top: 8px; font-size: 12px;"><strong>%s:</strong> %s</p>',
     760                        esc_html__( 'Error details', 'formscrm' ),
     761                        esc_html( $error_message )
     762                    );
     763                }
     764                break;
     765
     766            default: // 'html'
     767                $html  = '<div class="formscrm-connection-status" style="display: flex; align-items: center; gap: 10px; margin: 10px 0;">';
     768                $html .= '<strong>' . esc_html__( 'API Connection Status:', 'formscrm' ) . '</strong> ';
     769                $html .= sprintf(
     770                    '<span class="formscrm-status-badge" style="display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 3px; background-color: %1$s; color: white; font-weight: bold; font-size: 12px;">',
     771                    esc_attr( $status_color )
     772                );
     773                $html .= '<span style="margin-right: 5px;">' . esc_html( $status_icon ) . '</span>';
     774                $html .= esc_html( $status_text );
     775                $html .= '</span>';
     776
     777                if ( ! empty( $crm_type ) ) {
     778                    $html .= sprintf(
     779                        '<span style="color: #666; font-size: 12px;">(%s)</span>',
     780                        esc_html( ucfirst( $crm_type ) )
     781                    );
     782                }
     783
     784                $html .= '</div>';
     785
     786                if ( ! empty( $error_message ) ) {
     787                    $html .= sprintf(
     788                        '<p style="color: #dc3232; margin: 5px 0; font-size: 12px;"><strong>%s:</strong> %s</p>',
     789                        esc_html__( 'Error', 'formscrm' ),
     790                        esc_html( $error_message )
     791                    );
     792                }
     793                break;
     794        }
     795
     796        // Add help text if provided.
     797        if ( ! empty( $help_text ) ) {
     798            $html .= '<p class="formscrm-status-help" style="color: #666; margin-top: 8px; font-size: 12px;">';
     799            $html .= esc_html( $help_text );
     800            $html .= '</p>';
     801        }
     802
     803        return $html;
     804    }
     805}
     806
     807if ( ! function_exists( 'formscrm_render_connection_status' ) ) {
     808    /**
     809     * Render API connection status indicator.
     810     *
     811     * Echoes the HTML for API connection status.
     812     *
     813     * @param array  $settings    CRM settings array with fc_crm_type and credentials.
     814     * @param string $output_type Output type: 'html', 'notice', 'badge', or 'elementor'.
     815     * @param string $help_text   Optional help text to display below status.
     816     * @return void
     817     */
     818    function formscrm_render_connection_status( $settings, $output_type = 'html', $help_text = '' ) {
     819        $allowed_html = array(
     820            'div'    => array(
     821                'class' => array(),
     822                'style' => array(),
     823            ),
     824            'span'   => array(
     825                'class' => array(),
     826                'style' => array(),
     827            ),
     828            'strong' => array(),
     829            'p'      => array(
     830                'class' => array(),
     831                'style' => array(),
     832            ),
     833            'br'     => array(),
     834        );
     835
     836        echo wp_kses( formscrm_get_connection_status_html( $settings, $output_type, $help_text ), $allowed_html );
     837    }
     838}
  • formscrm/tags/4.3.0/includes/formscrm-library/loader.php

    r3415133 r3460128  
    5454
    5555    require_once 'class-gravityforms-widget.php';
     56    require_once 'class-gravityforms-markdown-export.php';
    5657}
    5758
  • formscrm/tags/4.3.0/readme.txt

    r3425471 r3460128  
    55Requires at least: 5.5
    66Tested up to: 6.9
    7 Stable tag: 4.2.1
    8 Version: 4.2.1
     7Stable tag: 4.3.0
     8Version: 4.3.0
    99License: GPL2
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    8888
    8989All Slack notifications use a compact, easy-to-read format with information presented in single lines. Messages are color-coded in red (danger) to stand out in your channel and ensure immediate attention to critical errors.
     90
    9091== Error Notifications ==
    9192**Custom Email for Error Reports**
     
    106107The email is professionally formatted with color-coded sections for easy reading and quick troubleshooting.
    107108
     109== Error Log with Automatic Retry System ==
     110
     111**Track, Manage, and Automatically Retry Failed Form Submissions**
     112
     113The Error Log feature provides a comprehensive interface to view, track, and manage all errors that occur when sending form submissions to your CRM. This powerful tool includes an automatic retry system that helps you troubleshoot issues and recover from failed submissions without requiring manual intervention or users to resubmit forms.
     114
     115**Key Features:**
     116
     117* **Automatic Retry System**: Failed entries are automatically retried up to 3 times with 1-hour intervals between attempts
     118* **Smart Retry Management**: Retries stop automatically when an entry is successfully sent or manually deleted
     119* **Complete Error Tracking**: All errors are automatically saved to the database with complete context including CRM type, error message, form information, lead data, and technical details
     120* **Advanced Filtering**: Filter errors by status (failed/success) and CRM type to quickly find specific issues
     121* **Detailed Error Information**: View complete error details including lead data, API URLs, JSON requests, and full error messages
     122* **One-Click Manual Resend**: Manually resend failed entries directly from the error log with a single click
     123* **Error Management**: Delete individual entries or clear all logs with confirmation dialogs
     124* **Pagination**: Navigate through large numbers of error logs with built-in pagination (20 entries per page)
     125* **Visual Status Tracking**: Status badges show failed and successful entries at a glance
     126* **Retry Progress Counter**: Shows retry attempts (e.g., "2/3") and displays time until next automatic retry
     127* **Responsive Design**: Fully responsive interface that works on all devices
     128
     129**Automatic Retry System:**
     130
     131When a form submission fails to send to your CRM:
     132
     1331. The error is logged immediately and the first retry is scheduled for 1 hour later
     1342. If the retry fails, another retry is scheduled for 1 hour after that
     1353. This continues for up to 3 total attempts (original submission + 2 retries)
     1364. If an attempt succeeds, all future retries are automatically cancelled
     1375. You can manually resend at any time, which counts toward the 3-attempt limit
     1386. The interface shows the current attempt count (e.g., "1/3", "2/3") and time until next retry
     139
     140**How to Use:**
     141
     1421. Go to **WordPress Admin → FormsCRM → Error Log tab**
     1432. View all form submission errors in an organized table
     1443. Filter by status or CRM type to find specific errors
     1454. Click **Details** to view complete error information including retry schedule
     1465. Click **Resend** to manually retry sending a failed entry to your CRM
     1476. Click **Delete** to remove individual log entries and cancel any pending retries
     1487. Use **Clear All Logs** to remove all entries at once and cancel all pending retries
     149
     150**What Information is Displayed:**
     151
     152* Date and time of error
     153* CRM type (Holded, Clientify, etc.)
     154* Form information (type, ID, name, entry ID)
     155* Complete error message
     156* All lead data from the form submission
     157* API endpoint URL
     158* JSON request payload
     159* Retry attempts count (e.g., "2/3")
     160* Time until next automatic retry (e.g., "Next: in 45 minutes")
     161* Last resend date and time
     162
     163The Error Log with automatic retry system helps you maintain data integrity by ensuring no form submissions are lost due to temporary errors, connectivity issues, or API downtime. The automatic retry mechanism increases the success rate of form submissions without requiring manual intervention.
     164
     165== Markdown Export for GravityForms Entries ==
     166
     167**Export your GravityForms entries as portable, human-readable Markdown files**
     168
     169The Markdown Export feature allows you to export GravityForms entries into clean, well-structured `.md` files. This makes it easy to document, share, version control, or integrate form submissions with knowledge bases, static site generators, or any Markdown-compatible system.
     170
     171**Key Features:**
     172
     173* **Single Entry Export**: Export individual entries directly from the entry detail page
     174* **Bulk Export**: Export multiple selected entries at once as a convenient ZIP file
     175* **Clean Formatting**: Produces readable, well-structured Markdown with proper headers and field organization
     176* **Comprehensive Field Support**: Handles all GravityForms field types including text, email, number, textarea, checkboxes, multiselect, name fields, address fields, file uploads, and list fields
     177* **Smart Content Handling**: Properly formats multi-line content, preserves line breaks, and handles file attachments with Markdown links
     178* **Metadata Included**: Each export includes form title, entry ID, submission date, and all field labels and values
     179* **Safe Character Escaping**: Automatically escapes Markdown special characters to ensure valid output
     180
     181**How to Use:**
     182
     183**Single Entry Export:**
     1841. Go to **Forms → Entries** in GravityForms
     1852. Click on any entry to view its details
     1863. Find the **Export to Markdown** widget in the right sidebar
     1874. Click **Download Markdown** to get the `.md` file
     188
     189**Bulk Export:**
     1901. Go to **Forms → Entries** in GravityForms
     1912. Select one or multiple entries using the checkboxes
     1923. Choose **Export to Markdown** from the bulk actions dropdown
     1934. Click **Apply** to download a ZIP file containing all selected entries as separate Markdown files
     194
     195**Exported Markdown Format:**
     196
     197Each Markdown file includes:
     198- Form title as the main heading
     199- Entry ID and submission timestamp
     200- All filled fields organized in a clean bullet list format
     201- Field labels in bold with their corresponding values
     202- Multi-line content properly formatted with preserved line breaks
     203- File attachments as clickable Markdown links
     204
     205**Use Cases:**
     206
     207* Document form submissions for record-keeping
     208* Share entry data with team members in a readable format
     209* Version control form submissions using Git or similar tools
     210* Import entries into knowledge bases or wikis
     211* Generate reports or documentation from form data
     212* Backup form entries in a portable, future-proof format
     213* Integrate with static site generators (Jekyll, Hugo, etc.)
     214
    108215== Settings for Clientify ==
    109216**Instructions for adding Clientify cookie in the forms**
     
    131238
    132239== Changelog ==
    133 = 4.2.1 =
     240
     241= 4.3.0 =
     242*  Added: API connection status indicators across all form integrations (GravityForms, WPForms, Elementor, Contact Form 7, WooCommerce).
     243*  Added: Visual connection status badges with color coding - green (connected), red (error), gray (not configured).
     244*  Added: Real-time connection validation with detailed error messages when authentication fails.
     245*  Added: Markdown Export feature for GravityForms entries with single and bulk export capabilities.
     246*  Added: Export entries as clean, well-structured Markdown files with full field type support.
     247*  Added: Bulk export creates ZIP file with multiple entry Markdown files for easy sharing.
     248*  Added: Automatic retry system with up to 3 attempts at 1-hour intervals, visual progress counter, and smart cancellation when entries succeed or are deleted.
     249*  Added: Error Log feature with comprehensive tracking, filtering by status/CRM, detailed error views, resend capability, and pagination for easy management.
     250*  Enhanced: Contact Form 7 module selection now auto-saves configuration with visual feedback.
     251*  Enhanced: Responsive AJAX-based interface with color-coded status badges and synchronized manual/automatic retry system.
     252*  Enhanced: Feed connection status in Forms list in Gravity Forms.
     253*  Fixed: Resend button missing in Gravity Forms Entries view.
     254*  Enhanced: Added feed selector in Resend Entry widget to choose between all feeds or individual feed.
     255*  Added date conversion in Clientify for birthday field.
    134256*  Hotfix: Error not sending correctly entry id in webhook.
    135257
    136258= 4.2.0 =
    137259*  Enhanced: New design for the settings page.
    138 Dedicated menu for FormsCRM settings.
     260Enhanced: Dedicated menu for FormsCRM settings.
    139261*  Improved: Added new tests for more consistent code coverage.
    140262*  Fixed: Fatal error in formscrm_debug_email_lead function.
  • formscrm/trunk/formscrm.php

    r3425471 r3460128  
    44 * Plugin URI : https://close.technology/wordpress-plugins/formscrm/
    55 * Description: Connects Forms with CRM, ERP and Email Marketing.
    6  * Version: 4.2.1
     6 * Version: 4.3.0
    77 * Author: CloseTechnology
    88 * Author URI: https://close.technology
     
    2424defined( 'ABSPATH' ) || die( 'No script kiddies please!' );
    2525
    26 define( 'FORMSCRM_VERSION', '4.2.1' );
     26define( 'FORMSCRM_VERSION', '4.3.0' );
    2727define( 'FORMSCRM_PLUGIN', __FILE__ );
    2828define( 'FORMSCRM_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
     
    9292// Include files.
    9393require_once FORMSCRM_PLUGIN_PATH . '/includes/admin/class-admin-options.php';
     94require_once FORMSCRM_PLUGIN_PATH . '/includes/admin/class-error-log.php';
     95require_once FORMSCRM_PLUGIN_PATH . '/includes/admin/class-error-log-page.php';
    9496require_once FORMSCRM_PLUGIN_PATH . '/includes/formscrm-library/loader.php';
  • formscrm/trunk/includes/admin/class-admin-options.php

    r3424189 r3460128  
    3737            add_action( 'admin_menu', array( $this, 'add_plugin_page' ) );
    3838            add_action( 'formscrm_settings', array( $this, 'settings_page' ) );
     39            add_action( 'formscrm_notifications', array( $this, 'notifications_page' ) );
    3940            add_action( 'admin_init', array( $this, 'register_settings' ) );
    4041        }
     
    142143                            'action' => 'formscrm_settings',
    143144                        ),
     145                        array(
     146                            'tab'    => 'notifications',
     147                            'label'  => esc_html__( 'Notifications', 'formscrm' ),
     148                            'action' => 'formscrm_notifications',
     149                        ),
     150                        array(
     151                            'tab'    => 'error-log',
     152                            'label'  => esc_html__( 'Error Log', 'formscrm' ),
     153                            'action' => 'formscrm_error_log_content',
     154                        ),
    144155                    )
    145156                );
     
    210221
    211222        /**
    212          * Renders the settings page.
    213          *
    214          * Displays the FormsCRM settings form with Slack integration options.
    215          *
    216          * @return void
    217          */
    218         public function settings_page() {
    219             $source_shop_url          = 'es' === strtok( get_locale(), '_' ) ? 'https://close.technology/' : 'https://close.technology/en/';
    220             $utm_source               = '?utm_source=WordPress+Settings&utm_medium=plugin&utm_campaign=link';
     223         * Renders the notifications page.
     224         *
     225         * Displays error notification settings including Slack and Email options.
     226         *
     227         * @return void
     228         */
     229        public function notifications_page() {
    221230            $slack_webhook_url        = get_option( 'formscrm_slack_webhook_url', '' );
    222231            $error_notification_email = get_option( 'formscrm_error_notification_email', '' );
    223232            ?>
    224233
    225         <!-- Notifications Section -->
     234        <!-- Error Notifications Section -->
    226235        <div class="fcrm-section">
    227236            <div class="fcrm-section-header">
     
    306315            </div>
    307316        </div>
     317            <?php
     318        }
     319
     320        /**
     321         * Renders the settings page.
     322         *
     323         * Displays supported forms and CRM integrations.
     324         *
     325         * @return void
     326         */
     327        public function settings_page() {
     328            $source_shop_url = 'es' === strtok( get_locale(), '_' ) ? 'https://close.technology/' : 'https://close.technology/en/';
     329            $utm_source      = '?utm_source=WordPress+Settings&utm_medium=plugin&utm_campaign=link';
     330            ?>
    308331
    309332        <!-- Forms Supported Section -->
  • formscrm/trunk/includes/assets/elementor-editor.js

    r3290078 r3460128  
    3030        };
    3131
    32         $form.html('Loading...');
     32        // Show loading state.
     33        $('#formscrm-connection-status').html(
     34            '<div style="padding: 12px; background: #f9f9f9; border-left: 4px solid #0073aa; border-radius: 4px;">' +
     35            '<div style="display: flex; align-items: center; gap: 8px;">' +
     36            '<strong style="color: #23282d;">' + 'API Connection Status:' + '</strong> ' +
     37            '<span style="display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 3px; background: #0073aa; color: white; font-size: 12px; font-weight: bold;">' +
     38            '<span style="margin-right: 5px;">⟳</span>' + 'Connecting...' +
     39            '</span>' +
     40            '</div></div>'
     41        );
     42        $form.html('<p style="text-align: center; padding: 20px; color: #666;">Loading...</p>');
    3343
    3444        $.ajax({
     
    3949            success: function(response) {
    4050                console.log('Response:', response);
    41                 $form.html(response.data);
     51               
     52                if (response.success) {
     53                    // Update connection status.
     54                    if (response.data.status_html) {
     55                        $('#formscrm-connection-status').html(response.data.status_html);
     56                    }
    4257
    43                 if ( !$('#fc_crm_module').val() ) {
    44                     // select first
    45                     let firstSelect = $form.find('select').first();
    46                     let firstOption = firstSelect.find('option').first();
    47                     firstSelect.val(firstOption.val());
     58                    // Update form content with modules and fields.
     59                    $form.html(response.data.form_html);
     60
     61                    if ( !$('#fc_crm_module').val() ) {
     62                        // select first
     63                        let firstSelect = $form.find('select').first();
     64                        let firstOption = firstSelect.find('option').first();
     65                        firstSelect.val(firstOption.val());
     66                    }
     67
     68                    $('.elementor-map-table[data-module="'+$('#fc_crm_module').val()+'"]').addClass('active');
     69                } else {
     70                    // Handle error response.
     71                    // Update connection status with error HTML if available.
     72                    if (response.data && response.data.status_html) {
     73                        $('#formscrm-connection-status').html(response.data.status_html);
     74                    } else {
     75                        let errorMessage = response.data && response.data.message ? response.data.message : (response.data || 'Connection failed');
     76                       
     77                        // Show error in status indicator.
     78                        $('#formscrm-connection-status').html(
     79                            '<div style="padding: 12px; background: #ffebee; border-left: 4px solid #dc3232; border-radius: 4px;">' +
     80                            '<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">' +
     81                            '<strong style="color: #23282d;">API Connection Status:</strong> ' +
     82                            '<span style="display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 3px; background: #dc3232; color: white; font-size: 12px; font-weight: bold;">' +
     83                            '<span style="margin-right: 5px;">✕</span>Error' +
     84                            '</span>' +
     85                            '</div>' +
     86                            '<p style="margin: 0; color: #dc3232; font-size: 12px;"><strong>Error:</strong> ' + errorMessage + '</p>' +
     87                            '</div>'
     88                        );
     89                    }
     90                    $form.html('');
    4891                }
    49 
    50                 $('.elementor-map-table[data-module="'+$('#fc_crm_module').val()+'"]').addClass('active');
    5192            },
    5293            error: function(xhr, status, error) {
    5394                console.error('AJAX Error:', error);
     95                // Show error in status indicator.
     96                $('#formscrm-connection-status').html(
     97                    '<div style="padding: 12px; background: #ffebee; border-left: 4px solid #dc3232; border-radius: 4px;">' +
     98                    '<div style="display: flex; align-items: center; gap: 8px;">' +
     99                    '<strong style="color: #23282d;">API Connection Status:</strong> ' +
     100                    '<span style="display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 3px; background: #dc3232; color: white; font-size: 12px; font-weight: bold;">' +
     101                    '<span style="margin-right: 5px;">✕</span>Error' +
     102                    '</span>' +
     103                    '</div>' +
     104                    '<p style="margin: 8px 0 0 0; color: #dc3232; font-size: 12px;"><strong>Error:</strong> Network error - ' + error + '</p>' +
     105                    '</div>'
     106                );
     107                $form.html('');
    54108            }
    55109        });
  • formscrm/trunk/includes/assets/formscrm-admin.css

    r3424189 r3460128  
    114114/* Tabs Navigation */
    115115.fcrm-tabs-wrapper {
    116     margin-bottom: 2rem;
     116    margin-bottom: 1rem;
    117117    background: white;
    118118    border-radius: 1rem;
     
    413413    align-items: center;
    414414    justify-content: center;
    415     padding: 0.75rem 1.5rem;
     415    padding: 0.40rem 1rem;
    416416    font-size: 1rem;
    417417    font-weight: 600;
     
    555555    height: 1.5rem;
    556556}
     557
     558/* Error Log Specific Styles */
     559.fcrm-error-log-table-wrapper {
     560    overflow-x: auto;
     561    margin-top: 1.5rem;
     562}
     563
     564.fcrm-table {
     565    width: 100%;
     566    border-collapse: collapse;
     567    background: white;
     568}
     569
     570.fcrm-table thead {
     571    background: var(--fcrm-gray-50);
     572}
     573
     574.fcrm-table th {
     575    padding: 0.75rem 1rem;
     576    text-align: left;
     577    border-bottom: 2px solid var(--fcrm-gray-200);
     578    font-weight: 600;
     579    color: var(--fcrm-gray-700);
     580    font-size: 0.875rem;
     581    white-space: nowrap;
     582}
     583
     584.fcrm-table td {
     585    padding: 0.75rem 1rem;
     586    border-bottom: 1px solid var(--fcrm-gray-200);
     587    font-size: 0.875rem;
     588    color: var(--fcrm-gray-700);
     589}
     590
     591.fcrm-table tbody tr:hover {
     592    background: var(--fcrm-gray-50);
     593}
     594
     595.fcrm-button-small {
     596    padding: 0.375rem 0.75rem;
     597    font-size: 0.8125rem;
     598}
     599
     600.fcrm-button-danger {
     601    background: var(--fcrm-error);
     602    color: white;
     603}
     604
     605.fcrm-button-danger:hover {
     606    background: #dc2626;
     607    transform: translateY(-2px);
     608    box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
     609}
     610
     611.fcrm-status {
     612    display: inline-block;
     613    padding: 0.25rem 0.625rem;
     614    border-radius: 0.375rem;
     615    font-size: 0.75rem;
     616    font-weight: 600;
     617    text-transform: uppercase;
     618    letter-spacing: 0.025em;
     619}
     620
     621.fcrm-status-error {
     622    background: #fef2f2;
     623    color: #991b1b;
     624}
     625
     626.fcrm-status-success {
     627    background: #f0fdf4;
     628    color: #15803d;
     629}
     630
     631.fcrm-log-details {
     632    background: var(--fcrm-gray-50);
     633}
     634
     635.fcrm-log-details td {
     636    padding: 1.5rem;
     637}
     638
     639.fcrm-pagination {
     640    display: flex;
     641    justify-content: center;
     642    gap: 0.5rem;
     643    margin-top: 1.5rem;
     644    flex-wrap: wrap;
     645}
     646
     647.fcrm-notice-info {
     648    background: #eff6ff;
     649    border-left: 4px solid var(--fcrm-blue);
     650}
     651
     652.fcrm-notice-info .fcrm-notice-text {
     653    color: #1e40af;
     654}
     655
     656/* Error Log Filters */
     657.fcrm-error-log-filters {
     658    margin-bottom: 1.25rem;
     659    display: flex;
     660    gap: 1rem;
     661    align-items: center;
     662    justify-content: space-between;
     663    flex-wrap: wrap;
     664}
     665
     666.fcrm-error-log-filters-form {
     667    width: 84%;
     668    display: flex;
     669    gap: 0.625rem;
     670    align-items: center;
     671    flex-wrap: wrap;
     672}
     673
     674.wp-core-ui select.fcrm-filter-select {
     675    max-width: 200px;
     676    flex-shrink: 0;
     677}
     678
     679/* Stats Summary */
     680.fcrm-stats-summary {
     681    margin-bottom: 1.25rem;
     682    padding: 0.9375rem;
     683    background: var(--fcrm-gray-100);
     684    border-radius: 0.3125rem;
     685}
     686
     687/* Table Actions Column */
     688.fcrm-table-actions {
     689    text-align: center;
     690    width: 280px;
     691    min-width: 280px;
     692}
     693
     694.fcrm-table-actions .fcrm-button {
     695    margin-right: 0.3125rem;
     696}
     697
     698.fcrm-table-actions .fcrm-button:last-child {
     699    margin-right: 0;
     700}
     701
     702/* Form Subtitle */
     703.fcrm-form-subtitle {
     704    color: var(--fcrm-gray-600);
     705}
     706
     707/* Error Message Column */
     708.fcrm-error-message {
     709    max-width: 300px;
     710    overflow: hidden;
     711    text-overflow: ellipsis;
     712}
     713
     714/* Details Row */
     715.fcrm-log-details {
     716    display: none;
     717}
     718
     719.fcrm-details-cell {
     720    padding: 1.25rem;
     721    background: var(--fcrm-gray-50);
     722}
     723
     724.fcrm-details-grid {
     725    display: grid;
     726    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
     727    gap: 1.25rem;
     728}
     729
     730.fcrm-details-section h4 {
     731    margin-top: 0;
     732}
     733
     734.fcrm-details-box {
     735    background: white;
     736    padding: 0.9375rem;
     737    border-radius: 0.3125rem;
     738}
     739
     740.fcrm-details-box-scroll {
     741    max-height: 300px;
     742    overflow-y: auto;
     743}
     744
     745/* Lead Data Table */
     746.fcrm-lead-data-table {
     747    width: 100%;
     748}
     749
     750.fcrm-lead-data-name {
     751    padding: 0.3125rem;
     752    font-weight: 600;
     753}
     754
     755.fcrm-lead-data-value {
     756    padding: 0.3125rem;
     757}
     758
     759/* Code Blocks */
     760.fcrm-code {
     761    word-break: break-all;
     762    font-size: 0.6875rem;
     763}
     764
     765.fcrm-code-block {
     766    display: block;
     767    max-height: 150px;
     768    overflow-y: auto;
     769    background: var(--fcrm-gray-100);
     770    padding: 0.625rem;
     771    border-radius: 0.1875rem;
     772}
     773
     774/* Full Width Details */
     775.fcrm-details-full {
     776    grid-column: 1 / -1;
     777}
     778
     779/* Error Box */
     780.fcrm-error-box {
     781    background: #ffebee;
     782    padding: 0.9375rem;
     783    border-radius: 0.3125rem;
     784    border-left: 4px solid #d32f2f;
     785}
     786
     787@media (max-width: 768px) {
     788    .fcrm-error-log-filters {
     789        flex-direction: column;
     790        align-items: stretch !important;
     791    }
     792
     793    .fcrm-error-log-filters-form {
     794        flex-direction: column;
     795        width: 80%;
     796    }
     797
     798    .fcrm-filter-select {
     799        max-width: 100% !important;
     800    }
     801
     802    .fcrm-table {
     803        font-size: 0.75rem;
     804    }
     805
     806    .fcrm-table th,
     807    .fcrm-table td {
     808        padding: 0.5rem;
     809    }
     810
     811    .fcrm-table-actions {
     812        width: auto;
     813        min-width: auto;
     814    }
     815
     816    .fcrm-table-actions .fcrm-button {
     817        display: block;
     818        margin-bottom: 0.3125rem;
     819        margin-right: 0;
     820    }
     821
     822    .fcrm-details-grid {
     823        grid-template-columns: 1fr;
     824    }
     825}
     826
     827/* =================================================================
     828   Gravity Forms - Connected Feeds Column
     829   ================================================================= */
     830
     831/* Column Styling */
     832.gform-table .formscrm_feeds,
     833.wp-list-table .formscrm_feeds {
     834    min-width: 200px;
     835    max-width: 350px;
     836    vertical-align: top;
     837}
     838
     839.wp-list-table td.formscrm_feeds {
     840    padding: 10px;
     841}
     842
     843/* Status Badges - Using GravityForms native classes */
     844/* .gform-status-indicator.gform-status--active for connected state */
     845/* .gform-status-indicator.gform-status--inactive for disconnected/error state */
     846
     847/* Feeds Wrapper */
     848.formscrm_feeds {
     849
     850    .gform-status--active {
     851    border-radius: .75rem;
     852    gap: .25rem;
     853    padding-block: 0.0625rem;
     854    padding-inline: 0.375rem 0.5rem;
     855        font-size: 12px;
     856    }
     857}
     858
     859/* Feeds List */
     860.formscrm-feeds-list {
     861    display: flex;
     862    flex-direction: column;
     863    gap: 4px;
     864    margin-top: 8px;
     865    padding-top: 8px;
     866    border-top: 1px solid #e0e0e0;
     867}
     868
     869.formscrm-feed-item {
     870    display: flex;
     871    align-items: center;
     872    gap: 6px;
     873    line-height: 1.4;
     874}
     875
     876.formscrm-feed-name {
     877    font-weight: 500;
     878    color: #000;
     879}
     880
     881.formscrm-feed-crm {
     882    color: #666;
     883    font-size: 0.85em;
     884    font-style: italic;
     885}
     886
     887.formscrm-feed-total {
     888    margin-top: 4px;
     889    padding-top: 4px;
     890    border-top: 1px solid #ddd;
     891    color: #666;
     892    font-size: 0.85em;
     893}
     894
     895/* Mobile responsive */
     896@media screen and (max-width: 782px) {
     897    .gform-table .formscrm_feeds,
     898    .wp-list-table .formscrm_feeds {
     899        min-width: 150px;
     900    }
     901   
     902    .formscrm-status-badge {
     903        padding: 4px 8px;
     904        font-size: 0.85em;
     905    }
     906   
     907    .formscrm-feed-item {
     908        font-size: 0.9em;
     909    }
     910}
  • formscrm/trunk/includes/assets/icons/icon-bell.svg

    r3424189 r3460128  
    22    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path>
    33</svg>
     4
  • formscrm/trunk/includes/assets/icons/icon-check-simple.svg

    r3424189 r3460128  
    22    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
    33</svg>
     4
  • formscrm/trunk/includes/assets/icons/icon-checkmark.svg

    r3424189 r3460128  
    22    <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
    33</svg>
     4
  • formscrm/trunk/includes/assets/icons/icon-document.svg

    r3424189 r3460128  
    22    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
    33</svg>
     4
  • formscrm/trunk/includes/assets/icons/icon-email.svg

    r3424189 r3460128  
    22    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
    33</svg>
     4
  • formscrm/trunk/includes/assets/icons/icon-plus.svg

    r3424189 r3460128  
    22    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
    33</svg>
     4
  • formscrm/trunk/includes/assets/icons/icon-slack.svg

    r3424189 r3460128  
    22    <path d="M6 8a2 2 0 1 0-4 0 2 2 0 0 0 4 0zm8 0a2 2 0 1 0-4 0 2 2 0 0 0 4 0zm8 0a2 2 0 1 0-4 0 2 2 0 0 0 4 0zM6 16a2 2 0 1 0-4 0 2 2 0 0 0 4 0zm8 0a2 2 0 1 0-4 0 2 2 0 0 0 4 0zm8 0a2 2 0 1 0-4 0 2 2 0 0 0 4 0z"/>
    33</svg>
     4
  • formscrm/trunk/includes/assets/icons/icon-support.svg

    r3424189 r3460128  
    22    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192l-3.536 3.536M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z"></path>
    33</svg>
     4
  • formscrm/trunk/includes/assets/icons/icon-users.svg

    r3424189 r3460128  
    22    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
    33</svg>
     4
  • formscrm/trunk/includes/crm-library/class-crmlib-brevo.php

    r3415133 r3460128  
    238238        $list_id = isset( $settings['fc_crm_module'] ) ? (int) $settings['fc_crm_module'] : '';
    239239
     240        // List of standard Brevo contact fields that should be at root level.
     241        // All other fields are automatically treated as custom attributes.
     242        // See https://developers.brevo.com/reference/createcontact.
     243        $standard_fields = array(
     244            'email',
     245            'ext_id',
     246            'emailBlacklisted',
     247            'smsBlacklisted',
     248            'listIds',
     249            'unlinkListIds',
     250            'updateEnabled',
     251            'smtpBlacklistSender',
     252        );
     253
    240254        $subscriber            = array();
    241255        $subscriber['listIds'] = array( $list_id );
    242256        foreach ( $merge_vars as $element ) {
    243             if ( false === strpos( $element['name'], '|' ) ) {
    244                 $subscriber[ $element['name'] ] = $element['value'];
     257            $field_name  = $element['name'];
     258            $field_value = $element['value'];
     259
     260            // Check if field contains pipe separator (attributes|FIELDNAME).
     261            if ( false === strpos( $field_name, '|' ) ) {
     262                // No pipe - check if it's a standard field.
     263                if ( in_array( $field_name, $standard_fields, true ) ) {
     264                    // Standard field - add to root level.
     265                    $subscriber[ $field_name ] = $field_value;
     266                } else {
     267                    // Custom attribute - add to attributes object.
     268                    $subscriber['attributes'][ $field_name ] = $field_value;
     269                }
    245270            } else {
    246                 $key                              = str_replace( 'attributes|', '', $element['name'] );
    247                 $subscriber['attributes'][ $key ] = $element['value'];
     271                // Pipe found - extract attribute name and add to attributes.
     272                $key                              = str_replace( 'attributes|', '', $field_name );
     273                $subscriber['attributes'][ $key ] = $field_value;
    248274            }
    249275        }
  • formscrm/trunk/includes/crm-library/class-crmlib-clientify.php

    r3424189 r3460128  
    33 * Clientify connect library
    44 *
    5  * Has functions to login, list fields and create leadº
     5 * Has functions to login, list fields and create lead
    66 *
    77 * @author   closemarketing
     
    874874            } elseif ( 'gdpr_accept' === $element['name'] || 'disclaimer' === $element['name'] ) {
    875875                $contact[ $element['name'] ] = empty( $element['value'] ) ? false : true;
     876            } elseif ( 'birthday' === $element['name'] ) {
     877                // Normalize birthday date format to YYYY-MM-DD.
     878                $normalized_date = formscrm_normalize_date_format( $element['value'] );
     879                if ( false !== $normalized_date ) {
     880                    $contact[ $element['name'] ] = $normalized_date;
     881                }
    876882            } else {
    877883                $contact[ $element['name'] ] = $element['value'];
  • formscrm/trunk/includes/formscrm-library/class-contactform7.php

    r3415133 r3460128  
    3434        add_action( 'wpcf7_after_save', array( $this, 'crm_save_options' ) );
    3535        add_action( 'wpcf7_before_send_mail', array( $this, 'crm_process_entry' ) );
     36        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_autosubmit_assets' ) );
    3637    }
    3738
     
    6263        $cf7_crm_defaults = array();
    6364        $cf7_crm          = get_option( 'cf7_crm_' . $args->id(), $cf7_crm_defaults );
     65        $settings_module  = isset( $cf7_crm['fc_crm_module'] ) ? $cf7_crm['fc_crm_module'] : '';
    6466        ?>
    6567        <div class="metabox-holder">
    6668            <div class="cme-main-fields">
    6769                <p>
    68                     <select name="wpcf7-crm[fc_crm_type]" class="medium" onchange="jQuery(this).parents('form').submit();" id="fc_crm_type">
     70                    <label for="fc_crm_type"><?php esc_html_e( 'CRM Type:', 'formscrm' ); ?></label><br />
     71                    <select name="wpcf7-crm[fc_crm_type]" class="medium formscrm-autosubmit" id="fc_crm_type" data-formscrm-autosubmit="true">
    6972                        <?php
    7073                        foreach ( formscrm_get_choices() as $choice ) {
     
    7780                        ?>
    7881                    </select>
     82                    <span class="formscrm-saving-indicator" style="display:none; margin-left:10px; color:#46b450;">
     83                        <span class="dashicons dashicons-update-alt" style="animation: rotation 1s infinite linear;"></span>
     84                        <?php esc_html_e( 'Saving...', 'formscrm' ); ?>
     85                    </span>
    7986                </p>
    8087                <?php if ( isset( $cf7_crm['fc_crm_type'] ) && $cf7_crm['fc_crm_type'] ) { ?>
     
    126133                    ?>
    127134                    <p>
    128                         <select name="wpcf7-crm[fc_crm_module]" class="medium" onchange="jQuery(this).parents('form').submit();" id="fc_crm_module">
     135                        <label for="fc_crm_module"><?php esc_html_e( 'CRM Module:', 'formscrm' ); ?></label><br />
     136                        <select name="wpcf7-crm[fc_crm_module]" class="medium formscrm-autosubmit" id="fc_crm_module" data-formscrm-autosubmit="true">
    129137                            <?php
    130                             $settings_module = isset( $cf7_crm['fc_crm_module'] ) ? $cf7_crm['fc_crm_module'] : '';
    131                             foreach ( $this->crmlib->list_modules( $cf7_crm ) as $module ) {
     138                            $modules = $this->crmlib->list_modules( $cf7_crm );
     139                            foreach ( $modules as $module ) {
    132140                                $value = '';
    133141                                if ( ! empty( $module['value'] ) ) {
     
    145153                                echo '>' . esc_html( $module['label'] ) . '</option>';
    146154                            }
     155                            if ( empty( $settings_module ) || ! in_array( $settings_module, array_column( $modules, 'value' ), true ) ) {
     156                                $default_value            = ! empty( $modules[0]['value'] ) ? $modules[0]['value'] : '';
     157                                $settings_module          = $default_value;
     158                                $cf7_crm['fc_crm_module'] = $default_value;
     159                            }
    147160                            ?>
    148161                        </select>
     162                        <span class="formscrm-saving-indicator" style="display:none; margin-left:10px; color:#46b450;">
     163                            <span class="dashicons dashicons-update-alt" style="animation: rotation 1s infinite linear;"></span>
     164                            <?php esc_html_e( 'Saving...', 'formscrm' ); ?>
     165                        </span>
    149166                    </p>
    150167                    <p>
     
    155172            </div>
    156173            <?php
     174            // Show API connection status.
     175            if ( ! empty( $cf7_crm['fc_crm_type'] ) ) {
     176                formscrm_render_connection_status( $cf7_crm, 'html' );
     177            }
     178
    157179            if ( ! empty( $this->crmlib ) ) {
    158180                $login_crm = $this->crmlib->login( $cf7_crm );
    159181                if ( is_array( $login_crm ) && isset( $login_crm['status'] ) && 'error' === $login_crm['status'] ) {
    160                     echo '<p>' . esc_html__( 'We could not login to the CRM', 'formscrm' ) . ' ' . esc_html( $login_crm['message'] ) . '</p>';
    161182                    return;
    162183                }
    163184
    164185                if ( false === $login_crm ) {
    165                     echo '<p>' . esc_html__( 'We could not login to the CRM', 'formscrm' ) . '</p>';
    166186                    return;
    167187                }
    168188            }
    169189
    170             if ( isset( $cf7_crm['fc_crm_module'] ) && $cf7_crm['fc_crm_module'] ) {
    171                 $crm_fields  = $this->crmlib->list_fields( $cf7_crm, $cf7_crm['fc_crm_module'] );
     190            if ( $settings_module ) {
     191                $crm_fields  = $this->crmlib->list_fields( $cf7_crm, $settings_module );
    172192                $cf7_form    = WPCF7_ContactForm::get_instance( $args->id() );
    173193                $form_fields = ! empty( $cf7_form ) ? $cf7_form->scan_form_tags() : array();
     
    275295
    276296            $form_info = array(
    277                 'form_type' => 'Contact Form 7',
    278                 'form_id'   => $contact_form->id(),
    279                 'form_name' => $contact_form->title(),
     297                'form_type'       => 'contactform7',
     298                'form_type_title' => 'Contact Form 7',
     299                'form_id'         => $contact_form->id(),
     300                'form_name'       => $contact_form->title(),
    280301            );
    281302
     
    310331            }
    311332
     333            // Process dynamic values (shortcodes).
     334            $value = $this->fill_dynamic_value( $value, $submitted_data );
     335
    312336            $merge_vars[] = array(
    313337                'name'  => $crm_key,
     
    318342        return $merge_vars;
    319343    }
     344
     345    /**
     346     * Enqueue auto-submit assets for CF7 settings
     347     *
     348     * @param string $hook Hook suffix for the current admin page.
     349     * @return void
     350     */
     351    public function enqueue_autosubmit_assets( $hook ) {
     352        // Only load on CF7 edit pages.
     353        if ( 'toplevel_page_wpcf7' !== $hook ) {
     354            return;
     355        }
     356
     357        // Enqueue CSS (reusing admin styles for consistency).
     358        wp_enqueue_style(
     359            'formscrm-admin',
     360            FORMSCRM_PLUGIN_URL . 'includes/assets/formscrm-admin.css',
     361            array(),
     362            FORMSCRM_VERSION,
     363            'all'
     364        );
     365
     366        // Enqueue JavaScript.
     367        wp_enqueue_script(
     368            'formscrm-cf7-autosubmit',
     369            FORMSCRM_PLUGIN_URL . 'includes/assets/js/cf7-autosubmit.js',
     370            array(),
     371            FORMSCRM_VERSION,
     372            true
     373        );
     374    }
     375    /**
     376     * Fills dynamic value with shortcode support.
     377     *
     378     * Supports {id:field_name} syntax to reference other form field values.
     379     * Example: "Customer: {id:your-name} - {id:your-email}"
     380     *
     381     * @param string $field_value Field value that may contain shortcodes.
     382     * @param array  $submitted_data All submitted form data.
     383     * @return string Processed field value with shortcodes replaced.
     384     */
     385    private function fill_dynamic_value( $field_value, $submitted_data ) {
     386        if ( ! str_contains( $field_value, '{id:' ) ) {
     387            return $field_value;
     388        }
     389
     390        // Generate dynamic value.
     391        $matches = array();
     392        preg_match_all( '/{([^}]*)}/', $field_value, $matches );
     393        if ( empty( $matches[1] ) ) {
     394            return $field_value;
     395        }
     396
     397        foreach ( $matches[1] as $match ) {
     398            $field_options = explode( ':', $match );
     399            if ( ! isset( $field_options[1] ) || 'id' !== $field_options[0] ) {
     400                continue;
     401            }
     402
     403            $field_name = $field_options[1];
     404            if ( ! isset( $submitted_data[ $field_name ] ) ) {
     405                continue;
     406            }
     407
     408            // Get the value from submitted data.
     409            $entry_value = $submitted_data[ $field_name ];
     410
     411            // Handle array values (checkboxes, etc.).
     412            if ( is_array( $entry_value ) ) {
     413                $entry_value = implode( ', ', $entry_value );
     414            }
     415
     416            // Replace the shortcode with the actual value.
     417            $field_value = str_replace( '{' . $match . '}', $entry_value, $field_value );
     418        }
     419
     420        return $field_value;
     421    }
    320422}
    321423
  • formscrm/trunk/includes/formscrm-library/class-elementor.php

    r3415133 r3460128  
    1616}
    1717
    18 use ElementorPro\Modules\Forms\Submissions\Database\Query;
    19 
    2018/**
    2119 * Action Class
     
    183181        );
    184182
     183        // API Connection Status indicator.
     184        $widget->add_control(
     185            'fc_connection_status_info',
     186            array(
     187                'type'            => \Elementor\Controls_Manager::RAW_HTML,
     188                'raw'             => '<div class="formscrm-elementor-status-container" id="formscrm-connection-status">' .
     189                    '<div style="padding: 12px; background: #f9f9f9; border-left: 4px solid #0073aa; border-radius: 4px; margin: 10px 0;">' .
     190                    '<div style="display: flex; align-items: center; gap: 8px;">' .
     191                    '<strong style="color: #23282d;">' . esc_html__( 'API Connection Status:', 'formscrm' ) . '</strong> ' .
     192                    '<span style="display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 3px; background: #999; color: white; font-size: 12px; font-weight: bold;">' .
     193                    '<span style="margin-right: 5px;">○</span>' . esc_html__( 'Not verified', 'formscrm' ) .
     194                    '</span>' .
     195                    '</div>' .
     196                    '<p style="margin: 8px 0 0 0; color: #666; font-size: 12px;">' . esc_html__( 'Click "Connect" button below to verify your CRM credentials', 'formscrm' ) . '</p>' .
     197                    '</div></div>',
     198                'content_classes' => 'formscrm-connection-status-wrapper',
     199                'separator'       => 'before',
     200            )
     201        );
     202
    185203        $widget->add_control(
    186204            'connect_crm',
     
    188206                'label'       => esc_html__( 'Connect CRM', 'formscrm' ),
    189207                'type'        => \Elementor\Controls_Manager::BUTTON,
    190                 'separator'   => 'before',
    191208                'button_type' => 'info',
    192209                'text'        => esc_html__( 'Connect', 'formscrm' ),
     
    281298
    282299            $form_info = array(
    283                 'form_type' => 'Elementor',
    284                 'form_id'   => isset( $settings['form_id'] ) ? $settings['form_id'] : ( isset( $settings['id'] ) ? $settings['id'] : '' ),
    285                 'form_name' => isset( $settings['form_name'] ) ? $settings['form_name'] : '',
     300                'form_type'       => 'elementor',
     301                'form_type_title' => 'Elementor',
     302                'form_id'         => isset( $settings['form_id'] ) ? $settings['form_id'] : ( isset( $settings['id'] ) ? $settings['id'] : '' ),
     303                'form_name'       => isset( $settings['form_name'] ) ? $settings['form_name'] : '',
    286304            );
    287305
  • formscrm/trunk/includes/formscrm-library/class-gravityforms-widget.php

    r3415133 r3460128  
    2222    public function __construct() {
    2323        add_filter( 'gform_entry_detail_meta_boxes', array( $this, 'widget_resend_entries' ), 10, 3 );
     24        add_action( 'gform_post_add_feed', array( $this, 'clear_feeds_cache' ), 10, 2 );
     25        add_action( 'gform_post_update_feed', array( $this, 'clear_feeds_cache' ), 10, 2 );
     26        add_action( 'gform_post_delete_feed', array( $this, 'clear_feeds_cache' ), 10, 2 );
     27    }
     28
     29    /**
     30     * Get feeds with caching and error handling to improve performance.
     31     *
     32     * @param int $form_id Form ID.
     33     * @return array Array of feeds.
     34     */
     35    private function get_feeds_cached( $form_id ) {
     36        $cache_key = 'formscrm_feeds_' . $form_id;
     37        $feeds     = get_transient( $cache_key );
     38
     39        if ( false === $feeds ) {
     40            try {
     41                // Increase max execution time temporarily for this operation.
     42                $original_time_limit = ini_get( 'max_execution_time' );
     43                if ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) ) {
     44                    set_time_limit( 60 );
     45                }
     46
     47                $feeds = GFCRM::get_instance()->get_feeds( null, $form_id, 'formscrm', true );
     48
     49                // Restore original time limit.
     50                if ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) ) {
     51                    set_time_limit( (int) $original_time_limit );
     52                }
     53
     54                // Only cache if we got valid data.
     55                if ( is_array( $feeds ) ) {
     56                    set_transient( $cache_key, $feeds, 5 * MINUTE_IN_SECONDS );
     57                } else {
     58                    $feeds = array();
     59                }
     60            } catch ( Exception $e ) {
     61                $feeds = array();
     62            }
     63        }
     64
     65        return is_array( $feeds ) ? $feeds : array();
     66    }
     67
     68    /**
     69     * Clear feeds cache for a form.
     70     *
     71     * @param int   $feed_id Feed ID.
     72     * @param array $form_id Form ID.
     73     * @return void
     74     */
     75    public function clear_feeds_cache( $feed_id, $form_id ) {
     76        if ( ! empty( $form_id ) ) {
     77            $cache_key = 'formscrm_feeds_' . $form_id;
     78            delete_transient( $cache_key );
     79        }
    2480    }
    2581
     
    3490    public function widget_resend_entries( $meta_boxes, $entry, $form ) {
    3591        $meta_boxes['formscrm'] = array(
    36             'title'         => esc_html__( 'Resend Entry to CRM', 'formscrm' ),
     92            'title'         => esc_html__( 'FormsCRM: Resend', 'formscrm' ),
    3793            'callback'      => array( $this, 'resend_metabox' ),
    3894            'context'       => 'side',
     
    4298        return $meta_boxes;
    4399    }
     100
    44101    /**
    45102     * The callback used to echo the content to the meta box.
     
    48105     */
    49106    public function resend_metabox( $args ) {
    50         $html    = '';
    51         $action  = 'formscrm_process_feeds';
    52         $form    = ! empty( $args['form'] ) ? $args['form'] : array();
    53         $form_id = isset( $form['id'] ) ? (int) $form['id'] : 0;
    54         $entry   = ! empty( $args['entry'] ) ? $args['entry'] : array();
    55 
    56         $feeds = GFCRM::get_instance()->get_feeds( null, $form_id, 'formscrm', true );
    57 
    58         if ( rgpost( 'action' ) === $action ) {
    59             check_admin_referer( 'gforms_save_entry', 'gforms_save_entry' );
    60             $html .= '<p><strong>' . esc_html__( 'Feeds processed:', 'formscrm' ) . '</strong></p>';
    61             $html .= '<ul>';
    62 
     107        $html     = '';
     108        $action   = 'formscrm_process_feeds';
     109        $form     = ! empty( $args['form'] ) ? $args['form'] : array();
     110        $form_id  = isset( $form['id'] ) ? (int) $form['id'] : 0;
     111        $entry    = ! empty( $args['entry'] ) ? $args['entry'] : array();
     112        $entry_id = isset( $entry['id'] ) ? (int) $entry['id'] : 0;
     113
     114        // Use cached version with error handling.
     115        $feeds = $this->get_feeds_cached( $form_id );
     116
     117        // Check if action was triggered.
     118        $resend_action = isset( $_POST['formscrm_action'] ) ? sanitize_text_field( wp_unslash( $_POST['formscrm_action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified below.
     119
     120        if ( $action === $resend_action ) {
     121            // Verify nonce for security.
     122            if ( ! isset( $_POST['formscrm_resend_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['formscrm_resend_nonce'] ) ), 'formscrm_resend_entry_' . $entry_id ) ) {
     123                $html .= '<p style="color:red;">' . esc_html__( 'Security check failed. Please try again.', 'formscrm' ) . '</p>';
     124            } else {
     125                // Get selected feed(s).
     126                $selected_feeds = isset( $_POST['formscrm_selected_feeds'] ) && is_array( $_POST['formscrm_selected_feeds'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_POST['formscrm_selected_feeds'] ) ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above.
     127                $process_all    = in_array( 'all', $selected_feeds, true );
     128
     129                $html .= '<p><strong>' . esc_html__( 'Feeds processed:', 'formscrm' ) . '</strong></p>';
     130                $html .= '<ul>';
     131
     132                foreach ( $feeds as $feed ) {
     133                    if ( ! $feed['is_active'] || $form_id !== (int) $feed['form_id'] ) {
     134                        continue;
     135                    }
     136
     137                    // Process feed if "all" is selected or if this specific feed is selected.
     138                    if ( $process_all || in_array( (string) $feed['id'], $selected_feeds, true ) ) {
     139                        GFCRM::get_instance()->process_feed( $feed, $entry, $form );
     140                        $html .= '<li>';
     141                        $html .= sprintf(
     142                            // translators: %s is the name of the feed.
     143                            __( 'Feed: %s', 'formscrm' ),
     144                            isset( $feed['meta']['feedName'] ) ? $feed['meta']['feedName'] : $feed['id'],
     145                        );
     146                        $html .= '</li>';
     147                    }
     148                }
     149                $html .= '</ul>';
     150            }
     151        }
     152
     153        // Always show the form with available feeds.
     154        $html .= '<p>' . esc_html__( 'This will resend the entry to the CRM.', 'formscrm' ) . '</p>';
     155        $html .= '<p>' . esc_html__( 'Actual feeds actived:', 'formscrm' ) . '</p>';
     156        $html .= '<ul>';
     157
     158        $active_feeds = 0;
     159        foreach ( $feeds as $feed ) {
     160            if ( ! $feed['is_active'] || $form_id !== (int) $feed['form_id'] ) {
     161                continue;
     162            }
     163            ++$active_feeds;
     164            $html .= '<li>';
     165            $html .= sprintf(
     166                // translators: %s is the name of the feed.
     167                __( 'Feed: %s', 'formscrm' ),
     168                isset( $feed['meta']['feedName'] ) ? $feed['meta']['feedName'] : $feed['id'],
     169            );
     170            $html .= '</li>';
     171        }
     172        $html .= '</ul>';
     173
     174        if ( $active_feeds > 0 ) {
     175            $html .= '<br/>';
     176            $html .= '<form method="post" style="display:inline;">';
     177            $html .= wp_nonce_field( 'formscrm_resend_entry_' . $entry_id, 'formscrm_resend_nonce', true, false );
     178            $html .= '<input type="hidden" name="formscrm_action" value="' . esc_attr( $action ) . '" />';
     179            $html .= '<label for="formscrm_feed_select">' . esc_html__( 'Select Feeds to Resend', 'formscrm' ) . ':</label> ';
     180            $html .= '<select id="formscrm_feed_select" name="formscrm_selected_feeds[]" style="min-width:200px;">';
     181            $html .= '<option value="all">' . esc_html__( 'All feeds', 'formscrm' ) . '</option>';
    63182            foreach ( $feeds as $feed ) {
    64183                if ( ! $feed['is_active'] || $form_id !== (int) $feed['form_id'] ) {
    65184                    continue;
    66185                }
    67                 GFCRM::get_instance()->process_feed( $feed, $entry, $form );
    68                 $html .= '<li>';
    69                 $html .= sprintf(
    70                     // translators: %s is the name of the feed.
    71                     __( 'Feed: %s', 'formscrm' ),
    72                     isset( $feed['meta']['feedName'] ) ? $feed['meta']['feedName'] : $feed['id'],
     186                $feed_name = isset( $feed['meta']['feedName'] ) ? $feed['meta']['feedName'] : $feed['id'];
     187                $html     .= sprintf(
     188                    '<option value="%s">%s</option>',
     189                    esc_attr( $feed['id'] ),
     190                    esc_html( $feed_name )
    73191                );
    74                 $html .= '</li>';
    75             }
    76             $html .= '</ul>';
     192            }
     193            $html .= '</select><br/><br/>';
     194            $html .= sprintf(
     195                '<input type="submit" value="%s" class="button button-primary" />',
     196                esc_attr__( 'Resend Entry', 'formscrm' )
     197            );
     198            $html .= '</form>';
    77199        } else {
    78             $html .= '<p>' . esc_html__( 'This will resend the entry to the CRM.', 'formscrm' ) . '</p>';
    79             $html .= '<p>' . esc_html__( 'Actual feeds actived:', 'formscrm' ) . '</p>';
    80             $html .= '<ul>';
    81 
    82             foreach ( $feeds as $feed ) {
    83                 if ( ! $feed['is_active'] || $form_id !== (int) $feed['form_id'] ) {
    84                     continue;
    85                 }
    86                 $html .= '<li>';
    87                 $html .= sprintf(
    88                     // translators: %s is the name of the feed.
    89                     __( 'Feed: %s', 'formscrm' ),
    90                     isset( $feed['meta']['feedName'] ) ? $feed['meta']['feedName'] : $feed['id'],
    91                 );
    92                 $html .= '</li>';
    93             }
    94             $html .= '</ul>';
    95             $html .= '</br>';
    96             // Add the 'Process Feeds' button.
    97             $html .= sprintf(
    98                 '<input type="submit" value="%s" class="button" onclick="jQuery(\'#action\').val(\'%s\');" />',
    99                 __( 'Resend Entry', 'formscrm' ),
    100                 $action
    101             );
    102         }
    103         echo wp_kses_post( $html );
     200            $html .= '<p><em>' . esc_html__( 'No active feeds found for this form.', 'formscrm' ) . '</em></p>';
     201        }
     202
     203        echo wp_kses(
     204            $html,
     205            array(
     206                'p'      => array( 'style' => array() ),
     207                'strong' => array(),
     208                'ul'     => array(),
     209                'li'     => array(),
     210                'br'     => array(),
     211                'form'   => array(
     212                    'method' => array(),
     213                    'style'  => array(),
     214                ),
     215                'input'  => array(
     216                    'type'  => array(),
     217                    'name'  => array(),
     218                    'value' => array(),
     219                    'class' => array(),
     220                    'id'    => array(),
     221                ),
     222                'select' => array(
     223                    'name'     => array(),
     224                    'id'       => array(),
     225                    'multiple' => array(),
     226                    'style'    => array(),
     227                ),
     228                'option' => array(
     229                    'value' => array(),
     230                ),
     231                'label'  => array(
     232                    'for' => array(),
     233                ),
     234                'em'     => array(),
     235            )
     236        );
    104237    }
    105238}
  • formscrm/trunk/includes/formscrm-library/class-gravityforms.php

    r3415133 r3460128  
    145145
    146146        $this->ensure_upgrade();
     147
     148        // Add custom columns to forms list.
     149        if ( is_admin() ) {
     150            add_filter( 'gform_form_list_columns', array( $this, 'add_feeds_column' ), 10 );
     151            add_action( 'gform_form_list_column_formscrm_feeds', array( $this, 'display_feeds_column' ), 10, 1 );
     152            add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_forms_list_styles' ) );
     153        }
     154    }
     155
     156    /**
     157     * Enqueue styles for forms list page
     158     *
     159     * @param string $hook Current admin page hook.
     160     * @return void
     161     */
     162    public function enqueue_forms_list_styles( $hook ) {
     163        // Only load on Gravity Forms pages.
     164        if ( 'toplevel_page_gf_edit_forms' === $hook || strpos( $hook, 'gf_' ) !== false ) {
     165            wp_enqueue_style(
     166                'formscrm-forms-list',
     167                FORMSCRM_PLUGIN_URL . 'includes/assets/formscrm-admin.css',
     168                array(),
     169                FORMSCRM_VERSION
     170            );
     171        }
    147172    }
    148173
     
    260285        $fields = $this->get_crm_fields( true, array(), 'settings' );
    261286
     287        // API Connection Status.
     288        $fields = array_merge(
     289            $fields,
     290            array(
     291                array(
     292                    'label' => __( 'API Connection Status', 'formscrm' ),
     293                    'type'  => 'connection_status',
     294                    'name'  => 'fc_crm_connection_status',
     295                ),
     296            ),
     297        );
     298
    262299        // Expert Mode.
    263300        $fields = array_merge(
     
    309346
    310347        return $api_key_field . '</br>' . $caption;
     348    }
     349
     350    /**
     351     * Settings Connection Status field.
     352     *
     353     * Renders the API connection status indicator for plugin settings.
     354     *
     355     * @param array $field   Field configuration.
     356     * @param bool  $display Whether to display or return the HTML.
     357     * @return string HTML output.
     358     */
     359    public function settings_connection_status( $field, $display = true ) {
     360        $settings  = $this->get_plugin_settings();
     361        $help_text = __( 'Save settings and reload the page to test the connection.', 'formscrm' );
     362        $html      = formscrm_get_connection_status_html( $settings, 'badge', $help_text );
     363
     364        if ( $display ) {
     365            formscrm_render_connection_status( $settings, 'badge', $help_text );
     366        }
     367
     368        return $html;
     369    }
     370
     371    /**
     372     * Settings Feed Connection Status field.
     373     *
     374     * Renders the API connection status indicator for feed settings.
     375     *
     376     * @param array $field   Field configuration.
     377     * @param bool  $display Whether to display or return the HTML.
     378     * @return string HTML output.
     379     */
     380    public function settings_feed_connection_status( $field, $display = true ) {
     381        $settings    = $this->get_api_settings_custom();
     382        $status_data = formscrm_check_connection_status( $settings );
     383        $help_text   = '';
     384
     385        // Show help text only for error states.
     386        if ( 'disconnected' === $status_data['status'] || 'error' === $status_data['status'] ) {
     387            $help_text = __( 'Please check your CRM credentials in the FormsCRM settings.', 'formscrm' );
     388        }
     389
     390        $html = formscrm_build_status_html( $status_data, 'badge', $help_text );
     391
     392        if ( $display ) {
     393            formscrm_render_connection_status( $settings, 'badge', $help_text );
     394        }
     395
     396        return $html;
    311397    }
    312398
     
    414500        $login_crm       = $this->login_api_crm();
    415501
     502        // Add connection status field.
     503        $crm_feed_fields[] = array(
     504            'name'  => 'fc_feed_connection_status',
     505            'label' => __( 'API Connection Status', 'formscrm' ),
     506            'type'  => 'feed_connection_status',
     507        );
     508
    416509        if ( is_array( $login_crm ) && isset( $login_crm['status'] ) && 'error' === $login_crm['status'] ) {
    417             $crm_feed_fields[] = array(
    418                 'name'  => 'fc_login_result',
    419                 'label' => __( 'We could not login to the CRM', 'formscrm' ) . ' ' . $login_crm['message'],
    420                 'type'  => 'hidden',
    421             );
    422510            return $crm_feed_fields;
    423511        }
    424512
    425513        if ( false === $login_crm ) {
    426             $crm_feed_fields[] = array(
    427                 'name'  => 'fc_login_result',
    428                 'label' => __( 'We could not login to the CRM', 'formscrm' ),
    429                 'type'  => 'hidden',
    430             );
     514            // Connection status field already added above, no additional fields needed.
     515            return $crm_feed_fields;
    431516        } else {
    432517            $module = $this->get_actual_feed_value( 'fc_crm_module', $feed_settings );
     
    576661        update_option( 'fc_crm_upgrade', 1 );
    577662        return true;
     663    }
     664
     665    /**
     666     * Add feeds column to forms list
     667     *
     668     * @param array $columns Existing columns.
     669     * @return array Modified columns.
     670     */
     671    public function add_feeds_column( $columns ) {
     672        $columns['formscrm_feeds'] = esc_html__( 'Connected Feeds', 'formscrm' );
     673        return $columns;
     674    }
     675
     676    /**
     677     * Display feeds column content
     678     *
     679     * @param array $form Form object.
     680     * @return void
     681     */
     682    public function display_feeds_column( $form ) {
     683        // Get form ID from array or object.
     684        $form_id = 0;
     685
     686        if ( is_array( $form ) ) {
     687            $form_id = isset( $form['id'] ) ? absint( $form['id'] ) : 0;
     688        } elseif ( is_object( $form ) ) {
     689            $form_id = isset( $form->id ) ? absint( $form->id ) : 0;
     690        }
     691
     692        // If no form ID, show disconnected.
     693        if ( ! $form_id ) {
     694            echo '<span class="gform-status-indicator gform-status--inactive">● ' . esc_html__( 'Disconnected', 'formscrm' ) . '</span>';
     695            return;
     696        }
     697
     698        try {
     699            // Get feeds for this form.
     700            $feeds = $this->get_feeds( $form_id );
     701
     702            // No feeds - show Disconnected.
     703            if ( empty( $feeds ) || ! is_array( $feeds ) ) {
     704                return;
     705            }
     706
     707            // Has feeds - show Connected.
     708            $feed_count = count( $feeds );
     709
     710            echo '<div class="formscrm-feeds-wrapper">';
     711            echo '<span class="gform-status-indicator gform-status--active">' . esc_html__( 'Connected', 'formscrm' ) . '</span>';
     712
     713            // Show feed details.
     714            echo '<div class="formscrm-feeds-list">';
     715
     716            foreach ( $feeds as $feed ) {
     717                if ( ! is_array( $feed ) || empty( $feed['meta'] ) ) {
     718                    continue;
     719                }
     720
     721                $feed_name = isset( $feed['meta']['feedName'] ) ? $feed['meta']['feedName'] : __( 'Unnamed Feed', 'formscrm' );
     722                $crm_type  = '';
     723
     724                // Get CRM type.
     725                if ( ! empty( $feed['meta']['fc_crm_custom_type'] ) && 'no' !== $feed['meta']['fc_crm_custom_type'] ) {
     726                    $crm_type = $feed['meta']['fc_crm_custom_type'];
     727                } else {
     728                    $settings = $this->get_plugin_settings();
     729                    if ( ! empty( $settings['fc_crm_type'] ) ) {
     730                        $crm_type = $settings['fc_crm_type'];
     731                    }
     732                }
     733
     734                $is_active = ! empty( $feed['is_active'] );
     735                $status    = $is_active ? '✓' : '✗';
     736                $color     = $is_active ? '#46b450' : '#dc3232';
     737                $title     = $is_active ? __( 'Active', 'formscrm' ) : __( 'Inactive', 'formscrm' );
     738
     739                echo '<div class="formscrm-feed-item">';
     740                printf(
     741                    '<span style="color: %s; font-weight: bold;" title="%s">%s</span> ',
     742                    esc_attr( $color ),
     743                    esc_attr( $title ),
     744                    esc_html( $status )
     745                );
     746                    echo '<span class="formscrm-feed-name">' . esc_html( $feed_name ) . '</span>';
     747
     748                if ( ! empty( $crm_type ) ) {
     749                    echo ' <span class="formscrm-feed-crm">(' . esc_html( ucfirst( $crm_type ) ) . ')</span>';
     750                }
     751                echo '</div>';
     752            }
     753
     754            // Show total if more than 1.
     755            if ( $feed_count > 1 ) {
     756                echo '<div class="formscrm-feed-total">';
     757                printf(
     758                    /* translators: %d: number of feeds */
     759                    esc_html__( 'Total: %d feeds', 'formscrm' ),
     760                    absint( $feed_count )
     761                );
     762                echo '</div>';
     763            }
     764
     765            echo '</div>'; // .formscrm-feeds-list
     766            echo '</div>'; // .formscrm-feeds-wrapper
     767        } catch ( Exception $e ) {
     768            echo '<span class="gform-status-indicator gform-status--inactive">● ' . esc_html__( 'Error', 'formscrm' ) . '</span>';
     769        }
    578770    }
    579771
     
    681873
    682874            $form_info = array(
    683                 'form_type' => 'Gravity Forms',
    684                 'form_id'   => isset( $form['id'] ) ? $form['id'] : '',
    685                 'form_name' => isset( $form['title'] ) ? $form['title'] : '',
    686                 'entry_id'  => isset( $entry['id'] ) ? $entry['id'] : '',
     875                'form_type'       => 'gravityforms',
     876                'form_type_title' => 'Gravity Forms',
     877                'form_id'         => isset( $form['id'] ) ? $form['id'] : '',
     878                'form_name'       => isset( $form['title'] ) ? $form['title'] : '',
     879                'entry_id'        => isset( $entry['id'] ) ? $entry['id'] : '',
    687880            );
    688881
  • formscrm/trunk/includes/formscrm-library/class-woocommerce.php

    r3415133 r3460128  
    194194            'id'   => 'wc_settings_formscrm_section_end',
    195195        );
     196
     197        // Show API connection status.
     198        if ( ! empty( $wc_formscrm['fc_crm_type'] ) ) {
     199            formscrm_render_connection_status( $wc_formscrm, 'notice' );
     200        }
     201
    196202        if ( ! empty( $this->crmlib ) && ! empty( $wc_formscrm ) ) {
    197203            $login_crm = $this->crmlib->login( $wc_formscrm );
    198204            if ( is_array( $login_crm ) && isset( $login_crm['status'] ) && 'error' === $login_crm['status'] ) {
    199                 echo '<div class="notice notice-error"><p>' . esc_html__( 'We could not login to the CRM', 'formscrm' ) . ' ' . esc_html( $login_crm['message'] ) . '</p></div>';
    200205                return $settings_crm;
    201206            }
    202207
    203208            if ( false === $login_crm ) {
    204                 echo '<div class="notice notice-error"><p>' . esc_html__( 'We could not login to the CRM', 'formscrm' ) . '</p></div>';
    205209                return $settings_crm;
    206210            }
     
    261265            if ( 'error' === $response_result['status'] ) {
    262266                $form_info = array(
    263                     'form_type' => 'WooCommerce',
    264                     'form_id'   => 'checkout',
    265                     'form_name' => 'WooCommerce Checkout',
    266                     'entry_id'  => $order_id,
     267                    'form_type'       => 'woocommerce',
     268                    'form_type_title' => 'WooCommerce',
     269                    'form_id'         => 'checkout',
     270                    'form_name'       => 'WooCommerce Checkout',
     271                    'entry_id'        => $order_id,
    267272                );
    268273
  • formscrm/trunk/includes/formscrm-library/class-wpforms.php

    r3415133 r3460128  
    174174                if ( 'error' === $api_status ) {
    175175                    $form_info = array(
    176                         'form_type' => 'WPForms',
    177                         'form_id'   => $form_id,
    178                         'form_name' => isset( $form_data['settings']['form_title'] ) ? $form_data['settings']['form_title'] : '',
    179                         'entry_id'  => $entry_id,
     176                        'form_type'       => 'wpforms',
     177                        'form_type_title' => 'WPForms',
     178                        'form_id'         => $form_id,
     179                        'form_name'       => isset( $form_data['settings']['form_title'] ) ? $form_data['settings']['form_title'] : '',
     180                        'entry_id'        => $entry_id,
    180181                    );
    181182                    formscrm_alert_error( $settings['fc_crm_type'], 'Error ' . $api_message, $merge_vars, '', '', $form_info );
     
    517518     */
    518519    public function output_options( $connection_id = '', $connection = array() ) {
    519 
    520         // Double opt in and a welcome email are defined in the List options on FormsCRM.
    521         // They can't be controlled via the API.
    522         return '';
     520        $account_id = ! empty( $connection['account_id'] ) ? $connection['account_id'] : '';
     521        $html       = '';
     522
     523        if ( ! empty( $account_id ) ) {
     524            $settings = $this->api_connect( $account_id );
     525            if ( is_array( $settings ) && ! empty( $settings['fc_crm_type'] ) ) {
     526                // Get connection status and display it prominently.
     527                $status_html = formscrm_get_connection_status_html( $settings, 'badge' );
     528
     529                // Wrap in a visible container with proper styling.
     530                $html  = '<div class="wpforms-provider-connection-status" style="margin: 15px 0; padding: 12px; background: #f9f9f9; border-radius: 4px; border-left: 4px solid #0073aa;">';
     531                $html .= '<div style="display: flex; align-items: center; justify-content: space-between;">';
     532                $html .= '<div>';
     533                $html .= '<strong style="display: block; margin-bottom: 5px; color: #23282d;">' . esc_html__( 'CRM Connection:', 'formscrm' ) . '</strong>';
     534                $html .= $status_html;
     535                $html .= '</div>';
     536
     537                // Add CRM type info.
     538                if ( ! empty( $settings['fc_crm_type'] ) ) {
     539                    $html .= '<div style="text-align: right;">';
     540                    $html .= '<span style="display: block; font-size: 11px; color: #666; text-transform: uppercase; letter-spacing: 0.5px;">' . esc_html__( 'CRM Type', 'formscrm' ) . '</span>';
     541                    $html .= '<strong style="font-size: 14px; color: #0073aa;">' . esc_html( ucfirst( $settings['fc_crm_type'] ) ) . '</strong>';
     542                    $html .= '</div>';
     543                }
     544
     545                $html .= '</div>';
     546                $html .= '</div>';
     547            }
     548        }
     549
     550        return $html;
    523551    }
    524552
  • formscrm/trunk/includes/formscrm-library/elementor-ajax.php

    r3415133 r3460128  
    5151    // 1. Check connection to CRM
    5252    $crmtype = isset( $_POST['crmSettings']['fc_crm_type'] ) ? sanitize_text_field( wp_unslash( $_POST['crmSettings']['fc_crm_type'] ) ) : '';
    53     $crmlib  = null;
    5453
    5554    if ( empty( $crmtype ) ) {
     
    5756    }
    5857
    59     $crmname      = strtolower( $crmtype );
    60     $crmclassname = str_replace( ' ', '', $crmname );
    61     $crmclassname = 'CRMLIB_' . strtoupper( $crmclassname );
    62     $crmname      = str_replace( ' ', '_', $crmname );
    63 
    64     $array_path = formscrm_get_crmlib_path();
    65     if ( isset( $array_path[ $crmname ] ) ) {
    66         include_once $array_path[ $crmname ];
    67     }
    68 
    69     formscrm_debug_message( $array_path[ $crmname ] );
    70 
    71     if ( ! class_exists( $crmclassname ) ) {
    72         wp_send_json_error( __( 'Class not found', 'formscrm' ) );
    73     }
    74 
    75     $crmlib = new $crmclassname();
     58    // Load CRM library class using helper function.
     59    $crmlib = formscrm_get_api_class( $crmtype );
     60
     61    if ( ! $crmlib ) {
     62        wp_send_json_error( __( 'Could not load CRM library', 'formscrm' ) );
     63    }
    7664
    7765    $crm_settings_raw = isset( $_POST['crmSettings'] ) ? wp_unslash( $_POST['crmSettings'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized in formscrm_elementor_process_settings().
    7866    $post_data        = formscrm_elementor_process_settings( $crm_settings_raw );
     67
     68    // Check connection status.
     69    $status_data = formscrm_check_connection_status( $post_data );
     70
     71    // Store connection status HTML separately.
     72    $status_html = formscrm_get_connection_status_html( $post_data, 'elementor' );
     73
     74    // If connection failed, return error with status HTML.
     75    if ( 'connected' !== $status_data['status'] ) {
     76        $error_msg = __( 'Could not connect to CRM', 'formscrm' );
     77        if ( ! empty( $status_data['error_message'] ) ) {
     78            $error_msg .= ': ' . $status_data['error_message'];
     79        }
     80
     81        // Return error with status HTML for JavaScript to handle.
     82        wp_send_json_error(
     83            array(
     84                'message'     => $error_msg,
     85                'status_html' => $status_html,
     86            )
     87        );
     88    }
    7989
    8090    // 2. Show modules dropdown
     
    196206    }
    197207
    198     wp_send_json_success( ob_get_clean() );
     208    $form_html = ob_get_clean();
     209
     210    // Return success with both form HTML and status HTML.
     211    wp_send_json_success(
     212        array(
     213            'form_html'   => $form_html,
     214            'status_html' => $status_html,
     215        )
     216    );
    199217}
  • formscrm/trunk/includes/formscrm-library/helpers-functions.php

    r3425471 r3460128  
    1616     *
    1717     * @param string $crm_type Type of CRM.
    18      * @return object|void
     18     * @return object|null
    1919     */
    2020    function formscrm_get_api_class( $crm_type ) {
    21         $crmname      = strtolower( $crm_type );
     21        // Normalize CRM type.
     22        $crmname      = strtolower( trim( $crm_type ) );
    2223        $crmclassname = str_replace( ' ', '', $crmname );
    23         $crmclassname = 'CRMLIB_' . strtoupper( $crmclassname );
     24        $crmclassname = 'CRMLIB_' . ucfirst( $crmclassname );
    2425        $crmname      = str_replace( ' ', '_', $crmname );
    2526
     27        formscrm_debug_message( 'Attempting to load CRM: ' . $crmname );
     28
    2629        $array_path = formscrm_get_crmlib_path();
    2730
     31        // Log available CRM paths for debugging.
     32        formscrm_debug_message( 'Available CRM paths: ' . wp_json_encode( array_keys( $array_path ) ) );
     33
    2834        if ( isset( $array_path[ $crmname ] ) ) {
    29             include_once $array_path[ $crmname ];
    30             formscrm_debug_message( $array_path[ $crmname ] );
    31         }
    32 
     35            $file_path = $array_path[ $crmname ];
     36
     37            // Verify file exists before including.
     38            if ( ! file_exists( $file_path ) ) {
     39                formscrm_debug_message( 'ERROR: CRM library file not found: ' . $file_path );
     40                return null;
     41            }
     42
     43            include_once $file_path;
     44            formscrm_debug_message( 'Included CRM library: ' . $file_path );
     45        } else {
     46            formscrm_debug_message( 'ERROR: CRM path not registered for: ' . $crmname );
     47            return null;
     48        }
     49
     50        // Verify class exists after including file.
    3351        if ( class_exists( $crmclassname ) ) {
     52            formscrm_debug_message( 'Successfully created instance of: ' . $crmclassname );
    3453            return new $crmclassname();
    3554        }
     55
     56        formscrm_debug_message( 'ERROR: CRM class not found: ' . $crmclassname );
     57        return null;
     58    }
     59}
     60
     61if ( ! function_exists( 'formscrm_get_crm_settings' ) ) {
     62    /**
     63     * Get CRM settings from WordPress options
     64     *
     65     * @param string $form_type Type of form (gravity, woocommerce, etc).
     66     * @return array Settings array.
     67     */
     68    function formscrm_get_crm_settings( $form_type = '' ) {
     69        $settings = array();
     70
     71        // Try to get settings based on form type.
     72        if ( 'gravity' === $form_type || 'gravityforms' === $form_type ) {
     73            $settings = get_option( 'gravityformsaddon_formscrm_settings', array() );
     74        } elseif ( 'woocommerce' === $form_type ) {
     75            $settings = get_option( 'wc_formscrm', array() );
     76        } else {
     77            // Default to Gravity Forms settings as fallback.
     78            $settings = get_option( 'gravityformsaddon_formscrm_settings', array() );
     79
     80            // Fallback to WooCommerce settings when Gravity Forms settings are empty.
     81            if ( empty( $settings ) ) {
     82                $settings = get_option( 'wc_formscrm', array() );
     83            }
     84        }
     85
     86        formscrm_debug_message( 'Retrieved CRM settings for form type: ' . $form_type );
     87
     88        return $settings;
    3689    }
    3790}
     
    106159     */
    107160    function formscrm_alert_error( $crm, $error, $data, $url = '', $json = '', $form_info = array() ) {
     161        // Log error to database.
     162        global $formscrm_error_log;
     163        if ( isset( $formscrm_error_log ) && method_exists( $formscrm_error_log, 'insert_log' ) ) {
     164            $formscrm_error_log->insert_log( $crm, $error, $data, $url, $json, $form_info );
     165        }
     166
    108167        // Get custom email or fallback to admin email.
    109168        $custom_email = get_option( 'formscrm_error_notification_email', '' );
     
    436495}
    437496
     497if ( ! function_exists( 'formscrm_normalize_date_format' ) ) {
     498    /**
     499     * Normalizes date to YYYY-MM-DD format required by APIs like Clientify.
     500     *
     501     * Supported input formats:
     502     * - dd/mm/yyyy (European format)
     503     * - dd-mm-yyyy (European format with dashes)
     504     * - dd.mm.yyyy (European format with dots)
     505     * - yyyy-mm-dd (ISO format - already correct)
     506     * - yyyy/mm/dd (ISO format with slashes)
     507     * - mm/dd/yyyy (US format)
     508     * - mm-dd-yyyy (US format with dashes)
     509     * - Unix timestamps
     510     *
     511     * @param string $date_value The date value to normalize.
     512     * @return string|false Normalized date in YYYY-MM-DD format or false if invalid.
     513     */
     514    function formscrm_normalize_date_format( $date_value ) {
     515        if ( empty( $date_value ) ) {
     516            return false;
     517        }
     518
     519        $date_value = trim( $date_value );
     520
     521        // Already in correct format YYYY-MM-DD.
     522        if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date_value ) ) {
     523            return $date_value;
     524        }
     525
     526        // Handle Unix timestamp.
     527        if ( is_numeric( $date_value ) && strlen( $date_value ) >= 8 ) {
     528            $timestamp = (int) $date_value;
     529            $date      = gmdate( 'Y-m-d', $timestamp );
     530            if ( false !== $date ) {
     531                return $date;
     532            }
     533        }
     534
     535        // European format: dd/mm/yyyy or dd-mm-yyyy or dd.mm.yyyy.
     536        if ( preg_match( '/^(\d{1,2})[\/\-\.](\d{1,2})[\/\-\.](\d{4})$/', $date_value, $matches ) ) {
     537            $day   = (int) $matches[1];
     538            $month = (int) $matches[2];
     539            $year  = (int) $matches[3];
     540
     541            // Validate date components.
     542            if ( $day > 31 || $month > 12 ) {
     543                // Could be US format mm/dd/yyyy, try swapping.
     544                if ( $month <= 31 && $day <= 12 ) {
     545                    $temp  = $day;
     546                    $day   = $month;
     547                    $month = $temp;
     548                }
     549            }
     550
     551            // Validate the date.
     552            if ( checkdate( $month, $day, $year ) ) {
     553                return sprintf( '%04d-%02d-%02d', $year, $month, $day );
     554            }
     555        }
     556
     557        // ISO format with slashes: yyyy/mm/dd.
     558        if ( preg_match( '/^(\d{4})[\/](\d{1,2})[\/](\d{1,2})$/', $date_value, $matches ) ) {
     559            $year  = (int) $matches[1];
     560            $month = (int) $matches[2];
     561            $day   = (int) $matches[3];
     562
     563            if ( checkdate( $month, $day, $year ) ) {
     564                return sprintf( '%04d-%02d-%02d', $year, $month, $day );
     565            }
     566        }
     567
     568        // Try PHP's strtotime as last resort for other formats.
     569        $timestamp = strtotime( $date_value );
     570        if ( false !== $timestamp && -1 !== $timestamp ) {
     571            return gmdate( 'Y-m-d', $timestamp );
     572        }
     573
     574        return false;
     575    }
     576}
     577
    438578if ( ! function_exists( 'formscrm_get_svg_icon' ) ) {
    439579    /**
     
    458598    }
    459599}
     600
     601if ( ! function_exists( 'formscrm_get_connection_status_html' ) ) {
     602    /**
     603     * Get HTML for API connection status indicator.
     604     *
     605     * @param array  $settings    CRM settings array with fc_crm_type and credentials.
     606     * @param string $output_type Output type: 'html', 'notice', 'badge', or 'elementor'.
     607     * @param string $help_text   Optional help text to display below status.
     608     * @return string HTML output for the connection status.
     609     */
     610    function formscrm_get_connection_status_html( $settings, $output_type = 'html', $help_text = '' ) {
     611        $status_data = formscrm_check_connection_status( $settings );
     612
     613        return formscrm_build_status_html( $status_data, $output_type, $help_text );
     614    }
     615}
     616
     617if ( ! function_exists( 'formscrm_check_connection_status' ) ) {
     618    /**
     619     * Check connection status and return status data array.
     620     *
     621     * @param array $settings CRM settings array with fc_crm_type and credentials.
     622     * @return array Status data with keys: status, text, color, icon, error_message, crm_type.
     623     */
     624    function formscrm_check_connection_status( $settings ) {
     625        $crm_type = isset( $settings['fc_crm_type'] ) ? $settings['fc_crm_type'] : '';
     626        $data     = array(
     627            'status'        => 'unknown',
     628            'text'          => __( 'Not configured', 'formscrm' ),
     629            'color'         => '#999999',
     630            'icon'          => '○',
     631            'error_message' => '',
     632            'crm_type'      => $crm_type,
     633        );
     634
     635        if ( empty( $crm_type ) ) {
     636            return $data;
     637        }
     638
     639        $crmlib = formscrm_get_api_class( $crm_type );
     640
     641        if ( ! isset( $crmlib ) || ! method_exists( $crmlib, 'login' ) ) {
     642            return $data;
     643        }
     644
     645        $login_result = $crmlib->login( $settings );
     646        $login_status = isset( $login_result['status'] ) ? $login_result['status'] : '';
     647
     648        if ( is_array( $login_result ) && 'error' === $login_status ) {
     649            $data['status']        = 'error';
     650            $data['text']          = __( 'Error', 'formscrm' );
     651            $data['color']         = '#dc3232';
     652            $data['icon']          = '✕';
     653            $data['error_message'] = isset( $login_result['message'] ) ? $login_result['message'] : '';
     654        } elseif ( true === $login_result || 'ok' === $login_status ) {
     655            $data['status'] = 'connected';
     656            $data['text']   = __( 'Connected', 'formscrm' );
     657            $data['color']  = '#46b450';
     658            $data['icon']   = '✓';
     659        } else {
     660            $data['status'] = 'disconnected';
     661            $data['text']   = __( 'Disconnected', 'formscrm' );
     662            $data['color']  = '#dc3232';
     663            $data['icon']   = '✕';
     664        }
     665
     666        return $data;
     667    }
     668}
     669
     670if ( ! function_exists( 'formscrm_build_status_html' ) ) {
     671    /**
     672     * Build HTML from status data.
     673     *
     674     * @param array  $status_data Status data from formscrm_check_connection_status().
     675     * @param string $output_type Output type: 'html', 'notice', 'badge', or 'elementor'.
     676     * @param string $help_text   Optional help text to display below status.
     677     * @return string HTML output.
     678     */
     679    function formscrm_build_status_html( $status_data, $output_type = 'html', $help_text = '' ) {
     680        $status        = $status_data['status'];
     681        $status_text   = $status_data['text'];
     682        $status_color  = $status_data['color'];
     683        $status_icon   = $status_data['icon'];
     684        $error_message = $status_data['error_message'];
     685        $crm_type      = $status_data['crm_type'];
     686        $html          = '';
     687
     688        switch ( $output_type ) {
     689            case 'notice':
     690                $notice_class = 'connected' === $status ? 'notice-success' : ( 'unknown' === $status ? 'notice-warning' : 'notice-error' );
     691                $html         = '<div class="notice ' . esc_attr( $notice_class ) . '" style="padding: 10px;">';
     692                $html        .= '<strong>' . esc_html__( 'API Connection Status:', 'formscrm' ) . '</strong> ';
     693                $html        .= '<span style="color: ' . esc_attr( $status_color ) . '; font-weight: bold;">';
     694                $html        .= esc_html( $status_icon ) . ' ' . esc_html( $status_text );
     695                $html        .= '</span>';
     696
     697                if ( ! empty( $crm_type ) ) {
     698                    $html .= ' <span style="color: #666;">(' . esc_html( ucfirst( $crm_type ) ) . ')</span>';
     699                }
     700
     701                if ( ! empty( $error_message ) ) {
     702                    $html .= '<br/><span style="color: #dc3232; font-size: 12px;">' . esc_html( $error_message ) . '</span>';
     703                }
     704
     705                $html .= '</div>';
     706                break;
     707
     708            case 'elementor':
     709                $bg_color     = 'connected' === $status ? '#f9f9f9' : ( 'unknown' === $status ? '#f9f9f9' : '#ffebee' );
     710                $border_color = 'connected' === $status ? '#46b450' : ( 'unknown' === $status ? '#0073aa' : '#dc3232' );
     711                $badge_color  = 'connected' === $status ? '#46b450' : ( 'unknown' === $status ? '#999' : '#dc3232' );
     712
     713                $html  = '<div style="padding: 12px; background: ' . esc_attr( $bg_color ) . '; border-left: 4px solid ' . esc_attr( $border_color ) . '; border-radius: 4px; margin-bottom: 15px;">';
     714                $html .= '<div style="display: flex; align-items: center; justify-content: space-between;">';
     715                $html .= '<div style="display: flex; align-items: center; gap: 8px;">';
     716                $html .= '<strong style="color: #23282d;">' . esc_html__( 'API Connection Status:', 'formscrm' ) . '</strong> ';
     717                $html .= '<span style="display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 3px; background: ' . esc_attr( $badge_color ) . '; color: white; font-size: 12px; font-weight: bold;">';
     718                $html .= '<span style="margin-right: 5px;">' . esc_html( $status_icon ) . '</span>' . esc_html( $status_text );
     719                $html .= '</span>';
     720                $html .= '</div>';
     721
     722                if ( ! empty( $crm_type ) ) {
     723                    $html .= '<div style="text-align: right;">';
     724                    $html .= '<span style="display: block; font-size: 11px; color: #666; text-transform: uppercase; letter-spacing: 0.5px;">' . esc_html__( 'CRM Type', 'formscrm' ) . '</span>';
     725                    $html .= '<strong style="font-size: 14px; color: #0073aa;">' . esc_html( ucfirst( $crm_type ) ) . '</strong>';
     726                    $html .= '</div>';
     727                }
     728
     729                $html .= '</div>';
     730
     731                if ( ! empty( $error_message ) ) {
     732                    $html .= '<p style="margin: 8px 0 0 0; padding-top: 8px; border-top: 1px solid #ddd; color: #dc3232; font-size: 12px;"><strong>' . esc_html__( 'Error:', 'formscrm' ) . '</strong> ' . esc_html( $error_message ) . '</p>';
     733                }
     734
     735                $html .= '</div>';
     736                break;
     737
     738            case 'badge':
     739                $html  = '<div class="formscrm-connection-status" style="display: flex; align-items: center; gap: 10px;">';
     740                $html .= sprintf(
     741                    '<span class="formscrm-status-badge" style="display: inline-flex; align-items: center; padding: 6px 12px; border-radius: 4px; background-color: %1$s; color: white; font-weight: bold; font-size: 13px;">',
     742                    esc_attr( $status_color )
     743                );
     744                $html .= '<span style="margin-right: 6px; font-size: 14px;">' . esc_html( $status_icon ) . '</span>';
     745                $html .= esc_html( $status_text );
     746                $html .= '</span>';
     747
     748                if ( ! empty( $crm_type ) ) {
     749                    $html .= sprintf(
     750                        '<span class="formscrm-crm-name" style="color: #666; font-size: 13px;">(%s)</span>',
     751                        esc_html( ucfirst( $crm_type ) )
     752                    );
     753                }
     754
     755                $html .= '</div>';
     756
     757                if ( ! empty( $error_message ) ) {
     758                    $html .= sprintf(
     759                        '<p class="formscrm-error-message" style="color: #dc3232; margin-top: 8px; font-size: 12px;"><strong>%s:</strong> %s</p>',
     760                        esc_html__( 'Error details', 'formscrm' ),
     761                        esc_html( $error_message )
     762                    );
     763                }
     764                break;
     765
     766            default: // 'html'
     767                $html  = '<div class="formscrm-connection-status" style="display: flex; align-items: center; gap: 10px; margin: 10px 0;">';
     768                $html .= '<strong>' . esc_html__( 'API Connection Status:', 'formscrm' ) . '</strong> ';
     769                $html .= sprintf(
     770                    '<span class="formscrm-status-badge" style="display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 3px; background-color: %1$s; color: white; font-weight: bold; font-size: 12px;">',
     771                    esc_attr( $status_color )
     772                );
     773                $html .= '<span style="margin-right: 5px;">' . esc_html( $status_icon ) . '</span>';
     774                $html .= esc_html( $status_text );
     775                $html .= '</span>';
     776
     777                if ( ! empty( $crm_type ) ) {
     778                    $html .= sprintf(
     779                        '<span style="color: #666; font-size: 12px;">(%s)</span>',
     780                        esc_html( ucfirst( $crm_type ) )
     781                    );
     782                }
     783
     784                $html .= '</div>';
     785
     786                if ( ! empty( $error_message ) ) {
     787                    $html .= sprintf(
     788                        '<p style="color: #dc3232; margin: 5px 0; font-size: 12px;"><strong>%s:</strong> %s</p>',
     789                        esc_html__( 'Error', 'formscrm' ),
     790                        esc_html( $error_message )
     791                    );
     792                }
     793                break;
     794        }
     795
     796        // Add help text if provided.
     797        if ( ! empty( $help_text ) ) {
     798            $html .= '<p class="formscrm-status-help" style="color: #666; margin-top: 8px; font-size: 12px;">';
     799            $html .= esc_html( $help_text );
     800            $html .= '</p>';
     801        }
     802
     803        return $html;
     804    }
     805}
     806
     807if ( ! function_exists( 'formscrm_render_connection_status' ) ) {
     808    /**
     809     * Render API connection status indicator.
     810     *
     811     * Echoes the HTML for API connection status.
     812     *
     813     * @param array  $settings    CRM settings array with fc_crm_type and credentials.
     814     * @param string $output_type Output type: 'html', 'notice', 'badge', or 'elementor'.
     815     * @param string $help_text   Optional help text to display below status.
     816     * @return void
     817     */
     818    function formscrm_render_connection_status( $settings, $output_type = 'html', $help_text = '' ) {
     819        $allowed_html = array(
     820            'div'    => array(
     821                'class' => array(),
     822                'style' => array(),
     823            ),
     824            'span'   => array(
     825                'class' => array(),
     826                'style' => array(),
     827            ),
     828            'strong' => array(),
     829            'p'      => array(
     830                'class' => array(),
     831                'style' => array(),
     832            ),
     833            'br'     => array(),
     834        );
     835
     836        echo wp_kses( formscrm_get_connection_status_html( $settings, $output_type, $help_text ), $allowed_html );
     837    }
     838}
  • formscrm/trunk/includes/formscrm-library/loader.php

    r3415133 r3460128  
    5454
    5555    require_once 'class-gravityforms-widget.php';
     56    require_once 'class-gravityforms-markdown-export.php';
    5657}
    5758
  • formscrm/trunk/readme.txt

    r3425471 r3460128  
    55Requires at least: 5.5
    66Tested up to: 6.9
    7 Stable tag: 4.2.1
    8 Version: 4.2.1
     7Stable tag: 4.3.0
     8Version: 4.3.0
    99License: GPL2
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    8888
    8989All Slack notifications use a compact, easy-to-read format with information presented in single lines. Messages are color-coded in red (danger) to stand out in your channel and ensure immediate attention to critical errors.
     90
    9091== Error Notifications ==
    9192**Custom Email for Error Reports**
     
    106107The email is professionally formatted with color-coded sections for easy reading and quick troubleshooting.
    107108
     109== Error Log with Automatic Retry System ==
     110
     111**Track, Manage, and Automatically Retry Failed Form Submissions**
     112
     113The Error Log feature provides a comprehensive interface to view, track, and manage all errors that occur when sending form submissions to your CRM. This powerful tool includes an automatic retry system that helps you troubleshoot issues and recover from failed submissions without requiring manual intervention or users to resubmit forms.
     114
     115**Key Features:**
     116
     117* **Automatic Retry System**: Failed entries are automatically retried up to 3 times with 1-hour intervals between attempts
     118* **Smart Retry Management**: Retries stop automatically when an entry is successfully sent or manually deleted
     119* **Complete Error Tracking**: All errors are automatically saved to the database with complete context including CRM type, error message, form information, lead data, and technical details
     120* **Advanced Filtering**: Filter errors by status (failed/success) and CRM type to quickly find specific issues
     121* **Detailed Error Information**: View complete error details including lead data, API URLs, JSON requests, and full error messages
     122* **One-Click Manual Resend**: Manually resend failed entries directly from the error log with a single click
     123* **Error Management**: Delete individual entries or clear all logs with confirmation dialogs
     124* **Pagination**: Navigate through large numbers of error logs with built-in pagination (20 entries per page)
     125* **Visual Status Tracking**: Status badges show failed and successful entries at a glance
     126* **Retry Progress Counter**: Shows retry attempts (e.g., "2/3") and displays time until next automatic retry
     127* **Responsive Design**: Fully responsive interface that works on all devices
     128
     129**Automatic Retry System:**
     130
     131When a form submission fails to send to your CRM:
     132
     1331. The error is logged immediately and the first retry is scheduled for 1 hour later
     1342. If the retry fails, another retry is scheduled for 1 hour after that
     1353. This continues for up to 3 total attempts (original submission + 2 retries)
     1364. If an attempt succeeds, all future retries are automatically cancelled
     1375. You can manually resend at any time, which counts toward the 3-attempt limit
     1386. The interface shows the current attempt count (e.g., "1/3", "2/3") and time until next retry
     139
     140**How to Use:**
     141
     1421. Go to **WordPress Admin → FormsCRM → Error Log tab**
     1432. View all form submission errors in an organized table
     1443. Filter by status or CRM type to find specific errors
     1454. Click **Details** to view complete error information including retry schedule
     1465. Click **Resend** to manually retry sending a failed entry to your CRM
     1476. Click **Delete** to remove individual log entries and cancel any pending retries
     1487. Use **Clear All Logs** to remove all entries at once and cancel all pending retries
     149
     150**What Information is Displayed:**
     151
     152* Date and time of error
     153* CRM type (Holded, Clientify, etc.)
     154* Form information (type, ID, name, entry ID)
     155* Complete error message
     156* All lead data from the form submission
     157* API endpoint URL
     158* JSON request payload
     159* Retry attempts count (e.g., "2/3")
     160* Time until next automatic retry (e.g., "Next: in 45 minutes")
     161* Last resend date and time
     162
     163The Error Log with automatic retry system helps you maintain data integrity by ensuring no form submissions are lost due to temporary errors, connectivity issues, or API downtime. The automatic retry mechanism increases the success rate of form submissions without requiring manual intervention.
     164
     165== Markdown Export for GravityForms Entries ==
     166
     167**Export your GravityForms entries as portable, human-readable Markdown files**
     168
     169The Markdown Export feature allows you to export GravityForms entries into clean, well-structured `.md` files. This makes it easy to document, share, version control, or integrate form submissions with knowledge bases, static site generators, or any Markdown-compatible system.
     170
     171**Key Features:**
     172
     173* **Single Entry Export**: Export individual entries directly from the entry detail page
     174* **Bulk Export**: Export multiple selected entries at once as a convenient ZIP file
     175* **Clean Formatting**: Produces readable, well-structured Markdown with proper headers and field organization
     176* **Comprehensive Field Support**: Handles all GravityForms field types including text, email, number, textarea, checkboxes, multiselect, name fields, address fields, file uploads, and list fields
     177* **Smart Content Handling**: Properly formats multi-line content, preserves line breaks, and handles file attachments with Markdown links
     178* **Metadata Included**: Each export includes form title, entry ID, submission date, and all field labels and values
     179* **Safe Character Escaping**: Automatically escapes Markdown special characters to ensure valid output
     180
     181**How to Use:**
     182
     183**Single Entry Export:**
     1841. Go to **Forms → Entries** in GravityForms
     1852. Click on any entry to view its details
     1863. Find the **Export to Markdown** widget in the right sidebar
     1874. Click **Download Markdown** to get the `.md` file
     188
     189**Bulk Export:**
     1901. Go to **Forms → Entries** in GravityForms
     1912. Select one or multiple entries using the checkboxes
     1923. Choose **Export to Markdown** from the bulk actions dropdown
     1934. Click **Apply** to download a ZIP file containing all selected entries as separate Markdown files
     194
     195**Exported Markdown Format:**
     196
     197Each Markdown file includes:
     198- Form title as the main heading
     199- Entry ID and submission timestamp
     200- All filled fields organized in a clean bullet list format
     201- Field labels in bold with their corresponding values
     202- Multi-line content properly formatted with preserved line breaks
     203- File attachments as clickable Markdown links
     204
     205**Use Cases:**
     206
     207* Document form submissions for record-keeping
     208* Share entry data with team members in a readable format
     209* Version control form submissions using Git or similar tools
     210* Import entries into knowledge bases or wikis
     211* Generate reports or documentation from form data
     212* Backup form entries in a portable, future-proof format
     213* Integrate with static site generators (Jekyll, Hugo, etc.)
     214
    108215== Settings for Clientify ==
    109216**Instructions for adding Clientify cookie in the forms**
     
    131238
    132239== Changelog ==
    133 = 4.2.1 =
     240
     241= 4.3.0 =
     242*  Added: API connection status indicators across all form integrations (GravityForms, WPForms, Elementor, Contact Form 7, WooCommerce).
     243*  Added: Visual connection status badges with color coding - green (connected), red (error), gray (not configured).
     244*  Added: Real-time connection validation with detailed error messages when authentication fails.
     245*  Added: Markdown Export feature for GravityForms entries with single and bulk export capabilities.
     246*  Added: Export entries as clean, well-structured Markdown files with full field type support.
     247*  Added: Bulk export creates ZIP file with multiple entry Markdown files for easy sharing.
     248*  Added: Automatic retry system with up to 3 attempts at 1-hour intervals, visual progress counter, and smart cancellation when entries succeed or are deleted.
     249*  Added: Error Log feature with comprehensive tracking, filtering by status/CRM, detailed error views, resend capability, and pagination for easy management.
     250*  Enhanced: Contact Form 7 module selection now auto-saves configuration with visual feedback.
     251*  Enhanced: Responsive AJAX-based interface with color-coded status badges and synchronized manual/automatic retry system.
     252*  Enhanced: Feed connection status in Forms list in Gravity Forms.
     253*  Fixed: Resend button missing in Gravity Forms Entries view.
     254*  Enhanced: Added feed selector in Resend Entry widget to choose between all feeds or individual feed.
     255*  Added date conversion in Clientify for birthday field.
    134256*  Hotfix: Error not sending correctly entry id in webhook.
    135257
    136258= 4.2.0 =
    137259*  Enhanced: New design for the settings page.
    138 Dedicated menu for FormsCRM settings.
     260Enhanced: Dedicated menu for FormsCRM settings.
    139261*  Improved: Added new tests for more consistent code coverage.
    140262*  Fixed: Fatal error in formscrm_debug_email_lead function.
Note: See TracChangeset for help on using the changeset viewer.