Compare commits

..

11 Commits

Author SHA1 Message Date
vasya cda29ccaf9 входной typescript для фронта с меню приложений magic 2026-03-21 20:51:03 +03:00
vasya 738c4535dc добавляю модель работы с ролями по приложенями magic 2.0 и таблицы БД, где хранятся роли по приложеням magic 2.0. Сейчас роли распределятся по логинам, необходимо переписать под выдачу ролей по группам доступа. Я не успел переделать логику 2026-03-21 20:37:16 +03:00
vasya 52920040a1 добавляю css стили страницы с меню приложений Magic 2.0 2026-03-21 20:03:39 +03:00
vasya 2097d3487c добавляю react компонент меню приложений magic 2.0 2026-03-21 20:02:25 +03:00
vasya a5151145ec добавляю blade шаблон для отрисовки в нем меню magic 2.0 2026-03-21 19:56:39 +03:00
vasya c541c09277 добавляю модель работы со всеми приложениями magic 2.0 и таблицы БД, где хранятся приложения magic 2.0 2026-03-21 19:51:37 +03:00
vasya 123fc969f0 добавляю модель работы с избраннымми приложениями пользователя и таблицы БД, где хранятся пользовательские избранные приложения 2026-03-21 19:48:37 +03:00
vasya 2aedec677e добавляю контроллер работы с меню приложений 2026-03-21 19:45:52 +03:00
vasya 2baec23c1e добавляю api ендпоинты для работы с меню 2026-03-14 20:38:54 +03:00
vasya 659bcb88f6 добавляю пустой файл с api ендпоинтами 2026-03-14 20:37:08 +03:00
vasya 362eeab9ce добавляю роут вызова меню с приложениями 2026-03-14 20:08:21 +03:00
58 changed files with 708 additions and 2758 deletions
@@ -1,90 +0,0 @@
<?php
namespace Modules\Taxi\App\Console;
use Error;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use App\Console\Commands\BaseScheduleCommand;
use App\Mail\BaseMailerObj;
use Illuminate\Support\Facades\Mail;
use App\Mail\Mailer;
use Modules\Taxi\App\Models\TaxiMain;
use DateTime;
class SendOrdersToTaxiCommand extends BaseScheduleCommand
{
/**
* The name and signature of the console command.
*/
public $signature = 'taxi:send';
/**
* The console command description.
*/
public $description = 'Отправка заказов такси на сегодня агрегаторам';
/**
* Create a new command instance.
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*/
public function handle()
{
$this->executeCommand(function () {
$todayOrders = TaxiMain::where('cancel_rqst', 0)
->where('taxi_date', (new DateTime())->format('Y-m-d'))
->get()
->toArray();
if (!$todayOrders) {
Mail::send(new Mailer(
new BaseMailerObj(
)
));
echo '<pre>'; var_dump('da'); echo'</pre>';
} else {
echo '<pre>'; var_dump('net'); echo'</pre>';
}
throw new Error('Ошипка!');
}, 'еще инва');
// try {
// throw new Error('Ошипка!');
// } catch (\Throwable $th) {
// \Log::channel('schedule_err')->error($th->getFile() . ": " . $th->getMessage() . " Line: " . $th->getLine());
// }
//$this->info('Daily report generated at ' . now());
return 0;
}
/**
* Get the console command arguments.
*/
protected function getArguments(): array
{
return [
['example', InputArgument::REQUIRED, 'An example argument.'],
];
}
/**
* Get the console command options.
*/
protected function getOptions(): array
{
return [
['example', null, InputOption::VALUE_OPTIONAL, 'An example option.', null],
];
}
}
-65
View File
@@ -1,65 +0,0 @@
<?php
namespace Modules\Taxi\App\DTO;
use Modules\Taxi\App\Http\Requests\TaxiOrderRequest;
/**
* Класс для формирования DTO с данными по заказу на такси
*/
class TaxiOrderDTO
{
public function __construct(
public readonly int | null $id,
public readonly string | null $emp_login,
public readonly string $emp_phone,
public readonly string $taxi_date,
public readonly string $taxi_time,
public readonly string $taxi_address_from,
public readonly string $taxi_address_to,
public readonly int $cancel_rqst = 0,
) {}
/**
* Метод формирования DTO с данными по заказу такси
*
* По сути, простой сеттер, позволяющий создать для обрабатываемого объекта данных экземпляр класса для более удобного взаимодействия с ним, более стандартизированного и более надежного (так как все параметры прописаны в одном месте)
* Использование self позволяет нам не зависеть от названия текущего класса, если мы захотим его переименовать
*
* @param TaxiOrderRequest $rqst запрос с валидными данными по заказу, пришедшими с фронта
* @param string $rqstMethod метод обрабатываемого запроса. Обновление или создание, например
* @return self
*/
public static function setTaxiOrderDTO(TaxiOrderRequest $rqst, string $rqstMethod): self
{
$validated = $rqst->validated();
switch ($rqstMethod) {
case 'PATCH':
return new self(
id: $validated['id'],
//При обновлении запроса логин пользователя должен оставаться неизменным
emp_login: $rqst->emp_login,
emp_phone: $validated['emp_phone'],
taxi_date: $validated['taxi_date'],
taxi_time: $validated['taxi_time'],
taxi_address_from: $validated['taxi_address_from'],
taxi_address_to: $validated['taxi_address_to'],
cancel_rqst: $validated['cancel_rqst'] ?? null,
);
break;
default:
return new self(
id: $validated['id'] ?? null,
emp_login: $validated['emp_login'],
emp_phone: $validated['emp_phone'],
taxi_date: $validated['taxi_date'],
taxi_time: $validated['taxi_time'],
taxi_address_from: $validated['taxi_address_from'],
taxi_address_to: $validated['taxi_address_to'],
cancel_rqst: $validated['cancel_rqst'] ?? null,
);
break;
}
}
}
@@ -1,351 +0,0 @@
<?php
namespace Modules\Taxi\App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Modules\Taxi\App\Models as TaxiModels;
use App\Models\OldMagicModels;
#Гаврилов
//Rule и Validatir НЕ НУЖЕН? ЕСЛИ МЫ ВАЛИДИРУЕМ В ОТДЕЛЬОМ FORM_REQUEST
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Validator;
use Modules\Taxi\App\Http\Requests\TaxiOrderRequest;
#Гаврилов
//КЛАСС НИЖЕ НЕ НУЖЕН, ТАК КАК ИСПОЛЬЗУЕМ СЕРВИС?
use Illuminate\Support\Facades\Mail;
use Modules\Taxi\App\Services\TaxiOrderService;
use Modules\Taxi\App\Services\TaxiMailerService;
use Modules\Taxi\App\DTO\TaxiOrderDTO;
use Modules\Taxi\App\Jobs\SendMailNewOrder;
//use Illuminate\Support\Facades\Config;
use App\Mail\Mailer;
use DateTime;
use Error;
use Exception;
use Illuminate\Support\Facades\DB;
use Modules\Taxi\App\Mail\NewOrderMail;
use Illuminate\Support\Facades\Redis;
use App\Facades\RedisNotifications;
class TaxiController extends Controller
{
/**
* Конструктор
*
* @param TaxiMailerService $sendMailService сервис отправки писем
* @param TaxiOrderService $orderService сервис работы с заказами
*/
public function __construct(protected TaxiMailerService $sendMailService, protected TaxiOrderService $orderService) {}
/**
* Получаем информацию по существующему заказу
*
* @param integer $orderId идентификатор заказа
*/
public function getOrderById(int $orderId)
{
$taxiOrder = TaxiModels\TaxiMain::where('id', $orderId)->first();
return $taxiOrder;
}
/**
* Получение адресов офисов для подстановки в запросы такси
*
* @return array
*/
public function getOfficeAddress()
{
$taxiAddress = TaxiModels\OfficeAddress::all();
return $taxiAddress;
}
/**
* Получение всех активных заказов
*
* @return array
*/
public function getActiveOrders()
{
$todaydate = new DateTime();
$todaydate->setTime(00, 00, 00);
$todaydate = $todaydate->format('Y-m-d');
$orders = TaxiModels\TaxiMain::where('taxi_date', '>=', $todayFdate)
->get();
return $orders;
}
/**
* Метод получения заказов по условиям фильтра
*
* @param Request $rqst запрос с параметрами поиска
* @return string
*/
public function getFilterOrders(Request $rqst)
{
$rqstCond = [];
foreach ($rqst->all() as $fieldName => $fieldVal) {
if (!$fieldVal) continue;
$rqstCond[] = [$fieldName, '=', $fieldVal];
}
$filterRqst = TaxiModels\TaxiMain::where($rqstCond)->get();
return json_encode($filterRqst->toArray());
}
//ГАВРИЛОВ используй транзакцию, а то создается запрос, не отправляется письмо и сообщение об ошибке
/**
* Show the form for creating a new resource.
*/
public function createTaxiOrder(TaxiOrderRequest $rqst)
{
$validateData = TaxiOrderDTO::setTaxiOrderDTO($rqst, $rqst->method());
//Пытаемся найти уже существующий запрос на этот же логин и эту же дату. Если запрос найден, возвращаем ошибку.
$searchExistsOrder = TaxiModels\TaxiMain::where([
['emp_login', '=', $validateData->emp_login],
['taxi_date', '=', $validateData->taxi_date]
])->take(1)->get();
if (count($searchExistsOrder)) {
return response()->json(['message' => "Ошибка! Заявка для сотрудника $validateData->emp_login на дату $validateData->taxi_date уже существует"], 400);
}
// $createRqstMsg;
try {
$newOrderId = (new TaxiModels\TaxiMain())->createOrder($validateData);
//$newOrderId = $this->orderService->createOrder($validateData);
$createRqstMsg = "Заявка успешно создана. Номер $newOrderId";
#Гаврилов
//ПРОВЕРЬ, ЧТОБЫ ПИСЬМО НЕ ОТПРАВЛЯЛОСЬ, ЕСЛИ ВОЗНИКАЕТ ОШИБКА НА ЭТАПЕ СОЗДАНИЯ ЗАЯВКИ
//Один метод отправки письма для всех методов работы с заказом: создание, редактирование, отмена
$this->sendMailService->sendMail(new NewOrderMail($validateData, $newOrderId));
// SendMailNewOrder::dispatch(json_decode(json_encode($validateData), true), $newOrderId)->onQueue('emails')->delay(10);
// Mail::send(new Mailer(
// [$validateData->emp_login],
// 'Создание заказа на такси',
// "Для вас создана заявка на заказ такси на дату <b>{$validateData->taxi_date}</b> и время <b>{$validateData->taxi_time}</b>. Номер заказа <b>{$newOrderId}</b>",
// #Гаврилов
// //ЗДЕСЬ НЕПОНЯТНО ОТКУДА ВЗЯТЬ ЗНАЧЕНИЕ. ОБРАТИТЬСЯ К ЛОКАЛЬНО МОДУЛЬНОМУ ENV?
// config('taxi.name_ru'),
// ));
return response()->json(['message' => "Заявка успешно создана. Номер заявки $newOrderId"]);
} catch (\Throwable $t) {
#Гаврилов
//ОТПРАВКА СООБЩЕНИЯ С ОШИБКОЙ. ЗАТЕСТИ и отправляй response
//ОШИБКИ $T->GETmESSAGE НУЖНО ЛОГИРОВАТЬ, НО ПОЛЬЗОВАТЕЛЯМ ВЫВОДИТЬ ПРОСТО СООБЩЕНИЕ "оШИБКА", ИНАЧЕ ОНИ ВИДЯТ ТЕХНиЧЕСКИЙ ТЕКСТ
//СНАЧАЛА ПЕРЕХВАТЫЙ EXCEPTION, ПОТОМ УЖЕ THOWBLE. ТЕКСТ ИЗ EXCEPTION ВЫВОДИ ПОЛЬЗОВАТЕЛЮ - ТАМ КАСТОМНЫЕ ТЕКСТ ОШИБКИ, А ТЕКСТ ИЗ THORWBLE ЛОГИРУЙ ОТДЕЛЬНО, ТАК КАК ТАМ ТЕХНИЧЕСКОЕ ОПИСАНИЕ ОШИБКИ
echo '<pre>';
var_dump($t->getMessage());
echo '</pre>';
echo '<pre>';
var_dump($t->getTrace());
echo '</pre>';
$createRqstMsg = "Ошибка!" . $t->getMessage() . ". Заявка не создана";
return response()->json(['message' => $createRqstMsg], 400);
}
//ГАВРИЛОВ
//ДОБАВЬ ФОРМИРОВАНИЕ ОТВЕТА ЧЕРЕЗ ApiResponder, а не через response()
}
#Гаврилов
//ИСПОЛЬЗУЙ TaxiOrderRequest ПРИ ОБРАБОТКЕ ЗАЯВОК НА ТАКСИ
/**
* Метод отмены запроса
*
* @param integer $rqstId идентификатор запроса
* @return string
*/
public function cancelRqst(int $rqstId): string
{
#Гаврилов
//ПОПРОБУЙ ОТПРАВИТЬ ЗАПРОС К API ЕНДПОИНТУ (ОТМЕНА ЗАПРОСА НА ТАКСИ) ЧЕРЕЗ POSTMAN С ПЕРЕДАЧЕЙ ЗАГОЛОВКА АУТЕНТИФИКАЦИИ С SANCTUM ТОКЕНОМ. ДОЛЖЕН ПРОПУСТИТЬ, А БЕЗ ЗАГОЛОВКА НЕ ДОЛЖЕН
$rqstData = $this->getOrderById($rqstId);
try {
#Гаврилов
//ПЕРЕНЕСИ ЭТУ ПРОВЕРКУ В РОУТИНГ? FINDORFAIL С РЕДИРЕКТОМ И ОШИБКОЙ
if (!$rqstData) {
throw new \Exception("Запрос $rqstId не существует");
}
if (!$this->checkRqstDate($rqstData->taxi_date)) {
throw new \Exception("Дата запроса $rqstId уже прошла");
}
} catch (\Throwable $t) {
return response()->json([
'errorMsg' => "Ошибка: $t->getMessage(). Заявка не отменена",
], 400);
}
$rqstData['cancel_rqst'] = 1;
$rqstData->save();
$this->sendMailService->sendCancelOrderMail($validateData, $newOrderId);
// Mail::send(new Mailer(
// [$rqstData->emp_login],
// 'Отмена заказа на такси',
// "Заказ номер $rqstId на дату $rqstData->taxi_date и время $rqstData->taxi_time отменен",
// $this->appName));
return redirect('/taxi/home')->with([
'notification' => "Запрос $rqstId отменен",
'notification_err' => false
]);
}
/**
* Проверяем находится ли дата запроса на такси в диапазоне сегодняшнего или завтрашнего дня.
*
* @param string $rqstDate дата запроса на такси
* @return boolean
*/
private function checkRqstDate($rqstDate): bool
{
$todaydate = new DateTime();
$todaydate->setTime(00, 00, 00);
return new DateTime($rqstDate) >= $todaydate;
}
/**
* Получаем временные промежутки
*
* @return array
*/
public function getTimePeriods()
{
$empAllData = TaxiModels\TaxiTimePeriod::select('time_period', 'is_morning_time')
->where('archive', 0)
->get();
return $empAllData;
}
/**
* Метод получает информацию по всем активным пользователям
*
* @return array
*/
public function getActiveUsersInfo()
{
$empAllData = OldMagicModels\CcEmp::whereIn('emp_area', ['Курск', 'Пенза'])
->whereNotIn('emp_state', ['Уволен', 'Декрет', 'Перевод на позицию', 'Перевод в ГРАН'])
->get();
foreach ($empAllData as &$empData) {
$empData['full_name'] = $empData['emp_surname'] . " " . $empData['emp_name'] . " " . $empData['emp_surname'];
$empData['extension_number'] = $empData['emp_phone'];
$empData['emp_phone'] = $empData['emp_mob_phone'];
}
return $empAllData;
}
/**
* Метод получает информацию по конкретному пользователю
*
* @return array
*/
public function getUserInfoByLogin(string $userLogin)
{
$empLoginData = OldMagicModels\CcEmp::where('emp_login', $userLogin)
->get();
// dd($empLoginData);
$empLoginData[0]['full_name'] = $empLoginData[0]['emp_surname'] . " " . $empLoginData[0]['emp_name'] . " " . $empLoginData[0]['emp_surname'];
$empLoginData[0]['extension_number'] = $empLoginData[0]['emp_phone'];
$empLoginData[0]['emp_phone'] = $empLoginData[0]['emp_mob_phone'];
return $empLoginData;
}
#Гаврилов
//НЕ ЗАБУДЬ УДАЛИТЬ
public function testRedisMethod()
{
RedisNotifications::module('taxi')
->notifications(['текст1', 'tеуые2'])
->types(['error', 'success'])
->put();
}
/**
* Обновление заявки на такси
*
* @param TaxiOrderRequest $rqst информация по измененному запросу
* @return response
*/
public function updateTaxiOrder(TaxiOrderRequest $rqst)
{
try {
DB::beginTransaction();
$orderData = TaxiModels\TaxiMain::lockForUpdate()->find($rqst->id);
$oldOrderData = TaxiModels\TaxiMain::find($rqst->id);
if (!$orderData) {
throw new Exception("Заказ номер " . $rqst->id . " не найден");
//return response()->json(['message' => 'Заказ номер ' . $rqst->id . ' не найден'], 400);
}
$editOrderData = TaxiOrderDTO::setTaxiOrderDTO($rqst, $rqst->method());
#Гаврилов
//НА ДОМАШНЕЙ СТРАНИЦЕ ПОКАЗЫВАТЬ ТОЛЬКО ТЕ ЗАПРОСЫ, ДАТА КОТОРЫХ СЕГОДНЯ ИЛИ ЗАВТРА
$dateInterval = (new DateTime())->diff(new DateTime($orderData->taxi_date));
if ($dateInterval->format('%R') == '-' && $dateInterval->format('%a') >= 1) {
throw new Exception("Нельзя менять заявки, дата которых уже прошла");
// return response()->json(['message' => 'Нельзя менять заявки, дата которых уже прошла'], 400);
}
foreach ($editOrderData as $fieldName => $fieldVal) {
if (isset($orderData->$fieldName)) {
$orderData[$fieldName] = $fieldVal;
}
}
$orderDataDiff = $orderData->getDirty();
if (empty($orderDataDiff)) {
throw new Exception("Вы не внесли никаких изменений");
//return response()->json(['message' => 'Вы не внесли никаких изменений'], 400);
}
$orderData->save();
$this->sendMailService->sendEditOrderMail($oldOrderData->toArray(), $orderDataDiff, $orderData['id']);
DB::commit();
return response()->json(['message' => 'Заявка успешно изменена'], 200);
} catch (\Exception $e) {
DB::rollBack();
return response()->json(['message' => "Ошибка." . $e->getMessage() . ". Не удалось обновить заявку"], 400);
}
//ГАВРИЛОВ. ДОБАВЬ БЛОКИРОВКУ В МОМЕНТ ПОЛУЧЕНИЯ ДАННЫХ ПО РЕДАКТИРУЕМОМУ ЗАПРОСУ И ДО МОМЕНТА ЕГО ОБНОВЛЕНИЯ. ЛИБО ЧЕРЕЗ УСТАНОВКУ УРОВНЯ ИЗОЛЯЦИИ
//ГАВРИЛОВ. ДОБАВЬ В DTO НЕ ОБНУЛЕНИЕ НЕНУЖНЫХД ДАННЫХ, А ПРИНУДИТЕЬНАЯ ИХ УСТАНОВКА. НАПРИМЕР ПРИ ОБНОВЛЕНИИ ЗАПРОСА НА ТАКСИ, УСТАНАВЛИВАТЬ EMP_LOGIN = ПЕРЕДАННОМУ ЗНАЧЕНИЮ, ВЗЯТОМУ ИЗ СУЩЕСТВУЮЩЕГО ЗАПРОСА.
//Обновляем текущую модель, добавляя туда измененные значения, затем проверяем есть ли измененные значения через getDirty()
//ГАВРИЛОВ. убрать поле Запрос актуален? и при отмене заявки убирать ее из таблицы Активные заявки?
//гаврилов. СДЕЛАТЬ ВОЗМОЖНОСТЬ ВЫГРУЗИТЬ В ОТЧЕТНОСТЬ ВСЕ ИМЕЮЩИЕСЯ ЗАЯВКИ В CSV ФАЙЛ.
}
}
@@ -1,54 +0,0 @@
<?php
namespace Modules\Taxi\App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
/**
* Блокировщик работы с заявками на такси в случае слишком ранееЙ, слишком поздней работы с ресурсом
*
* Решил блочить рабоут приложения в части работы с заявками в очень ранее или очень позднее время. Это связано с нежеланием валидировать при создании или редактировании заявки передаваемый временной промежуток. Например, если текущее время 22:15, а заявка созается на 22:00.
*/
class CheckTimeRqstAvailability
{
public function handle(Request $request, Closure $next)
{
#Гаврилов
//ПОМЕНЯЙ НА 21 НА РЕЛИЗЕ
$maxRqstTime = 23;
$minRqstTime = 7;
$timezone = new \DateTimeZone('Europe/Moscow');
$timeNow = (new \DateTime())->setTimezone($timezone);
$timeMax = (new \DateTime())->setTimezone($timezone);
$timeMin = (new \DateTime())->setTimezone($timezone);
$timeMax->setTime($maxRqstTime, 00, 00);
$timeMin->setTime($minRqstTime, 00, 00);
#Гаврилов
//ПОМЕНЯЙ (ПОКА ТАК ДЛЯ ТЕСТИРОВАНИЯ)
if (!($timeNow > $timeMin && $timeNow < $timeMax)) {
//if (!($timeNow > $timeMin && $timeNow > $timeMax)) {
$errMsg = "Работа с заявками на такси заблокирована с $maxRqstTime:00 по 0$minRqstTime:00";
#Гаврилов
//ПОДУМАТЬ КАКОЙ КОД ЛУЧШЕ ВЕРНУТЬ
//Метод expectsJson позволяет определить вызывается ли api роут через ajax/fetch, либо web route.
if ($request->expectsJson()) {
#Гаврилов
//ЗАМЕНИТЬ НА ОТВЕТ ЧЕРЕЗ APIRESPONDER. ТАМ ЛУЧШЕ РЕАЛИЗОВАТЬ ВСЮ КАСТОМНУЮ ЛОГИКУ ГЕНЕРАЦИИ ОТВЕТОВ НА API ЗАПРОСЫ С ФРОНТА
return response()->json([
'errorMsg' => $errMsg
], 400);
} else {
#Гаврилов
//ПРОВЕРЬ КОГДА РЕАЛИЗУЕШЬ ОПОВЕЩЕНИЯ
return redirect('/taxi/home')->with([
'notification' => $errMsg,
'notification_err' => true
]);
}
} else {
return $next($request);
}
}
}
@@ -1,110 +0,0 @@
<?php
namespace Modules\Taxi\App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use App\Models\OldMagicModels;
use Illuminate\Validation\Rule;
use DateTime;
//Не помню точно, но это считается частью логики ларавель - не моя задумка. Некая сущность, которая через себя пропускает данные запроса, валидируя их. Вроде даже вызывается автоматически при работе с формой. Вызывается сразу в начале передачи данных, поэтому гарантирует, что по всему флоу работы с запросом объект с данными уже валиден для нас
class TaxiOrderRequest extends FormRequest
{
/**
* Действия перед валидацией.
*
* @return void
*/
protected function prepareForValidation()
{
//echo '<pre>'; var_dump($this->all()); echo'</pre>';
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
//Получаем список активных пользователей
$empActiveLogins = OldMagicModels\CcEmp::select('emp_login')
->whereNotIn('emp_state', ['Уволен', 'Декрет'])
->get()
->toArray();
//Правила валидации
$validationRules = [
'id' => [
'int'
],
'taxi_date' => [
'date_format:Y-m-d',
//Заказ можно создать только на сегодняшний или завтрашний день
Rule::in([
(new DateTime())->format('Y-m-d'),
(new DateTime())->modify('+1 day')->format('Y-m-d')
]),
],
'taxi_time' => [
'required',
'string',
'regex:/^[0-9]{2}:[0-9]{2}$/', //время в формате 00:00
],
'cancel_rqst' => 'boolean',
'emp_phone' => [
'required',
'int',
'regex:/^(7|8)?[0-9]{10}$/'
],
'emp_login' => [
$validationRules['emp_login'] = [
'required',
'string',
'regex:/^[a-z\-]+[0-9]*$/',
//Логины ищем среди активных логинов на текущий момент
Rule::in(
array_map(fn($el) => $el['emp_login'], $empActiveLogins)
)
],
],
'taxi_address_from' => [
'required',
'string',
'max: 500'
],
'taxi_address_to' => [
'required',
'string',
'max: 500'
]
];
//Во время создания запроса поле ID не валидируется
if ($this->isMethod('POST')){
$validationRules['id'] = [
'prohibited'
];
}
return $validationRules;
}
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Переопределение сообщений об ошибках валидации
*
* @return array
*/
public function messages(): array
{
return [
'taxi_date.in' => 'Заявку на такси можно завести только на сегодняшний или завтрашний день'
];
}
}
-40
View File
@@ -1,40 +0,0 @@
<?php
namespace Modules\Taxi\App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Mail;
use App\Mail\Mailer;
use App\Mail\BaseMailerObj;
use App\Job\BaseJob;
use Error;
/**
* Джоба размещения задачи на отправку писем по такси
*/
class TaxiMailJob extends BaseJob
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*/
public function __construct(public BaseMailerObj $jobData)
{
//
}
/**
* Execute the job.
*/
public function handle(): void
{
Mail::send(new Mailer(
$this->jobData
));
}
}
-32
View File
@@ -1,32 +0,0 @@
<?php
namespace Modules\Taxi\App\Mail;
use App\Mail\BaseMailerObj;
use Modules\Taxi\App\DTO\TaxiOrderDTO;
/**
* Базовый класс отправки письма по заказу такси. Объявляет обязательный к реализации метод формирования данных для отправки.
*/
abstract class BaseTaxiMail
{
/**
* Undocumented function
*
* @param TaxiOrderDTO $orderData
* @param integer|null $orderId идентификатор заказа такси
*/
public function __construct(
public TaxiOrderDTO $orderData,
public ?int $orderId
){
}
/**
* Метод формирует объект с данными письма для передачи в джобу
*
* @return BaseMailerObj базовый класс объекта параметров письма для мейлера
*/
abstract public function prepareDataForMail(): BaseMailerObj;
}
-39
View File
@@ -1,39 +0,0 @@
<?php
namespace Modules\Taxi\App\Mail;
use Modules\Taxi\App\DTO\TaxiOrderDTO;
use App\Mail\BaseMailerObj;
/**
* Класс формирования объекта с данными для передачи в джобу отправки письма. Наследует базовый класс отправки письма по такси, где объвляется обязательный метод
*/
class NewOrderMail extends BaseTaxiMail
{
public function __construct(
public TaxiOrderDTO $orderData,
public ?int $orderId)
{
}
/**
* Метод подготавливает данные письма для передачи в джобу
*
* @return BaseMailerObj
*/
public function prepareDataForMail(): BaseMailerObj
{
return new BaseMailerObj(
[$this->orderData->emp_login],
"Создана заявка на такси",
"Для вас создана заявка на такси № {$this->orderId}:
<ul>
<li><b>Дата:</b> {$this->orderData->taxi_date}</li>
<li><b>Время:</b> {$this->orderData->taxi_time}</li>
<li><b>Откуда:</b> {$this->orderData->taxi_address_from}</li>
<li><b>Куда:</b> {$this->orderData->taxi_address_to}</li>
</ul>",
config('taxi.name_ru'),
);
}
}
-13
View File
@@ -1,13 +0,0 @@
<?php
namespace Modules\Taxi\App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Modules\Taxi\Database\factories\OfficeAddressFactory;
class OfficeAddress extends Model
{
protected $table = 'office_address';
protected $guarded = [];
}
-104
View File
@@ -1,104 +0,0 @@
<?php
namespace Modules\Taxi\App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Modules\Taxi\Database\factories\TaxiMainFactory;
//use Spatie\Activitylog\Traits\LogsActivity;
// use Spatie\Activitylog\LogOptions;
use App\Traits\LogsActivity_custom;
use Modules\Taxi\App\DTO\TaxiOrderDTO;
use App\Enums\LogBusinessEvents;
class TaxiMain extends Model
{
//use LogsActivity;
use LogsActivity_custom;
protected $table = 'taxi_main';
protected $guarded = [];
// protected $activityCustomProperties = [
// 'emp_login' => 'dgavrilov',
// ];
// public function getActivitylogOptions(): LogOptions
// {
// return LogOptions::defaults()
// ->logAll()
// ->logOnlyDirty()
// ->useLogName('Taxi')
// ->logExcept(['created_at', 'updated_at']);
// }
public function __construct()
{
parent::__construct();
//В трейте LogsActivity_custom это свойство отвечает за название приложения, события в котором логируются. Оно записывается в поле activity_log.log_name
$this->logActivity_custom__name = 'Taxi';
$this->addCustomLogProperties(['custom__test' => 'test']);
}
//ГАВРИЛОВ. сделай метод не статическим
public static function getFieldsTitle()
{
return [
'id' => 'Номер запроса',
'emp_login' => 'Логин сотрудника',
'emp_phone' => 'Телефон сотрудника',
'taxi_date' => 'Дата заказа',
'taxi_time' => 'Время заказа',
'taxi_address_from' => 'Адрес откуда',
'taxi_address_to' => 'Адрес куда',
'cancel_rqst' => 'Запрос отменен',
];
}
public function createOrder(TaxiOrderDTO $orderData): string
{
//ГАВРИЛОВ. проверь будет ли возвращаться ошибка без try catch
// try {
$this->logBusinessEvent(LogBusinessEvents::Create);
$newOrder = $this->create(
[
'emp_login' => $orderData->emp_login,
'taxi_date' => $orderData->taxi_date,
'emp_phone' => $orderData->emp_phone,
'taxi_time' => $orderData->taxi_time,
'taxi_address_from' => $orderData->taxi_address_from,
'taxi_address_to' => $orderData->taxi_address_to
]
);
return $newOrder->id;
// } catch (\Exception $e) {
// return $e->getMessage();
// }
}
// /**
// * @return string Метод нужен для переопределения параметра трейта. Простое переопределение вызывает конфликт
// */
//protected $logActivity_custom__name = 'Taxi';
// public function getLogName(): string
// {
// return 'Taxi';
// }
//protected $logActivity_custom__name;
// public function getActivitylogOptions(): LogOptions
// {
// //Через метод default() получаем параметры класса логирования по умолчанию. Ниже можем их переопределять в зависимости от специфики работы с моделью
// $logOptions = LogOptions::defaults()
// ->logOnly(['*']); //Устанавиваем список логируемых данных (полей модели), либо, как в этом случае, логируем все поля
// //->logExcept(['created_at', 'updated_at']) //Не логируем поля изменения и создания записи в ЛЮБОМ случае
// //->logOnlyDirty(); //Логируем только те поля, которые были изменены
// $logOptions->logName = 'Taxi'; //Устанавливаем значение для имени приложения
// return $logOptions;
// }
}
@@ -1,14 +0,0 @@
<?php
namespace Modules\Taxi\App\Models;
use Illuminate\Database\Eloquent\Model;
class TaxiTimePeriod extends Model
{
/**
* The attributes that are mass assignable.
*/
protected $guarded = [];
}
@@ -1,68 +0,0 @@
<?php
namespace Modules\Taxi\App\Providers;
use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
class RouteServiceProvider extends ServiceProvider
{
/**
* The module namespace to assume when generating URLs to actions.
*/
protected string $moduleNamespace = 'Modules\Taxi\App\Http\Controllers';
protected $middleware_checkTimeRqstAvailable = [Modules\Taxi\Http\Middleware\CheckTimeRqstAvailability::class];
/**
* Called before routes are registered.
*
* Register any model bindings or pattern based filters.
*/
public function boot(): void
{
parent::boot();
}
/**
* Define the routes for the application.
*/
public function map(): void
{
$this->mapApiRoutes();
$this->mapWebRoutes();
}
/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*/
protected function mapWebRoutes(): void
{
Route::middleware('web')
->namespace($this->moduleNamespace)
->group(module_path('Taxi', '/routes/web.php'));
Route::prefix('taxi')
->middleware('web')
->group(base_path('Modules/Taxi/routes/web.php'));
}
/**
* Define the "api" routes for the application.
*
* These routes are typically stateless.
*/
protected function mapApiRoutes(): void
{
Route::prefix('api')
->middleware('api')
->namespace($this->moduleNamespace)
->group(module_path('Taxi', '/routes/api.php'));
Route::prefix('taxi')
->middleware('api')
->group(base_path('Modules/Taxi/routes/api.php'));
}
}
@@ -1,28 +0,0 @@
<?php
namespace Modules\Taxi\App\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\Taxi\App\Services\TaxiMailerService;
//Регистрация сервиса отправки писем по работе с Такси
class TaxiMailerProvider extends ServiceProvider
{
/**
* Register the service provider.
*/
public function register(): void
{
$this->app->bind(TaxiMailerService::class, function ($app) {
return new TaxiMailerService();
});
}
/**
* Get the services provided by the provider.
*/
public function provides(): array
{
return [];
}
}
@@ -1,27 +0,0 @@
<?php
namespace Modules\Taxi\App\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\Taxi\App\Services\TaxiOrderService;
class TaxiOrderProvider extends ServiceProvider
{
/**
* Register the service provider.
*/
public function register(): void
{
$this->app->bind(TaxiOrderService::class, function($app) {
return new TaxiOrderService();
});
}
/**
* Get the services provided by the provider.
*/
public function provides(): array
{
return [];
}
}
@@ -1,31 +0,0 @@
<?php
namespace Modules\Taxi\App\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\Taxi\App\Console\SendOrdersToTaxiCommand;
//Не вполне помню зачем этот провайдер, для какого сервиса. Судя по названию, для работы с расписанием операций с приложением. Например, отправка итогового письма с заявками на такси или чистка чего-то
class TaxiScheduleProvider extends ServiceProvider
{
/**
* Register the service provider.
*/
public function register(): void
{
// Регистрируем команду в системе
if ($this->app->runningInConsole()) {
$this->commands([
SendOrdersToTaxiCommand::class, // ✅ Теперь Laravel знает о команде
]);
}
}
/**
* Get the services provided by the provider.
*/
public function provides(): array
{
return [];
}
}
@@ -1,115 +0,0 @@
<?php
namespace Modules\Taxi\App\Providers;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
//Подозреваю, что это какой-то core провайдер для модуля
class TaxiServiceProvider extends ServiceProvider
{
protected string $moduleName = 'Taxi';
protected string $moduleNameLower = 'taxi';
/**
* Boot the application events.
*/
public function boot(): void
{
$this->registerCommands();
$this->registerCommandSchedules();
$this->registerTranslations();
$this->registerConfig();
$this->registerViews();
$this->loadMigrationsFrom(module_path($this->moduleName, 'Database/migrations'));
}
/**
* Register the service provider.
*/
public function register(): void
{
$this->app->register(RouteServiceProvider::class);
}
/**
* Register commands in the format of Command::class
*/
protected function registerCommands(): void
{
// $this->commands([]);
}
/**
* Register command Schedules.
*/
protected function registerCommandSchedules(): void
{
// $this->app->booted(function () {
// $schedule = $this->app->make(Schedule::class);
// $schedule->command('inspire')->hourly();
// });
}
/**
* Register translations.
*/
public function registerTranslations(): void
{
$langPath = resource_path('lang/modules/'.$this->moduleNameLower);
if (is_dir($langPath)) {
$this->loadTranslationsFrom($langPath, $this->moduleNameLower);
$this->loadJsonTranslationsFrom($langPath);
} else {
$this->loadTranslationsFrom(module_path($this->moduleName, 'lang'), $this->moduleNameLower);
$this->loadJsonTranslationsFrom(module_path($this->moduleName, 'lang'));
}
}
/**
* Register config.
*/
protected function registerConfig(): void
{
$this->publishes([module_path($this->moduleName, 'config/config.php') => config_path($this->moduleNameLower.'.php')], 'config');
$this->mergeConfigFrom(module_path($this->moduleName, 'config/config.php'), $this->moduleNameLower);
}
/**
* Register views.
*/
public function registerViews(): void
{
$viewPath = resource_path('views/modules/'.$this->moduleNameLower);
$sourcePath = module_path($this->moduleName, 'resources/views');
$this->publishes([$sourcePath => $viewPath], ['views', $this->moduleNameLower.'-module-views']);
$this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->moduleNameLower);
$componentNamespace = str_replace('/', '\\', config('modules.namespace').'\\'.$this->moduleName.'\\'.config('modules.paths.generator.component-class.path'));
Blade::componentNamespace($componentNamespace, $this->moduleNameLower);
}
/**
* Get the services provided by the provider.
*/
public function provides(): array
{
return [];
}
private function getPublishableViewPaths(): array
{
$paths = [];
foreach (config('view.paths') as $path) {
if (is_dir($path.'/modules/'.$this->moduleNameLower)) {
$paths[] = $path.'/modules/'.$this->moduleNameLower;
}
}
return $paths;
}
}
@@ -1,27 +0,0 @@
<?php
namespace Modules\Taxi\App\Services;
use Modules\Taxi\App\Mail\BaseTaxiMail;
use Modules\Taxi\App\Jobs\TaxiMailJob;
/**
* Сервис отправки письма. Отвечает за формирование всех писем приложения Такси
*/
class TaxiMailerService
{
/**
* Единый метод отправки всех писем по такси
*
* @param BaseTaxiMail $taxiMailClass
* @return void
*/
public function sendMail(BaseTaxiMail $taxiMailClass)
{
#Гаврилов
//delay наверное не нужен?
//Вызов джобы для отправки письма
TaxiMailJob::dispatch($taxiMailClass->prepareDataForMail())->onQueue('emails')->delay(5);
}
}
@@ -1,42 +0,0 @@
<?php
namespace Modules\Taxi\App\Services;
use Modules\Taxi\App\Models as TaxiModels;
use Modules\Taxi\App\DTO\TaxiOrderDTO;
/**
* Сервис работы с заказами такси
*/
class TaxiOrderService
{
public function __construct()
{
}
/**
* Метод создания заказа на такси
*
* @param TaxiOrderDTO $orderData объект с данными по заказу
* @return int идентификатор созданного заказа
*/
public function createOrder(TaxiOrderDTO $orderData): string
{
//ГАВРИЛОВ. проверь будет ли возвращаться ошибка без try catch
// try {
$newOrder = TaxiModels\TaxiMain::create(
[
'emp_login' => $orderData->emp_login,
'taxi_date' => $orderData->taxi_date,
'emp_phone' => $orderData->emp_phone,
'taxi_time' => $orderData->taxi_time,
'taxi_address_from' => $orderData->taxi_address_from,
'taxi_address_to' => $orderData->taxi_address_to
]
);
return $newOrder->id;
// } catch (\Exception $e) {
// return $e->getMessage();
// }
}
}
@@ -1,16 +0,0 @@
<?php
namespace Modules\Taxi\Database\Seeders;
use Illuminate\Database\Seeder;
class TaxiDatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// $this->call([]);
}
}
-31
View File
@@ -1,31 +0,0 @@
{
"name": "nwidart/taxi",
"description": "",
"authors": [
{
"name": "Nicolas Widart",
"email": "n.widart@gmail.com"
}
],
"extra": {
"laravel": {
"providers": [],
"aliases": {
}
}
},
"autoload": {
"psr-4": {
"Modules\\Taxi\\": "",
"Modules\\Taxi\\App\\": "app/",
"Modules\\Taxi\\Database\\Factories\\": "database/factories/",
"Modules\\Taxi\\Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Modules\\Taxi\\Tests\\": "tests/"
}
}
}
-11
View File
@@ -1,11 +0,0 @@
<?php
#Гаврилов
//РЕШИЛ, ЧТО ТАКОЙ КОНФИГ - МОЖЕТ БЫТЬ ОБЯЗАТЕЛЬНОЙ ЧАСТЬЮ КАЖДОГО МОДУЛЯ
return [
'name' => 'Taxi',
#Гаврилов
//КАК ЗАСТАВИТЬ ПРИ СОЗДАНИИ МОДУЛЯ ЭТОТ ПАРАМЕТР ПРОПИСЫВАТЬ АВТОМАТИЧЕСКИ?
'name_ru' => 'Реестр заказа такси', //Добавляем вручную в каждый конфиг
];
-14
View File
@@ -1,14 +0,0 @@
{
"name": "Taxi",
"alias": "taxi",
"description": "",
"keywords": [],
"priority": 0,
"providers": [
"Modules\\Taxi\\App\\Providers\\TaxiServiceProvider",
"Modules\\Taxi\\App\\Providers\\TaxiOrderProvider",
"Modules\\Taxi\\App\\Providers\\TaxiMailerProvider",
"Modules\\Taxi\\App\\Providers\\TaxiScheduleProvider"
],
"files": []
}
-15
View File
@@ -1,15 +0,0 @@
{
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"axios": "^1.1.2",
"laravel-vite-plugin": "^0.7.5",
"sass": "^1.69.5",
"postcss": "^8.3.7",
"vite": "^4.0.0"
}
}
-6
View File
@@ -1,6 +0,0 @@
import { createRoot } from 'react-dom/client';
import MenuApp from './components/MenuApp.jsx'; // Создайте этот файл, если используете React
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<MenuApp />);
-44
View File
@@ -1,44 +0,0 @@
table {
& td {
padding: 5px;
}
& tr:nth-child(odd) {
background-color: #eeeeee;
}
}
#taxi__order-create{
padding: 10px;
}
#taxi__home__filter-form{
width: 20%;
}
.taxi-btn{
margin: 25px 0;
}
.elem--title.el-title--big{
padding: 7px;
margin: 50px 0 25px 0;
border-left: 5px solid #7864eb;
font-size: 1.3rem;
background: linear-gradient(90deg, rgb(237 234 255) 0%, rgb(255 255 255) 75%);
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
display: inline-block;
min-width: 400px;
}
.form-field-block{
display: flex;
}
.taxi-orders__order-options{
& button {
margin: 10px 0;
}
}
-3
View File
@@ -1,3 +0,0 @@
.order-create__btn-block{
margin: 20px 0;
}
@@ -1,387 +0,0 @@
import { tr } from "date-fns/locale";
import React, { useEffect, useState, useMemo, useContext } from "react";
import { TaxiOrder_type, TaxiOrder_initial, TaxiOrder_fieldName, TaxiOrder_tableData } from '../types/taxiOrderType';
import { differenceInDays } from 'date-fns';
import { getCsrfToken } from "../../../../../resources/js/services/getCsrfService";
import { Button, TextInput, Select, Option } from '@SharePoint/rencredit_uikit';
import { EntityHistoryProps } from "../../../../../resources/js/components/entityHistory/EntityHistory";
import { HistoryProvider, HistoryContext } from "../../../../../resources/js/contexts/HistoryContext";
import { PopupContext } from "../../../../../resources/js/contexts/PopupContext";
import { PreloaderContext } from "../../../../../resources/js/contexts/PreloaderContext";
import api from "../../../../../resources/js/api";
type EmpSupervisorsData = {
emp_login: string,
emp_supervisor: string
}
export default function TaxiHome ()
{
//Массив полей для отображения в таблицах с заяками. Учитывается, в том числе, порядок
const rqstTableFields = ['id', 'emp_login', 'emp_supervisor', 'emp_phone', 'taxi_date', 'taxi_time', 'taxi_address_from', 'taxi_address_to', 'cancel_rqst'];
//Формируем объект с полями для таблицы
const rqstTableHeaderFields: Record<keyof TaxiOrder_tableData, string> = Object.assign({}, ...rqstTableFields.map( field => {return {[field]: TaxiOrder_fieldName[field]}}));
const rqstTableHeader:string[] = Object.keys(TaxiOrder_fieldName).map(rqstFieldName => TaxiOrder_fieldName[rqstFieldName]);
const [ccEmp, setCcEmp] = useState<string[]>([]);
//Стейт для руководителей сотрудников
const [empSupervisors, setEmpSupervisors] = useState<EmpSupervisorsData[]>([]);
const [allOrders, setAllOrders] = useState<TaxiOrder_type[]>([]);
const [filterOrders, setFilterOrders] = useState<TaxiOrder_type[]>([]);
const [startSearch, setStartSearch] = useState<boolean>(false);
const [rqstNumber, setRqstNumber] = useState<string>('');
const [rqstTime, setRqstTime] = useState<string>('');
const [rqstLogin, setRqstLogin] = useState<string>('');
const [orderTimePeriods, setOrderTimePeriods] = useState<[{time_period: string}]>([{time_period: ''}]);
const popupContext = useContext(PopupContext);
const preloaderContext = useContext(PreloaderContext);
useEffect ( () => {
api.get('/taxi/getTimePeriods',
).then(timePeriods_json => setOrderTimePeriods(timePeriods_json.data));
// fetch('/public/api/taxi/getTimePeriods', {
// method: 'GET',
// credentials: 'include',
// }).then(timePeriods_resp => timePeriods_resp.json().then( timePeriods_json => {
// setOrderTimePeriods(timePeriods_json);
// }));
setTimeout(() => {
popupContext.addPopupArrTest([
{message: 'для информации', timeOut: true, type: 'attention'}
])
popupContext.addPopupArrTest([
{message: 'ошибка', timeOut: true, type: 'error'}
])
}, 1000);
console.log(Object.keys(TaxiOrder_fieldName))
}, [])
//ГАВРИЛОВ
//ЕСЛИ ОБЪЕКТ СО ЗНАЧЕНИЯМИ ДЛЯ ПОИСКА ПУСТОЙ - НЕ ВЫПОЛНЯЕМ ПОИСК, ВОЗВРАЩАЕМ ОПОВЕЩЕНИЕ О ПУСТЫХ УСЛОВИЯХ
const getFilterOrders = () => {
console.log(rqstTime)
console.log(rqstNumber)
console.log(rqstLogin)
setStartSearch(true);
// ГАВРИЛОВ. проверь
api.post('/taxi/getFilterOrders', {
id: rqstNumber,
taxi_time: rqstTime,
emp_login: rqstLogin
}).then(filterOrdersJson => {
setFilterOrders(filterOrdersJson.data);
})
// fetch('/public/api/taxi/getFilterOrders', {
// credentials: 'include',
// method: 'POST',
// body: JSON.stringify({
// id: rqstNumber,
// taxi_time: rqstTime,
// emp_login: rqstLogin
// }),
// headers: {
// 'content-type': 'application/json'
// }
// }).then(filterOrdersRqst => filterOrdersRqst.json())
// .then(filterOrdersJson => {
// setFilterOrders(filterOrdersJson);
// })
}
const timePeriodOptions: Option<string>[] = useMemo(() => {
return orderTimePeriods.map((time: {time_period: string}) => {
return {
caption: time.time_period,
value: time.time_period,
id: time.time_period
}
})
}, [orderTimePeriods])
const empLoginOptions: Option<string>[] = useMemo(() => {
return ccEmp.map((empLogin: string) => {
return {
caption: empLogin,
value: empLogin,
id: empLogin
}
})
}, [ccEmp])
useEffect ( () => {
Promise.all(
[
// fetch('/public/api/taxi/getEmpInfo', {
// credentials: 'include',
// method: 'GET',
// }).then( empInfo_resp => empInfo_resp.json() ),
api.get('taxi/getEmpInfo'),
api.get('taxi/getActiveOrders'),
// fetch('/public/api/taxi/getActiveOrders', {
// credentials: 'include',
// method: 'GET',
// }).then( allOrders_resp => allOrders_resp.json() )
]
).then(
([
{data: empInfo_json},
{data: allOrders_json}
]) => {
setCcEmp(empInfo_json.map( (empInfoEl: {emp_login: String}) => empInfoEl.emp_login ));
setAllOrders(allOrders_json);
setEmpSupervisors(empInfo_json.map( (empInfoEl: {emp_login: String, emp_group: String}) => ({emp_login: empInfoEl.emp_login, emp_supervisor: empInfoEl.emp_group}) ));
}
)
}, [])
const isReady = ccEmp.length > 0 && orderTimePeriods.length > 0;
useEffect ( () => {
console.log(!isReady)
preloaderContext.setPreloaderVisible(!isReady)
// setPreloaderProp(!isReady);
// console.log(empSupervisors)
}, [isReady])
if (!ccEmp.length || !orderTimePeriods.length) {
//setIsReady(true);
return;
} else {
//setIsReady(false);
// console.log(allOrders);
console.log(ccEmp);
console.log(orderTimePeriods);
}
// ГАВРИЛОВ. ОФОРМИ РЕЗУЛЬТАТЫ ПОИСКА. ПОСЛЕ ПОИСКА ПЛАВНО ОПУСКАЙ ДО ТАБЛИЦЫ РЕЗУЛЬТАТЫ ПОИСКА, ТАКЖЕ ВЫВЕДИ ЗОТЯ БЫ ТЕКСТ "РЕЗУЛЬТАТЫ ПОИСКА, СЕЙЧАС ЭТО ПРОСТО ТАБЛИЦА"
//Гаврилов
//Реализовать подтягивание информации
//Реализовать переключение между БД (new and old Magic)
//console.log(sanctumToken())
let newDate = new Date('2024-06-05');
newDate.setHours(0, 0, 0);
// function callHistory(entityId) {
// console.log(entityId)
// return fakeObjArr;
// }
// let resultObj = {
// entityId: 1,
// entityProps: fakeObjArr
// }
return (
<div id='taxi-container'>
<Button
type = 'button'
text = 'Новая заявка'
onClick = {
() => document.location.href = 'createRqst'
}
className = "taxi-btn"
/>
<div className = "elem--title el-title--big">Активные заявки</div>
<div className='container__table-block' id='taxi__rqst--active'>
<HistoryProvider>
<RqstTable
allOrders={allOrders}
rqstTableHeader={rqstTableHeaderFields}
empSupervisors={empSupervisors}
/>
</HistoryProvider>
</div>
<div id="taxi__home__filter-form">
<div className = "elem--title el-title--big">Поиск заявки</div>
<form>
<input
type="hidden"
name="_token"
value={getCsrfToken()}
/>
<div>
<TextInput
labelText = 'Номер заявки'
value = {rqstNumber}
onChange = {(e: string) => setRqstNumber(e)}
/>
{/* <label htmlFor="">Номер заявки</label>
<input type="number" onChange={ e => {
setRqstNumber(e.target.value)
}}></input> */}
</div>
{/* <div>
<label htmlFor="">Дата с</label>
<input type="date" />
</div>
<div>
<label htmlFor="">Дата по</label>
<input type="date" />
</div> */}
<div>
<Select
labelText = "Время"
options = {timePeriodOptions}
size = '1'
onChange = { (e: string) => {
setRqstTime(e)
}}
/>
{/* <label htmlFor="">Время</label>
<select name="taxi_time" id="" onChange={ e => {
setRqstTime(e.target.value)
}}>
<option key='0' value=''></option>
{
orderTimePeriods.map( (timePeriodEl, timePeriodIndex ) =>
<option key={timePeriodIndex++} value={timePeriodEl.time_period}>{timePeriodEl.time_period}</option>
)
}
</select> */}
</div>
<div>
<Select
labelText = "Сотрудник"
options = {empLoginOptions}
size = '1'
onChange = { (e, selectedOption: {value: string}) => {
setRqstLogin(selectedOption.value)
}}
/>
{/* <label htmlFor="">Логин</label>
<select onChange={ e => setRqstLogin(e.target.value) }>
<option key="0" value=""></option>
{
ccEmp.map( (empDataEl: string, empDataIndex: number) =>
<option key={empDataIndex++} value={empDataEl}>{empDataEl}</option>
)
}
</select> */}
</div>
<Button
className = "taxi-btn"
type = 'button'
text = 'Искать'
onClick = { getFilterOrders }
size = 's'
ui = 'secondary'
/>
{/* <button type="button" onClick={getFilterOrders}>Искать</button> */}
</form>
</div>
<div id = "container__filter-rqst-table">
{
startSearch ?
<table>
<thead>
<tr>
{
rqstTableHeader.map( (headEl, headElIndex) =>
<th key={headElIndex} className = "table-header">
{headEl}
</th>)
}
</tr>
</thead>
<tbody>
{
filterOrders.length ?
filterOrders.map( (rqstData: TaxiOrder_type, rqstIndex: number) =>
<tr key={rqstIndex}>
{
Object.keys(TaxiOrder_initial).map(initField =>
<td>
{initField == 'cancel_rqst' ? (rqstData[initField] ? 'Нет' : 'Да') : rqstData[initField]}
</td>)
}
</tr>)
: <tr>
<td>Запросов не найдено</td>
</tr>
}
</tbody>
</table>
: ''
}
</div>
</div>
)
}
function RqstTable(props: {allOrders: TaxiOrder_type[], rqstTableHeader: Record<keyof TaxiOrder_tableData, string>, empSupervisors: EmpSupervisorsData[]}){
const historyContext = useContext(HistoryContext);
if (!historyContext) {
return null;
}
let newDate = new Date('2024-06-05');
newDate.setHours(0, 0, 0);
return (
props.allOrders.length ?
<table className="taxi-orders">
<thead>
<tr>
{
Object.entries(props.rqstTableHeader).map((el, index) =>
<th key={index} className="table-header">{el[1]}</th>
)
}
<th className = "table-header">Управление</th>
</tr>
</thead>
<tbody>
{
props.allOrders.map( (rqstData: TaxiOrder_type, rqstIndex: number) =>
<tr key = {rqstIndex}>
{
Object.entries(props.rqstTableHeader).map( el => {
return (
// Если поле - руководитель сотрудника - вычисляем значение, ориентируясь на список props.empSupervisors
el[0] === 'emp_supervisor' ?
<td key={el[0]}>{ props.empSupervisors.find(empData => empData.emp_login == rqstData.emp_login)?.emp_supervisor }</td>
: <td key={el[0]}>{ rqstData[el[0]] }</td>
)
})
}
<td className="taxi-orders__order-options">
{/* Можно взаимодействовать только с заявками на такси на сегодняшнюю, либо завтрашнюю дату */}
{differenceInDays(new Date(rqstData.taxi_date), new Date()) >= 0 && !rqstData.cancel_rqst ?
<>
<Button
type = 'button'
onClick = { () => { document.location.href = `editOrder/${rqstData.id}` } }
text = {'Редактировать'}
ui = 'secondaryPurple'
/>
<Button
type = 'button'
onClick = { () => { document.location.href = `cancelRqst/${rqstData.id}` } }
text = {'Отменить'}
ui = 'secondaryPurple'
/>
<Button
type = 'button'
onClick = { () => { historyContext.getHistoryFromMagic('taxi', rqstData.id, TaxiOrder_fieldName) } }
text = {'История'}
ui = 'secondaryPurple'
/>
</>
: ""
}
</td>
</tr>
)
}
</tbody>
</table>
: <div>Нет активных заявок</div>
)
}
@@ -1,635 +0,0 @@
import React, { useEffect, useContext, useState, useMemo } from "react";
import { TaxiOrder_type, TaxiOrder_initial } from '../types/taxiOrderType';
import { addDays, format } from 'date-fns';
import { getCsrfToken } from "../../../../../resources/js/services/getCsrfService";
import { Button, TextInput, Select, Option } from '@SharePoint/rencredit_uikit';
import FormValidErr, { FormValidErrObject } from "../../../../../resources/js/components/formValidErr/FormValidErr";
import { PopupContext } from "../../../../../resources/js/contexts/PopupContext";
import { EntityHistoryProps } from "../../../../../resources/js/components/entityHistory/EntityHistory";
import { PreloaderContext } from "../../../../../resources/js/contexts/PreloaderContext";
//Гаврилов типы для пропсов?
// export default function TaxiOrder( {rqstId, setPreloaderProp}: {rqstId: number | undefined, setPreloaderProp: CallableFunction} )
export default function TaxiOrder( {rqstId}: {rqstId: number | undefined} )
{
let newDate = new Date('2024-06-05');
newDate.setHours(0, 0, 0);
let fakeObjArr: EntityHistoryProps[] = [
{
changeAction: 'insert',
changeAuthor: 'login',
changeDate: newDate,
changeDetails: [
{
propName: 'поле',
propValue: 'значение'
},
{
propName: 'поле',
propValue: 'значение'
},
{
propName: 'поле',
propValue: 'значение'
},
{
propName: 'поле2',
propValue: 'значение2'
}
]
},
{
changeAction: 'insert',
changeAuthor: 'login',
changeDate: new Date('2024-06-05'),
changeDetails: [
{
propName: 'поле',
propValue: 'значение'
},
{
propName: 'поле',
propValue: 'значение'
},
{
propName: 'поле',
propValue: 'значение'
},
{
propName: 'поле2',
propValue: 'значение2'
}
]
},
{
changeAction: 'insert',
changeAuthor: 'login',
changeDate: new Date('2024-06-05'),
changeDetails: [
{
propName: 'поле',
propValue: 'значение'
},
{
propName: 'поле',
propValue: 'значение'
},
{
propName: 'поле',
propValue: 'значение'
},
{
propName: 'поле2',
propValue: 'значение2'
}
]
},
{
changeAction: 'update',
changeAuthor: 'login2',
changeDate: new Date('2025-06-05'),
changeDetails: [
{
propName: 'поле3',
propValue: 'значение4'
},
{
propName: 'поле3',
propValue: 'значение4'
},
{
propName: 'поле3',
propValue: 'значение4'
},
{
propName: 'поле3',
propValue: 'значение4'
},
{
propName: 'поле5',
propValue: 'значение6'
}
]
},
{
changeAction: 'update',
changeAuthor: 'login2',
changeDate: new Date('2025-06-05'),
changeDetails: [
{
propName: 'поле3',
propValue: 'значение4'
},
{
propName: 'поле3',
propValue: 'значение4'
},
{
propName: 'поле3',
propValue: 'значение4'
},
{
propName: 'поле3',
propValue: 'значение4'
},
{
propName: 'поле5',
propValue: 'значение6'
}
]
}
];
const preloaderContext = useContext(PreloaderContext);
//ГАВРИЛОВ ПОСТОЯННЫЙ ПЕРЕРЕНДЕРИНГ СПРОСИТЬ У САШИ. ЭЛЕМЕНТ НЕ МИГАЕТ В КОНСОЛИ, НО ПИШЕТ ПОСТОЯННО ПЕРЕРЕНДЕР
console.log('🔁 TaxiForm перерендерился!');
interface UserData {
emp_id: number,
emp_address: string,
emp_lastname: string,
emp_name: string,
emp_surname: string,
emp_login: string | null
emp_phone: string,
//Все остальные поля объекта могут быть пустыми
[key: string]: unknown,
full_name: string,
};
const UserData_initial: UserData = {
emp_id: 0,
emp_address: '',
emp_lastname: '',
emp_name: '',
emp_surname: '',
emp_login: '',
emp_phone: '',
full_name: '',
// _token: getCsrfToken()
};
//Проверка загрузки необходимых для начала работ данных. После успешного получения всех данных происходит рендеринг
const [checkLoad, setCheckLoad] = useState(
{
ccEmp: false,
orderData: false,
checkEditOrderUserData: false,
getTimePeriods: false,
checkOfficeAddress: false,
}
);
const todayDate = new Date();
//Данные по всем пользователям
const [ccEmp, setCcEmp] = useState<UserData[]>( [UserData_initial] );
//Данные по редактируемому запросу (если вызвано редактирование)
const [orderData, setOrderData] = useState<TaxiOrder_type>(TaxiOrder_initial);
//Данные по пользователю, чей запрос редактируется (если вызвано редактирование запроса)
const [editOrderUserData, setEditOrderUserData] = useState<UserData>(UserData_initial);
//Данные по временным промежуткам
const [orderTimePeriods, setOrderTimePeriods] = useState< {time_period: string, is_morning_time: number}[] >(
[ {time_period: '', is_morning_time: 0} ]
);
//Массив доступных для заказа такси дат для их дальнейшего преобразования в теги option селекта
const [dateOrderArr, setDateOrderArr] = useState<string[]>(
[
format(todayDate, "yyyy-MM-dd"),
format(addDays(todayDate, 1), "yyyy-MM-dd")
]
);
const popupContext = useContext(PopupContext);
useEffect ( () => {
setFormCreateVisible(true);
setFormCreateValidObj([{fieldName: 'testField', fieldErrors: ['errOne']}]);
console.log(popupContext)
// console.log()
// setTimeout(() => {
// popupContext.addPopupArrTest([
// {message: 'для информации', timeOut: true, type: 'attention'}
// ])
// }, 1000);
// setTimeout(() => {
// setPopupArrProp([
// {message: 'для информации', timeOut: true, type: 'attention'},
// ])
// }, 1000);
// setTimeout(() => {
// setPopupArrProp([
// {message: 'для информации', timeOut: false, type: 'info'},
// ])
// }, 2000);
// setTimeout(() => {
// setPopupArrProp([
// {message: 'для информации', timeOut: false, type: 'info'},
// ])
// }, 3000);
// setPopupArrProp([
// {message: 'для информации', timeOut: false, type: 'info'},
// {message: 'успешно', timeOut: true, type: 'success'},
// {message: 'ошибка', timeOut: true, type: 'error'},
// {message: 'обратить внимание', timeOut: true, type: 'attention'}
// ])
}, [])
const [formCreateVisible, setFormCreateVisible] = useState<boolean>(false);
const [formCreateValidErrObj, setFormCreateValidObj] = useState<FormValidErrObject[]>([{fieldName: null, fieldErrors: []}])
//ГАВРИЛОВ ДОБАВИТЬ ВСПЛЫВАЮЩЕЕ ОКНО ЕСЛИ ПРОИСХОДИТ НЕСООТВЕТСТВИЕ ВРЕМЕНИ И ДАТЫ ЗАКАЗА ТАКСИ? А НЕ ПРОСТО УДАЛЯТЬ ЗНАЧЕНИЕ ИЗ СОСЕДНЕГО ПОЛЯ?
//Адреса офисов
const [officeAddressInfo, setOfficeAddressInfo] = useState< {place: string, address: string}[] >(
[ {place: '', address: ''} ]
);
//Адрес офиса, где работает сотрудник. Определяется на этапе выбора логина
const [empOfficeAddress, setEmpOfficeAddress] = useState<string | null>(null);
//const [orderTime, setOrderTime] = useState<string|null>(null);
//const [orderDate, setOrderDate] = useState<string|null>(null);
// const [orderAddressFrom, setOrderAddressFrom] = useState<string|null>(null);
// const [orderAddressTo, setOrderAddressTo] = useState<string|null>(null);
function gotoHome()
{
document.location.href = '/public/taxi/home';
}
//Гаврилов. Не передаешь CSRF токен
//Отправка заказа на такси
function sendTaxiOrder ()
{
console.log(orderData)
console.log(editOrderUserData)
//return
// setPreloaderProp(true, 'создаем заявку')
preloaderContext.setPreloaderVisible(true);
preloaderContext.setPreloaderText('создаем заявку');
let popupType;
fetch('/public/api/taxi/' + (rqstId ? `editOrder/${rqstId}` : 'createRqst'), {
credentials: 'include',
method: (rqstId ? "PATCH" : "POST"),
body: JSON.stringify(orderData),
headers: {
'content-type': 'application/json',
'Accept': 'application/json'
}
}).then(result => {
preloaderContext.setPreloaderVisible(false);
// setPreloaderProp(false)
result.json().then(jsonResult => {
console.log(jsonResult)
if (!result.ok) {
//ГАВРИЛОВ здесь обработка ошибок валидации формы
if (result.status == 422) {
// setPopupArrProp(
// [
// //ГАВРИЛОВ обработка ошибки
// {message: 'Произошла ошибка! Заявка не создана', type: 'error'},
// ]
// )
} else {
// setPopupArrProp(
// [
// //ГАВРИЛОВ обработка ошибки
// {message: jsonResult.result_msg, type: 'error'},
// ]
// )
}
popupType = 'error'
} else {
popupType = 'success'
}
popupContext.addPopupArrTest(
[
{message: jsonResult.message, type: popupType},
]
)
})
})
}
//Функция, собирающая нужный массив для формирования из него списка option для select
function transformToOptionsFunc <T,>(
dataArr: T[],
dataKey?: keyof T
): Option<string>[] {
let optionArr = dataArr.map(item => {
//Без проверки ниже, Typescript ругается на условный атрибут dataKey, который может отсутствовать, но при этом укаан как относящийся к Дженерику
const value = dataKey !== undefined
? item[dataKey]
: item;
return {
caption: String(value),
value: String(value),
id: String(value)
};
});
return optionArr;
}
/**
* Метод проверки соответствует ли дата и время заказа логике. Наприме, если дата заказа сегодняшняя - нельзя выбрать утренний промежуток времени
* @param orderTime время заказа такси
* @param orderDate дата заказа такси
* @returns результат проверки соответствия даты и времени заказа
*/
function checkTimeAndDate (orderTime: string|null, orderDate: string|null): boolean
{
// console.log(orderTime)
// console.log(!orderTime)
// console.log(orderDate)
// console.log(!orderDate)
if (!orderTime || !orderDate) {
//console.log(1)
return true;
}
let isMorningTimeCheck = orderTimePeriods.filter( e => e.time_period == orderTime )[0].is_morning_time;
// console.log(isMorningTimeCheck)
//Если выбранная дата заказа больше текущей - заказ на завтра, можно выбирать любую дату
if (!(new Date(orderDate) > todayDate)) {
//console.log(2)
return !isMorningTimeCheck;
}
return true;
}
// const ccEmpList: Option<string>[] = useMemo(() => {
// return ccEmp.map(empData => {
// return {
// caption: empData.emp_login,
// value: empData.emp_login,
// id: empData.emp_login
// }
// })
// }, [ccEmp]);
// console.log(ccEmpList)
const ccEmpList = useMemo( () => {
return transformToOptionsFunc(ccEmp, 'emp_login');
}, [ccEmp]);
const orderDateList = useMemo( () => {
return transformToOptionsFunc(dateOrderArr);
}, [dateOrderArr]);
const orderTimeList = useMemo( () => {
return transformToOptionsFunc(orderTimePeriods, 'time_period')
}, [orderTimePeriods]);
//Загрузка данных для старта работ
useEffect ( () => {
//Получаем информацию по сотруднику при загрузке страницы
fetch('/public/api/taxi/getEmpInfo', {
credentials: 'include',
method: 'GET',
// headers: new Headers({
// 'Authorization': `Bearer ${sanctumToken()}`
// })
}).then(empInfo_resp => empInfo_resp.json().then(empInfo_json => {
console.log(empInfo_json)
console.log(editOrderUserData)
setCcEmp(empInfo_json);
setCheckLoad(loadObj => ( {...loadObj, ccEmp: true} ));
}));
//Получаем доступные временные промежутки для заказа такси
fetch('/public/api/taxi/getTimePeriods', {
method: 'GET',
credentials: 'include',
// headers: new Headers({
// 'Authorization': `Bearer ${sanctumToken()}`
// })
}).then(timePeriods_resp => timePeriods_resp.json().then(timePeriods_json => {
setOrderTimePeriods(timePeriods_json);
setCheckLoad(loadObj => ( {...loadObj, getTimePeriods: true} ));
}));
//ГАВРИЛОВ. ВЕЗДЕ В FETCH ЗАПРОСАХ ПЕРЕХВАТЫВАЙ ОШИБКИ И ВЫВОДИ СООБЩЕНИЯ ОБ ОШИБКЕ. НАПРИМЕМР С 401. В ЭТОМ СЛУЧАЕ НУЖНО ПЕРЕБРАСЫВАТЬ ПОЛЬЗОВАТЕЛЯ НА СТРАНИЦУ АУТЕНТИФИКАЦИИ
//Получае адреса всех офисов, куда могут заказываться такси?
fetch('/public/api/taxi/getOfficeAddress', {
method: 'GET',
credentials: 'include',
}).then(officeAddress_resp => {officeAddress_resp.json().then(officeAddress_json => {
setOfficeAddressInfo(officeAddress_json);
setCheckLoad(loadObj => ( {...loadObj, checkOfficeAddress: true} ));
})});
//Если в пропсах передается rqstId, происходит редактирование заявки, а не создание новой - получаем данные по редактируемой заявке на такси
if (rqstId) {
fetch(`/public/api/taxi/getOrderById/${rqstId}`, {
method: 'GET',
credentials: 'include',
// headers: new Headers({
// 'Authorization': `Bearer ${sanctumToken()}`
// })
}).then(orderData_resp => orderData_resp.json().then( orderData_json => {
console.log(orderData_json)
setOrderData(orderData_json);
setCheckLoad(loadObj => ({...loadObj, orderData: true}));
}));
}
}, [])
//гаврилов. Нужно ввести объект с данными по запросу и туда класть только нужные данные , а не все параметры пользователя. и тогда не нужно нигде проверять rqstId. Внизу в методе класть в этот объект все нужные параметры
useEffect ( () => {
//Гаврилов. Причем тут проверка логина? Зачем она?
if (rqstId && orderData.emp_login) {
//Если передан id запроса на редактирование, устанавливаем значение с данными пользователя редактируемого запроса
fetch(`/public/api/taxi/getEmpInfo/${orderData.emp_login}`).then(userData_resp => userData_resp.json().then(userData_json => {
//гаврилов. Вот здесь нужно класть данные в orderData, а также заполнять адрес из и адрес куда
setEditOrderUserData(userData_json[0]);
//setOrderData(prevData => ({...prevData, userData_json[0]}));
//setEmpAddress(editOrderUserData['emp_address']);
setCheckLoad(loadObj => ( {...loadObj, checkEditOrderUserData: true} ));
//ГАВРИЛОВ. вынеси в отедльную функцию определение адреса офиса?
setEmpOfficeAddress(officeAddressInfo.filter(addressInfo => addressInfo.place == userData_json[0].emp_area)[0].address);
}));
}
}, [orderData.emp_login])
//Итоговая проверка все ли необходимые данные для рендеринга получены
const isReady = rqstId ?
checkLoad.ccEmp && checkLoad.checkEditOrderUserData && checkLoad.orderData && checkLoad.checkOfficeAddress && checkLoad.getTimePeriods
: checkLoad.ccEmp;
//Регулировка видимости прелоадера в зависимости от того все ли данные для рендеринга получены или нет
useEffect ( () => {
//isReady ? setPreloaderProp(false) : setPreloaderProp(true);
isReady ? preloaderContext.setPreloaderVisible(false) : preloaderContext.setPreloaderVisible(true);
}, [isReady])
if (!isReady) {
return;
}
return (
<div id = "taxi__order-create">
{/* <EntityHistory
entityId={1}
entityProps= {fakeObjArr}
/> */}
<div className = "order-create__btn-block">
<Button
type = 'button'
onClick = {gotoHome}
text = 'Домой'
ui = 'secondary'
/>
</div>
<h2>{rqstId ? 'Редактирование' : 'Создание'} заявки на такси</h2>
<div className="form-container form-container--medium-size form-containter--left-pos">
{/* <FormValidErr
visible = {formCreateVisible}
validErrorsObj = {formCreateValidErrObj}
/> */}
<form>
{/* ГАВРИЛОВ как защищаешься от csrf если комментируешь поле ниже? */}
{/* <input
type="hidden"
name="_token"
value={getCsrfToken()}
/> */}
<div className="form__field-block">
<Select
//ГАВРИЛОВ. ПЕРЕПИШИ НА ОПРЕДЕЛЕНИЕ НАЗВАНИЯ ПОЛЯ, СОГЛАСНО TaxiOrder_fieldName ИЗ TAXIORDERTYPE
labelText = 'Дата заказа'
options = {orderDateList}
//Гаврилов. Может где-то сверху нужно класть в объект с данными формы данные из orderData (если она есть) .тогда не нужно проверять props.rqstId ?
value = {orderData.taxi_date ? orderDateList.find(dateData => dateData.value === orderData.taxi_date) : null}
size = 'm'
onChange = {
(_, sel:{caption:string, id:string, value:string} ) => {
let selOrderDate: string|null = (sel ? sel.value : null);
//setEditOrderUserData(prevData => ({...prevData, taxi_date: selOrderDate}));
setOrderData(prevData => ( {...prevData, taxi_date: selOrderDate} ));
//Если проверка соответствия времени и даты заказа не выполняется, сбрасываем время.
// if (!checkTimeAndDate(orderData.taxi_time, selOrderDate)) {
popupContext.addPopupArrTest( [{message: 'Несоответствие времени и даты', type: 'error'}] )
// setOrderData(prevData => ( {...prevData, taxi_time: null} ));
// }
}}
/>
</div>
<div className="form__field-block">
<Select
labelText = "Время заказа"
options = {orderTimeList}
value = {orderData.taxi_time ? orderTimeList.find(dateData => dateData.value === orderData.taxi_time) : null}
size = 'm'
onChange = {(_, sel:{caption:string, id:string, value:string}) => {
let selOrderTime = (sel ? sel.value : null);
setOrderData(prevData => ( {...prevData, taxi_time: selOrderTime} ));
//Проверяем относится ли выбранный промежуток времени заказа к утру или нет. Если выбран утренний промежуток, дата откуда - адрес сотрудника, адрес куда - офис, если выбран вечерний промежуток - наоборот
if (orderTimePeriods.filter( e => selOrderTime == e.time_period )[0].is_morning_time == 1) {
console.log(1)
setOrderData(prevData => ( {...prevData, taxi_address_from: editOrderUserData.emp_address, taxi_address_to: empOfficeAddress} ));
} else {
console.log(2)
console.log(empOfficeAddress)
setOrderData(prevData => ( {...prevData, taxi_address_from: empOfficeAddress, taxi_address_to: editOrderUserData.emp_address} ));
}
//Если проверка соответствия и времени заказа не выполняется, сбрасываем дату
if (!checkTimeAndDate(selOrderTime, orderData.taxi_date)) {
popupContext.addPopupArrTest(
[{message: 'Несоответствие времени и даты', type: 'error'}]
)
setOrderData(prevData => ({...prevData, taxi_date: null}));
}
}}
/>
</div>
<div className="form__field-block">
<Select
labelText = 'Логин сотрудника'
options = {ccEmpList}
value = {orderData.emp_login}
size = 'm'
//В компоненте Select значение "выбранного" option помещается во второй аргумент onChange
onChange = { (_, sel:{caption:string, id:string, value:string}) => {
if (sel) {
let empData = ccEmp.filter(empData => {return empData.emp_login == sel.value})[0];
//При создании нового запроса, на этом действии время и дата могут быть уже записаны в объект editOrderUserData, но при выборе нового логина, если передать в сеттер setEditOrderUserData только объект empData, он перезапишет значения времени и даты, поэтому конкатенируем объект, отдавая приоритет при замене дублирующих значений empData (правый объект перезапишет левый)
setEditOrderUserData( {...editOrderUserData, ...empData} );
setOrderData(prevData => ({...prevData, emp_login: sel.value, emp_phone: empData.emp_phone}));
//Гаврилов. Если площадка РКЦ - выводить ошибку? Так как у этой площадки нет адреса офиса
//ГАВРИЛОВ. ВЫНЕСИ ОПРЕДЕЛЕНИЕ АДРЕСА В ОТДЕЛЬНУЮ ФУНКЦИЮ?
let officeAddress: string | null = empData.emp_area == 'РКЦ' ? null : officeAddressInfo.filter(addressInfo => addressInfo.place == empData.emp_area)[0].address;
setEmpOfficeAddress(officeAddress);
setOrderData(prevData => {
let setAddressTo: string|null, setAddressFrom: string|null;
//Если время выставлено в форме
if (orderData.taxi_time) {
//Проверяем относится ли выбранный промежуток времени заказа к утру или нет. Если выбран утренний промежуток, дата откуда - адрес сотрудника, адрес куда - офис, если выбран вечерний промежуток - наоборот
if (orderTimePeriods.filter( e => orderData.taxi_time == e.time_period )[0].is_morning_time == 1) {
setAddressTo = officeAddress;
setAddressFrom = empData.emp_address;
} else {
setAddressTo = empData.emp_address;
setAddressFrom = officeAddress;
}
//Если время заказа не выбрано, сбрасываем дату откуда и дату куда
} else {
setAddressTo = null;
setAddressFrom = null;
}
return {...prevData, taxi_address_from: setAddressFrom, taxi_address_to: setAddressTo};
});
} else {
setOrderData(prevData => ({...prevData, emp_login: null, emp_phone: null}));
setEditOrderUserData(UserData_initial);
setEmpOfficeAddress(null);
setOrderData(prevData => ({...prevData, taxi_address_from: null, taxi_address_to: null}));
}
} }
//При редактировании существующего запроса нельзя менять выбранный логин сотрудника для заказа
disabled = {rqstId ? true : false}
/>
</div>
<div className="form__field-block">
<TextInput
labelText = 'ФИО'
value = {editOrderUserData.full_name}
disabled = {true}
/>
</div>
<div className="form__field-block">
<TextInput
labelText = 'Мобильный номер телефона'
value = {orderData.emp_phone}
onChange = {(e: string) => {
setOrderData(prevData => ({...prevData, emp_phone: e}));
}}
/>
</div>
<div className="form__field-block">
<TextInput
labelText = 'Адрес (откуда)'
value = {
//Если в пропсы передается id запроса, проставляем значение из параметров имеющегося запроса. В противном случае определяем является ли выбранный временной промежуток утренним. Если да - прописываем, что едем из адреса сотрудника, в противном случае - из офиса
//Гаврилов. Будет ли при редактировании уже созданного запроса, работать смена адресов при изменении времени запроса? Или всегда будет искаться props.rqstId и логика будет исходить из этого?
orderData.taxi_address_from
}
onChange = { (e: string) => {
setOrderData(prevData => ({...prevData, taxi_address_from: e}));
}}
/>
</div>
<div className = "form__field-block">
<TextInput
labelText = 'Адрес (куда)'
value = {orderData.taxi_address_to}
onChange = {(e: string) => {
setOrderData(prevData => ({...prevData, taxi_address_to: e}));
}}
/>
</div>
<div className = "order-create__btn-block">
<Button
type = 'button'
onClick = {sendTaxiOrder}
text = 'Отправить'
/>
</div>
</form>
</div>
</div>
)
}
@@ -1,13 +0,0 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { AppProvider } from '../../../../../resources/js/providers/AppProvider.tsx';
import TaxiHome from '../components/TaxiHome.tsx';
const container:HTMLElement = document.getElementById('root')!;
const root = createRoot(container);
root.render(
<AppProvider>
<TaxiHome/>
</AppProvider>
);
@@ -1,37 +0,0 @@
import { createRoot } from 'react-dom/client';
import React from 'react';
import '@SharePoint/rencredit_uikit/dist/static/fonts/mont/Mont.css';
import { AppProvider } from '../../../../../resources/js/providers/AppProvider.tsx';
import TaxiOrder from '../components/TaxiOrder.tsx';
const container:HTMLElement = document.getElementById('root')!;
const root = createRoot(container);
//ГАВРИЛОВ
//не забудь обернуть рендер editOrder так же в PopupProvider. И вообще это дубирование выглядит костыльно
//Гаврилов
//Спроси у дипсика насколько корректно таким образом получать аргументы из URLA
if (document.location.pathname.split('/').find(el => el === 'editOrder')) {
let rqstIdUrl = parseInt(document.location.pathname.split('/').pop());
root.render(
<AppProvider>
<TaxiOrder
rqstId = {rqstIdUrl}
/>
</AppProvider>
);
} else {
root.render(
<AppProvider>
{/* Не получится передавать контейнер попапов на этом этапе, так как компоненты приложения (формы Такси) должны иметь общего родителя с компонентом контейнеров попапов, иначе они не смогут знать через общий стейт какое состояние у набора попапов */}
{/* ГАВРИЛОВ ругается на указание параметра rqstId в TaxiPage? */}
{/* <TaxiOrderPage rqstId = {undefined}/> */}
<TaxiOrder
rqstId = {undefined}
/>
</AppProvider>
);
}
@@ -1,50 +0,0 @@
/**
* Объект заказа такси
*/
export interface TaxiOrder_type{
id: number | null,
emp_login: string | null,
emp_phone: string | null,
taxi_date: string | null,
taxi_time: string | null,
taxi_address_from: string | null,
taxi_address_to: string | null,
cancel_rqst: boolean
};
/**
* Тип для отображения данных по заявке на такси в таблице (есть дополнительные поля чисто для отображения)
*/
export type TaxiOrder_tableData = TaxiOrder_type & {
emp_supervisor: string | null;
}
/**
* Начальное состояние типа заказа такси
*/
export const TaxiOrder_initial: TaxiOrder_type = {
id: null,
emp_login: null,
emp_phone: null,
taxi_date: null,
taxi_time: null,
taxi_address_from: null,
taxi_address_to: null,
cancel_rqst: false
};
/**
* Справочник названий полей запроса такси
* гаврилов. пересекается со свойством TaxiMain с названием полей. ПОПРАВИТЬ, РЕАЛИЗОВАВ ОБМЕН ТИПАМИ ЧЕРЕЗ СТОРОННИЙ ПАКЕТ
*/
export const TaxiOrder_fieldName = {
id: 'Номер запроса',
emp_login: 'Логин сотрудника',
emp_supervisor: 'Руководитель',
emp_phone: 'Телефон сотрудника',
taxi_date: 'Дата заказа',
taxi_time: 'Время заказа',
taxi_address_from: 'Адрес (откуда)',
taxi_address_to: 'Адрес (куда)',
cancel_rqst: 'Запрос актуален'
};
@@ -1,7 +0,0 @@
@extends('taxi::layouts.master')
@section('content')
<h1>Hello World</h1>
<p>Module: {!! config('taxi.name') !!}</p>
@endsection
@@ -1,31 +0,0 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<!-- ГАВРИЛОВ. НЕ УВЕРЕН, ЧТО ЭТО НУЖНО -->
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Taxi Module - {{ config('app.name', 'Laravel') }}</title>
<meta name="description" content="{{ $description ?? '' }}">
<meta name="keywords" content="{{ $keywords ?? '' }}">
<meta name="author" content="{{ $author ?? '' }}">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
{{-- Vite CSS --}}
{{-- {{ module_vite('build-taxi', 'resources/assets/sass/app.scss') }} --}}
</head>
<body>
@yield('content')
{{-- Vite JS --}}
{{-- {{ module_vite('build-taxi', 'resources/assets/js/app.js') }} --}}
</body>
@@ -1,13 +0,0 @@
@extends('layouts.app_main')
@section('app_styles')
@vite(['Modules/Taxi/resources/css/taxiHome.css'])
@endsection
@section('app_content')
<div id="root"></div>
@endsection
@section('app_scripts')
@vite(['Modules/Taxi/resources/js/page/taxiHomePage.tsx'])
@endsection
@@ -1,13 +0,0 @@
@extends('layouts.app_main')
@section('app_styles')
@vite(['Modules/Taxi/resources/css/taxiOrder.css'])
@endsection
@section('app_content')
<div id="root"></div>
@endsection
@section('app_scripts')
@vite(['Modules/Taxi/resources/js/page/taxiOrderPage.tsx'])
@endsection
-50
View File
@@ -1,50 +0,0 @@
<?php
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Modules\Taxi\App\Http\Controllers as TaxiControllers;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
// Route::middleware(['auth:sanctum'])->prefix('v1')->name('api.')->group(function () {
// Route::get('taxi', fn (Request $request) => $request->user())->name('taxi');
// });
Route::group([
'prefix' => 'taxi',
'middleware' => 'checkAppAccess'
],
function() {
Route::group(
['middleware' => \Modules\Taxi\App\Http\Middleware\CheckTimeRqstAvailability::class],
function() {
//Route::post('/createOrder', [TaxiControllers\TaxiController::class, 'createTaxiOrder']);
//TODO. Какая будет ошибка, если регулярка не сможет проверить переданные параметр? Можно ли ввести какую-то проверку на уровне роутинга с возвратом конкретной ошибки?
// Route::post('/editOrder/{orderId}', [TaxiControllers\TaxiController::class, 'updateTaxiOrder'])->where('orderId', '^[0-9]+$');
Route::get('/cancelRqst/{rqstId}', [TaxiControllers\TaxiController::class, 'cancelRqst'])->where('rqstId', '^[0-9]+$');
Route::post('/createRqst', [TaxiControllers\TaxiController::class, 'createTaxiOrder']);
Route::patch('/editOrder/{orderId}', [TaxiControllers\TaxiController::class, 'updateTaxiOrder'])->where('orderId', '^[0-9]+$');
});
Route::get('/getActiveOrders', [TaxiControllers\TaxiController::class, 'getActiveOrders']);
Route::post('/getFilterOrders', [TaxiControllers\TaxiController::class, 'getFilterOrders']);
Route::get('/getEmpInfo/{userLogin}', [TaxiControllers\TaxiController::class, 'getUserInfoByLogin'])->where('userLogin', '^[a-zA-Z_]+[0-9]*$');
Route::get('/getOfficeAddress/', [TaxiControllers\TaxiController::class, 'getOfficeAddress']);
#Гаврилов
//ВЫНЕСТИ ЭТОТ ЕНДПОИНТ ИЗ ГРУППЫ ТАКСИ И ВЫНЕСТИ МЕТОД ПОЛУЧЕНИЯ ВСЕХ ПОЛЬЗОВАТЕЛЕЙ ИЗ СТАРОГО МЭДЖИКА КАКИМ-ТО ОБЩИМ КОНТРОЛЛЕРОМ, СЕРВИСОМ?
Route::get('/getEmpInfo/', [TaxiControllers\TaxiController::class, 'getActiveUsersInfo']);
//Получаем временные промежутки для заказа такси
Route::get('/getTimePeriods/', [TaxiControllers\TaxiController::class, 'getTimePeriods']);
//Получаем данные по существующему заказу такси
Route::get('/getOrderById/{orderId}', [TaxiControllers\TaxiController::class, 'getOrderById'])->where('orderId', '^[0-9]+$');
}
);
-44
View File
@@ -1,44 +0,0 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\Taxi\App\Http\Controllers as TaxiControllers;
use Modules\Taxi\App\Http\Controllers\TaxiController;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::group([
'prefix' => 'taxi',
'middleware' => 'checkAppAccess'
],
function() {
Route::get('/home', function() {
return view('taxi::taxi_home');
})->name('taxi_home');
#Гаврилов
//УБЕРИ ЕНДПОИНТ
Route::get('/checkredis', [TaxiControllers\TaxiController::class, 'testRedisMethod']);
#Гаврилов
//ПОСРЕДНИК АУТЕНТИФИКАЦИИ ЗДЕСЬ БУДЕТ СРАБАТЫВАТЬ?
//внедри внутри роута проверку существует ли запрос и можно ли его редачить (заказ на сегодня + время отправки еще не подошло, либо заказ на завтра)
Route::group(['middleware' => \Modules\Taxi\App\Http\Middleware\CheckTimeRqstAvailability::class], function() {
Route::get('/editOrder/{orderId}', function() {
return view('taxi::taxi_order');
})->where('orderId', '^[0-9]+$');
Route::post('/createRqst', [TaxiControllers\TaxiController::class, 'createTaxiOrder']);
Route::post('/editOrder/{orderId}', [TaxiControllers\TaxiController::class, 'updateTaxiOrder'])->where('orderId', '^[0-9]+$');
Route::get('/createRqst', function() {
return view('taxi::taxi_order');
})->middleware('checkPermission:admin'); //ГАВРИЛОВ. НЕ ЗАБУДЬ УБРАТЬ ДОСТУП К ЭТОМУ РОУТУ ДЛЯ АДМИНОВ, ОН ДОСТУПЕН ДЛЯ ВСЕХ
});
}
);
-26
View File
@@ -1,26 +0,0 @@
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
build: {
outDir: '../../public/build-taxi',
emptyOutDir: true,
manifest: true,
},
plugins: [
laravel({
publicDirectory: '../../public',
buildDirectory: 'build-taxi',
input: [
__dirname + '/resources/assets/sass/app.scss',
__dirname + '/resources/assets/js/app.js'
],
refresh: true,
}),
],
});
//export const paths = [
// 'Modules/$STUDLY_NAME$/resources/assets/sass/app.scss',
// 'Modules/$STUDLY_NAME$/resources/assets/js/app.js',
//];
-40
View File
@@ -1,40 +0,0 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* Define the application's command schedule.
*/
protected function schedule(Schedule $schedule): void
{
// $schedule->command('inspire')->hourly();
//activity()->log('Look mum, I logged something');
#Гаврилов
//ПРОВЕРИТЬ, РАБОТАЕТ?
// $schedule->command('sanctum:prune-expired --hours=24')
// ->daily();
//->dailyAt('21:03');
// activity()
// ->byAnonymous()
// ->inLog('Shedule')
// ->event('clear_old_Sanctum_tokens');
//попытка поработать с командами по расписанию. Не успел протестить. В частности, в этом скрипте планируется в определенное время инициировать отправку всех заявок на такси
$schedule->command('taxi:send-today-orders-mail')->dailyAt('08:00');
}
/**
* Register the commands for the application.
*/
protected function commands(): void
{
$this->load(__DIR__ . '/Commands');
require base_path('routes/console.php');
}
}
+133
View File
@@ -0,0 +1,133 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models;
class MenuController extends Controller
{
public function __construct()
{
$this->userLogin = 'dgavrilov';
}
/**
* Получение всех записей из таблицы с приложениями для меню
* TODO
* передавай флаг isActive и используй его для получения записей с приложениями меню архивных и не архивных
*
* @return mixed
*/
public function getApps()
{
//Приложения старого мэджик из скрипта Config_AD
$oldApps = $this->getOldApps();
#Гаврилов
//НОВЫЕ ПРИЛОЖЕНИЯ НЕ ИСПОЛЬЗУЕШЬ
//Приложения нового мэджик из базы данных
#Гаврилов
//отрисовывать только то, что может видеть пользователь
$newApps = Models\MagicApps::get()->toArray();
return response()->json($oldApps);
}
/**
* Метод получает конфиг со старыми приложениями Magic
*
* @return string
*/
public function getOldApps()
{
$oldAppConf = json_decode(file_get_contents('http://cs/magic/return_config.AD.php'));
return $oldAppConf;
}
/**
* Метод должен проверить есть ли избранные приложения у пользователя. Если есть, их необходимо обновить либо добавив, либо убрав то приложение, по которому кликнул пользователь. Если избранных приложений нет - вставить новую запись
*
* @return boolean
*/
public function updateUserFavApp(Request $rqst)
{
#Гаврилов
//ВНИЗУ ИСПОЛЬЗУЕТСЯ ЛОГИН DGAVRILOV, ПЕРЕПИШИ НА ЛОГИН ПОЛЬЗОВАТЕЛЯ MAGIC
$userFavApp = $this->getUserFavApp('dgavrilov');
$appName = $rqst->all()['appName'];
$newFavAppList = null;
//Если массив с избранными приложениями пустой - пользователь не добавил ни 1 приложения мэджик в избранное
if (!$userFavApp) {
$newFavAppList = $appName;
} else {
$currentFavApp = $userFavApp;
if (in_array($appName, $currentFavApp) !== false) {
$newFavAppList = implode(';', array_filter($currentFavApp, function($el) use($appName) {return $el !== $appName;}));
} else {
$newFavAppList = implode(';', array_merge($currentFavApp, [$appName]));
}
}
if ($newFavAppList) {
#Гаврилов
//ПРИ ВЫЗОВЕ ИЗ REACT НЕ ОБНОВЛЯЕТСЯ ПОЛЕ UPDATE_AT
$this->insertFavApp($newFavAppList);
//Если список приложений пуст - просто удаляем запись с избранными приложениями пользователя
} else {
$this->delAllUserFavApp('dgavrilov');
}
return $this->getUserFavApp('dgavrilov');
}
#Гаврилов
//try catch для отслеживания ошибок вставки записей в БД?
/**
* Метод вставки новой записи с избранными приложениями
*
* @return boolean
*/
public function insertFavApp($appName)
{
Models\UserFavApp::updateOrCreate(
['user_login' => $this->userLogin],
['fav_apps' => $appName]
);
return true;
}
/**
* Метод удаляет запись с избранными приложениями пользователя
*
* @param string $userLogin логин пользователя
* @return boolean
*/
public function delAllUserFavApp($userLogin)
{
Models\UserFavApp::where('user_login', $userLogin)->delete();
return true;
}
/**
* Метод получает избранные приложения пользователя
*
* @param string $userLogin логин пользователя
* @return array
*/
public function getUserFavApp(string $userLogin): array
{
#Гаврилов
//ЗДЕСЬ ВМЕСТО DGAVRILOV ДОЛЖНО БЫТЬ ОБРАЩЕНИЕ К КУКАСАМ, ЧТОБЫ БРАТЬ ЛОГИН ОТТУДА
$userFavApp = Models\UserFavApp::where('user_login', $userLogin)->first();
return $userFavApp ? explode(';', $userFavApp['fav_apps']) : [];
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Models\MagicApps;
class AppRoles extends Model
{
protected $table = 'app_roles';
use HasFactory;
public function magicApp()
{
return $this->belongsTo(MagicApps::class, 'app_id', 'id');
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Models\AppRoles;
class MagicApps extends Model
{
protected $table = 'magic_apps';
use HasFactory;
public function appRoles()
{
return $this->hasMany(AppRoles::class, 'app_id', 'id')
->orderBy('role_priority', 'asc');
}
}
-13
View File
@@ -1,13 +0,0 @@
<?php
namespace App\Models\OldMagicModels;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CcEmp extends Model
{
use HasFactory;
protected $table = 'cc_emp';
protected $connection = 'magic_old';
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class UserFavApp extends Model
{
use HasFactory;
public $incrementing = true;
public $timestamps = true;
protected $table = 'user_fav_app';
protected $connection = "mysql";
protected $fillable = ['user_login', 'fav_apps'];
}
@@ -0,0 +1,13 @@
-- custom.app_roles definition
CREATE TABLE `app_roles` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`app_id` bigint(20) unsigned NOT NULL COMMENT 'id приложения (связь с таблицей magic_apps)',
`app_role` text COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Роль в приложении',
`app_title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Название роли на русском',
`role_priority` smallint(6) NOT NULL COMMENT 'Приоритет роли',
`role_access` text COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Кому доступна роль (зависит от драйвера в magic_apps)',
PRIMARY KEY (`id`),
KEY `app_roles_app_id_foreign` (`app_id`),
CONSTRAINT `app_roles_app_id_foreign` FOREIGN KEY (`app_id`) REFERENCES `magic_apps` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Ролевая модель приложений';
@@ -0,0 +1,3 @@
INSERT INTO custom.app_roles (app_id,app_role,app_title,role_priority,role_access) VALUES
(4,'admin','Администратор',1,'# Magic_admins'),
(4,'user','Пользователь',2,'# Test');
@@ -0,0 +1,13 @@
-- custom.magic_apps definition
CREATE TABLE `magic_apps` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`app_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Имя приложения',
`app_title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Название приложения на русском',
`app_link` varchar(150) COLLATE utf8mb4_unicode_ci NOT NULL,
`role_driver` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Драйвер определения ролей: по группам AD, по почтовым рассылкам, по логинам. NULL, если ролевая модель не предусмотрена',
`access_groups_email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Доступ к приложениям по почтовым рассылкам',
`access_groups_ad` text COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Доступ к приложениям по группам AD',
`is_active` tinyint(4) NOT NULL DEFAULT 1 COMMENT 'Активно ли приложение',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -0,0 +1,5 @@
INSERT INTO custom.magic_apps (app_name,app_title,app_link,role_driver,access_groups_email,access_groups_ad,is_active) VALUES
('Test','','test.index',NULL,NULL,'CTXAL.CryptoPRO',1),
('About','','about',NULL,NULL,'CTXAL.CryptoPRO',1),
('Test create','','test.create',NULL,NULL,'CTXAL.CryptoPRO',1),
('taxi','Реестр заказа такси','test','email','# Magic_admins',NULL,1);
@@ -0,0 +1,10 @@
-- custom.user_fav_app definition
CREATE TABLE `user_fav_app` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`user_login` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`fav_apps` text COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Избранные приложения пользователя из меню Magic';
@@ -0,0 +1,2 @@
INSERT INTO custom.user_fav_app (created_at,updated_at,user_login,fav_apps) VALUES
('2025-03-28 18:16:10','2025-11-14 10:35:04','dgavrilov','reestr_send_app;dadata;amlpor;accext;prlog;anketa;pilotIVI;limcut;nsr;minjust;mcocardreporting;taxi');
+187 -9
View File
@@ -1,11 +1,189 @@
@import 'tailwindcss'; /* ГАВРИЛОВ. ВЫЯСНИТЬ, ГДЕ ИСПОЛЬЗУЮТСЯ */
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; html {
@source '../../storage/framework/views/*.php'; overflow-y: scroll;
@source '../**/*.blade.php'; }
@source '../**/*.js';
#root{
@theme { --color_ruby: #ff0078;
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', }
'Segoe UI Symbol', 'Noto Color Emoji';
.container {
background-color: lightblue;
padding: 20px;
}
#menu-container{
display: flex;
&>#menu__left-block{
flex-basis: 15%;
&>.menu__left-block__call-app{
display: flex;
&>.fa{
flex-basis: 20%;
}
}
}
}
.switcher-container{
position: fixed;
top: 0;
right: 0;
display: flex;
justify-content: flex-end;
align-items: center;
&>.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
margin: 9px;
&>.switcher__favorite-app {
opacity: 0;
width: 0;
height: 0;
&.showFav + .slider{
background-color: var(--color_ruby);
&:before{
transform: translateX(26px);
color: var(--color_ruby);
padding: 2px 0 0 0;
align-content: baseline;
}
}
}
&>.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s cubic-bezier(0,1,0.5,1);
border-radius: 4px;
@import url('https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css');
&:before {
position: absolute;
content: "\f006";
font-family: FontAwesome;
text-align: center;
font-size: 1.2rem;
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s cubic-bezier(0,1,0.5,1);
border-radius: 3px;
}
&.round {
border-radius: 34px;
&:before {
border-radius: 50%;
}
}
}
}
}
.switcher__favorite-app.showFav + .slider {
background-color: var(--color_ruby);
}
.switcher__favorite-app.showFav + .slider:before {
transform: translateX(26px);
}
/* Rounded sliders */
/* .slider.round {
border-radius: 34px;
&:before {
border-radius: 50%;
}
} */
#round {
border-radius: 34px;
&:before {
border-radius: 50%;
}
}
#menu__app-container__app-block{
/* display: grid;
grid-template-columns: 50% 50%; */
column-count: 2;
>.apps-block__proc{
margin-bottom: 20px;
>.proc__title{
font-size: 1.1rem;
padding: 10px 0;
font-weight: 600;
}
>.proc__title.proc-hide{
display: none;
}
}
.script-list__el{
display: flex;
>.script-list__el__script-name{
margin-bottom: 5px;
transition: 0.2s;
&:hover{
transform: translateY(-1px);
}
>a{
text-decoration: none;
color: black;
&:hover{
color: var(--color_ruby);
}
}
}
>.fa-star{
margin-left: 5px;
cursor: pointer;
opacity: 0.2;
transition: 0.3s;
}
>.fa-star:hover{
opacity: 1;
}
>.fa-star.favorite{
opacity: 1;
color: var(--color_ruby);
}
}
.script-list__el.script-hide{
display: none;
}
} }
+14
View File
@@ -0,0 +1,14 @@
import { createRoot } from 'react-dom/client';
import '@SharePoint/rencredit_uikit/dist/static/fonts/mont/Mont.css';
import '@fortawesome/fontawesome-free/css/all.css';
import { UIKitThemeProvider } from '@SharePoint/rencredit_uikit';
import MenuApp from './components/MenuApp.tsx'; // Создайте этот файл, если используете React
import React from 'react';
const container:HTMLElement = document.getElementById('root')!;
const root = createRoot(container);
root.render(
<UIKitThemeProvider>
<MenuApp />
</UIKitThemeProvider>
);
+231
View File
@@ -0,0 +1,231 @@
import { Button } from '@SharePoint/rencredit_uikit';
import React, { useEffect, useState } from "react";
function MenuApp ()
{
//Приложения и процессы Magic для отображения в меню
const [menuApps, setMenuApps] = useState<{
'proccesses': object,
'scripts': object
}>({
'proccesses': {},
'scripts': {}
});
//TODO
//ВНЕДРИ В КОНТРОЛЛЕРЕ ПРОВЕРКУ ДОСТУПОВ, А НЕ ВОЗВРАЩАЙ ВСЕ ПОДРЯД ПРИЛОЖЕНИЯ
//Избранные приложения пользователя
const [favApps, setFavApps] = useState<string[]>([]);
//Массив избраннных процессов, в приложениях которых есть хотя бы одно избранное
const [favProcs, setFavProcs] = useState<string[]>([]);
//Состояния видимости скрипта
const [hideScriptClass, setHideScriptClass] = useState('');
//const [sanctumToken, setSanctumToken] = useState('');
const sanctumToken = () => {
const metaTag = document.getElementById('sanctum_token_block') as HTMLMetaElement;
return metaTag.dataset.token;
}
//Получаем при рендере страницы все приложения Magic и все избранные приложения пользователя
useEffect( () => {
Promise.all(
[
fetch('api/magic_apps').then(menuAppsRes => menuAppsRes.json()),
//TODO
//СЮДА ПОТЯГИВАЙ ЛОГИН ПОЛЬЗОВАТЕЛЯ, А НЕ DGAVRILOV
fetch('api/user_fav_app/dgavrilov').then(favAppsRes => favAppsRes.json())
]
).then(
([
responseMenuApp,
responseFavApp
]) => {
setMenuApps(responseMenuApp);
setFavApps(responseFavApp);
}
)
}, []);
//Обновление видимости процесса (собрания приложений) в зависимости от того находится ли в избранном хотя бы одно приложение
useEffect( () => {
if (menuApps === null) { return }
//Массив с процессами, в которых есть хотя бы одно избранное приложение
const favProcsArr:string[] = [];
//Мы собираем все избранные приложения в массив-состояние, чтобы при изменении состояния каждого приложения (например при переключении switcher) проверять входит ли это приложение в избранные. Если входит - его родительский процесс не прячем, так как в его дочерних приложениях есть избранные
Object.entries(menuApps.proccesses).forEach( (procData) => {
//Флаг - есть ли в приложениях процесса хотя бы 1 избранное
const hasFavorite: boolean = procData[1].tabs.split(';').some( (tab: string) => {
return favApps.includes(tab);
})
if (hideScriptClass !== 'script-hide' || hasFavorite) {
favProcsArr.push(procData[0]);
}
})
setFavProcs(favProcsArr);
}, [hideScriptClass, menuApps, favApps]);
//console.log(sanctumToken())
//ГАВРИЛОВ
//ПОЧЕМУ КОД НИЖЕ ИСПОЛНЯЕТСЯ ПОСТОЯННО ПОКА НЕ БУДЕТ ПОЛУЧЕН КОНТЕНТ ДЛЯ СТРАНИЦЫ ИЗ ПРОМИСОВ ВЫШЕ, А НЕ ОТРАБАТЫВАЕТ ТОЛЬКО ПРИ РЕНДЕРИНГЕ СТРАНИЦЫ
//Уместно ли отслеживать состояние до получения запросов fetch в подобном виде?
if (menuApps === null || favApps === null) {
return <span>прелоадер</span>
} else {
return (
<div id = 'menu-container'>
<div id = 'menu__left-block'>
<div className = 'menu__left-block__call-app'>
<i className = 'fa fa-th-large'></i>
<div className = 'mleft-block__call-app__title'>Приложения</div>
</div>
</div>
<div id = 'menu__app-container'>
<Button text='Текст' />
<div id = 'menu__app-container__app-block'>
{/* {console.log(menuApps.scripts)} */}
{Object.entries(menuApps.proccesses).map( (proc_val, proc_index) => (
<div className = "apps-block__proc" key = { proc_index } data-proc = {proc_val[0]}>
{/* В зависимости от того входит процесс в массив избранных делаем его видимым или скрываем */}
<div className = {`proc__title ${favProcs.includes(proc_val[0]) ? 'proc-visible' : 'proc-hide'}`}>{proc_val[1].title}</div>
<div className = "proc__script-list">
{proc_val[1].tabs.split(';').map( (app_el, app_index) => (
<AppElem
appIndex = { app_index }
appName = { app_el }
appTitle = { menuApps.scripts[app_el]?.title || "Неизвестный скрипт - " + app_el }
appUrl = { menuApps.scripts[app_el]?.url }
favIconClassName = { favApps.includes(app_el) ? 'favorite' : 'not_favorite' }
hideAppClass = { hideScriptClass }
//Функция обновления актуальный список избранных приложений
setUpdateFavApps = { (newFavApp) => setFavApps(newFavApp) }
/>
))}
</div>
</div>
)) }
</div>
<Switcher
toggleAppsVisible = { setHideScriptClass }
switcherId = "switcher-menu"
/>
</div>
</div>
)
}
}
//Гаврилов
//ПЕРЕПИСАТЬ АРГУМЕНТЫ ПОД ОБЪЕКТ СО СВОЙСТВАМИ, ИСПОЛЬЗОВАТЬ ...obj
/**
*
* @param {int} appIndex ключ для экземлпяра компонента
* @param {string} appName уникальное имя скрипта
* @param {string} appTitle название скрипта в меню
* @param {string} favIconClassName класс для иконки избранного
* @param {string} hideAppClass класс для видимости приложения
* @param {string} setUpdateFavApps функция для обновления состояния избранны приложений (при снятия, установки признака избранности)
* @returns
*/
function AppElem({
appIndex,
appName,
appTitle,
appUrl,
favIconClassName,
hideAppClass,
setUpdateFavApps
} : {
appIndex: string,
appName: string,
appTitle: string,
appUrl: string,
favIconClassName: string,
hideAppClass: string,
setUpdateFavApps: string
}) {
const [appElemClass, changeFav] = useState(favIconClassName);
const callChangeFav = ( changeAppName: string ) => {
fetch('api/user_fav_app', {
method: 'post',
body: JSON.stringify({ appName: changeAppName }),
headers: {
'content-type': 'application/json'
}
}
)
.then( (response) => response.json() )
.then( (response) => {
//Если скрипт был избранным - удаляем из избранного и наоборот
appElemClass == 'favorite' ? changeFav('not_favorite') : changeFav('favorite');
//Возвращаем обновленный список избранных приложений пользователя
setUpdateFavApps(response?.fav_apps?.split( ';' ) || [])
} )
}
return (
//Если приложение в избранном - оно всегда должно быть "видимо", даже если переключатель в положении Скрыть неизбранные приложения
<div key = { appIndex } className = {`script-list__el ${appElemClass == 'favorite' ? '' : hideAppClass}`} data-scriptname = { appName }>
<div className = "script-list__el__script-name"><a target="_blank" href={ appUrl }>{ appTitle }</a></div>
{/* Анонимная функция нужна для создания функции смены состояния для каждого компонента в отдельности, в противном случае вызов функции в одном экземпляре компонента вызывает функцию изменения для каждого экземпляра компонента */}
<i className = {`${(appElemClass == 'favorite' ? 'fas' : 'far')} fa-star ${appElemClass}`} onClick = { () => callChangeFav(appName) }></i>
</div>
)
}
//TODO
//ВЫНЕСИ В ПАПКУ COMPONENTS/MAIN
/**
* Компонент переключателя В ДАННОМ СЛУЧАЕ видимости скриптов (видны только избранные или все)
* @param {string} switcherId идентификатор переключателя (для стилей css), так как в будущем, на странице можно будет размещать несколько переключателей
* @param {function} toggleAppsVisible функция обновления состояния hideScriptClass
* @returns
*/
function Switcher({ switcherId, toggleAppsVisible }) {
const [favSwitcherClass, setFavSwitcherClass] = useState('showAll');
//Переключение состояния переключателя после рендеринга страницы
useEffect( () => {
//Получаем из локал сторадж состояние переключателя
const savedState = localStorage.getItem('magicMenuFavSwitcher');
if (savedState) {
setFavSwitcherClass(savedState);
}
}, []);
//При изменении состояния Переключателя изменяется состояние всех приложени (переключается их видимость в зависимости от состояния переключателя - только избранные или все)
useEffect( () => {
//Синхронизируем зависимые компоненты
toggleAppsVisible(favSwitcherClass === 'showFav' ? 'script-hide' : '');
}, [favSwitcherClass] );
const callToggleFavSwitcher = () => {
let switcherState = (favSwitcherClass === 'showFav' ? 'showAll' : 'showFav');
//favSwitcherClass => favSwitcherClass = ... - функциональное обновление. Здесь оно не особо нужно, но чтобы не забыть
//Меняем класс switcher, а также меняем состояние видимости всех приложений (в зависимости от того избранные они или нет)
setFavSwitcherClass( favSwitcherClass => {
if (favSwitcherClass === 'showFav') {
toggleAppsVisible('script-hide')
} else {
toggleAppsVisible('')
}
localStorage.setItem('magicMenuFavSwitcher', switcherState)
return switcherState;
} )
}
return (
<div id = { switcherId } className = "switcher-container">
<label className = "switch">
<input type = "checkbox" className = {`switcher__favorite-app ${favSwitcherClass}`} onChange = { callToggleFavSwitcher }/>
<span className = "slider round"></span>
</label>
</div>
)
}
export default MenuApp;
+17
View File
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Laravel + React</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Без команды ниже корректно не обрабатывается React скрипт -->
@viteReactRefresh
@vite(['resources/css/app.css', 'resources/js/app.jsx'])
<!-- Отрисовываем meta-тег с sanctum-токеном, чтобы на фронте его могли получить и передавать при api -->
<meta id="sanctum_token_block" data-token="{{ session('sanctum-token') ?? '' }}"/>
</head>
<body>
<div id="root"></div>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
<?php
use Illuminate\Support\Facades\Route;
use \App\Http\Controllers;
#Гаврилов
//НАВЕРНОЕ, НУЖНО ВСЕ ТАКИ СГРУППИРОВАТЬ API РОУТЫ НИЖЕ В ГРУППУ /api/MENU/endpoint
/**
* Получение всех приложений Magic
* todo
* возвращать только приложения, доступные согласно роли
*/
Route::get('magic_apps', [Controllers\MenuController::class, 'getApps']);
/**
* Получение избранных приложений
* {userLogin} - логин пользователя, чьи приложения необходимо вернуть
*/
Route::get('user_fav_app/{userLogin}', [Controllers\MenuController::class, 'getUserFavApp']);
/**
* Обновление избранных приложений
*/
Route::post('user_fav_app', [Controllers\MenuController::class, 'updateUserFavApp']);
+3
View File
@@ -5,3 +5,6 @@ use Illuminate\Support\Facades\Route;
Route::get('/', function () { Route::get('/', function () {
return view('welcome'); return view('welcome');
}); });
Route::get('/menu', function () {
return view('menu_start');
})->name('magic_menu');