├── .bowerrc ├── .gitignore ├── README.md ├── assets ├── AppAsset.php ├── MainAsset.php └── VendorAsset.php ├── behaviors └── FeedBehavior.php ├── commands └── WarmupController.php ├── composer.json ├── config ├── .gitignore ├── console.php ├── params.php └── web.php ├── controllers ├── CompaniesController.php ├── ContactsController.php ├── DashboardController.php ├── DealsController.php ├── EntityController.php ├── InboxController.php ├── LandingController.php ├── ProfileController.php ├── SecuredController.php ├── TasksController.php ├── ThirdPartyController.php └── UserController.php ├── data └── .gitkeep ├── fixtures ├── CompanyFixture.php ├── ContactFixture.php ├── DealFixture.php ├── DealStatusFixture.php ├── NoteFixture.php ├── TaskFixture.php ├── UserFixture.php ├── data │ ├── company.php │ ├── contact.php │ ├── deal.php │ ├── deal_status.php │ ├── note.php │ ├── task.php │ └── user.php └── templates │ ├── company.php │ ├── contact.php │ ├── deal.php │ ├── note.php │ ├── task.php │ └── user.php ├── helpers └── DashboardPresentation.php ├── interfaces └── PersonInterface.php ├── migrations ├── m200204_085219_create_tables.php ├── m200613_144405_contacts.php ├── m200729_174942_deals.php ├── m200821_144606_deal_notes.php ├── m200825_064858_feed.php ├── m200825_072235_deal_owner.php ├── m200909_075638_user_fields.php ├── m200915_133435_softdelete.php ├── m201127_083958_status_alias.php ├── m201127_122112_tasks.php └── m201202_134344_testuser.php ├── models ├── BaseUser.php ├── Company.php ├── Contact.php ├── ContactType.php ├── Deal.php ├── DealStatus.php ├── EmailSearchForm.php ├── Feed.php ├── LoginForm.php ├── Note.php ├── ProfileSettingsForm.php ├── SignupForm.php ├── SoftDelete.php ├── Task.php ├── TaskType.php └── User.php ├── modules └── api │ ├── Module.php │ └── controllers │ └── ContactsController.php ├── package-lock.json ├── package.json ├── requirements.php ├── runtime └── .gitignore ├── services ├── AuthClient.php ├── GmailClient.php ├── GoogleAuthClient.php ├── GoogleAuthClientFactory.php ├── MailClient.php └── containers │ ├── GmailMessage.php │ └── MailMessage.php ├── src ├── exceptions │ ├── FileFormatException.php │ └── SourceFileException.php └── utils │ └── ContactsImporter.php ├── validators └── RemoteEmailValidator.php ├── views ├── companies │ └── index.php ├── contacts │ └── index.php ├── dashboard │ └── index.php ├── deals │ ├── index.php │ └── view.php ├── inbox │ └── mail.php ├── landing │ └── index.php ├── layouts │ ├── anon.php │ └── common.php ├── modals │ ├── _company_form.php │ ├── _contact_form.php │ ├── _deal_form.php │ └── _login_form.php ├── partials │ ├── _action_row.php │ ├── _grid_filter.php │ ├── _grid_header.php │ └── _task_create.php ├── profile │ └── settings.php ├── tasks │ └── index.php └── user │ └── signup.php ├── web ├── .htaccess ├── assets │ └── .gitignore ├── css │ ├── sample.css │ ├── site.css │ └── style.css ├── favicon.png ├── fonts │ ├── ptsans.woff │ ├── ptsans.woff2 │ ├── ptsansbold.woff │ └── ptsansbold.woff2 ├── img │ ├── COLOR │ │ └── black.svg │ ├── arrow-down.svg │ ├── bg-empty.svg │ ├── cloud-bg.svg │ ├── cosmonaut.png │ ├── cosmonaut@2x.png │ ├── footer.png │ ├── footer@2x.png │ ├── html-academy-logo.png │ ├── html-academy-logo@2x.png │ ├── logo-mono.svg │ ├── next.svg │ ├── rocket.png │ ├── rocket@2x.png │ ├── shuttle.png │ ├── shuttle@2x.png │ ├── sprite.svg │ └── turbocrm-logo.svg ├── index.php └── js │ ├── deal-form.js │ ├── grid.js │ ├── inbox.js │ ├── main.js │ └── main.js.map ├── widgets ├── Alert.php ├── FeedItem.php ├── LinkPager.php ├── Notification.php └── views │ └── feed.php └── yii.bat /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory" : "vendor/bower-asset" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # yii console commands 2 | /yii 3 | /yii_test 4 | /yii_test.bat 5 | 6 | # phpstorm project files 7 | .idea 8 | 9 | # netbeans project files 10 | nbproject 11 | 12 | # zend studio for eclipse project files 13 | .buildpath 14 | .project 15 | .settings 16 | 17 | # windows thumbnail cache 18 | Thumbs.db 19 | 20 | # composer vendor dir 21 | /vendor 22 | 23 | # composer itself is not needed 24 | composer.phar 25 | 26 | # Mac DS_Store Files 27 | .DS_Store 28 | 29 | # phpunit itself is not needed 30 | phpunit.phar 31 | # local phpunit config 32 | /phpunit.xml 33 | 34 | # vagrant runtime 35 | /.vagrant 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Учебный проект "TurboCRM" для курса "PHP. Архитектура сложных веб-сервисов" 2 | -------------------------------------------------------------------------------- /assets/AppAsset.php: -------------------------------------------------------------------------------- 1 | 'addFeedItem' 27 | ]; 28 | } 29 | 30 | public function addFeedItem() 31 | { 32 | $feedItem = new Feed(); 33 | $feedItem->type = $this->eventType; 34 | $feedItem->value = $this->owner->id; 35 | $feedItem->deal_id = $this->owner->deal_id; 36 | 37 | $result = $feedItem->save(); 38 | 39 | return $result; 40 | } 41 | 42 | 43 | 44 | } 45 | -------------------------------------------------------------------------------- /commands/WarmupController.php: -------------------------------------------------------------------------------- 1 | stdout("Cache warmup command for user: $user\n", Console::BOLD); 12 | return 0; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frexin/turbocrm", 3 | "description": "Учебный проект TurboCRM", 4 | "homepage": "http://turbo-crm.ru/", 5 | "type": "project", 6 | "minimum-stability": "stable", 7 | "require": { 8 | "php": ">=7.1.0", 9 | "yiisoft/yii2": "2.0.40", 10 | "yiisoft/yii2-bootstrap4": "^2.0", 11 | "yiisoft/yii2-swiftmailer": "~2.0.0 || ~2.1.0", 12 | "guzzlehttp/guzzle": "~6.0", 13 | "ext-json": "*", 14 | "yiisoft/yii2-redis": "^2.0", 15 | "yiisoft/yii2-faker": "^2.0", 16 | "npm-asset/moment": "^2.24", 17 | "wapmorgan/morphos": "^3.2", 18 | "yii2tech/ar-softdelete": "^1.0", 19 | "google/apiclient": "^2.0" 20 | }, 21 | "require-dev": { 22 | "yiisoft/yii2-debug": "~2.1.0", 23 | "yiisoft/yii2-gii": "~2.1.0", 24 | "symfony/browser-kit": ">=2.7 <=4.2.4" 25 | }, 26 | "config": { 27 | "process-timeout": 1800, 28 | "fxp-asset": { 29 | "enabled": false 30 | } 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "htmlacademy\\": "src/" 35 | } 36 | }, 37 | "repositories": [ 38 | { 39 | "type": "composer", 40 | "url": "https://asset-packagist.org" 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /config/.gitignore: -------------------------------------------------------------------------------- 1 | db.php 2 | -------------------------------------------------------------------------------- /config/console.php: -------------------------------------------------------------------------------- 1 | 'app-console', 6 | 'basePath' => dirname(__DIR__), 7 | 'bootstrap' => ['log'], 8 | 'controllerNamespace' => 'console\controllers', 9 | 'aliases' => [ 10 | '@bower' => '@vendor/bower-asset', 11 | '@npm' => '@vendor/npm-asset', 12 | ], 13 | 'controllerMap' => [ 14 | 'fixture' => [ 15 | 'class' => 'yii\faker\FixtureController', 16 | 'templatePath' => '@app/fixtures/templates', 17 | 'fixtureDataPath' => '@app/fixtures/data', 18 | 'namespace' => 'app\fixtures', 19 | ], 20 | ], 21 | 'components' => [ 22 | 'db' => require(__DIR__ . '/db.php'), 23 | 'log' => [ 24 | 'targets' => [ 25 | [ 26 | 'class' => 'yii\log\FileTarget', 27 | 'levels' => ['error', 'warning'], 28 | ], 29 | ], 30 | ], 31 | ], 32 | 'params' => $params, 33 | ]; 34 | -------------------------------------------------------------------------------- /config/params.php: -------------------------------------------------------------------------------- 1 | 'admin@example.com', 4 | 'apiKey' => 'xxx' 5 | ]; 6 | -------------------------------------------------------------------------------- /config/web.php: -------------------------------------------------------------------------------- 1 | 'turbocrm', 6 | 'basePath' => dirname(__DIR__), 7 | 'layout' => 'common', 8 | 'language' => 'ru-RU', 9 | 'bootstrap' => ['log'], 10 | 'vendorPath' => dirname(__DIR__) . '/vendor', 11 | 'defaultRoute' => 'landing/index', 12 | 'modules' => [ 13 | 'api' => [ 14 | 'class' => 'app\modules\api\Module' 15 | ] 16 | ], 17 | 'components' => [ 18 | 'log' => [ 19 | 'traceLevel' => YII_DEBUG ? 3 : 0, 20 | 'targets' => [ 21 | [ 22 | 'class' => 'yii\log\FileTarget', 23 | 'levels' => ['error', 'warning'], 24 | ], 25 | [ 26 | 'class' => 'yii\log\EmailTarget', 27 | 'levels' => ['error'], 28 | 'message' => [ 29 | 'to' => ['admin@example.com'], 30 | 'subject' => 'Ошибки на сайте turbocrm', 31 | ], 32 | ], 33 | [ 34 | 'class' => 'yii\log\DbTarget', 35 | 'levels' => ['info'] 36 | ], 37 | ], 38 | ], 39 | 'request' => [ 40 | 'cookieValidationKey' => '9fs$(8423nklj24pjlkfds298', 41 | 'parsers' => [ 42 | 'application/json' => 'yii\web\JsonParser', 43 | ], 44 | 'csrfParam' => '_csrf-frontend', 45 | ], 46 | 'user' => [ 47 | 'identityClass' => 'app\models\User', 48 | 'enableAutoLogin' => true, 49 | 'identityCookie' => ['name' => '_identity-frontend', 'httpOnly' => true], 50 | ], 51 | 'errorHandler' => [ 52 | 'errorAction' => 'site/error', 53 | ], 54 | 'db' => require(__DIR__ . '/db.php'), 55 | 'urlManager' => [ 56 | 'enablePrettyUrl' => true, 57 | 'showScriptName' => false, 58 | 'enableStrictParsing' => false, 59 | 'rules' => [ 60 | ['class' => 'yii\rest\UrlRule', 'controller' => 'api/contacts'], 61 | '//' => '/', 62 | 'persons' => 'contacts/index', 63 | 'contacts/status/' => 'contacts/filter' 64 | ] 65 | ], 66 | 'cache' => [ 67 | 'class' => 'yii\redis\Cache', 68 | ], 69 | 'redis' => [ 70 | 'class' => 'yii\redis\Connection', 71 | 'hostname' => 'localhost', 72 | 'port' => 6379, 73 | 'database' => 0, 74 | ], 75 | ], 76 | 'params' => $params, 77 | ]; 78 | -------------------------------------------------------------------------------- /controllers/CompaniesController.php: -------------------------------------------------------------------------------- 1 | entity = new Company; 17 | $this->alias = 'companies'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /controllers/ContactsController.php: -------------------------------------------------------------------------------- 1 | entity = new Contact; 13 | $this->alias = 'contacts'; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /controllers/DashboardController.php: -------------------------------------------------------------------------------- 1 | render('index', ['presentationHelper' => $presentationHelper]); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /controllers/DealsController.php: -------------------------------------------------------------------------------- 1 | entity = new Deal; 18 | $this->alias = 'deals'; 19 | } 20 | 21 | public function actionIndex($id = null) 22 | { 23 | $dealStatuses = DealStatus::find()->all(); 24 | 25 | return $this->render('index', ['statuses' => $dealStatuses]); 26 | } 27 | 28 | public function actionNext($id) 29 | { 30 | return $this->changeStatus($id, 'next'); 31 | } 32 | 33 | public function actionPrev($id) 34 | { 35 | return $this->changeStatus($id, 'prev'); 36 | } 37 | 38 | public function actionView($id) 39 | { 40 | $deal = Deal::findOne($id); 41 | $note = new Note(); 42 | 43 | if (!$deal || $deal->deleted) { 44 | throw new NotFoundHttpException("Сделка с этим ID не найдена"); 45 | } 46 | 47 | if (\Yii::$app->request->isPost) { 48 | $note->load(\Yii::$app->request->post()); 49 | $note->save(); 50 | 51 | $deal->link('notes', $note); 52 | $note->content = null; 53 | } 54 | 55 | return $this->render('view', ['deal' => $deal, 'note' => $note]); 56 | } 57 | 58 | public function actionSave($id) 59 | { 60 | $deal = Deal::findOne($id); 61 | 62 | if ($deal) { 63 | $deal->load(\Yii::$app->request->post()); 64 | $deal->save(); 65 | } 66 | } 67 | 68 | private function changeStatus($id, $direction) 69 | { 70 | $deal = Deal::findOne($id); 71 | $status = null; 72 | 73 | if (!$deal) { 74 | throw new NotFoundHttpException("Сделка с этим ID не найдена"); 75 | } 76 | 77 | if ($direction === 'next') { 78 | $status = $deal->status->getNextStatus(); 79 | } 80 | else if ($direction === 'prev') { 81 | $status = $deal->status->getPrevStatus(); 82 | } 83 | 84 | if ($status) { 85 | $deal->link('status', $status); 86 | 87 | $feed = new Feed(); 88 | $feed->setAttributes(['type' => Feed::TYPE_STATUS, 'value' => $status->id, 'deal_id' => $deal->id]); 89 | $feed->save(); 90 | } 91 | 92 | return $this->redirect(['deals/view', 'id' => $id]); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /controllers/EntityController.php: -------------------------------------------------------------------------------- 1 | entity; 25 | $model->setScenario('insert'); 26 | 27 | return $this->storeModel($model); 28 | } 29 | 30 | public function actionUpdate($id) 31 | { 32 | $model = $this->entity::findOne($id); 33 | 34 | if ($model) { 35 | $model->setScenario('update'); 36 | 37 | return $this->storeModel($model); 38 | } 39 | } 40 | 41 | public function actionIndex($id = null) 42 | { 43 | $searchModel = $this->entity; 44 | $dataProvider = $searchModel->search(Yii::$app->request->get()); 45 | 46 | $model = $this->entity; 47 | 48 | if ($id) { 49 | $model = $model::findOne($id) ?: $this->entity; 50 | } 51 | 52 | $dataProvider = Yii::configure($dataProvider, [ 53 | 'pagination' => ['pageSize' => 10], 54 | 'sort' => ['defaultOrder' => ['id' => SORT_DESC]] 55 | ]); 56 | 57 | $dataProvider->getModels(); 58 | 59 | return $this->render('index', ['dataProvider' => $dataProvider, 'model' => $model, 'searchModel' => $searchModel]); 60 | } 61 | 62 | public function actionDelete($id) 63 | { 64 | $model = $this->entity::findOne($id); 65 | 66 | if ($model) { 67 | $model->softDelete(); 68 | 69 | return $this->redirect([$this->alias . '/index']); 70 | } 71 | } 72 | 73 | /** 74 | * @param ActiveRecord $model 75 | * @return array|Response 76 | */ 77 | protected function storeModel(ActiveRecord $model) 78 | { 79 | if (Yii::$app->request->isAjax && $model->load(Yii::$app->request->post())) { 80 | Yii::$app->response->format = Response::FORMAT_JSON; 81 | return ActiveForm::validate($model); 82 | } 83 | 84 | if (Yii::$app->request->isPost) { 85 | $model->load(Yii::$app->request->post()); 86 | 87 | if ($model->save()) { 88 | Yii::$app->getSession()->setFlash($this->alias . '_create'); 89 | $redirectUrl = $this->redirectAfterSaveUrl ?: [$this->alias . '/index']; 90 | 91 | return $this->redirect($redirectUrl); 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /controllers/InboxController.php: -------------------------------------------------------------------------------- 1 | prepareClient()) { 30 | header("Location: " . $authCLient->getAuthUrl()); 31 | exit; 32 | } 33 | 34 | $this->mailClient = new GmailClient($authCLient); 35 | } 36 | 37 | public function actionEmail($msgid = null) 38 | { 39 | $selected_message = null; 40 | 41 | $searchForm = new EmailSearchForm(); 42 | $searchForm->load(\Yii::$app->request->get()); 43 | $searchForm->validate(); 44 | 45 | $messages = $this->mailClient->getMessages(10, $searchForm->q); 46 | 47 | if ($msgid) { 48 | $selected_message = $this->mailClient->getMessageById($msgid); 49 | } 50 | 51 | return $this->render('mail', 52 | [ 53 | 'unread_count' => $this->mailClient->getUnreadCount(), 54 | 'messages' => $messages, 55 | 'selected_message' => $selected_message, 56 | 'msgid' => $msgid, 57 | 'model' => $searchForm 58 | ]); 59 | } 60 | 61 | public function actionCreateContact($msgid = null) 62 | { 63 | $message = $this->mailClient->getMessageById($msgid); 64 | $this->addContact($message); 65 | 66 | Yii::$app->getSession()->setFlash('persons_create'); 67 | 68 | return $this->redirect(['contacts/index']); 69 | } 70 | 71 | public function actionCreateDeal($msgid = null) 72 | { 73 | $message = $this->mailClient->getMessageById($msgid); 74 | $contact = $this->addContact($message); 75 | 76 | $deal = new Deal(); 77 | $deal->company_id = Company::find()->one()->id; 78 | $deal->status_id = 1; 79 | $deal->user_id = Yii::$app->user->getId(); 80 | $deal->contact_id = $contact->id; 81 | $deal->name = $message->getSubject(); 82 | $deal->description = $message->getBody(); 83 | 84 | $deal->save(); 85 | 86 | return $this->redirect(['deals/view', 'id' => $deal->id]); 87 | } 88 | 89 | /** 90 | * @param MailMessage $message 91 | * @return Contact 92 | */ 93 | protected function addContact(MailMessage $message) 94 | { 95 | $contact = new Contact(); 96 | $contact->name = $message->getSenderName(); 97 | $contact->email = $message->getSenderAddress(); 98 | $contact->save(); 99 | 100 | return $contact; 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /controllers/LandingController.php: -------------------------------------------------------------------------------- 1 | [ 13 | 'class' => AccessControl::class, 14 | 'denyCallback' => function($rule, $action) { 15 | $this->redirect(['dashboard/index']); 16 | }, 17 | 'rules' => [ 18 | [ 19 | 'allow' => true, 20 | 'roles' => ['?'] 21 | ] 22 | ] 23 | ] 24 | ]; 25 | } 26 | 27 | public function actionIndex() 28 | { 29 | $this->layout = 'anon'; 30 | 31 | return $this->render('index'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /controllers/ProfileController.php: -------------------------------------------------------------------------------- 1 | user->getId()); 15 | $model->scenario = ProfileSettingsForm::SCENARIO_PROFILE; 16 | 17 | if (\Yii::$app->request->isPost) { 18 | $model->load(\Yii::$app->request->post()); 19 | 20 | if ($model->save()) { 21 | $this->goHome(); 22 | } 23 | } 24 | 25 | return $this->render('settings', ['model' => $model]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /controllers/SecuredController.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'class' => AccessControl::class, 15 | 'rules' => [ 16 | [ 17 | 'allow' => true, 18 | 'roles' => ['@'] 19 | ] 20 | ] 21 | ] 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /controllers/TasksController.php: -------------------------------------------------------------------------------- 1 | entity = new Task(); 16 | $this->alias = 'tasks'; 17 | $this->redirectAfterSaveUrl = \Yii::$app->request->getReferrer(); 18 | } 19 | 20 | 21 | } 22 | -------------------------------------------------------------------------------- /controllers/ThirdPartyController.php: -------------------------------------------------------------------------------- 1 | fetchAccessTokenByCode($code); 18 | 19 | if ($accessToken) { 20 | $authCLient->storeToken($accessToken); 21 | 22 | $this->redirect(['inbox/email']); 23 | } 24 | 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /controllers/UserController.php: -------------------------------------------------------------------------------- 1 | [ 19 | 'class' => AccessControl::class, 20 | 'only' => ['index'], 21 | 'rules' => [ 22 | [ 23 | 'allow' => true, 24 | 'actions' => ['login', 'signup'], 25 | 'roles' => ['?'] 26 | ] 27 | ] 28 | ] 29 | ]; 30 | } 31 | 32 | public function actionLogout() 33 | { 34 | \Yii::$app->user->logout(); 35 | 36 | return $this->goHome(); 37 | } 38 | 39 | public function actionSignup() 40 | { 41 | $user = new User(); 42 | 43 | if (Yii::$app->request->getIsPost()) { 44 | $user->load(Yii::$app->request->post()); 45 | 46 | if (Yii::$app->request->isAjax) { 47 | Yii::$app->response->format = Response::FORMAT_JSON; 48 | 49 | return ActiveForm::validate($user); 50 | } 51 | 52 | if ($user->validate()) { 53 | $user->password = Yii::$app->security->generatePasswordHash($user->password); 54 | 55 | $user->save(false); 56 | $this->goHome(); 57 | } 58 | } 59 | 60 | return $this->render('signup', ['model' => $user]); 61 | } 62 | 63 | public function actionLogin() 64 | { 65 | $loginForm = new LoginForm(); 66 | 67 | if (\Yii::$app->request->getIsPost()) { 68 | $loginForm->load(\Yii::$app->request->post()); 69 | 70 | if (Yii::$app->request->isAjax) { 71 | Yii::$app->response->format = Response::FORMAT_JSON; 72 | 73 | return ActiveForm::validate($loginForm); 74 | } 75 | 76 | if ($loginForm->validate()) { 77 | \Yii::$app->user->login($loginForm->getUser()); 78 | 79 | return $this->goHome(); 80 | } 81 | } 82 | 83 | return $this->redirect(['landing/index']); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmlacademy/intensive-php2-turbocrm/ca0596b0da2c3bb573345ee461ad4ec94a289559/data/.gitkeep -------------------------------------------------------------------------------- /fixtures/CompanyFixture.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'name' => 'ООО Урал', 6 | 'email' => 'matvei.ilina@mail.ru', 7 | 'url' => 'http://bykov.org/', 8 | 'address' => '613581, Ярославская область, город Пушкино, пер. Сталина, 24', 9 | 'phone' => '32685905700', 10 | ], 11 | 'company1' => [ 12 | 'name' => 'ЗАО СтройМетал', 13 | 'email' => 'osipova.gordei@krasilnikov.ru', 14 | 'url' => 'http://www.sysoeva.ru/error-enim-consequatur-ipsam-pariatur-et-recusandae-voluptatem-corrupti', 15 | 'address' => '852871, Костромская область, город Дмитров, пер. Ленина, 27', 16 | 'phone' => '49340879653', 17 | ], 18 | 'company2' => [ 19 | 'name' => 'ООО Компания ТехОмск', 20 | 'email' => 'fedorov.stefan@sazonova.ru', 21 | 'url' => 'http://danilova.com/repellendus-temporibus-provident-debitis-in-et', 22 | 'address' => '840940, Астраханская область, город Москва, спуск Будапештсткая, 38', 23 | 'phone' => '65330393576', 24 | ], 25 | 'company3' => [ 26 | 'name' => 'ЗАО Гараж', 27 | 'email' => 'nonna.isakova@nosova.com', 28 | 'url' => 'http://www.belozerova.com/sint-nam-illo-totam-quae-magnam-et-aut-minus.html', 29 | 'address' => '492737, Смоленская область, город Талдом, бульвар Ладыгина, 91', 30 | 'phone' => '45313137534', 31 | ], 32 | 'company4' => [ 33 | 'name' => 'ПАО ЮпитерХмельРыбБанк', 34 | 'email' => 'mukina.alla@rambler.ru', 35 | 'url' => 'http://www.mironova.ru/modi-tempora-repudiandae-illo-labore-expedita-mollitia', 36 | 'address' => '584337, Ульяновская область, город Талдом, спуск Гагарина, 53', 37 | 'phone' => '53486075481', 38 | ], 39 | 'company5' => [ 40 | 'name' => 'МКК ИнфоАвтоАсбоцемент', 41 | 'email' => 'iosif.titova@gmail.com', 42 | 'url' => 'https://www.kopylov.ru/ab-voluptas-voluptatem-et-totam-aspernatur-unde-rem-quis', 43 | 'address' => '375596, Белгородская область, город Ногинск, въезд Бухарестская, 19', 44 | 'phone' => '72848487918', 45 | ], 46 | 'company6' => [ 47 | 'name' => 'ПАО НефтьРосТекстильСнос', 48 | 'email' => 'mariy.ustinov@semenov.ru', 49 | 'url' => 'https://www.doronina.com/voluptatum-rerum-animi-dolores-magnam', 50 | 'address' => '890183, Челябинская область, город Озёры, проезд Бухарестская, 49', 51 | 'phone' => '40999921837', 52 | ], 53 | 'company7' => [ 54 | 'name' => 'ООО Орион', 55 | 'email' => 'enikitina@karpov.ru', 56 | 'url' => 'https://www.gurev.ru/repellendus-sit-deleniti-libero-laudantium-non-quaerat', 57 | 'address' => '153914, Амурская область, город Можайск, пл. Косиора, 45', 58 | 'phone' => '30946316835', 59 | ], 60 | 'company8' => [ 61 | 'name' => 'ООО ITМикроЛенНаладка', 62 | 'email' => 'roman.ermakova@nosov.net', 63 | 'url' => 'http://komissarov.ru/excepturi-sed-assumenda-deserunt-ipsa-delectus-eius-voluptas', 64 | 'address' => '290008, Тверская область, город Солнечногорск, бульвар Чехова, 77', 65 | 'phone' => '78230275737', 66 | ], 67 | 'company9' => [ 68 | 'name' => 'ОАО СервисПивCиб', 69 | 'email' => 'egrisin@gmail.com', 70 | 'url' => 'http://www.loginov.com/qui-eius-voluptatum-accusantium-labore', 71 | 'address' => '012557, Курганская область, город Подольск, бульвар Космонавтов, 05', 72 | 'phone' => '89395524400', 73 | ], 74 | ]; 75 | -------------------------------------------------------------------------------- /fixtures/data/contact.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'name' => 'Ева Борисовна Игнатьева', 6 | 'email' => 'ybykova@fedorova.ru', 7 | 'phone' => '22994302030', 8 | 'position' => 'Штурман', 9 | 'type_id' => 3, 10 | 'company_id' => 1, 11 | ], 12 | 'contact1' => [ 13 | 'name' => 'Денисова Анфиса Андреевна', 14 | 'email' => 'sofia76@kolobov.ru', 15 | 'phone' => '23880389440', 16 | 'position' => 'Бариста', 17 | 'type_id' => 2, 18 | 'company_id' => 8, 19 | ], 20 | 'contact2' => [ 21 | 'name' => 'Самойлов Даниил Фёдорович', 22 | 'email' => 'simonova.eva@list.ru', 23 | 'phone' => '20958934141', 24 | 'position' => 'Пианист', 25 | 'type_id' => 3, 26 | 'company_id' => 7, 27 | ], 28 | 'contact3' => [ 29 | 'name' => 'Нестеровa Василий Андреевич', 30 | 'email' => 'nazar25@yandex.ru', 31 | 'phone' => '12279464923', 32 | 'position' => 'Водолаз', 33 | 'type_id' => 2, 34 | 'company_id' => 2, 35 | ], 36 | 'contact4' => [ 37 | 'name' => 'Зыковa Марк Иванович', 38 | 'email' => 'evgenij38@kondrateva.ru', 39 | 'phone' => '36363487466', 40 | 'position' => 'Чабан', 41 | 'type_id' => 1, 42 | 'company_id' => 4, 43 | ], 44 | 'contact5' => [ 45 | 'name' => 'Третьяков Адам Романович', 46 | 'email' => 'tamara47@fomin.com', 47 | 'phone' => '19815019587', 48 | 'position' => 'Телефонистка', 49 | 'type_id' => 3, 50 | 'company_id' => 10, 51 | ], 52 | 'contact6' => [ 53 | 'name' => 'Гаврилов Данила Фёдорович', 54 | 'email' => 'akim93@merkusev.com', 55 | 'phone' => '66803925793', 56 | 'position' => 'Администратор', 57 | 'type_id' => 1, 58 | 'company_id' => 8, 59 | ], 60 | 'contact7' => [ 61 | 'name' => 'Дорофеевaа Рада Ивановна', 62 | 'email' => 'gerasimova.faina@kovaleva.net', 63 | 'phone' => '70682402022', 64 | 'position' => 'Экономист', 65 | 'type_id' => 1, 66 | 'company_id' => 8, 67 | ], 68 | 'contact8' => [ 69 | 'name' => 'Киселёвa Илья Иванович', 70 | 'email' => 'evgenij22@inbox.ru', 71 | 'phone' => '94753181382', 72 | 'position' => 'Продюсер', 73 | 'type_id' => 3, 74 | 'company_id' => 10, 75 | ], 76 | 'contact9' => [ 77 | 'name' => 'Валентина Борисовна Калашниковaа', 78 | 'email' => 'zlata97@bk.ru', 79 | 'phone' => '85996384373', 80 | 'position' => 'Бариста', 81 | 'type_id' => 1, 82 | 'company_id' => 1, 83 | ], 84 | 'contact10' => [ 85 | 'name' => 'Мария Сергеевна Ершовaа', 86 | 'email' => 'leonid27@larionova.net', 87 | 'phone' => '21279119931', 88 | 'position' => 'Информационный работник', 89 | 'type_id' => 3, 90 | 'company_id' => 10, 91 | ], 92 | 'contact11' => [ 93 | 'name' => 'Лапинaа София Евгеньевна', 94 | 'email' => 'wbaranov@trofimova.ru', 95 | 'phone' => '98941878339', 96 | 'position' => 'Горный проводник', 97 | 'type_id' => 2, 98 | 'company_id' => 9, 99 | ], 100 | 'contact12' => [ 101 | 'name' => 'Белозёровa Валентин Владимирович', 102 | 'email' => 'safonova.dobryna@mail.ru', 103 | 'phone' => '31591682503', 104 | 'position' => 'Сторож', 105 | 'type_id' => 1, 106 | 'company_id' => 9, 107 | ], 108 | 'contact13' => [ 109 | 'name' => 'Титов Даниил Александрович', 110 | 'email' => 'adam72@turov.ru', 111 | 'phone' => '49397903242', 112 | 'position' => 'Садовник', 113 | 'type_id' => 1, 114 | 'company_id' => 5, 115 | ], 116 | 'contact14' => [ 117 | 'name' => 'Стрелков Адам Фёдорович', 118 | 'email' => 'markov.filipp@maslov.com', 119 | 'phone' => '91013102863', 120 | 'position' => 'Продюсер', 121 | 'type_id' => 3, 122 | 'company_id' => 3, 123 | ], 124 | 'contact15' => [ 125 | 'name' => 'Костин Болеслав Борисович', 126 | 'email' => 'qpetuhova@nekrasova.com', 127 | 'phone' => '61567462715', 128 | 'position' => 'Телефонистка', 129 | 'type_id' => 3, 130 | 'company_id' => 6, 131 | ], 132 | 'contact16' => [ 133 | 'name' => 'Ярослав Фёдорович Зайцев', 134 | 'email' => 'saskov.regina@list.ru', 135 | 'phone' => '73589680367', 136 | 'position' => 'Лингвист', 137 | 'type_id' => 1, 138 | 'company_id' => 4, 139 | ], 140 | 'contact17' => [ 141 | 'name' => 'Лев Романович Рожковa', 142 | 'email' => 'kulagina.sava@galkin.com', 143 | 'phone' => '53582467175', 144 | 'position' => 'Егерь', 145 | 'type_id' => 3, 146 | 'company_id' => 8, 147 | ], 148 | 'contact18' => [ 149 | 'name' => 'Василиса Владимировна Смирновaа', 150 | 'email' => 'mihail.ersova@eliseev.ru', 151 | 'phone' => '98784691882', 152 | 'position' => 'Учёный', 153 | 'type_id' => 1, 154 | 'company_id' => 2, 155 | ], 156 | 'contact19' => [ 157 | 'name' => 'Фаина Борисовна Щукинaа', 158 | 'email' => 'sobolev.bogdan@osipova.ru', 159 | 'phone' => '31478028677', 160 | 'position' => 'Информационный работник', 161 | 'type_id' => 3, 162 | 'company_id' => 4, 163 | ], 164 | ]; 165 | -------------------------------------------------------------------------------- /fixtures/data/deal_status.php: -------------------------------------------------------------------------------- 1 | ['name' => 'Новый'], 5 | 'status2' => ['name' => 'Презентация'], 6 | 'status3' => ['name' => 'В работе'], 7 | 'status4' => ['name' => 'Завершено'], 8 | ]; 9 | -------------------------------------------------------------------------------- /fixtures/data/note.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'content' => 'А вице-губернатор, не правда ли, прелюбезная женщина? — О, это одна из достойнейших женщин, каких.', 6 | 'user_id' => 3, 7 | 'deal_id' => 5, 8 | ], 9 | 'note1' => [ 10 | 'content' => 'К тому ж дело было совсем невыгодно. — Так ты не можешь, подлец! когда увидел, что раньше пяти.', 11 | 'user_id' => 1, 12 | 'deal_id' => 2, 13 | ], 14 | 'note2' => [ 15 | 'content' => 'Алкида, и Алкид, зажмурив глаза и открыв рот, готов был зарыдать самым жалким образом, но.', 16 | 'user_id' => 5, 17 | 'deal_id' => 5, 18 | ], 19 | 'note3' => [ 20 | 'content' => 'Собакевич и потом продолжал вслух с «некоторою досадою: — Да на что Чичиков отвечал всякий раз.', 21 | 'user_id' => 4, 22 | 'deal_id' => 1, 23 | ], 24 | 'note4' => [ 25 | 'content' => 'Правда, с такой дороги и очень хорошо тебя знаю. — Такая, право, ракалия! Ну, послушай, хочешь.', 26 | 'user_id' => 2, 27 | 'deal_id' => 5, 28 | ], 29 | 'note5' => [ 30 | 'content' => 'Резные узорочные карнизы из свежего дерева вокруг окон и под ним находилось пространство, занятое.', 31 | 'user_id' => 1, 32 | 'deal_id' => 13, 33 | ], 34 | 'note6' => [ 35 | 'content' => 'Впрочем, что до меня, — сказал Манилов с улыбкою и от удовольствия — почти совсем зажмурил глаза.', 36 | 'user_id' => 5, 37 | 'deal_id' => 11, 38 | ], 39 | 'note7' => [ 40 | 'content' => 'Давай его, клади сюда на пол! Порфирий положил щенка на пол, который, растянувшись на все руки. В.', 41 | 'user_id' => 3, 42 | 'deal_id' => 13, 43 | ], 44 | 'note8' => [ 45 | 'content' => 'Подъезжая ко двору, Чичиков заметил на крыльце и, как видно, была мастерица взбивать перины.', 46 | 'user_id' => 1, 47 | 'deal_id' => 5, 48 | ], 49 | 'note9' => [ 50 | 'content' => 'Чичиков объяснил ей, что эта бумага не такого роду, чтобы быть вверену Ноздреву… Ноздрев.', 51 | 'user_id' => 4, 52 | 'deal_id' => 14, 53 | ], 54 | 'note10' => [ 55 | 'content' => 'Вот все, что ни привезли из — деревни, продали по самой выгоднейшей цене. Эх, братец, как — у.', 56 | 'user_id' => 4, 57 | 'deal_id' => 2, 58 | ], 59 | 'note11' => [ 60 | 'content' => 'Это вам так показалось: он только топырится или горячится, как корамора!»[[3 - Корамора — большой.', 61 | 'user_id' => 3, 62 | 'deal_id' => 12, 63 | ], 64 | 'note12' => [ 65 | 'content' => 'Посередине столовой стояли деревянные козлы, и два ружья — одно в триста, а другое в восемьсот.', 66 | 'user_id' => 3, 67 | 'deal_id' => 7, 68 | ], 69 | 'note13' => [ 70 | 'content' => 'Чичиков вошел боком в столовую. — Прощайте, мои крошки. Вы — давайте настоящую цену! «Ну, уж черт.', 71 | 'user_id' => 5, 72 | 'deal_id' => 18, 73 | ], 74 | 'note14' => [ 75 | 'content' => 'Ведь он не без старания очень красивыми рядками. Заметно было, что это сущее ничего, что он.', 76 | 'user_id' => 2, 77 | 'deal_id' => 1, 78 | ], 79 | 'note15' => [ 80 | 'content' => 'Вы собирали его, может быть, это вам так показалось. Ведь я знаю тебя: ведь ты подлец, ведь ты.', 81 | 'user_id' => 4, 82 | 'deal_id' => 5, 83 | ], 84 | 'note16' => [ 85 | 'content' => 'Веришь ли, что — губы его шевелились без звука. — Бейте его! — Ты их продашь, тебе на первой.', 86 | 'user_id' => 5, 87 | 'deal_id' => 12, 88 | ], 89 | 'note17' => [ 90 | 'content' => 'О себе приезжий, как казалось, пробиралась в дамки; — откуда она взялась это один только бог знал.', 91 | 'user_id' => 4, 92 | 'deal_id' => 17, 93 | ], 94 | 'note18' => [ 95 | 'content' => 'Между крепкими греками, неизвестно каким образом и для бала; коляска с шестериком коней и почти.', 96 | 'user_id' => 3, 97 | 'deal_id' => 15, 98 | ], 99 | 'note19' => [ 100 | 'content' => 'А в плечищах у него — со страхом. — Да не нужно знать, какие у вас душа человеческая все равно что.', 101 | 'user_id' => 4, 102 | 'deal_id' => 19, 103 | ], 104 | 'note20' => [ 105 | 'content' => 'Собакевичу. «А что ж, — подумал Чичиков в довольном расположении духа сидел в своей бричке.', 106 | 'user_id' => 1, 107 | 'deal_id' => 18, 108 | ], 109 | 'note21' => [ 110 | 'content' => 'Повторивши это раза три, он попросил хозяйку приказать заложить его бричку. Настасья Петровна тут.', 111 | 'user_id' => 4, 112 | 'deal_id' => 5, 113 | ], 114 | 'note22' => [ 115 | 'content' => 'Куда ж? — сказал наконец Чичиков, изумленный в самом деле, пирог сам по себе был вкусен, а после.', 116 | 'user_id' => 1, 117 | 'deal_id' => 1, 118 | ], 119 | 'note23' => [ 120 | 'content' => 'Чичиков дал ей медный грош, и она побрела восвояси, уже довольная тем, что посидела на козлах.', 121 | 'user_id' => 4, 122 | 'deal_id' => 1, 123 | ], 124 | 'note24' => [ 125 | 'content' => 'Итак, вот что на картинах не всё пустые вопросы; он с чрезвычайною точностию расспросил, кто в.', 126 | 'user_id' => 3, 127 | 'deal_id' => 12, 128 | ], 129 | 'note25' => [ 130 | 'content' => 'Полицеймейстеру сказал что-то очень лестное насчет городских будочников; а в третью скажешь: «Черт.', 131 | 'user_id' => 5, 132 | 'deal_id' => 6, 133 | ], 134 | 'note26' => [ 135 | 'content' => 'Само собою разумеется, что ротик раскрывался при этом случае очень грациозно. Ко дню рождения.', 136 | 'user_id' => 5, 137 | 'deal_id' => 5, 138 | ], 139 | 'note27' => [ 140 | 'content' => 'Одичаешь, — знаете, будешь все время игры. Выходя с фигуры, он ударял по столу крепко рукою.', 141 | 'user_id' => 5, 142 | 'deal_id' => 1, 143 | ], 144 | 'note28' => [ 145 | 'content' => 'Ну уж, пожалуйста, меня-то отпусти, — говорил Чичиков. — Как, губернатор разбойник? — сказал.', 146 | 'user_id' => 5, 147 | 'deal_id' => 18, 148 | ], 149 | 'note29' => [ 150 | 'content' => 'В следующую за тем показалась гостям шарманка. Ноздрев тут же из-под козел какую-то дрянь из.', 151 | 'user_id' => 4, 152 | 'deal_id' => 19, 153 | ], 154 | ]; 155 | -------------------------------------------------------------------------------- /fixtures/data/user.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'email' => 'platon60@ya.ru', 6 | 'company' => 'ОАО Мяс', 7 | 'phone' => '36829936869', 8 | 'password' => '$2y$13$BXp8ZW2gw.YH9dqjU6..lOBBnP/X.zmvyLkc69T3bAh2woDHM8Z.y', 9 | 'name' => 'Стефан Алексеевич Рябовa', 10 | 'position' => 'Абразивоструйщик', 11 | ], 12 | 'user1' => [ 13 | 'email' => 'cdanilova@inbox.ru', 14 | 'company' => 'МФО КазаньГараж', 15 | 'phone' => '71817142464', 16 | 'password' => '$2y$13$Z3RI9lIL5QwzvtaWU4ccKOiy27hGmnQdGobimN6Km3tqpv/U0upLO', 17 | 'name' => 'Моисеевa Виль Фёдорович', 18 | 'position' => 'Сторож', 19 | ], 20 | 'user2' => [ 21 | 'email' => 'rafail93@bk.ru', 22 | 'company' => 'МФО Каз', 23 | 'phone' => '84796102355', 24 | 'password' => '$2y$13$G8e5q2sZnKAuJrxnJVhMuee3cHrVl6tdRPd4DO.QlRaSkGk.myENm', 25 | 'name' => 'Гусев Алексей Алексеевич', 26 | 'position' => 'Бизнес-аналитик', 27 | ], 28 | 'user3' => [ 29 | 'email' => 'lev.vladimirova@gusev.com', 30 | 'company' => 'ОАО Телеком', 31 | 'phone' => '96883337879', 32 | 'password' => '$2y$13$BNknH236GWW.09HQbyqDS.jBiGg9.iLda.yzsirOWzW8Ptvxt9JDe', 33 | 'name' => 'Симоновaа Любовь Алексеевна', 34 | 'position' => 'Учёный', 35 | ], 36 | 'user4' => [ 37 | 'email' => 'iosif78@inbox.ru', 38 | 'company' => 'ОАО ХмельНефть', 39 | 'phone' => '12048701671', 40 | 'password' => '$2y$13$ou.NL04.IvbzWdtdUtdjF.GZl0xkEF4x5oBhzU634dswmqOzpF6Ey', 41 | 'name' => 'Фёдор Александрович Тетеринa', 42 | 'position' => 'Продюсер', 43 | ], 44 | ]; 45 | -------------------------------------------------------------------------------- /fixtures/templates/company.php: -------------------------------------------------------------------------------- 1 | $faker->company, 8 | 'email' => $faker->email, 9 | 'url' => $faker->url, 10 | 'address' => $faker->address, 11 | 'phone' => substr($faker->e164PhoneNumber, 1, 11) 12 | ]; 13 | -------------------------------------------------------------------------------- /fixtures/templates/contact.php: -------------------------------------------------------------------------------- 1 | $faker->name, 9 | 'email' => $faker->email, 10 | 'phone' => substr($faker->e164PhoneNumber, 1, 11), 11 | 'position' => $faker->jobTitle, 12 | 'type_id' => rand(1, 3), 13 | 'company_id' => rand(1, 10) 14 | ]; 15 | -------------------------------------------------------------------------------- /fixtures/templates/deal.php: -------------------------------------------------------------------------------- 1 | $faker->sentence, 9 | 'company_id' => rand(1, 10), 10 | 'status_id' => rand(1, 4), 11 | 'contact_id' => rand(1, 10), 12 | 'executor_id' => rand(1, 5), 13 | 'user_id' => rand(1, 5), 14 | 'due_date' => $faker->dateTimeBetween('now', '+2 years')->format('Y-m-d'), 15 | 'description' => $faker->paragraph(2), 16 | 'budget_amount' => $faker->numberBetween(5000, 500000) 17 | ]; 18 | -------------------------------------------------------------------------------- /fixtures/templates/note.php: -------------------------------------------------------------------------------- 1 | $faker->realText(100), 8 | 'user_id' => rand(1, 5), 9 | 'deal_id' => rand(1, 20) 10 | ]; 11 | -------------------------------------------------------------------------------- /fixtures/templates/task.php: -------------------------------------------------------------------------------- 1 | $faker->text(150), 8 | 'executor_id' => rand(1, 5), 9 | 'deal_id' => rand(1, 20), 10 | 'due_date' => $faker->dateTimeBetween('now', '+6 months')->format('Y-m-d'), 11 | 'type_id' => rand(1, 5), 12 | 'dt_add' => $faker->dateTimeBetween('now', '+1 months')->format('Y-m-d'), 13 | ]; 14 | -------------------------------------------------------------------------------- /fixtures/templates/user.php: -------------------------------------------------------------------------------- 1 | $faker->email, 8 | 'company' => $faker->company, 9 | 'phone' => substr($faker->e164PhoneNumber, 1, 11), 10 | 'password' => Yii::$app->getSecurity()->generatePasswordHash('password_' . $index), 11 | 'name' => $faker->name, 12 | 'position' => $faker->jobTitle 13 | ]; 14 | -------------------------------------------------------------------------------- /helpers/DashboardPresentation.php: -------------------------------------------------------------------------------- 1 | orderBy('due_date ASC')->limit($limit); 20 | 21 | return $query->all(); 22 | } 23 | 24 | public function getNewestCompanies($limit = 2) 25 | { 26 | $query = Company::find(); 27 | $query->orderBy('dt_add DESC')->limit($limit); 28 | 29 | return $query->all(); 30 | } 31 | 32 | public function getFinanceOverview() 33 | { 34 | $query = Deal::find(); 35 | 36 | $forecast_amount = $query->where(['deleted' => 0])->sum('budget_amount'); 37 | $current_amount = $query->where(['deleted' => 0, 'status_id' => 4])->sum('budget_amount'); 38 | 39 | return [$forecast_amount, $current_amount]; 40 | } 41 | 42 | public function getDealsCounters() 43 | { 44 | $counters = []; 45 | 46 | foreach (DealStatus::find()->all() as $status) { 47 | $counters[$status->name] = $status->dealsCount; 48 | } 49 | 50 | return $counters; 51 | } 52 | 53 | public function getCommonCounters() 54 | { 55 | $tasks = Task::find()->count(); 56 | $users = User::find()->count(); 57 | 58 | $companies = Company::find()->count(); 59 | $contacts = Contact::find()->count(); 60 | 61 | $complete_deals = Deal::find()->where(['status_id' => 4])->count(); 62 | 63 | return compact(['tasks', 'users', 'companies', 'contacts', 'complete_deals']); 64 | } 65 | 66 | public function getWeekPerformance() 67 | { 68 | $sql = 'SELECT WEEKDAY(dt_add) wday, COUNT(id) cnt FROM feed WHERE dt_add >= DATE_SUB(NOW(), INTERVAL 1 WEEK) GROUP BY wday'; 69 | 70 | $rows = \Yii::$app->db->createCommand($sql)->queryAll(); 71 | $rows = ArrayHelper::map($rows, 'wday', 'cnt'); 72 | 73 | $wdays = array_fill(0, 7, 0); 74 | $result = array_replace($wdays, $rows); 75 | 76 | return $result; 77 | 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /interfaces/PersonInterface.php: -------------------------------------------------------------------------------- 1 | createTable('user', [ 16 | 'id' => $this->primaryKey()->notNull(), 17 | 'email' => $this->char(128)->notNull()->unique(), 18 | 'company' => $this->char(128), 19 | 'phone' => $this->char(11)->notNull(), 20 | 'password' => $this->char(64)->notNull() 21 | ]); 22 | 23 | $this->createTable('company', [ 24 | 'id' => $this->primaryKey()->notNull(), 25 | 'email' => $this->char(128)->notNull(), 26 | 'name' => $this->char(128), 27 | 'url' => $this->char(128), 28 | 'phone' => $this->char(11), 29 | 'address' => $this->char(255), 30 | 'dt_add' => $this->dateTime()->defaultValue(new \yii\db\Expression('NOW()')) 31 | ]); 32 | 33 | $this->createTable('contact', [ 34 | 'id' => $this->primaryKey()->notNull(), 35 | 'email' => $this->char(128)->notNull(), 36 | 'name' => $this->char(128), 37 | 'position' => $this->char(128), 38 | 'phone' => $this->char(11), 39 | 'dt_add' => $this->dateTime()->defaultValue(new \yii\db\Expression('NOW()')) 40 | ]); 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function safeDown() 47 | { 48 | echo "m200204_085219_create_tables cannot be reverted.\n"; 49 | 50 | return false; 51 | } 52 | 53 | /* 54 | // Use up()/down() to run migration code without a transaction. 55 | public function up() 56 | { 57 | 58 | } 59 | 60 | public function down() 61 | { 62 | echo "m200204_085219_create_tables cannot be reverted.\n"; 63 | 64 | return false; 65 | } 66 | */ 67 | } 68 | -------------------------------------------------------------------------------- /migrations/m200613_144405_contacts.php: -------------------------------------------------------------------------------- 1 | createTable('contact_types', [ 16 | 'id' => $this->primaryKey(), 17 | 'name' => $this->char(255)->unique() 18 | ]); 19 | 20 | $this->addColumn('contact', 'type_id', $this->integer()->unsigned()); 21 | $this->addColumn('contact', 'company_id', $this->integer()->unsigned()); 22 | 23 | $this->batchInsert('contact_types', ['name'], [['Новый'], ['Активный'], ['Архив']]); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function safeDown() 30 | { 31 | echo "m200613_144405_contacts cannot be reverted.\n"; 32 | 33 | return false; 34 | } 35 | 36 | /* 37 | // Use up()/down() to run migration code without a transaction. 38 | public function up() 39 | { 40 | 41 | } 42 | 43 | public function down() 44 | { 45 | echo "m200613_144405_contacts cannot be reverted.\n"; 46 | 47 | return false; 48 | } 49 | */ 50 | } 51 | -------------------------------------------------------------------------------- /migrations/m200729_174942_deals.php: -------------------------------------------------------------------------------- 1 | createTable('deal', [ 16 | 'id' => $this->primaryKey(), 17 | 'name' => $this->char(255), 18 | 'company_id' => $this->integer()->unsigned()->notNull(), 19 | 'status_id' => $this->integer()->unsigned(), 20 | 'contact_id' => $this->integer()->unsigned(), 21 | 'executor_id' => $this->integer()->unsigned(), 22 | 'due_date' => $this->date(), 23 | 'description' => $this->text(), 24 | 'budget_amount' => $this->integer(), 25 | 'dt_add' => $this->dateTime()->defaultValue(new \yii\db\Expression('NOW()')) 26 | ]); 27 | 28 | $this->createTable('deal_status', [ 29 | 'id' => $this->primaryKey(), 30 | 'name' => $this->char(255)->unique() 31 | ]); 32 | 33 | $this->createTable('deal_tag', [ 34 | 'id' => $this->primaryKey(), 35 | 'name' => $this->char(255), 36 | 'deal_id' => $this->integer()->unsigned(), 37 | ]); 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function safeDown() 44 | { 45 | echo "m200729_174942_deals cannot be reverted.\n"; 46 | 47 | return false; 48 | } 49 | 50 | /* 51 | // Use up()/down() to run migration code without a transaction. 52 | public function up() 53 | { 54 | 55 | } 56 | 57 | public function down() 58 | { 59 | echo "m200729_174942_deals cannot be reverted.\n"; 60 | 61 | return false; 62 | } 63 | */ 64 | } 65 | -------------------------------------------------------------------------------- /migrations/m200821_144606_deal_notes.php: -------------------------------------------------------------------------------- 1 | createTable('note', [ 17 | 'id' => $this->primaryKey(), 18 | 'deal_id' => $this->integer()->notNull(), 19 | 'dt_add' => $this->dateTime()->defaultValue(new Expression('NOW()')), 20 | 'user_id' => $this->integer()->notNull(), 21 | 'content' => $this->text() 22 | ]); 23 | 24 | $this->addForeignKey('user_note', 'note', 'user_id', 'user', 'id'); 25 | $this->addForeignKey('deal_note', 'note', 'deal_id', 'deal', 'id'); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function safeDown() 32 | { 33 | echo "m200821_144606_deal_notes cannot be reverted.\n"; 34 | 35 | return false; 36 | } 37 | 38 | /* 39 | // Use up()/down() to run migration code without a transaction. 40 | public function up() 41 | { 42 | 43 | } 44 | 45 | public function down() 46 | { 47 | echo "m200821_144606_deal_notes cannot be reverted.\n"; 48 | 49 | return false; 50 | } 51 | */ 52 | } 53 | -------------------------------------------------------------------------------- /migrations/m200825_064858_feed.php: -------------------------------------------------------------------------------- 1 | createTable('feed', [ 17 | 'id' => $this->primaryKey()->notNull(), 18 | 'type' => $this->char(32)->notNull(), 19 | 'dt_add' => $this->dateTime()->defaultValue(new Expression('NOW()')), 20 | 'user_id' => $this->integer()->notNull(), 21 | 'deal_id' => $this->integer()->notNull(), 22 | 'value' => $this->char(255)->notNull() 23 | ]); 24 | 25 | $this->addForeignKey('feed_user', 'feed', 'user_id', 'user', 'id'); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function safeDown() 32 | { 33 | echo "m200825_064858_feed cannot be reverted.\n"; 34 | 35 | return false; 36 | } 37 | 38 | /* 39 | // Use up()/down() to run migration code without a transaction. 40 | public function up() 41 | { 42 | 43 | } 44 | 45 | public function down() 46 | { 47 | echo "m200825_064858_feed cannot be reverted.\n"; 48 | 49 | return false; 50 | } 51 | */ 52 | } 53 | -------------------------------------------------------------------------------- /migrations/m200825_072235_deal_owner.php: -------------------------------------------------------------------------------- 1 | addColumn('deal', 'user_id', 'integer'); 16 | $this->addForeignKey('deal_owner', 'deal', 'user_id', 'user', 'id'); 17 | } 18 | 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function safeDown() 23 | { 24 | echo "m200825_072235_deal_owner cannot be reverted.\n"; 25 | 26 | return false; 27 | } 28 | 29 | /* 30 | // Use up()/down() to run migration code without a transaction. 31 | public function up() 32 | { 33 | 34 | } 35 | 36 | public function down() 37 | { 38 | echo "m200825_072235_deal_owner cannot be reverted.\n"; 39 | 40 | return false; 41 | } 42 | */ 43 | } 44 | -------------------------------------------------------------------------------- /migrations/m200909_075638_user_fields.php: -------------------------------------------------------------------------------- 1 | addColumn('user', 'name', $this->char(255)); 16 | $this->addColumn('user', 'position', $this->char(255)); 17 | } 18 | 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function safeDown() 23 | { 24 | echo "m200909_075638_user_fields cannot be reverted.\n"; 25 | 26 | return false; 27 | } 28 | 29 | /* 30 | // Use up()/down() to run migration code without a transaction. 31 | public function up() 32 | { 33 | 34 | } 35 | 36 | public function down() 37 | { 38 | echo "m200909_075638_user_fields cannot be reverted.\n"; 39 | 40 | return false; 41 | } 42 | */ 43 | } 44 | -------------------------------------------------------------------------------- /migrations/m200915_133435_softdelete.php: -------------------------------------------------------------------------------- 1 | addColumn('user', 'deleted', $this->boolean()->defaultValue(0)); 16 | $this->addColumn('company', 'deleted', $this->boolean()->defaultValue(0)); 17 | $this->addColumn('deal', 'deleted', $this->boolean()->defaultValue(0)); 18 | $this->addColumn('note', 'deleted', $this->boolean()->defaultValue(0)); 19 | $this->addColumn('contact', 'deleted', $this->boolean()->defaultValue(0)); 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function safeDown() 26 | { 27 | echo "m200915_133435_softdelete cannot be reverted.\n"; 28 | 29 | return false; 30 | } 31 | 32 | /* 33 | // Use up()/down() to run migration code without a transaction. 34 | public function up() 35 | { 36 | 37 | } 38 | 39 | public function down() 40 | { 41 | echo "m200915_133435_softdelete cannot be reverted.\n"; 42 | 43 | return false; 44 | } 45 | */ 46 | } 47 | -------------------------------------------------------------------------------- /migrations/m201127_083958_status_alias.php: -------------------------------------------------------------------------------- 1 | addColumn('deal_status', 'alias', $this->string()); 16 | 17 | $this->update('deal_status', ['alias' => 'new'], ['id' => 1]); 18 | $this->update('deal_status', ['alias' => 'presentation'], ['id' => 2]); 19 | $this->update('deal_status', ['alias' => 'in-work'], ['id' => 3]); 20 | $this->update('deal_status', ['alias' => 'completed'], ['id' => 4]); 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function safeDown() 27 | { 28 | echo "m201127_083958_status_alias cannot be reverted.\n"; 29 | 30 | return false; 31 | } 32 | 33 | /* 34 | // Use up()/down() to run migration code without a transaction. 35 | public function up() 36 | { 37 | 38 | } 39 | 40 | public function down() 41 | { 42 | echo "m201127_083958_status_alias cannot be reverted.\n"; 43 | 44 | return false; 45 | } 46 | */ 47 | } 48 | -------------------------------------------------------------------------------- /migrations/m201127_122112_tasks.php: -------------------------------------------------------------------------------- 1 | createTable('task_types', [ 16 | 'id' => $this->primaryKey()->notNull(), 17 | 'name' => $this->char(255)->unique() 18 | ]); 19 | 20 | $this->batchInsert('task_types', ['name'], [ 21 | ['Разработка'], ['Маркетинг'], ['Дизайн'], ['Аналитика'], ['Копирайтинг'], 22 | ]); 23 | 24 | $this->createTable('task', [ 25 | 'id' => $this->primaryKey()->notNull(), 26 | 'description' => $this->text()->notNull(), 27 | 'executor_id' => $this->integer()->notNull(), 28 | 'due_date' => $this->date(), 29 | 'type_id' => $this->integer()->notNull(), 30 | 'dt_add' => $this->dateTime()->defaultValue(new \yii\db\Expression('NOW()')), 31 | 'deal_id' => $this->integer()->notNull() 32 | ]); 33 | 34 | $this->addForeignKey('task_type_id', 'task', 'type_id', 'task_types', 'id'); 35 | $this->addForeignKey('task_deal_id', 'task', 'deal_id', 'deal', 'id'); 36 | $this->addForeignKey('task_executor_id', 'task', 'executor_id', 'user', 'id'); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function safeDown() 43 | { 44 | echo "m201127_122112_tasks cannot be reverted.\n"; 45 | 46 | return false; 47 | } 48 | 49 | /* 50 | // Use up()/down() to run migration code without a transaction. 51 | public function up() 52 | { 53 | 54 | } 55 | 56 | public function down() 57 | { 58 | echo "m201127_122112_tasks cannot be reverted.\n"; 59 | 60 | return false; 61 | } 62 | */ 63 | } 64 | -------------------------------------------------------------------------------- /migrations/m201202_134344_testuser.php: -------------------------------------------------------------------------------- 1 | insert('user', [ 16 | 'email' => 'demo@demo.ru', 'password' => Yii::$app->security->generatePasswordHash('demo'), 17 | 'company' => 'htmlacademy', 'name' => 'Тестовый пользователь', 'position' => 'Администратор', 18 | 'phone' => '84796102359' 19 | ]); 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function safeDown() 26 | { 27 | echo "m201202_134344_testuser cannot be reverted.\n"; 28 | 29 | return false; 30 | } 31 | 32 | /* 33 | // Use up()/down() to run migration code without a transaction. 34 | public function up() 35 | { 36 | 37 | } 38 | 39 | public function down() 40 | { 41 | echo "m201202_134344_testuser cannot be reverted.\n"; 42 | 43 | return false; 44 | } 45 | */ 46 | } 47 | -------------------------------------------------------------------------------- /models/BaseUser.php: -------------------------------------------------------------------------------- 1 | self::STATUS_INACTIVE], 43 | ['status', 'in', 'range' => [self::STATUS_ACTIVE, self::STATUS_INACTIVE, self::STATUS_DELETED]], 44 | ]; 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public static function findIdentity($id) 51 | { 52 | return static::findOne(['id' => $id, 'status' => self::STATUS_ACTIVE]); 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public static function findIdentityByAccessToken($token, $type = null) 59 | { 60 | throw new NotSupportedException('"findIdentityByAccessToken" is not implemented.'); 61 | } 62 | 63 | /** 64 | * Finds user by username 65 | * 66 | * @param string $username 67 | * @return static|null 68 | */ 69 | public static function findByUsername($username) 70 | { 71 | return static::findOne(['email' => $username]); 72 | } 73 | 74 | /** 75 | * Finds user by password reset token 76 | * 77 | * @param string $token password reset token 78 | * @return static|null 79 | */ 80 | public static function findByPasswordResetToken($token) 81 | { 82 | if (!static::isPasswordResetTokenValid($token)) { 83 | return null; 84 | } 85 | 86 | return static::findOne([ 87 | 'password_reset_token' => $token, 88 | 'status' => self::STATUS_ACTIVE, 89 | ]); 90 | } 91 | 92 | /** 93 | * Finds user by verification email token 94 | * 95 | * @param string $token verify email token 96 | * @return static|null 97 | */ 98 | public static function findByVerificationToken($token) { 99 | return static::findOne([ 100 | 'verification_token' => $token, 101 | 'status' => self::STATUS_INACTIVE 102 | ]); 103 | } 104 | 105 | /** 106 | * Finds out if password reset token is valid 107 | * 108 | * @param string $token password reset token 109 | * @return bool 110 | */ 111 | public static function isPasswordResetTokenValid($token) 112 | { 113 | if (empty($token)) { 114 | return false; 115 | } 116 | 117 | $timestamp = (int) substr($token, strrpos($token, '_') + 1); 118 | $expire = Yii::$app->params['user.passwordResetTokenExpire']; 119 | return $timestamp + $expire >= time(); 120 | } 121 | 122 | /** 123 | * {@inheritdoc} 124 | */ 125 | public function getId() 126 | { 127 | return $this->getPrimaryKey(); 128 | } 129 | 130 | /** 131 | * {@inheritdoc} 132 | */ 133 | public function getAuthKey() 134 | { 135 | return $this->auth_key; 136 | } 137 | 138 | /** 139 | * {@inheritdoc} 140 | */ 141 | public function validateAuthKey($authKey) 142 | { 143 | return $this->getAuthKey() === $authKey; 144 | } 145 | 146 | /** 147 | * Validates password 148 | * 149 | * @param string $password password to validate 150 | * @return bool if password provided is valid for current user 151 | */ 152 | public function validatePassword($password) 153 | { 154 | return Yii::$app->security->validatePassword($password, $this->password); 155 | } 156 | 157 | /** 158 | * Generates password hash from password and sets it to the model 159 | * 160 | * @param string $password 161 | */ 162 | public function setPassword($password) 163 | { 164 | $this->password = Yii::$app->security->generatePasswordHash($password); 165 | } 166 | 167 | /** 168 | * Generates "remember me" authentication key 169 | */ 170 | public function generateAuthKey() 171 | { 172 | $this->auth_key = Yii::$app->security->generateRandomString(); 173 | } 174 | 175 | /** 176 | * Generates new password reset token 177 | */ 178 | public function generatePasswordResetToken() 179 | { 180 | $this->password_reset_token = Yii::$app->security->generateRandomString() . '_' . time(); 181 | } 182 | 183 | public function generateEmailVerificationToken() 184 | { 185 | $this->verification_token = Yii::$app->security->generateRandomString() . '_' . time(); 186 | } 187 | 188 | /** 189 | * Removes password reset token 190 | */ 191 | public function removePasswordResetToken() 192 | { 193 | $this->password_reset_token = null; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /models/Company.php: -------------------------------------------------------------------------------- 1 | $query]); 16 | 17 | if ($params) { 18 | $this->load($params); 19 | 20 | if ($this->search) { 21 | $query->orWhere(['like', 'email', $this->search]); 22 | $query->orWhere(['like', 'name', $this->search]); 23 | $query->orWhere(['like', 'phone', $this->search]); 24 | } 25 | } 26 | 27 | return $dataProvider; 28 | } 29 | 30 | public static function tableName() 31 | { 32 | return "company"; 33 | } 34 | 35 | public function attributeLabels() 36 | { 37 | return [ 38 | 'name' => 'Имя', 39 | 'address' => 'Адрес', 40 | 'email' => 'Рабочий email', 41 | 'phone' => 'Рабочий телефон', 42 | 'url' => 'Сайт', 43 | 'ogrn' => 'ОГРН' 44 | ]; 45 | } 46 | 47 | public function rules() 48 | { 49 | return [ 50 | [['name', 'phone', 'email', 'address', 'url', 'search'], 'safe'], 51 | [['name', 'phone', 'email', 'address', 'url'], 'required', 'on' => 'insert'], 52 | [['name', 'phone', 'email'], 'required', 'on' => 'update'], 53 | [['phone', 'email', 'url'], 'unique'], 54 | ['phone', 'string', 'length' => 11], 55 | ['email', 'email'] 56 | ]; 57 | } 58 | 59 | public function getContacts() { 60 | return $this->hasMany(Contact::class, ['company_id' => 'id'])->inverseOf('company'); 61 | } 62 | 63 | public function getActiveContacts() { 64 | return $this->getContacts()->where(['status' => '1'])->orderBy(['dt_add' => SORT_ASC]); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /models/Contact.php: -------------------------------------------------------------------------------- 1 | $query]); 21 | 22 | $this->load($params); 23 | 24 | $query->orFilterWhere(['like', 'name', $this->search]); 25 | $query->orFilterWhere(['like', 'phone', $this->search]); 26 | $query->orFilterWhere(['like', 'email', $this->search]); 27 | 28 | $query->filterWhere(['type_id' => $this->type_id, 'company_id' => $this->company_id]); 29 | 30 | if ($this->onlyDeals) { 31 | $query->joinWith('deals', true, 'right join'); 32 | 33 | } 34 | 35 | return $dataProvider; 36 | } 37 | 38 | public static function tableName() 39 | { 40 | return "contact"; 41 | } 42 | 43 | public function attributeLabels() 44 | { 45 | return [ 46 | 'name' => 'Имя', 47 | 'phone' => 'Телефон', 48 | 'email' => 'Электронная почта', 49 | 'position' => 'Должность', 50 | 'dt_add' => 'Дата добавления', 51 | 'company_id' => 'Компания', 52 | 'type_id' => 'Тип', 53 | 'onlyDeals' => 'Только со сделками' 54 | ]; 55 | } 56 | 57 | public function rules() 58 | { 59 | return [ 60 | [['name', 'phone', 'email', 'position', 'type_id', 'company_id', 'search', 'onlyDeals'], 'safe'], 61 | [['name', 'phone', 'email', 'position', 'type_id', 'company_id'], 'required', 'on' => 'insert'], 62 | [['name', 'email'], 'required', 'on' => 'update'], 63 | ['company_id', 'exist', 'targetRelation' => 'company'], 64 | ['type_id', 'exist', 'targetRelation' => 'status'], 65 | [['phone', 'email'], 'unique'], 66 | ['phone', 'string', 'length' => 11], 67 | ['email', 'email'] 68 | ]; 69 | } 70 | 71 | public function getDeals() 72 | { 73 | return $this->hasMany(Deal::class, ['contact_id' => 'id'])->inverseOf('contact'); 74 | } 75 | 76 | public function getCompany() 77 | { 78 | return $this->hasOne(Company::class, ['id' => 'company_id']); 79 | } 80 | 81 | public function getStatus() 82 | { 83 | return $this->hasOne(ContactType::class, ['id' => 'type_id']); 84 | } 85 | 86 | public static function getItemsCountByStatus($status) 87 | { 88 | return self::find()->joinWith('status s')->where(['s.name' => $status])->count(); 89 | } 90 | 91 | public function getPersonName() 92 | { 93 | return $this->name; 94 | } 95 | 96 | public function getPersonPosition() 97 | { 98 | return $this->position; 99 | } 100 | 101 | public function getPersonCompany() 102 | { 103 | return $this->company->name ?? null; 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /models/ContactType.php: -------------------------------------------------------------------------------- 1 | SoftDeleteBehavior::class, 48 | 'softDeleteAttributeValues' => ['deleted' => true] 49 | ]; 50 | $behaviors['blame'] = [ 51 | 'class' => BlameableBehavior::class, 52 | 'createdByAttribute' => 'user_id', 'updatedByAttribute' => false 53 | ]; 54 | 55 | return $behaviors; 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function rules() 62 | { 63 | return [ 64 | [['company_id', 'status_id', 'contact_id', 'executor_id', 'budget_amount', 'name', 'description'], 'required', 'on' => 'insert'], 65 | [['company_id', 'status_id', 'contact_id', 'executor_id', 'budget_amount', 'due_date', 'name', 'description'], 'safe'], 66 | [['company_id', 'status_id', 'contact_id', 'executor_id', 'budget_amount'], 'integer'], 67 | [['description', 'name'], 'string'], 68 | [['name'], 'string', 'max' => 255], 69 | ]; 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function attributeLabels() 76 | { 77 | return [ 78 | 'id' => 'ID', 79 | 'name' => 'Название', 80 | 'company_id' => 'Компания', 81 | 'status_id' => 'Этап', 82 | 'user_id' => 'Создатель', 83 | 'contact_id' => 'Контакт', 84 | 'executor_id' => 'Исполнитель', 85 | 'due_date' => 'Дата исполнения', 86 | 'description' => 'Описание', 87 | 'budget_amount' => 'Стоимость работ', 88 | 'dt_add' => 'Дата создания', 89 | ]; 90 | } 91 | 92 | public function getFeedItems() 93 | { 94 | $firstItem = new Feed(); 95 | $firstItem->setAttributes([ 96 | 'user_id' => $this->user_id, 97 | 'type' => Feed::TYPE_NEW, 98 | 'value' => $this->id, 99 | 'deal_id' => $this->id, 100 | 'dt_add' => $this->dt_add 101 | ]); 102 | 103 | return array_merge([$firstItem], $this->feed); 104 | } 105 | 106 | public function getOwner() 107 | { 108 | return $this->hasOne(User::class, ['id' => 'user_id']); 109 | } 110 | 111 | public function getExecutor() 112 | { 113 | return $this->hasOne(User::class, ['id' => 'executor_id']); 114 | } 115 | 116 | public function getCompany() 117 | { 118 | return $this->hasOne(Company::class, ['id' => 'company_id']); 119 | } 120 | 121 | public function getContact() 122 | { 123 | return $this->hasOne(Contact::class, ['id' => 'contact_id'])->inverseOf('deals'); 124 | } 125 | 126 | public function getStatus() 127 | { 128 | return $this->hasOne(DealStatus::class, ['id' => 'status_id']); 129 | } 130 | 131 | public function getNotes() 132 | { 133 | return $this->hasMany(Note::class, ['deal_id' => 'id']); 134 | } 135 | 136 | public function getTasks() 137 | { 138 | return $this->hasMany(Task::class, ['deal_id' => 'id']); 139 | } 140 | 141 | public function getFeed() 142 | { 143 | return $this->hasMany(Feed::class, ['deal_id' => 'id'])->orderBy('dt_add ASC'); 144 | } 145 | 146 | /** 147 | * @return PersonInterface[] 148 | */ 149 | public function getAllParticipants() 150 | { 151 | $participants = [ 152 | $this->contact, $this->executor, $this->owner 153 | ]; 154 | 155 | return array_filter($participants); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /models/DealStatus.php: -------------------------------------------------------------------------------- 1 | 255], 30 | [['name'], 'unique'], 31 | ]; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function attributeLabels() 38 | { 39 | return [ 40 | 'id' => 'ID', 41 | 'name' => 'Name', 42 | ]; 43 | } 44 | 45 | public function getNextStatus() 46 | { 47 | $next = self::find()->orderBy('id ASC')->where(['>', 'id', $this->id])->one(); 48 | 49 | return $next; 50 | } 51 | 52 | public function getPrevStatus() 53 | { 54 | $prev = self::find()->orderBy('id DESC')->where(['<', 'id', $this->id])->one(); 55 | 56 | return $prev; 57 | } 58 | 59 | public function getDeals() 60 | { 61 | return $this->hasMany(Deal::class, ['status_id' => 'id'])->notDeleted(); 62 | } 63 | 64 | public function getDealsAmount() 65 | { 66 | return $this->getDeals()->sum('budget_amount'); 67 | } 68 | 69 | public function getDealsCount() 70 | { 71 | return $this->getDeals()->count(); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /models/EmailSearchForm.php: -------------------------------------------------------------------------------- 1 | function($value) { 17 | $value = trim($value); 18 | $value = "in:inbox $value"; 19 | 20 | return $value; 21 | }, 'skipOnEmpty' => true], 22 | [['q'], 'string', 'length' => [2, 128]] 23 | ]; 24 | } 25 | 26 | 27 | } 28 | -------------------------------------------------------------------------------- /models/Feed.php: -------------------------------------------------------------------------------- 1 | [ 41 | 'class' => BlameableBehavior::class, 42 | 'createdByAttribute' => 'user_id', 'updatedByAttribute' => null 43 | ] 44 | ]; 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function rules() 51 | { 52 | return [ 53 | [['type', 'value'], 'required'], 54 | [['dt_add', 'type', 'user_id', 'value', 'deal_id'], 'safe'], 55 | [['user_id'], 'integer'], 56 | [['type'], 'string', 'max' => 32], 57 | [['user_id'], 'exist', 'skipOnError' => true, 'targetClass' => User::class, 'targetAttribute' => ['user_id' => 'id']], 58 | ]; 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function attributeLabels() 65 | { 66 | return [ 67 | 'id' => 'ID', 68 | 'type' => 'Type', 69 | 'dt_add' => 'Dt Add', 70 | 'user_id' => 'User ID', 71 | 'value' => 'Value', 72 | ]; 73 | } 74 | 75 | /** 76 | * @return \yii\db\ActiveQuery 77 | */ 78 | public function getUser() 79 | { 80 | return $this->hasOne(User::class, ['id' => 'user_id']); 81 | } 82 | 83 | public function getDeal() 84 | { 85 | return $this->hasOne(Deal::class, ['id' => 'deal_id']); 86 | } 87 | 88 | public function getAssociatedContent() 89 | { 90 | switch ($this->type) { 91 | case self::TYPE_NOTE: 92 | return Note::findOne($this->value); 93 | case self::TYPE_STATUS: 94 | return DealStatus::findOne($this->value); 95 | } 96 | 97 | return null; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /models/LoginForm.php: -------------------------------------------------------------------------------- 1 | hasErrors()) { 25 | $user = $this->getUser(); 26 | if (!$user || !$user->validatePassword($this->password)) { 27 | $this->addError($attribute, 'Неправильный email или пароль'); 28 | } 29 | } 30 | } 31 | 32 | public function getUser() 33 | { 34 | if ($this->_user === null) { 35 | $this->_user = User::findOne(['email' => $this->email]); 36 | } 37 | 38 | return $this->_user; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /models/Note.php: -------------------------------------------------------------------------------- 1 | [ 35 | 'class' => BlameableBehavior::class, 36 | 'createdByAttribute' => 'user_id', 'updatedByAttribute' => null 37 | ], 38 | 'feed' => [ 39 | 'class' => FeedBehavior::class, 40 | 'eventType' => Feed::TYPE_NOTE, 'attrName' => 'content' 41 | ] 42 | ]; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function rules() 49 | { 50 | return [ 51 | [['content', 'deal_id'], 'required'], 52 | [['deal_id', 'user_id'], 'integer'], 53 | [['content'], 'safe'], 54 | [['content'], 'string'], 55 | [['deal_id'], 'exist', 'skipOnError' => true, 'targetClass' => Deal::class, 'targetAttribute' => ['deal_id' => 'id']], 56 | [['user_id'], 'exist', 'skipOnError' => true, 'targetClass' => User::class, 'targetAttribute' => ['user_id' => 'id']], 57 | ]; 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | public function attributeLabels() 64 | { 65 | return [ 66 | 'id' => 'ID', 67 | 'deal_id' => 'Deal ID', 68 | 'dt_add' => 'Dt Add', 69 | 'user_id' => 'User ID', 70 | 'content' => 'Content', 71 | ]; 72 | } 73 | 74 | /** 75 | * @return \yii\db\ActiveQuery 76 | */ 77 | public function getDeal() 78 | { 79 | return $this->hasOne(Deal::class, ['id' => 'deal_id']); 80 | } 81 | 82 | /** 83 | * @return \yii\db\ActiveQuery 84 | */ 85 | public function getUser() 86 | { 87 | return $this->hasOne(User::class, ['id' => 'user_id']); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /models/ProfileSettingsForm.php: -------------------------------------------------------------------------------- 1 | self::SCENARIO_PROFILE]; 19 | 20 | return $rules; 21 | } 22 | 23 | public function scenarios() 24 | { 25 | $scenarios = parent::scenarios(); 26 | $scenarios[self::SCENARIO_PROFILE] = ['new_password', 'new_password_repeat']; 27 | 28 | return $scenarios; 29 | } 30 | 31 | 32 | public function attributeLabels() 33 | { 34 | $labels = parent::attributeLabels(); 35 | 36 | $labels['new_password'] = 'Новый пароль'; 37 | $labels['new_password_repeat'] = 'Повтор нового пароля'; 38 | 39 | return $labels; 40 | } 41 | 42 | public function beforeSave($insert) 43 | { 44 | if ($this->new_password) { 45 | $this->password = \Yii::$app->security->generatePasswordHash($this->new_password); 46 | } 47 | 48 | return true; 49 | } 50 | 51 | 52 | } 53 | -------------------------------------------------------------------------------- /models/SignupForm.php: -------------------------------------------------------------------------------- 1 | '\app\models\BaseUser', 'message' => 'This username has already been taken.'], 27 | ['username', 'string', 'min' => 2, 'max' => 255], 28 | 29 | ['email', 'trim'], 30 | ['email', 'required'], 31 | ['email', 'email'], 32 | ['email', 'string', 'max' => 255], 33 | ['email', 'unique', 'targetClass' => '\app\models\BaseUser', 'message' => 'This email address has already been taken.'], 34 | 35 | ['password', 'required'], 36 | ['password', 'string', 'min' => 6], 37 | ]; 38 | } 39 | 40 | /** 41 | * Signs user up. 42 | * 43 | * @return bool whether the creating new account was successful and email was sent 44 | */ 45 | public function signup() 46 | { 47 | if (!$this->validate()) { 48 | return null; 49 | } 50 | 51 | $user = new BaseUser(); 52 | $user->username = $this->username; 53 | $user->email = $this->email; 54 | $user->setPassword($this->password); 55 | $user->generateAuthKey(); 56 | $user->generateEmailVerificationToken(); 57 | return $user->save() && $this->sendEmail($user); 58 | 59 | } 60 | 61 | /** 62 | * Sends confirmation email to user 63 | * @param BaseUser $user user model to with email should be send 64 | * @return bool whether the email was sent 65 | */ 66 | protected function sendEmail($user) 67 | { 68 | return Yii::$app 69 | ->mailer 70 | ->compose( 71 | ['html' => 'emailVerify-html', 'text' => 'emailVerify-text'], 72 | ['user' => $user] 73 | ) 74 | ->setFrom([Yii::$app->params['supportEmail'] => Yii::$app->name . ' robot']) 75 | ->setTo($this->email) 76 | ->setSubject('Account registration at ' . Yii::$app->name) 77 | ->send(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /models/SoftDelete.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'class' => SoftDeleteBehavior::class, 15 | 'softDeleteAttributeValues' => ['deleted' => true] 16 | ] 17 | ]; 18 | } 19 | 20 | public static function find() 21 | { 22 | $query = parent::find(); 23 | $query->attachBehavior('softDelete', SoftDeleteQueryBehavior::class); 24 | 25 | $query->notDeleted(); 26 | 27 | return $query; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /models/Task.php: -------------------------------------------------------------------------------- 1 | $query]); 32 | 33 | if ($params) { 34 | $this->load($params); 35 | 36 | if ($this->search) { 37 | $query->filterWhere(['like', 'description', $this->search]); 38 | } 39 | } 40 | 41 | return $dataProvider; 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function rules() 48 | { 49 | return [ 50 | [['description', 'executor_id', 'type_id', 'deal_id'], 'required', 'on' => 'insert'], 51 | [['description', 'executor_id', 'type_id', 'deal_id', 'search'], 'safe'], 52 | [['description'], 'string'], 53 | [['executor_id', 'type_id', 'deal_id'], 'integer'], 54 | [['due_date', 'dt_add'], 'safe'], 55 | [['deal_id'], 'exist', 'skipOnError' => true, 'targetClass' => Deal::class, 'targetAttribute' => ['deal_id' => 'id']], 56 | [['executor_id'], 'exist', 'skipOnError' => true, 'targetClass' => User::class, 'targetAttribute' => ['executor_id' => 'id']], 57 | [['type_id'], 'exist', 'skipOnError' => true, 'targetClass' => TaskType::class, 'targetAttribute' => ['type_id' => 'id']], 58 | ]; 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function attributeLabels() 65 | { 66 | return [ 67 | 'id' => 'ID', 68 | 'description' => 'Описание', 69 | 'executor_id' => 'Исполнитель', 70 | 'due_date' => 'Срок исполнения', 71 | 'type_id' => 'Тип', 72 | 'dt_add' => 'Дата создания', 73 | 'deal_id' => 'Сделка', 74 | ]; 75 | } 76 | 77 | /** 78 | * @return \yii\db\ActiveQuery 79 | */ 80 | public function getDeal() 81 | { 82 | return $this->hasOne(Deal::class, ['id' => 'deal_id']); 83 | } 84 | 85 | /** 86 | * @return \yii\db\ActiveQuery 87 | */ 88 | public function getExecutor() 89 | { 90 | return $this->hasOne(User::class, ['id' => 'executor_id']); 91 | } 92 | 93 | /** 94 | * @return \yii\db\ActiveQuery 95 | */ 96 | public function getType() 97 | { 98 | return $this->hasOne(TaskType::class, ['id' => 'type_id']); 99 | } 100 | 101 | public static function getExpiredCount() 102 | { 103 | return self::find()->where(['<', 'due_date', new Expression('NOW()')])->count(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /models/TaskType.php: -------------------------------------------------------------------------------- 1 | 255], 32 | [['name'], 'unique'], 33 | ]; 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function attributeLabels() 40 | { 41 | return [ 42 | 'id' => 'ID', 43 | 'name' => 'Name', 44 | ]; 45 | } 46 | 47 | /** 48 | * @return \yii\db\ActiveQuery 49 | */ 50 | public function getTasks() 51 | { 52 | return $this->hasMany(Task::class, ['type_id' => 'id']); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /models/User.php: -------------------------------------------------------------------------------- 1 | 'Электронная почта', 20 | 'phone' => 'Номер телефона', 21 | 'company' => 'Название компании', 22 | 'password' => 'Пароль', 23 | 'password_repeat' => 'Повтор пароля', 24 | ]; 25 | } 26 | 27 | public function rules() 28 | { 29 | return [ 30 | [['company', 'phone', 'email', 'password', 'password_repeat', 'name', 'position'], 'safe'], 31 | [['company', 'name', 'email', 'password', 'password_repeat'], 'required', 'on' => self::SCENARIO_DEFAULT], 32 | ['email', 'email'], 33 | ['email', 'unique', 'on' => self::SCENARIO_DEFAULT], 34 | ['email', RemoteEmailValidator::class, 'on' => self::SCENARIO_DEFAULT], 35 | ['phone', 'match', 'pattern' => '/^[\d]{11}/i', 36 | 'message' => 'Номер телефона должен состоять из 11 цифр'], 37 | ['company', 'string', 'min' => 3], 38 | ['password', 'string', 'min' => 8], 39 | ['password', 'compare', 'on' => self::SCENARIO_DEFAULT] 40 | ]; 41 | } 42 | 43 | public function getPersonName() 44 | { 45 | return $this->name; 46 | } 47 | 48 | public function getPersonPosition() 49 | { 50 | return $this->position; 51 | } 52 | 53 | public function getPersonCompany() 54 | { 55 | return $this->company; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /modules/api/Module.php: -------------------------------------------------------------------------------- 1 | Error\n\n" 30 | . "

The path to yii framework seems to be incorrect.

\n" 31 | . '

You need to install Yii framework via composer or adjust the framework path in file ' . basename(__FILE__) . ".

\n" 32 | . '

Please refer to the README on how to install Yii.

\n"; 33 | if (!empty($_SERVER['argv'])) { 34 | // do not print HTML when used in console mode 35 | echo strip_tags($message); 36 | } else { 37 | echo $message; 38 | } 39 | exit(1); 40 | } 41 | 42 | require_once $frameworkPath . '/requirements/YiiRequirementChecker.php'; 43 | $requirementsChecker = new YiiRequirementChecker(); 44 | 45 | $gdMemo = $imagickMemo = 'Either GD PHP extension with FreeType support or ImageMagick PHP extension with PNG support is required for image CAPTCHA.'; 46 | $gdOK = $imagickOK = false; 47 | 48 | if (extension_loaded('imagick')) { 49 | $imagick = new Imagick(); 50 | $imagickFormats = $imagick->queryFormats('PNG'); 51 | if (in_array('PNG', $imagickFormats)) { 52 | $imagickOK = true; 53 | } else { 54 | $imagickMemo = 'Imagick extension should be installed with PNG support in order to be used for image CAPTCHA.'; 55 | } 56 | } 57 | 58 | if (extension_loaded('gd')) { 59 | $gdInfo = gd_info(); 60 | if (!empty($gdInfo['FreeType Support'])) { 61 | $gdOK = true; 62 | } else { 63 | $gdMemo = 'GD extension should be installed with FreeType support in order to be used for image CAPTCHA.'; 64 | } 65 | } 66 | 67 | /** 68 | * Adjust requirements according to your application specifics. 69 | */ 70 | $requirements = array( 71 | // Database : 72 | array( 73 | 'name' => 'PDO extension', 74 | 'mandatory' => true, 75 | 'condition' => extension_loaded('pdo'), 76 | 'by' => 'All DB-related classes', 77 | ), 78 | array( 79 | 'name' => 'PDO SQLite extension', 80 | 'mandatory' => false, 81 | 'condition' => extension_loaded('pdo_sqlite'), 82 | 'by' => 'All DB-related classes', 83 | 'memo' => 'Required for SQLite database.', 84 | ), 85 | array( 86 | 'name' => 'PDO MySQL extension', 87 | 'mandatory' => false, 88 | 'condition' => extension_loaded('pdo_mysql'), 89 | 'by' => 'All DB-related classes', 90 | 'memo' => 'Required for MySQL database.', 91 | ), 92 | array( 93 | 'name' => 'PDO PostgreSQL extension', 94 | 'mandatory' => false, 95 | 'condition' => extension_loaded('pdo_pgsql'), 96 | 'by' => 'All DB-related classes', 97 | 'memo' => 'Required for PostgreSQL database.', 98 | ), 99 | // Cache : 100 | array( 101 | 'name' => 'Memcache extension', 102 | 'mandatory' => false, 103 | 'condition' => extension_loaded('memcache') || extension_loaded('memcached'), 104 | 'by' => 'MemCache', 105 | 'memo' => extension_loaded('memcached') ? 'To use memcached set MemCache::useMemcached to true.' : '' 106 | ), 107 | array( 108 | 'name' => 'APC extension', 109 | 'mandatory' => false, 110 | 'condition' => extension_loaded('apc'), 111 | 'by' => 'ApcCache', 112 | ), 113 | // CAPTCHA: 114 | array( 115 | 'name' => 'GD PHP extension with FreeType support', 116 | 'mandatory' => false, 117 | 'condition' => $gdOK, 118 | 'by' => 'Captcha', 119 | 'memo' => $gdMemo, 120 | ), 121 | array( 122 | 'name' => 'ImageMagick PHP extension with PNG support', 123 | 'mandatory' => false, 124 | 'condition' => $imagickOK, 125 | 'by' => 'Captcha', 126 | 'memo' => $imagickMemo, 127 | ), 128 | // PHP ini : 129 | 'phpExposePhp' => array( 130 | 'name' => 'Expose PHP', 131 | 'mandatory' => false, 132 | 'condition' => $requirementsChecker->checkPhpIniOff("expose_php"), 133 | 'by' => 'Security reasons', 134 | 'memo' => '"expose_php" should be disabled at php.ini', 135 | ), 136 | 'phpAllowUrlInclude' => array( 137 | 'name' => 'PHP allow url include', 138 | 'mandatory' => false, 139 | 'condition' => $requirementsChecker->checkPhpIniOff("allow_url_include"), 140 | 'by' => 'Security reasons', 141 | 'memo' => '"allow_url_include" should be disabled at php.ini', 142 | ), 143 | 'phpSmtp' => array( 144 | 'name' => 'PHP mail SMTP', 145 | 'mandatory' => false, 146 | 'condition' => strlen(ini_get('SMTP')) > 0, 147 | 'by' => 'Email sending', 148 | 'memo' => 'PHP mail SMTP server required', 149 | ), 150 | ); 151 | 152 | $result = $requirementsChecker->checkYii()->check($requirements)->getResult(); 153 | $requirementsChecker->render(); 154 | 155 | exit($result['summary']['errors'] === 0 ? 0 : 1); 156 | -------------------------------------------------------------------------------- /runtime/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /services/AuthClient.php: -------------------------------------------------------------------------------- 1 | authClient = $authClient; 26 | $this->serviceGmail = new Google_Service_Gmail($authClient->getVendorClient()); 27 | } 28 | 29 | /** 30 | * @inheritDoc 31 | */ 32 | public function getMessages($max_count = 10, $search_query = null) 33 | { 34 | $result = []; 35 | $params = ['maxResults' => $max_count]; 36 | 37 | if ($search_query) { 38 | $params['q'] = $search_query; 39 | } 40 | 41 | $response = $this->serviceGmail->users_messages->listUsersMessages($this->user, $params); 42 | 43 | /** 44 | * @var $messages Google_Service_Gmail_Message[] 45 | */ 46 | $messages = $response->getMessages(); 47 | 48 | foreach ($messages as $message) { 49 | $msg = $this->serviceGmail->users_messages->get($this->user, $message->getId(), ['format' => 'METADATA']); 50 | 51 | $result[] = new GmailMessage($msg); 52 | } 53 | 54 | return $result; 55 | } 56 | 57 | public function getUnreadCount() 58 | { 59 | $response = $this->serviceGmail->users_messages->listUsersMessages($this->user, ['labelIds' => ['UNREAD']]); 60 | 61 | return $response->count(); 62 | } 63 | 64 | public function getMessageById($msgid) 65 | { 66 | $rawMessage = $this->serviceGmail->users_messages->get($this->user, $msgid, ['format' => 'FULL']); 67 | $message = new GmailMessage($rawMessage); 68 | 69 | return $message; 70 | } 71 | 72 | 73 | } 74 | -------------------------------------------------------------------------------- /services/GoogleAuthClient.php: -------------------------------------------------------------------------------- 1 | setApplicationName("Google Auth"); 21 | $client->setScopes($scope); 22 | $client->setAuthConfig($credentials_path); 23 | $client->setAccessType("offline"); 24 | $client->setRedirectUri($redirectUrl); 25 | 26 | $this->googleClient = $client; 27 | 28 | $this->tokenPath = $token_path; 29 | } 30 | 31 | public function setToken($path) 32 | { 33 | if (file_exists($path)) { 34 | $accessToken = json_decode(file_get_contents($path), true); 35 | 36 | if ($accessToken) { 37 | $this->googleClient->setAccessToken($accessToken); 38 | } 39 | } 40 | } 41 | 42 | public function isTokenExpired() 43 | { 44 | return $this->googleClient->isAccessTokenExpired(); 45 | } 46 | 47 | public function isTokexExists() 48 | { 49 | return file_exists($this->tokenPath); 50 | } 51 | 52 | public function fetchAccessTokenByCode($code) 53 | { 54 | return $this->googleClient->fetchAccessTokenWithAuthCode($code); 55 | } 56 | 57 | public function storeToken($token) 58 | { 59 | if (!file_exists(dirname($this->tokenPath))) { 60 | mkdir(dirname($this->tokenPath), 0700, true); 61 | } 62 | 63 | file_put_contents($this->tokenPath, json_encode($token)); 64 | } 65 | 66 | public function prepareClient() 67 | { 68 | if (!$this->isTokexExists()) { 69 | return false; 70 | } 71 | 72 | $this->setToken($this->tokenPath); 73 | 74 | if ($this->isTokenExpired()) { 75 | if ($refresh = $this->googleClient->getRefreshToken()) { 76 | $access_token = $this->googleClient->fetchAccessTokenWithRefreshToken($refresh); 77 | $this->storeToken($access_token); 78 | 79 | return true; 80 | } 81 | 82 | return false; 83 | } 84 | 85 | return true; 86 | } 87 | 88 | public function getAuthUrl() 89 | { 90 | return $this->googleClient->createAuthUrl(); 91 | } 92 | 93 | public function getVendorClient() 94 | { 95 | return $this->googleClient; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /services/GoogleAuthClientFactory.php: -------------------------------------------------------------------------------- 1 | user->getId() . '.json'; 17 | 18 | $creds = Yii::getAlias('@app/data/credentials.json'); 19 | $token = Yii::getAlias('@app/data/' . $token); 20 | 21 | $redirectUrl = Url::to('third-party/auth', true); 22 | 23 | $client = new GoogleAuthClient($creds, $token, \Google_Service_Gmail::GMAIL_READONLY, $redirectUrl); 24 | 25 | return $client; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /services/MailClient.php: -------------------------------------------------------------------------------- 1 | $/m'; 19 | 20 | /** 21 | * GmailMessage constructor. 22 | * @param Google_Service_Gmail_Message $rawMessage 23 | */ 24 | public function __construct(Google_Service_Gmail_Message $rawMessage) 25 | { 26 | $this->prepare($rawMessage); 27 | } 28 | 29 | public function getId() 30 | { 31 | return $this->id; 32 | } 33 | 34 | public function getSubject() 35 | { 36 | return $this->subject; 37 | } 38 | 39 | public function getBody() 40 | { 41 | return $this->body; 42 | } 43 | 44 | public function getSenderName() 45 | { 46 | preg_match($this->regexp, $this->sender, $matches); 47 | 48 | return $matches[1] ?? 'Без имени'; 49 | } 50 | 51 | public function getSenderAddress() 52 | { 53 | preg_match($this->regexp, $this->sender, $matches); 54 | 55 | return $matches[2] ?? $this->sender; 56 | } 57 | 58 | public function getDate() 59 | { 60 | return $this->date; 61 | } 62 | 63 | public function getIsUnread() 64 | { 65 | return $this->unread; 66 | } 67 | 68 | protected function prepare(Google_Service_Gmail_Message $rawMessage) 69 | { 70 | $payload = $rawMessage->getPayload(); 71 | $headers = $payload ? $payload->getHeaders() : []; 72 | 73 | $this->id = $rawMessage->getId(); 74 | $this->body = $rawMessage->getSnippet(); 75 | 76 | foreach ($headers as $header) { 77 | $name = $header->getName(); 78 | $value = $header->getValue(); 79 | 80 | switch ($name) { 81 | case 'Date': 82 | $this->date = $value; 83 | break; 84 | case 'Subject': 85 | $this->subject = $value; 86 | break; 87 | case 'From': 88 | $this->sender = $value; 89 | break; 90 | } 91 | } 92 | 93 | $this->id = $rawMessage->getId(); 94 | $this->unread = $this->isUnread($rawMessage); 95 | } 96 | 97 | protected function isUnread(Google_Service_Gmail_Message $rawMessage) 98 | { 99 | $result = false; 100 | 101 | foreach ($rawMessage->getLabelIds() as $label) { 102 | if ($label == 'UNREAD') { 103 | $result = true; 104 | break; 105 | } 106 | } 107 | 108 | return $result; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /services/containers/MailMessage.php: -------------------------------------------------------------------------------- 1 | filename = $filename; 27 | $this->columns = $columns; 28 | } 29 | 30 | public function import():void 31 | { 32 | if (!$this->validateColumns($this->columns)) { 33 | throw new FileFormatException("Заданы неверные заголовки столбцов"); 34 | } 35 | 36 | if (!file_exists($this->filename)) { 37 | throw new SourceFileException("Файл не существует"); 38 | } 39 | 40 | try { 41 | $this->fileObject = new SplFileObject($this->filename); 42 | } 43 | catch (RuntimeException $exception) { 44 | throw new SourceFileException("Не удалось открыть файл на чтение"); 45 | } 46 | 47 | $header_data = $this->getHeaderData(); 48 | 49 | if ($header_data !== $this->columns) { 50 | throw new FileFormatException("Исходный файл не содержит необходимых столбцов"); 51 | } 52 | 53 | foreach ($this->getNextLine() as $line) { 54 | $this->result[] = $line; 55 | } 56 | } 57 | 58 | public function getData():array { 59 | return $this->result; 60 | } 61 | 62 | private function getHeaderData():?array { 63 | $this->fileObject->rewind(); 64 | $data = $this->fileObject->fgetcsv(); 65 | 66 | return $data; 67 | } 68 | 69 | private function getNextLine():?iterable { 70 | $result = null; 71 | 72 | while (!$this->fileObject->eof()) { 73 | yield $this->fileObject->fgetcsv(); 74 | } 75 | 76 | return $result; 77 | } 78 | 79 | private function validateColumns(array $columns):bool 80 | { 81 | $result = true; 82 | 83 | if (count($columns)) { 84 | foreach ($columns as $column) { 85 | if (!is_string($column)) { 86 | $result = false; 87 | } 88 | } 89 | } 90 | else { 91 | $result = false; 92 | } 93 | 94 | return $result; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /validators/RemoteEmailValidator.php: -------------------------------------------------------------------------------- 1 | 'https://apilayer.net/api/', 24 | ]); 25 | 26 | try { 27 | $request = new Request('GET', 'check'); 28 | $response = $client->send($request, [ 29 | 'query' => ['email' => $value, 'access_key' => Yii::$app->params['apiKey']] 30 | ]); 31 | 32 | if ($response->getStatusCode() !== 200) { 33 | throw new BadResponseException("Response error: " . $response->getReasonPhrase(), $request); 34 | } 35 | 36 | $content = $response->getBody()->getContents(); 37 | $response_data = json_decode($content, true); 38 | 39 | if (json_last_error() !== JSON_ERROR_NONE) { 40 | throw new ServerException("Invalid json format", $request); 41 | } 42 | 43 | if ($error = ArrayHelper::getValue($response_data, 'error.info')) { 44 | throw new BadResponseException("API error: " . $error, $request); 45 | } 46 | 47 | if (is_array($response_data)) { 48 | $result = !empty($response_data['mx_found']) && !empty($response_data['smtp_check']); 49 | } 50 | } catch (RequestException $e) { 51 | $result = true; 52 | } 53 | 54 | if (!$result) { 55 | return [$this->message, []]; 56 | } 57 | 58 | return null; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /views/companies/index.php: -------------------------------------------------------------------------------- 1 | title = 'Список компаний'; 20 | $this->registerJsFile('@web/js/grid.js', ['depends' => [JqueryAsset::class]]); 21 | 22 | ?> 23 |
24 | render('//partials/_grid_header', [ 25 | 'model' => $model, 'dataProvider' => $dataProvider, 'name' => 'Компании', 'showFilterBtn' => false 26 | ]); ?> 27 |
28 | $dataProvider, 30 | 'options' => ['class' => 'data-table'], 31 | 'summary' => '', 32 | 'layout' => "{summary}\n{items}", 33 | 'columns' => [ 34 | ['attribute' => 'name'], 35 | ['attribute' => 'address'], 36 | ['attribute' => 'phone'], 37 | ['attribute' => 'url', 'format' => 'url'], 38 | ['attribute' => 'email', 'format' => 'email'], 39 | [ 40 | 'class' => DataColumn::class, 41 | 'value' => function ($row) { 42 | return $this->render('/partials/_action_row', ['alias' => 'companies', 'id' => $row->id]); 43 | }, 44 | 'header' => '', 'label' => '', 'format' => 'raw' 45 | ] 46 | ]]); ?> 47 |
48 | 53 | render('//modals/_company_form', ['model' => $model]); ?> 54 | 'companies_create', 56 | 'title' => 'Новая компания успешно добавлена.' 57 | ]); ?> 58 |
59 | -------------------------------------------------------------------------------- /views/contacts/index.php: -------------------------------------------------------------------------------- 1 | title = 'Список контактов'; 17 | $this->registerJsFile('@web/js/grid.js', ['depends' => [JqueryAsset::class]]); 18 | 19 | ?> 20 |
21 | render('//partials/_grid_header', [ 22 | 'model' => $searchModel, 'dataProvider' => $dataProvider, 'name' => 'Контакты', 'showFilterBtn' => true 23 | ]); ?> 24 | render('//partials/_grid_filter', ['model' => $searchModel]); ?> 25 |
26 | $dataProvider, 28 | 'options' => ['class' => 'data-table'], 29 | 'summary' => '', 30 | 'layout' => "{summary}\n{items}", 31 | 'columns' => [ 32 | ['attribute' => 'name'], 33 | ['attribute' => 'company.name', 'label' => 'Компания'], 34 | ['attribute' => 'position'], 35 | ['attribute' => 'status.name', 'label' => 'Тип'], 36 | ['attribute' => 'phone'], 37 | ['attribute' => 'email', 'format' => 'email'], 38 | [ 39 | 'class' => DataColumn::class, 40 | 'value' => function($row) { 41 | return $this->render('/partials/_action_row', ['alias' => 'contacts', 'id' => $row->id]); 42 | }, 43 | 'header' => '', 'label' => '', 'format' => 'raw' 44 | ] 45 | ]]); ?> 46 |
47 | 54 | render('//modals/_contact_form', ['model' => $model]); ?> 55 | 'persons_create', 57 | 'title' => 'Новый контакт успешно добавлен.' 58 | ]); ?> 59 |
60 | -------------------------------------------------------------------------------- /views/dashboard/index.php: -------------------------------------------------------------------------------- 1 | params['main_class'] = 'crm-content'; 7 | $this->title = 'Дашборд проекта'; 8 | 9 | /** 10 | * @var DashboardPresentation $presentationHelper 11 | */ 12 | ?> 13 |
14 |
15 |

