AntonLV 6 лет назад
Родитель
Сommit
75b8b50544

+ 102 - 0
modules/boonex/feedback/classes/BxFdbConfig.php

@@ -0,0 +1,102 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Feedback Feedback
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxFdbConfig extends BxBaseModGeneralConfig
+{
+    function __construct($aModule)
+    {
+        parent::__construct($aModule);
+
+        $this->CNF = array (
+
+            // module icon
+            'ICON' => 'comment col-green3',
+
+            // database tables
+            'TABLE_QUESTIONS' => $aModule['db_prefix'] . 'questions',
+            'TABLE_ANSWERS' => $aModule['db_prefix'] . 'answers',
+            'TABLE_ANSWERS2USERS' => $aModule['db_prefix'] . 'answers2users',
+
+            // database fields
+            'FIELD_ID' => 'id',
+            'FIELD_AUTHOR' => 'author',
+            'FIELD_ADDED' => 'added',
+            'FIELD_CHANGED' => 'changed',
+            'FIELD_TEXT' => 'text',
+            'FIELD_ANSWERS' => 'answers',
+            'FIELD_STATUS_ADMIN' => 'status_admin',
+
+            'FIELD_ANS_TITLE' => 'title',
+            'FIELD_ANS_IMPORTANT' => 'important',
+
+            // page URIs
+
+            // some params
+            'PARAM_QUESTIONS' => 'bx_feedback_enable_questions',
+            'PARAM_QUESTION_LIFETIME' => 'bx_feedback_question_lifetime',
+            'PARAM_ANSWER_NOTIFY_IMPORTANT' => 'bx_feedback_enable_answer_notify_important',
+
+            // objects
+            'OBJECT_FORM_QUESTION' => 'bx_feedback_question',
+            'OBJECT_FORM_QUESTION_DISPLAY_ADD' => 'bx_feedback_question_add',
+            'OBJECT_FORM_QUESTION_DISPLAY_EDIT' => 'bx_feedback_question_edit',
+            'OBJECT_FORM_QUESTION_DISPLAY_DELETE' => 'bx_feedback_question_delete',
+            'OBJECT_FORM_QUESTION_DISPLAY_VIEW' => 'bx_feedback_question_view',
+            'OBJECT_GRID_QUESTIONS' => 'bx_feedback_questions',
+
+            // some language keys
+            'T' => array (
+                'txt_sample_question_single' => '_bx_feedback_txt_sample_question_single',
+                'txt_sample_question_single_with_article' => '_bx_feedback_txt_sample_question_single_with_article',
+                'txt_sample_answer_single' => '_bx_feedback_txt_sample_answer_single',
+                'txt_sample_answer_single_with_article' => '_bx_feedback_txt_sample_answer_single_with_article',
+                //'txt_sample_answer_action' => '_bx_feedback_txt_sample_answer_action',
+            ),
+        );
+
+        $this->_aJsClasses = array_merge($this->_aJsClasses, array(
+            'question' => 'BxFdbQuestion',
+        ));
+
+        $this->_aJsObjects = array_merge($this->_aJsObjects, array(
+            'question' => 'oBxFdbQuestion',
+        ));
+
+        $sPrefix = str_replace('_', '-', $this->_sName);
+        $this->_aHtmlIds = array(
+            'question' => $sPrefix . '-question-',
+            'question_popup_add' =>  $sPrefix . '-question-popup-add',
+            'question_popup_edit' =>  $sPrefix . '-question-popup-edit'
+        );
+
+        $this->_aPrefixes = array(
+            'style' => 'bx-fdb',
+        );
+    }
+
+    public function getHtmlIds($sKey = '')
+    {
+        if(empty($sKey))
+            return $this->_aHtmlIds;
+
+        return isset($this->_aHtmlIds[$sKey]) ? $this->_aHtmlIds[$sKey] : '';
+    }
+
+    /**
+     * Notify about important Answers only.
+     */
+    public function isModeAni()
+    {
+        return getParam($this->CNF['PARAM_ANSWER_NOTIFY_IMPORTANT']) == 'on';
+    }
+}
+
+/** @} */

+ 281 - 0
modules/boonex/feedback/classes/BxFdbDb.php

@@ -0,0 +1,281 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Feedback Feedback
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/*
+ * Module database queries
+ */
+class BxFdbDb extends BxBaseModGeneralDb
+{
+    function __construct(&$oConfig)
+    {
+        parent::__construct($oConfig);
+    }
+
+    public function getQuestions($aParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+    	$aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+        $sWhereClause = $sOrderByClause = $sLimitClause = "";
+
+        $sSelectClause = "`tq`.*";
+        switch($aParams['type']) {
+            case 'id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                    'id' => $aParams['id']
+                );
+
+                $sWhereClause .= " AND `tq`.`id`=:id";
+                break;
+            
+            case 'actual':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                    'status' => 'active'
+                );
+                
+                $sWhereClause .= " AND `tq`.`status_admin`=:status";
+                $sOrderByClause = "`tq`.`added` DESC";
+                break;
+
+            case 'all':
+            	break;
+        }
+
+        if(!empty($aParams['order_by']))
+            $sOrderByClause = '`' . $aParams['order_by'] . '` ' . (isset($aParams['order_way']) ? strtoupper($aParams['order_way']) : 'ASC');
+
+        $sOrderByClause = !empty($sOrderByClause) ? " ORDER BY " . $sOrderByClause : $sOrderByClause;
+        $sLimitClause = !empty($sLimitClause) ? " LIMIT " . $sLimitClause : $sLimitClause;
+        
+        $aMethod['params'][0] = "SELECT " . $sSelectClause . " FROM `" . $CNF['TABLE_QUESTIONS'] . "` AS `tq` WHERE 1 " . $sWhereClause . $sOrderByClause;
+
+        return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+    }
+
+    public function insertQuestion($aParamsSet)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($aParamsSet))
+            return false;
+
+        $sSql = "INSERT INTO `" . $CNF['TABLE_QUESTIONS'] . "` SET " . $this->arrayToSQL($aParamsSet);
+        return $this->query($sSql);
+    }
+
+    public function updateQuestion($aParamsSet, $aParamsWhere)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($aParamsSet) || empty($aParamsWhere))
+            return false;
+
+        $sSql = "UPDATE `" . $CNF['TABLE_QUESTIONS'] . "` SET " . $this->arrayToSQL($aParamsSet) . " WHERE " . $this->arrayToSQL($aParamsWhere, " AND ");
+        return $this->query($sSql);
+    }
+
+    public function deleteQuestion($aParams)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $sSql = "DELETE FROM `" . $CNF['TABLE_QUESTIONS'] . "` WHERE " . $this->arrayToSQL($aParams, " AND ");
+        return $this->query($sSql);
+    }
+
+    public function getAnswers($aParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+    	$aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+
+        $sSelectClause = "`ta`.*";
+        $sJoinClause = $sWhereClause = $sOrderByClause = "";
+
+        switch($aParams['type']) {
+            case 'id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                    'id' => $aParams['id']
+                );
+
+                $sWhereClause .= " AND `ta`.`id`=:id";
+                break;
+
+            case 'id_for_profile':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                    'id' => $aParams['id'],
+                    'profile_id' => $aParams['profile_id'],
+                );
+
+                $sSelectClause .= ", `tau`.`text` AS `text`, `tau`.`added` AS `added`";
+                $sJoinClause = " INNER JOIN `" . $CNF['TABLE_ANSWERS2USERS'] . "` AS `tau` ON `ta`.`id`=`tau`.`answer_id` AND `tau`.`profile_id`=:profile_id";
+                $sWhereClause .= " AND `ta`.`id`=:id";
+                break;
+
+            case 'ids':
+                $aMethod['name'] = 'getAllWithKey';
+                $aMethod['params'][1] = 'id';
+                
+                $sWhereClause .= " AND `ta`.`id` IN (" . $this->implode_escape($aParams['ids']) . ")";
+                break;
+
+            case 'question_id_pairs':
+                $aMethod['name'] = 'getPairs';
+                $aMethod['params'][1] = 'id';
+                $aMethod['params'][2] = 'title';
+                $aMethod['params'][3] = array(
+                    'question_id' => $aParams['question_id']
+                );
+
+                $sWhereClause .= " AND `ta`.`question_id`=:question_id";
+                break;
+
+            case 'question_id':
+                $aMethod['params'][1] = array(
+                    'question_id' => $aParams['question_id']
+                );
+
+                $sWhereClause .= " AND `ta`.`question_id`=:question_id";
+                break;
+
+            case 'question_id_for_profile':
+                $aMethod['params'][1] = array(
+                    'question_id' => $aParams['question_id'],
+                    'profile_id' => $aParams['profile_id'],
+                );
+
+                $sSelectClause .= ", `tau`.`added` AS `checked`";
+                $sJoinClause = " LEFT JOIN `" . $CNF['TABLE_ANSWERS2USERS'] . "` AS `tau` ON `ta`.`id`=`tau`.`answer_id` AND `tau`.`profile_id`=:profile_id";
+                $sWhereClause .= " AND `ta`.`question_id`=:question_id";
+                break;
+
+            case 'question_id_max_order':
+                $aMethod['name'] = 'getOne';
+                $aMethod['params'][1] = array(
+                    'question_id' => $aParams['question_id']
+                );
+
+                $sSelectClause = "MAX(`ta`.`order`)";
+                $sWhereClause .= " AND `ta`.`question_id`=:question_id";
+                break;
+
+            case 'answered':
+                $aMethod['name'] = 'getOne';
+                $aMethod['params'][1] = array(
+                    'question_id' => $aParams['question_id'],
+                    'profile_id' => $aParams['profile_id'],
+                );
+
+                $sSelectClause = "`tau`.`answer_id`";
+                $sJoinClause = " INNER JOIN `" . $CNF['TABLE_ANSWERS2USERS'] . "` AS `tau` ON `ta`.`id`=`tau`.`answer_id` AND `tau`.`profile_id`=:profile_id";
+                $sWhereClause .= " AND `ta`.`question_id`=:question_id";
+                break;
+
+            case 'all':
+            	break;
+        }
+
+        $sOrderByClause = " ORDER BY " . (isset($aParams['order_by']) ? '`' . $aParams['order_by'] . '`' : '`ta`.`order`');
+        $sOrderByClause .= " " . (isset($aParams['order_way']) ? strtoupper($aParams['order_way']) : 'ASC');
+
+        $aMethod['params'][0] = "SELECT " . $sSelectClause . " FROM `" . $CNF['TABLE_ANSWERS'] . "` AS `ta`" . $sJoinClause . " WHERE 1 " . $sWhereClause . $sOrderByClause;
+
+        return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+    }
+
+    public function insertAnswer($aParamsSet)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($aParamsSet))
+            return false;
+
+        $sSql = "INSERT INTO `" . $CNF['TABLE_ANSWERS'] . "` SET " . $this->arrayToSQL($aParamsSet);
+        return $this->query($sSql);
+    }
+
+    public function updateAnswer($aParamsSet, $aParamsWhere)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($aParamsSet) || empty($aParamsWhere))
+            return false;
+
+        $sSql = "UPDATE `" . $CNF['TABLE_ANSWERS'] . "` SET " . $this->arrayToSQL($aParamsSet) . " WHERE " . $this->arrayToSQL($aParamsWhere, " AND ");
+        return $this->query($sSql);
+    }
+
+    public function deleteAnswer($aParams)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $sSql = "DELETE FROM `" . $CNF['TABLE_ANSWERS'] . "` WHERE " . $this->arrayToSQL($aParams, " AND ");
+        return $this->query($sSql);
+    }
+
+    public function deleteAnswerById($mixedId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!is_array($mixedId))
+            $mixedId = array($mixedId);
+
+        $sSql = "DELETE FROM `" . $CNF['TABLE_ANSWERS'] . "` WHERE `id` IN (" . $this->implode_escape($mixedId) . ")";
+        return $this->query($sSql);
+    }
+
+    public function isAnswer($iQuestionId, $iProfileId)
+    {
+        $iAnswer = (int)$this->getAnswers(array(
+            'type' => 'answered', 
+            'question_id' => $iQuestionId, 
+            'profile_id' => $iProfileId
+        ));
+
+        return $iAnswer > 0 ? $iAnswer : false;
+    }
+
+    public function doAnswer($iAnswerId, $iProfileId, $sText = '')
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $bResult = (int)$this->query("INSERT INTO `" . $CNF['TABLE_ANSWERS2USERS'] . "` SET `answer_id`=:answer_id, `profile_id`=:profile_id, `text`=:text, `added`=UNIX_TIMESTAMP()", array(
+            'answer_id' => $iAnswerId,
+            'profile_id' => $iProfileId,
+            'text' => $sText
+        )) > 0;
+
+        if($bResult)
+            $this->query("UPDATE `" . $CNF['TABLE_ANSWERS'] . "` SET `votes`=`votes`+1 WHERE `id`=:id", array('id' => $iAnswerId));
+
+        return $bResult;
+    }
+
+    public function undoAnswer($iAnswerId, $iProfileId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $bResult = (int)$this->query("DELETE FROM `" . $CNF['TABLE_ANSWERS2USERS'] . "` WHERE `answer_id`=:answer_id AND `profile_id`=:profile_id", array(
+            'answer_id' => $iAnswerId,
+            'profile_id' => $iProfileId
+        )) > 0;
+        
+        if($bResult)
+            $this->query("UPDATE `" . $CNF['TABLE_ANSWERS'] . "` SET `votes`=`votes`-1 WHERE `id`=:id", array('id' => $iAnswerId));
+
+        return $bResult;
+    }
+}
+
+/** @} */

