From: Aleksey Filippov Date: Thu, 12 Feb 2026 14:29:15 +0000 (+0300) Subject: [ERP-39] Создание страницы для добавления курьеров. X-Git-Url: https://gitweb.erp-flowers.ru/?a=commitdiff_plain;h=03ffba760347d5a1933ba0e196c9e40b20d54c52;p=erp24_rep%2Fyii-erp24%2F.git [ERP-39] Создание страницы для добавления курьеров. --- diff --git a/erp24/controllers/CourierController.php b/erp24/controllers/CourierController.php new file mode 100644 index 00000000..802e6ae6 --- /dev/null +++ b/erp24/controllers/CourierController.php @@ -0,0 +1,237 @@ + [ + 'class' => VerbFilter::class, + 'actions' => [ + 'delete' => ['POST'], + ], + ], + ]); + } + + /** + * Список всех курьеров + * + * @return string + */ + public function actionIndex() + { + $searchModel = new CourierSearch(); + $dataProvider = $searchModel->search($this->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); + } + + /** + * Просмотр курьера + * + * @param int $id ID курьера + * @return string + * @throws NotFoundHttpException если курьер не найден + */ + public function actionView($id) + { + return $this->render('view', [ + 'model' => $this->findModel($id), + ]); + } + + /** + * Создание нового курьера + * + * @return string|\yii\web\Response + */ + public function actionCreate() + { + $model = new Admin(); + + // Устанавливаем значения по умолчанию для курьера + $model->guid = Yii::$app->security->generateRandomString(36); + $model->group_id = self::COURIER_GROUP_ID; + $model->active = '1'; + $model->vcompany = 1; + $model->grazhdanstvo = 'РФ'; + + if ($this->request->isPost) { + $this->loadAllowedFields($model, $this->request->post('Admin', [])); + + if ($model->save(false)) { + Yii::$app->session->setFlash('success', 'Курьер успешно добавлен'); + return $this->redirect(['view', 'id' => $model->id]); + } + } + + return $this->render('create', ['model' => $model]); + } + + /** + * Редактирование курьера + * + * @param int $id ID курьера + * @return string|\yii\web\Response + * @throws NotFoundHttpException если курьер не найден + */ + public function actionUpdate($id) + { + $model = $this->findModel($id); + + if ($this->request->isPost) { + $this->loadAllowedFields($model, $this->request->post('Admin', [])); + + if ($model->save(false)) { + Yii::$app->session->setFlash('success', 'Курьер успешно обновлён'); + return $this->redirect(['view', 'id' => $model->id]); + } + } + + return $this->render('update', ['model' => $model]); + } + + /** + * Удаление курьера (только неактивного) + * + * @param int $id ID курьера + * @return \yii\web\Response + * @throws NotFoundHttpException если курьер не найден + */ + public function actionDelete($id) + { + $model = $this->findModel($id); + + if ($model->active == 0 || $model->active === '0') { + $model->delete(); + Yii::$app->session->setFlash('success', 'Курьер удалён'); + } else { + Yii::$app->session->setFlash('error', 'Можно удалить только неактивного курьера'); + } + + return $this->redirect(['index']); + } + + /** + * Поиск модели курьера по ID + * + * @param int $id ID курьера + * @return Admin + * @throws NotFoundHttpException если курьер не найден + */ + protected function findModel($id) + { + $model = Admin::find() + ->where(['id' => $id, 'group_id' => self::COURIER_GROUP_ID]) + ->one(); + + if ($model === null) { + throw new NotFoundHttpException('Курьер не найден'); + } + + return $model; + } + + /** + * Загрузка разрешённых полей в модель из POST-данных + * + * @param Admin $model Модель курьера + * @param array $post POST-данные формы + */ + protected function loadAllowedFields(Admin $model, array $post): void + { + foreach (self::ALLOWED_FIELDS as $field) { + if (array_key_exists($field, $post)) { + $value = $post[$field]; + // Преобразуем пустые строки в null для числовых и enum полей + if ($value === '' && in_array($field, [ + // Числовые поля + 'work_status', 'tabel_number', 'kol_deti', 'summa_oklad', + 'summa_oklad_nalog', 'avans_percent', 'sale_percent', 'posit', + // Enum поля + 'pol', 'tip_ustroen', + ])) { + $value = null; + } + $model->$field = $value; + } + } + } +} diff --git a/erp24/records/CourierSearch.php b/erp24/records/CourierSearch.php new file mode 100644 index 00000000..9e9f31a0 --- /dev/null +++ b/erp24/records/CourierSearch.php @@ -0,0 +1,66 @@ +where(['group_id' => self::COURIER_GROUP_ID]) + ->orderBy(['active' => SORT_DESC, 'name' => SORT_ASC]); + + $dataProvider = new ActiveDataProvider([ + 'query' => $query, + 'pagination' => ['pageSize' => 50], + ]); + + $this->load($params); + + if (!$this->validate()) { + return $dataProvider; + } + + $query->andFilterWhere(['id' => $this->id]) + ->andFilterWhere(['active' => $this->active]) + ->andFilterWhere(['ilike', 'name', $this->name]) + ->andFilterWhere(['ilike', 'pass_user', $this->pass_user]); + + return $dataProvider; + } +} diff --git a/erp24/tests/unit/ShiftReminderP0Test.php b/erp24/tests/unit/ShiftReminderP0Test.php new file mode 100644 index 00000000..73ab09de --- /dev/null +++ b/erp24/tests/unit/ShiftReminderP0Test.php @@ -0,0 +1,430 @@ +cache = new ArrayCache(); + Yii::$app->set('cache', $this->cache); + } + + protected function _after() + { + parent::_after(); + } + + // ========================================== + // P0.1: RATE LIMITING CACHE PERSISTENCE (6 тестов) + // ========================================== + + /** + * Тест: Cache хранит данные rate limiting между вызовами + * + * Доказывает что cache-подход (исправленный баг) работает корректно, + * в отличие от static-переменной которая не сохранялась между HTTP-запросами. + */ + public function testRateLimit_CachePersistence_DataSurvivesBetweenCalls() + { + // Arrange + $key = 'shift_reminder_check_1'; + $requests = [time(), time()]; + + // Act - записываем данные в cache (имитируем первый HTTP-запрос) + $this->cache->set($key, $requests, 60); + + // Имитируем "новый HTTP-запрос" — данные должны быть в cache + $retrieved = $this->cache->get($key); + + // Assert + $this->assertNotFalse($retrieved, 'Cache должен сохранять данные между вызовами'); + $this->assertCount(2, $retrieved, 'Cache должен хранить все записи'); + $this->assertEquals($requests, $retrieved, 'Данные в cache должны быть идентичны записанным'); + } + + /** + * Тест: Rate limit блокирует при превышении лимита + * + * Имитирует алгоритм checkRateLimit из ShiftReminderController. + */ + public function testRateLimit_ExceedsLimit_ReturnsFalse() + { + // Arrange + $userId = 1; + $action = 'check'; + $limit = 10; // 10 запросов в минуту (как в контроллере) + $key = "shift_reminder_{$action}_{$userId}"; + $now = time(); + + // Имитируем 10 предыдущих запросов + $requests = []; + for ($i = 0; $i < $limit; $i++) { + $requests[] = $now; + } + $this->cache->set($key, $requests, 60); + + // Act — имитируем алгоритм checkRateLimit + $cachedRequests = $this->cache->get($key); + $cachedRequests = array_filter( + $cachedRequests, + function ($timestamp) use ($now) { + return ($now - $timestamp) < 60; + } + ); + $isBlocked = count($cachedRequests) >= $limit; + + // Assert + $this->assertTrue($isBlocked, 'Rate limit должен блокировать при превышении 10 запросов в минуту'); + } + + /** + * Тест: Rate limit пропускает если лимит не превышен + */ + public function testRateLimit_UnderLimit_ReturnsTrue() + { + // Arrange + $userId = 1; + $action = 'check'; + $limit = 10; + $key = "shift_reminder_{$action}_{$userId}"; + $now = time(); + + // Имитируем 5 предыдущих запросов (менее лимита) + $requests = []; + for ($i = 0; $i < 5; $i++) { + $requests[] = $now; + } + $this->cache->set($key, $requests, 60); + + // Act + $cachedRequests = $this->cache->get($key); + $cachedRequests = array_filter( + $cachedRequests, + function ($timestamp) use ($now) { + return ($now - $timestamp) < 60; + } + ); + $isAllowed = count($cachedRequests) < $limit; + + // Assert + $this->assertTrue($isAllowed, 'Rate limit должен пропускать при 5 запросах (лимит 10)'); + } + + /** + * Тест: Rate limit сбрасывается после истечения TTL (60 секунд) + * + * Имитирует старые запросы (>60 секунд назад) которые должны быть отфильтрованы. + */ + public function testRateLimit_ExpiredRequests_AreFiltered() + { + // Arrange + $userId = 1; + $action = 'check'; + $limit = 10; + $key = "shift_reminder_{$action}_{$userId}"; + $now = time(); + + // Создаём 10 запросов: 8 устаревших (>60 сек назад) + 2 свежих + $requests = []; + for ($i = 0; $i < 8; $i++) { + $requests[] = $now - 120; // 2 минуты назад — устаревшие + } + $requests[] = $now - 5; // 5 секунд назад — свежий + $requests[] = $now; // сейчас — свежий + + $this->cache->set($key, $requests, 60); + + // Act — фильтруем как в checkRateLimit + $cachedRequests = $this->cache->get($key); + $filteredRequests = array_filter( + $cachedRequests, + function ($timestamp) use ($now) { + return ($now - $timestamp) < 60; + } + ); + $isAllowed = count($filteredRequests) < $limit; + + // Assert + $this->assertCount(2, $filteredRequests, 'После фильтрации должно остаться 2 свежих запроса'); + $this->assertTrue($isAllowed, 'Rate limit должен пропускать (2 свежих запроса < 10)'); + } + + /** + * Тест: Rate limit для confirm отличается от check (5 vs 10) + */ + public function testRateLimit_ConfirmAction_HasLowerLimit() + { + // Arrange + $userId = 1; + $confirmLimit = 5; // confirm: 5 запросов в минуту + $key = "shift_reminder_confirm_{$userId}"; + $now = time(); + + // Имитируем 5 запросов (ровно на лимите) + $requests = []; + for ($i = 0; $i < $confirmLimit; $i++) { + $requests[] = $now; + } + $this->cache->set($key, $requests, 60); + + // Act + $cachedRequests = $this->cache->get($key); + $cachedRequests = array_filter( + $cachedRequests, + function ($timestamp) use ($now) { + return ($now - $timestamp) < 60; + } + ); + $isBlocked = count($cachedRequests) >= $confirmLimit; + + // Assert + $this->assertTrue($isBlocked, 'Confirm действие должно блокироваться при 5 запросах'); + } + + /** + * Тест: Полный цикл rate limiting — N запросов подряд через cache + * + * Симулирует реальную последовательность вызовов checkRateLimit. + */ + public function testRateLimit_FullCycle_BlocksAtCorrectPoint() + { + // Arrange + $userId = 42; + $action = 'check'; + $limit = 10; + $key = "shift_reminder_{$action}_{$userId}"; + $now = time(); + $results = []; + + // Act — имитируем 12 последовательных вызовов checkRateLimit + for ($i = 0; $i < 12; $i++) { + // Читаем из cache (как в checkRateLimit) + $requests = $this->cache->get($key); + if ($requests === false) { + $requests = []; + } + + // Фильтруем устаревшие + $requests = array_filter( + $requests, + function ($timestamp) use ($now) { + return ($now - $timestamp) < 60; + } + ); + + // Проверяем лимит + if (count($requests) >= $limit) { + $results[] = false; // Blocked + continue; + } + + // Добавляем текущий запрос + $requests[] = $now; + $this->cache->set($key, array_values($requests), 60); + $results[] = true; // Allowed + } + + // Assert + // Первые 10 запросов должны пройти + for ($i = 0; $i < 10; $i++) { + $this->assertTrue($results[$i], "Запрос #{$i} должен быть разрешён (внутри лимита)"); + } + // 11-й и 12-й должны быть заблокированы + $this->assertFalse($results[10], 'Запрос #10 должен быть заблокирован (превышение лимита)'); + $this->assertFalse($results[11], 'Запрос #11 должен быть заблокирован (превышение лимита)'); + } + + // ========================================== + // P0.2: RACE CONDITIONS (4 теста) + // ========================================== + + /** + * Тест: confirmReminder() идемпотентен — двойной вызов не создаёт дубликат + * + * Имитирует ситуацию когда два tab/window отправляют confirm одновременно, + * но запросы обрабатываются последовательно. + */ + public function testConfirmReminder_DoubleCall_OnlyOneRecordCreated() + { + // Arrange + $userId = 999; + $reminderKey = 'day_shift'; + $referenceDate = '2026-02-10'; + $testTime = '2026-02-10 08:15:00'; + + // Act — первый "запрос" создаёт запись и подтверждает + $model1 = new ShiftReminderShown(); + $model1->user_id = $userId; + $model1->reminder_key = $reminderKey; + $model1->reference_date = $referenceDate; + $model1->save(false); + $model1->markAsConfirmed(); + + // Второй "запрос" пытается создать такую же запись + // createReminderRecord() делает findOne сначала — должен найти + $existing = ShiftReminderShown::find() + ->where([ + 'user_id' => $userId, + 'reminder_key' => $reminderKey, + 'reference_date' => $referenceDate, + ]) + ->one(); + + // Assert + $this->assertNotNull($existing, 'Существующая запись должна быть найдена'); + $this->assertTrue($existing->isConfirmed(), 'Запись должна быть подтверждена'); + + // Проверяем что в БД только одна запись + $count = ShiftReminderShown::find() + ->where([ + 'user_id' => $userId, + 'reminder_key' => $reminderKey, + 'reference_date' => $referenceDate, + ]) + ->count(); + $this->assertEquals(1, $count, 'В БД должна быть ровно одна запись'); + } + + /** + * Тест: Unique constraint предотвращает создание дубликатов при concurrent insert + * + * Симулирует race condition: два процесса одновременно пытаются вставить + * одинаковую запись (оба прошли findOne → null). + */ + public function testConcurrentInsert_UniqueConstraint_PreventsSecondInsert() + { + // Arrange + $userId = 998; + $reminderKey = 'night_shift'; + $referenceDate = '2026-02-10'; + + // Act — первый insert проходит успешно + $first = new ShiftReminderShown(); + $first->user_id = $userId; + $first->reminder_key = $reminderKey; + $first->reference_date = $referenceDate; + $saveResult = $first->save(false); + $this->assertTrue($saveResult, 'Первый insert должен пройти успешно'); + + // Второй insert с тем же ключом — должен упасть на unique constraint + $second = new ShiftReminderShown(); + $second->user_id = $userId; + $second->reminder_key = $reminderKey; + $second->reference_date = $referenceDate; + + // Assert — IntegrityException от unique constraint + $this->expectException(IntegrityException::class); + $second->save(false); + } + + /** + * Тест: markAsConfirmed() при concurrent вызовах — оба вызова корректны + * + * Два процесса находят одну и ту же запись и оба вызывают markAsConfirmed(). + * Благодаря идемпотентности, оба вызова должны завершиться успешно. + */ + public function testMarkAsConfirmed_ConcurrentCalls_BothSucceed() + { + // Arrange + $model = new ShiftReminderShown(); + $model->user_id = 997; + $model->reminder_key = 'day_shift'; + $model->reference_date = '2026-02-10'; + $model->save(false); + + // Имитируем два параллельных процесса, оба нашли запись + $processA = ShiftReminderShown::findOne($model->id); + $processB = ShiftReminderShown::findOne($model->id); + + // Act — оба вызывают markAsConfirmed + $resultA = $processA->markAsConfirmed(); + $resultB = $processB->markAsConfirmed(); + + // Assert — оба должны успешно завершиться + $this->assertTrue($resultA, 'Процесс A: markAsConfirmed должен пройти'); + $this->assertTrue($resultB, 'Процесс B: markAsConfirmed должен пройти'); + + // Проверяем финальное состояние в БД + $final = ShiftReminderShown::findOne($model->id); + $this->assertTrue($final->isConfirmed(), 'Запись должна быть подтверждена'); + $this->assertNotNull($final->confirmed_at, 'confirmed_at должен быть установлен'); + } + + /** + * Тест: createReminderRecord() возвращает существующую запись (не дублирует) + * + * Доказывает что сервисный метод безопасен при concurrent вызовах — + * findOne() предотвращает дублирование на уровне приложения. + */ + public function testCreateReminderRecord_ExistingRecord_ReturnsExisting() + { + // Arrange — создаём запись напрямую + $userId = 996; + $reminderKey = 'day_shift'; + $referenceDate = '2026-02-10'; + + $original = new ShiftReminderShown(); + $original->user_id = $userId; + $original->reminder_key = $reminderKey; + $original->reference_date = $referenceDate; + $original->save(false); + $originalId = $original->id; + + // Act — находим запись как это делает createReminderRecord + $existing = ShiftReminderShown::find() + ->where([ + 'user_id' => $userId, + 'reminder_key' => $reminderKey, + 'reference_date' => $referenceDate, + ]) + ->one(); + + // Assert — должен вернуть существующую запись (не null, не новую) + $this->assertNotNull($existing, 'Должна найтись существующая запись'); + $this->assertEquals($originalId, $existing->id, 'Должен вернуть ту же запись (по ID)'); + $this->assertEquals($userId, $existing->user_id); + $this->assertEquals($reminderKey, $existing->reminder_key); + $this->assertEquals($referenceDate, $existing->reference_date); + + // Убеждаемся что запись единственная + $count = ShiftReminderShown::find() + ->where([ + 'user_id' => $userId, + 'reminder_key' => $reminderKey, + 'reference_date' => $referenceDate, + ]) + ->count(); + $this->assertEquals(1, $count, 'Должна быть ровно одна запись'); + } +} diff --git a/erp24/views/courier/_form.php b/erp24/views/courier/_form.php new file mode 100644 index 00000000..a17bbb2c --- /dev/null +++ b/erp24/views/courier/_form.php @@ -0,0 +1,350 @@ + + +
+ + + isNewRecord): ?> +
+ + +
+ + + +
+
+
Основная информация
+
+
+
+
+ field($model, 'name')->textInput([ + 'maxlength' => 55, + 'placeholder' => 'Краткое имя' + ])->label('Имя') ?> +
+
+ field($model, 'name_full')->textInput([ + 'maxlength' => 200, + 'placeholder' => 'Полное ФИО' + ])->label('Полное имя') ?> +
+
+ +
+
+ field($model, 'login_user')->textInput([ + 'maxlength' => 29, + 'placeholder' => 'Логин для входа' + ])->label('Логин') ?> +
+
+ field($model, 'pass_user')->textInput([ + 'maxlength' => 120, + 'placeholder' => 'Пароль' + ])->label('Пароль') ?> +
+
+ field($model, 'mobile')->textInput([ + 'maxlength' => 25, + 'placeholder' => '+7XXXXXXXXXX' + ])->label('Мобильный телефон') ?> +
+
+ +
+
+ field($model, 'active')->dropDownList([ + '1' => 'Активный', + '0' => 'Не активный', + ])->label('Статус') ?> +
+
+ field($model, 'work_status')->dropDownList([ + '' => '-- Выберите --', + '1' => 'Работает', + '4' => 'Уволен', + ], ['prompt' => '-- Выберите --'])->label('Статус работы') ?> +
+
+ field($model, 'pol')->dropDownList([ + '' => '-- Выберите --', + 'М' => 'Мужской', + 'Ж' => 'Женский', + ])->label('Пол') ?> +
+
+ +
+
+ field($model, 'birthdate')->textInput([ + 'type' => 'date' + ])->label('Дата рождения') ?> +
+
+ field($model, 'grazhdanstvo')->textInput([ + 'maxlength' => 120, + 'placeholder' => 'РФ' + ])->label('Гражданство') ?> +
+
+
+
+ + +
+
+
Паспортные данные
+
+
+
+
+ field($model, 'passport_seriya')->textInput([ + 'maxlength' => 120, + 'placeholder' => 'Серия' + ])->label('Серия паспорта') ?> +
+
+ field($model, 'passport_nomer')->textInput([ + 'maxlength' => 12, + 'placeholder' => 'Номер' + ])->label('Номер паспорта') ?> +
+
+ field($model, 'data_passport')->textInput([ + 'type' => 'date' + ])->label('Дата выдачи') ?> +
+
+ +
+
+ field($model, 'kem_vidan')->textarea([ + 'rows' => 2, + 'placeholder' => 'Кем выдан паспорт' + ])->label('Кем выдан') ?> +
+
+ field($model, 'passport_kod_podrazdel')->textInput([ + 'maxlength' => 10, + 'placeholder' => 'XXX-XXX' + ])->label('Код подразделения') ?> +
+
+ +
+
+ field($model, 'passport_mesto_rozhdeniya')->textarea([ + 'rows' => 2, + 'placeholder' => 'Место рождения по паспорту' + ])->label('Место рождения') ?> +
+
+ +
+
+ field($model, 'passport_srok_begin')->textInput([ + 'type' => 'date' + ])->label('Срок действия с') ?> +
+
+ field($model, 'passport_end')->textInput([ + 'type' => 'date' + ])->label('Срок действия до') ?> +
+
+
+
+ + +
+
+
Адреса
+
+
+
+
+ field($model, 'adress')->textarea([ + 'rows' => 2, + 'placeholder' => 'Адрес регистрации' + ])->label('Адрес регистрации') ?> +
+
+ +
+
+ field($model, 'adress_fakt')->textInput([ + 'maxlength' => 255, + 'placeholder' => 'Фактический адрес проживания' + ])->label('Фактический адрес') ?> +
+
+ +
+
+ field($model, 'adress_prozhivaniya')->textarea([ + 'rows' => 2, + 'placeholder' => 'Адрес проживания' + ])->label('Адрес проживания') ?> +
+
+ +
+
+ field($model, 'mesto_r')->textarea([ + 'rows' => 2, + 'placeholder' => 'Место рождения' + ])->label('Место рождения') ?> +
+
+ field($model, 'adress_info')->textarea([ + 'rows' => 2, + 'placeholder' => 'Дополнительная информация' + ])->label('Доп. информация по адресу') ?> +
+
+
+
+ + +
+
+
Документы
+
+
+
+
+ field($model, 'inn')->textInput([ + 'maxlength' => 17, + 'placeholder' => 'ИНН' + ])->label('ИНН') ?> +
+
+ field($model, 'snils')->textInput([ + 'maxlength' => 25, + 'placeholder' => 'СНИЛС' + ])->label('СНИЛС') ?> +
+
+
+
+ + +
+
+
Трудоустройство
+
+
+
+
+ field($model, 'data_priem')->textInput([ + 'type' => 'date' + ])->label('Дата приёма') ?> +
+
+ field($model, 'data_uval')->textInput([ + 'type' => 'date' + ])->label('Дата увольнения') ?> +
+
+ field($model, 'tabel_number')->textInput([ + 'type' => 'number' + ])->label('Табельный номер') ?> +
+
+ +
+
+ field($model, 'vid_zanatosti')->textInput([ + 'maxlength' => 100, + 'placeholder' => 'Вид занятости' + ])->label('Вид занятости') ?> +
+
+ field($model, 'tip_ustroen')->dropDownList([ + '' => '-- Выберите --', + 'ТК' => 'Трудовой кодекс', + 'ГПХ' => 'ГПХ', + 'СЗ' => 'Самозанятый', + ])->label('Тип устройства') ?> +
+
+ field($model, 'kol_deti')->textInput([ + 'type' => 'number', + 'min' => 0 + ])->label('Количество детей') ?> +
+
+ +
+
+ field($model, 'summa_oklad')->textInput([ + 'type' => 'number', + 'min' => 0 + ])->label('Оклад') ?> +
+
+ field($model, 'summa_oklad_nalog')->textInput([ + 'type' => 'number', + 'min' => 0 + ])->label('Оклад (налог)') ?> +
+
+ field($model, 'avans_percent')->textInput([ + 'type' => 'number', + 'min' => 0, + 'max' => 100 + ])->label('Процент аванса') ?> +
+
+ +
+
+ field($model, 'sale_percent')->textInput([ + 'type' => 'number', + 'step' => '0.01', + 'min' => 0 + ])->label('Процент продажи') ?> +
+
+ field($model, 'posit')->textInput([ + 'type' => 'number' + ])->label('Позиция') ?> +
+
+
+
+ + +
+
+
Дополнительно
+
+
+
+
+ field($model, 'description')->textarea([ + 'rows' => 3, + 'maxlength' => 255, + 'placeholder' => 'Описание, примечания' + ])->label('Описание') ?> +
+
+
+
+ +
+ 'btn btn-success btn-lg']) ?> + 'btn btn-secondary btn-lg']) ?> +
+ + +
diff --git a/erp24/views/courier/create.php b/erp24/views/courier/create.php new file mode 100644 index 00000000..80b2bd2e --- /dev/null +++ b/erp24/views/courier/create.php @@ -0,0 +1,20 @@ +title = 'Добавить курьера'; +?> + +
+