user->getIdentity()->company; ?>

16 | 17 |
18 |
19 |
20 |
21 |
22 |
23 |

Горящие задачи

24 |

Задачи сроки выполнения которых наступили или скоро наступят

25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | getHotTasks() as $task): ?> 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
СделкаТипИсполнительСрок
deal->name; ?>

type->name; ?>

executor->name; ?>

formatter->asDate($task->due_date, 'short'); ?>

44 |
45 |
46 |
47 |
48 |
49 |

Новые компании

50 |

Недавно добавленные компании

51 |
52 |
53 |
54 |
    55 | getNewestCompanies() as $company): ?> 56 |
  • 57 |

    name; ?>

    58 |

    address; ?>

    59 |
  • 60 | 61 |
62 |
63 |
64 |
65 |
66 |
67 |

Финансы

68 |

Данные по финансам в компании

69 |
70 |
71 |
72 | getFinanceOverview() ?> 73 |
    74 |
  • 75 |

    formatter->asCurrency($forecast, 'RUB');?>

    76 |

    потенциал

    77 |
  • 78 |
  • 79 |

    formatter->asCurrency($total, 'RUB');?>

    80 |

    заработано

    81 |
  • 82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |

Счетчик сделок

91 |

Количество сделок в разных состояниях

92 |
93 |
94 |
95 |
    96 | getDealsCounters() as $status => $count): ?> 97 |
  • 98 |

    99 |

    100 |
  • 101 | 102 |