+ 266 - 0
modules/boonex/feedback/classes/BxFdbFormQuestion.php

@@ -0,0 +1,266 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Feedback Feedback
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxFdbFormQuestionCheckerHelper extends BxDolFormCheckerHelper
+{
+    static public function checkAvailAnswers ($s)
+    {
+        $iCountMin = 2;
+
+        if(self::_isEmptyArray($s) || count($s) < $iCountMin)
+            return false;
+
+        $iCount = 0;
+        foreach($s as $k => $v)
+            if(!empty($v))
+                $iCount += 1;
+
+        return $iCount >= $iCountMin;
+    }
+}
+
+class BxFdbFormQuestion extends BxBaseModTextFormEntry
+{
+    protected $_sJsObject;
+
+    public function __construct($aInfo, $oTemplate = false)
+    {
+        $this->MODULE = 'bx_feedback';
+
+        parent::__construct($aInfo, $oTemplate);
+
+        $this->_sJsObject = $this->_oModule->_oConfig->getJsObject('question');
+    }
+
+    function initChecker ($aValues = array (), $aSpecificValues = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(isset($this->aInputs[$CNF['FIELD_ANSWERS']]) && !empty($aValues['id'])) {
+            $aAnswers = $this->_oModule->_oDb->getAnswers(array(
+                'type' => 'question_id',
+                'question_id' => $aValues['id']
+            ));
+
+            $this->aInputs[$CNF['FIELD_ANSWERS']] = array_merge($this->aInputs[$CNF['FIELD_ANSWERS']], array(
+                'value' => array(),
+                'value_ids' => array(),
+                'value_imps' => array()
+            ));
+
+            foreach($aAnswers as $aAnswer) {
+                $this->aInputs[$CNF['FIELD_ANSWERS']]['value'][] = $aAnswer['title'];
+                $this->aInputs[$CNF['FIELD_ANSWERS']]['value_ids'][] = $aAnswer['id'];
+                if((int)$aAnswer['important'] == 1)
+                    $this->aInputs[$CNF['FIELD_ANSWERS']]['value_imps'][] = $aAnswer['title'];
+            }
+        }
+
+        return parent::initChecker($aValues, $aSpecificValues);
+    }
+
+    public function insert ($aValsToAdd = array(), $isIgnore = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $iContentId = parent::insert($aValsToAdd, $isIgnore);
+        if(!empty($iContentId))
+            $this->processAnswersAdd($CNF['FIELD_ANSWERS'], $iContentId);
+
+        return $iContentId;
+    }
+
+    public function update ($iContentId, $aValsToAdd = array(), &$aTrackTextFieldsChanges = null)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $iResult = parent::update($iContentId, $aValsToAdd, $aTrackTextFieldsChanges);
+
+        $this->processAnswersUpdate($CNF['FIELD_ANSWERS'], $iContentId);
+
+        return $iResult;
+    }
+
+    public function processAnswersAdd ($sField, $iContentId = 0)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(!isset($this->aInputs[$sField]))
+            return true;
+
+        $aAnswers = $this->getCleanValue($sField);
+        if(empty($aAnswers) || !is_array($aAnswers))
+            return true;
+
+        $aAnswersImps = $this->getCleanValue($sField . '_imps');
+        $bAnswersImps = !empty($aAnswersImps) && is_array($aAnswersImps);
+
+        foreach($aAnswers as $iIndex => $sAnswer)
+            if($sAnswer != '' && get_mb_len($sAnswer) > 0)
+                $this->_oModule->_oDb->insertAnswer(array(
+                    'question_id' => $iContentId,
+                    'title' => bx_process_input($sAnswer),
+                    'important' => $bAnswersImps && in_array($sAnswer, $aAnswersImps) ? 1 : 0,
+                    'order' => $iIndex
+                ));
+
+        return true;
+    }
+
+    public function processAnswersUpdate($sField, $iContentId = 0)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(!isset($this->aInputs[$sField]))
+            return true;
+
+        $aAnswersValues = $this->getCleanValue($sField);
+
+        $aAnswersIds = $this->getCleanValue($sField . '_ids');
+        if(empty($aAnswersIds) || !is_array($aAnswersIds))
+            $aAnswersIds = array();
+
+        $aAnswersImps = $this->getCleanValue($sField . '_imps');
+        if(empty($aAnswersImps) || !is_array($aAnswersImps))
+            $aAnswersImps = array();
+
+        //--- Remove deleted
+        $aAnswersDb = $this->_oModule->_oDb->getAnswers(array('type' => 'question_id_pairs', 'question_id' => $iContentId));
+        $this->_oModule->_oDb->deleteAnswerById(array_diff(array_keys($aAnswersDb), $aAnswersIds));
+
+        //--- Update existed and remove empty
+        foreach($aAnswersIds as $iIndex => $iAnswerId) {
+            $sAnswersValue = $aAnswersValues[$iIndex];
+
+            if($sAnswersValue != '' && get_mb_len($sAnswersValue) > 0)
+                $this->_oModule->_oDb->updateAnswer(array(
+                    'title' => bx_process_input($sAnswersValue), 
+                    'important' => in_array($sAnswersValue, $aAnswersImps) ? 1 : 0
+                ), array('id' => (int)$iAnswerId));
+            else 
+                $this->_oModule->_oDb->deleteAnswer(array('id' => (int)$iAnswerId));
+        }
+
+        //--- Add new
+        $iAnswersIds = count($aAnswersIds);
+        $iAnswersValues = count($aAnswersValues);
+        if($iAnswersValues > $iAnswersIds) {
+            $iMaxOrder = (int)$this->_oModule->_oDb->getAnswers(array('type' => 'question_id_max_order', 'question_id' => $iContentId));
+
+            $aAnswersValues = array_slice($aAnswersValues, $iAnswersIds);
+            foreach($aAnswersValues as $sAnswersValue)
+                if($sAnswersValue != '' && get_mb_len($sAnswersValue) > 0)
+                    $this->_oModule->_oDb->insertAnswer(array(
+                        'question_id' => $iContentId,
+                        'title' => bx_process_input($sAnswersValue),
+                        'important' => in_array($sAnswersValue, $aAnswersImps) ? 1 : 0,
+                        'order' => ++$iMaxOrder
+                    ));
+        }
+
+        return true;
+    }
+
+    protected function genCustomInputAnswers(&$aInput)
+    {
+        if(!empty($aInput['value']) && is_array($aInput['value'])) {
+            $aAnswersImp = array();
+            if(!empty($aInput['value_imps']) && is_array($aInput['value_imps']))
+                $aAnswersImp = $aInput['value_imps'];
+
+            $aTmplVarsAnswers = array();
+            foreach($aInput['value'] as $iIndex => $sValue) {
+                $sInputText = $this->genCustomInputAnswersText($aInput, $sValue);
+                if(!empty($aInput['value_ids'][$iIndex]))
+                    $sInputText .= $this->genCustomInputAnswersHidden($aInput, (int)$aInput['value_ids'][$iIndex]);
+
+                $sInputCheckbox = $this->genCustomInputAnswersCheckbox($aInput, $sValue, in_array($sValue, $aAnswersImp));
+
+                $aTmplVarsAnswers[] = array(
+                    'js_object' => $this->_sJsObject, 
+                    'input_text' => $sInputText,
+                    'input_checkbox' => $sInputCheckbox
+                );
+            }
+        }
+        else {
+            $aAnswer = array(
+                'js_object' => $this->_sJsObject, 
+                'input_text' => $this->genCustomInputAnswersText($aInput),
+                'input_checkbox' => $this->genCustomInputAnswersCheckbox($aInput)
+            );
+
+            $aTmplVarsAnswers = array($aAnswer, $aAnswer);
+        }
+
+        return $this->_oModule->_oTemplate->parseHtmlByName('form_answers.html', array(
+            'bx_repeat:answers' => $aTmplVarsAnswers,
+            'btn_add' => $this->genCustomInputAnswersButton($aInput)
+        ));
+    }
+
+    protected function genCustomInputAnswersText($aInput, $mixedValue = '')
+    {
+        $aInput['type'] = 'text';
+        $aInput['name'] .= '[]';
+        $aInput['value'] = $mixedValue;
+        
+        if(!is_array($aInput['attrs']))
+            $aInput['attrs'] = array();
+
+        $sJsMethod = 'javascript:' . $this->_sJsObject . '.answerOnType(event)';
+        $aInput['attrs'] = array_merge($aInput['attrs'], array(
+            'class' => 'bx-def-margin-sec-top-auto',
+            'onkeyup' => $sJsMethod,
+            'onpaste' => $sJsMethod,
+            'onblur' => $sJsMethod
+        ));
+
+        return $this->genInput($aInput);
+    }
+
+    protected function genCustomInputAnswersCheckbox($aInput, $mixedValue = '', $bChecked = false)
+    {
+        $aInput['type'] = 'checkbox';
+        $aInput['name'] .= '_imps[]';
+        $aInput['caption'] = _t('_bx_feedback_form_question_input_answers_imp');
+        $aInput['value'] = $mixedValue;
+        $aInput['required'] = 0;
+        if($bChecked)
+            $aInput['checked'] = 'checked';
+
+        return $this->genRow($aInput);
+    }
+
+    protected function genCustomInputAnswersHidden($aInput, $mixedValue = '')
+    {
+        $aInput['type'] = 'hidden';
+        $aInput['name'] .= '_ids[]';
+        $aInput['value'] = $mixedValue;
+
+        return $this->genInput($aInput);
+    }
+
+    protected function genCustomInputAnswersButton($aInput)
+    {
+        $sName = $aInput['name'];
+
+        $aInput['type'] = 'button';
+        $aInput['name'] .= '_add';
+        $aInput['value'] = _t('_bx_feedback_form_question_input_answers_add');
+        $aInput['attrs']['class'] = 'bx-def-margin-sec-top';
+        $aInput['attrs']['onclick'] = $this->_oModule->_oConfig->getJsObject('question') . ".answerAdd(this, '" . $sName . "');";
+
+        return $this->genInputButton($aInput);
+    }
+}
+
+/** @} */

