diff --git a/behaviors/ArtBoxAccessBehavior.php b/behaviors/ArtBoxAccessBehavior.php
new file mode 100755
index 0000000..38130bc
--- /dev/null
+++ b/behaviors/ArtBoxAccessBehavior.php
@@ -0,0 +1,141 @@
+ 'interception',
+ ];
+ }
+
+ /**
+ * Check whether current user have access to current action.
+ *
+ * @param Event $event
+ *
+ * @return void
+ * @throws \yii\web\ForbiddenHttpException
+ */
+ public function interception($event)
+ {
+ if (!isset( Yii::$app->i18n->translations[ 'db_rbac' ] )) {
+ Yii::$app->i18n->translations[ 'db_rbac' ] = [
+ 'class' => 'yii\i18n\PhpMessageSource',
+ 'sourceLanguage' => 'ru-Ru',
+ 'basePath' => '@developeruz/db_rbac/messages',
+ ];
+ }
+
+ $route = Yii::$app->getRequest()
+ ->resolve();
+ //Проверяем права по конфигу
+ $this->createRule();
+ $user = Instance::ensure(Yii::$app->user, User::className());
+ $request = Yii::$app->getRequest();
+ $action = $event->action;
+
+ if (!$this->cheсkByRule($action, $user, $request)) {
+ //И по AuthManager
+ if (!$this->checkPermission($route)) {
+ if ($user->getIsGuest()) {
+ $user->loginRequired();
+ } else {
+ throw new ForbiddenHttpException(Yii::t('db_rbac', 'Недостаточно прав'));
+ }
+ }
+
+ }
+ }
+
+ /**
+ * Fill $ruleList with AccessRules
+ *
+ * @return void
+ */
+ protected function createRule()
+ {
+ foreach ($this->rules as $controller => $rule) {
+ foreach ($rule as $singleRule) {
+ if (is_array($singleRule)) {
+ $option = [
+ 'controllers' => [ $controller ],
+ 'class' => 'yii\filters\AccessRule',
+ ];
+ $this->ruleList[] = Yii::createObject(array_merge($option, $singleRule));
+ }
+ }
+ }
+ }
+
+ /**
+ * Check whether the User allowed to perform action
+ *
+ * @param Action $action
+ * @param User $user
+ * @param Request $request
+ *
+ * @return bool
+ */
+ protected function cheсkByRule($action, $user, $request)
+ {
+
+ foreach ($this->ruleList as $rule) {
+
+ if ($rule->allows($action, $user, $request)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check whether the User have permission for current operation
+ *
+ * @param array $route
+ *
+ * @return bool
+ */
+ protected function checkPermission($route)
+ {
+ //$route[0] - is the route, $route[1] - is the associated parameters
+ $routePathTmp = explode('/', $route[ 0 ]);
+ $routeVariant = array_shift($routePathTmp);
+ if (Yii::$app->user->can($routeVariant, $route[ 1 ])) {
+ return true;
+ }
+ /**
+ * @var string $routePart
+ */
+ foreach ($routePathTmp as $routePart) {
+ $routeVariant .= '/' . $routePart;
+ if (Yii::$app->user->can($routeVariant, $route[ 1 ])) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ }
+
\ No newline at end of file
diff --git a/behaviors/ImageBehavior.php b/behaviors/ImageBehavior.php
new file mode 100755
index 0000000..a458740
--- /dev/null
+++ b/behaviors/ImageBehavior.php
@@ -0,0 +1,68 @@
+ 'beforeDelete',
+ ];
+ }
+
+ /**
+ * @param Event $event
+ */
+ public function beforeDelete($event)
+ {
+ $file = $this->getImageFile();
+ if(file_exists($file)) {
+ unlink($file);
+ }
+ }
+
+ /**
+ * Get image file path
+ *
+ * @return null|string
+ */
+ public function getImageFile()
+ {
+ $link = $this->link;
+ return empty( $this->owner->$link ) ? NULL : \Yii::getAlias('@storage/' . $this->directory . '/' . $this->owner->$link);
+ }
+
+ /**
+ * Get image file url
+ *
+ * @return null|string
+ */
+ public function getImageUrl()
+ {
+ $link = $this->link;
+ return empty( $this->owner->$link ) ? NULL : '/storage/' . $this->directory . '/' . $this->owner->$link;
+ }
+ }
\ No newline at end of file
diff --git a/behaviors/MultipleImgBehavior.php b/behaviors/MultipleImgBehavior.php
new file mode 100755
index 0000000..09e3ef8
--- /dev/null
+++ b/behaviors/MultipleImgBehavior.php
@@ -0,0 +1,143 @@
+owner;
+ $query = $owner->hasOne($this->model, $this->links);
+ $conditions = $this->conditions;
+ foreach($conditions as $condition) {
+ $query->andWhere($condition);
+ }
+ return $query;
+ }
+
+ /**
+ * All images query
+ *
+ * @return \yii\db\ActiveQuery
+ */
+ public function getImages()
+ {
+ /**
+ * @var ActiveRecord $owner
+ */
+ $owner = $this->owner;
+ $query = $owner->hasMany($this->model, $this->links);
+ $conditions = $this->conditions;
+ foreach($conditions as $left => $right) {
+ $query->andWhere([$left => $right]);
+ }
+ return $query;
+ }
+
+ /**
+ * Get images config array for FileInput
+ *
+ * @return array
+ */
+ public function getImagesConfig()
+ {
+ $op = [];
+ $images = $this->getImages()->all();
+ $config = $this->config;
+ if(!isset( $config[ 'id' ] )) {
+ return $op;
+ }
+ foreach($images as $image) {
+ $op[] = [
+ 'caption' => ( isset( $config[ 'caption' ] ) ) ? $image->{$config[ 'caption' ]} : '',
+ 'url' => ( isset( $config[ 'delete_action' ] ) ) ? Url::to([
+ $config[ 'delete_action' ],
+ 'id' => $image->{$config[ 'id' ]},
+ ]) : '',
+ 'key' => $image->{$config[ 'id' ]},
+ 'extra' => [
+ 'id' => $image->{$config[ 'id' ]},
+ ],
+ ];
+ }
+ return $op;
+ }
+
+ /**
+ * Get images HTML
+ *
+ * @param string $preset
+ *
+ * @return array
+ */
+ public function getImagesHTML($preset = 'admin_thumb')
+ {
+ $op = [];
+ $images = $this->getImages()->all();
+ foreach($images as $image) {
+ $op[] = ArtboxImageHelper::getImage($image->imageUrl, $preset);
+ }
+ return $op;
+ }
+
+ public function getImageUrl()
+ {
+ $image = $this->getImage()->one();
+ if(!empty($image)) {
+ return $image->getImageUrl();
+ } else {
+ return NULL;
+ }
+ }
+ }
\ No newline at end of file
diff --git a/behaviors/SaveImgBehavior.php b/behaviors/SaveImgBehavior.php
new file mode 100755
index 0000000..af9d3d8
--- /dev/null
+++ b/behaviors/SaveImgBehavior.php
@@ -0,0 +1,95 @@
+ 'beforeSave',
+ ActiveRecord::EVENT_BEFORE_INSERT => 'beforeSave',
+ ];
+ }
+
+ /**
+ * @param ModelEvent $event
+ */
+ public function beforeSave($event)
+ {
+ foreach($this->fields as $field) {
+ $field_name = $field[ 'name' ];
+ $name = $field_name;
+ if($this->isLanguage) {
+ $name = '[' . $this->owner->language_id . ']' . $name;
+ }
+
+ $image = UploadedFile::getInstance($this->owner, $name);
+
+ if(empty( $image ) && $event->name == ActiveRecord::EVENT_BEFORE_UPDATE) {
+ $this->owner->$field_name = $this->owner->getOldAttribute($field_name);
+ } elseif(!empty( $image )) {
+ $imgDir = \Yii::getAlias('@storage/' . $field[ 'directory' ] . '/');
+
+ if(!is_dir($imgDir)) {
+ mkdir($imgDir, 0755, true);
+ }
+
+ $baseName = $image->baseName;
+
+ $iteration = 0;
+ $file_name = $imgDir . $baseName . '.' . $image->extension;
+ while(file_exists($file_name)) {
+ $baseName = $image->baseName . '_' . ++$iteration;
+ $file_name = $imgDir . $baseName . '.' . $image->extension;
+ }
+ unset( $iteration );
+
+ $this->owner->$field_name = $baseName . '.' . $image->extension;
+
+ $image->saveAs($file_name);
+ }
+ }
+ }
+
+ /**
+ * @param int $field
+ *
+ * @return null|string
+ */
+ public function getImageFile($field = 0)
+ {
+ $fieldset = $this->fields[ $field ];
+ $name = $fieldset[ 'name' ];
+ $directory = $fieldset[ 'directory' ];
+ return empty( $this->owner->$name ) ? NULL : '/storage/' . $directory . '/' . $this->owner->$name;
+ }
+
+ /**
+ * @param int $field
+ *
+ * @return null|string
+ */
+ public function getImageUrl($field = 0)
+ {
+ $fieldset = $this->fields[ $field ];
+ $name = $fieldset[ 'name' ];
+ $directory = $fieldset[ 'directory' ];
+ return empty( $this->owner->$name ) ? NULL : '/storage/' . $directory . '/' . $this->owner->$name;
+ }
+ }
\ No newline at end of file
diff --git a/behaviors/SaveMultipleFileBehavior.php b/behaviors/SaveMultipleFileBehavior.php
new file mode 100755
index 0000000..d8b3351
--- /dev/null
+++ b/behaviors/SaveMultipleFileBehavior.php
@@ -0,0 +1,119 @@
+ 'downloadFiles',
+ ActiveRecord::EVENT_AFTER_INSERT => 'downloadFiles',
+ ];
+ }
+
+ /**
+ * Save files to file table
+ *
+ * @param Event $event
+ */
+ public function downloadFiles($event)
+ {
+ /**
+ * @var ActiveRecord $owner
+ */
+ $owner = $this->owner;
+ $name = $this->name;
+ $owner->$name = UploadedFile::getInstances($owner, $name);
+ if(!empty( $files = $this->filesUpload() )) {
+ $model = $this->model;
+ $links = $this->links;
+ $column = $this->column;
+ foreach($files as $file) {
+ /**
+ * @var ActiveRecord $fileModel
+ */
+ $fileModel = new $model();
+ foreach($links as $link_owner => $link) {
+ $fileModel->$link = $owner->$link_owner;
+ }
+ $fileModel->$column = $file;
+ $fileModel->save();
+ }
+ }
+ $this->detach();
+ }
+
+ /**
+ * Save files to file system
+ *
+ * @return array
+ */
+ private function filesUpload()
+ {
+ $owner = $this->owner;
+ $name = $this->name;
+ $directory = $this->directory;
+ $fileDir = \Yii::getAlias('@storage/' . $directory . '/');
+ if(!is_dir($fileDir)) {
+ mkdir($fileDir, 0755, true);
+ }
+ $files = [];
+ /**
+ * @var UploadedFile $file
+ */
+ foreach($owner->$name as $file) {
+ $fileName = $file->baseName . '.' . $file->extension;
+ $i = 0;
+ while(file_exists(\Yii::getAlias($fileDir . $fileName))) {
+ $fileName = $file->baseName . '_' . ++$i . '.' . $file->extension;
+ }
+ $file->saveAs($fileDir . $fileName);
+ $files[] = $fileName;
+ }
+ return $files;
+ }
+ }
\ No newline at end of file
diff --git a/behaviors/Slug.php b/behaviors/Slug.php
new file mode 100755
index 0000000..61fa930
--- /dev/null
+++ b/behaviors/Slug.php
@@ -0,0 +1,200 @@
+ 'getSlug',
+ ActiveRecord::EVENT_BEFORE_UPDATE => 'getSlug',
+ ];
+ }
+
+ /**
+ * Generate slug
+ *
+ * @param yii\base\Event $event
+ *
+ * @return void
+ */
+ public function getSlug($event)
+ {
+ if(!empty( $this->owner->{$this->inAttribute} )) {
+ if(empty( $this->owner->{$this->outAttribute} )) {
+ $this->owner->{$this->outAttribute} = $this->generateSlug($this->owner->{$this->inAttribute});
+ } else {
+ $this->owner->{$this->outAttribute} = $this->generateSlug($this->owner->{$this->outAttribute});
+ }
+ }
+ }
+
+ /**
+ * @param string $slug
+ *
+ * @return string
+ */
+ private function generateSlug($slug)
+ {
+ $slug = $this->slugify($slug);
+ if($this->checkUniqueSlug($slug)) {
+ return $slug;
+ } else {
+ for($suffix = 2; !$this->checkUniqueSlug($new_slug = $slug . '-' . $suffix); $suffix++) {
+ }
+ return $new_slug;
+ }
+ }
+
+ /**
+ * @param string $slug
+ *
+ * @return string
+ */
+ private function slugify($slug)
+ {
+ if($this->translit) {
+ return yii\helpers\Inflector::slug( $this->translit( $slug ), '-', true );
+ } else {
+ return $this->slug($slug, '-', true);
+ }
+ }
+
+ /**
+ * @param string $string
+ * @param string $replacement
+ * @param bool $lowercase
+ *
+ * @return string
+ */
+ private function slug($string, $replacement = '-', $lowercase = true)
+ {
+ $string = preg_replace('/[^\p{L}\p{Nd}]+/u', $replacement, $string);
+ $string = trim($string, $replacement);
+ return $lowercase ? strtolower($string) : $string;
+ }
+
+ /**
+ * Check whether current slug is unique
+ *
+ * @param string $slug
+ *
+ * @return bool
+ */
+ private function checkUniqueSlug($slug)
+ {
+ /**
+ * @var ActiveRecord $owner
+ */
+ $owner = $this->owner;
+ $query = $owner->find()
+ ->where([
+ $this->outAttribute => $slug,
+ ]);
+ if(!$owner->isNewRecord) {
+ $pks = $owner->primaryKey();
+ if(!empty( $pks )) {
+ $pk_rules = [ 'and' ];
+ foreach($pks as $pk) {
+ $pk_rules[] = [ $pk => $owner->$pk ];
+ }
+ $query->andWhere([
+ 'not',
+ $pk_rules,
+ ]);
+ }
+ }
+ return !$query->exists();
+ }
+
+ static function translit ($string, $setting = 'all')
+ {
+ $letter = array (
+
+ 'а' => 'a', 'б' => 'b', 'в' => 'v',
+ 'г' => 'g', 'д' => 'd', 'е' => 'e',
+ 'ё' => 'e', 'ж' => 'zh', 'з' => 'z',
+ 'и' => 'i', 'й' => 'y', 'к' => 'k',
+ 'л' => 'l', 'м' => 'm', 'н' => 'n',
+ 'о' => 'o', 'п' => 'p', 'р' => 'r',
+ 'с' => 's', 'т' => 't', 'у' => 'u',
+ 'ф' => 'f', 'х' => 'h', 'ц' => 'c',
+ 'ч' => 'ch', 'ш' => 'sh', 'щ' => 'sch',
+ 'ь' => "", 'ы' => 'y', 'ъ' => "",
+ 'э' => 'e', 'ю' => 'yu', 'я' => 'ya',
+ 'ї' => 'yi', 'є' => 'ye', 'і' => 'ee',
+
+ 'А' => 'A', 'Б' => 'B', 'В' => 'V',
+ 'Г' => 'G', 'Д' => 'D', 'Е' => 'E',
+ 'Ё' => 'E', 'Ж' => 'Zh', 'З' => 'Z',
+ 'И' => 'I', 'Й' => 'Y', 'К' => 'K',
+ 'Л' => 'L', 'М' => 'M', 'Н' => 'N',
+ 'О' => 'O', 'П' => 'P', 'Р' => 'R',
+ 'С' => 'S', 'Т' => 'T', 'У' => 'U',
+ 'Ф' => 'F', 'Х' => 'H', 'Ц' => 'C',
+ 'Ч' => 'Ch', 'Ш' => 'Sh', 'Щ' => 'Sch',
+ 'Ь' => "", 'Ы' => 'Y', 'Ъ' => "",
+ 'Э' => 'E', 'Ю' => 'Yu', 'Я' => 'Ya',
+ 'Ї' => 'Yi', 'Є' => 'Ye', 'І' => 'Ee'
+ );
+
+ $symbol = array (
+ ' ' => '-', "'" => '', '"' => '',
+ '!' => '', "@" => '', '#' => '',
+ '$' => '', "%" => '', '^' => '',
+ ';' => '', "*" => '', '(' => '',
+ ')' => '', "+" => '', '~' => '',
+ '.' => '', ',' => '-', '?' => '',
+ '…' => '', '№' => 'N', '°' => '',
+ '`' => '', '|' => '', '&' => '-and-',
+ '<' => '', '>' => ''
+ );
+
+ if ($setting == 'all')
+ {
+ $converter = $letter + $symbol;
+ }
+ else if ($setting == 'letter')
+ {
+ $converter = $letter;
+ }
+ else if ($setting == 'symbol')
+ {
+ $converter = $symbol;
+ }
+
+ $url = strtr ($string, $converter);
+
+ $url = str_replace ("---", '-', $url);
+ $url = str_replace ("--", '-', $url);
+
+ return $url;
+ }
+
+ }
+
\ No newline at end of file
diff --git a/components/SmsSender.php b/components/SmsSender.php
new file mode 100755
index 0000000..d7e657c
--- /dev/null
+++ b/components/SmsSender.php
@@ -0,0 +1,49 @@
+\n";
+ $myXML .= "";
+ $myXML .= "SENDSMS";
+ $myXML .= ' ' . "\n";
+ $myXML .= " " . $text . "";
+ $myXML .= " " . $recipient . "";
+ $myXML .= "";
+ $myXML .= "";
+
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_USERPWD, $user . ':' . $password);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
+ curl_setopt($ch, CURLOPT_POST, 1);
+ curl_setopt($ch, CURLOPT_URL, 'http://sms-fly.com/api/api.php');
+ curl_setopt($ch, CURLOPT_HTTPHEADER, array("Content-Type: text/xml", "Accept: text/xml"));
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $myXML);
+ $response = curl_exec($ch);
+ curl_close($ch);
+
+ return $response;
+ }
+}
diff --git a/components/artboximage/ArtboxImage.php b/components/artboximage/ArtboxImage.php
new file mode 100755
index 0000000..0f3ef2c
--- /dev/null
+++ b/components/artboximage/ArtboxImage.php
@@ -0,0 +1,56 @@
+ 'jpeg',
+ 'jpeg' => 'jpeg',
+ 'png' => 'png',
+ 'gif' => 'gif',
+ 'bmp' => 'bmp',
+ ];
+
+ /**
+ * Try to load image and prepare it to manipulation.
+ *
+ * @param null|string $file
+ * @param null|string $driver
+ *
+ * @return \yii\image\drivers\Image
+ * @throws \yii\base\ErrorException
+ */
+ public function load($file = null, $driver = null)
+ {
+ if (empty( $file ) || !realpath($file)) {
+ throw new ErrorException('File name can not be empty and exists');
+ }
+ return Image::factory($file, $driver ? $driver : $this->driver);
+ }
+ }
\ No newline at end of file
diff --git a/components/artboximage/ArtboxImageHelper.php b/components/artboximage/ArtboxImageHelper.php
new file mode 100755
index 0000000..84e0768
--- /dev/null
+++ b/components/artboximage/ArtboxImageHelper.php
@@ -0,0 +1,196 @@
+get('artboximage');
+ }
+ return self::$imageDriver;
+ }
+
+ /**
+ * Get named preset from driver preset list.
+ *
+ * @param string $preset
+ *
+ * @return array|null
+ */
+ public static function getPreset($preset)
+ {
+ if (empty( self::$presets )) {
+ self::$presets = self::getDriver()->presets;
+ }
+ return empty( self::$presets[ $preset ] ) ? null : self::$presets[ $preset ];
+ }
+
+ /**
+ * Get image HTML for image
+ *
+ * @param string $file
+ * @param array|string $preset
+ * @param array $imgOptions
+ *
+ * @see Html::img()
+ * @return string
+ */
+ public static function getImage($file, $preset, $imgOptions = [])
+ {
+ $preset_alias = is_array($preset) ? array_keys($preset)[ 0 ] : null;
+ return Html::img(self::getImageSrc($file, $preset, $preset_alias), $imgOptions);
+ }
+
+ /**
+ * Get src for image
+ *
+ * @param string $file
+ * @param string $preset
+ * @param null|string $preset_alias
+ *
+ * @return bool|string
+ */
+ public static function getImageSrc($file, $preset, $preset_alias = null)
+ {
+ if (is_string($preset)) {
+ $preset_alias = $preset;
+ $preset = self::getPreset($preset);
+ }
+ if (empty( $preset ) || empty( $preset_alias )) {
+ return $file;
+ }
+
+ $filePath = self::getPathFromUrl($file);
+ if (!file_exists($filePath) || !preg_match(
+ '#^(.*)\.(' . self::getExtensionsRegexp() . ')$#',
+ $file,
+ $matches
+ )
+ ) {
+ return $file;
+ }
+ return self::getPresetUrl($filePath, $preset, $preset_alias);
+ }
+
+ /**
+ * Replace web path with file path
+ *
+ * @param string $url
+ *
+ * @return string
+ */
+ private static function getPathFromUrl($url)
+ {
+ return substr_replace($url, self::getDriver()->rootPath, 0, strlen(self::getDriver()->rootUrl));
+ }
+
+ /**
+ * Replace file path with web path
+ *
+ * @param string $path
+ *
+ * @return string
+ */
+ private static function getUrlFromPath($path)
+ {
+ return substr_replace($path, self::getDriver()->rootUrl, 0, strlen(self::getDriver()->rootPath));
+ }
+
+ /**
+ * Get formatted file url or create it if not exist
+ *
+ * @param string $filePath
+ * @param array $preset
+ * @param string $preset_alias
+ *
+ * @return bool|string
+ */
+ private static function getPresetUrl($filePath, $preset, $preset_alias)
+ {
+ $pathinfo = pathinfo($filePath);
+ $presetPath = $pathinfo[ 'dirname' ] . '/styles/' . strtolower($preset_alias);
+ $presetFilePath = $presetPath . '/' . $pathinfo[ 'basename' ];
+ $presetUrl = self::getUrlFromPath($presetFilePath);
+ if (file_exists($presetFilePath)) {
+ return $presetUrl;
+ }
+ if (!file_exists($presetPath)) {
+ @mkdir($presetPath, 0777, true);
+ }
+ $output = self::createPresetImage($filePath, $preset, $preset_alias);
+ if (!empty( $output )) {
+ $f = fopen($presetFilePath, 'w');
+ fwrite($f, $output);
+ fclose($f);
+ return $presetUrl;
+ }
+ return false;
+ }
+
+ /**
+ * Create formatted image.
+ * Available manipulations:
+ * * resize
+ * * flip
+ *
+ * @param string $filePath
+ * @param array $preset
+ * @param string $preset_alias
+ *
+ * @return string
+ */
+ private static function createPresetImage($filePath, $preset, $preset_alias)
+ {
+ $image = self::getDriver()
+ ->load($filePath);
+ foreach ($preset as $action => $data) {
+ switch ($action) {
+ case 'resize':
+ $width = empty( $data[ 'width' ] ) ? null : $data[ 'width' ];
+ $height = empty( $data[ 'height' ] ) ? null : $data[ 'height' ];
+ $master = empty( $data[ 'master' ] ) ? null : $data[ 'master' ];
+ $image->resize($width, $height, $master);
+ break;
+ case 'flip':
+ $image->flip(@$data[ 'direction' ]);
+ break;
+ default:
+ break;
+ }
+ }
+ return $image->render();
+ }
+
+ /**
+ * Get extensions regexp
+ *
+ * @return string regexp
+ */
+ private static function getExtensionsRegexp()
+ {
+ $keys = array_keys(self::getDriver()->extensions);
+ return '(?i)' . join('|', $keys);
+ }
+ }
\ No newline at end of file
diff --git a/components/artboxtree/ArtboxTreeBehavior.php b/components/artboxtree/ArtboxTreeBehavior.php
new file mode 100755
index 0000000..b2248a2
--- /dev/null
+++ b/components/artboxtree/ArtboxTreeBehavior.php
@@ -0,0 +1,443 @@
+ $keyNameDepth;
+ public $primaryKeyMode = true;
+
+ /**
+ * @var string
+ */
+ public $delimiter = '|';
+
+ /**
+ * @var ActiveRecord|self|null
+ */
+ protected $entity;
+
+ /**
+ * @param ActiveRecord $owner
+ * @throws Exception
+ */
+ public function attach($owner)
+ {
+ parent::attach($owner);
+ if ($this->keyNameId === null) {
+ $primaryKey = $owner->primaryKey();
+ if (!isset($primaryKey[0])) {
+ throw new Exception('"' . $owner->className() . '" must have a primary key.');
+ }
+ $this->keyNameId = $primaryKey[0];
+ }
+ }
+
+ public function events()
+ {
+ return [
+ // @todo Use beforeSave for automatic set MP-params
+ ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeUpdate',
+ ActiveRecord::EVENT_AFTER_INSERT => 'afterInsert',
+ ];
+ }
+
+ /*
+ * Main methods
+ */
+
+ /*
+ * get one parent
+ * use AL-method
+ */
+ public function getParent() {
+ return $this->getParentAL();
+ }
+
+ /*
+ * get all parents
+ * use MP-method
+ */
+ public function getParents() {
+ return $this->getParentsMP();
+ }
+
+ /*
+ * get one-level children items
+ * use AL-method
+ */
+ public function getChildren() {
+ return $this->getChildrenAL();
+ }
+
+ /*
+ * get all-level children items
+ * use MP-method
+ */
+ public function getAllChildren($depth = null, $where = [], $with = null) {
+ return $this->getAllChildrenMP($depth, $where, $with);
+ }
+
+ /*
+ * get all-level children items
+ * use MP-method
+ */
+ public function getAllChildrenTree($depth = null, $where = [], $with = null) {
+ $query = $this->getAllChildrenMP($depth, $where, $with);
+ return $this->buildTree($query->all(), $this->owner->getAttribute($this->keyNameId));
+ }
+
+ // @todo Check algorytm
+ public function buildTree(array $data, $parentId = 0) {
+ $result = [];
+ foreach ($data as $key => $element) {
+ if ($element->getAttribute($this->keyNameParentId) == $parentId) {
+ unset($data[$key]);
+ $children = $this->buildTree($data, $element->getAttribute($this->keyNameId));
+ $result[] = [
+ 'item' => $element,
+ 'children' => $children
+ ];
+ }
+ }
+ return $result;
+ }
+
+
+ /*
+ * ================================
+ * MP-methods
+ * ================================
+ */
+
+ /*
+ * Full-path (use MP-method)
+ */
+ public function getParentsMP($depth = null) {
+ $tableName = $this->owner->tableName();
+ $path = $this->owner->getAttribute($this->keyNamePath);
+ $query = $this->owner->find()
+ ->andWhere(['<@', "{$tableName}.[[{$this->keyNamePath}]]", $path]);
+ if ($depth > 0) {
+ $query->andWhere(['>=', "{$tableName}.[[{$this->keyNameDepth}]]", $this->owner->getAttribute($this->keyNameDepth) - $depth]);
+ }
+ $query->andWhere(['<', "{$tableName}.[[{$this->keyNameDepth}]]", $this->owner->getAttribute($this->keyNameDepth)]);
+
+ $orderBy = [];
+ $orderBy["{$tableName}.[[{$this->keyNameDepth}]]"] = SORT_ASC;
+ $orderBy["{$tableName}.[[{$this->keyNameId}]]"] = SORT_ASC;
+
+ $query
+ ->andWhere($this->groupWhere())
+ ->addOrderBy($orderBy);
+ $query->multiple = true;
+
+ return $query;
+ }
+ /*public function getParentsMP($depth = null) {
+ $path = $this->getParentPath();
+ if ($path !== null) {
+ $paths = explode(',', trim($path, '{}'));
+ if (!$this->primaryKeyMode) {
+ $path = null;
+ $paths = array_map(
+ function ($value) use (&$path) {
+ return $path = ($path !== null ? $path . ',' : '') . $value;
+ },
+ $paths
+ );
+ }
+ if ($depth !== null) {
+ $paths = array_slice($paths, -$depth);
+ }
+ } else {
+ $paths = [];
+ }
+
+ $tableName = $this->owner->tableName();
+ if ($this->primaryKeyMode) {
+ $condition[] = ["{$tableName}.[[{$this->keyNameId}]]" => $paths];
+ } else {
+ $condition[] = ["{$tableName}.[[{$this->keyNamePath}]]" => $paths];
+ }
+
+ $query = $this->owner->find()
+ ->andWhere($condition)
+ ->andWhere($this->groupWhere())
+ ->addOrderBy(["{$tableName}.[[{$this->keyNamePath}]]" => SORT_ASC]);
+ $query->multiple = true;
+
+ return $query;
+ }*/
+
+ /**
+ * @param bool $asArray = false
+ * @return null|string|array
+ */
+ public function getParentPath($asArray = false)
+ {
+ return static::getParentPathInternal($this->owner->getAttribute($this->keyNamePath), $asArray);
+ }
+ /**
+ * @return array
+ */
+ protected function groupWhere()
+ {
+ $tableName = $this->owner->tableName();
+ if ($this->keyNameGroup === null) {
+ return [];
+ } else {
+ return ["{$tableName}.[[{$this->keyNameGroup}]]" => $this->owner->getAttribute($this->keyNameGroup)];
+ }
+ }
+
+
+ public function getAllChildrenMP($depth = null, $where = [], $with = null)
+ {
+ $tableName = $this->owner->tableName();
+ $path = $this->owner->getAttribute($this->keyNamePath);
+ $query = $this->owner->find()
+ ->andWhere(['@>', "{$tableName}.[[{$this->keyNamePath}]]", $path]);
+
+ if ($depth > 0) {
+ $query->andWhere(['<=', "{$tableName}.[[{$this->keyNameDepth}]]", $this->owner->getAttribute($this->keyNameDepth) + $depth]);
+ }
+
+ $orderBy = [];
+ $orderBy["{$tableName}.[[{$this->keyNameDepth}]]"] = SORT_ASC;
+ $orderBy["{$tableName}.[[{$this->keyNameId}]]"] = SORT_ASC;
+
+ if ($where) {
+ $query->andWhere($where);
+ }
+ if ($with) {
+ $query->with($with);
+ }
+
+ $query
+ ->andWhere($this->groupWhere())
+ ->addOrderBy($orderBy);
+ $query->multiple = true;
+
+ return $query;
+ }
+
+ /*
+ * ================================
+ * AL methods
+ * ================================
+ */
+
+ /*
+ * Parent entity (use AL-method)
+ * @return \yii\db\ActiveRecord
+ */
+ public function getParentAL() {
+ $parent_id = $this->owner->getAttribute($this->keyNameParentId);
+ if (empty($parent_id))
+ return null;
+
+ $where = [$this->keyNameId => $parent_id];
+ if ($this->keyNameGroup) {
+ $where[$this->keyNameGroup] = $this->owner->getAttribute($this->keyNameGroup);
+ }
+
+ return $this->owner->find()->where($where)->one();
+ }
+
+ /*
+ * Get parents by AL-method
+ * @return array
+ */
+ public function getParentsAL() {
+ $parent_id = $this->owner->getAttribute($this->keyNameParentId);
+ if ($parent_id == 0) {
+ return [];
+ }
+
+ $parent = $this->owner;
+ $parents = [];
+ while(true) {
+ $parent = $parent->getParentAL();
+ if (is_null($parent))
+ break;
+ $parents[] = $parent;
+ }
+
+ return array_reverse($parents);
+ }
+
+ /*
+ * Children entities (one-step) (use AL-method)
+ * @return ActiveQuery
+ */
+ public function getChildrenAL() {
+ $where = [$this->keyNameParentId => $this->owner->getAttribute($this->keyNameId)];
+ if ($this->keyNameGroup) {
+ $where[$this->keyNameGroup] = $this->owner->getAttribute($this->keyNameGroup);
+ }
+ return $this->owner->find()->where($where);
+ }
+
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ /**
+ * @param array $changedAttributes
+ * @throws Exception
+ */
+ protected function rebuildChildren($changedAttributes)
+ {
+ $path = isset($changedAttributes[$this->keyNamePath]) ? $changedAttributes[$this->keyNamePath] : $this->owner->getAttribute($this->keyNamePath);
+ $update = [];
+ $condition = [
+ 'and',
+ ['@>', "[[{$this->keyNamePath}]]", $path, false],
+ ];
+ if ($this->keyNameGroup !== null) {
+ $group = isset($changedAttributes[$this->keyNameGroup]) ? $changedAttributes[$this->keyNameGroup] : $this->owner->getAttribute($this->keyNameGroup);
+ $condition[] = [$this->keyNameGroup => $group];
+ }
+ $params = [];
+
+ if (isset($changedAttributes[$this->keyNamePath])) {
+ $substringExpr = $this->substringExpression(
+ "[[{$this->keyNamePath}]]",
+ 'array_length(:pathOld) + 1',
+ "array_length([[{$this->keyNamePath}]]) - array_length(:pathOld)"
+ );
+ $update[$this->keyNamePath] = new Expression($this->concatExpression([':pathNew', $substringExpr]));
+ $params[':pathOld'] = $path;
+ $params[':pathNew'] = $this->owner->getAttribute($this->keyNamePath);
+ }
+
+ if ($this->keyNameGroup !== null && isset($changedAttributes[$this->keyNameGroup])) {
+ $update[$this->keyNameGroup] = $this->owner->getAttribute($this->keyNameGroup);
+ }
+
+ if ($this->keyNameDepth !== null && isset($changedAttributes[$this->keyNameDepth])) {
+ $delta = $this->owner->getAttribute($this->keyNameDepth) - $changedAttributes[$this->keyNameDepth];
+ $update[$this->keyNameDepth] = new Expression("[[{$this->keyNameDepth}]]" . sprintf('%+d', $delta));
+ }
+ if (!empty($update)) {
+ $this->owner->updateAll($update, $condition, $params);
+ }
+ }
+
+ /**
+ * @param string $path
+ * @param string $delimiter
+ * @param bool $asArray = false
+ * @return null|string|array
+ */
+ protected static function getParentPathInternal($path, $asArray = false)
+ {
+ $path = explode(',', trim($path, '{}'));
+ array_pop($path);
+ if ($asArray) {
+ return $path;
+ }
+ return count($path) > 0 ? implode(',', $path) : null;
+ }
+
+ protected function toLike($path) {
+ return strtr($path . ',', ['%' => '\%', '_' => '\_', '\\' => '\\\\']) . '%';
+ }
+
+ protected function concatExpression($items)
+ {
+ if ($this->owner->getDb()->driverName === 'sqlite' || $this->owner->getDb()->driverName === 'pgsql') {
+ return implode(' || ', $items);
+ }
+ return 'CONCAT(' . implode(',', $items) . ')';
+ }
+
+ protected function substringExpression($string, $from, $length)
+ {
+ if ($this->owner->getDb()->driverName === 'sqlite') {
+ return "SUBSTR({$string}, {$from}, {$length})";
+ }
+ return "SUBSTRING({$string}, {$from}, {$length})";
+ }
+
+ // =======================================================
+ public function afterInsert() {
+ $this->withSave();
+ $this->owner->updateAttributes([$this->keyNamePath => $this->owner->getAttribute($this->keyNamePath), $this->keyNameDepth => $this->owner->getAttribute($this->keyNameDepth)]);
+ }
+
+ public function beforeUpdate()
+ {
+ if ($this->owner->getIsNewRecord()) {
+ throw new NotSupportedException('Method "' . $this->owner->className() . '::insert" is not supported for inserting new entitys.');
+ }
+ $this->withSave();
+ }
+
+ protected function withSave() {
+ $id = $this->owner->getAttribute($this->keyNameId);
+ $parent_id = $this->owner->getAttribute($this->keyNameParentId);
+
+ if (is_null($parent_id)) {
+ $parent_id = 0;
+ }
+
+ // check parent_id value is changed!
+ /*if ($this->owner->getOldAttribute($this->keyNameParentId) == $parent_id) {
+ return;
+ }*/
+
+ // rebuild parents entities
+ if ($parent_id == 0) {
+ $depth = 0;
+ $path = [intval($id)];
+ } else {
+ $parents = $this->getParentsAL();
+ $path = [];
+ $depth = 0;
+ foreach ($parents as $entity) {
+ $path[] = $entity->getAttribute($this->keyNameId);
+ $depth++;
+ }
+ $path[] = intval($id);
+ }
+
+ $path = '{'. implode(',', $path) .'}';
+
+ // rebuild children entities (recurcive)
+// $this->rebuildChildren([
+// $this->keyNamePath => $path
+// ]);
+
+ $this->owner->setAttribute($this->keyNamePath, $path);
+// $this->owner->setAttribute($this->keyNamePath, $path);
+ $this->owner->setAttribute($this->keyNameDepth, $depth);
+ }
+
+ public function recursiveRebuildChildren() {
+ $children = $this->getChildrenAL()->all();
+ $root_path = explode(',', $this->owner->getAttribute($this->keyNamePath));
+ $root_depth = $this->owner->getAttribute($this->keyNameDepth);
+
+ /** @var $child ActiveRecord */
+ foreach ($children as $child) {
+ $path = $root_path;
+ $path[] = $child->getAttribute($this->keyNameId);
+ $depth = $root_depth + 1;
+
+ $child->recursiveRebuildChildren();
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/artboxtree/ArtboxTreeHelper.php b/components/artboxtree/ArtboxTreeHelper.php
new file mode 100755
index 0000000..f1b2d96
--- /dev/null
+++ b/components/artboxtree/ArtboxTreeHelper.php
@@ -0,0 +1,47 @@
+depth+1) . $value;
+ $result[$key] = $row;
+ if (!empty($item['children'])) {
+ self::recursiveTreeMap($result, $item['children'], $from, $to, $symbol);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/artboxtree/ArtboxTreeQueryTrait.php b/components/artboxtree/ArtboxTreeQueryTrait.php
new file mode 100755
index 0000000..3440ea1
--- /dev/null
+++ b/components/artboxtree/ArtboxTreeQueryTrait.php
@@ -0,0 +1,100 @@
+modelClass;
+ self::$model = new $class;
+ }
+ return self::$model;
+ }
+
+ public function getTree($group = NULL, $with = NULL)
+ {
+ $model = $this->getModel();
+ if($group !== NULL) {
+ $this->andWhere([ $model->keyNameGroup => $group ]);
+ }
+ if($with) {
+ $this->with($with);
+ }
+ $data = $this->all();
+ if(empty( $data )) {
+ return [];
+ }
+
+ return $this->buildTree($data);
+ }
+
+ private function recursiveRebuild($tree, $parentPath = NULL, $depth = 0)
+ {
+ $model = $this->getModel();
+
+ foreach($tree as $row) {
+ $path = ( is_null($parentPath) ? '' : $parentPath . $model->delimiter ) . $row[ 'item' ]->getAttribute($model->keyNameId);
+ $row[ 'item' ]->setAttribute($model->keyNamePath, $path);
+ $row[ 'item' ]->setAttribute($model->keyNameDepth, $depth);
+ $row[ 'item' ]->save();
+ if(!empty( $row[ 'children' ] )) {
+ $this->recursiveRebuild($row[ 'children' ], $path, $depth + 1);
+ }
+ }
+ }
+
+ /**
+ * @param int $group
+ */
+ public function rebuildMP($group, $with = NULL)
+ {
+ $tree = $this->getTree($group, $with);
+
+ $this->recursiveRebuild($tree);
+ }
+
+ protected function buildTree(array $data, $parentId = 0)
+ {
+ $model = $this->getModel();
+
+ $result = [];
+ foreach($data as $element) {
+ if($element[ $model->keyNameParentId ] == $parentId) {
+ $children = $this->buildTree($data, $element[ $model->keyNameId ]);
+ $result[] = [
+ 'item' => $element,
+ 'children' => $children,
+ ];
+ }
+ }
+ return $result;
+ }
+
+ public function normalizeTreeData(array $data, $parentId = NULL)
+ {
+ $model = $this->getModel();
+
+ $result = [];
+ foreach($data as $element) {
+ if($element[ $model->keyNameParentId ] == $parentId) {
+ $result[] = $element;
+ $children = $this->normalizeTreeData($data, $element[ $model->keyNameId ]);
+ if($children) {
+ $result = array_merge($result, $children);
+ }
+ }
+ }
+ return $result;
+ }
+ }
\ No newline at end of file
diff --git a/components/artboxtree/ArtboxTreeWidget.php b/components/artboxtree/ArtboxTreeWidget.php
new file mode 100755
index 0000000..d24ed76
--- /dev/null
+++ b/components/artboxtree/ArtboxTreeWidget.php
@@ -0,0 +1,138 @@
+dataProvider === null) {
+ throw new InvalidConfigException('The "dataProvider" property must be set.');
+ }
+ if ($this->keyNameId === null) {
+ throw new InvalidConfigException('The "keyNameId" property must be set.');
+ }
+ if ($this->formatter == null) {
+ $this->formatter = Yii::$app->getFormatter();
+ } elseif (is_array($this->formatter)) {
+ $this->formatter = Yii::createObject($this->formatter);
+ }
+ if (!$this->formatter instanceof Formatter) {
+ throw new InvalidConfigException('The "formatter" property must be either a Format object or a configuration array.');
+ }
+ }
+
+ /**
+ * Runs the widget.
+ */
+ public function run()
+ {
+ if (!empty($this->assetBundle) && class_exists($this->assetBundle)) {
+ $view = $this->getView();
+ $assetBundle = $this->assetBundle;
+ $assetBundle::register($view);
+ }
+ if ($this->dataProvider->getCount() == 0) {
+ return $this->renderEmptyResult();
+ }
+
+ parent::run();
+ }
+
+ protected function renderEmptyResult() {
+ return empty($this->emptyResult) ? Yii::t('artbox', 'TreeViewEmptyResult') : Yii::t('artbox', $this->emptyResult);
+ }
+
+ /**
+ * Normalize tree data
+ * @param array $data
+ * @param string $parentId
+ * @return array
+ */
+ protected function normalizeTreeData(array $data, $parentId = null) {
+ $result = [];
+ foreach ($data as $element) {
+ if ($element[$this->keyNameParentId] == $parentId) {
+ $result[] = $element;
+ $children = $this->normalizeTreeData($data, $element[$this->keyNameId]);
+ if ($children) {
+ $result = array_merge($result, $children);
+ }
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Hierarchy tree data
+ * @param array $data
+ * @param string $parentId
+ * @return array
+ */
+ protected function hierarchyTreeData(array $data, $parentId = null) {
+ $result = [];
+ foreach ($data as $element) {
+ if ($element[$this->keyNameParentId] == $parentId) {
+ $children = $this->hierarchyTreeData($data, $element[$this->keyNameId]);
+ $result[] = [
+ 'item' => $element,
+ 'children' => $children
+ ];
+ }
+ }
+ return $result;
+ }
+}
\ No newline at end of file
diff --git a/components/artboxtree/treegrid/TreeGridColumn.php b/components/artboxtree/treegrid/TreeGridColumn.php
new file mode 100755
index 0000000..5474766
--- /dev/null
+++ b/components/artboxtree/treegrid/TreeGridColumn.php
@@ -0,0 +1,250 @@
+
+ */
+class TreeGridColumn extends Object {
+
+ /**
+ * @var TreeGrid the grid view object that owns this column.
+ */
+ public $grid;
+
+ /**
+ * @var string the header cell content. Note that it will not be HTML-encoded.
+ */
+ public $header;
+
+ /**
+ * @var string the footer cell content. Note that it will not be HTML-encoded.
+ */
+ public $footer;
+
+ /**
+ * @var callable This is a callable that will be used to generate the content of each cell.
+ * The signature of the function should be the following: `function ($model, $key, $index, $column)`.
+ * Where `$model`, `$key`, and `$index` refer to the model, key and index of the row currently being rendered
+ * and `$column` is a reference to the [[TreeColumn]] object.
+ */
+ public $content;
+
+ /**
+ * @var boolean whether this column is visible. Defaults to true.
+ */
+ public $visible = true;
+
+ /**
+ * @var array the HTML attributes for the column group tag.
+ * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
+ */
+ public $options = [];
+
+ /**
+ * @var array the HTML attributes for the header cell tag.
+ * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
+ */
+ public $headerOptions = [];
+
+ /**
+ * @var array|\Closure the HTML attributes for the data cell tag. This can either be an array of
+ * attributes or an anonymous function ([[Closure]]) that returns such an array.
+ * The signature of the function should be the following: `function ($model, $key, $index, $column)`.
+ * Where `$model`, `$key`, and `$index` refer to the model, key and index of the row currently being rendered
+ * and `$column` is a reference to the [[Column]] object.
+ * A function may be used to assign different attributes to different rows based on the data in that row.
+ *
+ * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
+ */
+ public $contentOptions = [];
+
+ /**
+ * @var array the HTML attributes for the footer cell tag.
+ * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
+ */
+ public $footerOptions = [];
+
+ /**
+ * @var string the attribute name associated with this column. When neither [[content]] nor [[value]]
+ * is specified, the value of the specified attribute will be retrieved from each data model and displayed.
+ *
+ * Also, if [[label]] is not specified, the label associated with the attribute will be displayed.
+ */
+ public $attribute;
+
+ /**
+ * @var string label to be displayed in the [[header|header cell]] and also to be used as the sorting
+ * link label when sorting is enabled for this column.
+ * If it is not set and the models provided by the GridViews data provider are instances
+ * of [[\yii\db\ActiveRecord]], the label will be determined using [[\yii\db\ActiveRecord::getAttributeLabel()]].
+ * Otherwise [[\yii\helpers\Inflector::camel2words()]] will be used to get a label.
+ */
+ public $label;
+
+ /**
+ * @var boolean whether the header label should be HTML-encoded.
+ * @see label
+ */
+ public $encodeLabel = true;
+
+ /**
+ * @var string|\Closure an anonymous function or a string that is used to determine the value to display in the current column.
+ *
+ * If this is an anonymous function, it will be called for each row and the return value will be used as the value to
+ * display for every data model. The signature of this function should be: `function ($model, $key, $index, $column)`.
+ * Where `$model`, `$key`, and `$index` refer to the model, key and index of the row currently being rendered
+ * and `$column` is a reference to the [[DataColumn]] object.
+ *
+ * You may also set this property to a string representing the attribute name to be displayed in this column.
+ * This can be used when the attribute to be displayed is different from the [[attribute]] that is used for
+ * sorting and filtering.
+ *
+ * If this is not set, `$model[$attribute]` will be used to obtain the value, where `$attribute` is the value of [[attribute]].
+ */
+ public $value;
+
+ /**
+ * @var string|array in which format should the value of each data model be displayed as (e.g. `"raw"`, `"text"`, `"html"`,
+ * `['date', 'php:Y-m-d']`). Supported formats are determined by the [[GridView::formatter|formatter]] used by
+ * the [[GridView]]. Default format is "text" which will format the value as an HTML-encoded plain text when
+ * [[\yii\i18n\Formatter]] is used as the [[GridView::$formatter|formatter]] of the GridView.
+ */
+ public $format = 'text';
+
+ /**
+ * Renders the header cell.
+ */
+ public function renderHeaderCell()
+ {
+ return Html::tag('th', $this->renderHeaderCellContent(), $this->headerOptions);
+ }
+
+ /**
+ * Renders the footer cell.
+ */
+ public function renderFooterCell()
+ {
+ return Html::tag('td', $this->renderFooterCellContent(), $this->footerOptions);
+ }
+
+ /**
+ * Renders a data cell.
+ * @param mixed $model the data model being rendered
+ * @param mixed $key the key associated with the data model
+ * @param integer $index the zero-based index of the data item among the item array returned by [[GridView::dataProvider]].
+ * @return string the rendering result
+ */
+ public function renderDataCell($model, $key, $index, $is_first = false, $symbol = '–')
+ {
+ if ($this->contentOptions instanceof Closure) {
+ $options = call_user_func($this->contentOptions, $model, $key, $index, $this);
+ } else {
+ $options = $this->contentOptions;
+ }
+ return Html::tag('td', ($is_first ? str_repeat($symbol, $model->depth) : '') . $this->renderDataCellContent($model, $key, $index), $options);
+ }
+
+ /**
+ * Renders the header cell content.
+ * The default implementation simply renders [[header]].
+ * This method may be overridden to customize the rendering of the header cell.
+ * @return string the rendering result
+ */
+ protected function renderHeaderCellContent()
+ {
+ if ($this->header !== null || $this->label === null && $this->attribute === null) {
+ return trim($this->header) !== '' ? $this->header : $this->grid->emptyCell;
+ }
+
+ $provider = $this->grid->dataProvider;
+
+ if ($this->label === null) {
+ if ($provider instanceof ActiveDataProvider && $provider->query instanceof ActiveQueryInterface) {
+ /* @var $model Model */
+ $model = new $provider->query->modelClass;
+ $label = $model->getAttributeLabel($this->attribute);
+ } else {
+ $models = $provider->getModels();
+ if (($model = reset($models)) instanceof Model) {
+ /* @var $model Model */
+ $label = $model->getAttributeLabel($this->attribute);
+ } else {
+ $label = Inflector::camel2words($this->attribute);
+ }
+ }
+ } else {
+ $label = $this->label;
+ }
+
+ return $this->encodeLabel ? Html::encode($label) : $label;
+ }
+
+ /**
+ * Renders the footer cell content.
+ * The default implementation simply renders [[footer]].
+ * This method may be overridden to customize the rendering of the footer cell.
+ * @return string the rendering result
+ */
+ protected function renderFooterCellContent()
+ {
+ return trim($this->footer) !== '' ? $this->footer : $this->grid->emptyCell;
+ }
+
+ /**
+ * Renders the data cell content.
+ * @param mixed $model the data model
+ * @param mixed $key the key associated with the data model
+ * @param integer $index the zero-based index of the data model among the models array returned by [[GridView::dataProvider]].
+ * @return string the rendering result
+ */
+ protected function renderDataCellContent($model, $key, $index)
+ {
+ if ($this->content === null) {
+ return $this->grid->formatter->format($this->getDataCellValue($model, $key, $index), $this->format);
+ } else {
+ if ($this->content !== null) {
+ return call_user_func($this->content, $model, $key, $index, $this);
+ } else {
+ return $this->grid->emptyCell;
+ }
+ }
+
+
+ }
+
+ /**
+ * Returns the data cell value.
+ * @param mixed $model the data model
+ * @param mixed $key the key associated with the data model
+ * @param integer $index the zero-based index of the data model among the models array returned by [[GridView::dataProvider]].
+ * @return string the data cell value
+ */
+ public function getDataCellValue($model, $key, $index)
+ {
+ if ($this->value !== null) {
+ if (is_string($this->value)) {
+ return ArrayHelper::getValue($model, $this->value);
+ } else {
+ return call_user_func($this->value, $model, $key, $index, $this);
+ }
+ } elseif ($this->attribute !== null) {
+ return ArrayHelper::getValue($model, $this->attribute);
+ }
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/components/artboxtree/treegrid/TreeGridWidget.php b/components/artboxtree/treegrid/TreeGridWidget.php
new file mode 100755
index 0000000..9996437
--- /dev/null
+++ b/components/artboxtree/treegrid/TreeGridWidget.php
@@ -0,0 +1,277 @@
+ 'table table-striped table-bordered'];
+
+ /**
+ * @var array The plugin options
+ */
+ public $pluginOptions = [];
+
+ /**
+ * @var boolean whether to show the grid view if [[dataProvider]] returns no data.
+ */
+ public $showOnEmpty = true;
+
+ public $rowOptions = [];
+
+ /**
+ * @var Closure an anonymous function that is called once BEFORE rendering each data model.
+ * It should have the similar signature as [[rowOptions]]. The return result of the function
+ * will be rendered directly.
+ */
+ public $beforeRow;
+
+ /**
+ * @var Closure an anonymous function that is called once AFTER rendering each data model.
+ * It should have the similar signature as [[rowOptions]]. The return result of the function
+ * will be rendered directly.
+ */
+ public $afterRow;
+
+ /**
+ * @var boolean whether to show the header section of the grid table.
+ */
+ public $showHeader = true;
+
+ /**
+ * @var array the HTML attributes for the table header row.
+ * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
+ */
+ public $headerRowOptions = [];
+
+ /**
+ * @var boolean whether to show the footer section of the grid table.
+ */
+ public $showFooter = false;
+
+ /**
+ * @var string the HTML display when the content of a cell is empty
+ */
+ public $emptyCell = ' ';
+
+ public $levelSymbol = '–';
+
+ /**
+ * Init the widget object.
+ */
+ public function init() {
+ parent::init();
+
+ $this->initColumns();
+ }
+
+ /**
+ * Runs the widget.
+ */
+ public function run() {
+ $run = parent::run();
+ if (!is_null($run))
+ return $run;
+
+ if ($this->showOnEmpty || $this->dataProvider->getCount() > 0) {
+ $pagination = $this->dataProvider->getPagination();
+ $pagination->setPageSize($this->dataProvider->getTotalCount());
+
+ $header = $this->showHeader ? $this->renderTableHeader() : false;
+ $body = $this->renderItems();
+ $footer = $this->showFooter ? $this->renderTableFooter() : false;
+
+ $content = array_filter([
+ $header,
+ $body,
+ $footer
+ ]);
+
+ return Html::tag('table', implode("\n", $content), $this->options);
+ } else {
+ return $this->renderEmptyResult();
+ }
+ }
+
+ /**
+ * Renders the table header.
+ * @return string the rendering result.
+ */
+ public function renderTableHeader()
+ {
+ $cells = [];
+ foreach ($this->columns as $column) {
+ /* @var $column TreeGridColumn */
+ $cells[] = $column->renderHeaderCell();
+ }
+ $content = Html::tag('tr', implode('', $cells), $this->headerRowOptions);
+ return "\n" . $content . "\n";
+ }
+
+ /**
+ * Renders the table footer.
+ * @return string the rendering result.
+ */
+ public function renderTableFooter()
+ {
+ $cells = [];
+ foreach ($this->columns as $column) {
+ /* @var $column TreeGridColumn */
+ $cells[] = $column->renderFooterCell();
+ }
+ $content = Html::tag('tr', implode('', $cells), $this->footerRowOptions);
+ return "
\n" . $content . "\n";
+ }
+
+ /**
+ * Renders the data models for the grid view.
+ */
+ public function renderItems()
+ {
+ $rows = [];
+ $models = array_values($this->dataProvider->getModels());
+ $keys = $this->dataProvider->getKeys();
+ $models = TaxOption::find()->normalizeTreeData($models, $this->rootParentId);
+ foreach ($models as $index => $model) {
+ $key = $keys[$index];
+ if ($this->beforeRow !== null) {
+ $row = call_user_func($this->beforeRow, $model, $key, $index, $this);
+ if (!empty($row)) {
+ $rows[] = $row;
+ }
+ }
+
+ $rows[] = $this->renderTableRow($model, $key, $index);
+
+ if ($this->afterRow !== null) {
+ $row = call_user_func($this->afterRow, $model, $key, $index, $this);
+ if (!empty($row)) {
+ $rows[] = $row;
+ }
+ }
+ }
+
+ if (empty($rows)) {
+ $colspan = count($this->columns);
+ return "" . $this->renderEmpty() . " |
";
+ } else {
+ return implode("\n", $rows);
+ }
+ }
+
+ /**
+ * Renders a table row with the given data model and key.
+ * @param mixed $model the data model to be rendered
+ * @param mixed $key the key associated with the data model
+ * @param integer $index the zero-based index of the data model among the model array returned by [[dataProvider]].
+ * @return string the rendering result
+ */
+ public function renderTableRow($model, $key, $index)
+ {
+ $cells = [];
+ /* @var $column TreeGridColumn */
+ $i = 0;
+ foreach ($this->columns as $column) {
+ $cells[] = $column->renderDataCell($model, $key, $index, $i == 0, $this->levelSymbol);
+ $i++;
+ }
+ if ($this->rowOptions instanceof Closure) {
+ $options = call_user_func($this->rowOptions, $model, $key, $index, $this);
+ } else {
+ $options = $this->rowOptions;
+ }
+ $options['data-key'] = is_array($key) ? json_encode($key) : (string) $key;
+
+ $id = ArrayHelper::getValue($model, $this->keyNameId);
+ Html::addCssClass($options, "treegrid-$id");
+
+ $parentId = ArrayHelper::getValue($model, $this->keyNameParentId);
+ if ($parentId) {
+ Html::addCssClass($options, "treegrid-parent-$parentId");
+ }
+
+ return Html::tag('tr', implode('', $cells), $options);
+ }
+
+ /**
+ * Creates column objects and initializes them.
+ */
+ protected function initColumns()
+ {
+ if (empty($this->columns)) {
+ $this->guessColumns();
+ }
+ foreach ($this->columns as $i => $column) {
+ if (is_string($column)) {
+ $column = $this->createDataColumn($column);
+ } else {
+ $column = Yii::createObject(array_merge([
+ 'class' => $this->dataColumnClass ? : TreeGridColumn::className(),
+ 'grid' => $this,
+ ], $column));
+ }
+ if (!$column->visible) {
+ unset($this->columns[$i]);
+ continue;
+ }
+ $this->columns[$i] = $column;
+ }
+ }
+
+ /**
+ * Creates a [[DataColumn]] object based on a string in the format of "attribute:format:label".
+ * @param string $text the column specification string
+ * @return DataColumn the column instance
+ * @throws InvalidConfigException if the column specification is invalid
+ */
+ protected function createDataColumn($text)
+ {
+ if (!preg_match('/^([^:]+)(:(\w*))?(:(.*))?$/', $text, $matches)) {
+ throw new InvalidConfigException('The column must be specified in the format of "attribute", "attribute:format" or "attribute:format:label"');
+ }
+
+ return Yii::createObject([
+ 'class' => $this->dataColumnClass ? : TreeGridColumn::className(),
+ 'grid' => $this,
+ 'attribute' => $matches[1],
+ 'format' => isset($matches[3]) ? $matches[3] : 'text',
+ 'label' => isset($matches[5]) ? $matches[5] : null,
+ ]);
+ }
+
+ /**
+ * This function tries to guess the columns to show from the given data
+ * if [[columns]] are not explicitly specified.
+ */
+ protected function guessColumns()
+ {
+ $models = $this->dataProvider->getModels();
+ $model = reset($models);
+ if (is_array($model) || is_object($model)) {
+ foreach ($model as $name => $value) {
+ $this->columns[] = $name;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/artboxtree/treelist/TreeListWidget.php b/components/artboxtree/treelist/TreeListWidget.php
new file mode 100755
index 0000000..a752c2c
--- /dev/null
+++ b/components/artboxtree/treelist/TreeListWidget.php
@@ -0,0 +1,62 @@
+hierarchyTreeData(array_values($this->dataProvider->getModels()), $this->rootParentId);
+ return $this->renderTreelist($models);
+ }
+
+ protected function renderTreelist($models) {
+ foreach ($models as $index => $model) {
+ $row = $this->renderTreelistItem($model['item']);
+ $children = empty($model['children']) ? '' : $this->renderTreelist($model['children']);
+ $output[] = ''. $row . $children .'';
+ }
+
+ if (!empty($output))
+ return ''. implode("\n", $output) .'
';
+ }
+
+ protected function renderTreelistItem($model)
+ {
+ $options = [];
+ $id = ArrayHelper::getValue($model, $this->keyNameId);
+ Html::addCssClass($options, "treelistitem-$id");
+
+ $parent_id = ArrayHelper::getValue($model, $this->keyNameParentId);
+ if ($parent_id) {
+ Html::addCssClass($options, "treelistitem-parent-$parent_id");
+ }
+
+// if (is_string($this->value)) {
+// return ArrayHelper::getValue($model, $this->value);
+// } else {
+// return call_user_func($this->value, $model, $key, $index, $this);
+// }
+
+ return Html::tag('span', ArrayHelper::getValue($model, $this->displayField), $options);
+ }
+}
\ No newline at end of file
diff --git a/components/artboxtree/treemenu/TreeMenuWidget.php b/components/artboxtree/treemenu/TreeMenuWidget.php
new file mode 100755
index 0000000..f654293
--- /dev/null
+++ b/components/artboxtree/treemenu/TreeMenuWidget.php
@@ -0,0 +1,62 @@
+hierarchyTreeData(array_values($this->dataProvider->getModels()), $this->rootParentId);
+ return $this->renderTreelist($models);
+ }
+
+ protected function renderTreelist($models) {
+ foreach ($models as $index => $model) {
+ $row = $this->renderTreelistItem($model['item']);
+ $children = empty($model['children']) ? '' : $this->renderTreelist($model['children']);
+ $output[] = ''. $row . $children .'';
+ }
+
+ if (!empty($output))
+ return ''. implode("\n", $output) .'
';
+ }
+
+ protected function renderTreelistItem($model)
+ {
+ $options = [];
+ $id = ArrayHelper::getValue($model, $this->keyNameId);
+ Html::addCssClass($options, "treelistitem-$id");
+
+ $parent_id = ArrayHelper::getValue($model, $this->keyNameParentId);
+ if ($parent_id) {
+ Html::addCssClass($options, "treelistitem-parent-$parent_id");
+ }
+
+// if (is_string($this->value)) {
+// return ArrayHelper::getValue($model, $this->value);
+// } else {
+// return call_user_func($this->value, $model, $key, $index, $this);
+// }
+
+ return Html::tag('span', ArrayHelper::getValue($model, $this->displayField), $options);
+ }
+}
\ No newline at end of file
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..d7ce866
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,15 @@
+{
+ "name": "artweb/artbox",
+ "description": "Yii2 light-weight CMS",
+ "license": "BSD-3-Clause",
+ "require": {
+ "php": ">=7.0",
+ "yiisoft/yii2": "*",
+ "developeruz/yii2-db-rbac": "*"
+ },
+ "autoload": {
+ "psr-4": {
+ "artweb\\artbox\\": ""
+ }
+ }
+}
\ No newline at end of file
diff --git a/console/GenerateController.php b/console/GenerateController.php
new file mode 100644
index 0000000..7217bfe
--- /dev/null
+++ b/console/GenerateController.php
@@ -0,0 +1,285 @@
+ 'ru_RU',
+ 3 => 'uk_UA',
+ ];
+
+ /**
+ * Faker Generators instances
+ *
+ * @var Generator[] $fakers
+ */
+ private $fakers = [];
+
+ /**
+ * Create faker Generators for all $locales
+ *
+ * @param \yii\base\Action $action
+ *
+ * @return bool
+ */
+ public function beforeAction($action)
+ {
+ $parent = parent::beforeAction($action);
+ $fakers = [];
+ foreach ($this->locales as $locale_id => $locale) {
+ $fakers[ $locale_id ] = Factory::create($locale);
+ }
+ $this->fakers = $fakers;
+ return $parent;
+ }
+
+ /**
+ * Generate fake Brands
+ *
+ * @param int $count Brands count
+ *
+ * @return int
+ */
+ public function actionBrand(int $count = 10): int
+ {
+ /**
+ * @var Brand[] $models
+ */
+ $models = [];
+ $fakers = $this->fakers;
+ for ($i = 0; $i < $count; $i++) {
+ $models[ $i ] = \Yii::createObject(Brand::className());
+ $models[ $i ]->generateLangs();
+ foreach ($models[ $i ]->modelLangs as $language_id => $modelLang) {
+ $modelLang->title = $fakers[ $language_id ]->company;
+ }
+ if ($models[ $i ]->save() && $models[ $i ]->transactionStatus) {
+ $title = $this->ansiFormat($models[ $i ]->modelLangs[ 2 ]->title, Console::FG_YELLOW);
+ $id = $this->ansiFormat($models[ $i ]->id, Console::FG_YELLOW);
+ echo "Brand '$title' inserted under $id ID.\n";
+ };
+ }
+ return 0;
+ }
+
+ /**
+ * Generate fake categories
+ *
+ * @param int $count Category count
+ *
+ * @return int
+ */
+ public function actionCategory(int $count = 10):int
+ {
+ /**
+ * @var Category[] $models
+ */
+ $models = [];
+ $fakers = $this->fakers;
+ for ($i = 0; $i < $count; $i++) {
+ $models[ $i ] = \Yii::createObject(
+ [
+ 'class' => Category::className(),
+ 'depth' => 0,
+ 'parent_id' => 0,
+ ]
+ );
+ $models[ $i ]->generateLangs();
+ foreach ($models[ $i ]->modelLangs as $language_id => $modelLang) {
+ $modelLang->title = ucfirst($fakers[ $language_id ]->word);
+ }
+ if ($models[ $i ]->save() && $models[ $i ]->transactionStatus) {
+ $title = $this->ansiFormat($models[ $i ]->modelLangs[ 2 ]->title, Console::FG_YELLOW);
+ $id = $this->ansiFormat($models[ $i ]->id, Console::FG_YELLOW);
+ echo "Category '$title' inserted under $id ID.\n";
+ };
+ }
+ return 0;
+ }
+
+ /**
+ * Generate fake products with variants, categories and tax options
+ *
+ * @param int $count Product count
+ *
+ * @return int
+ */
+ public function actionProduct(int $count = 10):int
+ {
+ /**
+ * @var Product[] $models
+ */
+ $models = [];
+ $fakers = $this->fakers;
+ $brands = Brand::find()
+ ->limit(20)
+ ->asArray()
+ ->column();
+ $categories = Category::find()
+ ->limit(100)
+ ->asArray()
+ ->column();
+ $product_options = TaxOption::find()
+ ->joinWith('taxGroup')
+ ->where([ 'tax_group.level' => TaxGroup::GROUP_PRODUCT ])
+ ->limit(50)
+ ->asArray()
+ ->column();
+ $variant_options = TaxOption::find()
+ ->joinWith('taxGroup')
+ ->where([ 'tax_group.level' => TaxGroup::GROUP_VARIANT ])
+ ->limit(50)
+ ->asArray()
+ ->column();
+ for ($i = 0; $i < $count; $i++) {
+ $models[ $i ] = \Yii::createObject(
+ [
+ 'class' => Product::className(),
+ 'brand_id' => $fakers[ 2 ]->randomElement($brands),
+ ]
+ );
+ $models[ $i ]->setCategories(
+ $fakers[ 2 ]->randomElements($categories, $fakers[ 2 ]->randomDigitNotNull)
+ );
+ $models[ $i ]->setOptions(
+ $fakers[ 2 ]->randomElements($product_options, $fakers[ 2 ]->randomDigitNotNull)
+ );
+ $models[ $i ]->generateLangs();
+ foreach ($models[ $i ]->modelLangs as $language_id => $modelLang) {
+ $modelLang->title = ucfirst($fakers[ $language_id ]->word);
+ }
+ if ($models[ $i ]->save() && $models[ $i ]->transactionStatus) {
+ $title = $this->ansiFormat($models[ $i ]->modelLangs[ 2 ]->title, Console::FG_YELLOW);
+ $id = $this->ansiFormat($models[ $i ]->id, Console::FG_YELLOW);
+ echo "Product '$title' inserted under $id ID.\n";
+ $variant_count = $fakers[ 2 ]->numberBetween(4, 10);
+ for ($j = 0; $j < $variant_count; $j++) {
+ /**
+ * @var ProductVariant $variant
+ */
+ $variant = \Yii::createObject(
+ [
+ 'class' => ProductVariant::className(),
+ 'product_id' => $models[ $i ]->id,
+ 'sku' => $fakers[ 2 ]->uuid,
+ 'price' => $fakers[ 2 ]->randomFloat(2, 100, 10000),
+ 'stock' => 10,
+ 'product_unit_id' => 1,
+ ]
+ );
+ $variant->setOptions(
+ $fakers[ 2 ]->randomElements($variant_options, $fakers[ 2 ]->randomDigitNotNull)
+ );
+ $variant->generateLangs();
+ foreach ($variant->modelLangs as $variant_language_id => $variantLang) {
+ $variantLang->title = ucfirst($fakers[ $variant_language_id ]->word);
+ }
+ if ($variant->save() && $variant->transactionStatus) {
+ $variant_title = $this->ansiFormat($variant->modelLangs[ 2 ]->title, Console::FG_YELLOW);
+ $variant_id = $this->ansiFormat($variant->id, Console::FG_YELLOW);
+ echo "Variant '$variant_title' inserted under $variant_id ID for product $title.\n";
+ }
+ }
+ };
+ }
+ return 0;
+ }
+
+ /**
+ * Generate fake tax groups with tax options.
+ *
+ * @param int $count_product Tax Groups for Product
+ * @param int $count_variant Tax Groups for ProductVariant
+ *
+ * @return int
+ */
+ public function actionTax(int $count_product = 10, int $count_variant = 10):int
+ {
+ $categories = Category::find()
+ ->asArray()
+ ->column();
+ $this->generateTax(TaxGroup::GROUP_PRODUCT, $count_product, $categories);
+ $this->generateTax(TaxGroup::GROUP_VARIANT, $count_variant, $categories);
+ return 0;
+ }
+
+ /**
+ * Generates tax groups amd tax options for actionTax
+ *
+ * @param int $level Tax Group for Product or ProductVariant
+ * @param int $count Tax Group count
+ * @param array $categories Categories for Tax Groups
+ *
+ * @see GenerateController::actionTax()
+ */
+ private function generateTax(int $level, int $count, array $categories = [])
+ {
+ $count_option = 10;
+ $fakers = $this->fakers;
+ /**
+ * @var TaxGroup[] $models
+ */
+ $models = [];
+ for ($i = 0; $i < $count; $i++) {
+ $models[ $i ] = \Yii::createObject(
+ [
+ 'class' => TaxGroup::className(),
+ 'is_filter' => true,
+ 'display' => true,
+ ]
+ );
+ $models[ $i ]->level = $level;
+ $models[ $i ]->setCategories($categories);
+ $models[ $i ]->generateLangs();
+ foreach ($models[ $i ]->modelLangs as $language_id => $modelLang) {
+ $modelLang->title = ucfirst($fakers[ $language_id ]->word);
+ }
+ if ($models[ $i ]->save() && $models[ $i ]->transactionStatus) {
+ for ($j = 0; $j < $count_option; $j++) {
+ /**
+ * @var TaxOption $option
+ */
+ $option = \Yii::createObject(
+ TaxOption::className()
+ );
+ $option->tax_group_id = $models[ $i ]->id;
+ $option->generateLangs();
+ foreach ($option->modelLangs as $option_language_id => $taxOptionLang) {
+ $taxOptionLang->value = ucfirst($fakers[ $option_language_id ]->word);
+ }
+ $option->save();
+ }
+ $title = $this->ansiFormat($models[ $i ]->modelLangs[ 2 ]->title, Console::FG_YELLOW);
+ $id = $this->ansiFormat($models[ $i ]->id, Console::FG_YELLOW);
+ $element = $this->ansiFormat(
+ ( $level === TaxGroup::GROUP_PRODUCT ) ? 'Product' : 'Variant',
+ Console::FG_RED
+ );
+ echo "Category '$title' inserted for $element under $id ID.\n";
+ };
+ }
+ }
+ }
+
\ No newline at end of file
diff --git a/console/ImportController.php b/console/ImportController.php
new file mode 100755
index 0000000..445a60c
--- /dev/null
+++ b/console/ImportController.php
@@ -0,0 +1,57 @@
+stderr('Task already executed');
+ return Controller::EXIT_CODE_ERROR;
+ }
+ return fopen ($filename, 'r');
+ }
+
+ public function actionProducts() {
+// if (file_exists(Yii::getAlias('@uploadDir/goProducts.lock'))) {
+// $this->errors[] = 'Task already executed';
+// return Controller::EXIT_CODE_ERROR;
+// }
+// $ff = fopen(Yii::getAlias('@uploadDir/goProducts.lock'), 'w+');
+// fclose($ff);
+ $model = new Import();
+ $model->goProducts(0, null);
+// unlink(Yii::getAlias('@uploadDir/goProducts.lock'));
+ return Controller::EXIT_CODE_NORMAL;
+ }
+
+ public function actionPrices() {
+ if (file_exists(Yii::getAlias('@uploadDir/goPrices.lock'))) {
+ $this->stderr('Task already executed');
+ return Controller::EXIT_CODE_ERROR;
+ }
+ $ff = fopen(Yii::getAlias('@uploadDir/goPrices.lock'), 'w+');
+ fclose($ff);
+ $model = new Import();
+ $data = $model->goPrices(0, null);
+ unlink(Yii::getAlias('@uploadDir/goPrices.lock'));
+ return Controller::EXIT_CODE_NORMAL;
+ }
+
+ private function saveNotFoundRecord (array $line, $filename)
+ {
+ $str = implode (';', $line)."\n";
+ $str = iconv ("UTF-8//TRANSLIT//IGNORE", "windows-1251", $str);
+
+ $fg = fopen (Yii::getAlias('@uploadDir') .'/'. $filename, 'a+');
+ fputs ($fg, $str);
+ fclose ($fg);
+ }
+}
\ No newline at end of file
diff --git a/console/SiteMapController.php b/console/SiteMapController.php
new file mode 100755
index 0000000..842c2f1
--- /dev/null
+++ b/console/SiteMapController.php
@@ -0,0 +1,220 @@
+search($category, $filter);
+ if(!empty($productProvider->models)){
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+
+
+ public function getAddStatic(){
+ return [
+ 'http://www.rukzachok.com.ua',
+ 'http://www.rukzachok.com.ua/catalog'
+ ];
+ }
+
+
+ public function getProducts() {
+ return Product::find()->all();
+
+ }
+
+
+ public function getSeoLinks() {
+ return Seo::find()->where(['meta' => ''])->all();
+
+ }
+
+ public function getStaticPages(){
+ return Page::find()->all();
+ }
+
+
+ public function getCategories(){
+ return Category::find()->all();
+ }
+
+
+ public function getArticles(){
+ return Article::find()->all();
+ }
+
+ public function getBrands($category){
+
+ return $category->brands;
+ }
+
+ /**
+ * @param $category Category;
+ * @return mixed
+ */
+
+ public function getFilters($category){
+
+ return $category->getActiveFilters()->all();
+
+ }
+
+
+ public function checkUrl($url){
+ if(!in_array($url, $this->urlList)){
+ $this->urlList[] = $url;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+
+ public function createRow( $url, $priority, &$content ){
+ if($this->checkUrl($url)){
+ print $this->count++ . "\n";
+ $content .= '' .
+ '' . $url . '' .
+ '' . date('Y-m-d') . '' .
+ 'Daily' .
+ '' . $priority .'' .
+ '';
+ }
+ }
+
+
+ public function actionProcess() {
+
+ $config = ArrayHelper::merge(
+ require(__DIR__ . '/../../frontend/config/main.php'),
+ require(__DIR__ . '/../../common/config/main.php')
+
+ );
+
+ Yii::$app->urlManager->addRules($config['components']['urlManager']['rules']);
+
+
+
+ $dirName = Yii::getAlias('@frontend').'/web';
+
+ $filename = 'sitemap.xml';
+
+ setlocale(LC_ALL, 'ru_RU.CP1251');
+ $handle = fopen($dirName .'/'. $filename, "w");
+
+ $content = '';
+
+ foreach ($this->getAddStatic() as $page) {
+ $this->createRow($page , 1,$content);
+ }
+
+ foreach ($this->getStaticPages() as $page) {
+ $url = Url::to(['text/index','translit' => $page->translit]);
+ $this->createRow($url , 1,$content);
+ }
+
+ foreach ($this->getCategories() as $category) {
+ $url = Url::to(['catalog/category', 'category' => $category]);
+ $this->createRow($url , 1,$content);
+ }
+
+
+ foreach ($this->getProducts() as $product) {
+
+ $url = Url::to(['catalog/product', 'product' => $product]);
+ $this->createRow($url , 0.9, $content);
+ }
+
+
+ foreach ($this->getArticles() as $article) {
+
+ $url = Url::to(['articles/show', 'translit' => $article->translit, 'id' => $article->id,]);
+ $this->createRow($url , 0.8,$content);
+
+ }
+
+
+ foreach($this->getCategories() as $category){
+ foreach ($this->getBrands($category) as $brand) {
+ if($this->checkFilter($category, ['brands' => [$brand->id]])){
+ $url = Url::to(['catalog/category', 'category' => $category, 'filters' => ['brands' => [$brand->alias]]]) ;
+ $this->createRow($url , 0.8, $content);
+ }
+ }
+ }
+
+
+ foreach($this->getCategories() as $category){
+ foreach ($this->getFilters($category) as $filter) {
+ if($this->checkFilter($category, [$filter['group_alias'] => [$filter['option_alias']]])){
+ $url = Url::to(['catalog/category', 'category' => $category, 'filters' => [$filter['group_alias'] => [$filter['option_alias']]] ]);
+ $this->createRow($url , 0.8, $content);
+ }
+
+ }
+ }
+
+ foreach($this->getSeoLinks() as $link){
+ $url = Yii::$app->urlManager->baseUrl.$link->url;
+ $this->createRow($url , 0.7, $content);
+
+ }
+
+
+
+// foreach($this->getCategories() as $category){
+// foreach ($this->getFilters($category) as $filter1) {
+// foreach ($this->getFilters($category) as $filter2) {
+// if($this->checkFilter($category, [$filter1['group_alias'] => [$filter1['option_alias']],$filter2['group_alias'] => [$filter2['option_alias']]] )){
+// $url = Url::to(['catalog/category', 'category' => $category, 'filters' => [$filter1['group_alias'] => [$filter1['option_alias']],$filter2['group_alias'] => [$filter2['option_alias']]] ]);
+// $this->createRow($url , 0.7, $content);
+// }
+//
+// }
+//
+// foreach ($this->getBrands($category) as $brand) {
+// if($this->checkFilter($category, ['brands' => [$brand->id], $filter1['group_alias'] => [$filter1['option_alias']]] )){
+// $url = Url::to(['catalog/category', 'category' => $category, 'filters' => ['brands' => [$brand->alias],$filter1['group_alias'] => [$filter1['option_alias']]]]);
+// $this->createRow($url , 0.7,$content);
+// }
+//
+// }
+// }
+// }
+
+
+
+ $content .= '';
+
+ fwrite($handle, $content);
+ fclose($handle);
+
+ print $dirName .'/'. $filename;
+ }
+
+}
diff --git a/models/Customer.php b/models/Customer.php
new file mode 100755
index 0000000..92e2372
--- /dev/null
+++ b/models/Customer.php
@@ -0,0 +1,160 @@
+ '10',
+ ],
+ [
+ [
+ 'username',
+ 'name',
+ 'surname',
+ 'phone',
+ 'email',
+ 'password_reset_token',
+ ],
+ 'string',
+ 'max' => 255,
+ ],
+ [
+ [
+ 'gender',
+ 'auth_key',
+ ],
+ 'string',
+ 'max' => 32,
+ ],
+ ];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function attributeLabels()
+ {
+ return [
+ 'id' => Yii::t('app', 'id'),
+ 'username' => Yii::t('app', 'username'),
+ 'name' => Yii::t('app', 'cname'),
+ 'surname' => Yii::t('app', 'surname'),
+ 'phone' => Yii::t('app', 'phone'),
+ 'gender' => Yii::t('app', 'gender'),
+ 'birth_day' => Yii::t('app', 'birth_day'),
+ 'birth_month' => Yii::t('app', 'birth_month'),
+ 'birth_year' => Yii::t('app', 'birth_year'),
+ 'body' => Yii::t('app', 'body'),
+ 'group_id' => Yii::t('app', 'group_id'),
+ 'email' => Yii::t('app', 'email'),
+ 'auth_key' => Yii::t('app', 'auth_key'),
+ 'password_reset_token' => Yii::t('app', 'password_reset_token'),
+ 'status' => Yii::t('app', 'status'),
+ 'created_at' => Yii::t('app', 'created_at'),
+ 'updated_at' => Yii::t('app', 'updated_at'),
+ ];
+ }
+
+ /**
+ * Finds user by email
+ *
+ * @param string $email
+ *
+ * @return static|null
+ */
+ public static function findByEmail($email)
+ {
+ return static::findOne(
+ [
+ 'email' => $email,
+ 'status' => self::STATUS_ACTIVE,
+ ]
+ );
+ }
+
+ /**
+ * Get full name
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->username . ' ' . $this->surname;
+ }
+
+ public function getPassword()
+ {
+ return false;
+ }
+
+ }
diff --git a/models/CustomerSearch.php b/models/CustomerSearch.php
new file mode 100755
index 0000000..b81e8d9
--- /dev/null
+++ b/models/CustomerSearch.php
@@ -0,0 +1,128 @@
+ $query,
+ ]
+ );
+
+ $this->load($params);
+
+ if (!$this->validate()) {
+ // uncomment the following line if you do not want to return any records when validation fails
+ // $query->where('0=1');
+ return $dataProvider;
+ }
+
+ // grid filtering conditions
+ $query->andFilterWhere(
+ [
+ 'id' => $this->id,
+ 'birth_day' => $this->birth_day,
+ 'birth_month' => $this->birth_month,
+ 'birth_year' => $this->birth_year,
+ 'group_id' => $this->group_id,
+ ]
+ );
+
+ $query->andFilterWhere(
+ [
+ 'like',
+ 'username',
+ $this->username,
+ ]
+ )
+ ->andFilterWhere(
+ [
+ 'like',
+ 'name',
+ $this->name,
+ ]
+ )
+ ->andFilterWhere(
+ [
+ 'like',
+ 'surname',
+ $this->surname,
+ ]
+ )
+ ->andFilterWhere(
+ [
+ 'like',
+ 'phone',
+ $this->phone,
+ ]
+ )
+ ->andFilterWhere(
+ [
+ 'like',
+ 'body',
+ $this->body,
+ ]
+ );
+
+ return $dataProvider;
+ }
+ }
diff --git a/models/Feedback.php b/models/Feedback.php
new file mode 100755
index 0000000..beecd3a
--- /dev/null
+++ b/models/Feedback.php
@@ -0,0 +1,116 @@
+ [
+ 'name',
+ 'phone',
+ ],
+ self::SCENARIO_CALLBACK => [ 'phone' ],
+ ]
+ );
+ return $scenarios;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function behaviors()
+ {
+ return [
+ [
+ 'class' => TimestampBehavior::className(),
+ 'updatedAtAttribute' => false,
+ ],
+ [
+ 'class' => AttributeBehavior::className(),
+ 'attributes' => [
+ ActiveRecord::EVENT_BEFORE_INSERT => 'ip',
+ ],
+ 'value' => function ($event) {
+ return \Yii::$app->request->userIP;
+ },
+ ],
+ ];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function rules()
+ {
+ return [
+ [
+ [
+ 'phone',
+ 'name',
+ ],
+ 'required',
+ ],
+ [
+ [ 'phone' ],
+ 'match',
+ 'pattern' => '/^\+38\(\d{3}\)\d{3}-\d{2}-\d{2}$/',
+ ],
+ [
+ [
+ 'name',
+ 'phone',
+ ],
+ 'string',
+ 'max' => 255,
+ ],
+ ];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function attributeLabels()
+ {
+ return [
+ 'id' => Yii::t('app', 'id'),
+ 'name' => Yii::t('app', 'name'),
+ 'phone' => Yii::t('app', 'phone'),
+ 'created_at' => Yii::t('app', 'created_at'),
+ 'ip' => Yii::t('app', 'ip'),
+ ];
+ }
+ }
diff --git a/models/FeedbackSearch.php b/models/FeedbackSearch.php
new file mode 100755
index 0000000..a5b052c
--- /dev/null
+++ b/models/FeedbackSearch.php
@@ -0,0 +1,98 @@
+ $query,
+ ]
+ );
+
+ $this->load($params);
+
+ if (!$this->validate()) {
+ // uncomment the following line if you do not want to return any records when validation fails
+ // $query->where('0=1');
+ return $dataProvider;
+ }
+
+ // grid filtering conditions
+ $query->andFilterWhere(
+ [
+ 'id' => $this->id,
+ ]
+ );
+
+ $query->andFilterWhere(
+ [
+ 'like',
+ 'name',
+ $this->name,
+ ]
+ )
+ ->andFilterWhere(
+ [
+ 'like',
+ 'phone',
+ $this->phone,
+ ]
+ )
+ ->andFilterWhere(
+ [
+ 'like',
+ 'ip',
+ $this->ip,
+ ]
+ );
+
+ return $dataProvider;
+ }
+ }
diff --git a/models/Page.php b/models/Page.php
new file mode 100755
index 0000000..6b18009
--- /dev/null
+++ b/models/Page.php
@@ -0,0 +1,91 @@
+ [
+ 'class' => LanguageBehavior::className(),
+ ],
+ ];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function rules()
+ {
+ return [
+ [
+ [ 'title' ],
+ 'safe',
+ ],
+ [
+ [
+ 'in_menu',
+ ],
+ 'boolean',
+ ],
+ ];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function attributeLabels()
+ {
+ return [
+ 'id' => Yii::t('app', 'id'),
+ 'in_menu' => Yii::t('app', 'in_menu'),
+ ];
+ }
+ }
diff --git a/models/PageLang.php b/models/PageLang.php
new file mode 100755
index 0000000..e4c4b20
--- /dev/null
+++ b/models/PageLang.php
@@ -0,0 +1,148 @@
+ [
+ 'class' => 'common\behaviors\Slug',
+ ],
+ ];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function rules()
+ {
+ return [
+ [
+ [
+ 'title',
+ 'body',
+ ],
+ 'required',
+ ],
+ [
+ [
+ 'body',
+ 'seo_text',
+ ],
+ 'string',
+ ],
+ [
+ [
+ 'title',
+ 'meta_title',
+ 'meta_keywords',
+ 'meta_description',
+ 'h1',
+ 'alias',
+ ],
+ 'string',
+ 'max' => 255,
+ ],
+ [
+ [
+ 'page_id',
+ 'language_id',
+ ],
+ 'unique',
+ 'targetAttribute' => [
+ 'page_id',
+ 'language_id',
+ ],
+ 'message' => 'The combination of Page ID and Language ID has already been taken.',
+ ],
+ [
+ [ 'language_id' ],
+ 'exist',
+ 'skipOnError' => true,
+ 'targetClass' => Language::className(),
+ 'targetAttribute' => [ 'language_id' => 'id' ],
+ ],
+ [
+ [ 'page_id' ],
+ 'exist',
+ 'skipOnError' => true,
+ 'targetClass' => Page::className(),
+ 'targetAttribute' => [ 'page_id' => 'id' ],
+ ],
+ ];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function attributeLabels()
+ {
+ return [
+ 'page_id' => Yii::t('app', 'page_id'),
+ 'language_id' => Yii::t('app', 'language_id'),
+ 'title' => Yii::t('app', 'title'),
+ 'body' => Yii::t('app', 'body'),
+ 'meta_title' => Yii::t('app', 'meta_title'),
+ 'meta_keywords' => Yii::t('app', 'meta_keywords'),
+ 'meta_description' => Yii::t('app', 'meta_description'),
+ 'seo_text' => Yii::t('app', 'seo_text'),
+ 'h1' => Yii::t('app', 'h1'),
+ 'alias' => Yii::t('app', 'alias'),
+ ];
+ }
+
+ /**
+ * @return \yii\db\ActiveQuery
+ */
+ public function getLanguage()
+ {
+ return $this->hasOne(Language::className(), [ 'id' => 'language_id' ]);
+ }
+
+ /**
+ * @return \yii\db\ActiveQuery
+ */
+ public function getPage()
+ {
+ return $this->hasOne(Page::className(), [ 'id' => 'page_id' ]);
+ }
+ }
diff --git a/models/PageSearch.php b/models/PageSearch.php
new file mode 100755
index 0000000..d13e26e
--- /dev/null
+++ b/models/PageSearch.php
@@ -0,0 +1,106 @@
+joinWith('lang');
+
+ // add conditions that should always apply here
+
+ $dataProvider = new ActiveDataProvider(
+ [
+ 'query' => $query,
+ 'sort' => [
+ 'attributes' => [
+ 'title' => [
+ 'asc' => [ 'page_lang.title' => SORT_ASC ],
+ 'desc' => [ 'page_lang.title' => SORT_DESC ],
+ ],
+ 'id',
+ 'in_menu',
+ ],
+ ],
+ ]
+ );
+
+ $this->load($params);
+
+ if (!$this->validate()) {
+ // uncomment the following line if you do not want to return any records when validation fails
+ // $query->where('0=1');
+ return $dataProvider;
+ }
+
+ // grid filtering conditions
+ $query->andFilterWhere(
+ [
+ 'id' => $this->id,
+ 'in_menu' => $this->in_menu,
+ ]
+ )
+ ->andFilterWhere(
+ [
+ 'like',
+ 'page_lang.title',
+ $this->title,
+ ]
+ );
+
+ return $dataProvider;
+ }
+ }
diff --git a/translation/ru/app.php b/translation/ru/app.php
new file mode 100755
index 0000000..29f9ecd
--- /dev/null
+++ b/translation/ru/app.php
@@ -0,0 +1,176 @@
+ 'ID',
+ 'username' => 'Логин',
+ 'cname' => 'Имя',
+ 'surname' => 'Фамилия',
+ 'auth_key' => 'Ключ аутентификации',
+ 'password_hash' => 'Хэш пароля',
+ 'password_reset_token' => 'Password Reset Token',
+ 'email' => 'Логин (e-mail)',
+ 'phone' => 'Телефон',
+ 'status' => 'Статус',
+ 'gender' => 'Пол',
+ 'birth_day' => 'Birth Day',
+ 'birth_month' => 'Birth Month',
+ 'birth_year' => 'Birth Year',
+ 'group_id' => 'Group ID',
+ 'created_at' => 'Created At',
+ 'updated_at' => 'Updated At',
+ 'verifyCode' => 'Код проверки',
+ 'password' => 'Пароль',
+ 'password_repeat' => 'Повторить пароль',
+ 'registration' => 'Регистрация',
+ 'message' => 'Этот {field} уже занят',
+ 'message_match_password' => 'Пароли не совпадают',
+ 'exit' => 'Выход',
+ 'enter' => 'Войти',
+ 'your_personal_area' => 'Вход в личный кабинет',
+ 'forgot_password' => 'Забыли пароль?',
+ 'rememberMe' => 'Запомнить меня',
+ 'articles' => 'Всего товаров',
+ 'code' => 'Код: {0}',
+ 'checkout' => 'оформить заказ',
+ 'sum' => 'Сумма',
+ 'continue_shopping' => 'продолжить покупки',
+ 'edit_personal_data' => 'Редактировать личные данные',
+ 'personal_data' => 'Личные данные',
+ 'my_orders' => 'Мои заказы',
+ 'bookmarks' => 'Закладки',
+ 'basket' => 'Корзина',
+ 'banner_id' => 'Banner ID',
+ 'image' => 'Изображение',
+ 'alt' => 'Описание',
+ 'title' => 'Заголовок',
+ 'url' => 'Ссылка',
+ 'width' => 'Ширина',
+ 'height' => 'Высота',
+ 'blog_id' => 'Blog ID',
+ 'user_id' => 'User ID',
+ 'name' => 'Название',
+ 'link' => 'Ссылка',
+ 'user_add_id' => 'User Add ID',
+ 'view_count' => 'Количество просмотров',
+ 'description' => 'Описание',
+ 'cover' => 'Фото главное',
+ 'event_id' => 'Event ID',
+ 'alias' => 'Ссылка',
+ 'body' => 'Тело',
+ 'meta_title' => 'Мета заголовок',
+ 'h1' => 'H1',
+ 'seo_text' => 'Сео Текст',
+ 'end_at' => 'Срок действия по',
+ 'order_items_id' => 'Order Items ID',
+ 'order_id' => 'Order ID',
+ 'item_id' => 'Item ID',
+ 'item_count' => 'Количество',
+ 'price' => 'Цена',
+ 'customer_id' => 'Customer ID',
+ 'delivery' => 'Доставка',
+ 'payment' => 'Оплата',
+ 'seo_id' => 'Seo ID',
+ 'controller' => 'Controller',
+ 'seo_category_id' => 'Seo Category ID',
+ 'seo_dynamic_id' => 'Seo Dynamic ID',
+ 'action' => 'Action',
+ 'fields' => 'Поля',
+ 'param' => 'Параметры',
+ 'key' => 'Ключ',
+ 'service_id' => 'Service ID',
+ 'slider_id' => 'Slider ID',
+ 'speed' => 'Скорость',
+ 'duration' => 'Продолжительность',
+ 'slider_image_id' => 'Slider Image ID',
+ 'sort' => 'Сортировка',
+ 'order_name' => 'Ф.И.О',
+ 'order_phone' => 'Контактный телефон',
+ 'order_email' => 'email',
+ 'order_comment' => 'Комментарии',
+ 'articlesID' => 'ID',
+ 'articlesDate' => 'Дата',
+ 'articlesImage' => 'Изображение',
+ 'lang-Articles ID' => '',
+ 'lang-Language ID' => '',
+ 'lang-Title' => '',
+ 'lang-Body' => '',
+ 'lang-Meta Title' => '',
+ 'lang-Meta Keywords' => '',
+ 'lang-Meta Description' => '',
+ 'lang-Seo Text' => '',
+ 'lang-H1' => '',
+ 'lang-Body Preview' => '',
+ 'language_id' => '',
+ 'bg_id' => '',
+ 'feedback_id' => 'Feedback ID',
+ 'ip' => 'IP',
+ 'productName' => 'Продукт',
+ 'op_name' => 'Вид',
+ 'art' => 'Артикул',
+ 'cost' => 'Цена за один',
+ 'count' => 'Кол.',
+ 'sumCost' => 'Сумма',
+ 'in_menu' => 'Show in menu',
+ 'page_id' => 'Page ID',
+ 'meta_keywords' => 'Meta Keywords',
+ 'meta_description' => 'Meta Description',
+ 'product_spec_id' => 'Product Spec ID',
+ 'product_id' => 'Product ID',
+ 'tech_spec_link' => 'Tech Spec Link',
+ 'tech_char_link' => 'Tech Char Link',
+ 'techSpecFile' => 'techSpecFile',
+ 'techCharFile' => 'techCharFile',
+ 'tech_spec_text' => 'Tech Spec Text',
+ 'instruction' => 'Instruction',
+ 'product_to_project_id' => 'Product To Project ID',
+ 'product_variant_id' => 'Product Variant ID',
+ 'project_id' => 'Project ID',
+ 'product_to_rating_id' => 'Product To Rating ID',
+ 'value' => 'Value',
+ 'images' => 'Images',
+ 'project_image_id' => 'Project Image ID',
+ 'meta' => 'Meta',
+ 'date_time' => 'Дата',
+ 'template_location_id' => 'Template Location ID',
+ 'template_location_name' => 'Template Location Name',
+ 'template_location_title' => 'Template Location Title',
+ 'is_slider' => 'Is Slider',
+ 'is_banner' => 'Is Banner',
+ 'order_delivery_id' => 'order Delivery ID',
+ 'text' => 'Text',
+ 'emailis' => 'Такой email уже есть.',
+ 'меню' => 'меню',
+ 'контрактный отдел' => 'контрактный отдел',
+ 'отдел по работе с дизайнерами и архитекторами' => 'отдел по работе с дизайнерами и архитекторами',
+ 'диз_арх_2' => 'отдел по работе
с дизайнерами
и архитекторами',
+ 'search' => 'поиск',
+ 'copy1' => '2003 - 2016 Все права защищены и охраняются действующим законодательством Украины.',
+ 'copy2' => 'Использование материалов с данного сайта возможно только с письменного разрешения компании ООО «Витекс Украина».',
+ 'form1' => 'Запрос на просчет',
+ 'comment' => 'комментарий',
+ 'submit' => 'Отправить',
+ 'Сертификаты' => 'Сертификаты',
+ 'Монтаж, уборка, уход' => 'Монтаж, уборка, уход',
+ 'Галерея объектов' => 'Галерея объектов',
+ 'скачать' => 'скачать',
+ 'Документ технической документации' => 'Документ технической документации',
+ 'Вы также можете скачать таблицу с ' => 'Вы также можете скачать таблицу с ',
+ 'техническими характеристиками' => 'техническими характеристиками',
+ 'Номер по каталогу' => 'Номер по каталогу',
+ 'Заказать образец' => 'Заказать образец',
+ 'Технические характеристики' => 'Технические характеристики',
+ 'Контрактные продукты' => 'Контрактные продукты',
+ 'Статьи' => 'Статьи',
+ 'Контакты' => 'Контакты',
+ 'Отправить запрос' => 'Отправить запрос',
+ 'Создание сайтов' => 'Создание сайтов',
+ 'Подробнее' => 'Подробнее',
+ 'Запрос на просчет' => 'Запрос на просчет',
+ 'comment2' => 'Комментарий',
+ 'продолжить выбор' => 'продолжить выбор',
+ 'Отправить' => 'Отправить',
+ 'success1' => 'Ваша заявка принята.',
+ 'success2' => 'Мы свяжемся в Вами в ближайшее время',
+ 'Поиск' => 'Поиск',
+ 'Результаты поиска для' => 'Результаты поиска для',
+ 'certs' => 'Сертификаты',
+ ];
\ No newline at end of file
diff --git a/translation/ru/blog.php b/translation/ru/blog.php
new file mode 100644
index 0000000..936138e
--- /dev/null
+++ b/translation/ru/blog.php
@@ -0,0 +1,22 @@
+ 'Выберите категорию ...',
+ 'Select tag' => 'Выберите тэг ...',
+ 'Has no parent rubric' => 'Без категории',
+ 'Waiting for results' => 'Загрузка ...',
+ 'Select related products' => 'Выберите сопутствующие товары',
+ 'Select related articles' => 'Выберите статьи',
+ 'Blog Articles' => 'Статьи блога',
+ 'Create Blog Article' => 'Создать статью',
+ 'Update Blog Article: ' => 'Обновить статью: ',
+ 'Not active' => 'Не активна',
+ 'Active' => 'Активна',
+ 'Are you sure you want to delete this item?' => 'Вы точно хотите это удалить ?',
+ 'Update' => 'Обновить',
+ 'Blog Categories' => 'Рубрики',
+ 'Create Blog Category' => 'Создать рубрику',
+ 'Update Blog Category: ' => 'Обновить рубрику: ',
+ 'Blog Tags' => 'Тэги',
+ 'Create Blog Tag' => 'Создать тэг',
+ 'Update Blog Tag: ' => 'Обновить тэг: ',
+ ];
\ No newline at end of file
diff --git a/translation/ru/product.php b/translation/ru/product.php
new file mode 100755
index 0000000..1b39818
--- /dev/null
+++ b/translation/ru/product.php
@@ -0,0 +1,29 @@
+ 'Категории',
+ 'Create Category' => 'Создать Категорию',
+ 'Name' => 'Наименование',
+ 'Remote ID' => 'ID в 1С',
+ 'Search for "{keywords}"' => 'Поиск по "{keywords}"',
+ 'Search for "{keywords}" in category "{category}"' => 'Поиск по "{keywords}" в категории "{category}"',
+ 'Promo products' => 'Акционные товары',
+ 'New products' => 'Новинки',
+ 'Top products' => 'Популярные',
+ 'Similar products' => 'Похожие товары',
+ 'Brands' => 'Бренды',
+ 'Brand' => 'Бренд',
+ 'Category' => 'Категория',
+ 'Select brand' => 'Выберите бренд',
+ 'Select category' => 'Выберите категорию',
+ 'SKU' => 'Артикул',
+ 'Stock' => 'Остаток',
+ 'Price' => 'Цена',
+ 'Price Old' => 'Старая Цена',
+ 'Products' => 'Товары',
+ 'Product' => 'Товар',
+ 'Variants' => 'Модификации',
+ 'Variant' => 'Модификация',
+ 'Create Product' => 'Создать Товар',
+ 'Enable' => 'Доступно',
+ 'Disable' => 'Отсутсвует',
+ ];
\ No newline at end of file
diff --git a/translation/ua/app.php b/translation/ua/app.php
new file mode 100755
index 0000000..e49e1c0
--- /dev/null
+++ b/translation/ua/app.php
@@ -0,0 +1,176 @@
+ 'ID',
+ 'username' => "Логін",
+ 'cname' => 'Ім\'я',
+ 'surname' => 'Фамилия',
+ 'auth_key' => 'Ключ аутентифікації',
+ 'password_hash' => 'Хеш паролю',
+ 'password_reset_token' => 'Password Reset Token',
+ 'email' => 'Логін (e-mail)',
+ 'phone' => 'Телефон',
+ 'status' => 'Статус',
+ 'gender' => 'Пол',
+ 'birth_day' => 'Birth Day',
+ 'birth_month' => 'Birth Month',
+ 'birth_year' => 'Birth Year',
+ 'group_id' => 'Group ID',
+ 'created_at' => 'Створено',
+ 'updated_at' => 'Оновлено',
+ 'verifyCode' => 'Код перевірки',
+ 'password' => 'Пароль',
+ 'password_repeat' => 'Повторити пароль',
+ 'registration' => 'Реєстрація',
+ 'message' => 'Цей {field} вже зайнято',
+ 'message_match_password' => 'Пароли не совпадают',
+ 'exit' => 'Вихід',
+ 'enter' => 'Війти',
+ 'your_personal_area' => 'Вхід в особистий кабінет',
+ 'forgot_password' => 'Забули пароль?',
+ 'rememberMe' => 'Запам\'ятати мене',
+ 'articles' => 'Всього товарів',
+ 'code' => 'Код: {0}',
+ 'checkout' => 'оформити замовлення',
+ 'sum' => 'Сума',
+ 'continue_shopping' => 'продовжити покупки',
+ 'edit_personal_data' => 'Редагувати особисті дані',
+ 'personal_data' => 'Особисті дані',
+ 'my_orders' => 'Мої замовлення',
+ 'bookmarks' => 'Закладки',
+ 'basket' => 'Кошик',
+ 'banner_id' => 'Banner ID',
+ 'image' => 'Зображення',
+ 'alt' => 'Опис',
+ 'title' => 'Заголовок',
+ 'url' => 'Посилання',
+ 'width' => 'Ширина',
+ 'height' => 'Висота',
+ 'blog_id' => 'Blog ID',
+ 'user_id' => 'User ID',
+ 'name' => 'Назва',
+ 'link' => 'Посилання',
+ 'user_add_id' => 'User Add ID',
+ 'view_count' => 'Кількість переглядів',
+ 'description' => 'Опис',
+ 'cover' => 'Фото головне',
+ 'event_id' => 'Event ID',
+ 'alias' => 'Посилання',
+ 'body' => 'Тіло',
+ 'meta_title' => 'Мета заголовок',
+ 'h1' => 'H1',
+ 'seo_text' => 'Сео Текст',
+ 'end_at' => 'Термін дії до',
+ 'order_items_id' => 'Order Items ID',
+ 'order_id' => 'Order ID',
+ 'item_id' => 'Item ID',
+ 'item_count' => 'Кількість',
+ 'price' => 'Ціна',
+ 'customer_id' => 'Customer ID',
+ 'delivery' => 'Доставка',
+ 'payment' => 'Оплата',
+ 'seo_id' => 'Seo ID',
+ 'controller' => 'Controller',
+ 'seo_category_id' => 'Seo Category ID',
+ 'seo_dynamic_id' => 'Seo Dynamic ID',
+ 'action' => 'Action',
+ 'fields' => 'Поля',
+ 'param' => 'Параметри',
+ 'key' => 'Ключ',
+ 'service_id' => 'Service ID',
+ 'slider_id' => 'Slider ID',
+ 'speed' => 'Швидкість',
+ 'duration' => 'Тривалість',
+ 'slider_image_id' => 'Slider Image ID',
+ 'sort' => 'Сортування',
+ 'order_name' => 'П.І.Б.',
+ 'order_phone' => 'Контактний телефон',
+ 'order_email' => 'email',
+ 'order_comment' => 'Коментар',
+ 'articlesID' => 'ID',
+ 'articlesDate' => 'Дата',
+ 'articlesImage' => 'Зображення',
+ 'lang-Articles ID' => '',
+ 'lang-Language ID' => '',
+ 'lang-Title' => '',
+ 'lang-Body' => '',
+ 'lang-Meta Title' => '',
+ 'lang-Meta Keywords' => '',
+ 'lang-Meta Description' => '',
+ 'lang-Seo Text' => '',
+ 'lang-H1' => '',
+ 'lang-Body Preview' => '',
+ 'language_id' => '',
+ 'bg_id' => '',
+ 'feedback_id' => 'Feedback ID',
+ 'ip' => 'IP',
+ 'productName' => 'Продукт',
+ 'op_name' => 'Вид',
+ 'art' => 'Артикул',
+ 'cost' => 'Цена за один',
+ 'count' => 'Кол.',
+ 'sumCost' => 'Сумма',
+ 'in_menu' => 'Show in menu',
+ 'page_id' => 'Page ID',
+ 'meta_keywords' => 'Meta Keywords',
+ 'meta_description' => 'Meta Description',
+ 'product_spec_id' => 'Product Spec ID',
+ 'product_id' => 'Product ID',
+ 'tech_spec_link' => 'Tech Spec Link',
+ 'tech_char_link' => 'Tech Char Link',
+ 'techSpecFile' => 'techSpecFile',
+ 'techCharFile' => 'techCharFile',
+ 'tech_spec_text' => 'Tech Spec Text',
+ 'instruction' => 'Instruction',
+ 'product_to_project_id' => 'Product To Project ID',
+ 'product_variant_id' => 'Product Variant ID',
+ 'project_id' => 'Project ID',
+ 'product_to_rating_id' => 'Product To Rating ID',
+ 'value' => 'Value',
+ 'images' => 'Images',
+ 'project_image_id' => 'Project Image ID',
+ 'meta' => 'Meta',
+ 'date_time' => 'Дата',
+ 'template_location_id' => 'Template Location ID',
+ 'template_location_name' => 'Template Location Name',
+ 'template_location_title' => 'Template Location Title',
+ 'is_slider' => 'Is Slider',
+ 'is_banner' => 'Is Banner',
+ 'order_delivery_id' => 'order Delivery ID',
+ 'text' => 'Text',
+ 'emailis' => 'Такой email уже есть.',
+ 'меню' => 'меню',
+ 'контрактный отдел' => 'контрактне відділення',
+ 'отдел по работе с дизайнерами и архитекторами' => 'відділення по роботі з дизайнерами та архітекторами',
+ 'диз_арх_2' => 'відділення по роботі
з дизайнерами
та архітекторами',
+ 'search' => 'пошук',
+ 'copy1' => '2003 - 2016 Всі права захищені та охорняються чинним законодавством України.',
+ 'copy2' => 'Використання матеріалів з даного сайту можливе лише з письмого дозволу компанії ТОВ «Вітекс Україна».',
+ 'form1' => 'Запит на прорахування',
+ 'comment' => 'коментар',
+ 'submit' => 'Відправити',
+ 'Сертификаты' => 'Сертифікати',
+ 'Монтаж, уборка, уход' => 'Монтаж, прибирання, догляд',
+ 'Галерея объектов' => "Галерея об'єктів",
+ 'скачать' => 'завантажити',
+ 'Документ технической документации' => 'Документ технічної документації',
+ 'Вы также можете скачать таблицу с ' => 'Ви також можете завантажити таблицю з ',
+ 'техническими характеристиками' => 'технічними характеристиками',
+ 'Номер по каталогу' => 'Номер за каталогом',
+ 'Заказать образец' => 'Замовити зразок',
+ 'Технические характеристики' => 'Технічні характеристики',
+ 'Контрактные продукты' => 'Контрактні продукти',
+ 'Статьи' => 'Статті',
+ 'Контакты' => 'Контакти',
+ 'Отправить запрос' => 'Надіслати запит',
+ 'Создание сайтов' => 'Створення сайтів',
+ 'Подробнее' => 'Подробнее',
+ 'Запрос на просчет' => 'Запит на прорахування',
+ 'comment2' => 'Коментар',
+ 'продолжить выбор' => 'продовжити вибір',
+ 'Отправить' => 'Відправити',
+ 'success1' => 'Ваша заявка прийнята.',
+ 'success2' => "Ми зв'яжемось з Вами найближчим часом",
+ 'Поиск' => 'Пошук',
+ 'Результаты поиска для' => 'Результати пошуку для',
+ 'certs' => 'Сертифікати',
+ ];
\ No newline at end of file
diff --git a/translation/ua/product.php b/translation/ua/product.php
new file mode 100755
index 0000000..1b39818
--- /dev/null
+++ b/translation/ua/product.php
@@ -0,0 +1,29 @@
+ 'Категории',
+ 'Create Category' => 'Создать Категорию',
+ 'Name' => 'Наименование',
+ 'Remote ID' => 'ID в 1С',
+ 'Search for "{keywords}"' => 'Поиск по "{keywords}"',
+ 'Search for "{keywords}" in category "{category}"' => 'Поиск по "{keywords}" в категории "{category}"',
+ 'Promo products' => 'Акционные товары',
+ 'New products' => 'Новинки',
+ 'Top products' => 'Популярные',
+ 'Similar products' => 'Похожие товары',
+ 'Brands' => 'Бренды',
+ 'Brand' => 'Бренд',
+ 'Category' => 'Категория',
+ 'Select brand' => 'Выберите бренд',
+ 'Select category' => 'Выберите категорию',
+ 'SKU' => 'Артикул',
+ 'Stock' => 'Остаток',
+ 'Price' => 'Цена',
+ 'Price Old' => 'Старая Цена',
+ 'Products' => 'Товары',
+ 'Product' => 'Товар',
+ 'Variants' => 'Модификации',
+ 'Variant' => 'Модификация',
+ 'Create Product' => 'Создать Товар',
+ 'Enable' => 'Доступно',
+ 'Disable' => 'Отсутсвует',
+ ];
\ No newline at end of file
--
libgit2 0.21.4