103 |
104 |
105 |
106 |
107 |
108 |

Общая статистика

109 |

Данные о текущем состоянии воронки продаж

110 |
111 |
112 |
113 | getCommonCounters(); ?> 114 |
    115 |
  • Выполненные сделки
  • 116 |
  • Контакты
  • 117 |
  • Компании
  • 118 |
  • Задачи
  • 119 |
  • Сотрудники
  • 120 |
121 |
122 |
123 |
124 |
125 |
126 |

Производительность на этой неделе

127 |

Количество передвижения задач и сделок из состояния в состояние

128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 | -------------------------------------------------------------------------------- /views/deals/index.php: -------------------------------------------------------------------------------- 1 | title = 'Сделки'; 18 | 19 | ?> 20 |
21 |
22 |

Сделки

23 | Создать 32 |
33 |
34 | 35 | deals; ?> 36 |
37 |
38 |

name; ?>

39 |
40 |

41 | 42 |

43 |

44 | formatter->asCurrency($dealStatus->dealsAmount, 'RUB');?> 45 |

46 |
47 |
48 | 63 |
64 | 65 |
66 | render('//modals/_deal_form', ['model' => new Deal]); ?> 67 | 'deals_create', 69 | 'title' => 'Новая сделка успешно добавлена.' 70 | ]); ?> 71 |
72 | -------------------------------------------------------------------------------- /views/inbox/mail.php: -------------------------------------------------------------------------------- 1 | params['main_class'] = 'communication'; 10 | $this->title = 'Сообщения электронной почты'; 11 | 12 | /** 13 | * @var $messages MailMessage[] 14 | * @var $selected_message MailMessage 15 | * @var $msgid string 16 | * @var $unread_count integer 17 | * @var $model EmailSearchForm 18 | */ 19 | 20 | $this->registerJsFile('@web/js/inbox.js', ['depends' => [JqueryAsset::class]]) 21 | ?> 22 | 23 |
24 |