+ 166 - 0
modules/boonex/feedback/classes/BxFdbGridQuestions.php

@@ -0,0 +1,166 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Feedback Feedback
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxFdbGridQuestions extends BxTemplGrid
+{
+    protected $_sModule;
+    protected $_oModule;
+
+    protected $_sParamsDivider;
+
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        parent::__construct ($aOptions, $oTemplate);
+
+        $this->_sModule = 'bx_feedback';
+        $this->_oModule = BxDolModule::getInstance($this->_sModule);
+
+        $this->_sParamsDivider = '#-#';
+    }
+
+    public function getJsCode($sKey = 'question')
+    {
+        return $this->_oModule->_oTemplate->getJsCode($sKey, array(
+            'sObjNameGrid' => $this->_sObject,
+            'sParamsDivider' => $this->_sParamsDivider,
+            'sTextSearchInput' => _t('_sys_grid_search')
+        ));
+    }
+
+    public function getCode ($isDisplayHeader = true)
+    {
+        $sResult = parent::getCode($isDisplayHeader);
+        if(!empty($sResult))
+            $sResult .= $this->getJsCode();
+
+        return $sResult;
+    }
+
+    public function performActionAdd()
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sAction = 'add';
+        $aActionParams = array('o' => $this->_sObject, 'a' => $sAction);
+
+        $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_QUESTION'], $CNF['OBJECT_FORM_QUESTION_DISPLAY_ADD']);
+        $oForm->aFormAttrs['action'] = BX_DOL_URL_ROOT . bx_append_url_params('grid.php', $aActionParams);
+        $oForm->initChecker();
+
+        if($oForm->isSubmittedAndValid()) {
+            if(($iId = $oForm->insert()) !== false) {
+                $this->_oModule->onAddQuestion($iId);
+
+                $aRes = array('grid' => $this->getCode(false), 'blink' => $iId);
+            }
+            else
+                $aRes = array('msg' => _t('_bx_feedback_err_cannot_perform'));
+
+            return echoJson($aRes);
+        }
+
+        $sContent = BxTemplFunctions::getInstance()->popupBox($this->_oModule->_oConfig->getHtmlIds('question_popup_add'), _t('_bx_feedback_grid_question_popup_title_add'), $this->_oModule->_oTemplate->parseHtmlByName('question_popup_form.html', array(
+            'form_id' => $oForm->getId(),
+            'form' => $oForm->getCode(true),
+            'object' => $this->_sObject,
+            'action' => $sAction
+        )));
+
+        echoJson(array('popup' => array('html' => $sContent, 'options' => array('closeOnOuterClick' => false))));
+    }
+
+    public function performActionEdit()
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $aIds = bx_get('ids');
+        if(!$aIds || !is_array($aIds)) {
+            $iId = (int)bx_get('id');
+            if(!$iId)
+                return echoJson(array());
+
+            $aIds = array($iId);
+        }
+
+        $iId = $aIds[0];
+
+        $sAction = 'edit';
+        $aActionParams = array('o' => $this->_sObject, 'a' => $sAction, 'id' => $iId);
+
+        $aQuestion = $this->_oModule->_oDb->getQuestions(array('type' => 'id', 'id' => $iId));
+
+        $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_QUESTION'], $CNF['OBJECT_FORM_QUESTION_DISPLAY_EDIT']);
+        $oForm->aFormAttrs['action'] = BX_DOL_URL_ROOT . bx_append_url_params('grid.php', $aActionParams);
+        $oForm->initChecker($aQuestion);
+
+        if($oForm->isSubmittedAndValid()) {
+            if($oForm->update($iId) !== false) {
+                $this->_oModule->onEditQuestion($iId);
+
+                $aRes = array('grid' => $this->getCode(false), 'blink' => $iId);
+            }
+            else
+                $aRes = array('msg' => _t('_bx_feedback_err_cannot_perform'));
+
+            return echoJson($aRes);
+        }
+
+        $sContent = BxTemplFunctions::getInstance()->popupBox($this->_oModule->_oConfig->getHtmlIds('question_popup_edit'), _t('_bx_feedback_grid_question_popup_title_edit'), $this->_oModule->_oTemplate->parseHtmlByName('question_popup_form.html', array(
+            'form_id' => $oForm->getId(),
+            'form' => $oForm->getCode(true),
+            'object' => $this->_sObject,
+            'action' => $sAction
+        )));
+
+        echoJson(array('popup' => array('html' => $sContent, 'options' => array('closeOnOuterClick' => false))));
+    }
+
+    protected function _delete($mixedId)
+    {
+        $mixedResult = parent::_delete($mixedId);
+        if($mixedResult !== false)
+            $this->_oModule->onDeleteQuestion($mixedId);
+
+        return $mixedResult;
+    }
+
+    protected function _switcherChecked2State($isChecked)
+    {
+        return $isChecked ? 'active' : 'hidden';
+    }
+
+    protected function _switcherState2Checked($mixedState)
+    {
+        return 'active' == $mixedState ? true : false;
+    }
+
+    protected function _getCellSwitcher ($mixedValue, $sKey, $aField, $aRow)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(isset($CNF['FIELD_STATUS_ADMIN']) && isset($aRow[$CNF['FIELD_STATUS_ADMIN']]) && !in_array($aRow[$CNF['FIELD_STATUS_ADMIN']], array('active', 'hidden'))) {
+            $sStatusKey = '_sys_status_' . $aRow[$CNF['FIELD_STATUS_ADMIN']];
+            if(!empty($CNF['T']['txt_status_' . $aRow[$CNF['FIELD_STATUS_ADMIN']]]))
+                $sStatusKey = $CNF['T']['txt_status_' . $aRow[$CNF['FIELD_STATUS_ADMIN']]];
+
+            return parent::_getCellDefault(_t($sStatusKey), $sKey, $aField, $aRow);
+        }
+
+        return parent::_getCellSwitcher ($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellAdded($mixedValue, $sKey, $aField, $aRow)
+    {
+        return parent::_getCellDefault(bx_time_js($mixedValue, BX_FORMAT_DATE, true), $sKey, $aField, $aRow);
+    }
+}
+
+/** @} */

+ 415 - 0
modules/boonex/feedback/classes/BxFdbModule.php