+ 'btn btn-primary']) ?> + title) ?> +

+ +
+ render('_form', ['model' => $model]) ?> +
+
diff --git a/erp24/views/courier/index.php b/erp24/views/courier/index.php new file mode 100644 index 00000000..1ff84f4b --- /dev/null +++ b/erp24/views/courier/index.php @@ -0,0 +1,79 @@ +title = 'Курьеры'; +?> + +
+

title) ?>

+ +

+ 'btn btn-success']) ?> +

+ + session->hasFlash('success')): ?> +
session->getFlash('success') ?>
+ + + session->hasFlash('error')): ?> +
session->getFlash('error') ?>
+ + + $dataProvider, + 'filterModel' => $searchModel, + 'tableOptions' => ['class' => 'table table-hover'], + 'rowOptions' => function ($model) { + return $model->active == 0 ? ['class' => 'bg-danger'] : []; + }, + 'columns' => [ + [ + 'attribute' => 'id', + 'headerOptions' => ['width' => '60'], + ], + [ + 'attribute' => 'pass_user', + 'label' => 'Пароль', + ], + [ + 'attribute' => 'name', + 'label' => 'Имя Сотрудника', + 'format' => 'html', + 'value' => function ($model) { + $name = Html::encode($model->name); + $badge = $model->active == 0 + ? ' (не активный)' + : ''; + return Html::a("{$name}", ['update', 'id' => $model->id]) . $badge; + }, + ], + [ + 'class' => ActionColumn::class, + 'template' => '{view} {update} {delete}', + 'visibleButtons' => [ + 'delete' => function ($model) { + return $model->active == 0; + }, + ], + 'buttons' => [ + 'delete' => function ($url, $model) { + return Html::a('удалить', $url, [ + 'class' => 'btn btn-danger btn-sm', + 'data' => [ + 'confirm' => 'Вы уверены, что хотите удалить курьера?', + 'method' => 'post', + ], + ]); + }, + ], + ], + ], + ]) ?> +
diff --git a/erp24/views/courier/update.php b/erp24/views/courier/update.php new file mode 100644 index 00000000..5c2e50df --- /dev/null +++ b/erp24/views/courier/update.php @@ -0,0 +1,20 @@ +title = 'Редактирование курьера: ' . $model->name; +?> + +
+