Коммуникации

25 | 27 | 28 | 29 | 30 |
31 |
32 |
33 |
34 | 44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |

Отправитель

Тема

Дата

getSenderName(); ?>getSubject(); ?>formatter->asDate($message->getDate(), 'short'); ?>
64 |
65 |
66 | 67 |
68 | 76 |
77 |

getSubject(); ?>

formatter->asDatetime($selected_message->getDate(), 'short'); ?> 79 |
80 | getBody(); ?> 81 |
82 |
83 |
84 | 85 |
86 |
87 | -------------------------------------------------------------------------------- /views/landing/index.php: -------------------------------------------------------------------------------- 1 | params['bodyClass'] = 'landing'; 5 | $this->params['headerClass'] = 'landing-header'; 6 | $this->title = 'Лучшая CRM-система для вашей компании'; 7 | 8 | ?> 9 | 10 |
11 |

CRM-система для компаний любого профиля, работающих с клиентами через интернет.

12 | Попробовать › 13 |
14 |
15 |

Преимущества

16 |
    17 |
  • 18 | 19 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |

    Облачное решение

    34 |

    Наша CRM позволяет настроить двухстороннюю интеграцию с мессенджером 35 | telegram. Отвечайте и пишите вашим клиентам в telegram напрямую через наш сервис.

    36 |
  • 37 |
  • 38 | 39 | 41 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |

    Поддержка 24 / 7

    52 |

    Профессиональные консультанты ответят на ваши вопросы в любое время суток 53 | удобным для вас способом, по телефону, почте или через чат.

    54 |
  • 55 |
  • 56 | 57 | 59 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |

    Интеграция с Gmail

    70 |

    Наша CRM позволяет настроить двухстороннюю интеграцию с вашим почтовым ящиком на Gmail. 71 | Вы сможете добавлять клиентов и сделки прямо из электронной почты.

    72 |
  • 73 |