@@ -0,0 +1,415 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Feedback Feedback
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxFdbModule extends BxBaseModGeneralModule
+{
+    function __construct(&$aModule)
+    {
+        parent::__construct($aModule);
+    }
+
+    public function actionAnswer()
+    {
+        $iProfileId = bx_get_logged_profile_id();
+        if(!$iProfileId)
+            return echoJson(array('code' => 1, 'msg' => _t('_bx_feedback_err_login_required')));
+
+        $iQuestionId = (int)bx_get('question_id');
+        $iAnswerId = (int)bx_get('answer_id');
+        if(empty($iQuestionId) || empty($iAnswerId))
+            return echoJson(array('code' => 2, 'msg' => _t('_bx_feedback_err_wrong_data')));
+
+        if(($iAnswerIdOld = $this->_oDb->isAnswer($iQuestionId, $iProfileId)) !== false)
+            if($this->_oDb->undoAnswer($iAnswerIdOld, $iProfileId)) 
+                $this->onDeleteAnswer($iQuestionId, $iAnswerIdOld, $iProfileId);
+
+        $sText = bx_process_input(bx_get('text'));
+        if(!$this->_oDb->doAnswer($iAnswerId, $iProfileId, $sText))
+            return echoJson(array('code' => 3, 'msg' => _t('_bx_feedback_err_cannot_perform')));
+
+        $this->onAddAnswer($iQuestionId, $iAnswerId, $iProfileId, array(
+            'text' => $sText
+        ));
+
+        return echoJson(array(
+            'code' => 0
+        ));
+    }
+
+    public function serviceGetBlockQuestion()
+    {
+        $iProfileId = bx_get_logged_profile_id();
+        if(!$iProfileId)
+            return '';
+
+        $aQuestion = $this->_oDb->getQuestions(array('type' => 'actual'));
+        if(empty($aQuestion) || !is_array($aQuestion))
+            return '';
+
+        $aAnswers = $this->_oDb->getAnswers(array('type' => 'question_id_for_profile', 'question_id' => $aQuestion['id'], 'profile_id' => $iProfileId));
+        if(empty($aAnswers) || !is_array($aAnswers))
+            return '';
+
+        return $this->_oTemplate->getBlockQuestion($aQuestion, $aAnswers);
+    }
+
+
+    /**
+     * Integration methods.
+     * 
+     * Integration: Timeline.
+     */
+    public function serviceGetTimelineData()
+    {
+    	$sModule = $this->getName();
+
+        return array(
+            'handlers' => array(
+                array('group' => $sModule . '_question', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'added_question', 'module_name' => $sModule, 'module_method' => 'get_timeline_question', 'module_class' => 'Module',  'groupable' => 0, 'group_by' => ''),
+                array('group' => $sModule . '_question', 'type' => 'update', 'alert_unit' => $sModule, 'alert_action' => 'edited_question'),
+                array('group' => $sModule . '_question', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'deleted_question'),
+                
+                array('group' => $sModule . '_answer', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'added_answer', 'module_name' => $sModule, 'module_method' => 'get_timeline_answer', 'module_class' => 'Module',  'groupable' => 0, 'group_by' => ''),
+                array('group' => $sModule . '_answer', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'deleted_answer')
+            ),
+            'alerts' => array(
+                array('unit' => $sModule, 'action' => 'added_question'),
+                array('unit' => $sModule, 'action' => 'edited_question'),
+                array('unit' => $sModule, 'action' => 'deleted_question'),
+
+                array('unit' => $sModule, 'action' => 'added_answer'),
+                array('unit' => $sModule, 'action' => 'deleted_answer'),
+            )
+        );
+    }
+
+    public function serviceGetTimelineQuestion($aEvent, $aBrowseParams = array())
+    {
+        $aQuestion = $this->_oDb->getQuestions(array('type' => 'id', 'id' => $aEvent['object_id']));
+        if(empty($aQuestion) || !is_array($aQuestion))
+            return false;
+
+        $CNF = &$this->_oConfig->CNF;
+
+        if(($aQuestion[$CNF['FIELD_STATUS_ADMIN']] != 'active'))
+            return false;
+
+        $iUserId = $this->getUserId();
+        $iAuthorId = (int)$aQuestion[$CNF['FIELD_AUTHOR']];
+        $iAuthorIdAbs = abs($iAuthorId);
+        if($iAuthorId < 0 && ((is_numeric($aEvent['owner_id']) && $iAuthorIdAbs == (int)$aEvent['owner_id']) || (is_array($aEvent['owner_id']) && in_array($iAuthorIdAbs, $aEvent['owner_id']))) && $iAuthorIdAbs != $iUserId)
+            return false;
+
+        //--- Title & Description
+        $sTitle = !empty($aQuestion[$CNF['FIELD_TEXT']]) ? $aQuestion[$CNF['FIELD_TEXT']] : '';
+
+        $iOwnerId = $iAuthorIdAbs;
+        if(isset($aEvent['object_privacy_view']) && (int)$aEvent['object_privacy_view'] < 0)
+            $iOwnerId = abs($aEvent['object_privacy_view']);
+
+        $bCache = true;
+        $aContent = $this->_getContentForTimelineQuestion($aEvent, $aQuestion, $aBrowseParams);
+        if(isset($aContent['_cache'])) {
+            $bCache = (bool)$aContent['_cache'];
+
+            unset($aContent['_cache']);
+        }
+
+        return array(
+            '_cache' => $bCache,
+            'owner_id' => $iOwnerId,
+            'object_owner_id' => $iOwnerId,
+            'icon' => !empty($CNF['ICON']) ? $CNF['ICON'] : '',
+            'sample' => isset($CNF['T']['txt_sample_question_single_with_article']) ? $CNF['T']['txt_sample_question_single_with_article'] : $CNF['T']['txt_sample_single'],
+            'sample_wo_article' => $CNF['T']['txt_sample_question_single'],
+            'sample_action' => isset($CNF['T']['txt_sample_question_action']) ? $CNF['T']['txt_sample_question_action'] : '',
+            'url' => BX_DOL_URL_ROOT,
+            'content' => $aContent, //a string to display or array to parse default template before displaying.
+            'allowed_view' => array('module' => $this->getName(), 'method' => 'get_timeline_question_allowed_view'),
+            'date' => $aQuestion[$CNF['FIELD_ADDED']],
+            'title' => $sTitle, //may be empty.
+            'description' => '' //may be empty.
+        );
+    }
+
+    public function serviceGetTimelineQuestionAllowedView($aEvent)
+    {
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    public function serviceGetTimelineAnswer($aEvent, $aBrowseParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $bModeAni = $this->_oConfig->isModeAni();
+        $iOwnerId = (int)$aEvent['owner_id'];
+
+        $aAnswer = $this->_oDb->getAnswers(array('type' => 'id_for_profile', 'id' => $aEvent['object_id'], 'profile_id' => $iOwnerId));
+        if(empty($aAnswer) || !is_array($aAnswer) || ($bModeAni && (int)$aAnswer[$CNF['FIELD_ANS_IMPORTANT']] == 0))
+            return false;
+
+        $aQuestion = $this->_oDb->getQuestions(array('type' => 'id', 'id' => $aAnswer['question_id']));
+        if(empty($aQuestion) || !is_array($aQuestion))
+            return false;
+
+        if(($aQuestion[$CNF['FIELD_STATUS_ADMIN']] != 'active'))
+            return false;
+
+        //--- Title & Description
+        $sTitle = !empty($aAnswer[$CNF['FIELD_TEXT']]) ? $aAnswer[$CNF['FIELD_TEXT']] : '';
+
+        $bCache = true;
+        $aContent = $this->_getContentForTimelineAnswer($aEvent, $aQuestion, $aAnswer, $aBrowseParams);
+        if(isset($aContent['_cache'])) {
+            $bCache = (bool)$aContent['_cache'];
+
+            unset($aContent['_cache']);
+        }
+
+        return array(
+            '_cache' => $bCache,
+            'owner_id' => $iOwnerId,
+            'object_owner_id' => $iOwnerId,
+            'icon' => !empty($CNF['ICON']) ? $CNF['ICON'] : '',
+            'sample' => isset($CNF['T']['txt_sample_answer_single_with_article']) ? $CNF['T']['txt_sample_answer_single_with_article'] : $CNF['T']['txt_sample_single'],
+            'sample_wo_article' => $CNF['T']['txt_sample_answer_single'],
+            'sample_action' => isset($CNF['T']['txt_sample_answer_action']) ? $CNF['T']['txt_sample_answer_action'] : '',
+            'url' => BX_DOL_URL_ROOT,
+            'content' => $aContent, //a string to display or array to parse default template before displaying.
+            'allowed_view' => array('module' => $this->getName(), 'method' => 'get_timeline_answer_allowed_view'),
+            'date' => $aAnswer[$CNF['FIELD_ADDED']],
+            'title' => $sTitle, //may be empty.
+            'description' => '' //may be empty.
+        );
+    }
+
+    public function serviceGetTimelineAnswerAllowedView($aEvent)
+    {
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * Integration: Notifications.
+     */
+    public function serviceGetNotificationsData()
+    {
+    	$sModule = $this->getName();
+
+        $aResult = array(
+            'handlers' => array(
+                array('group' => $sModule . '_question', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'added_question', 'module_name' => $sModule, 'module_method' => 'get_notifications_question', 'module_class' => 'Module', 'module_event_privacy' => ''),
+                array('group' => $sModule . '_question', 'type' => 'update', 'alert_unit' => $sModule, 'alert_action' => 'edited_question'),
+                array('group' => $sModule . '_question', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'deleted_question'),
+
+                array('group' => $sModule . '_answer', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'added_answer_notif', 'module_name' => $sModule, 'module_method' => 'get_notifications_answer', 'module_class' => 'Module', 'module_event_privacy' => ''),
+                array('group' => $sModule . '_answer', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'deleted_answer_notif'),
+            ),
+            'settings' => array(
+                array('group' => 'content', 'unit' => $sModule, 'action' => 'added_question', 'types' => array('follow_member')),
+                array('group' => 'content', 'unit' => $sModule, 'action' => 'added_answer_notif', 'types' => array('follow_member'))
+            ),
+            'alerts' => array(
+                array('unit' => $sModule, 'action' => 'added_question'),
+                array('unit' => $sModule, 'action' => 'edited_question'),
+                array('unit' => $sModule, 'action' => 'deleted_question'),
+
+                array('unit' => $sModule, 'action' => 'added_answer_notif'),
+                array('unit' => $sModule, 'action' => 'deleted_answer_notif')
+            )
+        );
+
+        return $aResult;
+    }
+
+    public function serviceGetNotificationsQuestion($aEvent)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aQuestion = $this->_oDb->getQuestions(array('type' => 'id', 'id' => $aEvent['object_id']));
+        if(empty($aQuestion) || !is_array($aQuestion) || $aQuestion[$CNF['FIELD_STATUS_ADMIN']] != 'active')
+            return array();
+
+        $sEntryUrl = BX_DOL_URL_ROOT;
+        $sEntryCaption = strmaxtextlen($aQuestion[$CNF['FIELD_TEXT']], 20, '...');
+
+        return array(
+            'entry_sample' => $CNF['T']['txt_sample_question_single'],
+            'entry_url' => $sEntryUrl,
+            'entry_caption' => $sEntryCaption,
+            'entry_author' => $aQuestion[$CNF['FIELD_AUTHOR']]
+        );
+    }
+
+    public function serviceGetNotificationsAnswer($aEvent)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $iOwnerId = (int)$aEvent['owner_id'];
+        $oOwnerProfile = BxDolProfile::getInstance($iOwnerId);
+        if(!$oOwnerProfile)
+            return array();
+
+        $aQuestion = $this->_oDb->getQuestions(array('type' => 'id', 'id' => $aEvent['object_id']));
+        if(empty($aQuestion) || !is_array($aQuestion) || $aQuestion[$CNF['FIELD_STATUS_ADMIN']] != 'active')
+            return array();
+
+        $aAnswer = $this->_oDb->getAnswers(array('type' => 'id_for_profile', 'id' => $aEvent['subobject_id'], 'profile_id' => $iOwnerId));
+        if(empty($aAnswer) || !is_array($aAnswer))
+            return array();
+
+        $sEntryUrl = BX_DOL_URL_ROOT;
+        $sEntryCaption = strmaxtextlen($aQuestion[$CNF['FIELD_TEXT']], 20, '...');
+
+        return array(
+            'entry_sample' => $CNF['T']['txt_sample_question_single'],
+            'entry_url' => $sEntryUrl,
+            'entry_caption' => $sEntryCaption,
+            'entry_author' => $aQuestion[$CNF['FIELD_AUTHOR']],
+            'subentry_sample' => $CNF['T']['txt_sample_answer_single'],
+            'subentry_url' => $oOwnerProfile->getUrl(),
+        );
+    }
+
+
+    /**
+     * Privacy methods.
+     */
+    public function serviceCheckAllowedWithContentForProfile($sAction, $iContentId, $iProfileId, $isPerformAction = false)
+    {
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+
+    /**
+     * Event handlers.
+     */
+    public function onAddQuestion($iId)
+    {
+        bx_alert($this->getName(), 'added_question', $iId);
+    }
+
+    public function onEditQuestion($iId)
+    {
+        bx_alert($this->getName(), 'edited_question', $iId);
+    }
+
+    public function onDeleteQuestion($iId)
+    {
+        $this->_oDb->deleteAnswer(array('question_id' => $iId));
+
+        bx_alert($this->getName(), 'deleted_question', $iId);
+    }
+
+    public function onAddAnswer($iQuestionId, $iAnswerId, $iProfileId = false, $aParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $sModule = $this->getName();
+        $bModeAni = $this->_oConfig->isModeAni();
+
+        $aAnswer = $this->_oDb->getAnswers(array('type' => 'id', 'id' => $iAnswerId));
+
+        if(!$bModeAni || (int)$aAnswer[$CNF['FIELD_ANS_IMPORTANT']] > 0) {
+            bx_alert($sModule, 'added_answer', $iAnswerId, $iProfileId, array_merge(array(
+                'object_author_id' => $iProfileId,
+                'question_id' => $iQuestionId, 
+                'data' => $aAnswer['data'], 
+                'votes' => $aAnswer['votes']
+            ), $aParams));
+
+            bx_alert($sModule, 'added_answer_notif', $iQuestionId, $iProfileId, array_merge(array(
+                'object_author_id' => $iProfileId,
+                'subobject_id' => $iAnswerId, 
+                'data' => $aAnswer['data'], 
+                'votes' => $aAnswer['votes']
+            ), $aParams));
+        }
+    }
+
+    public function onDeleteAnswer($iQuestionId, $iAnswerId, $iProfileId = false, $aParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $sModule = $this->getName();
+        $bModeAni = $this->_oConfig->isModeAni();
+
+        $aAnswer = $this->_oDb->getAnswers(array('type' => 'id', 'id' => $iAnswerId));
+
+        if(!$bModeAni || (int)$aAnswer[$CNF['FIELD_ANS_IMPORTANT']] > 0) {
+            bx_alert($sModule, 'deleted_answer', $iAnswerId, $iProfileId, array_merge(array('question_id' => $iQuestionId), $aParams));
+
+            bx_alert($sModule, 'deleted_answer_notif', $iQuestionId, $iProfileId, array_merge(array('subobject_id' => $iAnswerId), $aParams));
+        }
+    }
+
+
+    /**
+     * Internal methods
+     */
+    protected function _getContentForTimelineQuestion($aEvent, $aQuestion, $aBrowseParams = array())
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+    	$sUrl = BX_DOL_URL_ROOT;
+
+        //--- Title
+        $sTitle = strmaxtextlen($aQuestion[$CNF['FIELD_TEXT']], 20, '...');
+
+    	return array(
+            'sample' => isset($CNF['T']['txt_sample_question_single_with_article']) ? $CNF['T']['txt_sample_question_single_with_article'] : $CNF['T']['txt_sample_question_single'],
+            'sample_wo_article' => $CNF['T']['txt_sample_question_single'],
+            'sample_action' => isset($CNF['T']['txt_sample_question_action']) ? $CNF['T']['txt_sample_question_action'] : '',
+            'url' => $sUrl,
+            'title' => $sTitle,
+            'text' => '',
+            'images' => array(),
+            'images_attach' => array(),
+            'videos' => array(),
+            'videos_attach' => array(),
+            'files' => array(),
+            'files_attach' => array()
+        );
+    }
+
+    protected function _getContentForTimelineAnswer($aEvent, $aQuestion, $aAnswer, $aBrowseParams = array())
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+        $oOwnerProfile = BxDolProfile::getInstance((int)$aEvent['owner_id']);
+
+    	$sUrl = BX_DOL_URL_ROOT;
+        if($oOwnerProfile)
+            $sUrl = $oOwnerProfile->getUrl();
+
+        //--- Title
+        $sTitle = strmaxtextlen($aQuestion[$CNF['FIELD_TEXT']], 20, '...');
+
+        $sTextKey = '_bx_feedback_txt_answer_format' . (empty($aAnswer[$CNF['FIELD_TEXT']]) ? '_wo_comment' : '');
+        $sText = _t($sTextKey, $aAnswer[$CNF['FIELD_ANS_TITLE']], $aAnswer[$CNF['FIELD_TEXT']]); 
+
+    	return array(
+            'sample' => isset($CNF['T']['txt_sample_answer_single_with_article']) ? $CNF['T']['txt_sample_answer_single_with_article'] : $CNF['T']['txt_sample_answer_single'],
+            'sample_wo_article' => $CNF['T']['txt_sample_answer_single'],
+            'sample_action' => isset($CNF['T']['txt_sample_answer_action']) ? $CNF['T']['txt_sample_answer_action'] : '',
+            'url' => $sUrl,
+            'title' => $sTitle,
+            'text' => $sText,
+            'images' => array(),
+            'images_attach' => array(),
+            'videos' => array(),
+            'videos_attach' => array(),
+            'files' => array(),
+            'files_attach' => array()
+        );
+    }
+}
+
+/** @} */

