Development of a multi-step feedback form for 1C-Bitrix

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1183
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    813
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    657
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

Multi-Step Feedback Form Development on 1C-Bitrix

A multi-step feedback form differs from a regular step-by-step form in that its goal is not a sale, but obtaining a structured inquiry. Technical requests, complaints, support tickets, return applications — situations where you need to collect context sequentially. The inquiry type determines which fields to show next. This is conditional logic, closer to a quiz funnel, but oriented toward service rather than sales scenarios.

Architecture with Conditional Fields

The key feature: on the first step, the user selects the inquiry type — and the subsequent steps change accordingly. This requires conditional rendering.

Step configuration with conditions:

const FEEDBACK_FORM_CONFIG = {
    steps: [
        {
            id: 'step_type',
            title: 'Inquiry type',
            fields: [
                {
                    name: 'type',
                    type: 'radio-cards',
                    required: true,
                    options: [
                        {value: 'technical', label: 'Technical question', icon: '⚙️'},
                        {value: 'billing',   label: 'Billing question',   icon: '💳'},
                        {value: 'complaint', label: 'Complaint',          icon: '⚠️'},
                        {value: 'other',     label: 'Other',              icon: '💬'},
                    ],
                },
            ],
        },
        {
            id: 'step_technical',
            showWhen: {field: 'type', value: 'technical'},
            title: 'Problem description',
            fields: [
                {name: 'product', type: 'select', label: 'Product', required: true,
                 options: ['Mobile app', 'Website', 'API']},
                {name: 'error_text', type: 'textarea', label: 'Describe the error', required: true},
                {name: 'screenshot', type: 'file', label: 'Screenshot (optional)', accept: 'image/*'},
            ],
        },
        {
            id: 'step_billing',
            showWhen: {field: 'type', value: 'billing'},
            title: 'Payment details',
            fields: [
                {name: 'order_id',    type: 'text',   label: 'Order number',  required: true},
                {name: 'amount',      type: 'number', label: 'Payment amount', required: true},
                {name: 'issue_type',  type: 'select', label: 'Issue',         required: true,
                 options: ['Double charge', 'No receipt', 'Wrong amount', 'Other']},
            ],
        },
        {
            id: 'step_contact',
            title: 'Your contacts',
            fields: [
                {name: 'name',  type: 'text',  label: 'Name',  required: true},
                {name: 'email', type: 'email', label: 'Email', required: true},
                {name: 'phone', type: 'tel',   label: 'Phone', required: false},
            ],
        },
    ],
};

JavaScript Controller with Conditional Logic

class ConditionalMultistepForm {
    constructor(config) {
        this.config    = config;
        this.answers   = {};
        this.stepsPath = this.calculatePath();
        this.currentIndex = 0;
    }

    // Calculate the actual step path based on current answers
    calculatePath() {
        return this.config.steps.filter(step => {
            if (!step.showWhen) return true;
            const {field, value} = step.showWhen;
            const answer = this.answers[field];
            return Array.isArray(value) ? value.includes(answer) : answer === value;
        });
    }

    handleAnswer(fieldName, value) {
        this.answers[fieldName] = value;
        // Recalculate path — the set of steps may have changed
        this.stepsPath = this.calculatePath();
    }

    get currentStep() {
        return this.stepsPath[this.currentIndex] ?? null;
    }

    get totalSteps() {
        return this.stepsPath.length;
    }

    canGoNext() {
        const step = this.currentStep;
        if (!step) return false;

        return step.fields
            .filter(f => f.required)
            .every(f => !!this.answers[f.name]);
    }

    next() {
        if (!this.canGoNext()) return false;
        if (this.currentIndex < this.stepsPath.length - 1) {
            this.currentIndex++;
            return true;
        }
        return false; // Last step
    }

    prev() {
        if (this.currentIndex > 0) {
            this.currentIndex--;
        }
    }

    isLastStep() {
        return this.currentIndex === this.stepsPath.length - 1;
    }

    getSubmitData() {
        return {
            type:    this.answers.type,
            answers: this.answers,
        };
    }
}

File Handling (Screenshot)

File uploads in a multi-step form require either immediate upload on file selection, or sending together with the rest of the data.

Immediate upload on file selection — via AJAX, returns the ID of the uploaded file in Bitrix:

// /local/ajax/upload_temp_file.php
if (!$_FILES['file'] || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
    echo json_encode(['error' => 'Upload error']);
    exit;
}

// File type check
$allowedMime = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($_FILES['file']['type'], $allowedMime)) {
    echo json_encode(['error' => 'Invalid file type']);
    exit;
}

// Save as a Bitrix temporary file
$fileId = \CFile::SaveFile([
    'name'     => $_FILES['file']['name'],
    'size'     => $_FILES['file']['size'],
    'tmp_name' => $_FILES['file']['tmp_name'],
    'type'     => $_FILES['file']['type'],
], 'feedback_forms');

echo json_encode(['file_id' => $fileId]);

On final submission — pass file_id along with the rest of the data.

Saving and Creating a CRM Inquiry

Inquiries are routed depending on type:

// /local/ajax/feedback_form_submit.php

$data = json_decode(file_get_contents('php://input'), true);
$type = $data['type'] ?? 'other';

\Bitrix\Main\Loader::includeModule('crm');

switch ($type) {
    case 'technical':
        // Technical inquiries → deal in the "Support" pipeline
        $deal = new \CCrmDeal(false);
        $deal->Add([
            'TITLE'      => 'Tech. inquiry: ' . ($data['answers']['product'] ?? ''),
            'CATEGORY_ID' => CRM_SUPPORT_PIPELINE_ID,
            'STAGE_ID'   => 'C' . CRM_SUPPORT_PIPELINE_ID . ':NEW',
            'COMMENTS'   => $data['answers']['error_text'] ?? '',
            'CONTACT_ID' => $this->findOrCreateContact($data),
        ]);
        break;

    case 'billing':
        // Financial questions → lead with a tag
        $lead = new \CCrmLead(false);
        $lead->Add([
            'TITLE'    => 'Billing: order #' . ($data['answers']['order_id'] ?? ''),
            'COMMENTS' => 'Issue: ' . ($data['answers']['issue_type'] ?? ''),
        ]);
        break;

    default:
        // Others — lead
        $lead = new \CCrmLead(false);
        $lead->Add([
            'TITLE'   => 'Inquiry: ' . $data['answers']['name'],
            'SOURCE_ID' => 'WEB',
        ]);
}

echo json_encode(['success' => true]);

Notifications

When an inquiry is received — notify the responsible person. Using Bitrix mail events (b_event_log):

\Bitrix\Main\Mail\Event::send([
    'EVENT_NAME' => 'FEEDBACK_NEW_TICKET',
    'LID'        => SITE_ID,
    'C_FIELDS'   => [
        'TYPE'  => $typeLabel,
        'NAME'  => $data['answers']['name'],
        'EMAIL' => $data['answers']['email'],
        'TEXT'  => json_encode($data['answers'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
    ],
    'TO_EMAIL'   => FEEDBACK_MANAGER_EMAIL,
]);

The mail event template is created in Settings → Mail events → Mail templates.

Development Timeline

Option Scope Timeline
Basic multi-step 2–3 steps, inquiry type, lead in CRM 3–5 days
With conditional logic Different fields by type, file uploads 6–10 days
Full Service Desk + Personal account, inquiry statuses, SLA 15–25 days