74 |
75 |
76 |

Все средства коммуникации в одном месте

77 |

Продвинутая интеграция с сервисами коммуникации. Есть поддержка электронной 78 | почты, СМС и Telegram. Общаться с клиентами и собирать заявки можно по любому из этих источников.

79 |
80 |
81 |

Запустите в работу ваши проекты уже сегодня

82 |
    83 |
  • Ведение базы клиентов и компаний
  • 84 |
  • Управление воронкой продаж
  • 85 |
  • Постановка и отслеживание задач
  • 86 |
  • Переписка с клиентами через встроенный интерфейс
  • 87 |
88 |
89 | -------------------------------------------------------------------------------- /views/layouts/anon.php: -------------------------------------------------------------------------------- 1 | registerLinkTag(['rel' => 'icon', 'type' => 'iamge/png', 'href' => '/favicon.png']); ?> 12 | beginPage() ?> 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | <?= Html::encode($this->title); ?> | TurboCRM 21 | registerCsrfMetaTags() ?> 22 | head() ?> 23 | 24 | 36 | 37 | 38 | 39 | 40 | beginBody() ?> 41 | 42 |
43 | Вход
46 |
47 | 48 |
49 |
50 | 53 | 54 | Логотип HTML Academy 56 | © ООО «Интерактивные обучающие технологии», 2013−2021 57 |
58 | render('//modals/_login_form', ['model' => new LoginForm]); ?> 59 | endBody(); ?> 60 | 61 | 62 | endPage(); ?> 63 | -------------------------------------------------------------------------------- /views/layouts/common.php: -------------------------------------------------------------------------------- 1 | registerLinkTag(['rel' => 'icon', 'type' => 'iamge/png', 'href' => '/favicon.png']); ?> 10 | beginPage(); ?> 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | <?= Html::encode($this->title); ?> | TurboCRM 19 | 20 | registerCsrfMetaTags(); ?> 21 | head(); ?> 22 | 23 | 35 | 36 | 37 | 38 | 39 | 40 | beginBody() ?> 41 | 42 | 67 |
68 | 69 |
70 | endBody() ?> 71 | 72 | 73 | endPage() ?> 74 | -------------------------------------------------------------------------------- /views/modals/_company_form.php: -------------------------------------------------------------------------------- 1 | id ? 'update' : 'create'; 10 | $title = $model->id ? 'Редактирование контакта' : 'Новый контакт'; 11 | ?> 12 | 13 | 46 | -------------------------------------------------------------------------------- /views/modals/_contact_form.php: -------------------------------------------------------------------------------- 1 | asArray()->all(); 10 | $types_list = ContactType::find()->asArray()->all(); 11 | 12 | $action = $model->id ? 'update' : 'create'; 13 | $title = $model->id ? 'Редактирование контакта' : 'Новый контакт'; 14 | 15 | ?> 16 | 17 | 63 | -------------------------------------------------------------------------------- /views/modals/_deal_form.php: -------------------------------------------------------------------------------- 1 | all(); 10 | $contacts = Contact::find()->all(); 11 | $statuses = DealStatus::find()->all(); 12 | $users = User::find()->all(); 13 | ?> 14 | 15 | 77 | 78 | -------------------------------------------------------------------------------- /views/modals/_login_form.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 31 | -------------------------------------------------------------------------------- /views/partials/_action_row.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 27 | -------------------------------------------------------------------------------- /views/partials/_grid_filter.php: -------------------------------------------------------------------------------- 1 | asArray()->all(); 12 | $companies = Company::find()->asArray()->all(); 13 | ?> 14 | 15 |
16 |
17 | 'get', 19 | 'options' => ['class' => 'filters-block__form'] 20 | ]); ?> 21 | field($model, 'type_id', [ 22 | 'template' => "{label}\n{input}", 23 | 'options' => ['class' => 'filters-block__field field field--select field--small'] 24 | ])->dropDownList(ArrayHelper::map($types_list, 'id', 'name'), [ 25 | 'class' => 'field__select select', 'prompt' => 'Статус']) 26 | ->label('Статус', ['class' => 'field__label']); ?> 27 | 28 | field($model, 'company_id', [ 29 | 'template' => "{label}\n{input}", 30 | 'options' => ['class' => 'filters-block__field field field--select field--small'] 31 | ])->dropDownList(ArrayHelper::map($companies, 'id', 'name'), [ 32 | 'class' => 'field__select select', 'prompt' => 'Компания'])->label('Компания', ['class' => 'field__label']); ?> 33 | 34 | field($model, 'onlyDeals', ['options' => ['class' => 'filters-block__field field']])->checkbox(); ?> 35 | 36 | 37 | 38 | 43 |
44 |
45 | -------------------------------------------------------------------------------- /views/partials/_grid_header.php: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 |