+ 43 - 0
modules/boonex/feedback/classes/BxFdbStudioPage.php

@@ -0,0 +1,43 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Credits Credits
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+define('BX_DOL_STUDIO_MOD_TYPE_QUESTIONS', 'questions');
+
+class BxFdbStudioPage extends BxTemplStudioModule
+{
+    protected $_sModule;
+    protected $_oModule;
+
+    function __construct($sModule = "", $sPage = "")
+    {
+    	$this->_sModule = 'bx_feedback';
+    	$this->_oModule = BxDolModule::getInstance($this->_sModule);
+
+        parent::__construct($sModule, $sPage);
+
+        $this->aMenuItems[] = array('name' => BX_DOL_STUDIO_MOD_TYPE_QUESTIONS, 'icon' => 'question', 'title' => '_bx_feedback_lmi_cpt_questions');
+    }
+
+    protected function getQuestions()
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $oGrid = BxDolGrid::getObjectInstance($CNF['OBJECT_GRID_QUESTIONS'], BxDolStudioTemplate::getInstance());
+        if(!$oGrid)
+            return '';
+
+        $this->_oModule->_oTemplate->addStudioJs(array('question.js'));
+        $this->_oModule->_oTemplate->addStudioCss(array('question.css'));
+        return $oGrid->getCode();
+    }
+}
+
+/** @} */

+ 60 - 0
modules/boonex/feedback/classes/BxFdbTemplate.php

@@ -0,0 +1,60 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Feedback Feedback
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxFdbTemplate extends BxBaseModGeneralTemplate
+{
+    function __construct(&$oConfig, &$oDb)
+    {
+        $this->MODULE = 'bx_feedback';
+
+        parent::__construct($oConfig, $oDb);
+    }
+
+    public function getJsCode($sType, $aParams = array(), $bWrap = true)
+    {
+        $aParams = array_merge(array(
+            'aHtmlIds' => $this->_oConfig->getHtmlIds()
+        ), $aParams);
+
+        return parent::getJsCode($sType, $aParams, $bWrap);
+    }
+
+    public function getBlockQuestion($aQuestion, $aAnswers)
+    {
+        $sJsObject = $this->_oConfig->getJsObject('question');
+
+        $iQuestion = (int)$aQuestion['id'];
+
+        $aTmplVarsAnswers = array();
+        foreach($aAnswers as $aAnswer) 
+            $aTmplVarsAnswers[] = array(
+                'js_object' => $sJsObject,
+                'id' => $aAnswer['id'],
+                'question_id' => $iQuestion,
+                'title' => bx_process_output($aAnswer['title']),
+                'bx_if:show_checked' => array(
+                    'condition' => (int)$aAnswer['checked'] > 0,
+                    'content' => array()
+                )
+            );
+
+        $this->addCss(array('question.css'));
+        $this->addJs(array('question.js'));
+        $this->addJsTranslation(array('_bx_feedback_txt_enter_text'));
+        return $this->getJsCode('question') . $this->parseHtmlByName('question.html', array(
+            'html_id' => $this->_oConfig->getHtmlIds('question') . $iQuestion,
+            'text' => bx_process_output($aQuestion['text']),
+            'bx_repeat:answers' => $aTmplVarsAnswers,
+        ));
+    }
+}
+
+/** @} */

+ 83 - 0
modules/boonex/feedback/install/config.php

@@ -0,0 +1,83 @@
+<?php
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Feedback Feedback
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+$aConfig = array(
+
+    /**
+     * Main Section.
+     */
+    'type' => BX_DOL_MODULE_TYPE_MODULE,
+    'name' => 'bx_feedback',
+    'title' => 'Feedback',
+    'note' => 'Feedback module.',
+    'version' => '11.0.0.DEV',
+    'vendor' => 'BoonEx',
+    'help_url' => 'http://feed.una.io/?section={module_name}',
+
+    'compatible_with' => array(
+        '11.0.x'
+    ),
+
+    /**
+     * 'home_dir' and 'home_uri' - should be unique. Don't use spaces in 'home_uri' and the other special chars.
+     */
+    'home_dir' => 'boonex/feedback/',
+    'home_uri' => 'feedback',
+
+    'db_prefix' => 'bx_fdb_',
+    'class_prefix' => 'BxFdb',
+
+    /**
+     * Category for language keys.
+     */
+    'language_category' => 'Feedback',
+
+    /**
+     * Installation/Uninstallation Section.
+     */
+    'install' => array(
+        'execute_sql' => 1,
+        'update_languages' => 1,
+        'clear_db_cache' => 1,
+    ),
+    'uninstall' => array (
+        'execute_sql' => 1,
+        'update_languages' => 1,
+        'update_relations' => 1,
+        'clear_db_cache' => 1,
+    ),
+    'enable' => array(
+        'execute_sql' => 1,
+        'update_relations' => 1,
+        'clear_db_cache' => 1,
+    ),
+    'disable' => array (
+        'execute_sql' => 1,
+        'update_relations' => 1,
+        'clear_db_cache' => 1,
+    ),
+
+    /**
+     * Dependencies Section
+     */
+    'dependencies' => array(),
+
+    /**
+     * Relations Section
+     */
+    'relations' => array(
+    	'bx_timeline',
+    	'bx_notifications'
+    ),
+
+);
+
+/** @} */

+ 20 - 0
modules/boonex/feedback/install/installer.php

@@ -0,0 +1,20 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Feedback Feedback
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxFdbInstaller extends BxBaseModTextInstaller
+{
+    public function __construct($aConfig)
+    {
+        parent::__construct($aConfig);
+    }
+}
+
+/** @} */

+ 74 - 0
modules/boonex/feedback/install/langs/en.xml