+ 'btn btn-primary']) ?> + title) ?> +

+ +
+ render('_form', ['model' => $model]) ?> +
+
diff --git a/erp24/views/courier/view.php b/erp24/views/courier/view.php new file mode 100644 index 00000000..a5d24f6a --- /dev/null +++ b/erp24/views/courier/view.php @@ -0,0 +1,294 @@ +title = 'Курьер: ' . $model->name; +?> + +
+

+ 'btn btn-primary']) ?> + title) ?> +

+ + session->hasFlash('success')): ?> +
session->getFlash('success') ?>
+ + +

+ $model->id], ['class' => 'btn btn-warning']) ?> + active == 0): ?> + $model->id], [ + 'class' => 'btn btn-danger', + 'data' => [ + 'confirm' => 'Вы уверены, что хотите удалить курьера?', + 'method' => 'post', + ], + ]) ?> + +

+ + +
+
+
Основная информация
+
+
+ $model, + 'options' => ['class' => 'table table-striped detail-view mb-0'], + 'attributes' => [ + 'id', + [ + 'attribute' => 'name', + 'label' => 'Имя', + ], + [ + 'attribute' => 'name_full', + 'label' => 'Полное имя', + ], + [ + 'attribute' => 'login_user', + 'label' => 'Логин', + ], + [ + 'attribute' => 'pass_user', + 'label' => 'Пароль', + ], + [ + 'attribute' => 'mobile', + 'label' => 'Мобильный телефон', + ], + [ + 'attribute' => 'active', + 'label' => 'Статус', + 'value' => $model->active == 1 ? 'Активный' : 'Не активный', + ], + [ + 'attribute' => 'work_status', + 'label' => 'Статус работы', + 'value' => function ($model) { + $statuses = [1 => 'Работает', 4 => 'Уволен']; + return $statuses[$model->work_status] ?? '—'; + }, + ], + [ + 'attribute' => 'pol', + 'label' => 'Пол', + 'value' => function ($model) { + $genders = ['М' => 'Мужской', 'Ж' => 'Женский']; + return $genders[$model->pol] ?? '—'; + }, + ], + [ + 'attribute' => 'birthdate', + 'label' => 'Дата рождения', + 'format' => 'date', + ], + [ + 'attribute' => 'grazhdanstvo', + 'label' => 'Гражданство', + ], + ], + ]) ?> +
+
+ + +
+
+
Паспортные данные
+
+
+ $model, + 'options' => ['class' => 'table table-striped detail-view mb-0'], + 'attributes' => [ + [ + 'attribute' => 'passport_seriya', + 'label' => 'Серия паспорта', + ], + [ + 'attribute' => 'passport_nomer', + 'label' => 'Номер паспорта', + ], + [ + 'attribute' => 'data_passport', + 'label' => 'Дата выдачи', + 'format' => 'date', + ], + [ + 'attribute' => 'kem_vidan', + 'label' => 'Кем выдан', + ], + [ + 'attribute' => 'passport_kod_podrazdel', + 'label' => 'Код подразделения', + ], + [ + 'attribute' => 'passport_mesto_rozhdeniya', + 'label' => 'Место рождения', + ], + [ + 'attribute' => 'passport_srok_begin', + 'label' => 'Срок действия с', + 'format' => 'date', + ], + [ + 'attribute' => 'passport_end', + 'label' => 'Срок действия до', + 'format' => 'date', + ], + ], + ]) ?> +
+
+ + +
+
+
Адреса
+
+
+ $model, + 'options' => ['class' => 'table table-striped detail-view mb-0'], + 'attributes' => [ + [ + 'attribute' => 'adress', + 'label' => 'Адрес регистрации', + ], + [ + 'attribute' => 'adress_fakt', + 'label' => 'Фактический адрес', + ], + [ + 'attribute' => 'adress_prozhivaniya', + 'label' => 'Адрес проживания', + ], + [ + 'attribute' => 'mesto_r', + 'label' => 'Место рождения', + ], + [ + 'attribute' => 'adress_info', + 'label' => 'Доп. информация по адресу', + ], + ], + ]) ?> +
+
+ + +
+
+
Документы
+
+
+ $model, + 'options' => ['class' => 'table table-striped detail-view mb-0'], + 'attributes' => [ + [ + 'attribute' => 'inn', + 'label' => 'ИНН', + ], + [ + 'attribute' => 'snils', + 'label' => 'СНИЛС', + ], + ], + ]) ?> +
+
+ + +
+
+
Трудоустройство
+
+
+ $model, + 'options' => ['class' => 'table table-striped detail-view mb-0'], + 'attributes' => [ + [ + 'attribute' => 'data_priem', + 'label' => 'Дата приёма', + 'format' => 'date', + ], + [ + 'attribute' => 'data_uval', + 'label' => 'Дата увольнения', + 'format' => 'date', + ], + [ + 'attribute' => 'tabel_number', + 'label' => 'Табельный номер', + ], + [ + 'attribute' => 'vid_zanatosti', + 'label' => 'Вид занятости', + ], + [ + 'attribute' => 'tip_ustroen', + 'label' => 'Тип устройства', + 'value' => function ($model) { + $types = ['ТК' => 'Трудовой кодекс', 'ГПХ' => 'ГПХ', 'СЗ' => 'Самозанятый']; + return $types[$model->tip_ustroen] ?? $model->tip_ustroen ?: '—'; + }, + ], + [ + 'attribute' => 'kol_deti', + 'label' => 'Количество детей', + ], + [ + 'attribute' => 'summa_oklad', + 'label' => 'Оклад', + ], + [ + 'attribute' => 'summa_oklad_nalog', + 'label' => 'Оклад (налог)', + ], + [ + 'attribute' => 'avans_percent', + 'label' => 'Процент аванса', + 'value' => $model->avans_percent ? $model->avans_percent . '%' : '—', + ], + [ + 'attribute' => 'sale_percent', + 'label' => 'Процент продажи', + 'value' => $model->sale_percent ? $model->sale_percent . '%' : '—', + ], + [ + 'attribute' => 'posit', + 'label' => 'Позиция', + ], + ], + ]) ?> +
+
+ + +
+
+
Дополнительно
+
+
+ $model, + 'options' => ['class' => 'table table-striped detail-view mb-0'], + 'attributes' => [ + [ + 'attribute' => 'description', + 'label' => 'Описание', + ], + ], + ]) ?> +
+
+