17 | 18 | Фильтровать 19 | 20 | Добавить 21 | 38 |
39 |
40 | ['class' => 'pagination-block'], 42 | 'linkContainerOptions' => ['tag' => 'span'], 43 | 'disabledPageCssClass' => 'pagination-block__btn--disabled', 44 | 'nextPageCssClass' => 'pagination-block__btn pagination-block__btn--next', 45 | 'nextPageLabel' => '', 46 | 'prevPageCssClass' => 'pagination-block__btn pagination-block__btn--prev', 47 | 'prevPageLabel' => '', 48 | 'pagination' => $dataProvider->getPagination() 49 | ]); ?> 50 |
51 |
52 |
53 | -------------------------------------------------------------------------------- /views/partials/_task_create.php: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 | Новая задача 17 |
18 | 23 |
24 | 'post', 'action' => ['tasks/create'], 25 | 'fieldConfig' => ['template' => '{input}']]); ?> 26 | field($model, 'description') 27 | ->textarea(['class' => 'textarea task-widget__textarea', 'rows' => 1, 'placeholder' => 'Описание']); ?> 28 | 29 |
30 | field($model, 'deal_id')->hiddenInput(['value' => $deal_id]); ?> 31 | field($model, 'type_id', ['options' => ['class' => 'task-widget__type']]) 32 | ->dropDownList(ArrayHelper::map(TaskType::find()->all(), 'id', 'name'), ['class' => 'select', 'prompt' => 'Тип']); ?> 33 | 34 | field($model, 'executor_id', ['options' => ['class' => 'task-widget__executor']]) 35 | ->dropDownList(ArrayHelper::map(User::find()->all(), 'id', 'name'), ['class' => 'select', 'prompt' => 'Исполнитель']); ?> 36 | 37 |
38 | field($model, 'due_date') 39 | ->input('text', ['class' => 'task-widget__input task-widget__input--date deal-form__field--date deal-form__field js-resizable', 'placeholder' => 'Срок']); ?> 40 |
41 |
42 | 43 |
44 | 45 |
46 |
47 |
48 | -------------------------------------------------------------------------------- /views/profile/settings.php: -------------------------------------------------------------------------------- 1 | params['main_class'] = 'settings'; 9 | $this->title = 'Настройки профиля'; 10 | 11 | ?> 12 | 13 |