@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources name="en" flag="gb" title="English">
+    <string name="_bx_feedback"><![CDATA[Feedback]]></string>
+
+    <!-- Options -->
+    <string name="_bx_feedback_option_enable_questions"><![CDATA[Enable questions]]></string>
+    <string name="_bx_feedback_option_question_lifetime"><![CDATA[Default question lifetime (in days).]]></string>
+    <string name="_bx_feedback_option_answer_notify_important"><![CDATA[Notify about 'important' answers only.]]></string>
+
+    <!-- Pages -->
+    <string name="_bx_feedback_page_block_title_question"><![CDATA[Question]]></string>
+
+    <!-- Menus -->
+    <string name="_bx_feedback_lmi_cpt_questions"><![CDATA[Questions]]></string>
+
+    <!-- Forms -->
+    <string name="_bx_feedback_form_question"><![CDATA[Question]]></string>
+    <string name="_bx_feedback_form_question_display_add"><![CDATA[Add Question]]></string>
+    <string name="_bx_feedback_form_question_display_edit"><![CDATA[Edit Question]]></string>
+    <string name="_bx_feedback_form_question_display_delete"><![CDATA[Delete Question]]></string>
+    <string name="_bx_feedback_form_question_display_view"><![CDATA[View Question]]></string>
+    <string name="_bx_feedback_form_question_input_author"><![CDATA[Author]]></string>
+    <string name="_bx_feedback_form_question_input_sys_text"><![CDATA[Question]]></string>
+    <string name="_bx_feedback_form_question_input_text"><![CDATA[Question]]></string>
+    <string name="_bx_feedback_form_question_input_text_err"><![CDATA[Question is essential. Please, fill in this field]]></string>
+    <string name="_bx_feedback_form_question_input_sys_answers"><![CDATA[Answers]]></string>
+    <string name="_bx_feedback_form_question_input_answers"><![CDATA[Answers]]></string>
+    <string name="_bx_feedback_form_question_input_answers_err"><![CDATA[Question should have at least 2 non-empty answers.]]></string>
+    <string name="_bx_feedback_form_question_input_answers_add"><![CDATA[Add More]]></string>
+    <string name="_bx_feedback_form_question_input_answers_imp"><![CDATA[Mark as important]]></string>
+    <string name="_bx_feedback_form_question_input_sys_lifetime"><![CDATA[Lifetime]]></string>
+    <string name="_bx_feedback_form_question_input_lifetime"><![CDATA[Lifetime]]></string>
+    <string name="_bx_feedback_form_question_input_lifetime_inf"><![CDATA[In days. Leave blank to create an unlimited question.]]></string>
+    <string name="_bx_feedback_form_question_input_sys_delete_confirm"><![CDATA[Delete confirmation]]></string>
+    <string name="_bx_feedback_form_question_input_delete_confirm"><![CDATA[I am sure I want to delete this question]]></string>
+    <string name="_bx_feedback_form_question_input_delete_confirm_info"><![CDATA[This action can not be undone]]></string>
+    <string name="_bx_feedback_form_question_input_delete_confirm_error"><![CDATA[Please check this checkbox if you really want to delete the question]]></string>
+    <string name="_bx_feedback_form_question_input_sys_allow_view_to"><![CDATA[Visibility]]></string>
+    <string name="_bx_feedback_form_question_input_allow_view_to"><![CDATA[Visibility]]></string>
+    <string name="_bx_feedback_form_question_input_sys_date_added"><![CDATA[Created]]></string>
+    <string name="_bx_feedback_form_question_input_date_added"><![CDATA[Created]]></string>
+    <string name="_bx_feedback_form_question_input_sys_date_changed"><![CDATA[Updated]]></string>
+    <string name="_bx_feedback_form_question_input_date_changed"><![CDATA[Updated]]></string>
+    <string name="_bx_feedback_form_question_input_sys_do_submit"><![CDATA[Submit]]></string>
+    <string name="_bx_feedback_form_question_input_do_submit"><![CDATA[Submit]]></string>
+    <string name="_bx_feedback_form_question_input_sys_do_cancel"><![CDATA[Cancel]]></string>
+    <string name="_bx_feedback_form_question_input_do_cancel"><![CDATA[Cancel]]></string>
+
+
+    <!-- Grids -->
+    <string name="_bx_feedback_grid_question_column_switcher"><![CDATA[Enable]]></string>
+    <string name="_bx_feedback_grid_question_column_text"><![CDATA[Question]]></string>
+    <string name="_bx_feedback_grid_question_column_title_added"><![CDATA[Date]]></string>
+    <string name="_bx_feedback_grid_question_action_title_add"><![CDATA[Add]]></string>
+    <string name="_bx_feedback_grid_question_action_title_edit"><![CDATA[Edit]]></string>
+    <string name="_bx_feedback_grid_question_action_title_delete"><![CDATA[Delete]]></string>
+    <string name="_bx_feedback_grid_question_popup_title_add"><![CDATA[Add]]></string>
+    <string name="_bx_feedback_grid_question_popup_title_edit"><![CDATA[Edit]]></string>
+    <string name=""><![CDATA[]]></string>
+
+
+    <!-- Other -->
+    <string name="_bx_feedback_txt_sample_question_single"><![CDATA[question]]></string>
+    <string name="_bx_feedback_txt_sample_question_single_with_article"><![CDATA[a question]]></string>
+    <string name="_bx_feedback_txt_sample_answer_single"><![CDATA[answer to question]]></string>
+    <string name="_bx_feedback_txt_sample_answer_single_with_article"><![CDATA[an answer to question]]></string>
+    <string name="_bx_feedback_txt_enter_text"><![CDATA[Feel free to enter your comment here]]></string>
+    <string name="_bx_feedback_txt_answer_format"><![CDATA[<b>{0}</b> (<span class="bx-def-font-grayed">{1}</span>)]]></string>
+    <string name="_bx_feedback_txt_answer_format_wo_comment"><![CDATA[<b>{0}</b>]]></string>
+
+    <string name="_bx_feedback_err_cannot_perform"><![CDATA[Cannot perform the action. Please report.]]></string>
+    <string name="_bx_feedback_err_login_required"><![CDATA[You need to login first.]]></string>
+    <string name="_bx_feedback_err_wrong_data"><![CDATA[Cannot accept the answer.]]></string>
+</resources>

+ 16 - 0
modules/boonex/feedback/install/sql/disable.sql

@@ -0,0 +1,16 @@
+
+-- SETTINGS
+SET @iTypeId = (SELECT `ID` FROM `sys_options_types` WHERE `name` = 'bx_feedback' LIMIT 1);
+SET @iCategId = (SELECT `ID` FROM `sys_options_categories` WHERE `type_id` = @iTypeId LIMIT 1);
+DELETE FROM `sys_options` WHERE `category_id` = @iCategId;
+DELETE FROM `sys_options_categories` WHERE `type_id` = @iTypeId;
+DELETE FROM `sys_options_types` WHERE `id` = @iTypeId;
+
+-- PAGES
+DELETE FROM `sys_objects_page` WHERE `module` = 'bx_feedback';
+DELETE FROM `sys_pages_blocks` WHERE `module` = 'bx_feedback' OR `object` LIKE 'bx_feedback_%';
+
+-- GRIDS
+DELETE FROM `sys_objects_grid` WHERE `object` LIKE 'bx_feedback_%';
+DELETE FROM `sys_grid_fields` WHERE `object` LIKE 'bx_feedback_%';
+DELETE FROM `sys_grid_actions` WHERE `object` LIKE 'bx_feedback_%';

+ 47 - 0
modules/boonex/feedback/install/sql/enable.sql

@@ -0,0 +1,47 @@
+
+-- SETTINGS
+SET @iTypeOrder = (SELECT MAX(`order`) FROM `sys_options_types` WHERE `group` = 'modules');
+INSERT INTO `sys_options_types`(`group`, `name`, `caption`, `icon`, `order`) VALUES 
+('modules', 'bx_feedback', '_bx_feedback', 'bx_feedback@modules/boonex/feedback/|std-icon.svg', IF(ISNULL(@iTypeOrder), 1, @iTypeOrder + 1));
+SET @iTypeId = LAST_INSERT_ID();
+
+INSERT INTO `sys_options_categories` (`type_id`, `name`, `caption`, `order`)
+VALUES (@iTypeId, 'bx_feedback', '_bx_feedback', 1);
+SET @iCategId = LAST_INSERT_ID();
+
+INSERT INTO `sys_options` (`name`, `value`, `category_id`, `caption`, `type`, `check`, `check_error`, `extra`, `order`) VALUES
+('bx_feedback_enable_questions', 'on', @iCategId, '_bx_feedback_option_enable_questions', 'checkbox', '', '', '', 1),
+('bx_feedback_question_lifetime', '', @iCategId, '_bx_feedback_option_question_lifetime', 'digit', '', '', '', 2),
+
+('bx_feedback_enable_answer_notify_important', 'on', @iCategId, '_bx_feedback_option_answer_notify_important', 'checkbox', '', '', '', 10);
+
+
+-- PAGE: add block on homepage
+SET @iPBCellHomepage = 2;
+SET @iPBOrderHomepage = (SELECT IFNULL(MAX(`order`), 0) FROM `sys_pages_blocks` WHERE `object`='sys_home' AND `cell_id`=@iPBCellHomepage ORDER BY `order` DESC LIMIT 1);
+INSERT INTO `sys_pages_blocks`(`object`, `cell_id`, `module`, `title`, `designbox_id`, `visible_for_levels`, `type`, `content`, `deletable`, `copyable`, `active`, `order`) VALUES 
+('sys_home', @iPBCellHomepage, 'bx_feedback', '_bx_feedback_page_block_title_question', 13, 2147483647, 'service', 'a:2:{s:6:"module";s:11:"bx_feedback";s:6:"method";s:18:"get_block_question";}', 0, 1, 1, @iPBOrderHomepage + 1);
+
+-- PAGES: add block on dashboard
+SET @iPBCellDashboard = 3;
+SET @iPBOrderDashboard = (SELECT IFNULL(MAX(`order`), 0) FROM `sys_pages_blocks` WHERE `object` = 'sys_dashboard' AND `cell_id` = @iPBCellDashboard LIMIT 1);
+INSERT INTO `sys_pages_blocks` (`object`, `cell_id`, `module`, `title`, `designbox_id`, `visible_for_levels`, `type`, `content`, `deletable`, `copyable`, `active`, `order`) VALUES
+('sys_dashboard', @iPBCellDashboard, 'bx_feedback', '_bx_feedback_page_block_title_question', 13, 2147483644, 'service', 'a:2:{s:6:"module";s:11:"bx_feedback";s:6:"method";s:18:"get_block_question";}', 0, 0, 1, @iPBOrderDashboard + 1);
+
+
+-- GRIDS: questions
+INSERT INTO `sys_objects_grid` (`object`, `source_type`, `source`, `table`, `field_id`, `field_order`, `field_active`, `paginate_url`, `paginate_per_page`, `paginate_simple`, `paginate_get_start`, `paginate_get_per_page`, `filter_fields`, `filter_fields_translatable`, `filter_mode`, `sorting_fields`, `sorting_fields_translatable`, `visible_for_levels`, `override_class_name`, `override_class_file`) VALUES
+('bx_feedback_questions', 'Sql', 'SELECT * FROM `bx_fdb_questions` WHERE 1 ', 'bx_fdb_questions', 'id', 'added', 'status_admin', '', 100, NULL, 'start', '', 'text', '', 'like', '', '', 192, 'BxFdbGridQuestions', 'modules/boonex/feedback/classes/BxFdbGridQuestions.php');
+
+INSERT INTO `sys_grid_fields` (`object`, `name`, `title`, `width`, `translatable`, `chars_limit`, `params`, `order`) VALUES
+('bx_feedback_questions', 'checkbox', '_sys_select', '2%', 0, '', '', 1),
+('bx_feedback_questions', 'switcher', '_bx_feedback_grid_question_column_switcher', '5%', 0, '', '', 2),
+('bx_feedback_questions', 'text', '_bx_feedback_grid_question_column_text', '63%', 0, 32, '', 3),
+('bx_feedback_questions', 'added', '_bx_feedback_grid_question_column_title_added', '10%', 0, 16, '', 4),
+('bx_feedback_questions', 'actions', '', '20%', 0, '', '', 5);
+
+INSERT INTO `sys_grid_actions` (`object`, `type`, `name`, `title`, `icon`, `icon_only`, `confirm`, `order`) VALUES
+('bx_feedback_questions', 'independent', 'add', '_bx_feedback_grid_question_action_title_add', '', 0, 0, 1),
+('bx_feedback_questions', 'single', 'edit', '_bx_feedback_grid_question_action_title_edit', 'pencil-alt', 1, 0, 1),
+('bx_feedback_questions', 'single', 'delete', '_bx_feedback_grid_question_action_title_delete', 'remove', 1, 1, 2),
+('bx_feedback_questions', 'bulk', 'delete', '_bx_feedback_grid_question_action_title_delete', '', 0, 1, 1);

+ 95 - 0
modules/boonex/feedback/install/sql/install.sql

@@ -0,0 +1,95 @@
+-- TABLE: entries
+CREATE TABLE IF NOT EXISTS `bx_fdb_questions` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+  `author` int(11) unsigned NOT NULL,
+  `added` int(11) NOT NULL default '0',
+  `changed` int(11) NOT NULL default '0',
+  `text` text NOT NULL,
+  `lifetime` int(11) NOT NULL default '0',
+  `allow_view_to` varchar(16) NOT NULL DEFAULT '3',
+  `status_admin` enum('active','hidden') NOT NULL DEFAULT 'active',
+  PRIMARY KEY (`id`),
+  FULLTEXT KEY `search_fields` (`text`)
+);
+
+CREATE TABLE IF NOT EXISTS `bx_fdb_answers` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+  `question_id` int(11) unsigned NOT NULL default '0',
+  `title` varchar(255) NOT NULL,
+  `important` tinyint(4) NOT NULL default '0',
+  `data` text NOT NULL default '',
+  `votes` int(11) NOT NULL default '0',
+  `order` int(11) NOT NULL default '0',
+  PRIMARY KEY (`id`),
+  FULLTEXT KEY `title` (`title`)
+);
+
+CREATE TABLE IF NOT EXISTS `bx_fdb_answers2users` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+  `answer_id` int(11) unsigned NOT NULL default '0',
+  `profile_id` int(11) unsigned NOT NULL default '0',
+  `text` varchar(255) NOT NULL default '',
+  `added` int(11) NOT NULL default '0',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `answer` (`answer_id`, `profile_id`)
+);
+
+
+-- FORMS
+INSERT INTO `sys_objects_form`(`object`, `module`, `title`, `action`, `form_attrs`, `table`, `key`, `uri`, `uri_title`, `submit_name`, `params`, `deletable`, `active`, `override_class_name`, `override_class_file`) VALUES 
+('bx_feedback_question', 'bx_feedback', '_bx_feedback_form_question', '', 'a:1:{s:7:"enctype";s:19:"multipart/form-data";}', 'bx_fdb_questions', 'id', '', '', 'a:1:{i:0;s:9:"do_submit";}', 'a:1:{s:14:"checker_helper";s:30:"BxFdbFormQuestionCheckerHelper";}', 0, 1, 'BxFdbFormQuestion', 'modules/boonex/feedback/classes/BxFdbFormQuestion.php');
+
+INSERT INTO `sys_form_displays`(`object`, `display_name`, `module`, `view_mode`, `title`) VALUES 
+('bx_feedback_question', 'bx_feedback_question_add', 'bx_feedback', 0, '_bx_feedback_form_question_display_add'),
+('bx_feedback_question', 'bx_feedback_question_delete', 'bx_feedback', 0, '_bx_feedback_form_question_display_delete'),
+('bx_feedback_question', 'bx_feedback_question_edit', 'bx_feedback', 0, '_bx_feedback_form_question_display_edit'),
+('bx_feedback_question', 'bx_feedback_question_view', 'bx_feedback', 1, '_bx_feedback_form_question_display_view');
+
+INSERT INTO `sys_form_inputs`(`object`, `module`, `name`, `value`, `values`, `checked`, `type`, `caption_system`, `caption`, `info`, `required`, `collapsed`, `html`, `attrs`, `attrs_tr`, `attrs_wrapper`, `checker_func`, `checker_params`, `checker_error`, `db_pass`, `db_params`, `editable`, `deletable`) VALUES 
+('bx_feedback_question', 'bx_feedback', 'text', '', '', 0, 'textarea', '_bx_feedback_form_question_input_sys_text', '_bx_feedback_form_question_input_text', '', 1, 0, 0, '', '', '', 'Avail', '', '_bx_feedback_form_question_input_text_err', 'Xss', '', 1, 0),
+('bx_feedback_question', 'bx_feedback', 'answers', '', '', 0, 'custom', '_bx_feedback_form_question_input_sys_answers', '_bx_feedback_form_question_input_answers', '', 1, 0, 0, '', '', '', 'AvailAnswers', '', '_bx_feedback_form_question_input_answers_err', '', '', 1, 0),
+('bx_feedback_question', 'bx_feedback', 'lifetime', '', '', 0, 'text', '_bx_feedback_form_question_input_sys_lifetime', '_bx_feedback_form_question_input_lifetime', '_bx_feedback_form_question_input_lifetime_inf', 0, 0, 0, '', '', '', '', '', '', 'Int', '', 1, 0),
+('bx_feedback_question', 'bx_feedback', 'allow_view_to', '', '', 0, 'custom', '_bx_feedback_form_question_input_sys_allow_view_to', '_bx_feedback_form_question_input_allow_view_to', '', 1, 0, 0, '', '', '', '', '', '', '', '', 1, 0),
+('bx_feedback_question', 'bx_feedback', 'delete_confirm', 1, '', 0, 'checkbox', '_bx_feedback_form_question_input_sys_delete_confirm', '_bx_feedback_form_question_input_delete_confirm', '_bx_feedback_form_question_input_delete_confirm_info', 1, 0, 0, '', '', '', 'Avail', '', '_bx_feedback_form_question_input_delete_confirm_error', '', '', 1, 0),
+('bx_feedback_question', 'bx_feedback', 'controls', '', 'do_submit,do_cancel', 0, 'input_set', '', '', '', 0, 0, 0, '', '', '', '', '', '', '', '', 0, 0),
+('bx_feedback_question', 'bx_feedback', 'do_submit', '_bx_feedback_form_question_input_do_submit', '', 0, 'submit', '_bx_feedback_form_question_input_sys_do_submit', '', '', 0, 0, 0, '', '', '', '', '', '', '', '', 1, 0),
+('bx_feedback_question', 'bx_feedback', 'do_cancel', '_bx_feedback_form_question_input_do_cancel', '', 0, 'button', '_bx_feedback_form_question_input_sys_do_cancel', '', '', 0, 0, 0, 'a:2:{s:7:"onclick";s:45:"$(''.bx-popup-applied:visible'').dolPopupHide()";s:5:"class";s:22:"bx-def-margin-sec-left";}', '', '', '', '', '', '', '', 1, 0),
+('bx_feedback_question', 'bx_feedback', 'added', '', '', 0, 'datetime', '_bx_feedback_form_question_input_sys_date_added', '_bx_feedback_form_question_input_date_added', '', 0, 0, 0, '', '', '', '', '', '', '', '', 1, 0),
+('bx_feedback_question', 'bx_feedback', 'changed', '', '', 0, 'datetime', '_bx_feedback_form_question_input_sys_date_changed', '_bx_feedback_form_question_input_date_changed', '', 0, 0, 0, '', '', '', '', '', '', '', '', 1, 0);
+
+
+INSERT INTO `sys_form_display_inputs`(`display_name`, `input_name`, `visible_for_levels`, `active`, `order`) VALUES 
+('bx_feedback_question_add', 'text', 2147483647, 1, 1),
+('bx_feedback_question_add', 'answers', 2147483647, 1, 2),
+('bx_feedback_question_add', 'lifetime', 2147483647, 1, 3),
+('bx_feedback_question_add', 'allow_view_to', 2147483647, 0, 4),
+('bx_feedback_question_add', 'controls', 2147483647, 1, 5),
+('bx_feedback_question_add', 'do_submit', 2147483647, 1, 6),
+('bx_feedback_question_add', 'do_cancel', 2147483647, 1, 7),
+
+('bx_feedback_question_delete', 'delete_confirm', 2147483647, 1, 1),
+('bx_feedback_question_delete', 'do_submit', 2147483647, 1, 2),
+
+('bx_feedback_question_edit', 'text', 2147483647, 1, 1),
+('bx_feedback_question_edit', 'answers', 2147483647, 1, 2),
+('bx_feedback_question_edit', 'lifetime', 2147483647, 1, 3),
+('bx_feedback_question_edit', 'allow_view_to', 2147483647, 0, 4),
+('bx_feedback_question_edit', 'controls', 2147483647, 1, 5),
+('bx_feedback_question_edit', 'do_submit', 2147483647, 1, 6),
+('bx_feedback_question_edit', 'do_cancel', 2147483647, 1, 7),
+
+('bx_feedback_question_view', 'added', 2147483647, 1, 1),
+('bx_feedback_question_view', 'changed', 2147483647, 1, 2);
+
+
+-- STUDIO: page & widget
+INSERT INTO `sys_std_pages`(`index`, `name`, `header`, `caption`, `icon`) VALUES
+(3, 'bx_feedback', '_bx_feedback', '_bx_feedback', 'bx_feedback@modules/boonex/feedback/|std-icon.svg');
+SET @iPageId = LAST_INSERT_ID();
+
+SET @iParentPageId = (SELECT `id` FROM `sys_std_pages` WHERE `name` = 'home');
+SET @iParentPageOrder = (SELECT MAX(`order`) FROM `sys_std_pages_widgets` WHERE `page_id` = @iParentPageId);
+INSERT INTO `sys_std_widgets` (`page_id`, `module`, `url`, `click`, `icon`, `caption`, `cnt_notices`, `cnt_actions`) VALUES
+(@iPageId, 'bx_feedback', '{url_studio}module.php?name=bx_feedback', '', 'bx_feedback@modules/boonex/feedback/|std-icon.svg', '_bx_feedback', '', 'a:4:{s:6:"module";s:6:"system";s:6:"method";s:11:"get_actions";s:6:"params";a:0:{}s:5:"class";s:18:"TemplStudioModules";}');
+INSERT INTO `sys_std_pages_widgets` (`page_id`, `widget_id`, `order`) VALUES
+(@iParentPageId, LAST_INSERT_ID(), IF(ISNULL(@iParentPageOrder), 1, @iParentPageOrder + 1));