Настройки

14 | 40 | 41 | -------------------------------------------------------------------------------- /views/tasks/index.php: -------------------------------------------------------------------------------- 1 | title = 'Список задач'; 13 | ?> 14 |
15 | render('//partials/_grid_header', [ 16 | 'model' => $model, 'dataProvider' => $dataProvider, 'name' => 'Задачи', 'showFilterBtn' => false 17 | ]); ?> 18 |
19 | $dataProvider, 21 | 'options' => ['class' => 'data-table'], 22 | 'summary' => '', 23 | 'layout' => "{summary}\n{items}", 24 | 'columns' => [ 25 | ['attribute' => 'dt_add', 'format' => 'date'], 26 | ['attribute' => 'type.name', 'label' => 'Тип'], 27 | ['attribute' => 'description'], 28 | ['attribute' => 'executor.name', 'label' => 'Исполнитель'], 29 | ['attribute' => 'due_date', 'format' => 'date'], 30 | ]]); ?> 31 |
32 | 38 |
39 | -------------------------------------------------------------------------------- /views/user/signup.php: -------------------------------------------------------------------------------- 1 | registerCssFile('css/sample.css'); 10 | $this->title = 'Регистрация нового пользователя'; 11 | 12 | ?> 13 | 14 |
15 | 'signup-form', 17 | 'enableAjaxValidation' => false, 18 | 'options' => ['class' => 'registration__form'], 19 | 'errorCssClass' => 'field--error', 20 | 'fieldConfig' => [ 21 | 'template' => "{input}\n{error}", 22 | 'options' => ['class' => 'field registration__field'], 23 | 'inputOptions' => ['class' => 'field__input input input--big placeholder-shown'], 24 | 'errorOptions' => ['tag' => 'span', 'class' => 'field__error-message'] 25 | ] 26 | ]); ?> 27 |