+ 16 - 0
modules/boonex/feedback/install/sql/uninstall.sql

@@ -0,0 +1,16 @@
+
+-- TABLES
+DROP TABLE IF EXISTS `bx_fdb_questions`, `bx_fdb_answers`, `bx_fdb_answers2users`;
+
+
+-- FORMS
+DELETE FROM `sys_objects_form` WHERE `module` = 'bx_feedback';
+DELETE FROM `sys_form_displays` WHERE `module` = 'bx_feedback';
+DELETE FROM `sys_form_inputs` WHERE `module` = 'bx_feedback';
+DELETE FROM `sys_form_display_inputs` WHERE `display_name` LIKE 'bx_feedback_%';
+
+
+-- STUDIO: page & widget
+DELETE FROM `tp`, `tw`, `tpw`
+USING `sys_std_pages` AS `tp`, `sys_std_widgets` AS `tw`, `sys_std_pages_widgets` AS `tpw`
+WHERE `tp`.`id` = `tw`.`page_id` AND `tw`.`id` = `tpw`.`widget_id` AND `tp`.`name` = 'bx_feedback';

+ 68 - 0
modules/boonex/feedback/js/question.js

@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Feedback Feedback
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+function BxFdbQuestion(oOptions) {
+    this._sActionsUrl = oOptions.sActionUrl;
+    this._sObjName = oOptions.sObjName == undefined ? 'oBxFdbQuestion' : oOptions.sObjName;
+
+    this._sAnimationEffect = oOptions.sAnimationEffect == undefined ? 'slide' : oOptions.sAnimationEffect;
+    this._iAnimationSpeed = oOptions.iAnimationSpeed == undefined ? 'slow' : oOptions.iAnimationSpeed;
+
+    this._aHtmlIds = oOptions.aHtmlIds == undefined ? {} : oOptions.aHtmlIds;
+    this._oRequestParams = oOptions.oRequestParams == undefined ? {} : oOptions.oRequestParams;
+}
+
+BxFdbQuestion.prototype.answerOnType = function(oEvent) {
+    var oText = $(oEvent.target);
+    oText.parents('.bx-form-input-answer:first').find("input[type = 'checkbox']").val(oText.val().trim());
+};
+
+BxFdbQuestion.prototype.answerOnSelect = function(oElement, iQuestion) {
+    var $this = this;
+
+    var fOnOk = function(oPopup) {
+        var oParams = jQuery.extend({}, $this._getDefaultParams(), {
+            question_id: iQuestion, 
+            answer_id: $(oElement).val(),
+            text: $(oPopup).find("input[type = 'text']").val()
+        });
+
+        $.get(
+            $this._sActionsUrl + 'answer/',
+            oParams,
+            function(oData){
+                processJsonData(oData);
+            },
+            'json'
+        );
+    };
+
+    bx_prompt(_t('_bx_feedback_txt_enter_text'), '', fOnOk);
+};
+
+BxFdbQuestion.prototype.answerAdd = function(oButton, sName) {
+    var oButton = $(oButton);
+
+    var oSubentry = oButton.parents('#bx-form-element-' + sName).find('.bx-form-input-answer:first').clone();
+    oSubentry.find("input[type = 'text']").val('');
+    oSubentry.find("input[type = 'checkbox']").val('1').removeAttr('checked');
+    oSubentry.find("input[type = 'hidden']").remove();
+
+    oButton.parents('.bx-form-input-answer-add:first').before(oSubentry);
+};
+
+BxFdbQuestion.prototype.answerDelete = function(oButton) {
+    $(oButton).parents('.bx-form-input-answer:first').remove();
+};
+
+BxFdbQuestion.prototype._getDefaultParams = function() {
+    var oDate = new Date();
+    return {_t:oDate.getTime()};
+};

+ 18 - 0
modules/boonex/feedback/request.php

@@ -0,0 +1,18 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Feedback Feedback
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+require_once(BX_DIRECTORY_PATH_INC . "design.inc.php");
+
+check_logged();
+
+BxBaseModTextRequest::processAsAction($GLOBALS['aModule'], $GLOBALS['aRequest']);
+
+/** @} */

+ 1 - 0
modules/boonex/feedback/template/css/main.css

@@ -0,0 +1 @@
+@import url(../../../../base/general/template/css/main.css);

+ 50 - 0
modules/boonex/feedback/template/css/question.css

@@ -0,0 +1,50 @@
+/*-- Forms custom element: Answers --*/
+.bx-form-input-answer {
+    position: relative;
+}
+.bx-form-input-answer-row {
+    display: -webkit-flex;
+    display: flex;
+    -webkit-align-items: center;
+    align-items: center;
+}
+.bx-form-input-answer-cell.bx-fi-sc-txt {
+    -webkit-flex: 1 1 auto;
+    -ms-flex: 1 1 auto;
+    flex: 1 1 auto;
+}
+.bx-form-input-answer-cell.bx-fi-sc-btn {
+    -webkit-flex: 0 0 auto;
+    -ms-flex: 0 0 auto;
+    flex: 0 0 auto;
+}
+
+.bx-form-input-answer-add {
+    text-align: right;
+}
+.bx-form-input-answer-add .bx-form-input-button {
+    display: inline-block;
+    float: none;
+}
+
+
+/*-- Question --*/
+.bx-fdb-answer {
+    display: -webkit-flex;
+    display: flex;
+    -webkit-align-items: center;
+    align-items: center;
+}
+.bx-fdb-a-option {
+    -webkit-flex: 0 0 auto;
+    -ms-flex: 0 0 auto;
+    flex: 0 0 auto;
+    
+    margin: 0;
+    padding: 0;
+}
+.bx-fdb-a-title {
+    -webkit-flex: 1 1 auto;
+    -ms-flex: 1 1 auto;
+    flex: 1 1 auto;
+}

+ 14 - 0
modules/boonex/feedback/template/form_answers.html

@@ -0,0 +1,14 @@
+<div class="bx-form-input-answers">
+    <bx_repeat:answers>
+        <div class="bx-form-input-answer bx-def-padding-thd-topbottom">
+            <div class="bx-form-input-answer-row bx-def-margin-thd-top-auto">
+                <div class="bx-form-input-answer-cell bx-fi-sc-txt">__input_text__</div>
+                <div class="bx-form-input-answer-cell bx-fi-sc-btn bx-def-padding-sec-left">
+                    <a class="bx-btn" href="javascript:void(0)" onclick="javascript:__js_object__.answerDelete(this)"><i class="sys-icon times"></i></a>
+                </div>
+            </div>
+            <div class="bx-form-input-answer-row bx-def-margin-thd-top-auto">__input_checkbox__</div>
+        </div>
+    </bx_repeat:answers>
+    <div class="bx-form-input-answer-add">__btn_add__</div>
+</div>

+ 23 - 0
modules/boonex/feedback/template/images/icons/std-icon.svg

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Слой_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 64 64" style="enable-background:new 0 0 64 64;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:#00B356;}
+	.st1{fill:#FFFFFF;}
+</style>
+<title>files</title>
+<desc>Created with Sketch.</desc>
+<g id="Page-1">
+	<g id="Artboard" transform="translate(-1636.000000, -511.000000)">
+		<g id="files" transform="translate(1636.000000, 511.000000)">
+			<g id="Path">
+				<path id="path-1_1_" class="st0" d="M32,64c14,0,21.8,0,26.9-5.1C64,53.8,64,46,64,32s0-21.8-5.1-26.9C53.8,0,46,0,32,0
+					S10.2,0,5.1,5.1C0,10.2,0,18,0,32s0,21.8,5.1,26.9C10.2,64,18,64,32,64z"/>
+			</g>
+		</g>
+	</g>
+</g>
+<path class="st1" d="M32,12.5c-12.3,0-22.3,8.1-22.3,18.1c0,4.3,1.9,8.3,5,11.4c-1.1,4.4-4.7,8.3-4.8,8.4c-0.2,0.2-0.2,0.5-0.1,0.8
+	c0.1,0.3,0.4,0.4,0.6,0.4c5.8,0,10.1-2.8,12.3-4.5c2.9,1.1,6,1.7,9.4,1.7c12.3,0,22.3-8.1,22.3-18.1S44.3,12.5,32,12.5z"/>
+</svg>

+ 13 - 0
modules/boonex/feedback/template/question.html

@@ -0,0 +1,13 @@
+<div id="__html_id__" class="bx-fdb-question">
+    <div class="bx-fdb-question-cnt">
+        <div class="bx-fdb-q-text bx-def-font-h3">__text__</div>
+        <div class="bx-fdb-q-answers bx-def-margin-top">
+            <bx_repeat:answers>
+                <div class="bx-fdb-answer bx-def-margin-top-auto">
+                    <input class="bx-fdb-a-option" type="radio" name="question__question_id__" value="__id__" <bx_if:show_checked>checked="checked"</bx_if:show_checked> onchange="javascript:__js_object__.answerOnSelect(this, __question_id__)" />
+                    <span class="bx-fdb-a-title bx-def-padding-sec-left">__title__</span>
+                </div>
+            </bx_repeat:answers>
+        </div>
+    </div>
+</div>

+ 20 - 0
modules/boonex/feedback/template/question_popup_form.html

@@ -0,0 +1,20 @@
+<div class="bx-fdb-question-form bx-def-color-bg-block">
+    <div class="bx-fdb-question-form-cnt">
+        __form__
+        <script type="text/javascript">
+            $(document).ready(function () {
+                $("#__form_id__").ajaxForm({ 
+                    dataType: "json",
+                    beforeSubmit: function (formData, jqForm, options) {
+                        bx_loading($("#__form_id__"), true);
+                    },
+                    success: function (data) {
+                        $(".bx-popup-applied:visible").dolPopupHide();
+
+                        glGrids.__object__.processJson(data, '__action__');
+                    }
+                });
+            });
+        </script>
+    </div>
+</div>