Регистрация

28 | 29 | field($model, 'email')->textInput(['placeholder' => 'Email']); ?> 30 | field($model, 'name')->textInput(['placeholder' => 'Ваше имя']); ?> 31 | field($model, 'phone')->textInput(['placeholder' => 'Номер телефона']); ?> 32 | field($model, 'company')->textInput(['placeholder' => 'Название компании']); ?> 33 | field($model, 'password')->passwordInput(['placeholder' => 'Пароль']); ?> 34 | field($model, 'password_repeat')->passwordInput(['placeholder' => 'Повтор пароля']); ?> 35 | 36 | 37 | 38 |
39 | -------------------------------------------------------------------------------- /web/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On RewriteBase / 2 | 3 | RewriteCond %{REQUEST_FILENAME} !-f 4 | RewriteCond %{REQUEST_FILENAME} !-d 5 | 6 | RewriteRule . index.php 7 | -------------------------------------------------------------------------------- /web/assets/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /web/css/sample.css: -------------------------------------------------------------------------------- 1 | /* этот файл нужен только для демонстрации его подключения средствами фреймворка */ 2 | -------------------------------------------------------------------------------- /web/css/site.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | 6 | .wrap { 7 | min-height: 100%; 8 | height: auto; 9 | margin: 0 auto -60px; 10 | padding: 0 0 60px; 11 | } 12 | 13 | .wrap > .container { 14 | padding: 70px 15px 20px; 15 | } 16 | 17 | .footer { 18 | height: 60px; 19 | background-color: #f5f5f5; 20 | border-top: 1px solid #ddd; 21 | padding-top: 20px; 22 | } 23 | 24 | .jumbotron { 25 | text-align: center; 26 | background-color: transparent; 27 | } 28 | 29 | .jumbotron .btn { 30 | font-size: 21px; 31 | padding: 14px 24px; 32 | } 33 | 34 | .not-set { 35 | color: #c55; 36 | font-style: italic; 37 | } 38 | 39 | /* add sorting icons to gridview sort links */ 40 | a.asc:after, a.desc:after { 41 | position: relative; 42 | top: 1px; 43 | display: inline-block; 44 | font-family: 'Glyphicons Halflings'; 45 | font-style: normal; 46 | font-weight: normal; 47 | line-height: 1; 48 | padding-left: 5px; 49 | } 50 | 51 | a.asc:after { 52 | content: "\e151"; 53 | } 54 | 55 | a.desc:after { 56 | content: "\e152"; 57 | } 58 | 59 | .sort-numerical a.asc:after { 60 | content: "\e153"; 61 | } 62 | 63 | .sort-numerical a.desc:after { 64 | content: "\e154"; 65 | } 66 | 67 | .sort-ordinal a.asc:after { 68 | content: "\e155"; 69 | } 70 | 71 | .sort-ordinal a.desc:after { 72 | content: "\e156"; 73 | } 74 | 75 | .grid-view td { 76 | white-space: nowrap; 77 | } 78 | 79 | .grid-view .filters input, 80 | .grid-view .filters select { 81 | min-width: 50px; 82 | } 83 | 84 | .hint-block { 85 | display: block; 86 | margin-top: 5px; 87 | color: #999; 88 | } 89 | 90 | .error-summary { 91 | color: #a94442; 92 | background: #fdf7f7; 93 | border-left: 3px solid #eed3d7; 94 | padding: 10px 20px; 95 | margin: 0 0 15px 0; 96 | } 97 | 98 | /* align the logout "link" (button in form) of the navbar */ 99 | .nav li > form > button.logout { 100 | padding: 15px; 101 | border: none; 102 | } 103 | 104 | @media(max-width:767px) { 105 | .nav li > form > button.logout { 106 | display:block; 107 | text-align: left; 108 | width: 100%; 109 | padding: 10px 15px; 110 | } 111 | } 112 | 113 | .nav > li > form > button.logout:focus, 114 | .nav > li > form > button.logout:hover { 115 | text-decoration: none; 116 | } 117 | 118 | .nav > li > form > button.logout:focus { 119 | outline: none; 120 | } 121 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmlacademy/intensive-php2-turbocrm/ca0596b0da2c3bb573345ee461ad4ec94a289559/web/favicon.png -------------------------------------------------------------------------------- /web/fonts/ptsans.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmlacademy/intensive-php2-turbocrm/ca0596b0da2c3bb573345ee461ad4ec94a289559/web/fonts/ptsans.woff -------------------------------------------------------------------------------- /web/fonts/ptsans.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmlacademy/intensive-php2-turbocrm/ca0596b0da2c3bb573345ee461ad4ec94a289559/web/fonts/ptsans.woff2 -------------------------------------------------------------------------------- /web/fonts/ptsansbold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmlacademy/intensive-php2-turbocrm/ca0596b0da2c3bb573345ee461ad4ec94a289559/web/fonts/ptsansbold.woff -------------------------------------------------------------------------------- /web/fonts/ptsansbold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmlacademy/intensive-php2-turbocrm/ca0596b0da2c3bb573345ee461ad4ec94a289559/web/fonts/ptsansbold.woff2 -------------------------------------------------------------------------------- /web/img/COLOR/black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | COLOR/ black 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /web/img/arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/img/bg-empty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/img/cloud-bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/img/cosmonaut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmlacademy/intensive-php2-turbocrm/ca0596b0da2c3bb573345ee461ad4ec94a289559/web/img/cosmonaut.png -------------------------------------------------------------------------------- /web/img/cosmonaut@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmlacademy/intensive-php2-turbocrm/ca0596b0da2c3bb573345ee461ad4ec94a289559/web/img/cosmonaut@2x.png -------------------------------------------------------------------------------- /web/img/footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmlacademy/intensive-php2-turbocrm/ca0596b0da2c3bb573345ee461ad4ec94a289559/web/img/footer.png -------------------------------------------------------------------------------- /web/img/footer@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmlacademy/intensive-php2-turbocrm/ca0596b0da2c3bb573345ee461ad4ec94a289559/web/img/footer@2x.png -------------------------------------------------------------------------------- /web/img/html-academy-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmlacademy/intensive-php2-turbocrm/ca0596b0da2c3bb573345ee461ad4ec94a289559/web/img/html-academy-logo.png -------------------------------------------------------------------------------- /web/img/html-academy-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmlacademy/intensive-php2-turbocrm/ca0596b0da2c3bb573345ee461ad4ec94a289559/web/img/html-academy-logo@2x.png -------------------------------------------------------------------------------- /web/img/logo-mono.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /web/img/next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | chevron-right 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /web/img/rocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmlacademy/intensive-php2-turbocrm/ca0596b0da2c3bb573345ee461ad4ec94a289559/web/img/rocket.png -------------------------------------------------------------------------------- /web/img/rocket@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmlacademy/intensive-php2-turbocrm/ca0596b0da2c3bb573345ee461ad4ec94a289559/web/img/rocket@2x.png -------------------------------------------------------------------------------- /web/img/shuttle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmlacademy/intensive-php2-turbocrm/ca0596b0da2c3bb573345ee461ad4ec94a289559/web/img/shuttle.png -------------------------------------------------------------------------------- /web/img/shuttle@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmlacademy/intensive-php2-turbocrm/ca0596b0da2c3bb573345ee461ad4ec94a289559/web/img/shuttle@2x.png -------------------------------------------------------------------------------- /web/img/turbocrm-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /web/index.php: -------------------------------------------------------------------------------- 1 | run(); 11 | -------------------------------------------------------------------------------- /web/js/deal-form.js: -------------------------------------------------------------------------------- 1 | var $form = $('#deal-form'); 2 | 3 | $form.on('change', 'select, input, textarea', function() { 4 | var data = $form.serialize(); 5 | 6 | $.ajax({ 7 | url: $form.attr('action'), 8 | type: 'POST', 9 | data: data 10 | }); 11 | 12 | return false; 13 | }); 14 | -------------------------------------------------------------------------------- /web/js/grid.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | var urlParams = new URLSearchParams(window.location.search); 3 | 4 | if (urlParams.get('modal')) { 5 | var body = document.querySelector('body'); 6 | var modal = document.querySelector('.modal'); 7 | 8 | modal.classList.add('show'); 9 | body.classList.add('modal-open'); 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /web/js/inbox.js: -------------------------------------------------------------------------------- 1 | $('table.mail-list__table tbody').on('click', 'tr', function (event) { 2 | var msgId = $(event.currentTarget).data('id'); 3 | 4 | window.location = '/inbox/email?msgid=' + msgId; 5 | }); 6 | -------------------------------------------------------------------------------- /widgets/Alert.php: -------------------------------------------------------------------------------- 1 | session->setFlash('error', 'This is the message'); 12 | * Yii::$app->session->setFlash('success', 'This is the message'); 13 | * Yii::$app->session->setFlash('info', 'This is the message'); 14 | * ``` 15 | * 16 | * Multiple messages could be set as follows: 17 | * 18 | * ```php 19 | * Yii::$app->session->setFlash('error', ['Error 1', 'Error 2']); 20 | * ``` 21 | * 22 | * @author Kartik Visweswaran 23 | * @author Alexander Makarov 24 | */ 25 | class Alert extends \yii\bootstrap\Widget 26 | { 27 | /** 28 | * @var array the alert types configuration for the flash messages. 29 | * This array is setup as $key => $value, where: 30 | * - key: the name of the session flash variable 31 | * - value: the bootstrap alert type (i.e. danger, success, info, warning) 32 | */ 33 | public $alertTypes = [ 34 | 'error' => 'alert-danger', 35 | 'danger' => 'alert-danger', 36 | 'success' => 'alert-success', 37 | 'info' => 'alert-info', 38 | 'warning' => 'alert-warning' 39 | ]; 40 | /** 41 | * @var array the options for rendering the close button tag. 42 | * Array will be passed to [[\yii\bootstrap\Alert::closeButton]]. 43 | */ 44 | public $closeButton = []; 45 | 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function run() 51 | { 52 | $session = Yii::$app->session; 53 | $flashes = $session->getAllFlashes(); 54 | $appendClass = isset($this->options['class']) ? ' ' . $this->options['class'] : ''; 55 | 56 | foreach ($flashes as $type => $flash) { 57 | if (!isset($this->alertTypes[$type])) { 58 | continue; 59 | } 60 | 61 | foreach ((array) $flash as $i => $message) { 62 | echo \yii\bootstrap\Alert::widget([ 63 | 'body' => $message, 64 | 'closeButton' => $this->closeButton, 65 | 'options' => array_merge($this->options, [ 66 | 'id' => $this->getId() . '-' . $type . '-' . $i, 67 | 'class' => $this->alertTypes[$type] . $appendClass, 68 | ]), 69 | ]); 70 | } 71 | 72 | $session->removeFlash($type); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /widgets/FeedItem.php: -------------------------------------------------------------------------------- 1 | render('feed', ['model' => $this->model]); 19 | } 20 | 21 | 22 | } 23 | -------------------------------------------------------------------------------- /widgets/LinkPager.php: -------------------------------------------------------------------------------- 1 | pagination->getPageCount(); 15 | if ($pageCount < 2 && $this->hideOnSinglePage) { 16 | return ''; 17 | } 18 | 19 | $buttons = []; 20 | $currentPage = $this->pagination->getPage(); 21 | 22 | 23 | // prev page 24 | if ($this->prevPageLabel !== false) { 25 | if (($page = $currentPage - 1) < 0) { 26 | $page = 0; 27 | } 28 | $buttons[] = $this->renderPageButton($this->prevPageLabel, $page, $this->prevPageCssClass, $currentPage <= 0, false); 29 | } 30 | 31 | $internal = Html::tag('div', 32 | Html::tag('span', $currentPage + 1, ['class' => 'pagination-block__current']) . 33 | Html::tag('span', ' / ') . 34 | Html::tag('span', $this->pagination->getPageCount(), ['class' => 'pagination-block__total']) 35 | , ['class' => 'pagination-block__pages']); 36 | 37 | $buttons[] = $internal; 38 | 39 | // next page 40 | if ($this->nextPageLabel !== false) { 41 | if (($page = $currentPage + 1) >= $pageCount - 1) { 42 | $page = $pageCount - 1; 43 | } 44 | $buttons[] = $this->renderPageButton($this->nextPageLabel, $page, $this->nextPageCssClass, $currentPage >= $pageCount - 1, false); 45 | } 46 | 47 | $options = $this->options; 48 | $tag = ArrayHelper::remove($options, 'tag', 'ul'); 49 | return Html::tag($tag, implode("\n", $buttons), $options); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /widgets/Notification.php: -------------------------------------------------------------------------------- 1 | renderBody(); 25 | 26 | if (!$this->flashName || Yii::$app->session->hasFlash($this->flashName)) { 27 | return $content; 28 | } 29 | } 30 | 31 | private function renderCloseButton() 32 | { 33 | $icon = ''; 34 | $button = Html::button($icon, ['class' => 'alert__close button button--icon']); 35 | 36 | return $button; 37 | } 38 | 39 | private function renderBody() 40 | { 41 | $msg = Html::tag('p', $this->title, ['class' => 'alert__message']); 42 | $body = Html::tag('div', $this->renderCloseButton() . $msg, ['class' => 'alert']); 43 | 44 | return $body; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /widgets/views/feed.php: -------------------------------------------------------------------------------- 1 | 11 | 12 |
  • 13 |

    14 | formatter->asDate($model->dt_add, 'short')?> 15 | 16 |

    17 |
    К
    18 |
    user->name; ?> 19 | type): ?> 20 | создал сделку, статусНовая 21 | 22 | 23 | 24 | getAssociatedContent(); ?> 25 | перевел сделку в новый статус 26 | name ?? null; ?> 27 | 28 | 29 | 30 | оставил заметку 31 |

    getAssociatedContent()->content ? Html::encode($model->getAssociatedContent()->content) : null; ?>

    32 | 33 |
    34 |
  • 35 | -------------------------------------------------------------------------------- /yii.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem ------------------------------------------------------------- 4 | rem Yii command line bootstrap script for Windows. 5 | rem 6 | rem @author Qiang Xue 7 | rem @link http://www.yiiframework.com/ 8 | rem @copyright Copyright (c) 2008 Yii Software LLC 9 | rem @license http://www.yiiframework.com/license/ 10 | rem ------------------------------------------------------------- 11 | 12 | @setlocal 13 | 14 | set YII_PATH=%~dp0 15 | 16 | if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe 17 | 18 | "%PHP_COMMAND%" "%YII_PATH%yii" %* 19 | 20 | @endlocal 21 | --------------------------------------------------------------------------------